/* * Copyright 2016 The WebRTC Project Authors. All rights reserved. * * 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.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.concurrent.ExecutorService; import java.util.concurrent.Executors; 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 does not support loopback * connections. */ public class DirectRTCClient implements AppRTCClient, TCPChannelClient.TCPChannelEvents { // Regex pattern used for checking if room id looks like an IP. static final Pattern IP_PATTERN = Pattern.compile("(" // 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}:){7}[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}:){7}[0-9a-fA-F]{1,4})|" // Literals + "localhost" + ")" // Optional port number + "(:(\\d+))?"); private static final String TAG = "DirectRTCClient"; private static final int DEFAULT_PORT = 8888; private final ExecutorService executor; private final SignalingEvents events; private TCPChannelClient tcpClient; private RoomConnectionParameters connectionParameters; // 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 = Executors.newSingleThreadExecutor(); roomState = ConnectionState.NEW; } // 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")); } /** * 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; 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(); } }); } /** * 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); if (!matcher.matches()) { reportError("roomId must match IP_PATTERN for DirectRTCClient."); return; } String ip = matcher.group(1); String portStr = matcher.group(matcher.groupCount()); int port; if (portStr != null) { try { port = Integer.parseInt(portStr); } catch (NumberFormatException e) { reportError("Invalid port number: " + portStr); return; } } 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;
}
executor.shutdown();
}
@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());
}
});
}
// -------------------------------------------------------------------
// TCPChannelClient event handlers
@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());
}
});
}
/**
* If the client is the server side, this will trigger onConnectedToRoom.
*/
@Override
public void onTCPConnected(boolean isServer) {
if (isServer) {
roomState = ConnectionState.CONNECTED;
SignalingParameters parameters = new SignalingParameters(
// Ice servers are not needed for direct connections.
new LinkedList