diff --git a/talk/examples/android/res/values/strings.xml b/talk/examples/android/res/values/strings.xml
index 774eee1c9..e96d6bfe0 100644
--- a/talk/examples/android/res/values/strings.xml
+++ b/talk/examples/android/res/values/strings.xml
@@ -27,11 +27,6 @@
room_preference
room_list_preference
- url_preference
- Connection URL:
- Enter AppRTC connection server URL.
- https://apprtc.appspot.com
-
resolution_preference
Video resolution.
Enter AppRTC local video resolution.
diff --git a/talk/examples/android/res/xml/preferences.xml b/talk/examples/android/res/xml/preferences.xml
index b8c08bbec..c5c3a1e9e 100644
--- a/talk/examples/android/res/xml/preferences.xml
+++ b/talk/examples/android/res/xml/preferences.xml
@@ -1,11 +1,5 @@
-
iceServers;
public final boolean initiator;
public final MediaConstraints pcConstraints;
public final MediaConstraints videoConstraints;
public final MediaConstraints audioConstraints;
+ public final String postMessageUrl;
+ public final String roomId;
+ public final String clientId;
+ public final String channelToken;
+ public final String offerSdp;
- public AppRTCSignalingParameters(
+ public SignalingParameters(
List iceServers,
boolean initiator, MediaConstraints pcConstraints,
- MediaConstraints videoConstraints, MediaConstraints audioConstraints) {
+ MediaConstraints videoConstraints, MediaConstraints audioConstraints,
+ String postMessageUrl, String roomId, String clientId,
+ String channelToken, String offerSdp ) {
this.iceServers = iceServers;
this.initiator = initiator;
this.pcConstraints = pcConstraints;
this.videoConstraints = videoConstraints;
this.audioConstraints = audioConstraints;
+ this.postMessageUrl = postMessageUrl;
+ this.roomId = roomId;
+ this.clientId = clientId;
+ this.channelToken = channelToken;
+ this.offerSdp = offerSdp;
+ if (channelToken == null || channelToken.length() == 0) {
+ this.websocketSignaling = true;
+ } else {
+ this.websocketSignaling = false;
+ }
}
}
/**
- * Callback interface for messages delivered on signalling channel.
+ * Callback interface for messages delivered on signaling channel.
*
* Methods are guaranteed to be invoked on the UI thread of |activity|.
*/
- public static interface AppRTCSignalingEvents {
+ public static interface SignalingEvents {
/**
* Callback fired once the room's signaling parameters
- * AppRTCSignalingParameters are extracted.
+ * SignalingParameters are extracted.
*/
- public void onConnectedToRoom(final AppRTCSignalingParameters params);
+ public void onConnectedToRoom(final SignalingParameters params);
/**
* Callback fired once channel for signaling messages is opened and
@@ -115,6 +137,6 @@ public interface AppRTCClient {
/**
* Callback fired once channel error happened.
*/
- public void onChannelError(int code, String description);
+ public void onChannelError(final String description);
}
}
diff --git a/talk/examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java b/talk/examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java
index 3ad26afca..7facd3c85 100644
--- a/talk/examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java
+++ b/talk/examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java
@@ -49,7 +49,7 @@ import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
-import org.appspot.apprtc.AppRTCClient.AppRTCSignalingParameters;
+import org.appspot.apprtc.AppRTCClient.SignalingParameters;
import org.webrtc.IceCandidate;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.SessionDescription;
@@ -60,17 +60,18 @@ import org.webrtc.VideoRendererGui;
import org.webrtc.VideoRendererGui.ScalingType;
/**
- * Main Activity of the AppRTCDemo Android app demonstrating interoperability
+ * Activity of the AppRTCDemo Android app demonstrating interoperability
* between the Android/Java implementation of PeerConnection and the
* apprtc.appspot.com demo webapp.
*/
public class AppRTCDemoActivity extends Activity
- implements AppRTCClient.AppRTCSignalingEvents,
+ implements AppRTCClient.SignalingEvents,
PeerConnectionClient.PeerConnectionEvents {
private static final String TAG = "AppRTCClient";
+ private final boolean USE_WEBSOCKETS = false;
private PeerConnectionClient pc;
- private AppRTCClient appRtcClient = new GAERTCClient(this, this);
- private AppRTCSignalingParameters appRtcParameters;
+ private AppRTCClient appRtcClient;
+ private SignalingParameters signalingParameters;
private AppRTCAudioManager audioManager = null;
private View rootView;
private View menuBar;
@@ -199,6 +200,11 @@ public class AppRTCDemoActivity extends Activity
if ((room != null && !room.equals("")) ||
(loopback != null && loopback.equals("loopback"))) {
logAndToast(getString(R.string.connecting_to, url));
+ if (USE_WEBSOCKETS) {
+ appRtcClient = new WebSocketRTCClient(this);
+ } else {
+ appRtcClient = new GAERTCClient(this, this);
+ }
appRtcClient.connectToRoom(url.toString());
if (room != null && !room.equals("")) {
roomName.setText(room);
@@ -324,7 +330,7 @@ public class AppRTCDemoActivity extends Activity
finish();
}
- private void disconnectWithMessage(String errorMessage) {
+ private void disconnectWithMessage(final String errorMessage) {
new AlertDialog.Builder(this)
.setTitle(getText(R.string.channel_error_title))
.setMessage(errorMessage)
@@ -357,20 +363,20 @@ public class AppRTCDemoActivity extends Activity
// -----Implementation of AppRTCClient.AppRTCSignalingEvents ---------------
// All events are called from UI thread.
@Override
- public void onConnectedToRoom(final AppRTCSignalingParameters params) {
+ public void onConnectedToRoom(final SignalingParameters params) {
if (audioManager != null) {
// Store existing audio settings and change audio mode to
// MODE_IN_COMMUNICATION for best possible VoIP performance.
logAndToast("Initializing the audio manager...");
audioManager.init();
}
- appRtcParameters = params;
+ signalingParameters = params;
abortUnless(PeerConnectionFactory.initializeAndroidGlobals(
this, true, true, VideoRendererGui.getEGLContext()),
"Failed to initializeAndroidGlobals");
logAndToast("Creating peer connection...");
pc = new PeerConnectionClient(
- this, localRender, remoteRender, appRtcParameters, this);
+ this, localRender, remoteRender, signalingParameters, this);
if (pc.isHDVideo()) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
} else {
@@ -417,7 +423,7 @@ public class AppRTCDemoActivity extends Activity
if (pc == null) {
return;
}
- if (appRtcParameters.initiator) {
+ if (signalingParameters.initiator) {
logAndToast("Creating OFFER...");
// Create offer. Offer SDP will be sent to answering client in
// PeerConnectionEvents.onLocalDescription event.
@@ -432,7 +438,7 @@ public class AppRTCDemoActivity extends Activity
}
logAndToast("Received remote " + sdp.type + " ...");
pc.setRemoteDescription(sdp);
- if (!appRtcParameters.initiator) {
+ if (!signalingParameters.initiator) {
logAndToast("Creating ANSWER...");
// Create answer. Answer SDP will be sent to offering client in
// PeerConnectionEvents.onLocalDescription event.
@@ -454,7 +460,7 @@ public class AppRTCDemoActivity extends Activity
}
@Override
- public void onChannelError(int code, String description) {
+ public void onChannelError(final String description) {
disconnectWithMessage(description);
}
@@ -465,7 +471,11 @@ public class AppRTCDemoActivity extends Activity
public void onLocalDescription(final SessionDescription sdp) {
if (appRtcClient != null) {
logAndToast("Sending " + sdp.type + " ...");
- appRtcClient.sendLocalDescription(sdp);
+ if (signalingParameters.initiator) {
+ appRtcClient.sendOfferSdp(sdp);
+ } else {
+ appRtcClient.sendAnswerSdp(sdp);
+ }
}
}
diff --git a/talk/examples/android/src/org/appspot/apprtc/ConnectActivity.java b/talk/examples/android/src/org/appspot/apprtc/ConnectActivity.java
index ce99bbf33..ff78ebce3 100644
--- a/talk/examples/android/src/org/appspot/apprtc/ConnectActivity.java
+++ b/talk/examples/android/src/org/appspot/apprtc/ConnectActivity.java
@@ -63,6 +63,10 @@ import org.webrtc.MediaCodecVideoEncoder;
public class ConnectActivity extends Activity {
private static final String TAG = "ConnectActivity";
+ private final boolean USE_WEBSOCKETS = false;
+ private final String APPRTC_SERVER = "https://apprtc.appspot.com";
+ private final String APPRTC_WS_SERVER = "https://8-dot-apprtc.appspot.com";
+
private ImageButton addRoomButton;
private ImageButton removeRoomButton;
private ImageButton connectButton;
@@ -70,7 +74,6 @@ public class ConnectActivity extends Activity {
private EditText roomEditText;
private ListView roomListView;
private SharedPreferences sharedPref;
- private String keyprefUrl;
private String keyprefResolution;
private String keyprefFps;
private String keyprefCpuUsageDetection;
@@ -86,7 +89,6 @@ public class ConnectActivity extends Activity {
// Get setting keys.
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
- keyprefUrl = getString(R.string.pref_url_key);
keyprefResolution = getString(R.string.pref_resolution_key);
keyprefFps = getString(R.string.pref_fps_key);
keyprefCpuUsageDetection = getString(R.string.pref_cpu_usage_detection_key);
@@ -193,9 +195,11 @@ public class ConnectActivity extends Activity {
if (view.getId() == R.id.connect_loopback_button) {
loopback = true;
}
- String url = sharedPref.getString(keyprefUrl,
- getString(R.string.pref_url_default));
- if (loopback) {
+ String url = APPRTC_SERVER;
+ if (USE_WEBSOCKETS) {
+ url = APPRTC_WS_SERVER;
+ }
+ if (loopback && !USE_WEBSOCKETS) {
url += "/?debug=loopback";
} else {
String roomName = getSelectedItem();
diff --git a/talk/examples/android/src/org/appspot/apprtc/GAEChannelClient.java b/talk/examples/android/src/org/appspot/apprtc/GAEChannelClient.java
index 5fd0a54ed..d44975b7b 100644
--- a/talk/examples/android/src/org/appspot/apprtc/GAEChannelClient.java
+++ b/talk/examples/android/src/org/appspot/apprtc/GAEChannelClient.java
@@ -141,6 +141,8 @@ public class GAEChannelClient {
@JavascriptInterface
public void onError(final int code, final String description) {
+ Log.e(TAG, "Channel error. Code: " + code +
+ ". Description: " + description);
if (!disconnected) {
handler.onError(code, description);
}
diff --git a/talk/examples/android/src/org/appspot/apprtc/GAERTCClient.java b/talk/examples/android/src/org/appspot/apprtc/GAERTCClient.java
index c3d95641d..2a1e8a1fd 100644
--- a/talk/examples/android/src/org/appspot/apprtc/GAERTCClient.java
+++ b/talk/examples/android/src/org/appspot/apprtc/GAERTCClient.java
@@ -30,20 +30,16 @@ import android.app.Activity;
import android.os.AsyncTask;
import android.util.Log;
-import org.json.JSONArray;
+import org.appspot.apprtc.RoomParametersFetcher.RoomParametersFetcherEvents;
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.URL;
import java.net.URLConnection;
import java.util.LinkedList;
-import java.util.Scanner;
/**
* Negotiates signaling for chatting with apprtc.appspot.com "rooms".
@@ -55,25 +51,23 @@ import java.util.Scanner;
* Messages to other party (with local Ice candidates and SDP) can
* be sent after GAE channel is opened and onChannelOpen() callback is invoked.
*/
-public class GAERTCClient implements AppRTCClient {
+public class GAERTCClient implements AppRTCClient, RoomParametersFetcherEvents {
private static final String TAG = "GAERTCClient";
private GAEChannelClient channelClient;
private final Activity activity;
- private AppRTCClient.AppRTCSignalingEvents events;
- private final GAEChannelClient.GAEMessageHandler gaeHandler =
- new GAEHandler();
- private AppRTCClient.AppRTCSignalingParameters appRTCSignalingParameters;
- private String gaeBaseHref;
- private String channelToken;
- private String postMessageUrl;
+ private SignalingEvents events;
+ private GAEChannelClient.GAEMessageHandler gaeHandler;
+ private SignalingParameters signalingParameters;
+ private RoomParametersFetcher fetcher;
private LinkedList sendQueue = new LinkedList();
- public GAERTCClient(Activity activity,
- AppRTCClient.AppRTCSignalingEvents events) {
+ public GAERTCClient(Activity activity, SignalingEvents events) {
this.activity = activity;
this.events = events;
}
+ // --------------------------------------------------------------------
+ // AppRTCClient interface implementation.
/**
* Asynchronously connect to an AppRTC room URL, e.g.
* https://apprtc.appspot.com/?r=NNN and register message-handling callbacks
@@ -81,7 +75,8 @@ public class GAERTCClient implements AppRTCClient {
*/
@Override
public void connectToRoom(String url) {
- (new RoomParameterGetter()).execute(url);
+ fetcher = new RoomParametersFetcher(this);
+ fetcher.execute(url);
}
/**
@@ -94,6 +89,7 @@ public class GAERTCClient implements AppRTCClient {
sendMessage("{\"type\": \"bye\"}");
channelClient.close();
channelClient = null;
+ gaeHandler = null;
}
}
@@ -105,9 +101,17 @@ public class GAERTCClient implements AppRTCClient {
* we might want to filter elsewhere.
*/
@Override
- public void sendLocalDescription(final SessionDescription sdp) {
+ public void sendOfferSdp(final SessionDescription sdp) {
JSONObject json = new JSONObject();
- jsonPut(json, "type", sdp.type.canonicalForm());
+ jsonPut(json, "type", "offer");
+ jsonPut(json, "sdp", sdp.description);
+ sendMessage(json.toString());
+ }
+
+ @Override
+ public void sendAnswerSdp(final SessionDescription sdp) {
+ JSONObject json = new JSONObject();
+ jsonPut(json, "type", "answer");
jsonPut(json, "sdp", sdp.description);
sendMessage(json.toString());
}
@@ -125,7 +129,6 @@ public class GAERTCClient implements AppRTCClient {
sendMessage(json.toString());
}
-
// 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).
@@ -145,223 +148,6 @@ public class GAERTCClient implements AppRTCClient {
}
}
- // AsyncTask that converts an AppRTC room URL into the set of signaling
- // parameters to use with that room.
- private class RoomParameterGetter
- extends AsyncTask {
- private Exception exception = null;
-
- @Override
- protected AppRTCSignalingParameters doInBackground(String... urls) {
- if (urls.length != 1) {
- exception = new RuntimeException("Must be called with a single URL");
- return null;
- }
- try {
- exception = null;
- return getParametersForRoomUrl(urls[0]);
- } catch (JSONException e) {
- exception = e;
- } catch (IOException e) {
- exception = e;
- }
- return null;
- }
-
- @Override
- protected void onPostExecute(AppRTCSignalingParameters params) {
- if (exception != null) {
- Log.e(TAG, "Room connection error: " + exception.toString());
- events.onChannelError(0, exception.getMessage());
- return;
- }
- channelClient =
- new GAEChannelClient(activity, channelToken, gaeHandler);
- synchronized (sendQueue) {
- appRTCSignalingParameters = params;
- }
- requestQueueDrainInBackground();
- events.onConnectedToRoom(appRTCSignalingParameters);
- }
-
- // Fetches |url| and fishes the signaling parameters out of the JSON.
- private AppRTCSignalingParameters getParametersForRoomUrl(String url)
- throws IOException, JSONException {
- url = url + "&t=json";
- String response = drainStream((new URL(url)).openConnection().getInputStream());
- Log.d(TAG, "Room response: " + response);
- JSONObject roomJson = new JSONObject(response);
-
- if (roomJson.has("error")) {
- JSONArray errors = roomJson.getJSONArray("error_messages");
- throw new IOException(errors.toString());
- }
-
- gaeBaseHref = url.substring(0, url.indexOf('?'));
- channelToken = roomJson.getString("token");
- postMessageUrl = "/message?r=" +
- roomJson.getString("room_key") + "&u=" +
- roomJson.getString("me");
- boolean initiator = roomJson.getInt("initiator") == 1;
- LinkedList iceServers =
- iceServersFromPCConfigJSON(roomJson.getString("pc_config"));
-
- boolean isTurnPresent = false;
- for (PeerConnection.IceServer server : iceServers) {
- Log.d(TAG, "IceServer: " + server);
- if (server.uri.startsWith("turn:")) {
- isTurnPresent = true;
- break;
- }
- }
- if (!isTurnPresent) {
- PeerConnection.IceServer server =
- requestTurnServer(roomJson.getString("turn_url"));
- Log.d(TAG, "TurnServer: " + server);
- iceServers.add(server);
- }
-
- 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, 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 iceServersFromPCConfigJSON(
- String pcConfig) {
- try {
- JSONObject json = new JSONObject(pcConfig);
- JSONArray servers = json.getJSONArray("iceServers");
- LinkedList ret =
- new LinkedList();
- 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() {
@@ -375,37 +161,68 @@ public class GAERTCClient implements AppRTCClient {
// Send all queued messages if connected to the room.
private void maybeDrainQueue() {
synchronized (sendQueue) {
- if (appRTCSignalingParameters == null) {
+ if (signalingParameters == null) {
return;
}
try {
for (String msg : sendQueue) {
Log.d(TAG, "SEND: " + msg);
- URLConnection connection =
- new URL(gaeBaseHref + postMessageUrl).openConnection();
+ URLConnection connection = new URL(
+ signalingParameters.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);
+ String errorMessage = "Non-200 response to POST: " +
+ connection.getHeaderField(null) + " for msg: " + msg;
+ reportChannelError(errorMessage);
}
}
} catch (IOException e) {
- throw new RuntimeException(e);
+ reportChannelError("GAE Post error: " + e.getMessage());
}
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() : "";
+ private void reportChannelError(final String errorMessage) {
+ Log.e(TAG, errorMessage);
+ activity.runOnUiThread(new Runnable() {
+ public void run() {
+ events.onChannelError(errorMessage);
+ }
+ });
}
+ // --------------------------------------------------------------------
+ // RoomConnectionEvents interface implementation.
+ // All events are called on UI thread.
+ @Override
+ public void onSignalingParametersReady(final SignalingParameters params) {
+ Log.d(TAG, "Room signaling parameters ready.");
+ if (params.websocketSignaling) {
+ reportChannelError("Room does not support GAE channel signaling.");
+ return;
+ }
+ gaeHandler = new GAEHandler();
+ channelClient =
+ new GAEChannelClient(activity, params.channelToken, gaeHandler);
+ synchronized (sendQueue) {
+ signalingParameters = params;
+ }
+ requestQueueDrainInBackground();
+ events.onConnectedToRoom(signalingParameters);
+ }
+
+ @Override
+ public void onSignalingParametersError(final String description) {
+ reportChannelError("Room connection error: " + description);
+ }
+
+
+ // --------------------------------------------------------------------
+ // GAEMessageHandler interface implementation.
// Implementation detail: handler for receiving GAE messages and dispatching
- // them appropriately.
+ // them appropriately. All dispatched messages are called from UI thread.
private class GAEHandler implements GAEChannelClient.GAEMessageHandler {
private boolean channelOpen = false;
@@ -442,10 +259,10 @@ public class GAERTCClient implements AppRTCClient {
} else if (type.equals("bye")) {
events.onChannelClose();
} else {
- events.onChannelError(1, "Unexpected channel message: " + msg);
+ reportChannelError("Unexpected channel message: " + msg);
}
} catch (JSONException e) {
- events.onChannelError(1, "Channel message JSON parsing error: " +
+ reportChannelError("Channel message JSON parsing error: " +
e.toString());
}
}
@@ -462,12 +279,9 @@ public class GAERTCClient implements AppRTCClient {
}
public void onError(final int code, final String description) {
- activity.runOnUiThread(new Runnable() {
- public void run() {
- events.onChannelError(code, description);
- channelOpen = false;
- }
- });
+ channelOpen = false;
+ reportChannelError("GAE Handler error. Code: " + code +
+ ". " + description);
}
}
diff --git a/talk/examples/android/src/org/appspot/apprtc/PeerConnectionClient.java b/talk/examples/android/src/org/appspot/apprtc/PeerConnectionClient.java
index 9c917bbbb..b005de7b3 100644
--- a/talk/examples/android/src/org/appspot/apprtc/PeerConnectionClient.java
+++ b/talk/examples/android/src/org/appspot/apprtc/PeerConnectionClient.java
@@ -30,7 +30,7 @@ package org.appspot.apprtc;
import android.app.Activity;
import android.util.Log;
-import org.appspot.apprtc.AppRTCClient.AppRTCSignalingParameters;
+import org.appspot.apprtc.AppRTCClient.SignalingParameters;
import org.webrtc.DataChannel;
import org.webrtc.IceCandidate;
import org.webrtc.MediaConstraints;
@@ -53,7 +53,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class PeerConnectionClient {
- private static final String TAG = "RTCClient";
+ private static final String TAG = "PCRTCClient";
private final Activity activity;
private PeerConnectionFactory factory;
private PeerConnection pc;
@@ -63,8 +63,11 @@ public class PeerConnectionClient {
private final SDPObserver sdpObserver = new SDPObserver();
private final VideoRenderer.Callbacks localRender;
private final VideoRenderer.Callbacks remoteRender;
- private LinkedList queuedRemoteCandidates =
- new LinkedList();
+ // Queued remote ICE candidates are consumed only after both local and
+ // remote descriptions are set. Similarly local ICE candidates are sent to
+ // remote peer after both local and remote description are set.
+ private LinkedList queuedRemoteCandidates = null;
+ private LinkedList queuedLocalCandidates = null;
private MediaConstraints sdpMediaConstraints;
private MediaConstraints videoConstraints;
private PeerConnectionEvents events;
@@ -77,26 +80,28 @@ public class PeerConnectionClient {
Activity activity,
VideoRenderer.Callbacks localRender,
VideoRenderer.Callbacks remoteRender,
- AppRTCSignalingParameters appRtcParameters,
+ SignalingParameters signalingParameters,
PeerConnectionEvents events) {
this.activity = activity;
this.localRender = localRender;
this.remoteRender = remoteRender;
this.events = events;
- isInitiator = appRtcParameters.initiator;
+ isInitiator = signalingParameters.initiator;
+ queuedRemoteCandidates = new LinkedList();
+ queuedLocalCandidates = new LinkedList();
sdpMediaConstraints = new MediaConstraints();
sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
"OfferToReceiveAudio", "true"));
sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
"OfferToReceiveVideo", "true"));
- videoConstraints = appRtcParameters.videoConstraints;
+ videoConstraints = signalingParameters.videoConstraints;
factory = new PeerConnectionFactory();
- MediaConstraints pcConstraints = appRtcParameters.pcConstraints;
+ MediaConstraints pcConstraints = signalingParameters.pcConstraints;
pcConstraints.optional.add(
new MediaConstraints.KeyValuePair("RtpDataChannels", "true"));
- pc = factory.createPeerConnection(appRtcParameters.iceServers,
+ pc = factory.createPeerConnection(signalingParameters.iceServers,
pcConstraints, pcObserver);
isInitiator = false;
@@ -113,11 +118,11 @@ public class PeerConnectionClient {
pc.addStream(videoMediaStream);
}
- if (appRtcParameters.audioConstraints != null) {
+ if (signalingParameters.audioConstraints != null) {
MediaStream lMS = factory.createLocalMediaStream("ARDAMSAudio");
lMS.addTrack(factory.createAudioTrack(
"ARDAMSa0",
- factory.createAudioSource(appRtcParameters.audioConstraints)));
+ factory.createAudioSource(signalingParameters.audioConstraints)));
pc.addStream(lMS);
}
}
@@ -176,7 +181,6 @@ public class PeerConnectionClient {
});
}
-
public void addRemoteIceCandidate(final IceCandidate candidate) {
activity.runOnUiThread(new Runnable() {
public void run() {
@@ -379,11 +383,21 @@ public class PeerConnectionClient {
return newSdpDescription.toString();
}
- private void drainRemoteCandidates() {
- for (IceCandidate candidate : queuedRemoteCandidates) {
- pc.addIceCandidate(candidate);
+ private void drainCandidates() {
+ if (queuedLocalCandidates != null) {
+ Log.d(TAG, "Send " + queuedLocalCandidates.size() + " local candidates");
+ for (IceCandidate candidate : queuedLocalCandidates) {
+ events.onIceCandidate(candidate);
+ }
+ queuedLocalCandidates = null;
+ }
+ if (queuedRemoteCandidates != null) {
+ Log.d(TAG, "Add " + queuedRemoteCandidates.size() + " remote candidates");
+ for (IceCandidate candidate : queuedRemoteCandidates) {
+ pc.addIceCandidate(candidate);
+ }
+ queuedRemoteCandidates = null;
}
- queuedRemoteCandidates = null;
}
public void switchCamera() {
@@ -435,7 +449,11 @@ public class PeerConnectionClient {
public void onIceCandidate(final IceCandidate candidate){
activity.runOnUiThread(new Runnable() {
public void run() {
- events.onIceCandidate(candidate);
+ if (queuedLocalCandidates != null) {
+ queuedLocalCandidates.add(candidate);
+ } else {
+ events.onIceCandidate(candidate);
+ }
}
});
}
@@ -470,6 +488,7 @@ public class PeerConnectionClient {
@Override
public void onIceGatheringChange(
PeerConnection.IceGatheringState newState) {
+ Log.d(TAG, "IceGatheringState: " + newState);
}
@Override
@@ -543,20 +562,20 @@ public class PeerConnectionClient {
Log.d(TAG, "Local SDP set succesfully");
events.onLocalDescription(localSdp);
} else {
- // We've just set remote description,
- // so drain remote ICE candidates.
+ // We've just set remote description, so drain remote
+ // and send local ICE candidates.
Log.d(TAG, "Remote SDP set succesfully");
- drainRemoteCandidates();
+ drainCandidates();
}
} else {
// For answering peer connection we set remote SDP and then
// create answer and set local SDP.
if (pc.getLocalDescription() != null) {
- // We've just set our local SDP so time to send it and drain
- // remote ICE candidates.
+ // We've just set our local SDP so time to send it, drain
+ // remote and send local ICE candidates.
Log.d(TAG, "Local SDP set succesfully");
events.onLocalDescription(localSdp);
- drainRemoteCandidates();
+ drainCandidates();
} else {
// We've just set remote SDP - do nothing for now -
// answer will be created soon.
diff --git a/talk/examples/android/src/org/appspot/apprtc/RoomParametersFetcher.java b/talk/examples/android/src/org/appspot/apprtc/RoomParametersFetcher.java
new file mode 100644
index 000000000..99981f38d
--- /dev/null
+++ b/talk/examples/android/src/org/appspot/apprtc/RoomParametersFetcher.java
@@ -0,0 +1,296 @@
+/*
+ * libjingle
+ * Copyright 2014, Google Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * 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.os.AsyncTask;
+import android.util.Log;
+
+import org.appspot.apprtc.AppRTCClient.SignalingParameters;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.webrtc.MediaConstraints;
+import org.webrtc.PeerConnection;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.LinkedList;
+import java.util.Scanner;
+
+// AsyncTask that converts an AppRTC room URL into the set of signaling
+// parameters to use with that room.
+public class RoomParametersFetcher
+ extends AsyncTask {
+ private static final String TAG = "RoomRTCClient";
+ private Exception exception = null;
+ private RoomParametersFetcherEvents events = null;
+
+ /**
+ * Room parameters fetcher callbacks.
+ */
+ public static interface RoomParametersFetcherEvents {
+ /**
+ * Callback fired once the room's signaling parameters
+ * SignalingParameters are extracted.
+ */
+ public void onSignalingParametersReady(final SignalingParameters params);
+
+ /**
+ * Callback for room parameters extraction error.
+ */
+ public void onSignalingParametersError(final String description);
+ }
+
+ public RoomParametersFetcher(RoomParametersFetcherEvents events) {
+ super();
+ this.events = events;
+ }
+
+ @Override
+ protected SignalingParameters doInBackground(String... urls) {
+ if (events == null) {
+ exception = new RuntimeException("Room conenction events should be set");
+ return null;
+ }
+ if (urls.length != 1) {
+ exception = new RuntimeException("Must be called with a single URL");
+ return null;
+ }
+ try {
+ exception = null;
+ return getParametersForRoomUrl(urls[0]);
+ } catch (JSONException e) {
+ exception = e;
+ } catch (IOException e) {
+ exception = e;
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(SignalingParameters params) {
+ if (exception != null) {
+ Log.e(TAG, "Room connection error: " + exception.toString());
+ events.onSignalingParametersError(exception.getMessage());
+ return;
+ }
+ if (params == null) {
+ Log.e(TAG, "Can not extract room parameters");
+ events.onSignalingParametersError("Can not extract room parameters");
+ return;
+ }
+ events.onSignalingParametersReady(params);
+ }
+
+ // Fetches |url| and fishes the signaling parameters out of the JSON.
+ private SignalingParameters getParametersForRoomUrl(String url)
+ throws IOException, JSONException {
+ url = url + "&t=json";
+ Log.d(TAG, "Connecting to room: " + url);
+ InputStream responseStream = new BufferedInputStream(
+ (new URL(url)).openConnection().getInputStream());
+ String response = drainStream(responseStream);
+ Log.d(TAG, "Room response: " + response);
+ JSONObject roomJson = new JSONObject(response);
+
+ if (roomJson.has("error")) {
+ JSONArray errors = roomJson.getJSONArray("error_messages");
+ throw new IOException(errors.toString());
+ }
+
+ String roomId = roomJson.getString("room_key");
+ String clientId = roomJson.getString("me");
+ Log.d(TAG, "RoomId: " + roomId + ". ClientId: " + clientId);
+ String channelToken = roomJson.optString("token");
+ String offerSdp = roomJson.optString("offer");
+ if (offerSdp != null && offerSdp.length() > 0) {
+ JSONObject offerJson = new JSONObject(offerSdp);
+ offerSdp = offerJson.getString("sdp");
+ Log.d(TAG, "SDP type: " + offerJson.getString("type"));
+ } else {
+ offerSdp = null;
+ }
+
+ String postMessageUrl = url.substring(0, url.indexOf('?'));
+ postMessageUrl += "/message?r=" + roomId + "&u=" + clientId;
+ Log.d(TAG, "Post url: " + postMessageUrl);
+
+ boolean initiator = roomJson.getInt("initiator") == 1;
+ Log.d(TAG, "Initiator: " + initiator);
+
+ LinkedList iceServers =
+ iceServersFromPCConfigJSON(roomJson.getString("pc_config"));
+ boolean isTurnPresent = false;
+ for (PeerConnection.IceServer server : iceServers) {
+ Log.d(TAG, "IceServer: " + server);
+ if (server.uri.startsWith("turn:")) {
+ isTurnPresent = true;
+ break;
+ }
+ }
+ if (!isTurnPresent) {
+ PeerConnection.IceServer server =
+ requestTurnServer(roomJson.getString("turn_url"));
+ Log.d(TAG, "TurnServer: " + server);
+ iceServers.add(server);
+ }
+
+ 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 SignalingParameters(
+ iceServers, initiator,
+ pcConstraints, videoConstraints, audioConstraints,
+ postMessageUrl, roomId, clientId,
+ channelToken, offerSdp);
+ }
+
+ // 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) throws JSONException {
+ 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();
+ }
+
+ private MediaConstraints constraintsFromJSON(String jsonString)
+ throws JSONException {
+ if (jsonString == null) {
+ return null;
+ }
+ 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;
+ }
+
+ // Requests & returns a TURN ICE Server based on a request URL. Must be run
+ // off the main thread!
+ private PeerConnection.IceServer requestTurnServer(String url)
+ throws IOException, JSONException {
+ 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);
+ }
+
+ // Return the list of ICE servers described by a WebRTCPeerConnection
+ // configuration string.
+ private LinkedList iceServersFromPCConfigJSON(
+ String pcConfig) throws JSONException {
+ JSONObject json = new JSONObject(pcConfig);
+ JSONArray servers = json.getJSONArray("iceServers");
+ LinkedList ret =
+ new LinkedList();
+ 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;
+ }
+
+ // Return the contents of an InputStream as a String.
+ private String drainStream(InputStream in) {
+ Scanner s = new Scanner(in).useDelimiter("\\A");
+ return s.hasNext() ? s.next() : "";
+ }
+
+}
diff --git a/talk/examples/android/src/org/appspot/apprtc/SettingsActivity.java b/talk/examples/android/src/org/appspot/apprtc/SettingsActivity.java
index eccb67ed2..367c834b9 100644
--- a/talk/examples/android/src/org/appspot/apprtc/SettingsActivity.java
+++ b/talk/examples/android/src/org/appspot/apprtc/SettingsActivity.java
@@ -36,7 +36,6 @@ import android.preference.Preference;
public class SettingsActivity extends Activity
implements OnSharedPreferenceChangeListener{
private SettingsFragment settingsFragment;
- private String keyprefUrl;
private String keyprefResolution;
private String keyprefFps;
private String keyprefCpuUsageDetection;
@@ -44,7 +43,6 @@ public class SettingsActivity extends Activity
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- keyprefUrl = getString(R.string.pref_url_key);
keyprefResolution = getString(R.string.pref_resolution_key);
keyprefFps = getString(R.string.pref_fps_key);
keyprefCpuUsageDetection = getString(R.string.pref_cpu_usage_detection_key);
@@ -63,7 +61,6 @@ public class SettingsActivity extends Activity
SharedPreferences sharedPreferences =
settingsFragment.getPreferenceScreen().getSharedPreferences();
sharedPreferences.registerOnSharedPreferenceChangeListener(this);
- updateSummary(sharedPreferences, keyprefUrl);
updateSummary(sharedPreferences, keyprefResolution);
updateSummary(sharedPreferences, keyprefFps);
updateSummaryB(sharedPreferences, keyprefCpuUsageDetection);
@@ -80,8 +77,7 @@ public class SettingsActivity extends Activity
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
String key) {
- if (key.equals(keyprefUrl) || key.equals(keyprefResolution) ||
- key.equals(keyprefFps)) {
+ if (key.equals(keyprefResolution) || key.equals(keyprefFps)) {
updateSummary(sharedPreferences, key);
} else if (key.equals(keyprefCpuUsageDetection)) {
updateSummaryB(sharedPreferences, key);
diff --git a/talk/examples/android/src/org/appspot/apprtc/WebSocketChannelClient.java b/talk/examples/android/src/org/appspot/apprtc/WebSocketChannelClient.java
new file mode 100644
index 000000000..373480d15
--- /dev/null
+++ b/talk/examples/android/src/org/appspot/apprtc/WebSocketChannelClient.java
@@ -0,0 +1,216 @@
+/*
+ * libjingle
+ * Copyright 2014, Google Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * 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.os.AsyncTask;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import de.tavendo.autobahn.WebSocketConnection;
+import de.tavendo.autobahn.WebSocketException;
+import de.tavendo.autobahn.WebSocket.WebSocketConnectionObserver;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * WebSocket client implementation.
+ * For proper synchronization all methods should be called from UI thread
+ * and all WebSocket events are delivered on UI thread as well.
+ */
+
+public class WebSocketChannelClient {
+ private final String TAG = "WSChannelRTCClient";
+ private final WebSocketChannelEvents events;
+ private final Handler uiHandler;
+ private WebSocketConnection ws;
+ private WebSocketObserver wsObserver;
+ private URI serverURI;
+ private WebSocketConnectionState state;
+
+ public enum WebSocketConnectionState {
+ NEW, CONNECTED, REGISTERED, CLOSED, ERROR
+ };
+
+ /**
+ * Callback interface for messages delivered on WebSocket.
+ * All events are invoked from UI thread.
+ */
+ public interface WebSocketChannelEvents {
+ public void onWebSocketOpen();
+ public void onWebSocketMessage(final String message);
+ public void onWebSocketClose();
+ public void onWebSocketError(final String description);
+ }
+
+ public WebSocketChannelClient(WebSocketChannelEvents events) {
+ this.events = events;
+ uiHandler = new Handler(Looper.getMainLooper());
+ state = WebSocketConnectionState.NEW;
+ }
+
+ public WebSocketConnectionState getState() {
+ return state;
+ }
+
+ public void connect(String url) {
+ if (state != WebSocketConnectionState.NEW) {
+ Log.e(TAG, "WebSocket is already connected.");
+ return;
+ }
+ Log.d(TAG, "Connecting WebSocket to: " + url);
+
+ ws = new WebSocketConnection();
+ wsObserver = new WebSocketObserver();
+ try {
+ serverURI = new URI(url);
+ ws.connect(serverURI, wsObserver);
+ } catch (URISyntaxException e) {
+ reportError("URI error: " + e.getMessage());
+ } catch (WebSocketException e) {
+ reportError("WebSocket connection error: " + e.getMessage());
+ }
+ }
+
+ public void register(String roomId, String clientId) {
+ if (state != WebSocketConnectionState.CONNECTED) {
+ Log.w(TAG, "WebSocket register() in state " + state);
+ return;
+ }
+ JSONObject json = new JSONObject();
+ try {
+ json.put("cmd", "register");
+ json.put("roomid", roomId);
+ json.put("clientid", clientId);
+ Log.d(TAG, "WS SEND: " + json.toString());
+ ws.sendTextMessage(json.toString());
+ state = WebSocketConnectionState.REGISTERED;
+ } catch (JSONException e) {
+ reportError("WebSocket register JSON error: " + e.getMessage());
+ }
+ }
+
+ public void send(String message) {
+ if (state != WebSocketConnectionState.REGISTERED) {
+ Log.e(TAG, "WebSocket send() in non registered state : " + message);
+ return;
+ }
+ JSONObject json = new JSONObject();
+ try {
+ json.put("cmd", "send");
+ json.put("msg", message);
+ message = json.toString();
+ Log.d(TAG, "WS SEND: " + message);
+ ws.sendTextMessage(message);
+ } catch (JSONException e) {
+ reportError("WebSocket send JSON error: " + e.getMessage());
+ }
+ }
+
+ public void disconnect() {
+ Log.d(TAG, "Disonnect WebSocket. State: " + state);
+ if (state == WebSocketConnectionState.REGISTERED) {
+ send("{\"type\": \"bye\"}");
+ state = WebSocketConnectionState.CONNECTED;
+ }
+ // TODO(glaznev): send DELETE to http WebSocket server once send()
+ // will switch to http POST.
+
+ // Close WebSocket in CONNECTED or ERROR states only.
+ if (state == WebSocketConnectionState.CONNECTED ||
+ state == WebSocketConnectionState.ERROR) {
+ state = WebSocketConnectionState.CLOSED;
+ ws.disconnect();
+ }
+ }
+
+ private void reportError(final String errorMessage) {
+ Log.e(TAG, errorMessage);
+ uiHandler.post(new Runnable() {
+ public void run() {
+ if (state != WebSocketConnectionState.ERROR) {
+ state = WebSocketConnectionState.ERROR;
+ events.onWebSocketError(errorMessage);
+ }
+ }
+ });
+ }
+
+ private class WebSocketObserver implements WebSocketConnectionObserver {
+ @Override
+ public void onOpen() {
+ Log.d(TAG, "WebSocket connection opened to: " + serverURI.toString());
+ uiHandler.post(new Runnable() {
+ public void run() {
+ state = WebSocketConnectionState.CONNECTED;
+ events.onWebSocketOpen();
+ }
+ });
+ }
+
+ @Override
+ public void onClose(WebSocketCloseNotification code, String reason) {
+ Log.d(TAG, "WebSocket connection closed. Code: " + code +
+ ". Reason: " + reason);
+ uiHandler.post(new Runnable() {
+ public void run() {
+ if (state != WebSocketConnectionState.CLOSED) {
+ state = WebSocketConnectionState.CLOSED;
+ events.onWebSocketClose();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onTextMessage(String payload) {
+ Log.d(TAG, "WS GET: " + payload);
+ final String message = payload;
+ uiHandler.post(new Runnable() {
+ public void run() {
+ if (state == WebSocketConnectionState.CONNECTED ||
+ state == WebSocketConnectionState.REGISTERED) {
+ events.onWebSocketMessage(message);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onRawTextMessage(byte[] payload) {
+ }
+
+ @Override
+ public void onBinaryMessage(byte[] payload) {
+ }
+ }
+
+}
diff --git a/talk/examples/android/src/org/appspot/apprtc/WebSocketRTCClient.java b/talk/examples/android/src/org/appspot/apprtc/WebSocketRTCClient.java
new file mode 100644
index 000000000..aaef09b98
--- /dev/null
+++ b/talk/examples/android/src/org/appspot/apprtc/WebSocketRTCClient.java
@@ -0,0 +1,313 @@
+/*
+ * libjingle
+ * Copyright 2014, Google Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * 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.os.AsyncTask;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.LinkedList;
+
+import org.appspot.apprtc.RoomParametersFetcher.RoomParametersFetcherEvents;
+import org.appspot.apprtc.WebSocketChannelClient.WebSocketChannelEvents;
+import org.appspot.apprtc.WebSocketChannelClient.WebSocketConnectionState;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.webrtc.IceCandidate;
+import org.webrtc.SessionDescription;
+
+/**
+ * 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 room connection is established
+ * onConnectedToRoom() callback with room parameters is invoked.
+ * Messages to other party (with local Ice candidates and answer SDP) can
+ * be sent after WebSocket connection is established.
+ */
+public class WebSocketRTCClient implements AppRTCClient,
+ RoomParametersFetcherEvents, WebSocketChannelEvents {
+ private static final String TAG = "WSRTCClient";
+ private static final String WSS_SERVER = "wss://apprtc-ws.webrtc.org:8089/ws";
+
+ private enum ConnectionState {
+ NEW, CONNECTED, CLOSED, ERROR
+ };
+ private final Handler uiHandler;
+ private SignalingEvents events;
+ private SignalingParameters signalingParameters;
+ private WebSocketChannelClient wsClient;
+ private RoomParametersFetcher fetcher;
+ private ConnectionState roomState;
+ private LinkedList gaePostQueue;
+
+ public WebSocketRTCClient(SignalingEvents events) {
+ this.events = events;
+ uiHandler = new Handler(Looper.getMainLooper());
+ gaePostQueue = new LinkedList();
+ }
+
+ // --------------------------------------------------------------------
+ // RoomConnectionEvents interface implementation.
+ // All events are called on UI thread.
+ @Override
+ public void onSignalingParametersReady(final SignalingParameters params) {
+ Log.d(TAG, "Room connection completed.");
+ if (!params.initiator && params.offerSdp == null) {
+ reportError("Offer SDP is not available");
+ return;
+ }
+ signalingParameters = params;
+ roomState = ConnectionState.CONNECTED;
+ events.onConnectedToRoom(signalingParameters);
+ wsClient.register(signalingParameters.roomId, signalingParameters.clientId);
+ events.onChannelOpen();
+ if (!signalingParameters.initiator) {
+ // For call receiver get sdp offer from room parameters.
+ SessionDescription sdp = new SessionDescription(
+ SessionDescription.Type.fromCanonicalForm("offer"),
+ signalingParameters.offerSdp);
+ events.onRemoteDescription(sdp);
+ }
+ }
+
+ @Override
+ public void onSignalingParametersError(final String description) {
+ reportError("Room connection error: " + description);
+ }
+
+ // --------------------------------------------------------------------
+ // WebSocketChannelEvents interface implementation.
+ // All events are called on UI thread.
+ @Override
+ public void onWebSocketOpen() {
+ Log.d(TAG, "Websocket connection completed.");
+ if (roomState == ConnectionState.CONNECTED) {
+ wsClient.register(
+ signalingParameters.roomId, signalingParameters.clientId);
+ }
+ }
+
+ @Override
+ public void onWebSocketMessage(final String msg) {
+ if (wsClient.getState() != WebSocketConnectionState.REGISTERED) {
+ Log.e(TAG, "Got WebSocket message in non registered state.");
+ return;
+ }
+ try {
+ JSONObject json = new JSONObject(msg);
+ String msgText = json.getString("msg");
+ String errorText = json.optString("error");
+ if (msgText.length() > 0) {
+ json = new JSONObject(msgText);
+ String type = json.optString("type");
+ if (type.equals("candidate")) {
+ IceCandidate candidate = new IceCandidate(
+ (String) json.get("id"),
+ json.getInt("label"),
+ (String) json.get("candidate"));
+ events.onRemoteIceCandidate(candidate);
+ } else if (type.equals("answer")) {
+ SessionDescription sdp = new SessionDescription(
+ SessionDescription.Type.fromCanonicalForm(type),
+ (String)json.get("sdp"));
+ events.onRemoteDescription(sdp);
+ } else if (type.equals("bye")) {
+ events.onChannelClose();
+ } else {
+ reportError("Unexpected WebSocket message: " + msg);
+ }
+ }
+ else {
+ if (errorText != null && errorText.length() > 0) {
+ reportError("WebSocket error message: " + errorText);
+ } else {
+ reportError("Unexpected WebSocket message: " + msg);
+ }
+ }
+ } catch (JSONException e) {
+ reportError("WebSocket message JSON parsing error: " + e.toString());
+ }
+ }
+
+ @Override
+ public void onWebSocketClose() {
+ events.onChannelClose();
+ }
+
+ @Override
+ public void onWebSocketError(String description) {
+ reportError("WebSocket error: " + description);
+ }
+
+ // --------------------------------------------------------------------
+ // AppRTCClient interface implementation.
+ // Asynchronously connect to an AppRTC room URL, e.g.
+ // https://apprtc.appspot.com/?r=NNN, retrieve room parameters
+ // and connect to WebSocket server.
+ @Override
+ public void connectToRoom(String url) {
+ // Get room parameters.
+ roomState = ConnectionState.NEW;
+ fetcher = new RoomParametersFetcher(this);
+ fetcher.execute(url);
+ // Connect to WebSocket server.
+ wsClient = new WebSocketChannelClient(this);
+ wsClient.connect(WSS_SERVER);
+ }
+
+ @Override
+ public void disconnect() {
+ Log.d(TAG, "Disconnect. Room state: " + roomState);
+ if (roomState == ConnectionState.CONNECTED) {
+ Log.d(TAG, "Closing room.");
+ sendGAEMessage("{\"type\": \"bye\"}");
+ }
+ wsClient.disconnect();
+ }
+
+ // Send local SDP (offer or answer, depending on role) to the
+ // other participant. Note that it is important to send the output of
+ // create{Offer,Answer} and not merely the current value of
+ // getLocalDescription() because the latter may include ICE candidates that
+ // we might want to filter elsewhere.
+ @Override
+ public void sendOfferSdp(final SessionDescription sdp) {
+ JSONObject json = new JSONObject();
+ jsonPut(json, "sdp", sdp.description);
+ jsonPut(json, "type", "offer");
+ sendGAEMessage(json.toString());
+ }
+
+ @Override
+ public void sendAnswerSdp(final SessionDescription sdp) {
+ if (wsClient.getState() != WebSocketConnectionState.REGISTERED) {
+ reportError("Sending answer SDP in non registered state.");
+ return;
+ }
+ JSONObject json = new JSONObject();
+ jsonPut(json, "sdp", sdp.description);
+ jsonPut(json, "type", "answer");
+ wsClient.send(json.toString());
+ }
+
+ // Send Ice candidate to the other participant.
+ @Override
+ public void sendLocalIceCandidate(final IceCandidate candidate) {
+ if (wsClient.getState() != WebSocketConnectionState.REGISTERED) {
+ reportError("Sending ICE candidate in non registered state.");
+ return;
+ }
+ JSONObject json = new JSONObject();
+ jsonPut(json, "type", "candidate");
+ jsonPut(json, "label", candidate.sdpMLineIndex);
+ jsonPut(json, "id", candidate.sdpMid);
+ jsonPut(json, "candidate", candidate.sdp);
+ wsClient.send(json.toString());
+ }
+
+ // --------------------------------------------------------------------
+ // Helper functions.
+ private void reportError(final String errorMessage) {
+ Log.e(TAG, errorMessage);
+ uiHandler.post(new Runnable() {
+ public void run() {
+ if (roomState != ConnectionState.ERROR) {
+ roomState = ConnectionState.ERROR;
+ events.onChannelError(errorMessage);
+ }
+ }
+ });
+ }
+
+ // 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);
+ }
+ }
+
+ // Queue a message for sending to the room and send it if already connected.
+ private synchronized void sendGAEMessage(String msg) {
+ synchronized (gaePostQueue) {
+ gaePostQueue.add(msg);
+ }
+ (new AsyncTask() {
+ public Void doInBackground(Void... unused) {
+ maybeDrainGAEPostQueue();
+ return null;
+ }
+ }).execute();
+ }
+
+ // Send all queued messages if connected to the room.
+ private void maybeDrainGAEPostQueue() {
+ synchronized (gaePostQueue) {
+ if (roomState != ConnectionState.CONNECTED) {
+ return;
+ }
+ try {
+ for (String msg : gaePostQueue) {
+ Log.d(TAG, "ROOM SEND: " + msg);
+ // Check if this is 'bye' message and update room connection state.
+ // TODO(glaznev): change this to new bye message format:
+ // https://apprtc.appspot.com/bye/{roomid}/{clientid}
+ JSONObject json = new JSONObject(msg);
+ String type = json.optString("type");
+ if (type != null && type.equals("bye")) {
+ roomState = ConnectionState.CLOSED;
+ }
+ // Send POST request.
+ URLConnection connection = new URL(
+ signalingParameters.postMessageUrl).openConnection();
+ connection.setDoOutput(true);
+ connection.setRequestProperty(
+ "content-type", "text/plain; charset=utf-8");
+ connection.getOutputStream().write(msg.getBytes("UTF-8"));
+ String replyHeader = connection.getHeaderField(null);
+ if (!replyHeader.startsWith("HTTP/1.1 200 ")) {
+ reportError("Non-200 response to POST: " +
+ connection.getHeaderField(null) + " for msg: " + msg);
+ }
+ }
+ } catch (IOException e) {
+ reportError("GAE POST error: " + e.getMessage());
+ } catch (JSONException e) {
+ reportError("GAE POST JSON error: " + e.getMessage());
+ }
+ gaePostQueue.clear();
+ }
+ }
+
+}
diff --git a/talk/examples/android/third_party/autobanh/LICENSE b/talk/examples/android/third_party/autobanh/LICENSE
new file mode 100644
index 000000000..f433b1a53
--- /dev/null
+++ b/talk/examples/android/third_party/autobanh/LICENSE
@@ -0,0 +1,177 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
diff --git a/talk/examples/android/third_party/autobanh/LICENSE.md b/talk/examples/android/third_party/autobanh/LICENSE.md
new file mode 100644
index 000000000..2079e90d6
--- /dev/null
+++ b/talk/examples/android/third_party/autobanh/LICENSE.md
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Cameron Lowell Palmer
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/talk/examples/android/third_party/autobanh/autobanh.jar b/talk/examples/android/third_party/autobanh/autobanh.jar
new file mode 100644
index 000000000..5a10b7f3f
Binary files /dev/null and b/talk/examples/android/third_party/autobanh/autobanh.jar differ
diff --git a/talk/libjingle_examples.gyp b/talk/libjingle_examples.gyp
index f7ce53bdd..740452d18 100755
--- a/talk/libjingle_examples.gyp
+++ b/talk/libjingle_examples.gyp
@@ -314,6 +314,7 @@
'examples/android/README',
'examples/android/ant.properties',
'examples/android/assets/channel.html',
+ 'examples/android/third_party/autobanh/autobanh.jar',
'examples/android/build.xml',
'examples/android/jni/Android.mk',
'examples/android/project.properties',
@@ -351,9 +352,12 @@
'examples/android/src/org/appspot/apprtc/GAEChannelClient.java',
'examples/android/src/org/appspot/apprtc/GAERTCClient.java',
'examples/android/src/org/appspot/apprtc/PeerConnectionClient.java',
+ 'examples/android/src/org/appspot/apprtc/RoomParametersFetcher.java',
'examples/android/src/org/appspot/apprtc/SettingsActivity.java',
'examples/android/src/org/appspot/apprtc/SettingsFragment.java',
'examples/android/src/org/appspot/apprtc/UnhandledExceptionHandler.java',
+ 'examples/android/src/org/appspot/apprtc/WebSocketChannelClient.java',
+ 'examples/android/src/org/appspot/apprtc/WebSocketRTCClient.java',
],
'outputs': [
'<(PRODUCT_DIR)/AppRTCDemo-debug.apk',
@@ -367,6 +371,7 @@
'mkdir -p <(INTERMEDIATE_DIR) && ' # Must happen _before_ the cd below
'mkdir -p examples/android/libs/<(android_app_abi) && '
'cp <(PRODUCT_DIR)/libjingle_peerconnection.jar examples/android/libs/ &&'
+ 'cp examples/android/third_party/autobanh/autobanh.jar examples/android/libs/ &&'
'<(android_strip) -o examples/android/libs/<(android_app_abi)/libjingle_peerconnection_so.so <(PRODUCT_DIR)/libjingle_peerconnection_so.so &&'
'cd examples/android && '
'{ ANDROID_SDK_ROOT=<(android_sdk_root) '
diff --git a/webrtc/modules/audio_coding/neteq/neteq_impl.cc b/webrtc/modules/audio_coding/neteq/neteq_impl.cc
index f3d1a4f6b..5ec38d434 100644
--- a/webrtc/modules/audio_coding/neteq/neteq_impl.cc
+++ b/webrtc/modules/audio_coding/neteq/neteq_impl.cc
@@ -15,6 +15,7 @@
#include
+#include "webrtc/base/checks.h"
#include "webrtc/common_audio/signal_processing/include/signal_processing_library.h"
#include "webrtc/modules/audio_coding/neteq/accelerate.h"
#include "webrtc/modules/audio_coding/neteq/background_noise.h"
@@ -607,6 +608,8 @@ int NetEqImpl::InsertPacketInternal(const WebRtcRTPHeader& rtp_header,
new_codec_ = true;
update_sample_rate_and_channels = true;
LOG_F(LS_WARNING) << "Packet buffer flushed";
+ DCHECK(!packet_buffer_->Empty())
+ << "One packet must have been inserted after the flush.";
} else if (ret != PacketBuffer::kOK) {
LOG_FERR1(LS_WARNING, InsertPacketList, packet_list.size());
PacketBuffer::DeleteAllPackets(&packet_list);