Remove peer connection and signaling calls from UI thread.

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

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

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

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

View File

@ -56,6 +56,7 @@ import org.webrtc.VideoRenderer.I420Frame;
*/
public class VideoRendererGui implements GLSurfaceView.Renderer {
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

View File

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

View File

@ -27,6 +27,8 @@
package org.appspot.apprtc;
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;

View File

@ -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.

View File

@ -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 <runTimeMs> 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);
}
}
});
}
}

View File

@ -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());
}
/**

View File

@ -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.
*
* <p>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<IceCandidate> 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<IceCandidate>();
sdpMediaConstraints = new MediaConstraints();
sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
"OfferToReceiveAudio", "true"));
sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
"OfferToReceiveVideo", "true"));
videoConstraints = signalingParameters.videoConstraints;
factory = new PeerConnectionFactory();
MediaConstraints pcConstraints = signalingParameters.pcConstraints;
pcConstraints.optional.add(
new MediaConstraints.KeyValuePair("RtpDataChannels", "true"));
pc = factory.createPeerConnection(signalingParameters.iceServers,
pcConstraints, pcObserver);
isInitiator = false;
// Uncomment to get ALL WebRTC tracing and SENSITIVE libjingle logging.
// NOTE: this _must_ happen while |factory| is alive!
// Logging.enableTracing(
// "logcat:",
// EnumSet.of(Logging.TraceLevel.TRACE_ALL),
// Logging.Severity.LS_SENSITIVE);
mediaStream = factory.createLocalMediaStream("ARDAMS");
if (videoConstraints != null) {
mediaStream.addTrack(createVideoTrack(useFrontFacingCamera));
}
if (signalingParameters.audioConstraints != null) {
mediaStream.addTrack(factory.createAudioTrack(
AUDIO_TRACK_ID,
factory.createAudioSource(signalingParameters.audioConstraints)));
}
pc.addStream(mediaStream);
}
public boolean isHDVideo() {
if (videoConstraints == null) {
return false;
}
int minWidth = 0;
int minHeight = 0;
for (KeyValuePair keyValuePair : videoConstraints.mandatory) {
if (keyValuePair.getKey().equals("minWidth")) {
try {
minWidth = Integer.parseInt(keyValuePair.getValue());
} catch (NumberFormatException e) {
Log.e(TAG, "Can not parse video width from video constraints");
}
} else if (keyValuePair.getKey().equals("minHeight")) {
try {
minHeight = Integer.parseInt(keyValuePair.getValue());
} catch (NumberFormatException e) {
Log.e(TAG, "Can not parse video height from video constraints");
}
}
}
if (minWidth * minHeight >= 1280 * 720) {
return true;
} else {
return false;
}
}
public boolean getStats(StatsObserver observer, MediaStreamTrack track) {
return pc.getStats(observer, track);
}
public void createOffer() {
uiHandler.post(new Runnable() {
public void run() {
if (pc != null) {
isInitiator = true;
pc.createOffer(sdpObserver, sdpMediaConstraints);
}
}
});
}
public void createAnswer() {
uiHandler.post(new Runnable() {
public void run() {
if (pc != null) {
isInitiator = false;
pc.createAnswer(sdpObserver, sdpMediaConstraints);
}
}
});
}
public void addRemoteIceCandidate(final IceCandidate candidate) {
uiHandler.post(new Runnable() {
public void run() {
if (pc != null) {
if (queuedRemoteCandidates != null) {
queuedRemoteCandidates.add(candidate);
} else {
pc.addIceCandidate(candidate);
}
}
}
});
}
public void setRemoteDescription(final SessionDescription sdp) {
uiHandler.post(new Runnable() {
public void run() {
if (pc != null) {
String sdpDescription = preferISAC(sdp.description);
if (startBitrate > 0) {
sdpDescription = setStartBitrate(sdpDescription, startBitrate);
}
Log.d(TAG, "Set remote SDP.");
SessionDescription sdpRemote = new SessionDescription(
sdp.type, sdpDescription);
pc.setRemoteDescription(sdpObserver, sdpRemote);
}
}
});
}
public void stopVideoSource() {
if (videoSource != null) {
Log.d(TAG, "Stop video source.");
videoSource.stop();
videoSourceStopped = true;
}
}
public void startVideoSource() {
if (videoSource != null && videoSourceStopped) {
Log.d(TAG, "Restart video source.");
videoSource.restart();
videoSourceStopped = false;
}
}
public void close() {
uiHandler.post(new Runnable() {
public void run() {
Log.d(TAG, "Closing peer connection.");
if (pc != null) {
pc.dispose();
pc = null;
}
if (videoSource != null) {
videoSource.dispose();
videoSource = null;
}
if (factory != null) {
factory.dispose();
factory = null;
}
Log.d(TAG, "Closing peer connection done.");
events.onPeerConnectionClosed();
}
});
}
/**
* SDP/ICE ready callbacks.
*/
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<IceCandidate>();
sdpMediaConstraints = new MediaConstraints();
sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
"OfferToReceiveAudio", "true"));
sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair(
"OfferToReceiveVideo", "true"));
MediaConstraints pcConstraints = signalingParameters.pcConstraints;
pcConstraints.optional.add(
new MediaConstraints.KeyValuePair("RtpDataChannels", "true"));
pc = factory.createPeerConnection(signalingParameters.iceServers,
pcConstraints, pcObserver);
isInitiator = false;
// Uncomment to get ALL WebRTC tracing and SENSITIVE libjingle logging.
// NOTE: this _must_ happen while |factory| is alive!
// Logging.enableTracing(
// "logcat:",
// EnumSet.of(Logging.TraceLevel.TRACE_ALL),
// Logging.Severity.LS_SENSITIVE);
mediaStream = factory.createLocalMediaStream("ARDAMS");
if (signalingParameters.videoConstraints != null) {
mediaStream.addTrack(createVideoTrack(useFrontFacingCamera));
}
if (signalingParameters.audioConstraints != null) {
mediaStream.addTrack(factory.createAudioTrack(
AUDIO_TRACK_ID,
factory.createAudioSource(signalingParameters.audioConstraints)));
}
pc.addStream(mediaStream);
Log.d(TAG, "Peer connection created.");
}
private void closeInternal() {
Log.d(TAG, "Closing peer connection.");
if (pc != null) {
pc.dispose();
pc = null;
}
if (videoSource != null) {
videoSource.dispose();
videoSource = null;
}
Log.d(TAG, "Closing peer connection factory.");
if (factory != null) {
factory.dispose();
factory = null;
}
Log.d(TAG, "Closing peer connection done.");
events.onPeerConnectionClosed();
}
public boolean isHDVideo() {
if (signalingParameters.videoConstraints == null) {
return false;
}
int minWidth = 0;
int minHeight = 0;
for (KeyValuePair keyValuePair :
signalingParameters.videoConstraints.mandatory) {
if (keyValuePair.getKey().equals("minWidth")) {
try {
minWidth = Integer.parseInt(keyValuePair.getValue());
} catch (NumberFormatException e) {
Log.e(TAG, "Can not parse video width from video constraints");
}
} else if (keyValuePair.getKey().equals("minHeight")) {
try {
minHeight = Integer.parseInt(keyValuePair.getValue());
} catch (NumberFormatException e) {
Log.e(TAG, "Can not parse video height from video constraints");
}
}
}
if (minWidth * minHeight >= 1280 * 720) {
return true;
} else {
return false;
}
}
public boolean getStats(StatsObserver observer, MediaStreamTrack track) {
if (pc != null && !isError) {
return pc.getStats(observer, track);
} else {
return false;
}
}
public void createOffer() {
executor.execute(new Runnable() {
@Override
public void run() {
if (pc != null && !isError) {
isInitiator = true;
pc.createOffer(sdpObserver, sdpMediaConstraints);
}
}
});
}
public void createAnswer() {
executor.execute(new Runnable() {
@Override
public void run() {
if (pc != null && !isError) {
isInitiator = false;
pc.createAnswer(sdpObserver, sdpMediaConstraints);
}
}
});
}
public void addRemoteIceCandidate(final IceCandidate candidate) {
executor.execute(new Runnable() {
@Override
public void run() {
if (pc != null && !isError) {
if (queuedRemoteCandidates != null) {
queuedRemoteCandidates.add(candidate);
} else {
pc.addIceCandidate(candidate);
}
}
}
});
}
public void setRemoteDescription(final SessionDescription sdp) {
executor.execute(new Runnable() {
@Override
public void run() {
if (pc == null || isError) {
return;
}
String sdpDescription = preferISAC(sdp.description);
if (startBitrate > 0) {
sdpDescription = setStartBitrate(sdpDescription, startBitrate);
}
Log.d(TAG, "Set remote SDP.");
SessionDescription sdpRemote = new SessionDescription(
sdp.type, sdpDescription);
pc.setRemoteDescription(sdpObserver, sdpRemote);
}
});
}
public void stopVideoSource() {
executor.execute(new Runnable() {
@Override
public void run() {
if (videoSource != null && !videoSourceStopped) {
Log.d(TAG, "Stop video source.");
videoSource.stop();
videoSourceStopped = true;
}
}
});
}
public void startVideoSource() {
executor.execute(new Runnable() {
@Override
public void run() {
if (videoSource != null && videoSourceStopped) {
Log.d(TAG, "Restart video source.");
videoSource.restart();
videoSourceStopped = false;
}
}
});
}
private void reportError(final String errorMessage) {
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) {

View File

@ -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<String, Void, SignalingParameters> {
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<IceCandidate> iceCandidates = null;
SessionDescription offerSdp = null;
try {
LinkedList<IceCandidate> 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<IceCandidate>();
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<IceCandidate>();
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<PeerConnection.IceServer> iceServers =
iceServersFromPCConfigJSON(roomJson.getString("pc_config"));
boolean isTurnPresent = false;
for (PeerConnection.IceServer server : iceServers) {
Log.d(TAG, "IceServer: " + server);
if (server.uri.startsWith("turn:")) {
isTurnPresent = true;
break;
LinkedList<PeerConnection.IceServer> iceServers =
iceServersFromPCConfigJSON(roomJson.getString("pc_config"));
boolean isTurnPresent = false;
for (PeerConnection.IceServer server : iceServers) {
Log.d(TAG, "IceServer: " + server);
if (server.uri.startsWith("turn:")) {
isTurnPresent = true;
break;
}
}
}
if (!isTurnPresent) {
LinkedList<PeerConnection.IceServer> turnServers =
requestTurnServers(roomJson.getString("turn_url"));
for (PeerConnection.IceServer turnServer : turnServers) {
Log.d(TAG, "TurnServer: " + turnServer);
iceServers.add(turnServer);
if (!isTurnPresent) {
LinkedList<PeerConnection.IceServer> 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)) {

View File

@ -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<String> 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<String>();
@ -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) {

View File

@ -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/<room>, 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/<room>, retrieve room parameters
// and connect to WebSocket server.
@Override
public void connectToRoom(String url, boolean loopback) {
this.loopback = loopback;
// Create WebSocket client.
wsClient = new WebSocketChannelClient(this);
// Get room parameters.
roomState = ConnectionState.NEW;
fetcher = new RoomParametersFetcher(this, loopback);
fetcher.execute(url);
}
@Override
public void disconnect() {
Log.d(TAG, "Disconnect. Room state: " + roomState);
if (roomState == ConnectionState.CONNECTED) {
Log.d(TAG, "Closing room.");
sendPostMessage(MessageType.BYE, byeMessageUrl, "");
}
roomState = ConnectionState.CLOSED;
if (wsClient != null) {
wsClient.disconnect();
}
}
// Send local SDP (offer or answer, depending on role) to the
// other participant. Note that it is important to send the output of
// create{Offer,Answer} and not merely the current value of
// getLocalDescription() because the latter may include ICE candidates that
// we might want to filter elsewhere.
@Override
public void sendOfferSdp(final SessionDescription sdp) {
if (roomState != ConnectionState.CONNECTED) {
reportError("Sending offer SDP in non connected state.");
return;
}
JSONObject json = new JSONObject();
jsonPut(json, "sdp", sdp.description);
jsonPut(json, "type", "offer");
sendPostMessage(MessageType.MESSAGE, postMessageUrl, json.toString());
if (loopback) {
// In loopback mode rename this offer to answer and route it back.
SessionDescription sdpAnswer = new SessionDescription(
SessionDescription.Type.fromCanonicalForm("answer"),
sdp.description);
events.onRemoteDescription(sdpAnswer);
}
}
@Override
public void sendAnswerSdp(final SessionDescription sdp) {
if (loopback) {
Log.e(TAG, "Sending answer in loopback mode.");
return;
}
if (wsClient.getState() != WebSocketConnectionState.REGISTERED) {
reportError("Sending answer SDP in non registered state.");
return;
}
JSONObject json = new JSONObject();
jsonPut(json, "sdp", sdp.description);
jsonPut(json, "type", "answer");
wsClient.send(json.toString());
}
// Send Ice candidate to the other participant.
@Override
public void sendLocalIceCandidate(final IceCandidate candidate) {
JSONObject json = new JSONObject();
jsonPut(json, "type", "candidate");
jsonPut(json, "label", candidate.sdpMLineIndex);
jsonPut(json, "id", candidate.sdpMid);
jsonPut(json, "candidate", candidate.sdp);
if (initiator) {
// Call initiator sends ice candidates to GAE server.
if (roomState != ConnectionState.CONNECTED) {
reportError("Sending ICE candidate in non connected state.");
return;
}
sendPostMessage(MessageType.MESSAGE, postMessageUrl, json.toString());
if (loopback) {
events.onRemoteIceCandidate(candidate);
}
} else {
// Call receiver sends ice candidates to websocket server.
if (wsClient.getState() != WebSocketConnectionState.REGISTERED) {
reportError("Sending ICE candidate in non registered state.");
return;
}
wsClient.send(json.toString());
}
}
// --------------------------------------------------------------------
// Helper functions.
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();
}
}

View File

@ -0,0 +1,123 @@
/*
* libjingle
* Copyright 2015, Google Inc.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. The name of the author may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.appspot.apprtc.util;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.util.Scanner;
/**
* Asynchronious http requests implementation.
*/
public class AsyncHttpURLConnection {
private static final int HTTP_TIMEOUT_MS = 5000;
private final String method;
private final String url;
private final String message;
private final AsyncHttpEvents events;
public interface AsyncHttpEvents {
public void OnHttpError(String errorMessage);
public void OnHttpComplete(String response);
}
public AsyncHttpURLConnection(String method, String url, String message,
AsyncHttpEvents events) {
this.method = method;
this.url = url;
this.message = message;
this.events = events;
}
public void send() {
Runnable runHttp = new Runnable() {
public void run() {
sendHttpMessage();
}
};
new Thread(runHttp).start();
}
private void sendHttpMessage() {
try {
HttpURLConnection connection =
(HttpURLConnection) new URL(url).openConnection();
byte[] postData = new byte[0];
if (message != null) {
postData = message.getBytes("UTF-8");
}
connection.setRequestMethod(method);
connection.setUseCaches(false);
connection.setDoInput(true);
connection.setConnectTimeout(HTTP_TIMEOUT_MS);
connection.setReadTimeout(HTTP_TIMEOUT_MS);
boolean doOutput = false;
if (method.equals("POST")) {
doOutput = true;
connection.setDoOutput(true);
connection.setFixedLengthStreamingMode(postData.length);
}
connection.setRequestProperty(
"content-type", "text/plain; charset=utf-8");
// Send POST request.
if (doOutput && postData.length > 0) {
OutputStream outStream = connection.getOutputStream();
outStream.write(postData);
outStream.close();
}
// Get response.
int responseCode = connection.getResponseCode();
if (responseCode != 200) {
events.OnHttpError("Non-200 response to " + method + " to URL: "
+ url + " : " + connection.getHeaderField(null));
return;
}
InputStream responseStream = connection.getInputStream();
String response = drainStream(responseStream);
responseStream.close();
events.OnHttpComplete(response);
} catch (SocketTimeoutException e) {
events.OnHttpError("HTTP " + method + " to " + url + " timeout");
} catch (IOException e) {
events.OnHttpError("HTTP " + method + " to " + url + " error: "
+ e.getMessage());
}
}
// Return the contents of an InputStream as a String.
private static String drainStream(InputStream in) {
Scanner s = new Scanner(in).useDelimiter("\\A");
return s.hasNext() ? s.next() : "";
}
}

View File

@ -0,0 +1,107 @@
/*
* libjingle
* Copyright 2015, Google Inc.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. The name of the author may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.appspot.apprtc.util;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import java.util.concurrent.Executor;
/**
* Looper based executor class.
*/
public class LooperExecutor extends Thread implements Executor {
private static final String TAG = "LooperExecutor";
// Object used to signal that looper thread has started and Handler instance
// associated with looper thread has been allocated.
private final Object looperStartedEvent = new Object();
private Handler handler = null;
private boolean running = false;
private long threadId;
@Override
public void run() {
Looper.prepare();
synchronized (looperStartedEvent) {
Log.d(TAG, "Looper thread started.");
handler = new Handler();
threadId = Thread.currentThread().getId();
looperStartedEvent.notify();
}
Looper.loop();
}
public synchronized void requestStart() {
if (running) {
return;
}
running = true;
handler = null;
start();
// Wait for Hander allocation.
synchronized (looperStartedEvent) {
while (handler == null) {
try {
looperStartedEvent.wait();
} catch (InterruptedException e) {
Log.e(TAG, "Can not start looper thread");
running = false;
}
}
}
}
public synchronized void requestStop() {
if (!running) {
return;
}
running = false;
handler.post( new Runnable() {
@Override
public void run() {
Looper.myLooper().quitSafely();
Log.d(TAG, "Looper thread finished.");
}
});
}
@Override
public synchronized void execute(final Runnable runnable) {
if (!running) {
Log.w(TAG, "Running looper executor without calling requestStart()");
return;
}
if (Thread.currentThread().getId() == threadId) {
runnable.run();
} else {
handler.post(runnable);
}
}
}

View File

@ -0,0 +1,84 @@
/*
* libjingle
* Copyright 2015, Google Inc.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* 3. The name of the author may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
* EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
* OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.appspot.apprtc.test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.appspot.apprtc.util.LooperExecutor;
import android.test.InstrumentationTestCase;
import android.util.Log;
public class LooperExecutorTest extends InstrumentationTestCase {
private static final String TAG = "LooperTest";
private static final int WAIT_TIMEOUT = 5000;
public void testLooperExecutor() throws InterruptedException {
Log.d(TAG, "testLooperExecutor");
final int counter[] = new int[1];
final int expectedCounter = 10;
final CountDownLatch looperDone = new CountDownLatch(1);
Runnable counterIncRunnable = new Runnable() {
@Override
public void run() {
counter[0]++;
Log.d(TAG, "Run " + counter[0]);
}
};
LooperExecutor executor = new LooperExecutor();
// Try to execute a counter increment task before starting an executor.
executor.execute(counterIncRunnable);
// Start the executor and run expected amount of counter increment task.
executor.requestStart();
for (int i = 0; i < expectedCounter; i++) {
executor.execute(counterIncRunnable);
}
executor.execute(new Runnable() {
@Override
public void run() {
looperDone.countDown();
}
});
executor.requestStop();
// Try to execute a task after stopping the executor.
executor.execute(counterIncRunnable);
// Wait for final looper task and make sure the counter increment task
// is executed expected amount of times.
looperDone.await(WAIT_TIMEOUT, TimeUnit.MILLISECONDS);
assertTrue (looperDone.getCount() == 0);
assertTrue (counter[0] == expectedCounter);
Log.d(TAG, "testLooperExecutor done");
}
}

View File

@ -35,10 +35,10 @@ import java.util.concurrent.TimeUnit;
import org.appspot.apprtc.AppRTCClient.SignalingParameters;
import org.appspot.apprtc.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.");
}
}

View File

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