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:
parent
2176db343c
commit
6f6ef72950
@ -27,11 +27,6 @@
|
||||
<string name="pref_room_key">room_preference</string>
|
||||
<string name="pref_room_list_key">room_list_preference</string>
|
||||
|
||||
<string name="pref_url_key">url_preference</string>
|
||||
<string name="pref_url_title">Connection URL:</string>
|
||||
<string name="pref_url_dlg">Enter AppRTC connection server URL.</string>
|
||||
<string name="pref_url_default">https://apprtc.appspot.com</string>
|
||||
|
||||
<string name="pref_resolution_key">resolution_preference</string>
|
||||
<string name="pref_resolution_title">Video resolution.</string>
|
||||
<string name="pref_resolution_dlg">Enter AppRTC local video resolution.</string>
|
||||
|
@ -1,11 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<EditTextPreference
|
||||
android:key="@string/pref_url_key"
|
||||
android:title="@string/pref_url_title"
|
||||
android:defaultValue="@string/pref_url_default"
|
||||
android:inputType="textWebEmailAddress"
|
||||
android:dialogTitle="@string/pref_url_dlg" />
|
||||
<ListPreference
|
||||
android:key="@string/pref_resolution_key"
|
||||
android:title="@string/pref_resolution_title"
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
177
talk/examples/android/third_party/autobanh/LICENSE
vendored
Normal file
177
talk/examples/android/third_party/autobanh/LICENSE
vendored
Normal file
@ -0,0 +1,177 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
21
talk/examples/android/third_party/autobanh/LICENSE.md
vendored
Normal file
21
talk/examples/android/third_party/autobanh/LICENSE.md
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Cameron Lowell Palmer
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
BIN
talk/examples/android/third_party/autobanh/autobanh.jar
vendored
Normal file
BIN
talk/examples/android/third_party/autobanh/autobanh.jar
vendored
Normal file
Binary file not shown.
@ -314,6 +314,7 @@
|
||||
'examples/android/README',
|
||||
'examples/android/ant.properties',
|
||||
'examples/android/assets/channel.html',
|
||||
'examples/android/third_party/autobanh/autobanh.jar',
|
||||
'examples/android/build.xml',
|
||||
'examples/android/jni/Android.mk',
|
||||
'examples/android/project.properties',
|
||||
@ -351,9 +352,12 @@
|
||||
'examples/android/src/org/appspot/apprtc/GAEChannelClient.java',
|
||||
'examples/android/src/org/appspot/apprtc/GAERTCClient.java',
|
||||
'examples/android/src/org/appspot/apprtc/PeerConnectionClient.java',
|
||||
'examples/android/src/org/appspot/apprtc/RoomParametersFetcher.java',
|
||||
'examples/android/src/org/appspot/apprtc/SettingsActivity.java',
|
||||
'examples/android/src/org/appspot/apprtc/SettingsFragment.java',
|
||||
'examples/android/src/org/appspot/apprtc/UnhandledExceptionHandler.java',
|
||||
'examples/android/src/org/appspot/apprtc/WebSocketChannelClient.java',
|
||||
'examples/android/src/org/appspot/apprtc/WebSocketRTCClient.java',
|
||||
],
|
||||
'outputs': [
|
||||
'<(PRODUCT_DIR)/AppRTCDemo-debug.apk',
|
||||
@ -367,6 +371,7 @@
|
||||
'mkdir -p <(INTERMEDIATE_DIR) && ' # Must happen _before_ the cd below
|
||||
'mkdir -p examples/android/libs/<(android_app_abi) && '
|
||||
'cp <(PRODUCT_DIR)/libjingle_peerconnection.jar examples/android/libs/ &&'
|
||||
'cp examples/android/third_party/autobanh/autobanh.jar examples/android/libs/ &&'
|
||||
'<(android_strip) -o examples/android/libs/<(android_app_abi)/libjingle_peerconnection_so.so <(PRODUCT_DIR)/libjingle_peerconnection_so.so &&'
|
||||
'cd examples/android && '
|
||||
'{ ANDROID_SDK_ROOT=<(android_sdk_root) '
|
||||
|
@ -15,6 +15,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "webrtc/base/checks.h"
|
||||
#include "webrtc/common_audio/signal_processing/include/signal_processing_library.h"
|
||||
#include "webrtc/modules/audio_coding/neteq/accelerate.h"
|
||||
#include "webrtc/modules/audio_coding/neteq/background_noise.h"
|
||||
@ -607,6 +608,8 @@ int NetEqImpl::InsertPacketInternal(const WebRtcRTPHeader& rtp_header,
|
||||
new_codec_ = true;
|
||||
update_sample_rate_and_channels = true;
|
||||
LOG_F(LS_WARNING) << "Packet buffer flushed";
|
||||
DCHECK(!packet_buffer_->Empty())
|
||||
<< "One packet must have been inserted after the flush.";
|
||||
} else if (ret != PacketBuffer::kOK) {
|
||||
LOG_FERR1(LS_WARNING, InsertPacketList, packet_list.size());
|
||||
PacketBuffer::DeleteAllPackets(&packet_list);
|
||||
|
Loading…
x
Reference in New Issue
Block a user