Upstream version 8.37.180.0
[platform/framework/web/crosswalk.git] / src / third_party / libjingle / source / talk / examples / android / src / org / appspot / apprtc / AppRTCClient.java
1 /*
2  * libjingle
3  * Copyright 2013, Google Inc.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are met:
7  *
8  *  1. Redistributions of source code must retain the above copyright notice,
9  *     this list of conditions and the following disclaimer.
10  *  2. Redistributions in binary form must reproduce the above copyright notice,
11  *     this list of conditions and the following disclaimer in the documentation
12  *     and/or other materials provided with the distribution.
13  *  3. The name of the author may not be used to endorse or promote products
14  *     derived from this software without specific prior written permission.
15  *
16  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
17  * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
18  * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
19  * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
21  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
22  * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
23  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
24  * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
25  * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26  */
27
28 package org.appspot.apprtc;
29
30 import android.app.Activity;
31 import android.os.AsyncTask;
32 import android.util.Log;
33
34 import org.json.JSONArray;
35 import org.json.JSONException;
36 import org.json.JSONObject;
37 import org.webrtc.MediaConstraints;
38 import org.webrtc.PeerConnection;
39
40 import java.io.IOException;
41 import java.io.InputStream;
42 import java.net.HttpURLConnection;
43 import java.net.URL;
44 import java.net.URLConnection;
45 import java.util.LinkedList;
46 import java.util.List;
47 import java.util.Scanner;
48
49 /**
50  * Negotiates signaling for chatting with apprtc.appspot.com "rooms".
51  * Uses the client<->server specifics of the apprtc AppEngine webapp.
52  *
53  * To use: create an instance of this object (registering a message handler) and
54  * call connectToRoom().  Once that's done call sendMessage() and wait for the
55  * registered handler to be called with received messages.
56  */
57 public class AppRTCClient {
58   private static final String TAG = "AppRTCClient";
59   private GAEChannelClient channelClient;
60   private final Activity activity;
61   private final GAEChannelClient.MessageHandler gaeHandler;
62   private final IceServersObserver iceServersObserver;
63
64   // These members are only read/written under sendQueue's lock.
65   private LinkedList<String> sendQueue = new LinkedList<String>();
66   private AppRTCSignalingParameters appRTCSignalingParameters;
67
68   /**
69    * Callback fired once the room's signaling parameters specify the set of
70    * ICE servers to use.
71    */
72   public static interface IceServersObserver {
73     public void onIceServers(List<PeerConnection.IceServer> iceServers);
74   }
75
76   public AppRTCClient(
77       Activity activity, GAEChannelClient.MessageHandler gaeHandler,
78       IceServersObserver iceServersObserver) {
79     this.activity = activity;
80     this.gaeHandler = gaeHandler;
81     this.iceServersObserver = iceServersObserver;
82   }
83
84   /**
85    * Asynchronously connect to an AppRTC room URL, e.g.
86    * https://apprtc.appspot.com/?r=NNN and register message-handling callbacks
87    * on its GAE Channel.
88    */
89   public void connectToRoom(String url) {
90     while (url.indexOf('?') < 0) {
91       // Keep redirecting until we get a room number.
92       (new RedirectResolver()).execute(url);
93       return;  // RedirectResolver above calls us back with the next URL.
94     }
95     (new RoomParameterGetter()).execute(url);
96   }
97
98   /**
99    * Disconnect from the GAE Channel.
100    */
101   public void disconnect() {
102     if (channelClient != null) {
103       channelClient.close();
104       channelClient = null;
105     }
106   }
107
108   /**
109    * Queue a message for sending to the room's channel and send it if already
110    * connected (other wise queued messages are drained when the channel is
111      eventually established).
112    */
113   public synchronized void sendMessage(String msg) {
114     synchronized (sendQueue) {
115       sendQueue.add(msg);
116     }
117     requestQueueDrainInBackground();
118   }
119
120   public boolean isInitiator() {
121     return appRTCSignalingParameters.initiator;
122   }
123
124   public MediaConstraints pcConstraints() {
125     return appRTCSignalingParameters.pcConstraints;
126   }
127
128   public MediaConstraints videoConstraints() {
129     return appRTCSignalingParameters.videoConstraints;
130   }
131
132   public MediaConstraints audioConstraints() {
133     return appRTCSignalingParameters.audioConstraints;
134   }
135
136   // Struct holding the signaling parameters of an AppRTC room.
137   private class AppRTCSignalingParameters {
138     public final List<PeerConnection.IceServer> iceServers;
139     public final String gaeBaseHref;
140     public final String channelToken;
141     public final String postMessageUrl;
142     public final boolean initiator;
143     public final MediaConstraints pcConstraints;
144     public final MediaConstraints videoConstraints;
145     public final MediaConstraints audioConstraints;
146
147     public AppRTCSignalingParameters(
148         List<PeerConnection.IceServer> iceServers,
149         String gaeBaseHref, String channelToken, String postMessageUrl,
150         boolean initiator, MediaConstraints pcConstraints,
151         MediaConstraints videoConstraints, MediaConstraints audioConstraints) {
152       this.iceServers = iceServers;
153       this.gaeBaseHref = gaeBaseHref;
154       this.channelToken = channelToken;
155       this.postMessageUrl = postMessageUrl;
156       this.initiator = initiator;
157       this.pcConstraints = pcConstraints;
158       this.videoConstraints = videoConstraints;
159       this.audioConstraints = audioConstraints;
160     }
161   }
162
163   // Load the given URL and return the value of the Location header of the
164   // resulting 302 response.  If the result is not a 302, throws.
165   private class RedirectResolver extends AsyncTask<String, Void, String> {
166     @Override
167     protected String doInBackground(String... urls) {
168       if (urls.length != 1) {
169         throw new RuntimeException("Must be called with a single URL");
170       }
171       try {
172         return followRedirect(urls[0]);
173       } catch (IOException e) {
174         throw new RuntimeException(e);
175       }
176     }
177
178     @Override
179     protected void onPostExecute(String url) {
180       connectToRoom(url);
181     }
182
183     private String followRedirect(String url) throws IOException {
184       HttpURLConnection connection = (HttpURLConnection)
185           new URL(url).openConnection();
186       connection.setInstanceFollowRedirects(false);
187       int code = connection.getResponseCode();
188       if (code != HttpURLConnection.HTTP_MOVED_TEMP) {
189         throw new IOException("Unexpected response: " + code + " for " + url +
190             ", with contents: " + drainStream(connection.getInputStream()));
191       }
192       int n = 0;
193       String name, value;
194       while ((name = connection.getHeaderFieldKey(n)) != null) {
195         value = connection.getHeaderField(n);
196         if (name.equals("Location")) {
197           return value;
198         }
199         ++n;
200       }
201       throw new IOException("Didn't find Location header!");
202     }
203   }
204
205   // AsyncTask that converts an AppRTC room URL into the set of signaling
206   // parameters to use with that room.
207   private class RoomParameterGetter
208       extends AsyncTask<String, Void, AppRTCSignalingParameters> {
209     @Override
210     protected AppRTCSignalingParameters doInBackground(String... urls) {
211       if (urls.length != 1) {
212         throw new RuntimeException("Must be called with a single URL");
213       }
214       try {
215         return getParametersForRoomUrl(urls[0]);
216       } catch (JSONException e) {
217         throw new RuntimeException(e);
218       } catch (IOException e) {
219         throw new RuntimeException(e);
220       }
221     }
222
223     @Override
224     protected void onPostExecute(AppRTCSignalingParameters params) {
225       channelClient =
226           new GAEChannelClient(activity, params.channelToken, gaeHandler);
227       synchronized (sendQueue) {
228         appRTCSignalingParameters = params;
229       }
230       requestQueueDrainInBackground();
231       iceServersObserver.onIceServers(appRTCSignalingParameters.iceServers);
232     }
233
234     // Fetches |url| and fishes the signaling parameters out of the JSON.
235     private AppRTCSignalingParameters getParametersForRoomUrl(String url)
236         throws IOException, JSONException {
237       url = url + "&t=json";
238       JSONObject roomJson = new JSONObject(
239           drainStream((new URL(url)).openConnection().getInputStream()));
240
241       if (roomJson.has("error")) {
242         JSONArray errors = roomJson.getJSONArray("error_messages");
243         throw new IOException(errors.toString());
244       }
245
246       String gaeBaseHref = url.substring(0, url.indexOf('?'));
247       String token = roomJson.getString("token");
248       String postMessageUrl = "/message?r=" +
249           roomJson.getString("room_key") + "&u=" +
250           roomJson.getString("me");
251       boolean initiator = roomJson.getInt("initiator") == 1;
252       LinkedList<PeerConnection.IceServer> iceServers =
253           iceServersFromPCConfigJSON(roomJson.getString("pc_config"));
254
255       boolean isTurnPresent = false;
256       for (PeerConnection.IceServer server : iceServers) {
257         if (server.uri.startsWith("turn:")) {
258           isTurnPresent = true;
259           break;
260         }
261       }
262       if (!isTurnPresent) {
263         iceServers.add(requestTurnServer(roomJson.getString("turn_url")));
264       }
265
266       MediaConstraints pcConstraints = constraintsFromJSON(
267           roomJson.getString("pc_constraints"));
268       addDTLSConstraintIfMissing(pcConstraints);
269       Log.d(TAG, "pcConstraints: " + pcConstraints);
270       MediaConstraints videoConstraints = constraintsFromJSON(
271           getAVConstraints("video",
272               roomJson.getString("media_constraints")));
273       Log.d(TAG, "videoConstraints: " + videoConstraints);
274       MediaConstraints audioConstraints = constraintsFromJSON(
275           getAVConstraints("audio",
276               roomJson.getString("media_constraints")));
277       Log.d(TAG, "audioConstraints: " + audioConstraints);
278
279       return new AppRTCSignalingParameters(
280           iceServers, gaeBaseHref, token, postMessageUrl, initiator,
281           pcConstraints, videoConstraints, audioConstraints);
282     }
283
284     // Mimic Chrome and set DtlsSrtpKeyAgreement to true if not set to false by
285     // the web-app.
286     private void addDTLSConstraintIfMissing(
287         MediaConstraints pcConstraints) {
288       for (MediaConstraints.KeyValuePair pair : pcConstraints.mandatory) {
289         if (pair.getKey().equals("DtlsSrtpKeyAgreement")) {
290           return;
291         }
292       }
293       for (MediaConstraints.KeyValuePair pair : pcConstraints.optional) {
294         if (pair.getKey().equals("DtlsSrtpKeyAgreement")) {
295           return;
296         }
297       }
298       // DTLS isn't being suppressed (e.g. for debug=loopback calls), so enable
299       // it by default.
300       pcConstraints.optional.add(
301           new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"));
302     }
303
304     // Return the constraints specified for |type| of "audio" or "video" in
305     // |mediaConstraintsString|.
306     private String getAVConstraints(
307         String type, String mediaConstraintsString) {
308       try {
309         JSONObject json = new JSONObject(mediaConstraintsString);
310         // Tricksy handling of values that are allowed to be (boolean or
311         // MediaTrackConstraints) by the getUserMedia() spec.  There are three
312         // cases below.
313         if (!json.has(type) || !json.optBoolean(type, true)) {
314           // Case 1: "audio"/"video" is not present, or is an explicit "false"
315           // boolean.
316           return null;
317         }
318         if (json.optBoolean(type, false)) {
319           // Case 2: "audio"/"video" is an explicit "true" boolean.
320           return "{\"mandatory\": {}, \"optional\": []}";
321         }
322         // Case 3: "audio"/"video" is an object.
323         return json.getJSONObject(type).toString();
324       } catch (JSONException e) {
325         throw new RuntimeException(e);
326       }
327     }
328
329     private MediaConstraints constraintsFromJSON(String jsonString) {
330       if (jsonString == null) {
331         return null;
332       }
333       try {
334         MediaConstraints constraints = new MediaConstraints();
335         JSONObject json = new JSONObject(jsonString);
336         JSONObject mandatoryJSON = json.optJSONObject("mandatory");
337         if (mandatoryJSON != null) {
338           JSONArray mandatoryKeys = mandatoryJSON.names();
339           if (mandatoryKeys != null) {
340             for (int i = 0; i < mandatoryKeys.length(); ++i) {
341               String key = mandatoryKeys.getString(i);
342               String value = mandatoryJSON.getString(key);
343               constraints.mandatory.add(
344                   new MediaConstraints.KeyValuePair(key, value));
345             }
346           }
347         }
348         JSONArray optionalJSON = json.optJSONArray("optional");
349         if (optionalJSON != null) {
350           for (int i = 0; i < optionalJSON.length(); ++i) {
351             JSONObject keyValueDict = optionalJSON.getJSONObject(i);
352             String key = keyValueDict.names().getString(0);
353             String value = keyValueDict.getString(key);
354             constraints.optional.add(
355                 new MediaConstraints.KeyValuePair(key, value));
356           }
357         }
358         return constraints;
359       } catch (JSONException e) {
360         throw new RuntimeException(e);
361       }
362     }
363
364     // Requests & returns a TURN ICE Server based on a request URL.  Must be run
365     // off the main thread!
366     private PeerConnection.IceServer requestTurnServer(String url) {
367       try {
368         URLConnection connection = (new URL(url)).openConnection();
369         connection.addRequestProperty("user-agent", "Mozilla/5.0");
370         connection.addRequestProperty("origin", "https://apprtc.appspot.com");
371         String response = drainStream(connection.getInputStream());
372         JSONObject responseJSON = new JSONObject(response);
373         String uri = responseJSON.getJSONArray("uris").getString(0);
374         String username = responseJSON.getString("username");
375         String password = responseJSON.getString("password");
376         return new PeerConnection.IceServer(uri, username, password);
377       } catch (JSONException e) {
378         throw new RuntimeException(e);
379       } catch (IOException e) {
380         throw new RuntimeException(e);
381       }
382     }
383   }
384
385   // Return the list of ICE servers described by a WebRTCPeerConnection
386   // configuration string.
387   private LinkedList<PeerConnection.IceServer> iceServersFromPCConfigJSON(
388       String pcConfig) {
389     try {
390       JSONObject json = new JSONObject(pcConfig);
391       JSONArray servers = json.getJSONArray("iceServers");
392       LinkedList<PeerConnection.IceServer> ret =
393           new LinkedList<PeerConnection.IceServer>();
394       for (int i = 0; i < servers.length(); ++i) {
395         JSONObject server = servers.getJSONObject(i);
396         String url = server.getString("urls");
397         String credential =
398             server.has("credential") ? server.getString("credential") : "";
399         ret.add(new PeerConnection.IceServer(url, "", credential));
400       }
401       return ret;
402     } catch (JSONException e) {
403       throw new RuntimeException(e);
404     }
405   }
406
407   // Request an attempt to drain the send queue, on a background thread.
408   private void requestQueueDrainInBackground() {
409     (new AsyncTask<Void, Void, Void>() {
410       public Void doInBackground(Void... unused) {
411         maybeDrainQueue();
412         return null;
413       }
414     }).execute();
415   }
416
417   // Send all queued messages if connected to the room.
418   private void maybeDrainQueue() {
419     synchronized (sendQueue) {
420       if (appRTCSignalingParameters == null) {
421         return;
422       }
423       try {
424         for (String msg : sendQueue) {
425           URLConnection connection = new URL(
426               appRTCSignalingParameters.gaeBaseHref +
427               appRTCSignalingParameters.postMessageUrl).openConnection();
428           connection.setDoOutput(true);
429           connection.getOutputStream().write(msg.getBytes("UTF-8"));
430           if (!connection.getHeaderField(null).startsWith("HTTP/1.1 200 ")) {
431             throw new IOException(
432                 "Non-200 response to POST: " + connection.getHeaderField(null) +
433                 " for msg: " + msg);
434           }
435         }
436       } catch (IOException e) {
437         throw new RuntimeException(e);
438       }
439       sendQueue.clear();
440     }
441   }
442
443   // Return the contents of an InputStream as a String.
444   private static String drainStream(InputStream in) {
445     Scanner s = new Scanner(in).useDelimiter("\\A");
446     return s.hasNext() ? s.next() : "";
447   }
448 }