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);