Index: webrtc/examples/androidapp/src/org/appspot/apprtc/DirectRTCClient.java |
diff --git a/webrtc/examples/androidapp/src/org/appspot/apprtc/DirectRTCClient.java b/webrtc/examples/androidapp/src/org/appspot/apprtc/DirectRTCClient.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..39ae986b11fe6eb556b149a7c8d1f9f26fb7a85e |
--- /dev/null |
+++ b/webrtc/examples/androidapp/src/org/appspot/apprtc/DirectRTCClient.java |
@@ -0,0 +1,336 @@ |
+/* |
+ * Copyright 2015 The WebRTC Project Authors. All rights reserved. |
magjed_webrtc
2016/05/10 14:50:02
nit: 2016
sakal
2016/05/11 08:38:50
Done.
|
+ * |
+ * Use of this source code is governed by a BSD-style license |
+ * that can be found in the LICENSE file in the root of the source |
+ * tree. An additional intellectual property rights grant can be found |
+ * in the file PATENTS. All contributing project authors may |
+ * be found in the AUTHORS file in the root of the source tree. |
+ */ |
+ |
+package org.appspot.apprtc; |
+ |
+import android.util.Log; |
+ |
+import org.appspot.apprtc.util.LooperExecutor; |
+import org.json.JSONArray; |
+import org.json.JSONException; |
+import org.json.JSONObject; |
+import org.webrtc.IceCandidate; |
+import org.webrtc.PeerConnection; |
+import org.webrtc.SessionDescription; |
+ |
+import java.util.LinkedList; |
+import java.util.regex.Matcher; |
+import java.util.regex.Pattern; |
+ |
+/** |
+ * Implementation of AppRTCClient that uses direct TCP connection as the signaling channel. |
+ * This eliminates the need for an external server. This class should not be used for loopback |
+ * connections. They are not supported. |
magjed_webrtc
2016/05/10 14:50:02
nit: Merge last two sentences to something like: "
sakal
2016/05/11 08:38:50
Done.
|
+ */ |
+public class DirectRTCClient implements AppRTCClient, TCPChannelClient.TCPChannelEvents { |
+ private static final String TAG = "DirectRTCClient"; |
+ private static final int DEFAULT_PORT = 8888; |
+ |
+ // Regex pattern used for checking if room id looks like an IP. |
+ static final Pattern IP_PATTERN = Pattern.compile( |
+ "(" |
magjed_webrtc
2016/05/10 14:50:02
nit: 4 space indent. Same for other places as well
sakal
2016/05/11 08:38:49
Done.
|
+ // IPv4 |
+ + "((\\d+\\.){3}\\d+)|" |
+ // IPv6 |
+ + "\\[((([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?::" |
+ + "(([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?)\\]|" |
+ + "\\[(([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})\\]|" |
+ // IPv6 without [] |
+ + "((([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?::(([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})?)|" |
+ + "(([0-9a-fA-F]{1,4}:)*[0-9a-fA-F]{1,4})|" |
+ // Literals |
+ + "localhost" |
+ + ")" |
+ // Optional port number |
+ + "(:\\d+)?" |
+ ); |
+ |
+ private final LooperExecutor executor; |
+ private final SignalingEvents events; |
+ private TCPChannelClient tcpClient; |
+ private RoomConnectionParameters connectionParameters; |
+ |
+ private enum ConnectionState { |
+ NEW, CONNECTED, CLOSED, ERROR |
+ }; |
+ |
+ // All alterations of the room state should be done from inside the looper thread |
+ private ConnectionState roomState; |
+ |
+ public DirectRTCClient(SignalingEvents events) { |
+ this.events = events; |
+ executor = new LooperExecutor(); |
+ |
+ executor.requestStart(); |
+ roomState = ConnectionState.NEW; |
+ } |
+ |
+ /** |
+ * Connects to the room, roomId in connectionsParameters is required. roomId must be a valid |
+ * IP address matching IP_PATTERN. |
+ */ |
+ @Override |
+ public void connectToRoom(RoomConnectionParameters connectionParameters) { |
+ this.connectionParameters = connectionParameters; |
+ |
+ // Loopback connections should always use the WebSocketRTCClient |
magjed_webrtc
2016/05/10 14:50:02
nit: The code is very clear, so I would just remov
sakal
2016/05/11 08:38:50
Done.
|
+ // They are not supported by this client |
+ if (connectionParameters.loopback) { |
+ reportError("Loopback connections aren't supported by DirectRTCClient."); |
+ } |
+ |
+ executor.execute(new Runnable() { |
+ @Override |
+ public void run() { |
+ connectToRoomInternal(); |
+ } |
+ }); |
+ } |
+ |
+ @Override |
+ public void disconnectFromRoom() { |
+ executor.execute(new Runnable() { |
+ @Override |
+ public void run() { |
+ disconnectFromRoomInternal(); |
+ } |
+ }); |
+ executor.requestStop(); |
+ } |
+ |
+ /** |
+ * Connects to the room. |
+ * |
+ * Runs on the looper thread. |
+ */ |
+ private void connectToRoomInternal() { |
+ this.roomState = ConnectionState.NEW; |
+ |
+ String endpoint = connectionParameters.roomId; |
+ |
+ Matcher matcher = IP_PATTERN.matcher(endpoint); |
+ matcher.matches(); |
magjed_webrtc
2016/05/10 14:50:02
Check if matches() returns false and/or check Ille
sakal
2016/05/11 08:38:50
Done.
|
+ |
+ String ip = matcher.group(1); |
+ String portStr = endpoint.substring(ip.length()); |
magjed_webrtc
2016/05/10 14:50:02
nit: I think it's a little bit clearer if you use
sakal
2016/05/11 08:38:49
matcher.group(2) would returns some part inside th
|
+ int port; |
+ |
+ if (!portStr.isEmpty()) { |
+ port = Integer.parseInt(portStr.substring(1)); |
+ } else { |
+ port = DEFAULT_PORT; |
+ } |
+ |
+ tcpClient = new TCPChannelClient(executor, this, ip, port); |
+ } |
+ |
+ /** |
+ * Disconnects from the room. |
+ * |
+ * Runs on the looper thread. |
+ */ |
+ private void disconnectFromRoomInternal() { |
+ roomState = ConnectionState.CLOSED; |
+ |
+ if (tcpClient != null) { |
+ tcpClient.disconnect(); |
+ tcpClient = null; |
+ } |
+ } |
+ |
+ @Override |
+ public void sendOfferSdp(final SessionDescription sdp) { |
+ executor.execute(new Runnable() { |
+ @Override |
+ public void run() { |
+ if (roomState != ConnectionState.CONNECTED) { |
+ reportError("Sending offer SDP in non connected state."); |
+ return; |
+ } |
+ JSONObject json = new JSONObject(); |
+ jsonPut(json, "sdp", sdp.description); |
+ jsonPut(json, "type", "offer"); |
+ sendMessage(json.toString()); |
+ } |
+ }); |
+ } |
+ |
+ @Override |
+ public void sendAnswerSdp(final SessionDescription sdp) { |
+ executor.execute(new Runnable() { |
+ @Override |
+ public void run() { |
+ JSONObject json = new JSONObject(); |
+ jsonPut(json, "sdp", sdp.description); |
+ jsonPut(json, "type", "answer"); |
+ sendMessage(json.toString()); |
+ } |
+ }); |
+ } |
+ |
+ @Override |
+ public void sendLocalIceCandidate(final IceCandidate candidate) { |
+ executor.execute(new Runnable() { |
+ @Override |
+ public void run() { |
+ JSONObject json = new JSONObject(); |
+ jsonPut(json, "type", "candidate"); |
+ jsonPut(json, "label", candidate.sdpMLineIndex); |
+ jsonPut(json, "id", candidate.sdpMid); |
+ jsonPut(json, "candidate", candidate.sdp); |
+ |
+ if (roomState != ConnectionState.CONNECTED) { |
+ reportError("Sending ICE candidate in non connected state."); |
+ return; |
+ } |
+ sendMessage(json.toString()); |
+ } |
+ }); |
+ } |
+ |
+ /** Send removed Ice candidates to the other participant. */ |
+ @Override |
+ public void sendLocalIceCandidateRemovals(final IceCandidate[] candidates) { |
+ executor.execute(new Runnable() { |
+ @Override |
+ public void run() { |
+ JSONObject json = new JSONObject(); |
+ jsonPut(json, "type", "remove-candidates"); |
+ JSONArray jsonArray = new JSONArray(); |
+ for (final IceCandidate candidate : candidates) { |
+ jsonArray.put(toJsonCandidate(candidate)); |
+ } |
+ jsonPut(json, "candidates", jsonArray); |
+ |
+ if (roomState != ConnectionState.CONNECTED) { |
+ reportError("Sending ICE candidate removals in non connected state."); |
+ return; |
+ } |
+ sendMessage(json.toString()); |
+ } |
+ }); |
+ } |
+ |
+ // ------------------------------------------------------------------- |
+ // TCPChannelClient event handlers |
+ |
+ /** |
+ * If the client is the server side, this will trigger onConnectedToRoom. |
+ * |
+ * @param isServer |
+ */ |
+ @Override |
+ public void onTCPConnected(boolean isServer) { |
+ if (isServer) { |
+ roomState = ConnectionState.CONNECTED; |
+ |
+ SignalingParameters parameters = new SignalingParameters( |
+ new LinkedList<PeerConnection.IceServer>(), isServer, null, null, null, null, null); |
magjed_webrtc
2016/05/10 14:50:02
nit: Add comments about the null parameters, e.g.
sakal
2016/05/11 08:38:50
Done.
|
+ events.onConnectedToRoom(parameters); |
+ } |
+ } |
+ |
+ @Override |
+ public void onTCPMessage(String msg) { |
+ try { |
+ JSONObject json = new JSONObject(msg); |
+ String type = json.optString("type"); |
+ if (type.equals("candidate")) { |
+ events.onRemoteIceCandidate(toJavaCandidate(json)); |
+ } else if (type.equals("remove-candidates")) { |
+ JSONArray candidateArray = json.getJSONArray("candidates"); |
+ IceCandidate[] candidates = new IceCandidate[candidateArray.length()]; |
+ for (int i = 0; i < candidateArray.length(); ++i) { |
+ candidates[i] = toJavaCandidate(candidateArray.getJSONObject(i)); |
+ } |
+ events.onRemoteIceCandidatesRemoved(candidates); |
+ } else if (type.equals("answer")) { |
+ SessionDescription sdp = new SessionDescription( |
+ SessionDescription.Type.fromCanonicalForm(type), |
+ json.getString("sdp")); |
+ events.onRemoteDescription(sdp); |
+ } else if (type.equals("offer")) { |
+ SessionDescription sdp = new SessionDescription( |
+ SessionDescription.Type.fromCanonicalForm(type), |
+ json.getString("sdp")); |
+ |
+ SignalingParameters parameters = new SignalingParameters( |
+ new LinkedList<PeerConnection.IceServer>(), false, null, null, null, sdp, null); |
magjed_webrtc
2016/05/10 14:50:02
ditto: Add comments about the null parameters, e.g
sakal
2016/05/11 08:38:49
Done.
|
+ roomState = ConnectionState.CONNECTED; |
+ events.onConnectedToRoom(parameters); |
+ } else if (type.equals("bye")) { |
+ events.onChannelClose(); |
+ } else { |
+ reportError("Unexpected TCP message: " + msg); |
+ } |
+ } catch (JSONException e) { |
+ reportError("TCP message JSON parsing error: " + e.toString()); |
+ } |
+ } |
+ |
+ @Override |
+ public void onTCPError(String description) { |
+ reportError("TCP connection error: " + description); |
+ } |
+ |
+ @Override |
+ public void onTCPClose() { |
+ events.onChannelClose(); |
+ } |
+ |
+ // -------------------------------------------------------------------- |
+ // Helper functions. |
+ private void reportError(final String errorMessage) { |
+ Log.e(TAG, errorMessage); |
+ executor.execute(new Runnable() { |
+ @Override |
+ public void run() { |
+ if (roomState != ConnectionState.ERROR) { |
+ roomState = ConnectionState.ERROR; |
+ events.onChannelError(errorMessage); |
+ } |
+ } |
+ }); |
+ } |
+ |
+ private void sendMessage(final String message) { |
+ executor.execute(new Runnable() { |
+ @Override |
+ public void run() { |
+ tcpClient.send(message); |
+ } |
+ }); |
+ } |
+ |
+ // Put a |key|->|value| mapping in |json|. |
+ private static void jsonPut(JSONObject json, String key, Object value) { |
+ try { |
+ json.put(key, value); |
+ } catch (JSONException e) { |
+ throw new RuntimeException(e); |
+ } |
+ } |
+ |
+ // Converts a Java candidate to a JSONObject. |
+ private static JSONObject toJsonCandidate(final IceCandidate candidate) { |
+ JSONObject json = new JSONObject(); |
+ jsonPut(json, "label", candidate.sdpMLineIndex); |
+ jsonPut(json, "id", candidate.sdpMid); |
+ jsonPut(json, "candidate", candidate.sdp); |
+ return json; |
+ } |
+ |
+ // Converts a JSON candidate to a Java object. |
+ private static IceCandidate toJavaCandidate(JSONObject json) throws JSONException { |
+ return new IceCandidate(json.getString("id"), |
+ json.getInt("label"), |
+ json.getString("candidate")); |
+ } |
+} |