• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  *  Copyright 2014 The WebRTC Project Authors. All rights reserved.
3  *
4  *  Use of this source code is governed by a BSD-style license
5  *  that can be found in the LICENSE file in the root of the source
6  *  tree. An additional intellectual property rights grant can be found
7  *  in the file PATENTS.  All contributing project authors may
8  *  be found in the AUTHORS file in the root of the source tree.
9  */
10 
11 package org.appspot.apprtc;
12 
13 import org.appspot.apprtc.RoomParametersFetcher.RoomParametersFetcherEvents;
14 import org.appspot.apprtc.WebSocketChannelClient.WebSocketChannelEvents;
15 import org.appspot.apprtc.WebSocketChannelClient.WebSocketConnectionState;
16 import org.appspot.apprtc.util.AsyncHttpURLConnection;
17 import org.appspot.apprtc.util.AsyncHttpURLConnection.AsyncHttpEvents;
18 import org.appspot.apprtc.util.LooperExecutor;
19 
20 import android.util.Log;
21 
22 import org.json.JSONException;
23 import org.json.JSONObject;
24 import org.webrtc.IceCandidate;
25 import org.webrtc.SessionDescription;
26 
27 /**
28  * Negotiates signaling for chatting with apprtc.appspot.com "rooms".
29  * Uses the client<->server specifics of the apprtc AppEngine webapp.
30  *
31  * <p>To use: create an instance of this object (registering a message handler) and
32  * call connectToRoom().  Once room connection is established
33  * onConnectedToRoom() callback with room parameters is invoked.
34  * Messages to other party (with local Ice candidates and answer SDP) can
35  * be sent after WebSocket connection is established.
36  */
37 public class WebSocketRTCClient implements AppRTCClient,
38     WebSocketChannelEvents {
39   private static final String TAG = "WSRTCClient";
40   private static final String ROOM_JOIN = "join";
41   private static final String ROOM_MESSAGE = "message";
42   private static final String ROOM_LEAVE = "leave";
43 
44   private enum ConnectionState {
45     NEW, CONNECTED, CLOSED, ERROR
46   };
47   private enum MessageType {
48     MESSAGE, LEAVE
49   };
50   private final LooperExecutor executor;
51   private boolean initiator;
52   private SignalingEvents events;
53   private WebSocketChannelClient wsClient;
54   private ConnectionState roomState;
55   private RoomConnectionParameters connectionParameters;
56   private String messageUrl;
57   private String leaveUrl;
58 
WebSocketRTCClient(SignalingEvents events, LooperExecutor executor)59   public WebSocketRTCClient(SignalingEvents events, LooperExecutor executor) {
60     this.events = events;
61     this.executor = executor;
62     roomState = ConnectionState.NEW;
63     executor.requestStart();
64   }
65 
66   // --------------------------------------------------------------------
67   // AppRTCClient interface implementation.
68   // Asynchronously connect to an AppRTC room URL using supplied connection
69   // parameters, retrieves room parameters and connect to WebSocket server.
70   @Override
connectToRoom(RoomConnectionParameters connectionParameters)71   public void connectToRoom(RoomConnectionParameters connectionParameters) {
72     this.connectionParameters = connectionParameters;
73     executor.execute(new Runnable() {
74       @Override
75       public void run() {
76         connectToRoomInternal();
77       }
78     });
79   }
80 
81   @Override
disconnectFromRoom()82   public void disconnectFromRoom() {
83     executor.execute(new Runnable() {
84       @Override
85       public void run() {
86         disconnectFromRoomInternal();
87       }
88     });
89     executor.requestStop();
90   }
91 
92   // Connects to room - function runs on a local looper thread.
connectToRoomInternal()93   private void connectToRoomInternal() {
94     String connectionUrl = getConnectionUrl(connectionParameters);
95     Log.d(TAG, "Connect to room: " + connectionUrl);
96     roomState = ConnectionState.NEW;
97     wsClient = new WebSocketChannelClient(executor, this);
98 
99     RoomParametersFetcherEvents callbacks = new RoomParametersFetcherEvents() {
100       @Override
101       public void onSignalingParametersReady(
102           final SignalingParameters params) {
103         WebSocketRTCClient.this.executor.execute(new Runnable() {
104           @Override
105           public void run() {
106             WebSocketRTCClient.this.signalingParametersReady(params);
107           }
108         });
109       }
110 
111       @Override
112       public void onSignalingParametersError(String description) {
113         WebSocketRTCClient.this.reportError(description);
114       }
115     };
116 
117     new RoomParametersFetcher(connectionUrl, null, callbacks).makeRequest();
118   }
119 
120   // Disconnect from room and send bye messages - runs on a local looper thread.
disconnectFromRoomInternal()121   private void disconnectFromRoomInternal() {
122     Log.d(TAG, "Disconnect. Room state: " + roomState);
123     if (roomState == ConnectionState.CONNECTED) {
124       Log.d(TAG, "Closing room.");
125       sendPostMessage(MessageType.LEAVE, leaveUrl, null);
126     }
127     roomState = ConnectionState.CLOSED;
128     if (wsClient != null) {
129       wsClient.disconnect(true);
130     }
131   }
132 
133   // Helper functions to get connection, post message and leave message URLs
getConnectionUrl( RoomConnectionParameters connectionParameters)134   private String getConnectionUrl(
135       RoomConnectionParameters connectionParameters) {
136     return connectionParameters.roomUrl + "/" + ROOM_JOIN + "/"
137         + connectionParameters.roomId;
138   }
139 
getMessageUrl(RoomConnectionParameters connectionParameters, SignalingParameters signalingParameters)140   private String getMessageUrl(RoomConnectionParameters connectionParameters,
141       SignalingParameters signalingParameters) {
142     return connectionParameters.roomUrl + "/" + ROOM_MESSAGE + "/"
143       + connectionParameters.roomId + "/" + signalingParameters.clientId;
144   }
145 
getLeaveUrl(RoomConnectionParameters connectionParameters, SignalingParameters signalingParameters)146   private String getLeaveUrl(RoomConnectionParameters connectionParameters,
147       SignalingParameters signalingParameters) {
148     return connectionParameters.roomUrl + "/" + ROOM_LEAVE + "/"
149         + connectionParameters.roomId + "/" + signalingParameters.clientId;
150   }
151 
152   // Callback issued when room parameters are extracted. Runs on local
153   // looper thread.
signalingParametersReady( final SignalingParameters signalingParameters)154   private void signalingParametersReady(
155       final SignalingParameters signalingParameters) {
156     Log.d(TAG, "Room connection completed.");
157     if (connectionParameters.loopback
158         && (!signalingParameters.initiator
159             || signalingParameters.offerSdp != null)) {
160       reportError("Loopback room is busy.");
161       return;
162     }
163     if (!connectionParameters.loopback
164         && !signalingParameters.initiator
165         && signalingParameters.offerSdp == null) {
166       Log.w(TAG, "No offer SDP in room response.");
167     }
168     initiator = signalingParameters.initiator;
169     messageUrl = getMessageUrl(connectionParameters, signalingParameters);
170     leaveUrl = getLeaveUrl(connectionParameters, signalingParameters);
171     Log.d(TAG, "Message URL: " + messageUrl);
172     Log.d(TAG, "Leave URL: " + leaveUrl);
173     roomState = ConnectionState.CONNECTED;
174 
175     // Fire connection and signaling parameters events.
176     events.onConnectedToRoom(signalingParameters);
177 
178     // Connect and register WebSocket client.
179     wsClient.connect(signalingParameters.wssUrl, signalingParameters.wssPostUrl);
180     wsClient.register(connectionParameters.roomId, signalingParameters.clientId);
181   }
182 
183   // Send local offer SDP to the other participant.
184   @Override
sendOfferSdp(final SessionDescription sdp)185   public void sendOfferSdp(final SessionDescription sdp) {
186     executor.execute(new Runnable() {
187       @Override
188       public void run() {
189         if (roomState != ConnectionState.CONNECTED) {
190           reportError("Sending offer SDP in non connected state.");
191           return;
192         }
193         JSONObject json = new JSONObject();
194         jsonPut(json, "sdp", sdp.description);
195         jsonPut(json, "type", "offer");
196         sendPostMessage(MessageType.MESSAGE, messageUrl, json.toString());
197         if (connectionParameters.loopback) {
198           // In loopback mode rename this offer to answer and route it back.
199           SessionDescription sdpAnswer = new SessionDescription(
200               SessionDescription.Type.fromCanonicalForm("answer"),
201               sdp.description);
202           events.onRemoteDescription(sdpAnswer);
203         }
204       }
205     });
206   }
207 
208   // Send local answer SDP to the other participant.
209   @Override
sendAnswerSdp(final SessionDescription sdp)210   public void sendAnswerSdp(final SessionDescription sdp) {
211     executor.execute(new Runnable() {
212       @Override
213       public void run() {
214         if (connectionParameters.loopback) {
215           Log.e(TAG, "Sending answer in loopback mode.");
216           return;
217         }
218         JSONObject json = new JSONObject();
219         jsonPut(json, "sdp", sdp.description);
220         jsonPut(json, "type", "answer");
221         wsClient.send(json.toString());
222       }
223     });
224   }
225 
226   // Send Ice candidate to the other participant.
227   @Override
sendLocalIceCandidate(final IceCandidate candidate)228   public void sendLocalIceCandidate(final IceCandidate candidate) {
229     executor.execute(new Runnable() {
230       @Override
231       public void run() {
232         JSONObject json = new JSONObject();
233         jsonPut(json, "type", "candidate");
234         jsonPut(json, "label", candidate.sdpMLineIndex);
235         jsonPut(json, "id", candidate.sdpMid);
236         jsonPut(json, "candidate", candidate.sdp);
237         if (initiator) {
238           // Call initiator sends ice candidates to GAE server.
239           if (roomState != ConnectionState.CONNECTED) {
240             reportError("Sending ICE candidate in non connected state.");
241             return;
242           }
243           sendPostMessage(MessageType.MESSAGE, messageUrl, json.toString());
244           if (connectionParameters.loopback) {
245             events.onRemoteIceCandidate(candidate);
246           }
247         } else {
248           // Call receiver sends ice candidates to websocket server.
249           wsClient.send(json.toString());
250         }
251       }
252     });
253   }
254 
255   // --------------------------------------------------------------------
256   // WebSocketChannelEvents interface implementation.
257   // All events are called by WebSocketChannelClient on a local looper thread
258   // (passed to WebSocket client constructor).
259   @Override
onWebSocketMessage(final String msg)260   public void onWebSocketMessage(final String msg) {
261     if (wsClient.getState() != WebSocketConnectionState.REGISTERED) {
262       Log.e(TAG, "Got WebSocket message in non registered state.");
263       return;
264     }
265     try {
266       JSONObject json = new JSONObject(msg);
267       String msgText = json.getString("msg");
268       String errorText = json.optString("error");
269       if (msgText.length() > 0) {
270         json = new JSONObject(msgText);
271         String type = json.optString("type");
272         if (type.equals("candidate")) {
273           IceCandidate candidate = new IceCandidate(
274               json.getString("id"),
275               json.getInt("label"),
276               json.getString("candidate"));
277           events.onRemoteIceCandidate(candidate);
278         } else if (type.equals("answer")) {
279           if (initiator) {
280             SessionDescription sdp = new SessionDescription(
281                 SessionDescription.Type.fromCanonicalForm(type),
282                 json.getString("sdp"));
283             events.onRemoteDescription(sdp);
284           } else {
285             reportError("Received answer for call initiator: " + msg);
286           }
287         } else if (type.equals("offer")) {
288           if (!initiator) {
289             SessionDescription sdp = new SessionDescription(
290                 SessionDescription.Type.fromCanonicalForm(type),
291                 json.getString("sdp"));
292             events.onRemoteDescription(sdp);
293           } else {
294             reportError("Received offer for call receiver: " + msg);
295           }
296         } else if (type.equals("bye")) {
297           events.onChannelClose();
298         } else {
299           reportError("Unexpected WebSocket message: " + msg);
300         }
301       } else {
302         if (errorText != null && errorText.length() > 0) {
303           reportError("WebSocket error message: " + errorText);
304         } else {
305           reportError("Unexpected WebSocket message: " + msg);
306         }
307       }
308     } catch (JSONException e) {
309       reportError("WebSocket message JSON parsing error: " + e.toString());
310     }
311   }
312 
313   @Override
onWebSocketClose()314   public void onWebSocketClose() {
315     events.onChannelClose();
316   }
317 
318   @Override
onWebSocketError(String description)319   public void onWebSocketError(String description) {
320     reportError("WebSocket error: " + description);
321   }
322 
323   // --------------------------------------------------------------------
324   // Helper functions.
reportError(final String errorMessage)325   private void reportError(final String errorMessage) {
326     Log.e(TAG, errorMessage);
327     executor.execute(new Runnable() {
328       @Override
329       public void run() {
330         if (roomState != ConnectionState.ERROR) {
331           roomState = ConnectionState.ERROR;
332           events.onChannelError(errorMessage);
333         }
334       }
335     });
336   }
337 
338   // Put a |key|->|value| mapping in |json|.
jsonPut(JSONObject json, String key, Object value)339   private static void jsonPut(JSONObject json, String key, Object value) {
340     try {
341       json.put(key, value);
342     } catch (JSONException e) {
343       throw new RuntimeException(e);
344     }
345   }
346 
347   // Send SDP or ICE candidate to a room server.
sendPostMessage( final MessageType messageType, final String url, final String message)348   private void sendPostMessage(
349       final MessageType messageType, final String url, final String message) {
350     String logInfo = url;
351     if (message != null) {
352       logInfo += ". Message: " + message;
353     }
354     Log.d(TAG, "C->GAE: " + logInfo);
355     AsyncHttpURLConnection httpConnection = new AsyncHttpURLConnection(
356       "POST", url, message, new AsyncHttpEvents() {
357         @Override
358         public void onHttpError(String errorMessage) {
359           reportError("GAE POST error: " + errorMessage);
360         }
361 
362         @Override
363         public void onHttpComplete(String response) {
364           if (messageType == MessageType.MESSAGE) {
365             try {
366               JSONObject roomJson = new JSONObject(response);
367               String result = roomJson.getString("result");
368               if (!result.equals("SUCCESS")) {
369                 reportError("GAE POST error: " + result);
370               }
371             } catch (JSONException e) {
372               reportError("GAE POST JSON error: " + e.toString());
373             }
374           }
375         }
376       });
377     httpConnection.send();
378   }
379 }
380