OLD | NEW |
---|---|
(Empty) | |
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.util.Log; | |
14 | |
15 import org.appspot.apprtc.util.LooperExecutor; | |
16 import org.json.JSONArray; | |
17 import org.json.JSONException; | |
18 import org.json.JSONObject; | |
19 import org.webrtc.IceCandidate; | |
20 import org.webrtc.PeerConnection; | |
21 import org.webrtc.SessionDescription; | |
22 | |
23 import java.util.LinkedList; | |
24 import java.util.regex.Matcher; | |
25 import java.util.regex.Pattern; | |
26 | |
27 /** | |
28 * Implementation of AppRTCClient that uses direct TCP connection as the signali ng channel. | |
29 * This eliminates the need for an external server. This class does not support loopback | |
30 * connections. | |
31 */ | |
32 public class DirectRTCClient implements AppRTCClient, TCPChannelClient.TCPChanne lEvents { | |
33 private static final String TAG = "DirectRTCClient"; | |
34 private static final int DEFAULT_PORT = 8888; | |
35 | |
36 // Regex pattern used for checking if room id looks like an IP. | |
37 static final Pattern IP_PATTERN = Pattern.compile( | |
38 "(" | |
39 // IPv4 | |
40 + "((\\d+\\.){3}\\d+)|" | |
41 // IPv6 | |
42 + "\\[((([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?::" | |
43 + "(([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?)\\]|" | |
44 + "\\[(([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})\\]|" | |
45 // IPv6 without [] | |
46 + "((([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?::(([0-9a-fA-F]{1,4}:)*[0-9a- fA-F]{1,4})?)|" | |
47 + "(([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})|" | |
48 // Literals | |
49 + "localhost" | |
50 + ")" | |
51 // Optional port number | |
52 + "(:\\d+)?" | |
53 ); | |
54 | |
55 private final LooperExecutor executor; | |
56 private final SignalingEvents events; | |
57 private TCPChannelClient tcpClient; | |
58 private RoomConnectionParameters connectionParameters; | |
59 | |
60 private enum ConnectionState { | |
61 NEW, CONNECTED, CLOSED, ERROR | |
62 }; | |
63 | |
64 // All alterations of the room state should be done from inside the looper thr ead. | |
65 private ConnectionState roomState; | |
66 | |
67 public DirectRTCClient(SignalingEvents events) { | |
68 this.events = events; | |
69 executor = new LooperExecutor(); | |
70 | |
71 executor.requestStart(); | |
72 roomState = ConnectionState.NEW; | |
73 } | |
74 | |
75 /** | |
76 * Connects to the room, roomId in connectionsParameters is required. roomId m ust be a valid | |
77 * IP address matching IP_PATTERN. | |
78 */ | |
79 @Override | |
80 public void connectToRoom(RoomConnectionParameters connectionParameters) { | |
81 this.connectionParameters = connectionParameters; | |
82 | |
83 if (connectionParameters.loopback) { | |
84 reportError("Loopback connections aren't supported by DirectRTCClient."); | |
85 } | |
86 | |
87 executor.execute(new Runnable() { | |
88 @Override | |
89 public void run() { | |
90 connectToRoomInternal(); | |
91 } | |
92 }); | |
93 } | |
94 | |
95 @Override | |
96 public void disconnectFromRoom() { | |
97 executor.execute(new Runnable() { | |
98 @Override | |
99 public void run() { | |
100 disconnectFromRoomInternal(); | |
101 } | |
102 }); | |
103 executor.requestStop(); | |
104 } | |
105 | |
106 /** | |
107 * Connects to the room. | |
108 * | |
109 * Runs on the looper thread. | |
110 */ | |
111 private void connectToRoomInternal() { | |
112 this.roomState = ConnectionState.NEW; | |
113 | |
114 String endpoint = connectionParameters.roomId; | |
115 | |
116 Matcher matcher = IP_PATTERN.matcher(endpoint); | |
117 if(!matcher.matches()) { | |
magjed_webrtc
2016/05/11 11:22:56
nit: add space between 'if' and '('
sakal
2016/05/11 12:19:43
Done.
| |
118 reportError("roomId must match IP_PATTERN for DirectRTCClient."); | |
119 return; | |
120 } | |
121 | |
122 String ip = matcher.group(1); | |
123 String portStr = matcher.group(matcher.groupCount()); | |
124 int port; | |
125 | |
126 if (portStr != null) { | |
127 port = Integer.parseInt(portStr.substring(1)); | |
magjed_webrtc
2016/05/11 11:22:56
This will never throw NumberFormatException as lon
sakal
2016/05/11 12:19:43
It will throw a NumberFormatException if the numbe
| |
128 } else { | |
129 port = DEFAULT_PORT; | |
130 } | |
131 | |
132 tcpClient = new TCPChannelClient(executor, this, ip, port); | |
133 } | |
134 | |
135 /** | |
136 * Disconnects from the room. | |
137 * | |
138 * Runs on the looper thread. | |
139 */ | |
140 private void disconnectFromRoomInternal() { | |
141 roomState = ConnectionState.CLOSED; | |
142 | |
143 if (tcpClient != null) { | |
144 tcpClient.disconnect(); | |
145 tcpClient = null; | |
146 } | |
147 } | |
148 | |
149 @Override | |
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 | |
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 | |
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 | |
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 | |
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 LinkedList<PeerConnection.IceServer>(), | |
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 | |
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), | |
264 json.getString("sdp")); | |
265 events.onRemoteDescription(sdp); | |
266 } else if (type.equals("offer")) { | |
267 SessionDescription sdp = new SessionDescription( | |
268 SessionDescription.Type.fromCanonicalForm(type), | |
269 json.getString("sdp")); | |
270 | |
271 SignalingParameters parameters = new SignalingParameters( | |
272 // Ice servers are not needed for direct connections. | |
273 new LinkedList<PeerConnection.IceServer>(), | |
274 false, // This code will only be run on the client side. So, we are not the initiator. | |
275 null, // clientId | |
276 null, // wssUrl | |
277 null, // wssPostUrl | |
278 sdp, // offerSdp | |
279 null // iceCandidates | |
280 ); | |
281 roomState = ConnectionState.CONNECTED; | |
282 events.onConnectedToRoom(parameters); | |
283 } else { | |
284 reportError("Unexpected TCP message: " + msg); | |
285 } | |
286 } catch (JSONException e) { | |
287 reportError("TCP message JSON parsing error: " + e.toString()); | |
288 } | |
289 } | |
290 | |
291 @Override | |
292 public void onTCPError(String description) { | |
293 reportError("TCP connection error: " + description); | |
294 } | |
295 | |
296 @Override | |
297 public void onTCPClose() { | |
298 events.onChannelClose(); | |
299 } | |
300 | |
301 // -------------------------------------------------------------------- | |
302 // Helper functions. | |
303 private void reportError(final String errorMessage) { | |
304 Log.e(TAG, errorMessage); | |
305 executor.execute(new Runnable() { | |
306 @Override | |
307 public void run() { | |
308 if (roomState != ConnectionState.ERROR) { | |
309 roomState = ConnectionState.ERROR; | |
310 events.onChannelError(errorMessage); | |
311 } | |
312 } | |
313 }); | |
314 } | |
315 | |
316 private void sendMessage(final String message) { | |
317 executor.execute(new Runnable() { | |
318 @Override | |
319 public void run() { | |
320 tcpClient.send(message); | |
321 } | |
322 }); | |
323 } | |
324 | |
325 // Put a |key|->|value| mapping in |json|. | |
326 private static void jsonPut(JSONObject json, String key, Object value) { | |
327 try { | |
328 json.put(key, value); | |
329 } catch (JSONException e) { | |
330 throw new RuntimeException(e); | |
331 } | |
332 } | |
333 | |
334 // Converts a Java candidate to a JSONObject. | |
335 private static JSONObject toJsonCandidate(final IceCandidate candidate) { | |
336 JSONObject json = new JSONObject(); | |
337 jsonPut(json, "label", candidate.sdpMLineIndex); | |
338 jsonPut(json, "id", candidate.sdpMid); | |
339 jsonPut(json, "candidate", candidate.sdp); | |
340 return json; | |
341 } | |
342 | |
343 // Converts a JSON candidate to a Java object. | |
344 private static IceCandidate toJavaCandidate(JSONObject json) throws JSONExcept ion { | |
345 return new IceCandidate(json.getString("id"), | |
346 json.getInt("label"), | |
347 json.getString("candidate")); | |
348 } | |
349 } | |
OLD | NEW |