1 /*
2  *  Copyright 2015 The WebRTC Project Authors. All rights reserved.
3  *
4  *  Use of this source code is governed by a BSD-style license
5  *  that can be found in the LICENSE file in the root of the source
6  *  tree. An additional intellectual property rights grant can be found
7  *  in the file PATENTS.  All contributing project authors may
8  *  be found in the AUTHORS file in the root of the source tree.
9  */
10 
11 package org.appspot.apprtc;
12 
13 import android.annotation.TargetApi;
14 import android.app.Activity;
15 import android.app.AlertDialog;
16 import android.app.FragmentTransaction;
17 import android.content.Context;
18 import android.content.DialogInterface;
19 import android.content.Intent;
20 import android.content.pm.PackageManager;
21 import android.media.projection.MediaProjection;
22 import android.media.projection.MediaProjectionManager;
23 import android.net.Uri;
24 import android.os.Build;
25 import android.os.Bundle;
26 import android.os.Handler;
27 import android.support.annotation.Nullable;
28 import android.util.DisplayMetrics;
29 import android.util.Log;
30 import android.view.View;
31 import android.view.Window;
32 import android.view.WindowManager;
33 import android.view.WindowManager.LayoutParams;
34 import android.widget.Toast;
35 import java.io.IOException;
36 import java.lang.RuntimeException;
37 import java.util.ArrayList;
38 import java.util.List;
39 import java.util.Set;
40 import org.appspot.apprtc.AppRTCAudioManager.AudioDevice;
41 import org.appspot.apprtc.AppRTCAudioManager.AudioManagerEvents;
42 import org.appspot.apprtc.AppRTCClient.RoomConnectionParameters;
43 import org.appspot.apprtc.AppRTCClient.SignalingParameters;
44 import org.appspot.apprtc.PeerConnectionClient.DataChannelParameters;
45 import org.appspot.apprtc.PeerConnectionClient.PeerConnectionParameters;
46 import org.webrtc.Camera1Enumerator;
47 import org.webrtc.Camera2Enumerator;
48 import org.webrtc.CameraEnumerator;
49 import org.webrtc.EglBase;
50 import org.webrtc.FileVideoCapturer;
51 import org.webrtc.IceCandidate;
52 import org.webrtc.Logging;
53 import org.webrtc.PeerConnectionFactory;
54 import org.webrtc.RendererCommon.ScalingType;
55 import org.webrtc.ScreenCapturerAndroid;
56 import org.webrtc.SessionDescription;
57 import org.webrtc.StatsReport;
58 import org.webrtc.SurfaceViewRenderer;
59 import org.webrtc.VideoCapturer;
60 import org.webrtc.VideoFileRenderer;
61 import org.webrtc.VideoFrame;
62 import org.webrtc.VideoSink;
63 
64 /**
65  * Activity for peer connection call setup, call waiting
66  * and call view.
67  */
68 public class CallActivity extends Activity implements AppRTCClient.SignalingEvents,
69                                                       PeerConnectionClient.PeerConnectionEvents,
70                                                       CallFragment.OnCallEvents {
71   private static final String TAG = "CallRTCClient";
72 
73   public static final String EXTRA_ROOMID = "org.appspot.apprtc.ROOMID";
74   public static final String EXTRA_URLPARAMETERS = "org.appspot.apprtc.URLPARAMETERS";
75   public static final String EXTRA_LOOPBACK = "org.appspot.apprtc.LOOPBACK";
76   public static final String EXTRA_VIDEO_CALL = "org.appspot.apprtc.VIDEO_CALL";
77   public static final String EXTRA_SCREENCAPTURE = "org.appspot.apprtc.SCREENCAPTURE";
78   public static final String EXTRA_CAMERA2 = "org.appspot.apprtc.CAMERA2";
79   public static final String EXTRA_VIDEO_WIDTH = "org.appspot.apprtc.VIDEO_WIDTH";
80   public static final String EXTRA_VIDEO_HEIGHT = "org.appspot.apprtc.VIDEO_HEIGHT";
81   public static final String EXTRA_VIDEO_FPS = "org.appspot.apprtc.VIDEO_FPS";
82   public static final String EXTRA_VIDEO_CAPTUREQUALITYSLIDER_ENABLED =
83       "org.appsopt.apprtc.VIDEO_CAPTUREQUALITYSLIDER";
84   public static final String EXTRA_VIDEO_BITRATE = "org.appspot.apprtc.VIDEO_BITRATE";
85   public static final String EXTRA_VIDEOCODEC = "org.appspot.apprtc.VIDEOCODEC";
86   public static final String EXTRA_HWCODEC_ENABLED = "org.appspot.apprtc.HWCODEC";
87   public static final String EXTRA_CAPTURETOTEXTURE_ENABLED = "org.appspot.apprtc.CAPTURETOTEXTURE";
88   public static final String EXTRA_FLEXFEC_ENABLED = "org.appspot.apprtc.FLEXFEC";
89   public static final String EXTRA_AUDIO_BITRATE = "org.appspot.apprtc.AUDIO_BITRATE";
90   public static final String EXTRA_AUDIOCODEC = "org.appspot.apprtc.AUDIOCODEC";
91   public static final String EXTRA_NOAUDIOPROCESSING_ENABLED =
92       "org.appspot.apprtc.NOAUDIOPROCESSING";
93   public static final String EXTRA_AECDUMP_ENABLED = "org.appspot.apprtc.AECDUMP";
94   public static final String EXTRA_SAVE_INPUT_AUDIO_TO_FILE_ENABLED =
95       "org.appspot.apprtc.SAVE_INPUT_AUDIO_TO_FILE";
96   public static final String EXTRA_OPENSLES_ENABLED = "org.appspot.apprtc.OPENSLES";
97   public static final String EXTRA_DISABLE_BUILT_IN_AEC = "org.appspot.apprtc.DISABLE_BUILT_IN_AEC";
98   public static final String EXTRA_DISABLE_BUILT_IN_AGC = "org.appspot.apprtc.DISABLE_BUILT_IN_AGC";
99   public static final String EXTRA_DISABLE_BUILT_IN_NS = "org.appspot.apprtc.DISABLE_BUILT_IN_NS";
100   public static final String EXTRA_DISABLE_WEBRTC_AGC_AND_HPF =
101       "org.appspot.apprtc.DISABLE_WEBRTC_GAIN_CONTROL";
102   public static final String EXTRA_DISPLAY_HUD = "org.appspot.apprtc.DISPLAY_HUD";
103   public static final String EXTRA_TRACING = "org.appspot.apprtc.TRACING";
104   public static final String EXTRA_CMDLINE = "org.appspot.apprtc.CMDLINE";
105   public static final String EXTRA_RUNTIME = "org.appspot.apprtc.RUNTIME";
106   public static final String EXTRA_VIDEO_FILE_AS_CAMERA = "org.appspot.apprtc.VIDEO_FILE_AS_CAMERA";
107   public static final String EXTRA_SAVE_REMOTE_VIDEO_TO_FILE =
108       "org.appspot.apprtc.SAVE_REMOTE_VIDEO_TO_FILE";
109   public static final String EXTRA_SAVE_REMOTE_VIDEO_TO_FILE_WIDTH =
110       "org.appspot.apprtc.SAVE_REMOTE_VIDEO_TO_FILE_WIDTH";
111   public static final String EXTRA_SAVE_REMOTE_VIDEO_TO_FILE_HEIGHT =
112       "org.appspot.apprtc.SAVE_REMOTE_VIDEO_TO_FILE_HEIGHT";
113   public static final String EXTRA_USE_VALUES_FROM_INTENT =
114       "org.appspot.apprtc.USE_VALUES_FROM_INTENT";
115   public static final String EXTRA_DATA_CHANNEL_ENABLED = "org.appspot.apprtc.DATA_CHANNEL_ENABLED";
116   public static final String EXTRA_ORDERED = "org.appspot.apprtc.ORDERED";
117   public static final String EXTRA_MAX_RETRANSMITS_MS = "org.appspot.apprtc.MAX_RETRANSMITS_MS";
118   public static final String EXTRA_MAX_RETRANSMITS = "org.appspot.apprtc.MAX_RETRANSMITS";
119   public static final String EXTRA_PROTOCOL = "org.appspot.apprtc.PROTOCOL";
120   public static final String EXTRA_NEGOTIATED = "org.appspot.apprtc.NEGOTIATED";
121   public static final String EXTRA_ID = "org.appspot.apprtc.ID";
122   public static final String EXTRA_ENABLE_RTCEVENTLOG = "org.appspot.apprtc.ENABLE_RTCEVENTLOG";
123 
124   private static final int CAPTURE_PERMISSION_REQUEST_CODE = 1;
125 
126   // List of mandatory application permissions.
127   private static final String[] MANDATORY_PERMISSIONS = {"android.permission.MODIFY_AUDIO_SETTINGS",
128       "android.permission.RECORD_AUDIO", "android.permission.INTERNET"};
129 
130   // Peer connection statistics callback period in ms.
131   private static final int STAT_CALLBACK_PERIOD = 1000;
132 
133   private static class ProxyVideoSink implements VideoSink {
134     private VideoSink target;
135 
136     @Override
onFrame(VideoFrame frame)137     synchronized public void onFrame(VideoFrame frame) {
138       if (target == null) {
139         Logging.d(TAG, "Dropping frame in proxy because target is null.");
140         return;
141       }
142 
143       target.onFrame(frame);
144     }
145 
setTarget(VideoSink target)146     synchronized public void setTarget(VideoSink target) {
147       this.target = target;
148     }
149   }
150 
151   private final ProxyVideoSink remoteProxyRenderer = new ProxyVideoSink();
152   private final ProxyVideoSink localProxyVideoSink = new ProxyVideoSink();
153   @Nullable private PeerConnectionClient peerConnectionClient;
154   @Nullable
155   private AppRTCClient appRtcClient;
156   @Nullable
157   private SignalingParameters signalingParameters;
158   @Nullable private AppRTCAudioManager audioManager;
159   @Nullable
160   private SurfaceViewRenderer pipRenderer;
161   @Nullable
162   private SurfaceViewRenderer fullscreenRenderer;
163   @Nullable
164   private VideoFileRenderer videoFileRenderer;
165   private final List<VideoSink> remoteSinks = new ArrayList<>();
166   private Toast logToast;
167   private boolean commandLineRun;
168   private boolean activityRunning;
169   private RoomConnectionParameters roomConnectionParameters;
170   @Nullable
171   private PeerConnectionParameters peerConnectionParameters;
172   private boolean connected;
173   private boolean isError;
174   private boolean callControlFragmentVisible = true;
175   private long callStartedTimeMs;
176   private boolean micEnabled = true;
177   private boolean screencaptureEnabled;
178   private static Intent mediaProjectionPermissionResultData;
179   private static int mediaProjectionPermissionResultCode;
180   // True if local view is in the fullscreen renderer.
181   private boolean isSwappedFeeds;
182 
183   // Controls
184   private CallFragment callFragment;
185   private HudFragment hudFragment;
186   private CpuMonitor cpuMonitor;
187 
188   @Override
189   // TODO(bugs.webrtc.org/8580): LayoutParams.FLAG_TURN_SCREEN_ON and
190   // LayoutParams.FLAG_SHOW_WHEN_LOCKED are deprecated.
191   @SuppressWarnings("deprecation")
onCreate(Bundle savedInstanceState)192   public void onCreate(Bundle savedInstanceState) {
193     super.onCreate(savedInstanceState);
194     Thread.setDefaultUncaughtExceptionHandler(new UnhandledExceptionHandler(this));
195 
196     // Set window styles for fullscreen-window size. Needs to be done before
197     // adding content.
198     requestWindowFeature(Window.FEATURE_NO_TITLE);
199     getWindow().addFlags(LayoutParams.FLAG_FULLSCREEN | LayoutParams.FLAG_KEEP_SCREEN_ON
200         | LayoutParams.FLAG_SHOW_WHEN_LOCKED | LayoutParams.FLAG_TURN_SCREEN_ON);
201     getWindow().getDecorView().setSystemUiVisibility(getSystemUiVisibility());
202     setContentView(R.layout.activity_call);
203 
204     connected = false;
205     signalingParameters = null;
206 
207     // Create UI controls.
208     pipRenderer = findViewById(R.id.pip_video_view);
209     fullscreenRenderer = findViewById(R.id.fullscreen_video_view);
210     callFragment = new CallFragment();
211     hudFragment = new HudFragment();
212 
213     // Show/hide call control fragment on view click.
214     View.OnClickListener listener = new View.OnClickListener() {
215       @Override
216       public void onClick(View view) {
217         toggleCallControlFragmentVisibility();
218       }
219     };
220 
221     // Swap feeds on pip view click.
222     pipRenderer.setOnClickListener(new View.OnClickListener() {
223       @Override
224       public void onClick(View view) {
225         setSwappedFeeds(!isSwappedFeeds);
226       }
227     });
228 
229     fullscreenRenderer.setOnClickListener(listener);
230     remoteSinks.add(remoteProxyRenderer);
231 
232     final Intent intent = getIntent();
233     final EglBase eglBase = EglBase.create();
234 
235     // Create video renderers.
236     pipRenderer.init(eglBase.getEglBaseContext(), null);
237     pipRenderer.setScalingType(ScalingType.SCALE_ASPECT_FIT);
238     String saveRemoteVideoToFile = intent.getStringExtra(EXTRA_SAVE_REMOTE_VIDEO_TO_FILE);
239 
240     // When saveRemoteVideoToFile is set we save the video from the remote to a file.
241     if (saveRemoteVideoToFile != null) {
242       int videoOutWidth = intent.getIntExtra(EXTRA_SAVE_REMOTE_VIDEO_TO_FILE_WIDTH, 0);
243       int videoOutHeight = intent.getIntExtra(EXTRA_SAVE_REMOTE_VIDEO_TO_FILE_HEIGHT, 0);
244       try {
245         videoFileRenderer = new VideoFileRenderer(
246             saveRemoteVideoToFile, videoOutWidth, videoOutHeight, eglBase.getEglBaseContext());
247         remoteSinks.add(videoFileRenderer);
248       } catch (IOException e) {
249         throw new RuntimeException(
250             "Failed to open video file for output: " + saveRemoteVideoToFile, e);
251       }
252     }
253     fullscreenRenderer.init(eglBase.getEglBaseContext(), null);
254     fullscreenRenderer.setScalingType(ScalingType.SCALE_ASPECT_FILL);
255 
256     pipRenderer.setZOrderMediaOverlay(true);
257     pipRenderer.setEnableHardwareScaler(true /* enabled */);
258     fullscreenRenderer.setEnableHardwareScaler(false /* enabled */);
259     // Start with local feed in fullscreen and swap it to the pip when the call is connected.
260     setSwappedFeeds(true /* isSwappedFeeds */);
261 
262     // Check for mandatory permissions.
263     for (String permission : MANDATORY_PERMISSIONS) {
264       if (checkCallingOrSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
265         logAndToast("Permission " + permission + " is not granted");
266         setResult(RESULT_CANCELED);
267         finish();
268         return;
269       }
270     }
271 
272     Uri roomUri = intent.getData();
273     if (roomUri == null) {
274       logAndToast(getString(R.string.missing_url));
275       Log.e(TAG, "Didn't get any URL in intent!");
276       setResult(RESULT_CANCELED);
277       finish();
278       return;
279     }
280 
281     // Get Intent parameters.
282     String roomId = intent.getStringExtra(EXTRA_ROOMID);
283     Log.d(TAG, "Room ID: " + roomId);
284     if (roomId == null || roomId.length() == 0) {
285       logAndToast(getString(R.string.missing_url));
286       Log.e(TAG, "Incorrect room ID in intent!");
287       setResult(RESULT_CANCELED);
288       finish();
289       return;
290     }
291 
292     boolean loopback = intent.getBooleanExtra(EXTRA_LOOPBACK, false);
293     boolean tracing = intent.getBooleanExtra(EXTRA_TRACING, false);
294 
295     int videoWidth = intent.getIntExtra(EXTRA_VIDEO_WIDTH, 0);
296     int videoHeight = intent.getIntExtra(EXTRA_VIDEO_HEIGHT, 0);
297 
298     screencaptureEnabled = intent.getBooleanExtra(EXTRA_SCREENCAPTURE, false);
299     // If capturing format is not specified for screencapture, use screen resolution.
300     if (screencaptureEnabled && videoWidth == 0 && videoHeight == 0) {
301       DisplayMetrics displayMetrics = getDisplayMetrics();
302       videoWidth = displayMetrics.widthPixels;
303       videoHeight = displayMetrics.heightPixels;
304     }
305     DataChannelParameters dataChannelParameters = null;
306     if (intent.getBooleanExtra(EXTRA_DATA_CHANNEL_ENABLED, false)) {
307       dataChannelParameters = new DataChannelParameters(intent.getBooleanExtra(EXTRA_ORDERED, true),
308           intent.getIntExtra(EXTRA_MAX_RETRANSMITS_MS, -1),
309           intent.getIntExtra(EXTRA_MAX_RETRANSMITS, -1), intent.getStringExtra(EXTRA_PROTOCOL),
310           intent.getBooleanExtra(EXTRA_NEGOTIATED, false), intent.getIntExtra(EXTRA_ID, -1));
311     }
312     peerConnectionParameters =
313         new PeerConnectionParameters(intent.getBooleanExtra(EXTRA_VIDEO_CALL, true), loopback,
314             tracing, videoWidth, videoHeight, intent.getIntExtra(EXTRA_VIDEO_FPS, 0),
315             intent.getIntExtra(EXTRA_VIDEO_BITRATE, 0), intent.getStringExtra(EXTRA_VIDEOCODEC),
316             intent.getBooleanExtra(EXTRA_HWCODEC_ENABLED, true),
317             intent.getBooleanExtra(EXTRA_FLEXFEC_ENABLED, false),
318             intent.getIntExtra(EXTRA_AUDIO_BITRATE, 0), intent.getStringExtra(EXTRA_AUDIOCODEC),
319             intent.getBooleanExtra(EXTRA_NOAUDIOPROCESSING_ENABLED, false),
320             intent.getBooleanExtra(EXTRA_AECDUMP_ENABLED, false),
321             intent.getBooleanExtra(EXTRA_SAVE_INPUT_AUDIO_TO_FILE_ENABLED, false),
322             intent.getBooleanExtra(EXTRA_OPENSLES_ENABLED, false),
323             intent.getBooleanExtra(EXTRA_DISABLE_BUILT_IN_AEC, false),
324             intent.getBooleanExtra(EXTRA_DISABLE_BUILT_IN_AGC, false),
325             intent.getBooleanExtra(EXTRA_DISABLE_BUILT_IN_NS, false),
326             intent.getBooleanExtra(EXTRA_DISABLE_WEBRTC_AGC_AND_HPF, false),
327             intent.getBooleanExtra(EXTRA_ENABLE_RTCEVENTLOG, false), dataChannelParameters);
328     commandLineRun = intent.getBooleanExtra(EXTRA_CMDLINE, false);
329     int runTimeMs = intent.getIntExtra(EXTRA_RUNTIME, 0);
330 
331     Log.d(TAG, "VIDEO_FILE: '" + intent.getStringExtra(EXTRA_VIDEO_FILE_AS_CAMERA) + "'");
332 
333     // Create connection client. Use DirectRTCClient if room name is an IP otherwise use the
334     // standard WebSocketRTCClient.
335     if (loopback || !DirectRTCClient.IP_PATTERN.matcher(roomId).matches()) {
336       appRtcClient = new WebSocketRTCClient(this);
337     } else {
338       Log.i(TAG, "Using DirectRTCClient because room name looks like an IP.");
339       appRtcClient = new DirectRTCClient(this);
340     }
341     // Create connection parameters.
342     String urlParameters = intent.getStringExtra(EXTRA_URLPARAMETERS);
343     roomConnectionParameters =
344         new RoomConnectionParameters(roomUri.toString(), roomId, loopback, urlParameters);
345 
346     // Create CPU monitor
347     if (CpuMonitor.isSupported()) {
348       cpuMonitor = new CpuMonitor(this);
349       hudFragment.setCpuMonitor(cpuMonitor);
350     }
351 
352     // Send intent arguments to fragments.
353     callFragment.setArguments(intent.getExtras());
354     hudFragment.setArguments(intent.getExtras());
355     // Activate call and HUD fragments and start the call.
356     FragmentTransaction ft = getFragmentManager().beginTransaction();
357     ft.add(R.id.call_fragment_container, callFragment);
358     ft.add(R.id.hud_fragment_container, hudFragment);
359     ft.commit();
360 
361     // For command line execution run connection for <runTimeMs> and exit.
362     if (commandLineRun && runTimeMs > 0) {
363       (new Handler()).postDelayed(new Runnable() {
364         @Override
365         public void run() {
366           disconnect();
367         }
368       }, runTimeMs);
369     }
370 
371     // Create peer connection client.
372     peerConnectionClient = new PeerConnectionClient(
373         getApplicationContext(), eglBase, peerConnectionParameters, CallActivity.this);
374     PeerConnectionFactory.Options options = new PeerConnectionFactory.Options();
375     if (loopback) {
376       options.networkIgnoreMask = 0;
377     }
378     peerConnectionClient.createPeerConnectionFactory(options);
379 
380     if (screencaptureEnabled) {
381       startScreenCapture();
382     } else {
383       startCall();
384     }
385   }
386 
387   @TargetApi(17)
getDisplayMetrics()388   private DisplayMetrics getDisplayMetrics() {
389     DisplayMetrics displayMetrics = new DisplayMetrics();
390     WindowManager windowManager =
391         (WindowManager) getApplication().getSystemService(Context.WINDOW_SERVICE);
392     windowManager.getDefaultDisplay().getRealMetrics(displayMetrics);
393     return displayMetrics;
394   }
395 
396   @TargetApi(19)
getSystemUiVisibility()397   private static int getSystemUiVisibility() {
398     int flags = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN;
399     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
400       flags |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
401     }
402     return flags;
403   }
404 
405   @TargetApi(21)
startScreenCapture()406   private void startScreenCapture() {
407     MediaProjectionManager mediaProjectionManager =
408         (MediaProjectionManager) getApplication().getSystemService(
409             Context.MEDIA_PROJECTION_SERVICE);
410     startActivityForResult(
411         mediaProjectionManager.createScreenCaptureIntent(), CAPTURE_PERMISSION_REQUEST_CODE);
412   }
413 
414   @Override
onActivityResult(int requestCode, int resultCode, Intent data)415   public void onActivityResult(int requestCode, int resultCode, Intent data) {
416     if (requestCode != CAPTURE_PERMISSION_REQUEST_CODE)
417       return;
418     mediaProjectionPermissionResultCode = resultCode;
419     mediaProjectionPermissionResultData = data;
420     startCall();
421   }
422 
useCamera2()423   private boolean useCamera2() {
424     return Camera2Enumerator.isSupported(this) && getIntent().getBooleanExtra(EXTRA_CAMERA2, true);
425   }
426 
captureToTexture()427   private boolean captureToTexture() {
428     return getIntent().getBooleanExtra(EXTRA_CAPTURETOTEXTURE_ENABLED, false);
429   }
430 
createCameraCapturer(CameraEnumerator enumerator)431   private @Nullable VideoCapturer createCameraCapturer(CameraEnumerator enumerator) {
432     final String[] deviceNames = enumerator.getDeviceNames();
433 
434     // First, try to find front facing camera
435     Logging.d(TAG, "Looking for front facing cameras.");
436     for (String deviceName : deviceNames) {
437       if (enumerator.isFrontFacing(deviceName)) {
438         Logging.d(TAG, "Creating front facing camera capturer.");
439         VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
440 
441         if (videoCapturer != null) {
442           return videoCapturer;
443         }
444       }
445     }
446 
447     // Front facing camera not found, try something else
448     Logging.d(TAG, "Looking for other cameras.");
449     for (String deviceName : deviceNames) {
450       if (!enumerator.isFrontFacing(deviceName)) {
451         Logging.d(TAG, "Creating other camera capturer.");
452         VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
453 
454         if (videoCapturer != null) {
455           return videoCapturer;
456         }
457       }
458     }
459 
460     return null;
461   }
462 
463   @TargetApi(21)
createScreenCapturer()464   private @Nullable VideoCapturer createScreenCapturer() {
465     if (mediaProjectionPermissionResultCode != Activity.RESULT_OK) {
466       reportError("User didn't give permission to capture the screen.");
467       return null;
468     }
469     return new ScreenCapturerAndroid(
470         mediaProjectionPermissionResultData, new MediaProjection.Callback() {
471       @Override
472       public void onStop() {
473         reportError("User revoked permission to capture the screen.");
474       }
475     });
476   }
477 
478   // Activity interfaces
479   @Override
480   public void onStop() {
481     super.onStop();
482     activityRunning = false;
483     // Don't stop the video when using screencapture to allow user to show other apps to the remote
484     // end.
485     if (peerConnectionClient != null && !screencaptureEnabled) {
486       peerConnectionClient.stopVideoSource();
487     }
488     if (cpuMonitor != null) {
489       cpuMonitor.pause();
490     }
491   }
492 
493   @Override
494   public void onStart() {
495     super.onStart();
496     activityRunning = true;
497     // Video is not paused for screencapture. See onPause.
498     if (peerConnectionClient != null && !screencaptureEnabled) {
499       peerConnectionClient.startVideoSource();
500     }
501     if (cpuMonitor != null) {
502       cpuMonitor.resume();
503     }
504   }
505 
506   @Override
507   protected void onDestroy() {
508     Thread.setDefaultUncaughtExceptionHandler(null);
509     disconnect();
510     if (logToast != null) {
511       logToast.cancel();
512     }
513     activityRunning = false;
514     super.onDestroy();
515   }
516 
517   // CallFragment.OnCallEvents interface implementation.
518   @Override
519   public void onCallHangUp() {
520     disconnect();
521   }
522 
523   @Override
524   public void onCameraSwitch() {
525     if (peerConnectionClient != null) {
526       peerConnectionClient.switchCamera();
527     }
528   }
529 
530   @Override
531   public void onVideoScalingSwitch(ScalingType scalingType) {
532     fullscreenRenderer.setScalingType(scalingType);
533   }
534 
535   @Override
536   public void onCaptureFormatChange(int width, int height, int framerate) {
537     if (peerConnectionClient != null) {
538       peerConnectionClient.changeCaptureFormat(width, height, framerate);
539     }
540   }
541 
542   @Override
543   public boolean onToggleMic() {
544     if (peerConnectionClient != null) {
545       micEnabled = !micEnabled;
546       peerConnectionClient.setAudioEnabled(micEnabled);
547     }
548     return micEnabled;
549   }
550 
551   // Helper functions.
552   private void toggleCallControlFragmentVisibility() {
553     if (!connected || !callFragment.isAdded()) {
554       return;
555     }
556     // Show/hide call control fragment
557     callControlFragmentVisible = !callControlFragmentVisible;
558     FragmentTransaction ft = getFragmentManager().beginTransaction();
559     if (callControlFragmentVisible) {
560       ft.show(callFragment);
561       ft.show(hudFragment);
562     } else {
563       ft.hide(callFragment);
564       ft.hide(hudFragment);
565     }
566     ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
567     ft.commit();
568   }
569 
570   private void startCall() {
571     if (appRtcClient == null) {
572       Log.e(TAG, "AppRTC client is not allocated for a call.");
573       return;
574     }
575     callStartedTimeMs = System.currentTimeMillis();
576 
577     // Start room connection.
578     logAndToast(getString(R.string.connecting_to, roomConnectionParameters.roomUrl));
579     appRtcClient.connectToRoom(roomConnectionParameters);
580 
581     // Create and audio manager that will take care of audio routing,
582     // audio modes, audio device enumeration etc.
583     audioManager = AppRTCAudioManager.create(getApplicationContext());
584     // Store existing audio settings and change audio mode to
585     // MODE_IN_COMMUNICATION for best possible VoIP performance.
586     Log.d(TAG, "Starting the audio manager...");
587     audioManager.start(new AudioManagerEvents() {
588       // This method will be called each time the number of available audio
589       // devices has changed.
590       @Override
591       public void onAudioDeviceChanged(
592           AudioDevice audioDevice, Set<AudioDevice> availableAudioDevices) {
593         onAudioManagerDevicesChanged(audioDevice, availableAudioDevices);
594       }
595     });
596   }
597 
598   // Should be called from UI thread
599   private void callConnected() {
600     final long delta = System.currentTimeMillis() - callStartedTimeMs;
601     Log.i(TAG, "Call connected: delay=" + delta + "ms");
602     if (peerConnectionClient == null || isError) {
603       Log.w(TAG, "Call is connected in closed or error state");
604       return;
605     }
606     // Enable statistics callback.
607     peerConnectionClient.enableStatsEvents(true, STAT_CALLBACK_PERIOD);
608     setSwappedFeeds(false /* isSwappedFeeds */);
609   }
610 
611   // This method is called when the audio manager reports audio device change,
612   // e.g. from wired headset to speakerphone.
613   private void onAudioManagerDevicesChanged(
614       final AudioDevice device, final Set<AudioDevice> availableDevices) {
615     Log.d(TAG, "onAudioManagerDevicesChanged: " + availableDevices + ", "
616             + "selected: " + device);
617     // TODO(henrika): add callback handler.
618   }
619 
620   // Disconnect from remote resources, dispose of local resources, and exit.
621   private void disconnect() {
622     activityRunning = false;
623     remoteProxyRenderer.setTarget(null);
624     localProxyVideoSink.setTarget(null);
625     if (appRtcClient != null) {
626       appRtcClient.disconnectFromRoom();
627       appRtcClient = null;
628     }
629     if (pipRenderer != null) {
630       pipRenderer.release();
631       pipRenderer = null;
632     }
633     if (videoFileRenderer != null) {
634       videoFileRenderer.release();
635       videoFileRenderer = null;
636     }
637     if (fullscreenRenderer != null) {
638       fullscreenRenderer.release();
639       fullscreenRenderer = null;
640     }
641     if (peerConnectionClient != null) {
642       peerConnectionClient.close();
643       peerConnectionClient = null;
644     }
645     if (audioManager != null) {
646       audioManager.stop();
647       audioManager = null;
648     }
649     if (connected && !isError) {
650       setResult(RESULT_OK);
651     } else {
652       setResult(RESULT_CANCELED);
653     }
654     finish();
655   }
656 
657   private void disconnectWithErrorMessage(final String errorMessage) {
658     if (commandLineRun || !activityRunning) {
659       Log.e(TAG, "Critical error: " + errorMessage);
660       disconnect();
661     } else {
662       new AlertDialog.Builder(this)
663           .setTitle(getText(R.string.channel_error_title))
664           .setMessage(errorMessage)
665           .setCancelable(false)
666           .setNeutralButton(R.string.ok,
667               new DialogInterface.OnClickListener() {
668                 @Override
669                 public void onClick(DialogInterface dialog, int id) {
670                   dialog.cancel();
671                   disconnect();
672                 }
673               })
674           .create()
675           .show();
676     }
677   }
678 
679   // Log |msg| and Toast about it.
680   private void logAndToast(String msg) {
681     Log.d(TAG, msg);
682     if (logToast != null) {
683       logToast.cancel();
684     }
685     logToast = Toast.makeText(this, msg, Toast.LENGTH_SHORT);
686     logToast.show();
687   }
688 
689   private void reportError(final String description) {
690     runOnUiThread(new Runnable() {
691       @Override
692       public void run() {
693         if (!isError) {
694           isError = true;
695           disconnectWithErrorMessage(description);
696         }
697       }
698     });
699   }
700 
701   private @Nullable VideoCapturer createVideoCapturer() {
702     final VideoCapturer videoCapturer;
703     String videoFileAsCamera = getIntent().getStringExtra(EXTRA_VIDEO_FILE_AS_CAMERA);
704     if (videoFileAsCamera != null) {
705       try {
706         videoCapturer = new FileVideoCapturer(videoFileAsCamera);
707       } catch (IOException e) {
708         reportError("Failed to open video file for emulated camera");
709         return null;
710       }
711     } else if (screencaptureEnabled) {
712       return createScreenCapturer();
713     } else if (useCamera2()) {
714       if (!captureToTexture()) {
715         reportError(getString(R.string.camera2_texture_only_error));
716         return null;
717       }
718 
719       Logging.d(TAG, "Creating capturer using camera2 API.");
720       videoCapturer = createCameraCapturer(new Camera2Enumerator(this));
721     } else {
722       Logging.d(TAG, "Creating capturer using camera1 API.");
723       videoCapturer = createCameraCapturer(new Camera1Enumerator(captureToTexture()));
724     }
725     if (videoCapturer == null) {
726       reportError("Failed to open camera");
727       return null;
728     }
729     return videoCapturer;
730   }
731 
732   private void setSwappedFeeds(boolean isSwappedFeeds) {
733     Logging.d(TAG, "setSwappedFeeds: " + isSwappedFeeds);
734     this.isSwappedFeeds = isSwappedFeeds;
735     localProxyVideoSink.setTarget(isSwappedFeeds ? fullscreenRenderer : pipRenderer);
736     remoteProxyRenderer.setTarget(isSwappedFeeds ? pipRenderer : fullscreenRenderer);
737     fullscreenRenderer.setMirror(isSwappedFeeds);
738     pipRenderer.setMirror(!isSwappedFeeds);
739   }
740 
741   // -----Implementation of AppRTCClient.AppRTCSignalingEvents ---------------
742   // All callbacks are invoked from websocket signaling looper thread and
743   // are routed to UI thread.
744   private void onConnectedToRoomInternal(final SignalingParameters params) {
745     final long delta = System.currentTimeMillis() - callStartedTimeMs;
746 
747     signalingParameters = params;
748     logAndToast("Creating peer connection, delay=" + delta + "ms");
749     VideoCapturer videoCapturer = null;
750     if (peerConnectionParameters.videoCallEnabled) {
751       videoCapturer = createVideoCapturer();
752     }
753     peerConnectionClient.createPeerConnection(
754         localProxyVideoSink, remoteSinks, videoCapturer, signalingParameters);
755 
756     if (signalingParameters.initiator) {
757       logAndToast("Creating OFFER...");
758       // Create offer. Offer SDP will be sent to answering client in
759       // PeerConnectionEvents.onLocalDescription event.
760       peerConnectionClient.createOffer();
761     } else {
762       if (params.offerSdp != null) {
763         peerConnectionClient.setRemoteDescription(params.offerSdp);
764         logAndToast("Creating ANSWER...");
765         // Create answer. Answer SDP will be sent to offering client in
766         // PeerConnectionEvents.onLocalDescription event.
767         peerConnectionClient.createAnswer();
768       }
769       if (params.iceCandidates != null) {
770         // Add remote ICE candidates from room.
771         for (IceCandidate iceCandidate : params.iceCandidates) {
772           peerConnectionClient.addRemoteIceCandidate(iceCandidate);
773         }
774       }
775     }
776   }
777 
778   @Override
779   public void onConnectedToRoom(final SignalingParameters params) {
780     runOnUiThread(new Runnable() {
781       @Override
782       public void run() {
783         onConnectedToRoomInternal(params);
784       }
785     });
786   }
787 
788   @Override
789   public void onRemoteDescription(final SessionDescription desc) {
790     final long delta = System.currentTimeMillis() - callStartedTimeMs;
791     runOnUiThread(new Runnable() {
792       @Override
793       public void run() {
794         if (peerConnectionClient == null) {
795           Log.e(TAG, "Received remote SDP for non-initilized peer connection.");
796           return;
797         }
798         logAndToast("Received remote " + desc.type + ", delay=" + delta + "ms");
799         peerConnectionClient.setRemoteDescription(desc);
800         if (!signalingParameters.initiator) {
801           logAndToast("Creating ANSWER...");
802           // Create answer. Answer SDP will be sent to offering client in
803           // PeerConnectionEvents.onLocalDescription event.
804           peerConnectionClient.createAnswer();
805         }
806       }
807     });
808   }
809 
810   @Override
811   public void onRemoteIceCandidate(final IceCandidate candidate) {
812     runOnUiThread(new Runnable() {
813       @Override
814       public void run() {
815         if (peerConnectionClient == null) {
816           Log.e(TAG, "Received ICE candidate for a non-initialized peer connection.");
817           return;
818         }
819         peerConnectionClient.addRemoteIceCandidate(candidate);
820       }
821     });
822   }
823 
824   @Override
825   public void onRemoteIceCandidatesRemoved(final IceCandidate[] candidates) {
826     runOnUiThread(new Runnable() {
827       @Override
828       public void run() {
829         if (peerConnectionClient == null) {
830           Log.e(TAG, "Received ICE candidate removals for a non-initialized peer connection.");
831           return;
832         }
833         peerConnectionClient.removeRemoteIceCandidates(candidates);
834       }
835     });
836   }
837 
838   @Override
839   public void onChannelClose() {
840     runOnUiThread(new Runnable() {
841       @Override
842       public void run() {
843         logAndToast("Remote end hung up; dropping PeerConnection");
844         disconnect();
845       }
846     });
847   }
848 
849   @Override
850   public void onChannelError(final String description) {
851     reportError(description);
852   }
853 
854   // -----Implementation of PeerConnectionClient.PeerConnectionEvents.---------
855   // Send local peer connection SDP and ICE candidates to remote party.
856   // All callbacks are invoked from peer connection client looper thread and
857   // are routed to UI thread.
858   @Override
859   public void onLocalDescription(final SessionDescription desc) {
860     final long delta = System.currentTimeMillis() - callStartedTimeMs;
861     runOnUiThread(new Runnable() {
862       @Override
863       public void run() {
864         if (appRtcClient != null) {
865           logAndToast("Sending " + desc.type + ", delay=" + delta + "ms");
866           if (signalingParameters.initiator) {
867             appRtcClient.sendOfferSdp(desc);
868           } else {
869             appRtcClient.sendAnswerSdp(desc);
870           }
871         }
872         if (peerConnectionParameters.videoMaxBitrate > 0) {
873           Log.d(TAG, "Set video maximum bitrate: " + peerConnectionParameters.videoMaxBitrate);
874           peerConnectionClient.setVideoMaxBitrate(peerConnectionParameters.videoMaxBitrate);
875         }
876       }
877     });
878   }
879 
880   @Override
881   public void onIceCandidate(final IceCandidate candidate) {
882     runOnUiThread(new Runnable() {
883       @Override
884       public void run() {
885         if (appRtcClient != null) {
886           appRtcClient.sendLocalIceCandidate(candidate);
887         }
888       }
889     });
890   }
891 
892   @Override
893   public void onIceCandidatesRemoved(final IceCandidate[] candidates) {
894     runOnUiThread(new Runnable() {
895       @Override
896       public void run() {
897         if (appRtcClient != null) {
898           appRtcClient.sendLocalIceCandidateRemovals(candidates);
899         }
900       }
901     });
902   }
903 
904   @Override
905   public void onIceConnected() {
906     final long delta = System.currentTimeMillis() - callStartedTimeMs;
907     runOnUiThread(new Runnable() {
908       @Override
909       public void run() {
910         logAndToast("ICE connected, delay=" + delta + "ms");
911       }
912     });
913   }
914 
915   @Override
916   public void onIceDisconnected() {
917     runOnUiThread(new Runnable() {
918       @Override
919       public void run() {
920         logAndToast("ICE disconnected");
921       }
922     });
923   }
924 
925   @Override
926   public void onConnected() {
927     final long delta = System.currentTimeMillis() - callStartedTimeMs;
928     runOnUiThread(new Runnable() {
929       @Override
930       public void run() {
931         logAndToast("DTLS connected, delay=" + delta + "ms");
932         connected = true;
933         callConnected();
934       }
935     });
936   }
937 
938   @Override
939   public void onDisconnected() {
940     runOnUiThread(new Runnable() {
941       @Override
942       public void run() {
943         logAndToast("DTLS disconnected");
944         connected = false;
945         disconnect();
946       }
947     });
948   }
949 
950   @Override
951   public void onPeerConnectionClosed() {}
952 
953   @Override
954   public void onPeerConnectionStatsReady(final StatsReport[] reports) {
955     runOnUiThread(new Runnable() {
956       @Override
957       public void run() {
958         if (!isError && connected) {
959           hudFragment.updateEncoderStatistics(reports);
960         }
961       }
962     });
963   }
964 
965   @Override
966   public void onPeerConnectionError(final String description) {
967     reportError(description);
968   }
969 }
970