* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
-
package org.appspot.apprtc;
-import android.app.Activity;
-import android.os.AsyncTask;
-import android.util.Log;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
+import org.webrtc.IceCandidate;
import org.webrtc.MediaConstraints;
import org.webrtc.PeerConnection;
+import org.webrtc.SessionDescription;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.net.URLConnection;
-import java.util.LinkedList;
import java.util.List;
-import java.util.Scanner;
-
-/**
- * Negotiates signaling for chatting with apprtc.appspot.com "rooms".
- * Uses the client<->server specifics of the apprtc AppEngine webapp.
- *
- * To use: create an instance of this object (registering a message handler) and
- * call connectToRoom(). Once that's done call sendMessage() and wait for the
- * registered handler to be called with received messages.
- */
-public class AppRTCClient {
- private static final String TAG = "AppRTCClient";
- private GAEChannelClient channelClient;
- private final Activity activity;
- private final GAEChannelClient.MessageHandler gaeHandler;
- private final IceServersObserver iceServersObserver;
-
- // These members are only read/written under sendQueue's lock.
- private LinkedList<String> sendQueue = new LinkedList<String>();
- private AppRTCSignalingParameters appRTCSignalingParameters;
+public interface AppRTCClient {
/**
- * Callback fired once the room's signaling parameters specify the set of
- * ICE servers to use.
+ * Asynchronously connect to an AppRTC room URL, e.g.
+ * https://apprtc.appspot.com/?r=NNN. Once connection is established
+ * onConnectedToRoom() callback with room parameters is invoked.
*/
- public static interface IceServersObserver {
- public void onIceServers(List<PeerConnection.IceServer> iceServers);
- }
-
- public AppRTCClient(
- Activity activity, GAEChannelClient.MessageHandler gaeHandler,
- IceServersObserver iceServersObserver) {
- this.activity = activity;
- this.gaeHandler = gaeHandler;
- this.iceServersObserver = iceServersObserver;
- }
+ public void connectToRoom(String url);
/**
- * Asynchronously connect to an AppRTC room URL, e.g.
- * https://apprtc.appspot.com/?r=NNN and register message-handling callbacks
- * on its GAE Channel.
+ * Send local SDP (offer or answer, depending on role) to the
+ * other participant.
*/
- public void connectToRoom(String url) {
- while (url.indexOf('?') < 0) {
- // Keep redirecting until we get a room number.
- (new RedirectResolver()).execute(url);
- return; // RedirectResolver above calls us back with the next URL.
- }
- (new RoomParameterGetter()).execute(url);
- }
+ public void sendLocalDescription(final SessionDescription sdp);
/**
- * Disconnect from the GAE Channel.
+ * Send Ice candidate to the other participant.
*/
- public void disconnect() {
- if (channelClient != null) {
- channelClient.close();
- channelClient = null;
- }
- }
+ public void sendLocalIceCandidate(final IceCandidate candidate);
/**
- * Queue a message for sending to the room's channel and send it if already
- * connected (other wise queued messages are drained when the channel is
- eventually established).
+ * Disconnect from the channel.
*/
- public synchronized void sendMessage(String msg) {
- synchronized (sendQueue) {
- sendQueue.add(msg);
- }
- requestQueueDrainInBackground();
- }
-
- public boolean isInitiator() {
- return appRTCSignalingParameters.initiator;
- }
-
- public MediaConstraints pcConstraints() {
- return appRTCSignalingParameters.pcConstraints;
- }
-
- public MediaConstraints videoConstraints() {
- return appRTCSignalingParameters.videoConstraints;
- }
-
- public MediaConstraints audioConstraints() {
- return appRTCSignalingParameters.audioConstraints;
- }
+ public void disconnect();
- // Struct holding the signaling parameters of an AppRTC room.
- private class AppRTCSignalingParameters {
+ /**
+ * Struct holding the signaling parameters of an AppRTC room.
+ */
+ public class AppRTCSignalingParameters {
public final List<PeerConnection.IceServer> iceServers;
- public final String gaeBaseHref;
- public final String channelToken;
- public final String postMessageUrl;
public final boolean initiator;
public final MediaConstraints pcConstraints;
public final MediaConstraints videoConstraints;
public AppRTCSignalingParameters(
List<PeerConnection.IceServer> iceServers,
- String gaeBaseHref, String channelToken, String postMessageUrl,
boolean initiator, MediaConstraints pcConstraints,
MediaConstraints videoConstraints, MediaConstraints audioConstraints) {
this.iceServers = iceServers;
- this.gaeBaseHref = gaeBaseHref;
- this.channelToken = channelToken;
- this.postMessageUrl = postMessageUrl;
this.initiator = initiator;
this.pcConstraints = pcConstraints;
this.videoConstraints = videoConstraints;
}
}
- // Load the given URL and return the value of the Location header of the
- // resulting 302 response. If the result is not a 302, throws.
- private class RedirectResolver extends AsyncTask<String, Void, String> {
- @Override
- protected String doInBackground(String... urls) {
- if (urls.length != 1) {
- throw new RuntimeException("Must be called with a single URL");
- }
- try {
- return followRedirect(urls[0]);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
-
- @Override
- protected void onPostExecute(String url) {
- connectToRoom(url);
- }
-
- private String followRedirect(String url) throws IOException {
- HttpURLConnection connection = (HttpURLConnection)
- new URL(url).openConnection();
- connection.setInstanceFollowRedirects(false);
- int code = connection.getResponseCode();
- if (code != HttpURLConnection.HTTP_MOVED_TEMP) {
- throw new IOException("Unexpected response: " + code + " for " + url +
- ", with contents: " + drainStream(connection.getInputStream()));
- }
- int n = 0;
- String name, value;
- while ((name = connection.getHeaderFieldKey(n)) != null) {
- value = connection.getHeaderField(n);
- if (name.equals("Location")) {
- return value;
- }
- ++n;
- }
- throw new IOException("Didn't find Location header!");
- }
- }
-
- // AsyncTask that converts an AppRTC room URL into the set of signaling
- // parameters to use with that room.
- private class RoomParameterGetter
- extends AsyncTask<String, Void, AppRTCSignalingParameters> {
- @Override
- protected AppRTCSignalingParameters doInBackground(String... urls) {
- if (urls.length != 1) {
- throw new RuntimeException("Must be called with a single URL");
- }
- try {
- return getParametersForRoomUrl(urls[0]);
- } catch (JSONException e) {
- throw new RuntimeException(e);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
-
- @Override
- protected void onPostExecute(AppRTCSignalingParameters params) {
- channelClient =
- new GAEChannelClient(activity, params.channelToken, gaeHandler);
- synchronized (sendQueue) {
- appRTCSignalingParameters = params;
- }
- requestQueueDrainInBackground();
- iceServersObserver.onIceServers(appRTCSignalingParameters.iceServers);
- }
-
- // Fetches |url| and fishes the signaling parameters out of the JSON.
- private AppRTCSignalingParameters getParametersForRoomUrl(String url)
- throws IOException, JSONException {
- url = url + "&t=json";
- JSONObject roomJson = new JSONObject(
- drainStream((new URL(url)).openConnection().getInputStream()));
-
- if (roomJson.has("error")) {
- JSONArray errors = roomJson.getJSONArray("error_messages");
- throw new IOException(errors.toString());
- }
-
- String gaeBaseHref = url.substring(0, url.indexOf('?'));
- String token = roomJson.getString("token");
- String postMessageUrl = "/message?r=" +
- roomJson.getString("room_key") + "&u=" +
- roomJson.getString("me");
- boolean initiator = roomJson.getInt("initiator") == 1;
- LinkedList<PeerConnection.IceServer> iceServers =
- iceServersFromPCConfigJSON(roomJson.getString("pc_config"));
-
- boolean isTurnPresent = false;
- for (PeerConnection.IceServer server : iceServers) {
- if (server.uri.startsWith("turn:")) {
- isTurnPresent = true;
- break;
- }
- }
- if (!isTurnPresent) {
- iceServers.add(requestTurnServer(roomJson.getString("turn_url")));
- }
-
- MediaConstraints pcConstraints = constraintsFromJSON(
- roomJson.getString("pc_constraints"));
- addDTLSConstraintIfMissing(pcConstraints);
- Log.d(TAG, "pcConstraints: " + pcConstraints);
- MediaConstraints videoConstraints = constraintsFromJSON(
- getAVConstraints("video",
- roomJson.getString("media_constraints")));
- Log.d(TAG, "videoConstraints: " + videoConstraints);
- MediaConstraints audioConstraints = constraintsFromJSON(
- getAVConstraints("audio",
- roomJson.getString("media_constraints")));
- Log.d(TAG, "audioConstraints: " + audioConstraints);
-
- return new AppRTCSignalingParameters(
- iceServers, gaeBaseHref, token, postMessageUrl, initiator,
- pcConstraints, videoConstraints, audioConstraints);
- }
-
- // Mimic Chrome and set DtlsSrtpKeyAgreement to true if not set to false by
- // the web-app.
- private void addDTLSConstraintIfMissing(
- MediaConstraints pcConstraints) {
- for (MediaConstraints.KeyValuePair pair : pcConstraints.mandatory) {
- if (pair.getKey().equals("DtlsSrtpKeyAgreement")) {
- return;
- }
- }
- for (MediaConstraints.KeyValuePair pair : pcConstraints.optional) {
- if (pair.getKey().equals("DtlsSrtpKeyAgreement")) {
- return;
- }
- }
- // DTLS isn't being suppressed (e.g. for debug=loopback calls), so enable
- // it by default.
- pcConstraints.optional.add(
- new MediaConstraints.KeyValuePair("DtlsSrtpKeyAgreement", "true"));
- }
-
- // Return the constraints specified for |type| of "audio" or "video" in
- // |mediaConstraintsString|.
- private String getAVConstraints(
- String type, String mediaConstraintsString) {
- try {
- JSONObject json = new JSONObject(mediaConstraintsString);
- // Tricksy handling of values that are allowed to be (boolean or
- // MediaTrackConstraints) by the getUserMedia() spec. There are three
- // cases below.
- if (!json.has(type) || !json.optBoolean(type, true)) {
- // Case 1: "audio"/"video" is not present, or is an explicit "false"
- // boolean.
- return null;
- }
- if (json.optBoolean(type, false)) {
- // Case 2: "audio"/"video" is an explicit "true" boolean.
- return "{\"mandatory\": {}, \"optional\": []}";
- }
- // Case 3: "audio"/"video" is an object.
- return json.getJSONObject(type).toString();
- } catch (JSONException e) {
- throw new RuntimeException(e);
- }
- }
-
- private MediaConstraints constraintsFromJSON(String jsonString) {
- if (jsonString == null) {
- return null;
- }
- try {
- MediaConstraints constraints = new MediaConstraints();
- JSONObject json = new JSONObject(jsonString);
- JSONObject mandatoryJSON = json.optJSONObject("mandatory");
- if (mandatoryJSON != null) {
- JSONArray mandatoryKeys = mandatoryJSON.names();
- if (mandatoryKeys != null) {
- for (int i = 0; i < mandatoryKeys.length(); ++i) {
- String key = mandatoryKeys.getString(i);
- String value = mandatoryJSON.getString(key);
- constraints.mandatory.add(
- new MediaConstraints.KeyValuePair(key, value));
- }
- }
- }
- JSONArray optionalJSON = json.optJSONArray("optional");
- if (optionalJSON != null) {
- for (int i = 0; i < optionalJSON.length(); ++i) {
- JSONObject keyValueDict = optionalJSON.getJSONObject(i);
- String key = keyValueDict.names().getString(0);
- String value = keyValueDict.getString(key);
- constraints.optional.add(
- new MediaConstraints.KeyValuePair(key, value));
- }
- }
- return constraints;
- } catch (JSONException e) {
- throw new RuntimeException(e);
- }
- }
-
- // Requests & returns a TURN ICE Server based on a request URL. Must be run
- // off the main thread!
- private PeerConnection.IceServer requestTurnServer(String url) {
- try {
- URLConnection connection = (new URL(url)).openConnection();
- connection.addRequestProperty("user-agent", "Mozilla/5.0");
- connection.addRequestProperty("origin", "https://apprtc.appspot.com");
- String response = drainStream(connection.getInputStream());
- JSONObject responseJSON = new JSONObject(response);
- String uri = responseJSON.getJSONArray("uris").getString(0);
- String username = responseJSON.getString("username");
- String password = responseJSON.getString("password");
- return new PeerConnection.IceServer(uri, username, password);
- } catch (JSONException e) {
- throw new RuntimeException(e);
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- }
- }
-
- // Return the list of ICE servers described by a WebRTCPeerConnection
- // configuration string.
- private LinkedList<PeerConnection.IceServer> iceServersFromPCConfigJSON(
- String pcConfig) {
- try {
- JSONObject json = new JSONObject(pcConfig);
- JSONArray servers = json.getJSONArray("iceServers");
- LinkedList<PeerConnection.IceServer> ret =
- new LinkedList<PeerConnection.IceServer>();
- for (int i = 0; i < servers.length(); ++i) {
- JSONObject server = servers.getJSONObject(i);
- String url = server.getString("urls");
- String credential =
- server.has("credential") ? server.getString("credential") : "";
- ret.add(new PeerConnection.IceServer(url, "", credential));
- }
- return ret;
- } catch (JSONException e) {
- throw new RuntimeException(e);
- }
- }
-
- // Request an attempt to drain the send queue, on a background thread.
- private void requestQueueDrainInBackground() {
- (new AsyncTask<Void, Void, Void>() {
- public Void doInBackground(Void... unused) {
- maybeDrainQueue();
- return null;
- }
- }).execute();
- }
-
- // Send all queued messages if connected to the room.
- private void maybeDrainQueue() {
- synchronized (sendQueue) {
- if (appRTCSignalingParameters == null) {
- return;
- }
- try {
- for (String msg : sendQueue) {
- URLConnection connection = new URL(
- appRTCSignalingParameters.gaeBaseHref +
- appRTCSignalingParameters.postMessageUrl).openConnection();
- connection.setDoOutput(true);
- connection.getOutputStream().write(msg.getBytes("UTF-8"));
- if (!connection.getHeaderField(null).startsWith("HTTP/1.1 200 ")) {
- throw new IOException(
- "Non-200 response to POST: " + connection.getHeaderField(null) +
- " for msg: " + msg);
- }
- }
- } catch (IOException e) {
- throw new RuntimeException(e);
- }
- sendQueue.clear();
- }
- }
-
- // Return the contents of an InputStream as a String.
- private static String drainStream(InputStream in) {
- Scanner s = new Scanner(in).useDelimiter("\\A");
- return s.hasNext() ? s.next() : "";
+ /**
+ * Callback interface for messages delivered on signalling channel.
+ *
+ * Methods are guaranteed to be invoked on the UI thread of |activity|.
+ */
+ public static interface AppRTCSignalingEvents {
+ /**
+ * Callback fired once the room's signaling parameters
+ * AppRTCSignalingParameters are extracted.
+ */
+ public void onConnectedToRoom(final AppRTCSignalingParameters params);
+
+ /**
+ * Callback fired once channel for signaling messages is opened and
+ * ready to receive messages.
+ */
+ public void onChannelOpen();
+
+ /**
+ * Callback fired once remote SDP is received.
+ */
+ public void onRemoteDescription(final SessionDescription sdp);
+
+ /**
+ * Callback fired once remote Ice candidate is received.
+ */
+ public void onRemoteIceCandidate(final IceCandidate candidate);
+
+ /**
+ * Callback fired once channel is closed.
+ */
+ public void onChannelClose();
+
+ /**
+ * Callback fired once channel error happened.
+ */
+ public void onChannelError(int code, String description);
}
}