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:
parent
2ec50f2b0f
commit
f6a9714760
@ -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
|
||||
|
@ -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>
|
@ -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;
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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) {
|
||||
|
@ -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)) {
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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() : "";
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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.");
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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': [
|
||||
|
Loading…
x
Reference in New Issue
Block a user