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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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() : "";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user