blob: 1c802660387415e53a94c3584a934a7c85a3645f [file] [log] [blame]
/*
* 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;
import org.appspot.apprtc.AppRTCClient.RoomConnectionParameters;
import org.appspot.apprtc.AppRTCClient.SignalingParameters;
import org.appspot.apprtc.PeerConnectionClient.PeerConnectionParameters;
import org.appspot.apprtc.util.LooperExecutor;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.FragmentTransaction;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.opengl.GLSurfaceView;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Toast;
import org.webrtc.IceCandidate;
import org.webrtc.SessionDescription;
import org.webrtc.StatsReport;
import org.webrtc.VideoRenderer;
import org.webrtc.VideoRendererGui;
import org.webrtc.VideoRendererGui.ScalingType;
/**
* Activity for peer connection call setup, call waiting
* and call view.
*/
public class CallActivity extends Activity
implements AppRTCClient.SignalingEvents,
PeerConnectionClient.PeerConnectionEvents,
CallFragment.OnCallEvents {
public static final String EXTRA_ROOMID =
"org.appspot.apprtc.ROOMID";
public static final String EXTRA_LOOPBACK =
"org.appspot.apprtc.LOOPBACK";
public static final String EXTRA_HWCODEC =
"org.appspot.apprtc.HWCODEC";
public static final String EXTRA_VIDEO_BITRATE =
"org.appspot.apprtc.VIDEO_BITRATE";
public static final String EXTRA_VIDEO_WIDTH =
"org.appspot.apprtc.VIDEO_WIDTH";
public static final String EXTRA_VIDEO_HEIGHT =
"org.appspot.apprtc.VIDEO_HEIGHT";
public static final String EXTRA_VIDEO_FPS =
"org.appspot.apprtc.VIDEO_FPS";
public static final String EXTRA_VIDEOCODEC =
"org.appspot.apprtc.VIDEOCODEC";
public static final String EXTRA_CPUOVERUSE_DETECTION =
"org.appspot.apprtc.CPUOVERUSE_DETECTION";
public static final String EXTRA_DISPLAY_HUD =
"org.appspot.apprtc.DISPLAY_HUD";
public static final String EXTRA_CMDLINE =
"org.appspot.apprtc.CMDLINE";
public static final String EXTRA_RUNTIME =
"org.appspot.apprtc.RUNTIME";
private static final String TAG = "CallRTCClient";
// Peer connection statistics callback period in ms.
private static final int STAT_CALLBACK_PERIOD = 1000;
// Local preview screen position before call is connected.
private static final int LOCAL_X_CONNECTING = 0;
private static final int LOCAL_Y_CONNECTING = 0;
private static final int LOCAL_WIDTH_CONNECTING = 100;
private static final int LOCAL_HEIGHT_CONNECTING = 100;
// Local preview screen position after call is connected.
private static final int LOCAL_X_CONNECTED = 72;
private static final int LOCAL_Y_CONNECTED = 72;
private static final int LOCAL_WIDTH_CONNECTED = 25;
private static final int LOCAL_HEIGHT_CONNECTED = 25;
// Remote video screen position
private static final int REMOTE_X = 0;
private static final int REMOTE_Y = 0;
private static final int REMOTE_WIDTH = 100;
private static final int REMOTE_HEIGHT = 100;
private PeerConnectionClient peerConnectionClient = null;
private AppRTCClient appRtcClient;
private SignalingParameters signalingParameters;
private AppRTCAudioManager audioManager = null;
private VideoRenderer.Callbacks localRender;
private VideoRenderer.Callbacks remoteRender;
private ScalingType scalingType;
private Toast logToast;
private boolean commandLineRun;
private int runTimeMs;
private boolean activityRunning;
private RoomConnectionParameters roomConnectionParameters;
private PeerConnectionParameters peerConnectionParameters;
private boolean hwCodecAcceleration;
private String videoCodec;
private boolean iceConnected;
private boolean isError;
private boolean callControlFragmentVisible = true;
// Controls
private GLSurfaceView videoView;
CallFragment callFragment;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Thread.setDefaultUncaughtExceptionHandler(
new UnhandledExceptionHandler(this));
// Set window styles for fullscreen-window size. Needs to be done before
// adding content.
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
getWindow().getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
setContentView(R.layout.activity_call);
iceConnected = false;
signalingParameters = null;
scalingType = ScalingType.SCALE_ASPECT_FILL;
// Create UI controls.
videoView = (GLSurfaceView) findViewById(R.id.glview_call);
callFragment = new CallFragment();
// Create video renderers.
VideoRendererGui.setView(videoView, new Runnable() {
@Override
public void run() {
createPeerConnectionFactory();
}
});
remoteRender = VideoRendererGui.create(
REMOTE_X, REMOTE_Y,
REMOTE_WIDTH, REMOTE_HEIGHT, scalingType, false);
localRender = VideoRendererGui.create(
LOCAL_X_CONNECTING, LOCAL_Y_CONNECTING,
LOCAL_WIDTH_CONNECTING, LOCAL_HEIGHT_CONNECTING, scalingType, true);
// Show/hide call control fragment on view click.
videoView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
toggleCallControlFragmentVisibility();
}
});
// Get Intent parameters.
final Intent intent = getIntent();
Uri roomUri = intent.getData();
if (roomUri == null) {
logAndToast(getString(R.string.missing_url));
Log.e(TAG, "Didn't get any URL in intent!");
setResult(RESULT_CANCELED);
finish();
return;
}
String roomId = intent.getStringExtra(EXTRA_ROOMID);
if (roomId == null || roomId.length() == 0) {
logAndToast(getString(R.string.missing_url));
Log.e(TAG, "Incorrect room ID in intent!");
setResult(RESULT_CANCELED);
finish();
return;
}
boolean loopback = intent.getBooleanExtra(EXTRA_LOOPBACK, false);
hwCodecAcceleration = intent.getBooleanExtra(EXTRA_HWCODEC, true);
if (intent.hasExtra(EXTRA_VIDEOCODEC)) {
videoCodec = intent.getStringExtra(EXTRA_VIDEOCODEC);
} else {
videoCodec = PeerConnectionClient.VIDEO_CODEC_VP8; // use VP8 by default.
}
peerConnectionParameters = new PeerConnectionParameters(
intent.getIntExtra(EXTRA_VIDEO_WIDTH, 0),
intent.getIntExtra(EXTRA_VIDEO_HEIGHT, 0),
intent.getIntExtra(EXTRA_VIDEO_FPS, 0),
intent.getIntExtra(EXTRA_VIDEO_BITRATE, 0),
intent.getBooleanExtra(EXTRA_CPUOVERUSE_DETECTION, true));
commandLineRun = intent.getBooleanExtra(EXTRA_CMDLINE, false);
runTimeMs = intent.getIntExtra(EXTRA_RUNTIME, 0);
// Create connection client and connection parameters.
appRtcClient = new WebSocketRTCClient(this, new LooperExecutor());
roomConnectionParameters = new RoomConnectionParameters(
roomUri.toString(), roomId, loopback);
// Send intent arguments to fragment.
callFragment.setArguments(intent.getExtras());
// Activate call fragment and start the call.
getFragmentManager().beginTransaction()
.add(R.id.call_fragment_container, callFragment).commit();
startCall();
// For command line execution run connection for <runTimeMs> and exit.
if (commandLineRun && runTimeMs > 0) {
videoView.postDelayed(new Runnable() {
public void run() {
disconnect();
}
}, runTimeMs);
}
}
// Activity interfaces
@Override
public void onPause() {
super.onPause();
videoView.onPause();
activityRunning = false;
if (peerConnectionClient != null) {
peerConnectionClient.stopVideoSource();
}
}
@Override
public void onResume() {
super.onResume();
videoView.onResume();
activityRunning = true;
if (peerConnectionClient != null) {
peerConnectionClient.startVideoSource();
}
}
@Override
protected void onDestroy() {
disconnect();
super.onDestroy();
if (logToast != null) {
logToast.cancel();
}
activityRunning = false;
}
// CallFragment.OnCallEvents interface implementation.
@Override
public void onCallHangUp() {
disconnect();
}
@Override
public void onCameraSwitch() {
if (peerConnectionClient != null) {
peerConnectionClient.switchCamera();
}
}
@Override
public void onVideoScalingSwitch(ScalingType scalingType) {
this.scalingType = scalingType;
updateVideoView();
}
// Helper functions.
private void toggleCallControlFragmentVisibility() {
if (!iceConnected || !callFragment.isAdded()) {
return;
}
// Show/hide call control fragment
callControlFragmentVisible = !callControlFragmentVisible;
FragmentTransaction ft = getFragmentManager().beginTransaction();
if (callControlFragmentVisible) {
ft.show(callFragment);
} else {
ft.hide(callFragment);
}
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
ft.commit();
}
private void updateVideoView() {
VideoRendererGui.update(remoteRender,
REMOTE_X, REMOTE_Y,
REMOTE_WIDTH, REMOTE_HEIGHT, scalingType);
if (iceConnected) {
VideoRendererGui.update(localRender,
LOCAL_X_CONNECTED, LOCAL_Y_CONNECTED,
LOCAL_WIDTH_CONNECTED, LOCAL_HEIGHT_CONNECTED,
ScalingType.SCALE_ASPECT_FIT);
} else {
VideoRendererGui.update(localRender,
LOCAL_X_CONNECTING, LOCAL_Y_CONNECTING,
LOCAL_WIDTH_CONNECTING, LOCAL_HEIGHT_CONNECTING, scalingType);
}
}
private void startCall() {
if (appRtcClient == null) {
Log.e(TAG, "AppRTC client is not allocated for a call.");
return;
}
// Start room connection.
logAndToast(getString(R.string.connecting_to,
roomConnectionParameters.roomUrl));
appRtcClient.connectToRoom(roomConnectionParameters);
// 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();
}
// Should be called from UI thread
private void callConnected() {
// Update video view.
updateVideoView();
// Enable statistics callback.
peerConnectionClient.enableStatsEvents(true, STAT_CALLBACK_PERIOD);
}
private void onAudioManagerChangedState() {
// TODO(henrika): disable video if AppRTCAudioManager.AudioDevice.EARPIECE
// is active.
}
// Create peer connection factory when EGL context is ready.
private void createPeerConnectionFactory() {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (peerConnectionClient == null) {
peerConnectionClient = new PeerConnectionClient();
peerConnectionClient.createPeerConnectionFactory(CallActivity.this,
videoCodec, hwCodecAcceleration,
VideoRendererGui.getEGLContext(), CallActivity.this);
}
if (signalingParameters != null) {
Log.w(TAG, "EGL context is ready after room connection.");
onConnectedToRoomInternal(signalingParameters);
}
}
});
}
// Disconnect from remote resources, dispose of local resources, and exit.
private void disconnect() {
if (appRtcClient != null) {
appRtcClient.disconnectFromRoom();
appRtcClient = null;
}
if (peerConnectionClient != null) {
peerConnectionClient.close();
peerConnectionClient = null;
}
if (audioManager != null) {
audioManager.close();
audioManager = null;
}
if (iceConnected && !isError) {
setResult(RESULT_OK);
} else {
setResult(RESULT_CANCELED);
}
finish();
}
private void disconnectWithErrorMessage(final String errorMessage) {
if (commandLineRun || !activityRunning) {
Log.e(TAG, "Critical error: " + errorMessage);
disconnect();
} else {
new AlertDialog.Builder(this)
.setTitle(getText(R.string.channel_error_title))
.setMessage(errorMessage)
.setCancelable(false)
.setNeutralButton(R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
dialog.cancel();
disconnect();
}
}).create().show();
}
}
// Log |msg| and Toast about it.
private void logAndToast(String msg) {
Log.d(TAG, msg);
if (logToast != null) {
logToast.cancel();
}
logToast = Toast.makeText(this, msg, Toast.LENGTH_SHORT);
logToast.show();
}
// -----Implementation of AppRTCClient.AppRTCSignalingEvents ---------------
// All callbacks are invoked from websocket signaling looper thread and
// are routed to UI thread.
private void onConnectedToRoomInternal(final SignalingParameters params) {
signalingParameters = params;
if (peerConnectionClient == null) {
Log.w(TAG, "Room is connected, but EGL context is not ready yet.");
return;
}
logAndToast("Creating peer connection...");
peerConnectionClient.createPeerConnection(
localRender, remoteRender,
signalingParameters, peerConnectionParameters);
if (signalingParameters.initiator) {
logAndToast("Creating OFFER...");
// Create offer. Offer SDP will be sent to answering client in
// PeerConnectionEvents.onLocalDescription event.
peerConnectionClient.createOffer();
} else {
if (params.offerSdp != null) {
peerConnectionClient.setRemoteDescription(params.offerSdp);
logAndToast("Creating ANSWER...");
// Create answer. Answer SDP will be sent to offering client in
// PeerConnectionEvents.onLocalDescription event.
peerConnectionClient.createAnswer();
}
if (params.iceCandidates != null) {
// Add remote ICE candidates from room.
for (IceCandidate iceCandidate : params.iceCandidates) {
peerConnectionClient.addRemoteIceCandidate(iceCandidate);
}
}
}
}
@Override
public void onConnectedToRoom(final SignalingParameters params) {
runOnUiThread(new Runnable() {
@Override
public void run() {
onConnectedToRoomInternal(params);
}
});
}
@Override
public void onRemoteDescription(final SessionDescription sdp) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (peerConnectionClient == null) {
Log.e(TAG, "Received remote SDP for non-initilized peer connection.");
return;
}
logAndToast("Received remote " + sdp.type + " ...");
peerConnectionClient.setRemoteDescription(sdp);
if (!signalingParameters.initiator) {
logAndToast("Creating ANSWER...");
// Create answer. Answer SDP will be sent to offering client in
// PeerConnectionEvents.onLocalDescription event.
peerConnectionClient.createAnswer();
}
}
});
}
@Override
public void onRemoteIceCandidate(final IceCandidate candidate) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (peerConnectionClient == null) {
Log.e(TAG,
"Received ICE candidate for non-initilized peer connection.");
return;
}
peerConnectionClient.addRemoteIceCandidate(candidate);
}
});
}
@Override
public void onChannelClose() {
runOnUiThread(new Runnable() {
@Override
public void run() {
logAndToast("Remote end hung up; dropping PeerConnection");
disconnect();
}
});
}
@Override
public void onChannelError(final String 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 peer connection client looper thread and
// are routed to UI thread.
@Override
public void onLocalDescription(final SessionDescription 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) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (appRtcClient != null) {
appRtcClient.sendLocalIceCandidate(candidate);
}
}
});
}
@Override
public void onIceConnected() {
runOnUiThread(new Runnable() {
@Override
public void run() {
logAndToast("ICE connected");
iceConnected = true;
callConnected();
}
});
}
@Override
public void onIceDisconnected() {
runOnUiThread(new Runnable() {
@Override
public void run() {
logAndToast("ICE disconnected");
iceConnected = false;
disconnect();
}
});
}
@Override
public void onPeerConnectionClosed() {
}
@Override
public void onPeerConnectionStatsReady(final StatsReport[] reports) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (!isError && iceConnected) {
callFragment.updateEncoderStatistics(reports);
}
}
});
}
@Override
public void onPeerConnectionError(final String description) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (!isError) {
isError = true;
disconnectWithErrorMessage(description);
}
}
});
}
}