Home | History | Annotate | Download | only in apprtc
      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 
     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
     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
     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.
     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.
    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
    134   private String getConnectionUrl(
    135       RoomConnectionParameters connectionParameters) {
    136     return connectionParameters.roomUrl + "/" + ROOM_JOIN + "/"
    137         + connectionParameters.roomId;
    138   }
    139 
    140   private String getMessageUrl(RoomConnectionParameters connectionParameters,
    141       SignalingParameters signalingParameters) {
    142     return connectionParameters.roomUrl + "/" + ROOM_MESSAGE + "/"
    143       + connectionParameters.roomId + "/" + signalingParameters.clientId;
    144   }
    145 
    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.
    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
    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
    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
    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
    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
    314   public void onWebSocketClose() {
    315     events.onChannelClose();
    316   }
    317 
    318   @Override
    319   public void onWebSocketError(String description) {
    320     reportError("WebSocket error: " + description);
    321   }
    322 
    323   // --------------------------------------------------------------------
    324   // Helper functions.
    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|.
    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.
    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