/* * 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_VIDEO_CALL = "org.appspot.apprtc.VIDEO_CALL"; 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_VIDEO_BITRATE = "org.appspot.apprtc.VIDEO_BITRATE"; public static final String EXTRA_VIDEOCODEC = "org.appspot.apprtc.VIDEOCODEC"; public static final String EXTRA_HWCODEC_ENABLED = "org.appspot.apprtc.HWCODEC"; public static final String EXTRA_AUDIO_BITRATE = "org.appspot.apprtc.AUDIO_BITRATE"; public static final String EXTRA_AUDIOCODEC = "org.appspot.apprtc.AUDIOCODEC"; 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 iceConnected; private boolean isError; private boolean callControlFragmentVisible = true; private long callStartedTimeMs = 0; // 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); peerConnectionParameters = new PeerConnectionParameters( intent.getBooleanExtra(EXTRA_VIDEO_CALL, true), loopback, 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.getStringExtra(EXTRA_VIDEOCODEC), intent.getBooleanExtra(EXTRA_HWCODEC_ENABLED, true), intent.getIntExtra(EXTRA_AUDIO_BITRATE, 0), intent.getStringExtra(EXTRA_AUDIOCODEC), 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 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; } callStartedTimeMs = System.currentTimeMillis(); // 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() { final long delta = System.currentTimeMillis() - callStartedTimeMs; Log.i(TAG, "Call connected: delay=" + delta + "ms"); // 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) { final long delta = System.currentTimeMillis() - callStartedTimeMs; Log.d(TAG, "Creating peer connection factory, delay=" + delta + "ms"); peerConnectionClient = new PeerConnectionClient(); peerConnectionClient.createPeerConnectionFactory(CallActivity.this, VideoRendererGui.getEGLContext(), peerConnectionParameters, 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) { final long delta = System.currentTimeMillis() - callStartedTimeMs; signalingParameters = params; if (peerConnectionClient == null) { Log.w(TAG, "Room is connected, but EGL context is not ready yet."); return; } logAndToast("Creating peer connection, delay=" + delta + "ms"); peerConnectionClient.createPeerConnection( localRender, remoteRender, signalingParameters); 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) { final long delta = System.currentTimeMillis() - callStartedTimeMs; 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 + ", delay=" + delta + "ms"); 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) { final long delta = System.currentTimeMillis() - callStartedTimeMs; runOnUiThread(new Runnable() { @Override public void run() { if (appRtcClient != null) { logAndToast("Sending " + sdp.type + ", delay=" + delta + "ms"); 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() { final long delta = System.currentTimeMillis() - callStartedTimeMs; runOnUiThread(new Runnable() { @Override public void run() { logAndToast("ICE connected, delay=" + delta + "ms"); 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); } } }); } }