diff --git a/talk/app/webrtc/java/android/org/webrtc/VideoRendererGui.java b/talk/app/webrtc/java/android/org/webrtc/VideoRendererGui.java
index d9d9b4a61..160cfbf3f 100644
--- a/talk/app/webrtc/java/android/org/webrtc/VideoRendererGui.java
+++ b/talk/app/webrtc/java/android/org/webrtc/VideoRendererGui.java
@@ -56,6 +56,7 @@ import org.webrtc.VideoRenderer.I420Frame;
*/
public class VideoRendererGui implements GLSurfaceView.Renderer {
private static VideoRendererGui instance = null;
+ private static Runnable eglContextReady = null;
private static final String TAG = "VideoRendererGui";
private GLSurfaceView surface;
private static EGLContext eglContext = null;
@@ -595,9 +596,11 @@ public class VideoRendererGui implements GLSurfaceView.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");
instance = new VideoRendererGui(surface);
+ eglContextReady = eglContextReadyCallback;
}
public static EGLContext getEGLContext() {
@@ -690,7 +693,7 @@ public class VideoRendererGui implements GLSurfaceView.Renderer {
@Override
public void onSurfaceCreated(GL10 unused, EGLConfig config) {
Log.d(TAG, "VideoRendererGui.onSurfaceCreated");
- // Store render EGL context
+ // Store render EGL context.
if (CURRENT_SDK_VERSION >= EGL14_SDK_VERSION) {
eglContext = EGL14.eglGetCurrentContext();
Log.d(TAG, "VideoRendererGui EGL Context: " + eglContext);
@@ -711,6 +714,11 @@ public class VideoRendererGui implements GLSurfaceView.Renderer {
}
checkNoGLES2Error();
GLES20.glClearColor(0.15f, 0.15f, 0.15f, 1.0f);
+
+ // Fire EGL context ready event.
+ if (eglContextReady != null) {
+ eglContextReady.run();
+ }
}
@Override
diff --git a/talk/examples/android/assets/channel.html b/talk/examples/android/assets/channel.html
deleted file mode 100644
index d59344287..000000000
--- a/talk/examples/android/assets/channel.html
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/talk/examples/android/src/org/appspot/apprtc/AppRTCAudioManager.java b/talk/examples/android/src/org/appspot/apprtc/AppRTCAudioManager.java
index fe76197f4..86bfdbeac 100644
--- a/talk/examples/android/src/org/appspot/apprtc/AppRTCAudioManager.java
+++ b/talk/examples/android/src/org/appspot/apprtc/AppRTCAudioManager.java
@@ -27,6 +27,8 @@
package org.appspot.apprtc;
+import org.appspot.apprtc.util.AppRTCUtils;
+
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -35,8 +37,6 @@ import android.content.pm.PackageManager;
import android.media.AudioManager;
import android.util.Log;
-import org.appspot.apprtc.util.AppRTCUtils;
-
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
diff --git a/talk/examples/android/src/org/appspot/apprtc/AppRTCClient.java b/talk/examples/android/src/org/appspot/apprtc/AppRTCClient.java
index 97bebe17b..564d4941e 100644
--- a/talk/examples/android/src/org/appspot/apprtc/AppRTCClient.java
+++ b/talk/examples/android/src/org/appspot/apprtc/AppRTCClient.java
@@ -42,7 +42,7 @@ public interface AppRTCClient {
* https://apprtc.appspot.com/?r=NNN. Once connection is established
* 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.
@@ -60,9 +60,9 @@ public interface AppRTCClient {
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.
diff --git a/talk/examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java b/talk/examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java
index b63bf2e03..2e38cc2e3 100644
--- a/talk/examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java
+++ b/talk/examples/android/src/org/appspot/apprtc/AppRTCDemoActivity.java
@@ -27,6 +27,8 @@
package org.appspot.apprtc;
+import org.appspot.apprtc.AppRTCClient.SignalingParameters;
+
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Fragment;
@@ -49,9 +51,7 @@ import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
-import org.appspot.apprtc.AppRTCClient.SignalingParameters;
import org.webrtc.IceCandidate;
-import org.webrtc.PeerConnectionFactory;
import org.webrtc.SessionDescription;
import org.webrtc.StatsObserver;
import org.webrtc.StatsReport;
@@ -71,7 +71,7 @@ public class AppRTCDemoActivity extends Activity
implements AppRTCClient.SignalingEvents,
PeerConnectionClient.PeerConnectionEvents {
private static final String TAG = "AppRTCClient";
- private PeerConnectionClient pc;
+ private PeerConnectionClient pc = null;
private AppRTCClient appRtcClient;
private SignalingParameters signalingParameters;
private AppRTCAudioManager audioManager = null;
@@ -123,7 +123,12 @@ public class AppRTCDemoActivity extends Activity
roomNameView = (TextView) findViewById(R.id.room_name);
videoView = (GLSurfaceView) findViewById(R.id.glview);
- VideoRendererGui.setView(videoView);
+ VideoRendererGui.setView(videoView, new Runnable() {
+ @Override
+ public void run() {
+ createPeerConnectionFactory();
+ }
+ });
scalingType = ScalingType.SCALE_ASPECT_FILL;
remoteRender = VideoRendererGui.create(0, 0, 100, 100, scalingType, false);
localRender = VideoRendererGui.create(0, 0, 100, 100, scalingType, true);
@@ -201,17 +206,6 @@ public class AppRTCDemoActivity extends Activity
hudView.setVisibility(View.INVISIBLE);
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();
Uri url = intent.getData();
roomName = intent.getStringExtra(ConnectActivity.EXTRA_ROOMNAME);
@@ -225,6 +219,7 @@ public class AppRTCDemoActivity extends Activity
if (url != null) {
if (loopback || (roomName != null && !roomName.equals(""))) {
+ // Start room connection.
logAndToast(getString(R.string.connecting_to, url));
appRtcClient = new WebSocketRTCClient(this);
appRtcClient.connectToRoom(url.toString(), loopback);
@@ -233,6 +228,23 @@ public class AppRTCDemoActivity extends Activity
} else {
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 and exit.
if (commandLineRun && runTimeMs > 0) {
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.
*/
@@ -291,6 +318,9 @@ public class AppRTCDemoActivity extends Activity
protected void onDestroy() {
disconnect();
super.onDestroy();
+ if (logToast != null) {
+ logToast.cancel();
+ }
activityRunning = false;
}
@@ -312,7 +342,7 @@ public class AppRTCDemoActivity extends Activity
// Disconnect from remote resources, dispose of local resources, and exit.
private void disconnect() {
if (appRtcClient != null) {
- appRtcClient.disconnect();
+ appRtcClient.disconnectFromRoom();
appRtcClient = 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.
private void logAndToast(String msg) {
Log.d(TAG, msg);
@@ -460,22 +483,20 @@ public class AppRTCDemoActivity extends Activity
}
// -----Implementation of AppRTCClient.AppRTCSignalingEvents ---------------
- // All events are called from UI thread.
- @Override
- public void onConnectedToRoom(final SignalingParameters params) {
- if (audioManager != null) {
- // Store existing audio settings and change audio mode to
- // MODE_IN_COMMUNICATION for best possible VoIP performance.
- Log.d(TAG, "Initializing the audio manager...");
- audioManager.init();
- }
+ // All callbacks are invoked from websocket signaling looper thread and
+ // are routed to UI thread.
+ private void onConnectedToRoomInternal(final SignalingParameters params) {
signalingParameters = params;
- abortUnless(PeerConnectionFactory.initializeAndroidGlobals(
- this, true, true, hwCodec, VideoRendererGui.getEGLContext()),
- "Failed to initializeAndroidGlobals");
logAndToast("Creating peer connection...");
- pc = new PeerConnectionClient(localRender, remoteRender,
- signalingParameters, this, startBitrate);
+ if (pc == null) {
+ // 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()) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
} else {
@@ -510,7 +531,7 @@ public class AppRTCDemoActivity extends Activity
}
}, null);
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
public void onRemoteDescription(final SessionDescription sdp) {
- if (pc == null) {
- return;
- }
- logAndToast("Received remote " + sdp.type + " ...");
- pc.setRemoteDescription(sdp);
- if (!signalingParameters.initiator) {
- logAndToast("Creating ANSWER...");
- // Create answer. Answer SDP will be sent to offering client in
- // PeerConnectionEvents.onLocalDescription event.
- pc.createAnswer();
- }
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (pc == null) {
+ return;
+ }
+ logAndToast("Received remote " + sdp.type + " ...");
+ pc.setRemoteDescription(sdp);
+ if (!signalingParameters.initiator) {
+ logAndToast("Creating ANSWER...");
+ // Create answer. Answer SDP will be sent to offering client in
+ // PeerConnectionEvents.onLocalDescription event.
+ pc.createAnswer();
+ }
+ }
+ });
}
@Override
public void onRemoteIceCandidate(final IceCandidate candidate) {
- if (pc != null) {
- pc.addRemoteIceCandidate(candidate);
- }
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (pc != null) {
+ pc.addRemoteIceCandidate(candidate);
+ }
+ }
+ });
}
@Override
public void onChannelClose() {
- logAndToast("Remote end hung up; dropping PeerConnection");
- disconnect();
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ logAndToast("Remote end hung up; dropping PeerConnection");
+ disconnect();
+ }
+ });
}
@Override
public void onChannelError(final String description) {
- if (!isError) {
- isError = true;
- disconnectWithErrorMessage(description);
- }
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (!isError) {
+ isError = true;
+ disconnectWithErrorMessage(description);
+ }
+ }
+ });
}
// -----Implementation of PeerConnectionClient.PeerConnectionEvents.---------
// 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
public void onLocalDescription(final SessionDescription sdp) {
- if (appRtcClient != null) {
- logAndToast("Sending " + sdp.type + " ...");
- if (signalingParameters.initiator) {
- appRtcClient.sendOfferSdp(sdp);
- } else {
- appRtcClient.sendAnswerSdp(sdp);
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (appRtcClient != null) {
+ logAndToast("Sending " + sdp.type + " ...");
+ if (signalingParameters.initiator) {
+ appRtcClient.sendOfferSdp(sdp);
+ } else {
+ appRtcClient.sendAnswerSdp(sdp);
+ }
+ }
}
- }
+ });
}
@Override
public void onIceCandidate(final IceCandidate candidate) {
- if (appRtcClient != null) {
- appRtcClient.sendLocalIceCandidate(candidate);
- }
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (appRtcClient != null) {
+ appRtcClient.sendLocalIceCandidate(candidate);
+ }
+ }
+ });
}
@Override
public void onIceConnected() {
- logAndToast("ICE connected");
- iceConnected = true;
- updateVideoView();
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ logAndToast("ICE connected");
+ iceConnected = true;
+ updateVideoView();
+ }
+ });
}
@Override
public void onIceDisconnected() {
- logAndToast("ICE disconnected");
- disconnect();
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ logAndToast("ICE disconnected");
+ iceConnected = false;
+ disconnect();
+ }
+ });
}
@Override
@@ -600,10 +673,15 @@ public class AppRTCDemoActivity extends Activity
}
@Override
- public void onPeerConnectionError(String description) {
- if (!isError) {
- isError = true;
- disconnectWithErrorMessage(description);
- }
+ public void onPeerConnectionError(final String description) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (!isError) {
+ isError = true;
+ disconnectWithErrorMessage(description);
+ }
+ }
+ });
}
}
diff --git a/talk/examples/android/src/org/appspot/apprtc/AppRTCProximitySensor.java b/talk/examples/android/src/org/appspot/apprtc/AppRTCProximitySensor.java
index c8ba788cb..a984eba23 100644
--- a/talk/examples/android/src/org/appspot/apprtc/AppRTCProximitySensor.java
+++ b/talk/examples/android/src/org/appspot/apprtc/AppRTCProximitySensor.java
@@ -27,15 +27,16 @@
package org.appspot.apprtc;
+import org.appspot.apprtc.util.AppRTCUtils;
+import org.appspot.apprtc.util.AppRTCUtils.NonThreadSafe;
+
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
+import android.os.Build;
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
@@ -161,16 +162,27 @@ public class AppRTCProximitySensor implements SensorEventListener {
if (proximitySensor == null) {
return;
}
- Log.d(TAG, "Proximity sensor: " + "name=" + proximitySensor.getName()
- + ", vendor: " + proximitySensor.getVendor()
- + ", type: " + proximitySensor.getStringType()
- + ", reporting mode: " + proximitySensor.getReportingMode()
- + ", power: " + proximitySensor.getPower()
- + ", min delay: " + proximitySensor.getMinDelay()
- + ", max delay: " + proximitySensor.getMaxDelay()
- + ", resolution: " + proximitySensor.getResolution()
- + ", max range: " + proximitySensor.getMaximumRange()
- + ", isWakeUpSensor: " + proximitySensor.isWakeUpSensor());
+ StringBuilder info = new StringBuilder("Proximity sensor: ");
+ info.append("name=" + proximitySensor.getName());
+ info.append(", vendor: " + proximitySensor.getVendor());
+ info.append(", power: " + proximitySensor.getPower());
+ info.append(", resolution: " + proximitySensor.getResolution());
+ info.append(", max range: " + proximitySensor.getMaximumRange());
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
+ // Added in API level 9.
+ info.append(", min delay: " + proximitySensor.getMinDelay());
+ }
+ 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());
}
/**
diff --git a/talk/examples/android/src/org/appspot/apprtc/PeerConnectionClient.java b/talk/examples/android/src/org/appspot/apprtc/PeerConnectionClient.java
index 94160cf50..4092f6bf7 100644
--- a/talk/examples/android/src/org/appspot/apprtc/PeerConnectionClient.java
+++ b/talk/examples/android/src/org/appspot/apprtc/PeerConnectionClient.java
@@ -27,11 +27,13 @@
package org.appspot.apprtc;
-import android.os.Handler;
-import android.os.Looper;
+import org.appspot.apprtc.AppRTCClient.SignalingParameters;
+import org.appspot.apprtc.util.LooperExecutor;
+
+import android.content.Context;
+import android.opengl.EGLContext;
import android.util.Log;
-import org.appspot.apprtc.AppRTCClient.SignalingParameters;
import org.webrtc.DataChannel;
import org.webrtc.IceCandidate;
import org.webrtc.MediaConstraints;
@@ -54,28 +56,32 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
- * PeerConnection client for AppRTC.
+ * Peer connection client implementation.
+ *
+ * All public methods are routed to local looper thread.
+ * All PeerConnectionEvents callbacks are invoked from the same looper thread.
*/
public class PeerConnectionClient {
private static final String TAG = "PCRTCClient";
public static final String VIDEO_TRACK_ID = "ARDAMSv0";
public static final String AUDIO_TRACK_ID = "ARDAMSa0";
- private final Handler uiHandler;
- private PeerConnectionFactory factory;
- private PeerConnection pc;
+ private final LooperExecutor executor;
+ private PeerConnectionFactory factory = null;
+ private PeerConnection pc = null;
private VideoSource videoSource;
- private boolean videoSourceStopped;
+ private boolean videoSourceStopped = false;
+ private boolean isError = false;
private final PCObserver pcObserver = new PCObserver();
private final SDPObserver sdpObserver = new SDPObserver();
- private final VideoRenderer.Callbacks localRender;
- private final VideoRenderer.Callbacks remoteRender;
+ private VideoRenderer.Callbacks localRender;
+ private VideoRenderer.Callbacks remoteRender;
+ private SignalingParameters signalingParameters;
// Queued remote ICE candidates are consumed only after both local and
// remote descriptions are set. Similarly local ICE candidates are sent to
// remote peer after both local and remote description are set.
private LinkedList queuedRemoteCandidates = null;
private MediaConstraints sdpMediaConstraints;
- private MediaConstraints videoConstraints;
private PeerConnectionEvents events;
private int startBitrate;
private boolean isInitiator;
@@ -83,184 +89,12 @@ public class PeerConnectionClient {
private SessionDescription localSdp = null; // either offer or answer SDP
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();
-
- 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.
*/
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);
@@ -289,15 +123,263 @@ public class PeerConnectionClient {
/**
* 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();
+
+ 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) {
Log.e(TAG, "Peerconnection error: " + errorMessage);
- uiHandler.post(new Runnable() {
+ executor.execute(new Runnable() {
+ @Override
public void run() {
- events.onPeerConnectionError(errorMessage);
+ if (!isError) {
+ events.onPeerConnectionError(errorMessage);
+ isError = true;
+ }
}
});
}
@@ -336,7 +418,8 @@ public class PeerConnectionClient {
videoSource.dispose();
}
- videoSource = factory.createVideoSource(capturer, videoConstraints);
+ videoSource = factory.createVideoSource(
+ capturer, signalingParameters.videoConstraints);
String trackExtension = frontFacing ? "frontFacing" : "backFacing";
VideoTrack videoTrack =
factory.createVideoTrack(VIDEO_TRACK_ID + trackExtension, videoSource);
@@ -344,13 +427,6 @@ public class PeerConnectionClient {
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(
String sdpDescription, int bitrateKbps) {
String[] lines = sdpDescription.split("\r\n");
@@ -443,16 +519,16 @@ public class PeerConnectionClient {
}
}
- public void switchCamera() {
- if (videoConstraints == null) {
+ private void switchCameraInternal() {
+ if (signalingParameters.videoConstraints == null) {
return; // No video is sent.
}
-
if (pc.signalingState() != PeerConnection.SignalingState.STABLE) {
Log.e(TAG, "Switching camera during negotiation is not handled.");
return;
}
+ Log.d(TAG, "Switch camera");
pc.removeStream(mediaStream);
VideoTrack currentTrack = mediaStream.videoTracks.get(0);
mediaStream.removeTrack(currentTrack);
@@ -485,13 +561,26 @@ public class PeerConnectionClient {
pc.setRemoteDescription(new SwitchCameraSdbObserver(), remoteDesc);
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.
private class PCObserver implements PeerConnection.Observer {
@Override
public void onIceCandidate(final IceCandidate candidate){
- uiHandler.post(new Runnable() {
+ executor.execute(new Runnable() {
+ @Override
public void run() {
events.onIceCandidate(candidate);
}
@@ -506,23 +595,20 @@ public class PeerConnectionClient {
@Override
public void onIceConnectionChange(
- PeerConnection.IceConnectionState newState) {
- Log.d(TAG, "IceConnectionState: " + newState);
- if (newState == IceConnectionState.CONNECTED) {
- uiHandler.post(new Runnable() {
- public void run() {
+ final PeerConnection.IceConnectionState newState) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ Log.d(TAG, "IceConnectionState: " + newState);
+ if (newState == IceConnectionState.CONNECTED) {
events.onIceConnected();
- }
- });
- } else if (newState == IceConnectionState.DISCONNECTED) {
- uiHandler.post(new Runnable() {
- public void run() {
+ } else if (newState == IceConnectionState.DISCONNECTED) {
events.onIceDisconnected();
+ } else if (newState == IceConnectionState.FAILED) {
+ reportError("ICE connection failed.");
}
- });
- } else if (newState == IceConnectionState.FAILED) {
- reportError("ICE connection failed.");
- }
+ }
+ });
}
@Override
@@ -533,11 +619,16 @@ public class PeerConnectionClient {
@Override
public void onAddStream(final MediaStream stream){
- uiHandler.post(new Runnable() {
+ executor.execute(new Runnable() {
+ @Override
public void run() {
- abortUnless(stream.audioTracks.size() <= 1
- && stream.videoTracks.size() <= 1,
- "Weird-looking stream: " + stream);
+ if (pc == null || isError) {
+ return;
+ }
+ if (stream.audioTracks.size() > 1 || stream.videoTracks.size() > 1) {
+ reportError("Weird-looking stream: " + stream);
+ return;
+ }
if (stream.videoTracks.size() == 1) {
stream.videoTracks.get(0).addRenderer(
new VideoRenderer(remoteRender));
@@ -548,8 +639,12 @@ public class PeerConnectionClient {
@Override
public void onRemoveStream(final MediaStream stream){
- uiHandler.post(new Runnable() {
+ executor.execute(new Runnable() {
+ @Override
public void run() {
+ if (pc == null || isError) {
+ return;
+ }
stream.videoTracks.get(0).dispose();
}
});
@@ -573,13 +668,17 @@ public class PeerConnectionClient {
private class SDPObserver implements SdpObserver {
@Override
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(
origSdp.type, preferISAC(origSdp.description));
localSdp = sdp;
- uiHandler.post(new Runnable() {
+ executor.execute(new Runnable() {
+ @Override
public void run() {
- if (pc != null) {
+ if (pc != null && !isError) {
Log.d(TAG, "Set local SDP from " + sdp.type);
pc.setLocalDescription(sdpObserver, sdp);
}
@@ -589,9 +688,10 @@ public class PeerConnectionClient {
@Override
public void onSetSuccess() {
- uiHandler.post(new Runnable() {
+ executor.execute(new Runnable() {
+ @Override
public void run() {
- if (pc == null) {
+ if (pc == null || isError) {
return;
}
if (isInitiator) {
diff --git a/talk/examples/android/src/org/appspot/apprtc/RoomParametersFetcher.java b/talk/examples/android/src/org/appspot/apprtc/RoomParametersFetcher.java
index 4c015ba21..58e8d4fdc 100644
--- a/talk/examples/android/src/org/appspot/apprtc/RoomParametersFetcher.java
+++ b/talk/examples/android/src/org/appspot/apprtc/RoomParametersFetcher.java
@@ -26,10 +26,12 @@
*/
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 org.appspot.apprtc.AppRTCClient.SignalingParameters;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
@@ -40,7 +42,6 @@ import org.webrtc.SessionDescription;
import java.io.IOException;
import java.io.InputStream;
-import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.LinkedList;
@@ -50,12 +51,12 @@ import java.util.Scanner;
* AsyncTask that converts an AppRTC room URL into the set of signaling
* parameters to use with that room.
*/
-public class RoomParametersFetcher
- extends AsyncTask {
+public class RoomParametersFetcher {
private static final String TAG = "RoomRTCClient";
- private Exception exception = null;
- private RoomParametersFetcherEvents events = null;
- private boolean loopback;
+ private final RoomParametersFetcherEvents events;
+ private final boolean loopback;
+ private final String registerUrl;
+ private AsyncHttpURLConnection httpConnection;
/**
* Room parameters fetcher callbacks.
@@ -73,146 +74,125 @@ public class RoomParametersFetcher
public void onSignalingParametersError(final String description);
}
- public RoomParametersFetcher(RoomParametersFetcherEvents events,
- boolean loopback) {
- super();
- this.events = events;
+ public RoomParametersFetcher(boolean loopback, String registerUrl,
+ final RoomParametersFetcherEvents events) {
+ Log.d(TAG, "Connecting to room: " + registerUrl);
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
- 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();
+ private void RoomHttpResponseParse(String response) {
Log.d(TAG, "Room response: " + response);
- JSONObject roomJson = new JSONObject(response);
- LinkedList iceCandidates = null;
- SessionDescription offerSdp = null;
+ try {
+ LinkedList iceCandidates = null;
+ SessionDescription offerSdp = null;
+ JSONObject roomJson = new JSONObject(response);
- String result = roomJson.getString("result");
- if (!result.equals("SUCCESS")) {
- throw new JSONException(result);
- }
- response = roomJson.getString("params");
- roomJson = new JSONObject(response);
- String roomId = roomJson.getString("room_id");
- String clientId = roomJson.getString("client_id");
- String wssUrl = roomJson.getString("wss_url");
- String wssPostUrl = roomJson.getString("wss_post_url");
- boolean initiator = (roomJson.getBoolean("is_initiator"));
- String roomUrl = url.substring(0, url.indexOf("/register"));
- if (!initiator) {
- iceCandidates = new LinkedList();
- String messagesString = roomJson.getString("messages");
- JSONArray messages = new JSONArray(messagesString);
- for (int i = 0; i < messages.length(); ++i) {
- String messageString = messages.getString(i);
- JSONObject message = new JSONObject(messageString);
- String messageType = message.getString("type");
- Log.d(TAG, "GAE->C #" + i + " : " + messageString);
- if (messageType.equals("offer")) {
- offerSdp = new SessionDescription(
- SessionDescription.Type.fromCanonicalForm(messageType),
- message.getString("sdp"));
- } else if (messageType.equals("candidate")) {
- IceCandidate candidate = new IceCandidate(
- message.getString("id"),
- message.getInt("label"),
- message.getString("candidate"));
- iceCandidates.add(candidate);
- } else {
- Log.e(TAG, "Unknown message: " + messageString);
+ String result = roomJson.getString("result");
+ if (!result.equals("SUCCESS")) {
+ events.onSignalingParametersError("Room response error: " + result);
+ return;
+ }
+ response = roomJson.getString("params");
+ roomJson = new JSONObject(response);
+ String roomId = roomJson.getString("room_id");
+ String clientId = roomJson.getString("client_id");
+ String wssUrl = roomJson.getString("wss_url");
+ String wssPostUrl = roomJson.getString("wss_post_url");
+ boolean initiator = (roomJson.getBoolean("is_initiator"));
+ String roomUrl =
+ registerUrl.substring(0, registerUrl.indexOf("/register"));
+ if (!initiator) {
+ iceCandidates = new LinkedList();
+ String messagesString = roomJson.getString("messages");
+ JSONArray messages = new JSONArray(messagesString);
+ for (int i = 0; i < messages.length(); ++i) {
+ String messageString = messages.getString(i);
+ JSONObject message = new JSONObject(messageString);
+ String messageType = message.getString("type");
+ Log.d(TAG, "GAE->C #" + i + " : " + messageString);
+ if (messageType.equals("offer")) {
+ offerSdp = new SessionDescription(
+ SessionDescription.Type.fromCanonicalForm(messageType),
+ message.getString("sdp"));
+ } else if (messageType.equals("candidate")) {
+ IceCandidate candidate = new IceCandidate(
+ message.getString("id"),
+ message.getInt("label"),
+ message.getString("candidate"));
+ 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);
- Log.d(TAG, "Initiator: " + initiator);
- Log.d(TAG, "Room url: " + roomUrl);
-
- LinkedList iceServers =
- iceServersFromPCConfigJSON(roomJson.getString("pc_config"));
- boolean isTurnPresent = false;
- for (PeerConnection.IceServer server : iceServers) {
- Log.d(TAG, "IceServer: " + server);
- if (server.uri.startsWith("turn:")) {
- isTurnPresent = true;
- break;
+ LinkedList iceServers =
+ iceServersFromPCConfigJSON(roomJson.getString("pc_config"));
+ boolean isTurnPresent = false;
+ for (PeerConnection.IceServer server : iceServers) {
+ Log.d(TAG, "IceServer: " + server);
+ if (server.uri.startsWith("turn:")) {
+ isTurnPresent = true;
+ break;
+ }
}
- }
- if (!isTurnPresent) {
- LinkedList turnServers =
- requestTurnServers(roomJson.getString("turn_url"));
- for (PeerConnection.IceServer turnServer : turnServers) {
- Log.d(TAG, "TurnServer: " + turnServer);
- iceServers.add(turnServer);
+ if (!isTurnPresent) {
+ LinkedList turnServers =
+ requestTurnServers(roomJson.getString("turn_url"));
+ for (PeerConnection.IceServer turnServer : turnServers) {
+ Log.d(TAG, "TurnServer: " + 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
@@ -245,7 +225,7 @@ public class RoomParametersFetcher
private String getAVConstraints (
String type, String mediaConstraintsString) throws JSONException {
JSONObject json = new JSONObject(mediaConstraintsString);
- // Tricksy handling of values that are allowed to be (boolean or
+ // Tricky handling of values that are allowed to be (boolean or
// MediaTrackConstraints) by the getUserMedia() spec. There are three
// cases below.
if (!json.has(type) || !json.optBoolean(type, true)) {
diff --git a/talk/examples/android/src/org/appspot/apprtc/WebSocketChannelClient.java b/talk/examples/android/src/org/appspot/apprtc/WebSocketChannelClient.java
index 99572536e..ff2665144 100644
--- a/talk/examples/android/src/org/appspot/apprtc/WebSocketChannelClient.java
+++ b/talk/examples/android/src/org/appspot/apprtc/WebSocketChannelClient.java
@@ -26,34 +26,35 @@
*/
package org.appspot.apprtc;
-import android.os.Handler;
-import android.os.Looper;
import android.util.Log;
+import de.tavendo.autobahn.WebSocket.WebSocketConnectionObserver;
import de.tavendo.autobahn.WebSocketConnection;
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.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.
- * For proper synchronization all methods should be called from UI thread
- * and all WebSocket events are delivered on UI thread as well.
+ * All public methods should be called from a looper executor thread
+ * passed in constructor.
+ * All events are issued on the same thread.
*/
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 Handler uiHandler;
+ private final LooperExecutor executor;
private WebSocketConnection ws;
private WebSocketObserver wsObserver;
private String wsServerUrl;
@@ -61,10 +62,15 @@ public class WebSocketChannelClient {
private String roomID;
private String clientID;
private WebSocketConnectionState state;
+ private final Object closeEventLock = new Object();
+ private boolean closeEvent;
// WebSocket send queue. Messages are added to the queue when WebSocket
// client is not registered and are consumed in register() call.
private LinkedList wsSendQueue;
+ /**
+ * WebSocketConnectionState is the names of possible WS connection states.
+ */
public enum WebSocketConnectionState {
NEW, CONNECTED, REGISTERED, CLOSED, ERROR
};
@@ -80,9 +86,10 @@ public class WebSocketChannelClient {
public void onWebSocketError(final String description);
}
- public WebSocketChannelClient(WebSocketChannelEvents events) {
+ public WebSocketChannelClient(LooperExecutor executor,
+ WebSocketChannelEvents events) {
+ this.executor = executor;
this.events = events;
- uiHandler = new Handler(Looper.getMainLooper());
roomID = null;
clientID = null;
wsSendQueue = new LinkedList();
@@ -93,18 +100,22 @@ public class WebSocketChannelClient {
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) {
Log.e(TAG, "WebSocket is already connected.");
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();
wsObserver = new WebSocketObserver();
try {
- wsServerUrl = wsUrl;
- postServerUrl = postUrl;
ws.connect(new URI(wsServerUrl), wsObserver);
} catch (URISyntaxException e) {
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() {
if (state != WebSocketConnectionState.CONNECTED) {
Log.w(TAG, "WebSocket register() in state " + state);
return;
}
- if (roomID == null || clientID == null) {
- Log.w(TAG, "Call WebSocket register() without setting client ID");
- return;
- }
JSONObject json = new JSONObject();
try {
json.put("cmd", "register");
@@ -187,7 +189,7 @@ public class WebSocketChannelClient {
sendWSSMessage("POST", message);
}
- public void disconnect() {
+ public void disconnect(boolean waitForComplete) {
Log.d(TAG, "Disonnect WebSocket. State: " + state);
if (state == WebSocketConnectionState.REGISTERED) {
send("{\"type\": \"bye\"}");
@@ -202,12 +204,28 @@ public class WebSocketChannelClient {
sendWSSMessage("DELETE", "");
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) {
Log.e(TAG, errorMessage);
- uiHandler.post(new Runnable() {
+ executor.execute(new Runnable() {
+ @Override
public void run() {
if (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.
- private void sendWSSMessage(String method, String message) {
- final WsHttpMessage wsHttpMessage = new WsHttpMessage(method, message);
- Runnable runAsync = new Runnable() {
- public void run() {
- sendWSSMessageAsync(wsHttpMessage);
- }
- };
- new Thread(runAsync).start();
- }
+ private void sendWSSMessage(final String method, final String message) {
+ String postUrl = postServerUrl + "/" + roomID + "/" + clientID;
+ Log.d(TAG, "WS " + method + " : " + postUrl + " : " + message);
+ AsyncHttpURLConnection httpConnection = new AsyncHttpURLConnection(
+ method, postUrl, message, new AsyncHttpEvents() {
+ @Override
+ public void OnHttpError(String errorMessage) {
+ reportError("WS " + method + " error: " + errorMessage);
+ }
- private void sendWSSMessageAsync(WsHttpMessage wsHttpMessage) {
- if (roomID == null || clientID == null) {
- return;
- }
- try {
- // 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());
- }
+ @Override
+ public void OnHttpComplete(String response) {
+ }
+ });
+ httpConnection.send();
}
private class WebSocketObserver implements WebSocketConnectionObserver {
@Override
public void onOpen() {
Log.d(TAG, "WebSocket connection opened to: " + wsServerUrl);
- uiHandler.post(new Runnable() {
+ executor.execute(new Runnable() {
+ @Override
public void run() {
state = WebSocketConnectionState.CONNECTED;
events.onWebSocketOpen();
@@ -281,8 +269,13 @@ public class WebSocketChannelClient {
@Override
public void onClose(WebSocketCloseNotification code, String reason) {
Log.d(TAG, "WebSocket connection closed. Code: " + code
- + ". Reason: " + reason);
- uiHandler.post(new Runnable() {
+ + ". Reason: " + reason + ". State: " + state);
+ synchronized (closeEventLock) {
+ closeEvent = true;
+ closeEventLock.notify();
+ }
+ executor.execute(new Runnable() {
+ @Override
public void run() {
if (state != WebSocketConnectionState.CLOSED) {
state = WebSocketConnectionState.CLOSED;
@@ -296,7 +289,8 @@ public class WebSocketChannelClient {
public void onTextMessage(String payload) {
Log.d(TAG, "WSS->C: " + payload);
final String message = payload;
- uiHandler.post(new Runnable() {
+ executor.execute(new Runnable() {
+ @Override
public void run() {
if (state == WebSocketConnectionState.CONNECTED
|| state == WebSocketConnectionState.REGISTERED) {
diff --git a/talk/examples/android/src/org/appspot/apprtc/WebSocketRTCClient.java b/talk/examples/android/src/org/appspot/apprtc/WebSocketRTCClient.java
index 227ebe03b..3d3cdf67b 100644
--- a/talk/examples/android/src/org/appspot/apprtc/WebSocketRTCClient.java
+++ b/talk/examples/android/src/org/appspot/apprtc/WebSocketRTCClient.java
@@ -26,17 +26,11 @@
*/
package org.appspot.apprtc;
-import android.os.Handler;
-import android.os.Looper;
import android.util.Log;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.util.Scanner;
-
+import org.appspot.apprtc.util.AsyncHttpURLConnection;
+import org.appspot.apprtc.util.AsyncHttpURLConnection.AsyncHttpEvents;
+import org.appspot.apprtc.util.LooperExecutor;
import org.appspot.apprtc.RoomParametersFetcher.RoomParametersFetcherEvents;
import org.appspot.apprtc.WebSocketChannelClient.WebSocketChannelEvents;
import org.appspot.apprtc.WebSocketChannelClient.WebSocketConnectionState;
@@ -56,7 +50,7 @@ import org.webrtc.SessionDescription;
* be sent after WebSocket connection is established.
*/
public class WebSocketRTCClient implements AppRTCClient,
- RoomParametersFetcherEvents, WebSocketChannelEvents {
+ WebSocketChannelEvents {
private static final String TAG = "WSRTCClient";
private enum ConnectionState {
@@ -65,7 +59,7 @@ public class WebSocketRTCClient implements AppRTCClient,
private enum MessageType {
MESSAGE, BYE
};
- private final Handler uiHandler;
+ private final LooperExecutor executor;
private boolean loopback;
private boolean initiator;
private SignalingEvents events;
@@ -77,14 +71,81 @@ public class WebSocketRTCClient implements AppRTCClient,
public WebSocketRTCClient(SignalingEvents events) {
this.events = events;
- uiHandler = new Handler(Looper.getMainLooper());
+ executor = new LooperExecutor();
}
// --------------------------------------------------------------------
- // RoomConnectionEvents interface implementation.
- // All events are called on UI thread.
+ // AppRTCClient interface implementation.
+ // Asynchronously connect to an AppRTC room URL, e.g.
+ // https://apprtc.appspot.com/register/, retrieve room parameters
+ // and connect to WebSocket server.
@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.");
if (loopback && (!params.initiator || params.offerSdp != null)) {
reportError("Loopback room is busy.");
@@ -100,15 +161,16 @@ public class WebSocketRTCClient implements AppRTCClient,
+ params.roomId + "/" + params.clientId;
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.
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) {
- // For call receiver get sdp offer and ice candidates
- // from room parameters.
if (params.offerSdp != null) {
events.onRemoteDescription(params.offerSdp);
}
@@ -120,14 +182,90 @@ public class WebSocketRTCClient implements AppRTCClient,
}
}
+ // Send local offer SDP to the other participant.
@Override
- public void onSignalingParametersError(final String description) {
- reportError("Room connection error: " + description);
+ public void sendOfferSdp(final SessionDescription sdp) {
+ 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.
- // All events are called on UI thread.
+ // All events are called by WebSocketChannelClient on a local looper thread
+ // (passed to WebSocket client constructor).
@Override
public void onWebSocketOpen() {
Log.d(TAG, "Websocket connection completed. Registering...");
@@ -198,108 +336,12 @@ public class WebSocketRTCClient implements AppRTCClient,
reportError("WebSocket error: " + description);
}
- // --------------------------------------------------------------------
- // AppRTCClient interface implementation.
- // Asynchronously connect to an AppRTC room URL, e.g.
- // https://apprtc.appspot.com/register/, 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.
private void reportError(final String errorMessage) {
Log.e(TAG, errorMessage);
- uiHandler.post(new Runnable() {
+ executor.execute(new Runnable() {
+ @Override
public void run() {
if (roomState != ConnectionState.ERROR) {
roomState = ConnectionState.ERROR;
@@ -318,81 +360,36 @@ public class WebSocketRTCClient implements AppRTCClient,
}
}
- private class PostMessage {
- PostMessage(MessageType type, String postUrl, String message) {
- this.messageType = type;
- this.postUrl = postUrl;
- this.message = message;
- }
- 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);
+ // Send SDP or ICE candidate to a room server.
+ private void sendPostMessage(
+ final MessageType messageType, final String url, final String message) {
+ if (messageType == MessageType.BYE) {
+ Log.d(TAG, "C->GAE: " + url);
} else {
- Log.d(TAG, "C->GAE: " + postMessage.message);
+ Log.d(TAG, "C->GAE: " + message);
}
- try {
- // Get connection.
- HttpURLConnection connection =
- (HttpURLConnection) new URL(postMessage.postUrl).openConnection();
- byte[] postData = postMessage.message.getBytes("UTF-8");
- 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);
+ AsyncHttpURLConnection httpConnection = new AsyncHttpURLConnection(
+ "POST", url, message, new AsyncHttpEvents() {
+ @Override
+ public void OnHttpError(String errorMessage) {
+ reportError("GAE POST error: " + errorMessage);
}
- }
- } 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.
- private String drainStream(InputStream in) {
- Scanner s = new Scanner(in).useDelimiter("\\A");
- return s.hasNext() ? s.next() : "";
+ @Override
+ public void OnHttpComplete(String response) {
+ if (messageType == MessageType.MESSAGE) {
+ 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();
}
-
}
diff --git a/talk/examples/android/src/org/appspot/apprtc/util/AsyncHttpURLConnection.java b/talk/examples/android/src/org/appspot/apprtc/util/AsyncHttpURLConnection.java
new file mode 100644
index 000000000..784ff3069
--- /dev/null
+++ b/talk/examples/android/src/org/appspot/apprtc/util/AsyncHttpURLConnection.java
@@ -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() : "";
+ }
+}
diff --git a/talk/examples/android/src/org/appspot/apprtc/util/LooperExecutor.java b/talk/examples/android/src/org/appspot/apprtc/util/LooperExecutor.java
new file mode 100644
index 000000000..50091d00b
--- /dev/null
+++ b/talk/examples/android/src/org/appspot/apprtc/util/LooperExecutor.java
@@ -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);
+ }
+ }
+
+}
diff --git a/talk/examples/androidtests/src/org/appspot/apprtc/test/LooperExecutorTest.java b/talk/examples/androidtests/src/org/appspot/apprtc/test/LooperExecutorTest.java
new file mode 100644
index 000000000..c90c216c5
--- /dev/null
+++ b/talk/examples/androidtests/src/org/appspot/apprtc/test/LooperExecutorTest.java
@@ -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");
+ }
+}
diff --git a/talk/examples/androidtests/src/org/appspot/apprtc/test/PeerConnectionClientTest.java b/talk/examples/androidtests/src/org/appspot/apprtc/test/PeerConnectionClientTest.java
index 05d54dbef..1a5bbfad3 100644
--- a/talk/examples/androidtests/src/org/appspot/apprtc/test/PeerConnectionClientTest.java
+++ b/talk/examples/androidtests/src/org/appspot/apprtc/test/PeerConnectionClientTest.java
@@ -35,10 +35,10 @@ import java.util.concurrent.TimeUnit;
import org.appspot.apprtc.AppRTCClient.SignalingParameters;
import org.appspot.apprtc.PeerConnectionClient;
import org.appspot.apprtc.PeerConnectionClient.PeerConnectionEvents;
+import org.appspot.apprtc.util.LooperExecutor;
import org.webrtc.IceCandidate;
import org.webrtc.MediaConstraints;
import org.webrtc.PeerConnection;
-import org.webrtc.PeerConnectionFactory;
import org.webrtc.SessionDescription;
import org.webrtc.VideoRenderer;
@@ -49,7 +49,7 @@ public class PeerConnectionClientTest extends InstrumentationTestCase
implements PeerConnectionEvents {
private static final String TAG = "RTCClientTest";
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 volatile PeerConnectionClient pcClient;
@@ -222,9 +222,6 @@ public class PeerConnectionClientTest extends InstrumentationTestCase
isClosed = false;
isIceConnected = false;
loopback = false;
- Log.d(TAG, "initializeAndroidGlobals");
- assertTrue(PeerConnectionFactory.initializeAndroidGlobals(
- getInstrumentation().getContext(), true, true, true, null));
}
public void testInitiatorCreation() throws InterruptedException {
@@ -233,8 +230,11 @@ public class PeerConnectionClientTest extends InstrumentationTestCase
MockRenderer remoteRender = new MockRenderer(EXPECTED_VIDEO_FRAMES);
SignalingParameters signalingParameters = getTestSignalingParameters();
- 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();
// 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);
SignalingParameters signalingParameters = getTestSignalingParameters();
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();
// 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));
Log.d(TAG, "testLoopback Done.");
}
-
}
diff --git a/talk/libjingle_examples.gyp b/talk/libjingle_examples.gyp
index a94610fa9..27b0b8aec 100755
--- a/talk/libjingle_examples.gyp
+++ b/talk/libjingle_examples.gyp
@@ -320,7 +320,6 @@
'examples/android/AndroidManifest.xml',
'examples/android/README',
'examples/android/ant.properties',
- 'examples/android/assets/channel.html',
'examples/android/third_party/autobanh/autobanh.jar',
'examples/android/build.xml',
'examples/android/jni/Android.mk',
@@ -362,9 +361,11 @@
'examples/android/src/org/appspot/apprtc/SettingsActivity.java',
'examples/android/src/org/appspot/apprtc/SettingsFragment.java',
'examples/android/src/org/appspot/apprtc/UnhandledExceptionHandler.java',
- 'examples/android/src/org/appspot/apprtc/util/AppRTCUtils.java',
'examples/android/src/org/appspot/apprtc/WebSocketChannelClient.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': [
'<(PRODUCT_DIR)/AppRTCDemo-debug.apk',
@@ -412,6 +413,7 @@
'examples/androidtests/ant.properties',
'examples/androidtests/build.xml',
'examples/androidtests/project.properties',
+ 'examples/androidtests/src/org/appspot/apprtc/test/LooperExecutorTest.java',
'examples/androidtests/src/org/appspot/apprtc/test/PeerConnectionClientTest.java',
],
'outputs': [