Remove peer connection and signaling calls from UI thread.

- Add separate looper threads for peer connection and websocket
signaling classes.
- To improve the connection speed start peer connection factory
initialization once EGL context is ready in parallel with the room
connection.
- Add asynchronious http request class and start using it in
webscoket signaling and room parameters extractor.
- Add helper looper based executor class.
- Port some of henrika changes from
https://webrtc-codereview.appspot.com/36629004/ to fix sensor
crashes on non L devices - will remove the change if CL will
be submitted soon.

R=jiayl@webrtc.org, wzh@webrtc.org

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

git-svn-id: http://webrtc.googlecode.com/svn/trunk@8006 4adac7df-926f-26a2-2b94-8c16560cd09d
This commit is contained in:
glaznev@webrtc.org
2015-01-06 22:24:09 +00:00
parent 2ec50f2b0f
commit f6a9714760
15 changed files with 1234 additions and 780 deletions

View File

@@ -56,6 +56,7 @@ import org.webrtc.VideoRenderer.I420Frame;
*/ */
public class VideoRendererGui implements GLSurfaceView.Renderer { public class VideoRendererGui implements GLSurfaceView.Renderer {
private static VideoRendererGui instance = null; private static VideoRendererGui instance = null;
private static Runnable eglContextReady = null;
private static final String TAG = "VideoRendererGui"; private static final String TAG = "VideoRendererGui";
private GLSurfaceView surface; private GLSurfaceView surface;
private static EGLContext eglContext = null; private static EGLContext eglContext = null;
@@ -595,9 +596,11 @@ public class VideoRendererGui implements GLSurfaceView.Renderer {
} }
/** Passes GLSurfaceView to video renderer. */ /** Passes GLSurfaceView to video renderer. */
public static void setView(GLSurfaceView surface) { public static void setView(GLSurfaceView surface,
Runnable eglContextReadyCallback) {
Log.d(TAG, "VideoRendererGui.setView"); Log.d(TAG, "VideoRendererGui.setView");
instance = new VideoRendererGui(surface); instance = new VideoRendererGui(surface);
eglContextReady = eglContextReadyCallback;
} }
public static EGLContext getEGLContext() { public static EGLContext getEGLContext() {
@@ -690,7 +693,7 @@ public class VideoRendererGui implements GLSurfaceView.Renderer {
@Override @Override
public void onSurfaceCreated(GL10 unused, EGLConfig config) { public void onSurfaceCreated(GL10 unused, EGLConfig config) {
Log.d(TAG, "VideoRendererGui.onSurfaceCreated"); Log.d(TAG, "VideoRendererGui.onSurfaceCreated");
// Store render EGL context // Store render EGL context.
if (CURRENT_SDK_VERSION >= EGL14_SDK_VERSION) { if (CURRENT_SDK_VERSION >= EGL14_SDK_VERSION) {
eglContext = EGL14.eglGetCurrentContext(); eglContext = EGL14.eglGetCurrentContext();
Log.d(TAG, "VideoRendererGui EGL Context: " + eglContext); Log.d(TAG, "VideoRendererGui EGL Context: " + eglContext);
@@ -711,6 +714,11 @@ public class VideoRendererGui implements GLSurfaceView.Renderer {
} }
checkNoGLES2Error(); checkNoGLES2Error();
GLES20.glClearColor(0.15f, 0.15f, 0.15f, 1.0f); GLES20.glClearColor(0.15f, 0.15f, 0.15f, 1.0f);
// Fire EGL context ready event.
if (eglContextReady != null) {
eglContextReady.run();
}
} }
@Override @Override

View File

@@ -1,34 +0,0 @@
<html>
<head>
<script src="https://apprtc.appspot.com/_ah/channel/jsapi"></script>
</head>
<!--
Helper HTML that redirects Google AppEngine's Channel API to a JS object named
|androidMessageHandler|, which is expected to be injected into the WebView
rendering this page by an Android app's class such as AppRTCClient.
-->
<body onbeforeunload="closeSocket()" onload="openSocket()">
<script type="text/javascript">
var token = androidMessageHandler.getToken();
if (!token)
throw "Missing/malformed token parameter: [" + token + "]";
var channel = null;
var socket = null;
function openSocket() {
channel = new goog.appengine.Channel(token);
socket = channel.open({
'onopen': function() { androidMessageHandler.onOpen(); },
'onmessage': function(msg) { androidMessageHandler.onMessage(msg.data); },
'onclose': function() { androidMessageHandler.onClose(); },
'onerror': function(err) { androidMessageHandler.onError(err.code, err.description); }
});
}
function closeSocket() {
socket.close();
}
</script>
</body>
</html>

View File

@@ -27,6 +27,8 @@
package org.appspot.apprtc; package org.appspot.apprtc;
import org.appspot.apprtc.util.AppRTCUtils;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
@@ -35,8 +37,6 @@ import android.content.pm.PackageManager;
import android.media.AudioManager; import android.media.AudioManager;
import android.util.Log; import android.util.Log;
import org.appspot.apprtc.util.AppRTCUtils;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;

View File

@@ -42,7 +42,7 @@ public interface AppRTCClient {
* https://apprtc.appspot.com/?r=NNN. Once connection is established * https://apprtc.appspot.com/?r=NNN. Once connection is established
* onConnectedToRoom() callback with room parameters is invoked. * onConnectedToRoom() callback with room parameters is invoked.
*/ */
public void connectToRoom(String url, boolean loopback); public void connectToRoom(final String url, final boolean loopback);
/** /**
* Send offer SDP to the other participant. * Send offer SDP to the other participant.
@@ -60,9 +60,9 @@ public interface AppRTCClient {
public void sendLocalIceCandidate(final IceCandidate candidate); public void sendLocalIceCandidate(final IceCandidate candidate);
/** /**
* Disconnect from the channel. * Disconnect from room.
*/ */
public void disconnect(); public void disconnectFromRoom();
/** /**
* Struct holding the signaling parameters of an AppRTC room. * Struct holding the signaling parameters of an AppRTC room.

View File

@@ -27,6 +27,8 @@
package org.appspot.apprtc; package org.appspot.apprtc;
import org.appspot.apprtc.AppRTCClient.SignalingParameters;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.Fragment; import android.app.Fragment;
@@ -49,9 +51,7 @@ import android.widget.ImageButton;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import org.appspot.apprtc.AppRTCClient.SignalingParameters;
import org.webrtc.IceCandidate; import org.webrtc.IceCandidate;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.SessionDescription; import org.webrtc.SessionDescription;
import org.webrtc.StatsObserver; import org.webrtc.StatsObserver;
import org.webrtc.StatsReport; import org.webrtc.StatsReport;
@@ -71,7 +71,7 @@ public class AppRTCDemoActivity extends Activity
implements AppRTCClient.SignalingEvents, implements AppRTCClient.SignalingEvents,
PeerConnectionClient.PeerConnectionEvents { PeerConnectionClient.PeerConnectionEvents {
private static final String TAG = "AppRTCClient"; private static final String TAG = "AppRTCClient";
private PeerConnectionClient pc; private PeerConnectionClient pc = null;
private AppRTCClient appRtcClient; private AppRTCClient appRtcClient;
private SignalingParameters signalingParameters; private SignalingParameters signalingParameters;
private AppRTCAudioManager audioManager = null; private AppRTCAudioManager audioManager = null;
@@ -123,7 +123,12 @@ public class AppRTCDemoActivity extends Activity
roomNameView = (TextView) findViewById(R.id.room_name); roomNameView = (TextView) findViewById(R.id.room_name);
videoView = (GLSurfaceView) findViewById(R.id.glview); videoView = (GLSurfaceView) findViewById(R.id.glview);
VideoRendererGui.setView(videoView); VideoRendererGui.setView(videoView, new Runnable() {
@Override
public void run() {
createPeerConnectionFactory();
}
});
scalingType = ScalingType.SCALE_ASPECT_FILL; scalingType = ScalingType.SCALE_ASPECT_FILL;
remoteRender = VideoRendererGui.create(0, 0, 100, 100, scalingType, false); remoteRender = VideoRendererGui.create(0, 0, 100, 100, scalingType, false);
localRender = VideoRendererGui.create(0, 0, 100, 100, scalingType, true); localRender = VideoRendererGui.create(0, 0, 100, 100, scalingType, true);
@@ -201,17 +206,6 @@ public class AppRTCDemoActivity extends Activity
hudView.setVisibility(View.INVISIBLE); hudView.setVisibility(View.INVISIBLE);
addContentView(hudView, hudLayout); addContentView(hudView, hudLayout);
// Create and audio manager that will take care of audio routing,
// audio modes, audio device enumeration etc.
audioManager = AppRTCAudioManager.create(this, new Runnable() {
// This method will be called each time the audio state (number and
// type of devices) has been changed.
public void run() {
onAudioManagerChangedState();
}
}
);
final Intent intent = getIntent(); final Intent intent = getIntent();
Uri url = intent.getData(); Uri url = intent.getData();
roomName = intent.getStringExtra(ConnectActivity.EXTRA_ROOMNAME); roomName = intent.getStringExtra(ConnectActivity.EXTRA_ROOMNAME);
@@ -225,6 +219,7 @@ public class AppRTCDemoActivity extends Activity
if (url != null) { if (url != null) {
if (loopback || (roomName != null && !roomName.equals(""))) { if (loopback || (roomName != null && !roomName.equals(""))) {
// Start room connection.
logAndToast(getString(R.string.connecting_to, url)); logAndToast(getString(R.string.connecting_to, url));
appRtcClient = new WebSocketRTCClient(this); appRtcClient = new WebSocketRTCClient(this);
appRtcClient.connectToRoom(url.toString(), loopback); appRtcClient.connectToRoom(url.toString(), loopback);
@@ -233,6 +228,23 @@ public class AppRTCDemoActivity extends Activity
} else { } else {
roomNameView.setText(roomName); roomNameView.setText(roomName);
} }
// Create and audio manager that will take care of audio routing,
// audio modes, audio device enumeration etc.
audioManager = AppRTCAudioManager.create(this, new Runnable() {
// This method will be called each time the audio state (number and
// type of devices) has been changed.
@Override
public void run() {
onAudioManagerChangedState();
}
}
);
// Store existing audio settings and change audio mode to
// MODE_IN_COMMUNICATION for best possible VoIP performance.
Log.d(TAG, "Initializing the audio manager...");
audioManager.init();
// For command line execution run connection for <runTimeMs> and exit. // For command line execution run connection for <runTimeMs> and exit.
if (commandLineRun && runTimeMs > 0) { if (commandLineRun && runTimeMs > 0) {
videoView.postDelayed(new Runnable() { videoView.postDelayed(new Runnable() {
@@ -254,6 +266,21 @@ public class AppRTCDemoActivity extends Activity
} }
} }
// Create peer connection factory when EGL context is ready.
private void createPeerConnectionFactory() {
final AppRTCDemoActivity thisCopy = this;
runOnUiThread(new Runnable() {
@Override
public void run() {
if (pc == null) {
pc = new PeerConnectionClient();
pc.createPeerConnectionFactory(
thisCopy, hwCodec, VideoRendererGui.getEGLContext(), thisCopy);
}
}
});
}
/** /**
* MenuBar fragment for AppRTC. * MenuBar fragment for AppRTC.
*/ */
@@ -291,6 +318,9 @@ public class AppRTCDemoActivity extends Activity
protected void onDestroy() { protected void onDestroy() {
disconnect(); disconnect();
super.onDestroy(); super.onDestroy();
if (logToast != null) {
logToast.cancel();
}
activityRunning = false; activityRunning = false;
} }
@@ -312,7 +342,7 @@ public class AppRTCDemoActivity extends Activity
// Disconnect from remote resources, dispose of local resources, and exit. // Disconnect from remote resources, dispose of local resources, and exit.
private void disconnect() { private void disconnect() {
if (appRtcClient != null) { if (appRtcClient != null) {
appRtcClient.disconnect(); appRtcClient.disconnectFromRoom();
appRtcClient = null; appRtcClient = null;
} }
if (pc != null) { if (pc != null) {
@@ -349,13 +379,6 @@ public class AppRTCDemoActivity extends Activity
} }
} }
// Poor-man's assert(): die with |msg| unless |condition| is true.
private static void abortUnless(boolean condition, String msg) {
if (!condition) {
throw new RuntimeException(msg);
}
}
// Log |msg| and Toast about it. // Log |msg| and Toast about it.
private void logAndToast(String msg) { private void logAndToast(String msg) {
Log.d(TAG, msg); Log.d(TAG, msg);
@@ -460,22 +483,20 @@ public class AppRTCDemoActivity extends Activity
} }
// -----Implementation of AppRTCClient.AppRTCSignalingEvents --------------- // -----Implementation of AppRTCClient.AppRTCSignalingEvents ---------------
// All events are called from UI thread. // All callbacks are invoked from websocket signaling looper thread and
@Override // are routed to UI thread.
public void onConnectedToRoom(final SignalingParameters params) { private void onConnectedToRoomInternal(final SignalingParameters params) {
if (audioManager != null) {
// Store existing audio settings and change audio mode to
// MODE_IN_COMMUNICATION for best possible VoIP performance.
Log.d(TAG, "Initializing the audio manager...");
audioManager.init();
}
signalingParameters = params; signalingParameters = params;
abortUnless(PeerConnectionFactory.initializeAndroidGlobals(
this, true, true, hwCodec, VideoRendererGui.getEGLContext()),
"Failed to initializeAndroidGlobals");
logAndToast("Creating peer connection..."); logAndToast("Creating peer connection...");
pc = new PeerConnectionClient(localRender, remoteRender, if (pc == null) {
signalingParameters, this, startBitrate); // Create peer connection factory if render EGL context ready event
// has not been fired yet.
pc = new PeerConnectionClient();
pc.createPeerConnectionFactory(
this, hwCodec, VideoRendererGui.getEGLContext(), this);
}
pc.createPeerConnection(
localRender, remoteRender, signalingParameters, startBitrate);
if (pc.isHDVideo()) { if (pc.isHDVideo()) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
} else { } else {
@@ -510,7 +531,7 @@ public class AppRTCDemoActivity extends Activity
} }
}, null); }, null);
if (!success) { if (!success) {
throw new RuntimeException("getStats() return false!"); Log.e(TAG, "getStats() return false!");
} }
} }
}; };
@@ -524,75 +545,127 @@ public class AppRTCDemoActivity extends Activity
} }
} }
@Override
public void onConnectedToRoom(final SignalingParameters params) {
runOnUiThread(new Runnable() {
@Override
public void run() {
onConnectedToRoomInternal(params);
}
});
}
@Override @Override
public void onRemoteDescription(final SessionDescription sdp) { public void onRemoteDescription(final SessionDescription sdp) {
if (pc == null) { runOnUiThread(new Runnable() {
return; @Override
} public void run() {
logAndToast("Received remote " + sdp.type + " ..."); if (pc == null) {
pc.setRemoteDescription(sdp); return;
if (!signalingParameters.initiator) { }
logAndToast("Creating ANSWER..."); logAndToast("Received remote " + sdp.type + " ...");
// Create answer. Answer SDP will be sent to offering client in pc.setRemoteDescription(sdp);
// PeerConnectionEvents.onLocalDescription event. if (!signalingParameters.initiator) {
pc.createAnswer(); logAndToast("Creating ANSWER...");
} // Create answer. Answer SDP will be sent to offering client in
// PeerConnectionEvents.onLocalDescription event.
pc.createAnswer();
}
}
});
} }
@Override @Override
public void onRemoteIceCandidate(final IceCandidate candidate) { public void onRemoteIceCandidate(final IceCandidate candidate) {
if (pc != null) { runOnUiThread(new Runnable() {
pc.addRemoteIceCandidate(candidate); @Override
} public void run() {
if (pc != null) {
pc.addRemoteIceCandidate(candidate);
}
}
});
} }
@Override @Override
public void onChannelClose() { public void onChannelClose() {
logAndToast("Remote end hung up; dropping PeerConnection"); runOnUiThread(new Runnable() {
disconnect(); @Override
public void run() {
logAndToast("Remote end hung up; dropping PeerConnection");
disconnect();
}
});
} }
@Override @Override
public void onChannelError(final String description) { public void onChannelError(final String description) {
if (!isError) { runOnUiThread(new Runnable() {
isError = true; @Override
disconnectWithErrorMessage(description); public void run() {
} if (!isError) {
isError = true;
disconnectWithErrorMessage(description);
}
}
});
} }
// -----Implementation of PeerConnectionClient.PeerConnectionEvents.--------- // -----Implementation of PeerConnectionClient.PeerConnectionEvents.---------
// Send local peer connection SDP and ICE candidates to remote party. // Send local peer connection SDP and ICE candidates to remote party.
// All callbacks are invoked from UI thread. // All callbacks are invoked from peer connection client looper thread and
// are routed to UI thread.
@Override @Override
public void onLocalDescription(final SessionDescription sdp) { public void onLocalDescription(final SessionDescription sdp) {
if (appRtcClient != null) { runOnUiThread(new Runnable() {
logAndToast("Sending " + sdp.type + " ..."); @Override
if (signalingParameters.initiator) { public void run() {
appRtcClient.sendOfferSdp(sdp); if (appRtcClient != null) {
} else { logAndToast("Sending " + sdp.type + " ...");
appRtcClient.sendAnswerSdp(sdp); if (signalingParameters.initiator) {
appRtcClient.sendOfferSdp(sdp);
} else {
appRtcClient.sendAnswerSdp(sdp);
}
}
} }
} });
} }
@Override @Override
public void onIceCandidate(final IceCandidate candidate) { public void onIceCandidate(final IceCandidate candidate) {
if (appRtcClient != null) { runOnUiThread(new Runnable() {
appRtcClient.sendLocalIceCandidate(candidate); @Override
} public void run() {
if (appRtcClient != null) {
appRtcClient.sendLocalIceCandidate(candidate);
}
}
});
} }
@Override @Override
public void onIceConnected() { public void onIceConnected() {
logAndToast("ICE connected"); runOnUiThread(new Runnable() {
iceConnected = true; @Override
updateVideoView(); public void run() {
logAndToast("ICE connected");
iceConnected = true;
updateVideoView();
}
});
} }
@Override @Override
public void onIceDisconnected() { public void onIceDisconnected() {
logAndToast("ICE disconnected"); runOnUiThread(new Runnable() {
disconnect(); @Override
public void run() {
logAndToast("ICE disconnected");
iceConnected = false;
disconnect();
}
});
} }
@Override @Override
@@ -600,10 +673,15 @@ public class AppRTCDemoActivity extends Activity
} }
@Override @Override
public void onPeerConnectionError(String description) { public void onPeerConnectionError(final String description) {
if (!isError) { runOnUiThread(new Runnable() {
isError = true; @Override
disconnectWithErrorMessage(description); public void run() {
} if (!isError) {
isError = true;
disconnectWithErrorMessage(description);
}
}
});
} }
} }

View File

@@ -27,15 +27,16 @@
package org.appspot.apprtc; package org.appspot.apprtc;
import org.appspot.apprtc.util.AppRTCUtils;
import org.appspot.apprtc.util.AppRTCUtils.NonThreadSafe;
import android.content.Context; import android.content.Context;
import android.hardware.Sensor; import android.hardware.Sensor;
import android.hardware.SensorEvent; import android.hardware.SensorEvent;
import android.hardware.SensorEventListener; import android.hardware.SensorEventListener;
import android.hardware.SensorManager; import android.hardware.SensorManager;
import android.os.Build;
import android.util.Log; import android.util.Log;
import java.util.List;
import org.appspot.apprtc.util.AppRTCUtils;
import org.appspot.apprtc.util.AppRTCUtils.NonThreadSafe;
/** /**
* AppRTCProximitySensor manages functions related to the proximity sensor in * AppRTCProximitySensor manages functions related to the proximity sensor in
@@ -161,16 +162,27 @@ public class AppRTCProximitySensor implements SensorEventListener {
if (proximitySensor == null) { if (proximitySensor == null) {
return; return;
} }
Log.d(TAG, "Proximity sensor: " + "name=" + proximitySensor.getName() StringBuilder info = new StringBuilder("Proximity sensor: ");
+ ", vendor: " + proximitySensor.getVendor() info.append("name=" + proximitySensor.getName());
+ ", type: " + proximitySensor.getStringType() info.append(", vendor: " + proximitySensor.getVendor());
+ ", reporting mode: " + proximitySensor.getReportingMode() info.append(", power: " + proximitySensor.getPower());
+ ", power: " + proximitySensor.getPower() info.append(", resolution: " + proximitySensor.getResolution());
+ ", min delay: " + proximitySensor.getMinDelay() info.append(", max range: " + proximitySensor.getMaximumRange());
+ ", max delay: " + proximitySensor.getMaxDelay() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
+ ", resolution: " + proximitySensor.getResolution() // Added in API level 9.
+ ", max range: " + proximitySensor.getMaximumRange() info.append(", min delay: " + proximitySensor.getMinDelay());
+ ", isWakeUpSensor: " + proximitySensor.isWakeUpSensor()); }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
// Added in API level 20.
info.append(", type: " + proximitySensor.getStringType());
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Added in API level 21.
info.append(", max delay: " + proximitySensor.getMaxDelay());
info.append(", reporting mode: " + proximitySensor.getReportingMode());
info.append(", isWakeUpSensor: " + proximitySensor.isWakeUpSensor());
}
Log.d(TAG, info.toString());
} }
/** /**

View File

@@ -27,11 +27,13 @@
package org.appspot.apprtc; package org.appspot.apprtc;
import android.os.Handler; import org.appspot.apprtc.AppRTCClient.SignalingParameters;
import android.os.Looper; import org.appspot.apprtc.util.LooperExecutor;
import android.content.Context;
import android.opengl.EGLContext;
import android.util.Log; import android.util.Log;
import org.appspot.apprtc.AppRTCClient.SignalingParameters;
import org.webrtc.DataChannel; import org.webrtc.DataChannel;
import org.webrtc.IceCandidate; import org.webrtc.IceCandidate;
import org.webrtc.MediaConstraints; import org.webrtc.MediaConstraints;
@@ -54,28 +56,32 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
/** /**
* PeerConnection client for AppRTC. * Peer connection client implementation.
*
* <p>All public methods are routed to local looper thread.
* All PeerConnectionEvents callbacks are invoked from the same looper thread.
*/ */
public class PeerConnectionClient { public class PeerConnectionClient {
private static final String TAG = "PCRTCClient"; private static final String TAG = "PCRTCClient";
public static final String VIDEO_TRACK_ID = "ARDAMSv0"; public static final String VIDEO_TRACK_ID = "ARDAMSv0";
public static final String AUDIO_TRACK_ID = "ARDAMSa0"; public static final String AUDIO_TRACK_ID = "ARDAMSa0";
private final Handler uiHandler; private final LooperExecutor executor;
private PeerConnectionFactory factory; private PeerConnectionFactory factory = null;
private PeerConnection pc; private PeerConnection pc = null;
private VideoSource videoSource; private VideoSource videoSource;
private boolean videoSourceStopped; private boolean videoSourceStopped = false;
private boolean isError = false;
private final PCObserver pcObserver = new PCObserver(); private final PCObserver pcObserver = new PCObserver();
private final SDPObserver sdpObserver = new SDPObserver(); private final SDPObserver sdpObserver = new SDPObserver();
private final VideoRenderer.Callbacks localRender; private VideoRenderer.Callbacks localRender;
private final VideoRenderer.Callbacks remoteRender; private VideoRenderer.Callbacks remoteRender;
private SignalingParameters signalingParameters;
// Queued remote ICE candidates are consumed only after both local and // Queued remote ICE candidates are consumed only after both local and
// remote descriptions are set. Similarly local ICE candidates are sent to // remote descriptions are set. Similarly local ICE candidates are sent to
// remote peer after both local and remote description are set. // remote peer after both local and remote description are set.
private LinkedList<IceCandidate> queuedRemoteCandidates = null; private LinkedList<IceCandidate> queuedRemoteCandidates = null;
private MediaConstraints sdpMediaConstraints; private MediaConstraints sdpMediaConstraints;
private MediaConstraints videoConstraints;
private PeerConnectionEvents events; private PeerConnectionEvents events;
private int startBitrate; private int startBitrate;
private boolean isInitiator; private boolean isInitiator;
@@ -83,184 +89,12 @@ public class PeerConnectionClient {
private SessionDescription localSdp = null; // either offer or answer SDP private SessionDescription localSdp = null; // either offer or answer SDP
private MediaStream mediaStream = null; private MediaStream mediaStream = null;
public PeerConnectionClient(
VideoRenderer.Callbacks localRender,
VideoRenderer.Callbacks remoteRender,
SignalingParameters signalingParameters,
PeerConnectionEvents events,
int startBitrate) {
this.localRender = localRender;
this.remoteRender = remoteRender;
this.events = events;
this.startBitrate = startBitrate;
uiHandler = new Handler(Looper.getMainLooper());
isInitiator = signalingParameters.initiator;
queuedRemoteCandidates = new LinkedList<IceCandidate>();
sdpMediaConstraints = new MediaConstraints();
sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
"OfferToReceiveAudio", "true"));
sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
"OfferToReceiveVideo", "true"));
videoConstraints = signalingParameters.videoConstraints;
factory = new PeerConnectionFactory();
MediaConstraints pcConstraints = signalingParameters.pcConstraints;
pcConstraints.optional.add(
new MediaConstraints.KeyValuePair("RtpDataChannels", "true"));
pc = factory.createPeerConnection(signalingParameters.iceServers,
pcConstraints, pcObserver);
isInitiator = false;
// Uncomment to get ALL WebRTC tracing and SENSITIVE libjingle logging.
// NOTE: this _must_ happen while |factory| is alive!
// Logging.enableTracing(
// "logcat:",
// EnumSet.of(Logging.TraceLevel.TRACE_ALL),
// Logging.Severity.LS_SENSITIVE);
mediaStream = factory.createLocalMediaStream("ARDAMS");
if (videoConstraints != null) {
mediaStream.addTrack(createVideoTrack(useFrontFacingCamera));
}
if (signalingParameters.audioConstraints != null) {
mediaStream.addTrack(factory.createAudioTrack(
AUDIO_TRACK_ID,
factory.createAudioSource(signalingParameters.audioConstraints)));
}
pc.addStream(mediaStream);
}
public boolean isHDVideo() {
if (videoConstraints == null) {
return false;
}
int minWidth = 0;
int minHeight = 0;
for (KeyValuePair keyValuePair : videoConstraints.mandatory) {
if (keyValuePair.getKey().equals("minWidth")) {
try {
minWidth = Integer.parseInt(keyValuePair.getValue());
} catch (NumberFormatException e) {
Log.e(TAG, "Can not parse video width from video constraints");
}
} else if (keyValuePair.getKey().equals("minHeight")) {
try {
minHeight = Integer.parseInt(keyValuePair.getValue());
} catch (NumberFormatException e) {
Log.e(TAG, "Can not parse video height from video constraints");
}
}
}
if (minWidth * minHeight >= 1280 * 720) {
return true;
} else {
return false;
}
}
public boolean getStats(StatsObserver observer, MediaStreamTrack track) {
return pc.getStats(observer, track);
}
public void createOffer() {
uiHandler.post(new Runnable() {
public void run() {
if (pc != null) {
isInitiator = true;
pc.createOffer(sdpObserver, sdpMediaConstraints);
}
}
});
}
public void createAnswer() {
uiHandler.post(new Runnable() {
public void run() {
if (pc != null) {
isInitiator = false;
pc.createAnswer(sdpObserver, sdpMediaConstraints);
}
}
});
}
public void addRemoteIceCandidate(final IceCandidate candidate) {
uiHandler.post(new Runnable() {
public void run() {
if (pc != null) {
if (queuedRemoteCandidates != null) {
queuedRemoteCandidates.add(candidate);
} else {
pc.addIceCandidate(candidate);
}
}
}
});
}
public void setRemoteDescription(final SessionDescription sdp) {
uiHandler.post(new Runnable() {
public void run() {
if (pc != null) {
String sdpDescription = preferISAC(sdp.description);
if (startBitrate > 0) {
sdpDescription = setStartBitrate(sdpDescription, startBitrate);
}
Log.d(TAG, "Set remote SDP.");
SessionDescription sdpRemote = new SessionDescription(
sdp.type, sdpDescription);
pc.setRemoteDescription(sdpObserver, sdpRemote);
}
}
});
}
public void stopVideoSource() {
if (videoSource != null) {
Log.d(TAG, "Stop video source.");
videoSource.stop();
videoSourceStopped = true;
}
}
public void startVideoSource() {
if (videoSource != null && videoSourceStopped) {
Log.d(TAG, "Restart video source.");
videoSource.restart();
videoSourceStopped = false;
}
}
public void close() {
uiHandler.post(new Runnable() {
public void run() {
Log.d(TAG, "Closing peer connection.");
if (pc != null) {
pc.dispose();
pc = null;
}
if (videoSource != null) {
videoSource.dispose();
videoSource = null;
}
if (factory != null) {
factory.dispose();
factory = null;
}
Log.d(TAG, "Closing peer connection done.");
events.onPeerConnectionClosed();
}
});
}
/** /**
* SDP/ICE ready callbacks. * SDP/ICE ready callbacks.
*/ */
public static interface PeerConnectionEvents { public static interface PeerConnectionEvents {
/** /**
* Callback fired once offer is created and local SDP is set. * Callback fired once local SDP is created and set.
*/ */
public void onLocalDescription(final SessionDescription sdp); public void onLocalDescription(final SessionDescription sdp);
@@ -289,15 +123,263 @@ public class PeerConnectionClient {
/** /**
* Callback fired once peer connection error happened. * Callback fired once peer connection error happened.
*/ */
public void onPeerConnectionError(String description); public void onPeerConnectionError(final String description);
} }
public PeerConnectionClient() {
executor = new LooperExecutor();
}
public void createPeerConnectionFactory(
final Context context,
final boolean vp8HwAcceleration,
final EGLContext renderEGLContext,
final PeerConnectionEvents events) {
this.events = events;
executor.requestStart();
executor.execute(new Runnable() {
@Override
public void run() {
createPeerConnectionFactoryInternal(
context, vp8HwAcceleration, renderEGLContext);
}
});
}
public void createPeerConnection(
final VideoRenderer.Callbacks localRender,
final VideoRenderer.Callbacks remoteRender,
final SignalingParameters signalingParameters,
final int startBitrate) {
this.localRender = localRender;
this.remoteRender = remoteRender;
this.signalingParameters = signalingParameters;
this.startBitrate = startBitrate;
executor.execute(new Runnable() {
@Override
public void run() {
createPeerConnectionInternal();
}
});
}
public void close() {
executor.execute(new Runnable() {
@Override
public void run() {
closeInternal();
}
});
executor.requestStop();
}
private void createPeerConnectionFactoryInternal(
Context context,
boolean vp8HwAcceleration,
EGLContext renderEGLContext) {
Log.d(TAG, "Create peer connection factory.");
isError = false;
if (!PeerConnectionFactory.initializeAndroidGlobals(
context, true, true, vp8HwAcceleration, renderEGLContext)) {
events.onPeerConnectionError("Failed to initializeAndroidGlobals");
}
factory = new PeerConnectionFactory();
Log.d(TAG, "Peer connection factory created.");
}
private void createPeerConnectionInternal() {
if (factory == null || isError) {
Log.e(TAG, "Peerconnection factory is not created");
return;
}
Log.d(TAG, "Create peer connection.");
isInitiator = signalingParameters.initiator;
queuedRemoteCandidates = new LinkedList<IceCandidate>();
sdpMediaConstraints = new MediaConstraints();
sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
"OfferToReceiveAudio", "true"));
sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
"OfferToReceiveVideo", "true"));
MediaConstraints pcConstraints = signalingParameters.pcConstraints;
pcConstraints.optional.add(
new MediaConstraints.KeyValuePair("RtpDataChannels", "true"));
pc = factory.createPeerConnection(signalingParameters.iceServers,
pcConstraints, pcObserver);
isInitiator = false;
// Uncomment to get ALL WebRTC tracing and SENSITIVE libjingle logging.
// NOTE: this _must_ happen while |factory| is alive!
// Logging.enableTracing(
// "logcat:",
// EnumSet.of(Logging.TraceLevel.TRACE_ALL),
// Logging.Severity.LS_SENSITIVE);
mediaStream = factory.createLocalMediaStream("ARDAMS");
if (signalingParameters.videoConstraints != null) {
mediaStream.addTrack(createVideoTrack(useFrontFacingCamera));
}
if (signalingParameters.audioConstraints != null) {
mediaStream.addTrack(factory.createAudioTrack(
AUDIO_TRACK_ID,
factory.createAudioSource(signalingParameters.audioConstraints)));
}
pc.addStream(mediaStream);
Log.d(TAG, "Peer connection created.");
}
private void closeInternal() {
Log.d(TAG, "Closing peer connection.");
if (pc != null) {
pc.dispose();
pc = null;
}
if (videoSource != null) {
videoSource.dispose();
videoSource = null;
}
Log.d(TAG, "Closing peer connection factory.");
if (factory != null) {
factory.dispose();
factory = null;
}
Log.d(TAG, "Closing peer connection done.");
events.onPeerConnectionClosed();
}
public boolean isHDVideo() {
if (signalingParameters.videoConstraints == null) {
return false;
}
int minWidth = 0;
int minHeight = 0;
for (KeyValuePair keyValuePair :
signalingParameters.videoConstraints.mandatory) {
if (keyValuePair.getKey().equals("minWidth")) {
try {
minWidth = Integer.parseInt(keyValuePair.getValue());
} catch (NumberFormatException e) {
Log.e(TAG, "Can not parse video width from video constraints");
}
} else if (keyValuePair.getKey().equals("minHeight")) {
try {
minHeight = Integer.parseInt(keyValuePair.getValue());
} catch (NumberFormatException e) {
Log.e(TAG, "Can not parse video height from video constraints");
}
}
}
if (minWidth * minHeight >= 1280 * 720) {
return true;
} else {
return false;
}
}
public boolean getStats(StatsObserver observer, MediaStreamTrack track) {
if (pc != null && !isError) {
return pc.getStats(observer, track);
} else {
return false;
}
}
public void createOffer() {
executor.execute(new Runnable() {
@Override
public void run() {
if (pc != null && !isError) {
isInitiator = true;
pc.createOffer(sdpObserver, sdpMediaConstraints);
}
}
});
}
public void createAnswer() {
executor.execute(new Runnable() {
@Override
public void run() {
if (pc != null && !isError) {
isInitiator = false;
pc.createAnswer(sdpObserver, sdpMediaConstraints);
}
}
});
}
public void addRemoteIceCandidate(final IceCandidate candidate) {
executor.execute(new Runnable() {
@Override
public void run() {
if (pc != null && !isError) {
if (queuedRemoteCandidates != null) {
queuedRemoteCandidates.add(candidate);
} else {
pc.addIceCandidate(candidate);
}
}
}
});
}
public void setRemoteDescription(final SessionDescription sdp) {
executor.execute(new Runnable() {
@Override
public void run() {
if (pc == null || isError) {
return;
}
String sdpDescription = preferISAC(sdp.description);
if (startBitrate > 0) {
sdpDescription = setStartBitrate(sdpDescription, startBitrate);
}
Log.d(TAG, "Set remote SDP.");
SessionDescription sdpRemote = new SessionDescription(
sdp.type, sdpDescription);
pc.setRemoteDescription(sdpObserver, sdpRemote);
}
});
}
public void stopVideoSource() {
executor.execute(new Runnable() {
@Override
public void run() {
if (videoSource != null && !videoSourceStopped) {
Log.d(TAG, "Stop video source.");
videoSource.stop();
videoSourceStopped = true;
}
}
});
}
public void startVideoSource() {
executor.execute(new Runnable() {
@Override
public void run() {
if (videoSource != null && videoSourceStopped) {
Log.d(TAG, "Restart video source.");
videoSource.restart();
videoSourceStopped = false;
}
}
});
}
private void reportError(final String errorMessage) { private void reportError(final String errorMessage) {
Log.e(TAG, "Peerconnection error: " + errorMessage); Log.e(TAG, "Peerconnection error: " + errorMessage);
uiHandler.post(new Runnable() { executor.execute(new Runnable() {
@Override
public void run() { public void run() {
events.onPeerConnectionError(errorMessage); if (!isError) {
events.onPeerConnectionError(errorMessage);
isError = true;
}
} }
}); });
} }
@@ -336,7 +418,8 @@ public class PeerConnectionClient {
videoSource.dispose(); videoSource.dispose();
} }
videoSource = factory.createVideoSource(capturer, videoConstraints); videoSource = factory.createVideoSource(
capturer, signalingParameters.videoConstraints);
String trackExtension = frontFacing ? "frontFacing" : "backFacing"; String trackExtension = frontFacing ? "frontFacing" : "backFacing";
VideoTrack videoTrack = VideoTrack videoTrack =
factory.createVideoTrack(VIDEO_TRACK_ID + trackExtension, videoSource); factory.createVideoTrack(VIDEO_TRACK_ID + trackExtension, videoSource);
@@ -344,13 +427,6 @@ public class PeerConnectionClient {
return videoTrack; return videoTrack;
} }
// Poor-man's assert(): die with |msg| unless |condition| is true.
private void abortUnless(boolean condition, String msg) {
if (!condition) {
reportError(msg);
}
}
private static String setStartBitrate( private static String setStartBitrate(
String sdpDescription, int bitrateKbps) { String sdpDescription, int bitrateKbps) {
String[] lines = sdpDescription.split("\r\n"); String[] lines = sdpDescription.split("\r\n");
@@ -443,16 +519,16 @@ public class PeerConnectionClient {
} }
} }
public void switchCamera() { private void switchCameraInternal() {
if (videoConstraints == null) { if (signalingParameters.videoConstraints == null) {
return; // No video is sent. return; // No video is sent.
} }
if (pc.signalingState() != PeerConnection.SignalingState.STABLE) { if (pc.signalingState() != PeerConnection.SignalingState.STABLE) {
Log.e(TAG, "Switching camera during negotiation is not handled."); Log.e(TAG, "Switching camera during negotiation is not handled.");
return; return;
} }
Log.d(TAG, "Switch camera");
pc.removeStream(mediaStream); pc.removeStream(mediaStream);
VideoTrack currentTrack = mediaStream.videoTracks.get(0); VideoTrack currentTrack = mediaStream.videoTracks.get(0);
mediaStream.removeTrack(currentTrack); mediaStream.removeTrack(currentTrack);
@@ -485,13 +561,26 @@ public class PeerConnectionClient {
pc.setRemoteDescription(new SwitchCameraSdbObserver(), remoteDesc); pc.setRemoteDescription(new SwitchCameraSdbObserver(), remoteDesc);
pc.setLocalDescription(new SwitchCameraSdbObserver(), localSdp); pc.setLocalDescription(new SwitchCameraSdbObserver(), localSdp);
} }
Log.d(TAG, "Switch camera done");
}
public void switchCamera() {
executor.execute(new Runnable() {
@Override
public void run() {
if (pc != null && !isError) {
switchCameraInternal();
}
}
});
} }
// Implementation detail: observe ICE & stream changes and react accordingly. // Implementation detail: observe ICE & stream changes and react accordingly.
private class PCObserver implements PeerConnection.Observer { private class PCObserver implements PeerConnection.Observer {
@Override @Override
public void onIceCandidate(final IceCandidate candidate){ public void onIceCandidate(final IceCandidate candidate){
uiHandler.post(new Runnable() { executor.execute(new Runnable() {
@Override
public void run() { public void run() {
events.onIceCandidate(candidate); events.onIceCandidate(candidate);
} }
@@ -506,23 +595,20 @@ public class PeerConnectionClient {
@Override @Override
public void onIceConnectionChange( public void onIceConnectionChange(
PeerConnection.IceConnectionState newState) { final PeerConnection.IceConnectionState newState) {
Log.d(TAG, "IceConnectionState: " + newState); executor.execute(new Runnable() {
if (newState == IceConnectionState.CONNECTED) { @Override
uiHandler.post(new Runnable() { public void run() {
public void run() { Log.d(TAG, "IceConnectionState: " + newState);
if (newState == IceConnectionState.CONNECTED) {
events.onIceConnected(); events.onIceConnected();
} } else if (newState == IceConnectionState.DISCONNECTED) {
});
} else if (newState == IceConnectionState.DISCONNECTED) {
uiHandler.post(new Runnable() {
public void run() {
events.onIceDisconnected(); events.onIceDisconnected();
} else if (newState == IceConnectionState.FAILED) {
reportError("ICE connection failed.");
} }
}); }
} else if (newState == IceConnectionState.FAILED) { });
reportError("ICE connection failed.");
}
} }
@Override @Override
@@ -533,11 +619,16 @@ public class PeerConnectionClient {
@Override @Override
public void onAddStream(final MediaStream stream){ public void onAddStream(final MediaStream stream){
uiHandler.post(new Runnable() { executor.execute(new Runnable() {
@Override
public void run() { public void run() {
abortUnless(stream.audioTracks.size() <= 1 if (pc == null || isError) {
&& stream.videoTracks.size() <= 1, return;
"Weird-looking stream: " + stream); }
if (stream.audioTracks.size() > 1 || stream.videoTracks.size() > 1) {
reportError("Weird-looking stream: " + stream);
return;
}
if (stream.videoTracks.size() == 1) { if (stream.videoTracks.size() == 1) {
stream.videoTracks.get(0).addRenderer( stream.videoTracks.get(0).addRenderer(
new VideoRenderer(remoteRender)); new VideoRenderer(remoteRender));
@@ -548,8 +639,12 @@ public class PeerConnectionClient {
@Override @Override
public void onRemoveStream(final MediaStream stream){ public void onRemoveStream(final MediaStream stream){
uiHandler.post(new Runnable() { executor.execute(new Runnable() {
@Override
public void run() { public void run() {
if (pc == null || isError) {
return;
}
stream.videoTracks.get(0).dispose(); stream.videoTracks.get(0).dispose();
} }
}); });
@@ -573,13 +668,17 @@ public class PeerConnectionClient {
private class SDPObserver implements SdpObserver { private class SDPObserver implements SdpObserver {
@Override @Override
public void onCreateSuccess(final SessionDescription origSdp) { public void onCreateSuccess(final SessionDescription origSdp) {
abortUnless(localSdp == null, "multiple SDP create?!?"); if (localSdp != null) {
reportError("Multiple SDP create.");
return;
}
final SessionDescription sdp = new SessionDescription( final SessionDescription sdp = new SessionDescription(
origSdp.type, preferISAC(origSdp.description)); origSdp.type, preferISAC(origSdp.description));
localSdp = sdp; localSdp = sdp;
uiHandler.post(new Runnable() { executor.execute(new Runnable() {
@Override
public void run() { public void run() {
if (pc != null) { if (pc != null && !isError) {
Log.d(TAG, "Set local SDP from " + sdp.type); Log.d(TAG, "Set local SDP from " + sdp.type);
pc.setLocalDescription(sdpObserver, sdp); pc.setLocalDescription(sdpObserver, sdp);
} }
@@ -589,9 +688,10 @@ public class PeerConnectionClient {
@Override @Override
public void onSetSuccess() { public void onSetSuccess() {
uiHandler.post(new Runnable() { executor.execute(new Runnable() {
@Override
public void run() { public void run() {
if (pc == null) { if (pc == null || isError) {
return; return;
} }
if (isInitiator) { if (isInitiator) {

View File

@@ -26,10 +26,12 @@
*/ */
package org.appspot.apprtc; package org.appspot.apprtc;
import android.os.AsyncTask; import org.appspot.apprtc.AppRTCClient.SignalingParameters;
import org.appspot.apprtc.util.AsyncHttpURLConnection;
import org.appspot.apprtc.util.AsyncHttpURLConnection.AsyncHttpEvents;
import android.util.Log; import android.util.Log;
import org.appspot.apprtc.AppRTCClient.SignalingParameters;
import org.json.JSONArray; import org.json.JSONArray;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
@@ -40,7 +42,6 @@ import org.webrtc.SessionDescription;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL; import java.net.URL;
import java.net.URLConnection; import java.net.URLConnection;
import java.util.LinkedList; import java.util.LinkedList;
@@ -50,12 +51,12 @@ import java.util.Scanner;
* AsyncTask that converts an AppRTC room URL into the set of signaling * AsyncTask that converts an AppRTC room URL into the set of signaling
* parameters to use with that room. * parameters to use with that room.
*/ */
public class RoomParametersFetcher public class RoomParametersFetcher {
extends AsyncTask<String, Void, SignalingParameters> {
private static final String TAG = "RoomRTCClient"; private static final String TAG = "RoomRTCClient";
private Exception exception = null; private final RoomParametersFetcherEvents events;
private RoomParametersFetcherEvents events = null; private final boolean loopback;
private boolean loopback; private final String registerUrl;
private AsyncHttpURLConnection httpConnection;
/** /**
* Room parameters fetcher callbacks. * Room parameters fetcher callbacks.
@@ -73,146 +74,125 @@ public class RoomParametersFetcher
public void onSignalingParametersError(final String description); public void onSignalingParametersError(final String description);
} }
public RoomParametersFetcher(RoomParametersFetcherEvents events, public RoomParametersFetcher(boolean loopback, String registerUrl,
boolean loopback) { final RoomParametersFetcherEvents events) {
super(); Log.d(TAG, "Connecting to room: " + registerUrl);
this.events = events;
this.loopback = loopback; this.loopback = loopback;
this.registerUrl = registerUrl;
this.events = events;
httpConnection = new AsyncHttpURLConnection("POST", registerUrl, null,
new AsyncHttpEvents() {
@Override
public void OnHttpError(String errorMessage) {
Log.e(TAG, "Room connection error: " + errorMessage);
events.onSignalingParametersError(errorMessage);
}
@Override
public void OnHttpComplete(String response) {
RoomHttpResponseParse(response);
}
});
httpConnection.send();
} }
@Override private void RoomHttpResponseParse(String response) {
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 {
Log.d(TAG, "Connecting to room: " + url);
HttpURLConnection connection =
(HttpURLConnection) new URL(url).openConnection();
connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.setDoInput(true);
InputStream responseStream = connection.getInputStream();
String response = drainStream(responseStream);
responseStream.close();
Log.d(TAG, "Room response: " + response); Log.d(TAG, "Room response: " + response);
JSONObject roomJson = new JSONObject(response); try {
LinkedList<IceCandidate> iceCandidates = null; LinkedList<IceCandidate> iceCandidates = null;
SessionDescription offerSdp = null; SessionDescription offerSdp = null;
JSONObject roomJson = new JSONObject(response);
String result = roomJson.getString("result"); String result = roomJson.getString("result");
if (!result.equals("SUCCESS")) { if (!result.equals("SUCCESS")) {
throw new JSONException(result); events.onSignalingParametersError("Room response error: " + result);
} return;
response = roomJson.getString("params"); }
roomJson = new JSONObject(response); response = roomJson.getString("params");
String roomId = roomJson.getString("room_id"); roomJson = new JSONObject(response);
String clientId = roomJson.getString("client_id"); String roomId = roomJson.getString("room_id");
String wssUrl = roomJson.getString("wss_url"); String clientId = roomJson.getString("client_id");
String wssPostUrl = roomJson.getString("wss_post_url"); String wssUrl = roomJson.getString("wss_url");
boolean initiator = (roomJson.getBoolean("is_initiator")); String wssPostUrl = roomJson.getString("wss_post_url");
String roomUrl = url.substring(0, url.indexOf("/register")); boolean initiator = (roomJson.getBoolean("is_initiator"));
if (!initiator) { String roomUrl =
iceCandidates = new LinkedList<IceCandidate>(); registerUrl.substring(0, registerUrl.indexOf("/register"));
String messagesString = roomJson.getString("messages"); if (!initiator) {
JSONArray messages = new JSONArray(messagesString); iceCandidates = new LinkedList<IceCandidate>();
for (int i = 0; i < messages.length(); ++i) { String messagesString = roomJson.getString("messages");
String messageString = messages.getString(i); JSONArray messages = new JSONArray(messagesString);
JSONObject message = new JSONObject(messageString); for (int i = 0; i < messages.length(); ++i) {
String messageType = message.getString("type"); String messageString = messages.getString(i);
Log.d(TAG, "GAE->C #" + i + " : " + messageString); JSONObject message = new JSONObject(messageString);
if (messageType.equals("offer")) { String messageType = message.getString("type");
offerSdp = new SessionDescription( Log.d(TAG, "GAE->C #" + i + " : " + messageString);
SessionDescription.Type.fromCanonicalForm(messageType), if (messageType.equals("offer")) {
message.getString("sdp")); offerSdp = new SessionDescription(
} else if (messageType.equals("candidate")) { SessionDescription.Type.fromCanonicalForm(messageType),
IceCandidate candidate = new IceCandidate( message.getString("sdp"));
message.getString("id"), } else if (messageType.equals("candidate")) {
message.getInt("label"), IceCandidate candidate = new IceCandidate(
message.getString("candidate")); message.getString("id"),
iceCandidates.add(candidate); message.getInt("label"),
} else { message.getString("candidate"));
Log.e(TAG, "Unknown message: " + messageString); iceCandidates.add(candidate);
} else {
Log.e(TAG, "Unknown message: " + messageString);
}
} }
} }
} Log.d(TAG, "RoomId: " + roomId + ". ClientId: " + clientId);
Log.d(TAG, "Initiator: " + initiator);
Log.d(TAG, "Room url: " + roomUrl);
Log.d(TAG, "WSS url: " + wssUrl);
Log.d(TAG, "WSS POST url: " + wssPostUrl);
Log.d(TAG, "RoomId: " + roomId + ". ClientId: " + clientId); LinkedList<PeerConnection.IceServer> iceServers =
Log.d(TAG, "Initiator: " + initiator); iceServersFromPCConfigJSON(roomJson.getString("pc_config"));
Log.d(TAG, "Room url: " + roomUrl); boolean isTurnPresent = false;
for (PeerConnection.IceServer server : iceServers) {
LinkedList<PeerConnection.IceServer> iceServers = Log.d(TAG, "IceServer: " + server);
iceServersFromPCConfigJSON(roomJson.getString("pc_config")); if (server.uri.startsWith("turn:")) {
boolean isTurnPresent = false; isTurnPresent = true;
for (PeerConnection.IceServer server : iceServers) { break;
Log.d(TAG, "IceServer: " + server); }
if (server.uri.startsWith("turn:")) {
isTurnPresent = true;
break;
} }
} if (!isTurnPresent) {
if (!isTurnPresent) { LinkedList<PeerConnection.IceServer> turnServers =
LinkedList<PeerConnection.IceServer> turnServers = requestTurnServers(roomJson.getString("turn_url"));
requestTurnServers(roomJson.getString("turn_url")); for (PeerConnection.IceServer turnServer : turnServers) {
for (PeerConnection.IceServer turnServer : turnServers) { Log.d(TAG, "TurnServer: " + turnServer);
Log.d(TAG, "TurnServer: " + turnServer); iceServers.add(turnServer);
iceServers.add(turnServer); }
} }
MediaConstraints pcConstraints = constraintsFromJSON(
roomJson.getString("pc_constraints"));
addDTLSConstraintIfMissing(pcConstraints, loopback);
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);
SignalingParameters params = new SignalingParameters(
iceServers, initiator,
pcConstraints, videoConstraints, audioConstraints,
roomUrl, roomId, clientId,
wssUrl, wssPostUrl,
offerSdp, iceCandidates);
events.onSignalingParametersReady(params);
} catch (JSONException e) {
events.onSignalingParametersError(
"Room JSON parsing error: " + e.toString());
} catch (IOException e) {
events.onSignalingParametersError("Room IO error: " + e.toString());
} }
MediaConstraints pcConstraints = constraintsFromJSON(
roomJson.getString("pc_constraints"));
addDTLSConstraintIfMissing(pcConstraints, loopback);
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,
roomUrl, roomId, clientId,
wssUrl, wssPostUrl,
offerSdp, iceCandidates);
} }
// Mimic Chrome and set DtlsSrtpKeyAgreement to true if not set to false by // Mimic Chrome and set DtlsSrtpKeyAgreement to true if not set to false by
@@ -245,7 +225,7 @@ public class RoomParametersFetcher
private String getAVConstraints ( private String getAVConstraints (
String type, String mediaConstraintsString) throws JSONException { String type, String mediaConstraintsString) throws JSONException {
JSONObject json = new JSONObject(mediaConstraintsString); JSONObject json = new JSONObject(mediaConstraintsString);
// Tricksy handling of values that are allowed to be (boolean or // Tricky handling of values that are allowed to be (boolean or
// MediaTrackConstraints) by the getUserMedia() spec. There are three // MediaTrackConstraints) by the getUserMedia() spec. There are three
// cases below. // cases below.
if (!json.has(type) || !json.optBoolean(type, true)) { if (!json.has(type) || !json.optBoolean(type, true)) {

View File

@@ -26,34 +26,35 @@
*/ */
package org.appspot.apprtc; package org.appspot.apprtc;
import android.os.Handler;
import android.os.Looper;
import android.util.Log; import android.util.Log;
import de.tavendo.autobahn.WebSocket.WebSocketConnectionObserver;
import de.tavendo.autobahn.WebSocketConnection; import de.tavendo.autobahn.WebSocketConnection;
import de.tavendo.autobahn.WebSocketException; import de.tavendo.autobahn.WebSocketException;
import de.tavendo.autobahn.WebSocket.WebSocketConnectionObserver;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.LinkedList;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.LinkedList;
import org.appspot.apprtc.util.AsyncHttpURLConnection;
import org.appspot.apprtc.util.AsyncHttpURLConnection.AsyncHttpEvents;
import org.appspot.apprtc.util.LooperExecutor;
/** /**
* WebSocket client implementation. * WebSocket client implementation.
* For proper synchronization all methods should be called from UI thread * All public methods should be called from a looper executor thread
* and all WebSocket events are delivered on UI thread as well. * passed in constructor.
* All events are issued on the same thread.
*/ */
public class WebSocketChannelClient { public class WebSocketChannelClient {
private final String TAG = "WSChannelRTCClient"; private static final String TAG = "WSChannelRTCClient";
private static final int CLOSE_TIMEOUT = 1000;
private final WebSocketChannelEvents events; private final WebSocketChannelEvents events;
private final Handler uiHandler; private final LooperExecutor executor;
private WebSocketConnection ws; private WebSocketConnection ws;
private WebSocketObserver wsObserver; private WebSocketObserver wsObserver;
private String wsServerUrl; private String wsServerUrl;
@@ -61,10 +62,15 @@ public class WebSocketChannelClient {
private String roomID; private String roomID;
private String clientID; private String clientID;
private WebSocketConnectionState state; private WebSocketConnectionState state;
private final Object closeEventLock = new Object();
private boolean closeEvent;
// WebSocket send queue. Messages are added to the queue when WebSocket // WebSocket send queue. Messages are added to the queue when WebSocket
// client is not registered and are consumed in register() call. // client is not registered and are consumed in register() call.
private LinkedList<String> wsSendQueue; private LinkedList<String> wsSendQueue;
/**
* WebSocketConnectionState is the names of possible WS connection states.
*/
public enum WebSocketConnectionState { public enum WebSocketConnectionState {
NEW, CONNECTED, REGISTERED, CLOSED, ERROR NEW, CONNECTED, REGISTERED, CLOSED, ERROR
}; };
@@ -80,9 +86,10 @@ public class WebSocketChannelClient {
public void onWebSocketError(final String description); public void onWebSocketError(final String description);
} }
public WebSocketChannelClient(WebSocketChannelEvents events) { public WebSocketChannelClient(LooperExecutor executor,
WebSocketChannelEvents events) {
this.executor = executor;
this.events = events; this.events = events;
uiHandler = new Handler(Looper.getMainLooper());
roomID = null; roomID = null;
clientID = null; clientID = null;
wsSendQueue = new LinkedList<String>(); wsSendQueue = new LinkedList<String>();
@@ -93,18 +100,22 @@ public class WebSocketChannelClient {
return state; return state;
} }
public void connect(String wsUrl, String postUrl) { public void connect(final String wsUrl, final String postUrl,
final String roomID, final String clientID) {
if (state != WebSocketConnectionState.NEW) { if (state != WebSocketConnectionState.NEW) {
Log.e(TAG, "WebSocket is already connected."); Log.e(TAG, "WebSocket is already connected.");
return; return;
} }
Log.d(TAG, "Connecting WebSocket to: " + wsUrl + ". Post URL: " + postUrl); wsServerUrl = wsUrl;
postServerUrl = postUrl;
this.roomID = roomID;
this.clientID = clientID;
closeEvent = false;
Log.d(TAG, "Connecting WebSocket to: " + wsUrl + ". Post URL: " + postUrl);
ws = new WebSocketConnection(); ws = new WebSocketConnection();
wsObserver = new WebSocketObserver(); wsObserver = new WebSocketObserver();
try { try {
wsServerUrl = wsUrl;
postServerUrl = postUrl;
ws.connect(new URI(wsServerUrl), wsObserver); ws.connect(new URI(wsServerUrl), wsObserver);
} catch (URISyntaxException e) { } catch (URISyntaxException e) {
reportError("URI error: " + e.getMessage()); reportError("URI error: " + e.getMessage());
@@ -113,20 +124,11 @@ public class WebSocketChannelClient {
} }
} }
public void setClientParameters(String roomID, String clientID) {
this.roomID = roomID;
this.clientID = clientID;
}
public void register() { public void register() {
if (state != WebSocketConnectionState.CONNECTED) { if (state != WebSocketConnectionState.CONNECTED) {
Log.w(TAG, "WebSocket register() in state " + state); Log.w(TAG, "WebSocket register() in state " + state);
return; return;
} }
if (roomID == null || clientID == null) {
Log.w(TAG, "Call WebSocket register() without setting client ID");
return;
}
JSONObject json = new JSONObject(); JSONObject json = new JSONObject();
try { try {
json.put("cmd", "register"); json.put("cmd", "register");
@@ -187,7 +189,7 @@ public class WebSocketChannelClient {
sendWSSMessage("POST", message); sendWSSMessage("POST", message);
} }
public void disconnect() { public void disconnect(boolean waitForComplete) {
Log.d(TAG, "Disonnect WebSocket. State: " + state); Log.d(TAG, "Disonnect WebSocket. State: " + state);
if (state == WebSocketConnectionState.REGISTERED) { if (state == WebSocketConnectionState.REGISTERED) {
send("{\"type\": \"bye\"}"); send("{\"type\": \"bye\"}");
@@ -202,12 +204,28 @@ public class WebSocketChannelClient {
sendWSSMessage("DELETE", ""); sendWSSMessage("DELETE", "");
state = WebSocketConnectionState.CLOSED; state = WebSocketConnectionState.CLOSED;
// Wait for websocket close event to prevent websocket library from
// sending any pending messages to deleted looper thread.
if (waitForComplete) {
synchronized (closeEventLock) {
if (!closeEvent) {
try {
closeEventLock.wait(CLOSE_TIMEOUT);
} catch (InterruptedException e) {
Log.e(TAG, "Wait error: " + e.toString());
}
}
}
}
} }
Log.d(TAG, "Disonnecting WebSocket done.");
} }
private void reportError(final String errorMessage) { private void reportError(final String errorMessage) {
Log.e(TAG, errorMessage); Log.e(TAG, errorMessage);
uiHandler.post(new Runnable() { executor.execute(new Runnable() {
@Override
public void run() { public void run() {
if (state != WebSocketConnectionState.ERROR) { if (state != WebSocketConnectionState.ERROR) {
state = WebSocketConnectionState.ERROR; state = WebSocketConnectionState.ERROR;
@@ -217,60 +235,30 @@ public class WebSocketChannelClient {
}); });
} }
private class WsHttpMessage {
WsHttpMessage(String method, String message) {
this.method = method;
this.message = message;
}
public final String method;
public final String message;
}
// Asynchronously send POST/DELETE to WebSocket server. // Asynchronously send POST/DELETE to WebSocket server.
private void sendWSSMessage(String method, String message) { private void sendWSSMessage(final String method, final String message) {
final WsHttpMessage wsHttpMessage = new WsHttpMessage(method, message); String postUrl = postServerUrl + "/" + roomID + "/" + clientID;
Runnable runAsync = new Runnable() { Log.d(TAG, "WS " + method + " : " + postUrl + " : " + message);
public void run() { AsyncHttpURLConnection httpConnection = new AsyncHttpURLConnection(
sendWSSMessageAsync(wsHttpMessage); method, postUrl, message, new AsyncHttpEvents() {
} @Override
}; public void OnHttpError(String errorMessage) {
new Thread(runAsync).start(); reportError("WS " + method + " error: " + errorMessage);
} }
private void sendWSSMessageAsync(WsHttpMessage wsHttpMessage) { @Override
if (roomID == null || clientID == null) { public void OnHttpComplete(String response) {
return; }
} });
try { httpConnection.send();
// Send POST or DELETE request.
String postUrl = postServerUrl + "/" + roomID + "/" + clientID;
Log.d(TAG, "WS " + wsHttpMessage.method + " : " + postUrl + " : "
+ wsHttpMessage.message);
HttpURLConnection connection =
(HttpURLConnection) new URL(postUrl).openConnection();
connection.setRequestProperty(
"content-type", "text/plain; charset=utf-8");
connection.setRequestMethod(wsHttpMessage.method);
if (wsHttpMessage.method.equals("POST")) {
connection.setDoOutput(true);
String message = wsHttpMessage.message;
connection.getOutputStream().write(message.getBytes("UTF-8"));
}
int responseCode = connection.getResponseCode();
if (responseCode != 200) {
reportError("Non-200 response to " + wsHttpMessage.method + " : "
+ connection.getHeaderField(null));
}
} catch (IOException e) {
reportError("WS POST error: " + e.getMessage());
}
} }
private class WebSocketObserver implements WebSocketConnectionObserver { private class WebSocketObserver implements WebSocketConnectionObserver {
@Override @Override
public void onOpen() { public void onOpen() {
Log.d(TAG, "WebSocket connection opened to: " + wsServerUrl); Log.d(TAG, "WebSocket connection opened to: " + wsServerUrl);
uiHandler.post(new Runnable() { executor.execute(new Runnable() {
@Override
public void run() { public void run() {
state = WebSocketConnectionState.CONNECTED; state = WebSocketConnectionState.CONNECTED;
events.onWebSocketOpen(); events.onWebSocketOpen();
@@ -281,8 +269,13 @@ public class WebSocketChannelClient {
@Override @Override
public void onClose(WebSocketCloseNotification code, String reason) { public void onClose(WebSocketCloseNotification code, String reason) {
Log.d(TAG, "WebSocket connection closed. Code: " + code Log.d(TAG, "WebSocket connection closed. Code: " + code
+ ". Reason: " + reason); + ". Reason: " + reason + ". State: " + state);
uiHandler.post(new Runnable() { synchronized (closeEventLock) {
closeEvent = true;
closeEventLock.notify();
}
executor.execute(new Runnable() {
@Override
public void run() { public void run() {
if (state != WebSocketConnectionState.CLOSED) { if (state != WebSocketConnectionState.CLOSED) {
state = WebSocketConnectionState.CLOSED; state = WebSocketConnectionState.CLOSED;
@@ -296,7 +289,8 @@ public class WebSocketChannelClient {
public void onTextMessage(String payload) { public void onTextMessage(String payload) {
Log.d(TAG, "WSS->C: " + payload); Log.d(TAG, "WSS->C: " + payload);
final String message = payload; final String message = payload;
uiHandler.post(new Runnable() { executor.execute(new Runnable() {
@Override
public void run() { public void run() {
if (state == WebSocketConnectionState.CONNECTED if (state == WebSocketConnectionState.CONNECTED
|| state == WebSocketConnectionState.REGISTERED) { || state == WebSocketConnectionState.REGISTERED) {

View File

@@ -26,17 +26,11 @@
*/ */
package org.appspot.apprtc; package org.appspot.apprtc;
import android.os.Handler;
import android.os.Looper;
import android.util.Log; import android.util.Log;
import java.io.IOException; import org.appspot.apprtc.util.AsyncHttpURLConnection;
import java.io.InputStream; import org.appspot.apprtc.util.AsyncHttpURLConnection.AsyncHttpEvents;
import java.io.OutputStream; import org.appspot.apprtc.util.LooperExecutor;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Scanner;
import org.appspot.apprtc.RoomParametersFetcher.RoomParametersFetcherEvents; import org.appspot.apprtc.RoomParametersFetcher.RoomParametersFetcherEvents;
import org.appspot.apprtc.WebSocketChannelClient.WebSocketChannelEvents; import org.appspot.apprtc.WebSocketChannelClient.WebSocketChannelEvents;
import org.appspot.apprtc.WebSocketChannelClient.WebSocketConnectionState; import org.appspot.apprtc.WebSocketChannelClient.WebSocketConnectionState;
@@ -56,7 +50,7 @@ import org.webrtc.SessionDescription;
* be sent after WebSocket connection is established. * be sent after WebSocket connection is established.
*/ */
public class WebSocketRTCClient implements AppRTCClient, public class WebSocketRTCClient implements AppRTCClient,
RoomParametersFetcherEvents, WebSocketChannelEvents { WebSocketChannelEvents {
private static final String TAG = "WSRTCClient"; private static final String TAG = "WSRTCClient";
private enum ConnectionState { private enum ConnectionState {
@@ -65,7 +59,7 @@ public class WebSocketRTCClient implements AppRTCClient,
private enum MessageType { private enum MessageType {
MESSAGE, BYE MESSAGE, BYE
}; };
private final Handler uiHandler; private final LooperExecutor executor;
private boolean loopback; private boolean loopback;
private boolean initiator; private boolean initiator;
private SignalingEvents events; private SignalingEvents events;
@@ -77,14 +71,81 @@ public class WebSocketRTCClient implements AppRTCClient,
public WebSocketRTCClient(SignalingEvents events) { public WebSocketRTCClient(SignalingEvents events) {
this.events = events; this.events = events;
uiHandler = new Handler(Looper.getMainLooper()); executor = new LooperExecutor();
} }
// -------------------------------------------------------------------- // --------------------------------------------------------------------
// RoomConnectionEvents interface implementation. // AppRTCClient interface implementation.
// All events are called on UI thread. // Asynchronously connect to an AppRTC room URL, e.g.
// https://apprtc.appspot.com/register/<room>, retrieve room parameters
// and connect to WebSocket server.
@Override @Override
public void onSignalingParametersReady(final SignalingParameters params) { public void connectToRoom(final String url, final boolean loopback) {
executor.requestStart();
executor.execute(new Runnable() {
@Override
public void run() {
connectToRoomInternal(url, loopback);
}
});
}
@Override
public void disconnectFromRoom() {
executor.execute(new Runnable() {
@Override
public void run() {
disconnectFromRoomInternal();
}
});
executor.requestStop();
}
// Connects to room - function runs on a local looper thread.
private void connectToRoomInternal(String url, boolean loopback) {
Log.d(TAG, "Connect to room: " + url);
this.loopback = loopback;
roomState = ConnectionState.NEW;
// Create WebSocket client.
wsClient = new WebSocketChannelClient(executor, this);
// Get room parameters.
fetcher = new RoomParametersFetcher(loopback, url,
new RoomParametersFetcherEvents() {
@Override
public void onSignalingParametersReady(
final SignalingParameters params) {
executor.execute(new Runnable() {
@Override
public void run() {
signalingParametersReady(params);
}
});
}
@Override
public void onSignalingParametersError(String description) {
reportError(description);
}
}
);
}
// Disconnect from room and send bye messages - runs on a local looper thread.
private void disconnectFromRoomInternal() {
Log.d(TAG, "Disconnect. Room state: " + roomState);
if (roomState == ConnectionState.CONNECTED) {
Log.d(TAG, "Closing room.");
sendPostMessage(MessageType.BYE, byeMessageUrl, "");
}
roomState = ConnectionState.CLOSED;
if (wsClient != null) {
wsClient.disconnect(true);
}
}
// Callback issued when room parameters are extracted. Runs on local
// looper thread.
private void signalingParametersReady(final SignalingParameters params) {
Log.d(TAG, "Room connection completed."); Log.d(TAG, "Room connection completed.");
if (loopback && (!params.initiator || params.offerSdp != null)) { if (loopback && (!params.initiator || params.offerSdp != null)) {
reportError("Loopback room is busy."); reportError("Loopback room is busy.");
@@ -100,15 +161,16 @@ public class WebSocketRTCClient implements AppRTCClient,
+ params.roomId + "/" + params.clientId; + params.roomId + "/" + params.clientId;
roomState = ConnectionState.CONNECTED; roomState = ConnectionState.CONNECTED;
// Connect to WebSocket server.
wsClient.connect(params.wssUrl, params.wssPostUrl);
wsClient.setClientParameters(params.roomId, params.clientId);
// Fire connection and signaling parameters events. // Fire connection and signaling parameters events.
events.onConnectedToRoom(params); events.onConnectedToRoom(params);
// Connect to WebSocket server.
wsClient.connect(
params.wssUrl, params.wssPostUrl, params.roomId, params.clientId);
// For call receiver get sdp offer and ice candidates
// from room parameters and fire corresponding events.
if (!params.initiator) { if (!params.initiator) {
// For call receiver get sdp offer and ice candidates
// from room parameters.
if (params.offerSdp != null) { if (params.offerSdp != null) {
events.onRemoteDescription(params.offerSdp); events.onRemoteDescription(params.offerSdp);
} }
@@ -120,14 +182,90 @@ public class WebSocketRTCClient implements AppRTCClient,
} }
} }
// Send local offer SDP to the other participant.
@Override @Override
public void onSignalingParametersError(final String description) { public void sendOfferSdp(final SessionDescription sdp) {
reportError("Room connection error: " + description); executor.execute(new Runnable() {
@Override
public void run() {
if (roomState != ConnectionState.CONNECTED) {
reportError("Sending offer SDP in non connected state.");
return;
}
JSONObject json = new JSONObject();
jsonPut(json, "sdp", sdp.description);
jsonPut(json, "type", "offer");
sendPostMessage(MessageType.MESSAGE, postMessageUrl, json.toString());
if (loopback) {
// In loopback mode rename this offer to answer and route it back.
SessionDescription sdpAnswer = new SessionDescription(
SessionDescription.Type.fromCanonicalForm("answer"),
sdp.description);
events.onRemoteDescription(sdpAnswer);
}
}
});
}
// Send local answer SDP to the other participant.
@Override
public void sendAnswerSdp(final SessionDescription sdp) {
executor.execute(new Runnable() {
@Override
public void run() {
if (loopback) {
Log.e(TAG, "Sending answer in loopback mode.");
return;
}
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) {
executor.execute(new Runnable() {
@Override
public void run() {
JSONObject json = new JSONObject();
jsonPut(json, "type", "candidate");
jsonPut(json, "label", candidate.sdpMLineIndex);
jsonPut(json, "id", candidate.sdpMid);
jsonPut(json, "candidate", candidate.sdp);
if (initiator) {
// Call initiator sends ice candidates to GAE server.
if (roomState != ConnectionState.CONNECTED) {
reportError("Sending ICE candidate in non connected state.");
return;
}
sendPostMessage(MessageType.MESSAGE, postMessageUrl, json.toString());
if (loopback) {
events.onRemoteIceCandidate(candidate);
}
} else {
// Call receiver sends ice candidates to websocket server.
if (wsClient.getState() != WebSocketConnectionState.REGISTERED) {
reportError("Sending ICE candidate in non registered state.");
return;
}
wsClient.send(json.toString());
}
}
});
} }
// -------------------------------------------------------------------- // --------------------------------------------------------------------
// WebSocketChannelEvents interface implementation. // WebSocketChannelEvents interface implementation.
// All events are called on UI thread. // All events are called by WebSocketChannelClient on a local looper thread
// (passed to WebSocket client constructor).
@Override @Override
public void onWebSocketOpen() { public void onWebSocketOpen() {
Log.d(TAG, "Websocket connection completed. Registering..."); Log.d(TAG, "Websocket connection completed. Registering...");
@@ -198,108 +336,12 @@ public class WebSocketRTCClient implements AppRTCClient,
reportError("WebSocket error: " + description); reportError("WebSocket error: " + description);
} }
// --------------------------------------------------------------------
// AppRTCClient interface implementation.
// Asynchronously connect to an AppRTC room URL, e.g.
// https://apprtc.appspot.com/register/<room>, retrieve room parameters
// and connect to WebSocket server.
@Override
public void connectToRoom(String url, boolean loopback) {
this.loopback = loopback;
// Create WebSocket client.
wsClient = new WebSocketChannelClient(this);
// Get room parameters.
roomState = ConnectionState.NEW;
fetcher = new RoomParametersFetcher(this, loopback);
fetcher.execute(url);
}
@Override
public void disconnect() {
Log.d(TAG, "Disconnect. Room state: " + roomState);
if (roomState == ConnectionState.CONNECTED) {
Log.d(TAG, "Closing room.");
sendPostMessage(MessageType.BYE, byeMessageUrl, "");
}
roomState = ConnectionState.CLOSED;
if (wsClient != null) {
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) {
if (roomState != ConnectionState.CONNECTED) {
reportError("Sending offer SDP in non connected state.");
return;
}
JSONObject json = new JSONObject();
jsonPut(json, "sdp", sdp.description);
jsonPut(json, "type", "offer");
sendPostMessage(MessageType.MESSAGE, postMessageUrl, json.toString());
if (loopback) {
// In loopback mode rename this offer to answer and route it back.
SessionDescription sdpAnswer = new SessionDescription(
SessionDescription.Type.fromCanonicalForm("answer"),
sdp.description);
events.onRemoteDescription(sdpAnswer);
}
}
@Override
public void sendAnswerSdp(final SessionDescription sdp) {
if (loopback) {
Log.e(TAG, "Sending answer in loopback mode.");
return;
}
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) {
JSONObject json = new JSONObject();
jsonPut(json, "type", "candidate");
jsonPut(json, "label", candidate.sdpMLineIndex);
jsonPut(json, "id", candidate.sdpMid);
jsonPut(json, "candidate", candidate.sdp);
if (initiator) {
// Call initiator sends ice candidates to GAE server.
if (roomState != ConnectionState.CONNECTED) {
reportError("Sending ICE candidate in non connected state.");
return;
}
sendPostMessage(MessageType.MESSAGE, postMessageUrl, json.toString());
if (loopback) {
events.onRemoteIceCandidate(candidate);
}
} else {
// Call receiver sends ice candidates to websocket server.
if (wsClient.getState() != WebSocketConnectionState.REGISTERED) {
reportError("Sending ICE candidate in non registered state.");
return;
}
wsClient.send(json.toString());
}
}
// -------------------------------------------------------------------- // --------------------------------------------------------------------
// Helper functions. // Helper functions.
private void reportError(final String errorMessage) { private void reportError(final String errorMessage) {
Log.e(TAG, errorMessage); Log.e(TAG, errorMessage);
uiHandler.post(new Runnable() { executor.execute(new Runnable() {
@Override
public void run() { public void run() {
if (roomState != ConnectionState.ERROR) { if (roomState != ConnectionState.ERROR) {
roomState = ConnectionState.ERROR; roomState = ConnectionState.ERROR;
@@ -318,81 +360,36 @@ public class WebSocketRTCClient implements AppRTCClient,
} }
} }
private class PostMessage { // Send SDP or ICE candidate to a room server.
PostMessage(MessageType type, String postUrl, String message) { private void sendPostMessage(
this.messageType = type; final MessageType messageType, final String url, final String message) {
this.postUrl = postUrl; if (messageType == MessageType.BYE) {
this.message = message; Log.d(TAG, "C->GAE: " + url);
}
public final MessageType messageType;
public final String postUrl;
public final String message;
}
// Queue a message for sending to the room and send it if already connected.
private synchronized void sendPostMessage(
MessageType messageType, String url, String message) {
final PostMessage postMessage = new PostMessage(messageType, url, message);
Runnable runDrain = new Runnable() {
public void run() {
sendPostMessageAsync(postMessage);
}
};
new Thread(runDrain).start();
}
// Send all queued POST messages to app engine server.
private void sendPostMessageAsync(PostMessage postMessage) {
if (postMessage.messageType == MessageType.BYE) {
Log.d(TAG, "C->GAE: " + postMessage.postUrl);
} else { } else {
Log.d(TAG, "C->GAE: " + postMessage.message); Log.d(TAG, "C->GAE: " + message);
} }
try { AsyncHttpURLConnection httpConnection = new AsyncHttpURLConnection(
// Get connection. "POST", url, message, new AsyncHttpEvents() {
HttpURLConnection connection = @Override
(HttpURLConnection) new URL(postMessage.postUrl).openConnection(); public void OnHttpError(String errorMessage) {
byte[] postData = postMessage.message.getBytes("UTF-8"); reportError("GAE POST error: " + errorMessage);
connection.setUseCaches(false);
connection.setDoOutput(true);
connection.setDoInput(true);
connection.setRequestMethod("POST");
connection.setFixedLengthStreamingMode(postData.length);
connection.setRequestProperty(
"content-type", "text/plain; charset=utf-8");
// Send POST request.
OutputStream outStream = connection.getOutputStream();
outStream.write(postData);
outStream.close();
// Get response.
int responseCode = connection.getResponseCode();
if (responseCode != 200) {
reportError("Non-200 response to POST: "
+ connection.getHeaderField(null));
}
InputStream responseStream = connection.getInputStream();
String response = drainStream(responseStream);
responseStream.close();
if (postMessage.messageType == MessageType.MESSAGE) {
JSONObject roomJson = new JSONObject(response);
String result = roomJson.getString("result");
if (!result.equals("SUCCESS")) {
reportError("Room POST error: " + result);
} }
}
} catch (IOException e) {
reportError("GAE POST error: " + e.getMessage());
} catch (JSONException e) {
reportError("GAE POST JSON error: " + e.getMessage());
}
}
// Return the contents of an InputStream as a String. @Override
private String drainStream(InputStream in) { public void OnHttpComplete(String response) {
Scanner s = new Scanner(in).useDelimiter("\\A"); if (messageType == MessageType.MESSAGE) {
return s.hasNext() ? s.next() : ""; try {
JSONObject roomJson = new JSONObject(response);
String result = roomJson.getString("result");
if (!result.equals("SUCCESS")) {
reportError("GAE POST error: " + result);
}
} catch (JSONException e) {
reportError("GAE POST JSON error: " + e.toString());
}
}
}
});
httpConnection.send();
} }
} }

View File

@@ -0,0 +1,123 @@
/*
* libjingle
* Copyright 2015, 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.util;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.util.Scanner;
/**
* Asynchronious http requests implementation.
*/
public class AsyncHttpURLConnection {
private static final int HTTP_TIMEOUT_MS = 5000;
private final String method;
private final String url;
private final String message;
private final AsyncHttpEvents events;
public interface AsyncHttpEvents {
public void OnHttpError(String errorMessage);
public void OnHttpComplete(String response);
}
public AsyncHttpURLConnection(String method, String url, String message,
AsyncHttpEvents events) {
this.method = method;
this.url = url;
this.message = message;
this.events = events;
}
public void send() {
Runnable runHttp = new Runnable() {
public void run() {
sendHttpMessage();
}
};
new Thread(runHttp).start();
}
private void sendHttpMessage() {
try {
HttpURLConnection connection =
(HttpURLConnection) new URL(url).openConnection();
byte[] postData = new byte[0];
if (message != null) {
postData = message.getBytes("UTF-8");
}
connection.setRequestMethod(method);
connection.setUseCaches(false);
connection.setDoInput(true);
connection.setConnectTimeout(HTTP_TIMEOUT_MS);
connection.setReadTimeout(HTTP_TIMEOUT_MS);
boolean doOutput = false;
if (method.equals("POST")) {
doOutput = true;
connection.setDoOutput(true);
connection.setFixedLengthStreamingMode(postData.length);
}
connection.setRequestProperty(
"content-type", "text/plain; charset=utf-8");
// Send POST request.
if (doOutput && postData.length > 0) {
OutputStream outStream = connection.getOutputStream();
outStream.write(postData);
outStream.close();
}
// Get response.
int responseCode = connection.getResponseCode();
if (responseCode != 200) {
events.OnHttpError("Non-200 response to " + method + " to URL: "
+ url + " : " + connection.getHeaderField(null));
return;
}
InputStream responseStream = connection.getInputStream();
String response = drainStream(responseStream);
responseStream.close();
events.OnHttpComplete(response);
} catch (SocketTimeoutException e) {
events.OnHttpError("HTTP " + method + " to " + url + " timeout");
} catch (IOException e) {
events.OnHttpError("HTTP " + method + " to " + url + " error: "
+ e.getMessage());
}
}
// 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() : "";
}
}

View File

@@ -0,0 +1,107 @@
/*
* libjingle
* Copyright 2015, 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.util;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import java.util.concurrent.Executor;
/**
* Looper based executor class.
*/
public class LooperExecutor extends Thread implements Executor {
private static final String TAG = "LooperExecutor";
// Object used to signal that looper thread has started and Handler instance
// associated with looper thread has been allocated.
private final Object looperStartedEvent = new Object();
private Handler handler = null;
private boolean running = false;
private long threadId;
@Override
public void run() {
Looper.prepare();
synchronized (looperStartedEvent) {
Log.d(TAG, "Looper thread started.");
handler = new Handler();
threadId = Thread.currentThread().getId();
looperStartedEvent.notify();
}
Looper.loop();
}
public synchronized void requestStart() {
if (running) {
return;
}
running = true;
handler = null;
start();
// Wait for Hander allocation.
synchronized (looperStartedEvent) {
while (handler == null) {
try {
looperStartedEvent.wait();
} catch (InterruptedException e) {
Log.e(TAG, "Can not start looper thread");
running = false;
}
}
}
}
public synchronized void requestStop() {
if (!running) {
return;
}
running = false;
handler.post( new Runnable() {
@Override
public void run() {
Looper.myLooper().quitSafely();
Log.d(TAG, "Looper thread finished.");
}
});
}
@Override
public synchronized void execute(final Runnable runnable) {
if (!running) {
Log.w(TAG, "Running looper executor without calling requestStart()");
return;
}
if (Thread.currentThread().getId() == threadId) {
runnable.run();
} else {
handler.post(runnable);
}
}
}

View File

@@ -0,0 +1,84 @@
/*
* libjingle
* Copyright 2015, 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.test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.appspot.apprtc.util.LooperExecutor;
import android.test.InstrumentationTestCase;
import android.util.Log;
public class LooperExecutorTest extends InstrumentationTestCase {
private static final String TAG = "LooperTest";
private static final int WAIT_TIMEOUT = 5000;
public void testLooperExecutor() throws InterruptedException {
Log.d(TAG, "testLooperExecutor");
final int counter[] = new int[1];
final int expectedCounter = 10;
final CountDownLatch looperDone = new CountDownLatch(1);
Runnable counterIncRunnable = new Runnable() {
@Override
public void run() {
counter[0]++;
Log.d(TAG, "Run " + counter[0]);
}
};
LooperExecutor executor = new LooperExecutor();
// Try to execute a counter increment task before starting an executor.
executor.execute(counterIncRunnable);
// Start the executor and run expected amount of counter increment task.
executor.requestStart();
for (int i = 0; i < expectedCounter; i++) {
executor.execute(counterIncRunnable);
}
executor.execute(new Runnable() {
@Override
public void run() {
looperDone.countDown();
}
});
executor.requestStop();
// Try to execute a task after stopping the executor.
executor.execute(counterIncRunnable);
// Wait for final looper task and make sure the counter increment task
// is executed expected amount of times.
looperDone.await(WAIT_TIMEOUT, TimeUnit.MILLISECONDS);
assertTrue (looperDone.getCount() == 0);
assertTrue (counter[0] == expectedCounter);
Log.d(TAG, "testLooperExecutor done");
}
}

View File

@@ -35,10 +35,10 @@ import java.util.concurrent.TimeUnit;
import org.appspot.apprtc.AppRTCClient.SignalingParameters; import org.appspot.apprtc.AppRTCClient.SignalingParameters;
import org.appspot.apprtc.PeerConnectionClient; import org.appspot.apprtc.PeerConnectionClient;
import org.appspot.apprtc.PeerConnectionClient.PeerConnectionEvents; import org.appspot.apprtc.PeerConnectionClient.PeerConnectionEvents;
import org.appspot.apprtc.util.LooperExecutor;
import org.webrtc.IceCandidate; import org.webrtc.IceCandidate;
import org.webrtc.MediaConstraints; import org.webrtc.MediaConstraints;
import org.webrtc.PeerConnection; import org.webrtc.PeerConnection;
import org.webrtc.PeerConnectionFactory;
import org.webrtc.SessionDescription; import org.webrtc.SessionDescription;
import org.webrtc.VideoRenderer; import org.webrtc.VideoRenderer;
@@ -49,7 +49,7 @@ public class PeerConnectionClientTest extends InstrumentationTestCase
implements PeerConnectionEvents { implements PeerConnectionEvents {
private static final String TAG = "RTCClientTest"; private static final String TAG = "RTCClientTest";
private static final String STUN_SERVER = "stun:stun.l.google.com:19302"; private static final String STUN_SERVER = "stun:stun.l.google.com:19302";
private static final int WAIT_TIMEOUT = 3000; private static final int WAIT_TIMEOUT = 5000;
private static final int EXPECTED_VIDEO_FRAMES = 15; private static final int EXPECTED_VIDEO_FRAMES = 15;
private volatile PeerConnectionClient pcClient; private volatile PeerConnectionClient pcClient;
@@ -222,9 +222,6 @@ public class PeerConnectionClientTest extends InstrumentationTestCase
isClosed = false; isClosed = false;
isIceConnected = false; isIceConnected = false;
loopback = false; loopback = false;
Log.d(TAG, "initializeAndroidGlobals");
assertTrue(PeerConnectionFactory.initializeAndroidGlobals(
getInstrumentation().getContext(), true, true, true, null));
} }
public void testInitiatorCreation() throws InterruptedException { public void testInitiatorCreation() throws InterruptedException {
@@ -233,8 +230,11 @@ public class PeerConnectionClientTest extends InstrumentationTestCase
MockRenderer remoteRender = new MockRenderer(EXPECTED_VIDEO_FRAMES); MockRenderer remoteRender = new MockRenderer(EXPECTED_VIDEO_FRAMES);
SignalingParameters signalingParameters = getTestSignalingParameters(); SignalingParameters signalingParameters = getTestSignalingParameters();
pcClient = new PeerConnectionClient( pcClient = new PeerConnectionClient();
localRender, remoteRender, signalingParameters, this, 1000); pcClient.createPeerConnectionFactory(
getInstrumentation().getContext(), true, null, this);
pcClient.createPeerConnection(
localRender, remoteRender, signalingParameters, 1000);
pcClient.createOffer(); pcClient.createOffer();
// Wait for local SDP and ice candidates set events. // Wait for local SDP and ice candidates set events.
@@ -258,8 +258,12 @@ public class PeerConnectionClientTest extends InstrumentationTestCase
MockRenderer remoteRender = new MockRenderer(EXPECTED_VIDEO_FRAMES); MockRenderer remoteRender = new MockRenderer(EXPECTED_VIDEO_FRAMES);
SignalingParameters signalingParameters = getTestSignalingParameters(); SignalingParameters signalingParameters = getTestSignalingParameters();
loopback = true; loopback = true;
pcClient = new PeerConnectionClient(
localRender, remoteRender, signalingParameters, this, 1000); pcClient = new PeerConnectionClient();
pcClient.createPeerConnectionFactory(
getInstrumentation().getContext(), true, null, this);
pcClient.createPeerConnection(
localRender, remoteRender, signalingParameters, 1000);
pcClient.createOffer(); pcClient.createOffer();
// Wait for local SDP, rename it to answer and set as remote SDP. // Wait for local SDP, rename it to answer and set as remote SDP.
@@ -284,5 +288,4 @@ public class PeerConnectionClientTest extends InstrumentationTestCase
assertTrue(waitForPeerConnectionClosed(WAIT_TIMEOUT)); assertTrue(waitForPeerConnectionClosed(WAIT_TIMEOUT));
Log.d(TAG, "testLoopback Done."); Log.d(TAG, "testLoopback Done.");
} }
} }

View File

@@ -320,7 +320,6 @@
'examples/android/AndroidManifest.xml', 'examples/android/AndroidManifest.xml',
'examples/android/README', 'examples/android/README',
'examples/android/ant.properties', 'examples/android/ant.properties',
'examples/android/assets/channel.html',
'examples/android/third_party/autobanh/autobanh.jar', 'examples/android/third_party/autobanh/autobanh.jar',
'examples/android/build.xml', 'examples/android/build.xml',
'examples/android/jni/Android.mk', 'examples/android/jni/Android.mk',
@@ -362,9 +361,11 @@
'examples/android/src/org/appspot/apprtc/SettingsActivity.java', 'examples/android/src/org/appspot/apprtc/SettingsActivity.java',
'examples/android/src/org/appspot/apprtc/SettingsFragment.java', 'examples/android/src/org/appspot/apprtc/SettingsFragment.java',
'examples/android/src/org/appspot/apprtc/UnhandledExceptionHandler.java', 'examples/android/src/org/appspot/apprtc/UnhandledExceptionHandler.java',
'examples/android/src/org/appspot/apprtc/util/AppRTCUtils.java',
'examples/android/src/org/appspot/apprtc/WebSocketChannelClient.java', 'examples/android/src/org/appspot/apprtc/WebSocketChannelClient.java',
'examples/android/src/org/appspot/apprtc/WebSocketRTCClient.java', 'examples/android/src/org/appspot/apprtc/WebSocketRTCClient.java',
'examples/android/src/org/appspot/apprtc/util/AppRTCUtils.java',
'examples/android/src/org/appspot/apprtc/util/AsyncHttpURLConnection.java',
'examples/android/src/org/appspot/apprtc/util/LooperExecutor.java',
], ],
'outputs': [ 'outputs': [
'<(PRODUCT_DIR)/AppRTCDemo-debug.apk', '<(PRODUCT_DIR)/AppRTCDemo-debug.apk',
@@ -412,6 +413,7 @@
'examples/androidtests/ant.properties', 'examples/androidtests/ant.properties',
'examples/androidtests/build.xml', 'examples/androidtests/build.xml',
'examples/androidtests/project.properties', 'examples/androidtests/project.properties',
'examples/androidtests/src/org/appspot/apprtc/test/LooperExecutorTest.java',
'examples/androidtests/src/org/appspot/apprtc/test/PeerConnectionClientTest.java', 'examples/androidtests/src/org/appspot/apprtc/test/PeerConnectionClientTest.java',
], ],
'outputs': [ 'outputs': [