• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  *  Copyright 2016 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 android.support.annotation.Nullable;
14 import android.util.Log;
15 import java.util.ArrayList;
16 import java.util.concurrent.ExecutorService;
17 import java.util.concurrent.Executors;
18 import java.util.regex.Matcher;
19 import java.util.regex.Pattern;
20 import org.json.JSONArray;
21 import org.json.JSONException;
22 import org.json.JSONObject;
23 import org.webrtc.IceCandidate;
24 import org.webrtc.SessionDescription;
25 
26 /**
27  * Implementation of AppRTCClient that uses direct TCP connection as the signaling channel.
28  * This eliminates the need for an external server. This class does not support loopback
29  * connections.
30  */
31 public class DirectRTCClient implements AppRTCClient, TCPChannelClient.TCPChannelEvents {
32   private static final String TAG = "DirectRTCClient";
33   private static final int DEFAULT_PORT = 8888;
34 
35   // Regex pattern used for checking if room id looks like an IP.
36   static final Pattern IP_PATTERN = Pattern.compile("("
37       // IPv4
38       + "((\\d+\\.){3}\\d+)|"
39       // IPv6
40       + "\\[((([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?::"
41       + "(([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?)\\]|"
42       + "\\[(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4})\\]|"
43       // IPv6 without []
44       + "((([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?::(([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?)|"
45       + "(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4})|"
46       // Literals
47       + "localhost"
48       + ")"
49       // Optional port number
50       + "(:(\\d+))?");
51 
52   private final ExecutorService executor;
53   private final SignalingEvents events;
54   @Nullable
55   private TCPChannelClient tcpClient;
56   private RoomConnectionParameters connectionParameters;
57 
58   private enum ConnectionState { NEW, CONNECTED, CLOSED, ERROR }
59 
60   // All alterations of the room state should be done from inside the looper thread.
61   private ConnectionState roomState;
62 
DirectRTCClient(SignalingEvents events)63   public DirectRTCClient(SignalingEvents events) {
64     this.events = events;
65 
66     executor = Executors.newSingleThreadExecutor();
67     roomState = ConnectionState.NEW;
68   }
69 
70   /**
71    * Connects to the room, roomId in connectionsParameters is required. roomId must be a valid
72    * IP address matching IP_PATTERN.
73    */
74   @Override
connectToRoom(RoomConnectionParameters connectionParameters)75   public void connectToRoom(RoomConnectionParameters connectionParameters) {
76     this.connectionParameters = connectionParameters;
77 
78     if (connectionParameters.loopback) {
79       reportError("Loopback connections aren't supported by DirectRTCClient.");
80     }
81 
82     executor.execute(new Runnable() {
83       @Override
84       public void run() {
85         connectToRoomInternal();
86       }
87     });
88   }
89 
90   @Override
disconnectFromRoom()91   public void disconnectFromRoom() {
92     executor.execute(new Runnable() {
93       @Override
94       public void run() {
95         disconnectFromRoomInternal();
96       }
97     });
98   }
99 
100   /**
101    * Connects to the room.
102    *
103    * Runs on the looper thread.
104    */
connectToRoomInternal()105   private void connectToRoomInternal() {
106     this.roomState = ConnectionState.NEW;
107 
108     String endpoint = connectionParameters.roomId;
109 
110     Matcher matcher = IP_PATTERN.matcher(endpoint);
111     if (!matcher.matches()) {
112       reportError("roomId must match IP_PATTERN for DirectRTCClient.");
113       return;
114     }
115 
116     String ip = matcher.group(1);
117     String portStr = matcher.group(matcher.groupCount());
118     int port;
119 
120     if (portStr != null) {
121       try {
122         port = Integer.parseInt(portStr);
123       } catch (NumberFormatException e) {
124         reportError("Invalid port number: " + portStr);
125         return;
126       }
127     } else {
128       port = DEFAULT_PORT;
129     }
130 
131     tcpClient = new TCPChannelClient(executor, this, ip, port);
132   }
133 
134   /**
135    * Disconnects from the room.
136    *
137    * Runs on the looper thread.
138    */
disconnectFromRoomInternal()139   private void disconnectFromRoomInternal() {
140     roomState = ConnectionState.CLOSED;
141 
142     if (tcpClient != null) {
143       tcpClient.disconnect();
144       tcpClient = null;
145     }
146     executor.shutdown();
147   }
148 
149   @Override
sendOfferSdp(final SessionDescription sdp)150   public void sendOfferSdp(final SessionDescription sdp) {
151     executor.execute(new Runnable() {
152       @Override
153       public void run() {
154         if (roomState != ConnectionState.CONNECTED) {
155           reportError("Sending offer SDP in non connected state.");
156           return;
157         }
158         JSONObject json = new JSONObject();
159         jsonPut(json, "sdp", sdp.description);
160         jsonPut(json, "type", "offer");
161         sendMessage(json.toString());
162       }
163     });
164   }
165 
166   @Override
sendAnswerSdp(final SessionDescription sdp)167   public void sendAnswerSdp(final SessionDescription sdp) {
168     executor.execute(new Runnable() {
169       @Override
170       public void run() {
171         JSONObject json = new JSONObject();
172         jsonPut(json, "sdp", sdp.description);
173         jsonPut(json, "type", "answer");
174         sendMessage(json.toString());
175       }
176     });
177   }
178 
179   @Override
sendLocalIceCandidate(final IceCandidate candidate)180   public void sendLocalIceCandidate(final IceCandidate candidate) {
181     executor.execute(new Runnable() {
182       @Override
183       public void run() {
184         JSONObject json = new JSONObject();
185         jsonPut(json, "type", "candidate");
186         jsonPut(json, "label", candidate.sdpMLineIndex);
187         jsonPut(json, "id", candidate.sdpMid);
188         jsonPut(json, "candidate", candidate.sdp);
189 
190         if (roomState != ConnectionState.CONNECTED) {
191           reportError("Sending ICE candidate in non connected state.");
192           return;
193         }
194         sendMessage(json.toString());
195       }
196     });
197   }
198 
199   /** Send removed Ice candidates to the other participant. */
200   @Override
sendLocalIceCandidateRemovals(final IceCandidate[] candidates)201   public void sendLocalIceCandidateRemovals(final IceCandidate[] candidates) {
202     executor.execute(new Runnable() {
203       @Override
204       public void run() {
205         JSONObject json = new JSONObject();
206         jsonPut(json, "type", "remove-candidates");
207         JSONArray jsonArray = new JSONArray();
208         for (final IceCandidate candidate : candidates) {
209           jsonArray.put(toJsonCandidate(candidate));
210         }
211         jsonPut(json, "candidates", jsonArray);
212 
213         if (roomState != ConnectionState.CONNECTED) {
214           reportError("Sending ICE candidate removals in non connected state.");
215           return;
216         }
217         sendMessage(json.toString());
218       }
219     });
220   }
221 
222   // -------------------------------------------------------------------
223   // TCPChannelClient event handlers
224 
225   /**
226    * If the client is the server side, this will trigger onConnectedToRoom.
227    */
228   @Override
onTCPConnected(boolean isServer)229   public void onTCPConnected(boolean isServer) {
230     if (isServer) {
231       roomState = ConnectionState.CONNECTED;
232 
233       SignalingParameters parameters = new SignalingParameters(
234           // Ice servers are not needed for direct connections.
235           new ArrayList<>(),
236           isServer, // Server side acts as the initiator on direct connections.
237           null, // clientId
238           null, // wssUrl
239           null, // wwsPostUrl
240           null, // offerSdp
241           null // iceCandidates
242           );
243       events.onConnectedToRoom(parameters);
244     }
245   }
246 
247   @Override
onTCPMessage(String msg)248   public void onTCPMessage(String msg) {
249     try {
250       JSONObject json = new JSONObject(msg);
251       String type = json.optString("type");
252       if (type.equals("candidate")) {
253         events.onRemoteIceCandidate(toJavaCandidate(json));
254       } else if (type.equals("remove-candidates")) {
255         JSONArray candidateArray = json.getJSONArray("candidates");
256         IceCandidate[] candidates = new IceCandidate[candidateArray.length()];
257         for (int i = 0; i < candidateArray.length(); ++i) {
258           candidates[i] = toJavaCandidate(candidateArray.getJSONObject(i));
259         }
260         events.onRemoteIceCandidatesRemoved(candidates);
261       } else if (type.equals("answer")) {
262         SessionDescription sdp = new SessionDescription(
263             SessionDescription.Type.fromCanonicalForm(type), json.getString("sdp"));
264         events.onRemoteDescription(sdp);
265       } else if (type.equals("offer")) {
266         SessionDescription sdp = new SessionDescription(
267             SessionDescription.Type.fromCanonicalForm(type), json.getString("sdp"));
268 
269         SignalingParameters parameters = new SignalingParameters(
270             // Ice servers are not needed for direct connections.
271             new ArrayList<>(),
272             false, // This code will only be run on the client side. So, we are not the initiator.
273             null, // clientId
274             null, // wssUrl
275             null, // wssPostUrl
276             sdp, // offerSdp
277             null // iceCandidates
278             );
279         roomState = ConnectionState.CONNECTED;
280         events.onConnectedToRoom(parameters);
281       } else {
282         reportError("Unexpected TCP message: " + msg);
283       }
284     } catch (JSONException e) {
285       reportError("TCP message JSON parsing error: " + e.toString());
286     }
287   }
288 
289   @Override
onTCPError(String description)290   public void onTCPError(String description) {
291     reportError("TCP connection error: " + description);
292   }
293 
294   @Override
onTCPClose()295   public void onTCPClose() {
296     events.onChannelClose();
297   }
298 
299   // --------------------------------------------------------------------
300   // Helper functions.
reportError(final String errorMessage)301   private void reportError(final String errorMessage) {
302     Log.e(TAG, errorMessage);
303     executor.execute(new Runnable() {
304       @Override
305       public void run() {
306         if (roomState != ConnectionState.ERROR) {
307           roomState = ConnectionState.ERROR;
308           events.onChannelError(errorMessage);
309         }
310       }
311     });
312   }
313 
sendMessage(final String message)314   private void sendMessage(final String message) {
315     executor.execute(new Runnable() {
316       @Override
317       public void run() {
318         tcpClient.send(message);
319       }
320     });
321   }
322 
323   // Put a |key|->|value| mapping in |json|.
jsonPut(JSONObject json, String key, Object value)324   private static void jsonPut(JSONObject json, String key, Object value) {
325     try {
326       json.put(key, value);
327     } catch (JSONException e) {
328       throw new RuntimeException(e);
329     }
330   }
331 
332   // Converts a Java candidate to a JSONObject.
toJsonCandidate(final IceCandidate candidate)333   private static JSONObject toJsonCandidate(final IceCandidate candidate) {
334     JSONObject json = new JSONObject();
335     jsonPut(json, "label", candidate.sdpMLineIndex);
336     jsonPut(json, "id", candidate.sdpMid);
337     jsonPut(json, "candidate", candidate.sdp);
338     return json;
339   }
340 
341   // Converts a JSON candidate to a Java object.
toJavaCandidate(JSONObject json)342   private static IceCandidate toJavaCandidate(JSONObject json) throws JSONException {
343     return new IceCandidate(
344         json.getString("id"), json.getInt("label"), json.getString("candidate"));
345   }
346 }
347