AI2 Component  (Version nb184)
WebRTCNativeMgr.java
Go to the documentation of this file.
1 // -*- mode: java; c-basic-offset: 2; -*-
2 // Copyright 2018 MIT, All rights reserved
3 // Released under the Apache License, Version 2.0
4 // http://www.apache.org/licenses/LICENSE-2.0
5 
6 package com.google.appinventor.components.runtime.util;
7 
8 import android.content.Context;
9 
10 import android.util.Log;
11 
15 
16 import java.io.BufferedReader;
17 import java.io.IOException;
18 import java.io.InputStreamReader;
19 import java.io.UnsupportedEncodingException;
20 
21 import java.nio.ByteBuffer;
22 import java.nio.charset.CharacterCodingException;
23 import java.nio.charset.Charset;
24 import java.nio.charset.CharsetDecoder;
25 
26 import java.util.ArrayList;
27 import java.util.List;
28 import java.util.Random;
29 import java.util.Timer;
30 import java.util.TimerTask;
31 import java.util.TreeSet;
32 
33 import org.apache.http.HttpResponse;
34 import org.apache.http.client.HttpClient;
35 import org.apache.http.client.methods.HttpGet;
36 import org.apache.http.client.methods.HttpPost;
37 
38 import org.apache.http.entity.StringEntity;
39 import org.apache.http.entity.StringEntity;
40 
41 import org.apache.http.impl.client.DefaultHttpClient;
42 
43 import org.json.JSONArray;
44 import org.json.JSONException;
45 import org.json.JSONObject;
46 
47 import org.webrtc.DataChannel.Buffer;
48 import org.webrtc.DataChannel;
49 import org.webrtc.IceCandidate;
50 import org.webrtc.MediaConstraints;
51 import org.webrtc.MediaStream;
52 import org.webrtc.PeerConnection.ContinualGatheringPolicy;
53 import org.webrtc.PeerConnection.IceConnectionState;
54 import org.webrtc.PeerConnection.IceGatheringState;
55 import org.webrtc.PeerConnection.Observer;
56 import org.webrtc.PeerConnection.RTCConfiguration;
57 import org.webrtc.PeerConnection.SignalingState;
58 import org.webrtc.PeerConnection;
59 import org.webrtc.PeerConnectionFactory;
60 import org.webrtc.RtpReceiver;
61 import org.webrtc.SdpObserver;
62 import org.webrtc.SessionDescription;
63 
64 
65 public class WebRTCNativeMgr {
66 
67  private static final boolean DEBUG = true;
68 
69  private static final String LOG_TAG = "AppInvWebRTC";
70  private static final CharsetDecoder utf8Decoder = Charset.forName("UTF-8").newDecoder();
71 
72  private ReplForm form;
73 
74  private PeerConnection peerConnection;
75  /* We need to keep track of whether or not we have processed an element */
76  /* Received from the rendezvous server. */
77  private TreeSet<String> seenNonces = new TreeSet();
78  private boolean haveOffer = false;
79  private String rCode;
80  private volatile boolean keepPolling = true;
81  private volatile boolean haveLocalDescription = false;
82  private boolean first = true; // This is used for logging in the Rendezvous server
83  private Random random = new Random();
84  private DataChannel dataChannel = null;
85  private String rendezvousServer = null; // Primary (first level) Rendezvous server
86  private String rendezvousServer2 = null; // Second level (webrtc rendezvous) Rendezvous server
87  private List<PeerConnection.IceServer> iceServers = new ArrayList();
88 
89  Timer timer = new Timer();
90 
91  /* Callback that handles sdp offer/answers */
92  SdpObserver sdpObserver = new SdpObserver() {
93 
94  public void onCreateFailure(String str) {
95  if (DEBUG) {
96  Log.d(LOG_TAG, "onCreateFailure: " + str);
97  }
98  }
99 
100  public void onCreateSuccess(SessionDescription sessionDescription) {
101  try {
102  if (DEBUG) {
103  Log.d(LOG_TAG, "sdp.type = " + sessionDescription.type.canonicalForm());
104  Log.d(LOG_TAG, "sdp.description = " + sessionDescription.description);
105  }
106  DataChannel.Init init = new DataChannel.Init();
107  if (sessionDescription.type == SessionDescription.Type.OFFER) {
108  if (DEBUG) {
109  Log.d(LOG_TAG, "Got offer, about to set remote description (again?)");
110  }
111  peerConnection.setRemoteDescription(sdpObserver, sessionDescription);
112  } else if (sessionDescription.type == SessionDescription.Type.ANSWER) {
113  if (DEBUG) {
114  Log.d(LOG_TAG, "onCreateSuccess: type = ANSWER");
115  }
116  peerConnection.setLocalDescription(sdpObserver, sessionDescription);
117  haveLocalDescription = true;
118  /* Send to peer */
119  JSONObject offer = new JSONObject();
120  offer.put("type", "answer");
121  offer.put("sdp", sessionDescription.description);
122  JSONObject response = new JSONObject();
123  response.put("offer", offer);
124  sendRendezvous(response);
125  }
126  } catch (Exception e) {
127  Log.e(LOG_TAG, "Exception during onCreateSuccess", e);
128  }
129  }
130 
131  public void onSetFailure(String str) {
132  }
133 
134  public void onSetSuccess() {
135  }
136  };
137 
138  /* callback that handles iceCandidate negotiation */
139  Observer observer = new Observer() {
140  public void onAddStream(MediaStream mediaStream) {
141  }
142 
143  public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreamArr) {
144  }
145 
146  public void onDataChannel(DataChannel dataChannel) {
147  if (DEBUG) {
148  Log.d(LOG_TAG, "Have Data Channel!");
149  Log.d(LOG_TAG, "v5");
150  }
151  WebRTCNativeMgr.this.dataChannel = dataChannel;
152  dataChannel.registerObserver(dataObserver);
153  keepPolling = false; // Turn off talking to the rendezvous server
154  timer.cancel();
155  if (DEBUG) {
156  Log.d(LOG_TAG, "Poller() Canceled");
157  }
158  seenNonces.clear();
159  }
160 
161  public void onIceCandidate(IceCandidate iceCandidate) {
162  try {
163  if (DEBUG) {
164  Log.d(LOG_TAG, "IceCandidate = " + iceCandidate.toString());
165  if (iceCandidate.sdp == null) {
166  Log.d(LOG_TAG, "IceCandidate is null");
167  } else {
168  Log.d(LOG_TAG, "IceCandidateSDP = " + iceCandidate.sdp);
169  }
170  }
171  /* Send to Peer */
172  JSONObject response = new JSONObject();
173  response.put("nonce", random.nextInt(100000));
174  JSONObject jsonCandidate = new JSONObject();
175  jsonCandidate.put("candidate", iceCandidate.sdp);
176  jsonCandidate.put("sdpMLineIndex", iceCandidate.sdpMLineIndex);
177  jsonCandidate.put("sdpMid", iceCandidate.sdpMid);
178  response.put("candidate", jsonCandidate);
179  sendRendezvous(response);
180  } catch (Exception e) {
181  Log.e(LOG_TAG, "Exception during onIceCandidate", e);
182  }
183  }
184 
185  public void onIceCandidatesRemoved(IceCandidate[] iceCandidateArr) {
186  }
187 
188  public void onIceConnectionChange(IceConnectionState iceConnectionState) {
189  }
190 
191  public void onIceConnectionReceivingChange(boolean z) {
192  }
193 
194  public void onIceGatheringChange(IceGatheringState iceGatheringState) {
195  if (DEBUG) {
196  Log.d(LOG_TAG, "onIceGatheringChange: iceGatheringState = " + iceGatheringState);
197  }
198  }
199 
200  public void onRemoveStream(MediaStream mediaStream) {
201  }
202 
203  public void onRenegotiationNeeded() {
204  }
205 
206  public void onSignalingChange(SignalingState signalingState) {
207  if (DEBUG) {
208  Log.d(LOG_TAG, "onSignalingChange: signalingState = " + signalingState);
209  }
210  }
211  };
212 
213  /* Callback to process incoming data from the browser */
214  DataChannel.Observer dataObserver = new DataChannel.Observer() {
215  public void onBufferedAmountChange(long j) {
216  }
217 
218  public void onMessage(Buffer buffer) {
219  String input;
220  try {
221  input = utf8Decoder.decode(buffer.data).toString();
222  } catch (CharacterCodingException e) {
223  Log.e(LOG_TAG, "onMessage decoder error", e);
224  return;
225  }
226  if (DEBUG) {
227  Log.d(LOG_TAG, "onMessage: received: " + input);
228  }
229  form.evalScheme(input);
230  }
231 
232  public void onStateChange() {
233  }
234  };
235 
236  public WebRTCNativeMgr(String rendezvousServer, String rendezvousResult) {
237  this.rendezvousServer = rendezvousServer;
238  if (rendezvousResult.isEmpty() || rendezvousResult.startsWith("OK")) {
239  /* Provide a default when the rendezvous server doesn't provide one */
240  rendezvousResult = "{\"rendezvous2\" : \"" + YaVersion.RENDEZVOUS_SERVER + "\"," +
241  "\"iceservers\" : " +
242  "[{ \"server\" : \"stun:stun.l.google.com:19302\" }," +
243  "{ \"server\" : \"turn:turn.appinventor.mit.edu:3478\"," +
244  "\"username\" : \"oh\"," +
245  "\"password\" : \"boy\"}]}";
246  }
247  try {
248  JSONObject resultJson = new JSONObject(rendezvousResult);
249  this.rendezvousServer2 = resultJson.getString("rendezvous2");
250  JSONArray iceServerArray = resultJson.getJSONArray("iceservers");
251  this.iceServers = new ArrayList(iceServerArray.length());
252  for (int i = 0; i < iceServerArray.length(); i++) {
253  JSONObject jsonServer = iceServerArray.getJSONObject(i);
254  PeerConnection.IceServer.Builder builder = PeerConnection.IceServer.builder(jsonServer.getString("server"));
255  if (DEBUG) {
256  Log.d(LOG_TAG, "Adding iceServer = " + jsonServer.getString("server"));
257  }
258  if (jsonServer.has("username")) {
259  builder.setUsername(jsonServer.getString("username"));
260  }
261  if (jsonServer.has("password")) {
262  builder.setPassword(jsonServer.getString("password"));
263  }
264  this.iceServers.add(builder.createIceServer());
265  }
266  } catch (JSONException e) {
267  Log.e(LOG_TAG, "parsing iceServers:", e);
268  }
269  }
270 
271  public void initiate(ReplForm form, Context context, String code) {
272 
273  this.form = form;
274  rCode = code;
275  /* Initialize WebRTC globally */
276  PeerConnectionFactory.initializeAndroidGlobals(context, false);
277  /* Setup factory options */
278  PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
279  /* Create the factory */
280  PeerConnectionFactory factory = new PeerConnectionFactory(options);
281  /* Create the peer connection using the iceServers we received in the constructor */
282  RTCConfiguration rtcConfig = new RTCConfiguration(iceServers);
283  rtcConfig.continualGatheringPolicy = ContinualGatheringPolicy.GATHER_CONTINUALLY;
284  peerConnection = factory.createPeerConnection(rtcConfig, new MediaConstraints(),
285  observer);
286  timer.schedule(new TimerTask() {
287  @Override
288  public void run() {
289  Poller();
290  }
291  }, 0, 1000); // Start the Poller now and then every second
292  }
293 
294  /*
295  * startPolling: poll the Rendezvous server looking for the information via the
296  * the provided code with "-s" appended (because we are the receiver, replmgr.js
297  * is in the sender roll.
298  */
299  private void Poller() {
300  try {
301  if (!keepPolling) {
302  return;
303  }
304 
305  if (DEBUG) {
306  Log.d(LOG_TAG, "Poller() Called");
307  Log.d(LOG_TAG, "Poller: rendezvousServer2 = " + rendezvousServer2);
308  }
309  HttpClient client = new DefaultHttpClient();
310  HttpGet request = new HttpGet("http://" + rendezvousServer2 + "/rendezvous2/" + rCode + "-s");
311  HttpResponse response = client.execute(request);
312  StringBuilder sb = new StringBuilder();
313 
314  BufferedReader rd = null;
315  try {
316  rd = new BufferedReader
317  (new InputStreamReader(
318  response.getEntity().getContent()));
319  String line = "";
320  while ((line = rd.readLine()) != null) {
321  sb.append(line);
322  }
323  } finally {
324  if (rd != null) {
325  rd.close();
326  }
327  }
328 
329  if (!keepPolling) {
330  if (DEBUG) {
331  Log.d(LOG_TAG, "keepPolling is false, we're done!");
332  }
333  return;
334  }
335 
336  String responseText = sb.toString();
337 
338  if (DEBUG) {
339  Log.d(LOG_TAG, "response = " + responseText);
340  }
341 
342  if (responseText.equals("")) {
343  if (DEBUG) {
344  Log.d(LOG_TAG, "Received an empty response");
345  }
346  // Empty Response
347  return;
348  }
349 
350  JSONArray jsonArray = new JSONArray(responseText);
351  if (DEBUG) {
352  Log.d(LOG_TAG, "jsonArray.length() = " + jsonArray.length());
353  }
354  int i = 0;
355  while (i < jsonArray.length()) {
356  if (DEBUG) {
357  Log.d(LOG_TAG, "i = " + i);
358  Log.d(LOG_TAG, "element = " + jsonArray.optString(i));
359  }
360  JSONObject element = (JSONObject) jsonArray.get(i);
361  if (!haveOffer) {
362  if (!element.has("offer")) {
363  i++;
364  continue;
365  }
366  JSONObject offer = (JSONObject) element.get("offer");
367  String sdp = offer.optString("sdp");
368  String type = offer.optString("type");
369  haveOffer = true;
370  if (DEBUG) {
371  Log.d(LOG_TAG, "sdb = " + sdp);
372  Log.d(LOG_TAG, "type = " + type);
373  Log.d(LOG_TAG, "About to set remote offer");
374  }
375  if (DEBUG) {
376  Log.d(LOG_TAG, "Got offer, about to set remote description (maincode)");
377  }
378  peerConnection.setRemoteDescription(sdpObserver,
379  new SessionDescription(SessionDescription.Type.OFFER, sdp));
380  peerConnection.createAnswer(sdpObserver, new MediaConstraints());
381  if (DEBUG) {
382  Log.d(LOG_TAG, "createAnswer returned");
383  }
384  i = -1;
385  } else if (element.has("nonce")) {
386  if (!haveLocalDescription) {
387  if (DEBUG) {
388  Log.d(LOG_TAG, "Incoming candidate before local description set, punting");
389  }
390  return;
391  }
392  if (element.has("offer")) { // Only take in the offer once!
393  i++;
394  if (DEBUG) {
395  Log.d(LOG_TAG, "skipping offer, already processed");
396  }
397  continue;
398  }
399  if (element.isNull("candidate")) {
400  i++;
401  // do nothing on a received null
402  continue;
403  }
404  String nonce = element.optString("nonce");
405  JSONObject candidate = (JSONObject) element.get("candidate");
406  String sdpcandidate = candidate.optString("candidate");
407  String sdpMid = candidate.optString("sdpMid");
408  int sdpMLineIndex = candidate.optInt("sdpMLineIndex");
409  if (!seenNonces.contains(nonce)) {
410  seenNonces.add(nonce);
411  if (DEBUG) {
412  Log.d(LOG_TAG, "new nonce, about to add candidate!");
413  Log.d(LOG_TAG, "candidate = " + sdpcandidate);
414  }
415  IceCandidate iceCandidate = new IceCandidate(sdpMid, sdpMLineIndex, sdpcandidate);
416  peerConnection.addIceCandidate(iceCandidate);
417  if (DEBUG) {
418  Log.d(LOG_TAG, "addIceCandidate returned");
419  }
420  }
421  }
422  i++;
423  }
424  if (DEBUG) {
425  Log.d(LOG_TAG, "exited loop");
426  }
427  } catch (IOException e) {
428  Log.e(LOG_TAG, "Caught IOException: " + e.toString(), e);
429  } catch (JSONException e) {
430  Log.e(LOG_TAG, "Caught JSONException: " + e.toString(), e);
431  } catch (Exception e) {
432  Log.e(LOG_TAG, "Caught Exception: " + e.toString(), e);
433  }
434  }
435 
436  private void sendRendezvous(final JSONObject data) {
437  AsynchUtil.runAsynchronously(new Runnable() {
438  @Override
439  public void run() {
440  try {
441  data.put("first", first);
442  data.put("webrtc", true);
443  data.put("key", rCode + "-r");
444  if (first) {
445  first = false;
446  data.put("apiversion", SdkLevel.getLevel());
447  }
448  HttpClient client = new DefaultHttpClient();
449  HttpPost post = new HttpPost("http://" + rendezvousServer2 + "/rendezvous2/");
450  try {
451  if (DEBUG) {
452  Log.d(LOG_TAG, "About to send = " + data.toString());
453  }
454  post.setEntity(new StringEntity(data.toString()));
455  client.execute(post);
456  } catch (IOException e) {
457  Log.e(LOG_TAG, "sendRedezvous IOException", e);
458  }
459  } catch (Exception e) {
460  Log.e(LOG_TAG, "Exception in sendRendezvous", e);
461  }
462  }
463  });
464  }
465 
466  public void send(String output) {
467  try {
468  if (dataChannel == null) {
469  Log.w(LOG_TAG, "No Data Channel in Send");
470  return;
471  }
472  ByteBuffer bbuffer = ByteBuffer.wrap(output.getBytes("UTF-8"));
473  Buffer buffer = new Buffer(bbuffer, false); // false = not binary
474  dataChannel.send(buffer);
475  } catch (UnsupportedEncodingException e) {
476  Log.e(LOG_TAG, "While encoding data to send to companion", e);
477  }
478  }
479 
480 }
com.google.appinventor.components.runtime.ReplForm
Definition: ReplForm.java:62
com.google.appinventor.components.runtime.util
-*- mode: java; c-basic-offset: 2; -*-
Definition: AccountChooser.java:7
com.google.appinventor.components.common.YaVersion
Definition: YaVersion.java:14
com.google.appinventor.components.runtime.util.WebRTCNativeMgr.send
void send(String output)
Definition: WebRTCNativeMgr.java:466
com.google.appinventor.components
com.google.appinventor.components.runtime.util.WebRTCNativeMgr.WebRTCNativeMgr
WebRTCNativeMgr(String rendezvousServer, String rendezvousResult)
Definition: WebRTCNativeMgr.java:236
com.google.appinventor.components.runtime.ReplForm.evalScheme
void evalScheme(String sexp)
Definition: ReplForm.java:467
com.google.appinventor.components.runtime
Copyright 2009-2011 Google, All Rights reserved.
Definition: AccelerometerSensor.java:8
com.google.appinventor.components.runtime.util.WebRTCNativeMgr
Definition: WebRTCNativeMgr.java:65
com.google.appinventor.components.runtime.util.WebRTCNativeMgr.initiate
void initiate(ReplForm form, Context context, String code)
Definition: WebRTCNativeMgr.java:271
com.google.appinventor.components.common
Definition: ComponentCategory.java:7
com.google.appinventor.components.runtime.util.AsynchUtil
Definition: AsynchUtil.java:17
com.google
com
com.google.appinventor.components.common.YaVersion.RENDEZVOUS_SERVER
static final String RENDEZVOUS_SERVER
Definition: YaVersion.java:1357
com.google.appinventor