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': [