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()) { |
| 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 try { |
| 128 port = Integer.parseInt(portStr); |
| 129 } catch (NumberFormatException e) { |
| 130 reportError("Invalid port number: " + portStr); |
| 131 return; |
| 132 } |
| 133 } else { |
| 134 port = DEFAULT_PORT; |
| 135 } |
| 136 |
| 137 tcpClient = new TCPChannelClient(executor, this, ip, port); |
| 138 } |
| 139 |
| 140 /** |
| 141 * Disconnects from the room. |
| 142 * |
| 143 * Runs on the looper thread. |
| 144 */ |
| 145 private void disconnectFromRoomInternal() { |
| 146 roomState = ConnectionState.CLOSED; |
| 147 |
| 148 if (tcpClient != null) { |
| 149 tcpClient.disconnect(); |
| 150 tcpClient = null; |
| 151 } |
| 152 } |
| 153 |
| 154 @Override |
| 155 public void sendOfferSdp(final SessionDescription sdp) { |
| 156 executor.execute(new Runnable() { |
| 157 @Override |
| 158 public void run() { |
| 159 if (roomState != ConnectionState.CONNECTED) { |
| 160 reportError("Sending offer SDP in non connected state."); |
| 161 return; |
| 162 } |
| 163 JSONObject json = new JSONObject(); |
| 164 jsonPut(json, "sdp", sdp.description); |
| 165 jsonPut(json, "type", "offer"); |
| 166 sendMessage(json.toString()); |
| 167 } |
| 168 }); |
| 169 } |
| 170 |
| 171 @Override |
| 172 public void sendAnswerSdp(final SessionDescription sdp) { |
| 173 executor.execute(new Runnable() { |
| 174 @Override |
| 175 public void run() { |
| 176 JSONObject json = new JSONObject(); |
| 177 jsonPut(json, "sdp", sdp.description); |
| 178 jsonPut(json, "type", "answer"); |
| 179 sendMessage(json.toString()); |
| 180 } |
| 181 }); |
| 182 } |
| 183 |
| 184 @Override |
| 185 public void sendLocalIceCandidate(final IceCandidate candidate) { |
| 186 executor.execute(new Runnable() { |
| 187 @Override |
| 188 public void run() { |
| 189 JSONObject json = new JSONObject(); |
| 190 jsonPut(json, "type", "candidate"); |
| 191 jsonPut(json, "label", candidate.sdpMLineIndex); |
| 192 jsonPut(json, "id", candidate.sdpMid); |
| 193 jsonPut(json, "candidate", candidate.sdp); |
| 194 |
| 195 if (roomState != ConnectionState.CONNECTED) { |
| 196 reportError("Sending ICE candidate in non connected state."); |
| 197 return; |
| 198 } |
| 199 sendMessage(json.toString()); |
| 200 } |
| 201 }); |
| 202 } |
| 203 |
| 204 /** Send removed Ice candidates to the other participant. */ |
| 205 @Override |
| 206 public void sendLocalIceCandidateRemovals(final IceCandidate[] candidates) { |
| 207 executor.execute(new Runnable() { |
| 208 @Override |
| 209 public void run() { |
| 210 JSONObject json = new JSONObject(); |
| 211 jsonPut(json, "type", "remove-candidates"); |
| 212 JSONArray jsonArray = new JSONArray(); |
| 213 for (final IceCandidate candidate : candidates) { |
| 214 jsonArray.put(toJsonCandidate(candidate)); |
| 215 } |
| 216 jsonPut(json, "candidates", jsonArray); |
| 217 |
| 218 if (roomState != ConnectionState.CONNECTED) { |
| 219 reportError("Sending ICE candidate removals in non connected state."); |
| 220 return; |
| 221 } |
| 222 sendMessage(json.toString()); |
| 223 } |
| 224 }); |
| 225 } |
| 226 |
| 227 // ------------------------------------------------------------------- |
| 228 // TCPChannelClient event handlers |
| 229 |
| 230 /** |
| 231 * If the client is the server side, this will trigger onConnectedToRoom. |
| 232 */ |
| 233 @Override |
| 234 public void onTCPConnected(boolean isServer) { |
| 235 if (isServer) { |
| 236 roomState = ConnectionState.CONNECTED; |
| 237 |
| 238 SignalingParameters parameters = new SignalingParameters( |
| 239 // Ice servers are not needed for direct connections. |
| 240 new LinkedList<PeerConnection.IceServer>(), |
| 241 isServer, // Server side acts as the initiator on direct connections. |
| 242 null, // clientId |
| 243 null, // wssUrl |
| 244 null, // wwsPostUrl |
| 245 null, // offerSdp |
| 246 null // iceCandidates |
| 247 ); |
| 248 events.onConnectedToRoom(parameters); |
| 249 } |
| 250 } |
| 251 |
| 252 @Override |
| 253 public void onTCPMessage(String msg) { |
| 254 try { |
| 255 JSONObject json = new JSONObject(msg); |
| 256 String type = json.optString("type"); |
| 257 if (type.equals("candidate")) { |
| 258 events.onRemoteIceCandidate(toJavaCandidate(json)); |
| 259 } else if (type.equals("remove-candidates")) { |
| 260 JSONArray candidateArray = json.getJSONArray("candidates"); |
| 261 IceCandidate[] candidates = new IceCandidate[candidateArray.length()]; |
| 262 for (int i = 0; i < candidateArray.length(); ++i) { |
| 263 candidates[i] = toJavaCandidate(candidateArray.getJSONObject(i)); |
| 264 } |
| 265 events.onRemoteIceCandidatesRemoved(candidates); |
| 266 } else if (type.equals("answer")) { |
| 267 SessionDescription sdp = new SessionDescription( |
| 268 SessionDescription.Type.fromCanonicalForm(type), |
| 269 json.getString("sdp")); |
| 270 events.onRemoteDescription(sdp); |
| 271 } else if (type.equals("offer")) { |
| 272 SessionDescription sdp = new SessionDescription( |
| 273 SessionDescription.Type.fromCanonicalForm(type), |
| 274 json.getString("sdp")); |
| 275 |
| 276 SignalingParameters parameters = new SignalingParameters( |
| 277 // Ice servers are not needed for direct connections. |
| 278 new LinkedList<PeerConnection.IceServer>(), |
| 279 false, // This code will only be run on the client side. So, we are
not the initiator. |
| 280 null, // clientId |
| 281 null, // wssUrl |
| 282 null, // wssPostUrl |
| 283 sdp, // offerSdp |
| 284 null // iceCandidates |
| 285 ); |
| 286 roomState = ConnectionState.CONNECTED; |
| 287 events.onConnectedToRoom(parameters); |
| 288 } else { |
| 289 reportError("Unexpected TCP message: " + msg); |
| 290 } |
| 291 } catch (JSONException e) { |
| 292 reportError("TCP message JSON parsing error: " + e.toString()); |
| 293 } |
| 294 } |
| 295 |
| 296 @Override |
| 297 public void onTCPError(String description) { |
| 298 reportError("TCP connection error: " + description); |
| 299 } |
| 300 |
| 301 @Override |
| 302 public void onTCPClose() { |
| 303 events.onChannelClose(); |
| 304 } |
| 305 |
| 306 // -------------------------------------------------------------------- |
| 307 // Helper functions. |
| 308 private void reportError(final String errorMessage) { |
| 309 Log.e(TAG, errorMessage); |
| 310 executor.execute(new Runnable() { |
| 311 @Override |
| 312 public void run() { |
| 313 if (roomState != ConnectionState.ERROR) { |
| 314 roomState = ConnectionState.ERROR; |
| 315 events.onChannelError(errorMessage); |
| 316 } |
| 317 } |
| 318 }); |
| 319 } |
| 320 |
| 321 private void sendMessage(final String message) { |
| 322 executor.execute(new Runnable() { |
| 323 @Override |
| 324 public void run() { |
| 325 tcpClient.send(message); |
| 326 } |
| 327 }); |
| 328 } |
| 329 |
| 330 // Put a |key|->|value| mapping in |json|. |
| 331 private static void jsonPut(JSONObject json, String key, Object value) { |
| 332 try { |
| 333 json.put(key, value); |
| 334 } catch (JSONException e) { |
| 335 throw new RuntimeException(e); |
| 336 } |
| 337 } |
| 338 |
| 339 // Converts a Java candidate to a JSONObject. |
| 340 private static JSONObject toJsonCandidate(final IceCandidate candidate) { |
| 341 JSONObject json = new JSONObject(); |
| 342 jsonPut(json, "label", candidate.sdpMLineIndex); |
| 343 jsonPut(json, "id", candidate.sdpMid); |
| 344 jsonPut(json, "candidate", candidate.sdp); |
| 345 return json; |
| 346 } |
| 347 |
| 348 // Converts a JSON candidate to a Java object. |
| 349 private static IceCandidate toJavaCandidate(JSONObject json) throws JSONExcept
ion { |
| 350 return new IceCandidate(json.getString("id"), |
| 351 json.getInt("label"), |
| 352 json.getString("candidate")); |
| 353 } |
| 354 } |
OLD | NEW |