Add DCHECK to ensure that NetEq's packet buffer is not empty

This DCHECK ensures that one packet was inserted after the buffer was
flushed.

R=kwiberg@webrtc.org

Review URL: https://webrtc-codereview.appspot.com/30169004

git-svn-id: http://webrtc.googlecode.com/svn/trunk@7719 4adac7df-926f-26a2-2b94-8c16560cd09d
This commit is contained in:
henrik.lundin@webrtc.org
2014-11-19 13:02:24 +00:00
parent 2176db343c
commit 6f6ef72950
17 changed files with 1211 additions and 324 deletions

View File

@@ -42,10 +42,14 @@ public interface AppRTCClient {
public void connectToRoom(String url);
/**
* Send local SDP (offer or answer, depending on role) to the
* other participant.
* Send offer SDP to the other participant.
*/
public void sendLocalDescription(final SessionDescription sdp);
public void sendOfferSdp(final SessionDescription sdp);
/**
* Send answer SDP to the other participant.
*/
public void sendAnswerSdp(final SessionDescription sdp);
/**
* Send Ice candidate to the other participant.
@@ -60,36 +64,54 @@ public interface AppRTCClient {
/**
* Struct holding the signaling parameters of an AppRTC room.
*/
public class AppRTCSignalingParameters {
public class SignalingParameters {
public final boolean websocketSignaling;
public final List<PeerConnection.IceServer> 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<PeerConnection.IceServer> 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);
}
}

View File

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

View File

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

View File

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

View File

@@ -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<String> sendQueue = new LinkedList<String>();
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<String, Void, AppRTCSignalingParameters> {
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<PeerConnection.IceServer> 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<PeerConnection.IceServer> iceServersFromPCConfigJSON(
String pcConfig) {
try {
JSONObject json = new JSONObject(pcConfig);
JSONArray servers = json.getJSONArray("iceServers");
LinkedList<PeerConnection.IceServer> ret =
new LinkedList<PeerConnection.IceServer>();
for (int i = 0; i < servers.length(); ++i) {
JSONObject server = servers.getJSONObject(i);
String url = server.getString("urls");
String credential =
server.has("credential") ? server.getString("credential") : "";
ret.add(new PeerConnection.IceServer(url, "", credential));
}
return ret;
} catch (JSONException e) {
throw new RuntimeException(e);
}
}
// Request an attempt to drain the send queue, on a background thread.
private void requestQueueDrainInBackground() {
(new AsyncTask<Void, Void, Void>() {
@@ -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);
}
}

View File

@@ -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<IceCandidate> queuedRemoteCandidates =
new LinkedList<IceCandidate>();
// 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<IceCandidate> queuedRemoteCandidates = null;
private LinkedList<IceCandidate> 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<IceCandidate>();
queuedLocalCandidates = new LinkedList<IceCandidate>();
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.

View File

@@ -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<String, Void, SignalingParameters> {
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<PeerConnection.IceServer> 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<PeerConnection.IceServer> iceServersFromPCConfigJSON(
String pcConfig) throws JSONException {
JSONObject json = new JSONObject(pcConfig);
JSONArray servers = json.getJSONArray("iceServers");
LinkedList<PeerConnection.IceServer> ret =
new LinkedList<PeerConnection.IceServer>();
for (int i = 0; i < servers.length(); ++i) {
JSONObject server = servers.getJSONObject(i);
String url = server.getString("urls");
String credential =
server.has("credential") ? server.getString("credential") : "";
ret.add(new PeerConnection.IceServer(url, "", credential));
}
return ret;
}
// 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() : "";
}
}

View File

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

View File

@@ -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) {
}
}
}

View File

@@ -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<String> gaePostQueue;
public WebSocketRTCClient(SignalingEvents events) {
this.events = events;
uiHandler = new Handler(Looper.getMainLooper());
gaePostQueue = new LinkedList<String>();
}
// --------------------------------------------------------------------
// 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<Void, Void, Void>() {
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();
}
}
}