xref: /aosp_15_r20/external/webrtc/examples/androidapp/src/org/appspot/apprtc/CallActivity.java (revision d9f758449e529ab9291ac668be2861e7a55c2422)
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.util.DisplayMetrics;
28 import android.util.Log;
29 import android.view.View;
30 import android.view.Window;
31 import android.view.WindowManager;
32 import android.view.WindowManager.LayoutParams;
33 import android.widget.Toast;
34 import androidx.annotation.Nullable;
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.RTCStatsReport;
55 import org.webrtc.RendererCommon.ScalingType;
56 import org.webrtc.ScreenCapturerAndroid;
57 import org.webrtc.SessionDescription;
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 
getDisplayMetrics()387   private DisplayMetrics getDisplayMetrics() {
388     DisplayMetrics displayMetrics = new DisplayMetrics();
389     WindowManager windowManager =
390         (WindowManager) getApplication().getSystemService(Context.WINDOW_SERVICE);
391     windowManager.getDefaultDisplay().getRealMetrics(displayMetrics);
392     return displayMetrics;
393   }
394 
getSystemUiVisibility()395   private static int getSystemUiVisibility() {
396     return View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN
397         | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
398   }
399 
startScreenCapture()400   private void startScreenCapture() {
401     MediaProjectionManager mediaProjectionManager =
402         (MediaProjectionManager) getApplication().getSystemService(
403             Context.MEDIA_PROJECTION_SERVICE);
404     startActivityForResult(
405         mediaProjectionManager.createScreenCaptureIntent(), CAPTURE_PERMISSION_REQUEST_CODE);
406   }
407 
408   @Override
onActivityResult(int requestCode, int resultCode, Intent data)409   public void onActivityResult(int requestCode, int resultCode, Intent data) {
410     if (requestCode != CAPTURE_PERMISSION_REQUEST_CODE)
411       return;
412     mediaProjectionPermissionResultCode = resultCode;
413     mediaProjectionPermissionResultData = data;
414     startCall();
415   }
416 
useCamera2()417   private boolean useCamera2() {
418     return Camera2Enumerator.isSupported(this) && getIntent().getBooleanExtra(EXTRA_CAMERA2, true);
419   }
420 
captureToTexture()421   private boolean captureToTexture() {
422     return getIntent().getBooleanExtra(EXTRA_CAPTURETOTEXTURE_ENABLED, false);
423   }
424 
createCameraCapturer(CameraEnumerator enumerator)425   private @Nullable VideoCapturer createCameraCapturer(CameraEnumerator enumerator) {
426     final String[] deviceNames = enumerator.getDeviceNames();
427 
428     // First, try to find front facing camera
429     Logging.d(TAG, "Looking for front facing cameras.");
430     for (String deviceName : deviceNames) {
431       if (enumerator.isFrontFacing(deviceName)) {
432         Logging.d(TAG, "Creating front facing camera capturer.");
433         VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
434 
435         if (videoCapturer != null) {
436           return videoCapturer;
437         }
438       }
439     }
440 
441     // Front facing camera not found, try something else
442     Logging.d(TAG, "Looking for other cameras.");
443     for (String deviceName : deviceNames) {
444       if (!enumerator.isFrontFacing(deviceName)) {
445         Logging.d(TAG, "Creating other camera capturer.");
446         VideoCapturer videoCapturer = enumerator.createCapturer(deviceName, null);
447 
448         if (videoCapturer != null) {
449           return videoCapturer;
450         }
451       }
452     }
453 
454     return null;
455   }
456 
createScreenCapturer()457   private @Nullable VideoCapturer createScreenCapturer() {
458     if (mediaProjectionPermissionResultCode != Activity.RESULT_OK) {
459       reportError("User didn't give permission to capture the screen.");
460       return null;
461     }
462     return new ScreenCapturerAndroid(
463         mediaProjectionPermissionResultData, new MediaProjection.Callback() {
464       @Override
465       public void onStop() {
466         reportError("User revoked permission to capture the screen.");
467       }
468     });
469   }
470 
471   // Activity interfaces
472   @Override
473   public void onStop() {
474     super.onStop();
475     activityRunning = false;
476     // Don't stop the video when using screencapture to allow user to show other apps to the remote
477     // end.
478     if (peerConnectionClient != null && !screencaptureEnabled) {
479       peerConnectionClient.stopVideoSource();
480     }
481     if (cpuMonitor != null) {
482       cpuMonitor.pause();
483     }
484   }
485 
486   @Override
487   public void onStart() {
488     super.onStart();
489     activityRunning = true;
490     // Video is not paused for screencapture. See onPause.
491     if (peerConnectionClient != null && !screencaptureEnabled) {
492       peerConnectionClient.startVideoSource();
493     }
494     if (cpuMonitor != null) {
495       cpuMonitor.resume();
496     }
497   }
498 
499   @Override
500   protected void onDestroy() {
501     Thread.setDefaultUncaughtExceptionHandler(null);
502     disconnect();
503     if (logToast != null) {
504       logToast.cancel();
505     }
506     activityRunning = false;
507     super.onDestroy();
508   }
509 
510   // CallFragment.OnCallEvents interface implementation.
511   @Override
512   public void onCallHangUp() {
513     disconnect();
514   }
515 
516   @Override
517   public void onCameraSwitch() {
518     if (peerConnectionClient != null) {
519       peerConnectionClient.switchCamera();
520     }
521   }
522 
523   @Override
524   public void onVideoScalingSwitch(ScalingType scalingType) {
525     fullscreenRenderer.setScalingType(scalingType);
526   }
527 
528   @Override
529   public void onCaptureFormatChange(int width, int height, int framerate) {
530     if (peerConnectionClient != null) {
531       peerConnectionClient.changeCaptureFormat(width, height, framerate);
532     }
533   }
534 
535   @Override
536   public boolean onToggleMic() {
537     if (peerConnectionClient != null) {
538       micEnabled = !micEnabled;
539       peerConnectionClient.setAudioEnabled(micEnabled);
540     }
541     return micEnabled;
542   }
543 
544   // Helper functions.
545   private void toggleCallControlFragmentVisibility() {
546     if (!connected || !callFragment.isAdded()) {
547       return;
548     }
549     // Show/hide call control fragment
550     callControlFragmentVisible = !callControlFragmentVisible;
551     FragmentTransaction ft = getFragmentManager().beginTransaction();
552     if (callControlFragmentVisible) {
553       ft.show(callFragment);
554       ft.show(hudFragment);
555     } else {
556       ft.hide(callFragment);
557       ft.hide(hudFragment);
558     }
559     ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE);
560     ft.commit();
561   }
562 
563   private void startCall() {
564     if (appRtcClient == null) {
565       Log.e(TAG, "AppRTC client is not allocated for a call.");
566       return;
567     }
568     callStartedTimeMs = System.currentTimeMillis();
569 
570     // Start room connection.
571     logAndToast(getString(R.string.connecting_to, roomConnectionParameters.roomUrl));
572     appRtcClient.connectToRoom(roomConnectionParameters);
573 
574     // Create and audio manager that will take care of audio routing,
575     // audio modes, audio device enumeration etc.
576     audioManager = AppRTCAudioManager.create(getApplicationContext());
577     // Store existing audio settings and change audio mode to
578     // MODE_IN_COMMUNICATION for best possible VoIP performance.
579     Log.d(TAG, "Starting the audio manager...");
580     audioManager.start(new AudioManagerEvents() {
581       // This method will be called each time the number of available audio
582       // devices has changed.
583       @Override
584       public void onAudioDeviceChanged(
585           AudioDevice audioDevice, Set<AudioDevice> availableAudioDevices) {
586         onAudioManagerDevicesChanged(audioDevice, availableAudioDevices);
587       }
588     });
589   }
590 
591   // Should be called from UI thread
592   private void callConnected() {
593     final long delta = System.currentTimeMillis() - callStartedTimeMs;
594     Log.i(TAG, "Call connected: delay=" + delta + "ms");
595     if (peerConnectionClient == null || isError) {
596       Log.w(TAG, "Call is connected in closed or error state");
597       return;
598     }
599     // Enable statistics callback.
600     peerConnectionClient.enableStatsEvents(true, STAT_CALLBACK_PERIOD);
601     setSwappedFeeds(false /* isSwappedFeeds */);
602   }
603 
604   // This method is called when the audio manager reports audio device change,
605   // e.g. from wired headset to speakerphone.
606   private void onAudioManagerDevicesChanged(
607       final AudioDevice device, final Set<AudioDevice> availableDevices) {
608     Log.d(TAG, "onAudioManagerDevicesChanged: " + availableDevices + ", "
609             + "selected: " + device);
610     // TODO(henrika): add callback handler.
611   }
612 
613   // Disconnect from remote resources, dispose of local resources, and exit.
614   private void disconnect() {
615     activityRunning = false;
616     remoteProxyRenderer.setTarget(null);
617     localProxyVideoSink.setTarget(null);
618     if (appRtcClient != null) {
619       appRtcClient.disconnectFromRoom();
620       appRtcClient = null;
621     }
622     if (pipRenderer != null) {
623       pipRenderer.release();
624       pipRenderer = null;
625     }
626     if (videoFileRenderer != null) {
627       videoFileRenderer.release();
628       videoFileRenderer = null;
629     }
630     if (fullscreenRenderer != null) {
631       fullscreenRenderer.release();
632       fullscreenRenderer = null;
633     }
634     if (peerConnectionClient != null) {
635       peerConnectionClient.close();
636       peerConnectionClient = null;
637     }
638     if (audioManager != null) {
639       audioManager.stop();
640       audioManager = null;
641     }
642     if (connected && !isError) {
643       setResult(RESULT_OK);
644     } else {
645       setResult(RESULT_CANCELED);
646     }
647     finish();
648   }
649 
650   private void disconnectWithErrorMessage(final String errorMessage) {
651     if (commandLineRun || !activityRunning) {
652       Log.e(TAG, "Critical error: " + errorMessage);
653       disconnect();
654     } else {
655       new AlertDialog.Builder(this)
656           .setTitle(getText(R.string.channel_error_title))
657           .setMessage(errorMessage)
658           .setCancelable(false)
659           .setNeutralButton(R.string.ok,
660               new DialogInterface.OnClickListener() {
661                 @Override
662                 public void onClick(DialogInterface dialog, int id) {
663                   dialog.cancel();
664                   disconnect();
665                 }
666               })
667           .create()
668           .show();
669     }
670   }
671 
672   // Log `msg` and Toast about it.
673   private void logAndToast(String msg) {
674     Log.d(TAG, msg);
675     if (logToast != null) {
676       logToast.cancel();
677     }
678     logToast = Toast.makeText(this, msg, Toast.LENGTH_SHORT);
679     logToast.show();
680   }
681 
682   private void reportError(final String description) {
683     runOnUiThread(new Runnable() {
684       @Override
685       public void run() {
686         if (!isError) {
687           isError = true;
688           disconnectWithErrorMessage(description);
689         }
690       }
691     });
692   }
693 
694   private @Nullable VideoCapturer createVideoCapturer() {
695     final VideoCapturer videoCapturer;
696     String videoFileAsCamera = getIntent().getStringExtra(EXTRA_VIDEO_FILE_AS_CAMERA);
697     if (videoFileAsCamera != null) {
698       try {
699         videoCapturer = new FileVideoCapturer(videoFileAsCamera);
700       } catch (IOException e) {
701         reportError("Failed to open video file for emulated camera");
702         return null;
703       }
704     } else if (screencaptureEnabled) {
705       return createScreenCapturer();
706     } else if (useCamera2()) {
707       if (!captureToTexture()) {
708         reportError(getString(R.string.camera2_texture_only_error));
709         return null;
710       }
711 
712       Logging.d(TAG, "Creating capturer using camera2 API.");
713       videoCapturer = createCameraCapturer(new Camera2Enumerator(this));
714     } else {
715       Logging.d(TAG, "Creating capturer using camera1 API.");
716       videoCapturer = createCameraCapturer(new Camera1Enumerator(captureToTexture()));
717     }
718     if (videoCapturer == null) {
719       reportError("Failed to open camera");
720       return null;
721     }
722     return videoCapturer;
723   }
724 
725   private void setSwappedFeeds(boolean isSwappedFeeds) {
726     Logging.d(TAG, "setSwappedFeeds: " + isSwappedFeeds);
727     this.isSwappedFeeds = isSwappedFeeds;
728     localProxyVideoSink.setTarget(isSwappedFeeds ? fullscreenRenderer : pipRenderer);
729     remoteProxyRenderer.setTarget(isSwappedFeeds ? pipRenderer : fullscreenRenderer);
730     fullscreenRenderer.setMirror(isSwappedFeeds);
731     pipRenderer.setMirror(!isSwappedFeeds);
732   }
733 
734   // -----Implementation of AppRTCClient.AppRTCSignalingEvents ---------------
735   // All callbacks are invoked from websocket signaling looper thread and
736   // are routed to UI thread.
737   private void onConnectedToRoomInternal(final SignalingParameters params) {
738     final long delta = System.currentTimeMillis() - callStartedTimeMs;
739 
740     signalingParameters = params;
741     logAndToast("Creating peer connection, delay=" + delta + "ms");
742     VideoCapturer videoCapturer = null;
743     if (peerConnectionParameters.videoCallEnabled) {
744       videoCapturer = createVideoCapturer();
745     }
746     peerConnectionClient.createPeerConnection(
747         localProxyVideoSink, remoteSinks, videoCapturer, signalingParameters);
748 
749     if (signalingParameters.initiator) {
750       logAndToast("Creating OFFER...");
751       // Create offer. Offer SDP will be sent to answering client in
752       // PeerConnectionEvents.onLocalDescription event.
753       peerConnectionClient.createOffer();
754     } else {
755       if (params.offerSdp != null) {
756         peerConnectionClient.setRemoteDescription(params.offerSdp);
757         logAndToast("Creating ANSWER...");
758         // Create answer. Answer SDP will be sent to offering client in
759         // PeerConnectionEvents.onLocalDescription event.
760         peerConnectionClient.createAnswer();
761       }
762       if (params.iceCandidates != null) {
763         // Add remote ICE candidates from room.
764         for (IceCandidate iceCandidate : params.iceCandidates) {
765           peerConnectionClient.addRemoteIceCandidate(iceCandidate);
766         }
767       }
768     }
769   }
770 
771   @Override
772   public void onConnectedToRoom(final SignalingParameters params) {
773     runOnUiThread(new Runnable() {
774       @Override
775       public void run() {
776         onConnectedToRoomInternal(params);
777       }
778     });
779   }
780 
781   @Override
782   public void onRemoteDescription(final SessionDescription desc) {
783     final long delta = System.currentTimeMillis() - callStartedTimeMs;
784     runOnUiThread(new Runnable() {
785       @Override
786       public void run() {
787         if (peerConnectionClient == null) {
788           Log.e(TAG, "Received remote SDP for non-initilized peer connection.");
789           return;
790         }
791         logAndToast("Received remote " + desc.type + ", delay=" + delta + "ms");
792         peerConnectionClient.setRemoteDescription(desc);
793         if (!signalingParameters.initiator) {
794           logAndToast("Creating ANSWER...");
795           // Create answer. Answer SDP will be sent to offering client in
796           // PeerConnectionEvents.onLocalDescription event.
797           peerConnectionClient.createAnswer();
798         }
799       }
800     });
801   }
802 
803   @Override
804   public void onRemoteIceCandidate(final IceCandidate candidate) {
805     runOnUiThread(new Runnable() {
806       @Override
807       public void run() {
808         if (peerConnectionClient == null) {
809           Log.e(TAG, "Received ICE candidate for a non-initialized peer connection.");
810           return;
811         }
812         peerConnectionClient.addRemoteIceCandidate(candidate);
813       }
814     });
815   }
816 
817   @Override
818   public void onRemoteIceCandidatesRemoved(final IceCandidate[] candidates) {
819     runOnUiThread(new Runnable() {
820       @Override
821       public void run() {
822         if (peerConnectionClient == null) {
823           Log.e(TAG, "Received ICE candidate removals for a non-initialized peer connection.");
824           return;
825         }
826         peerConnectionClient.removeRemoteIceCandidates(candidates);
827       }
828     });
829   }
830 
831   @Override
832   public void onChannelClose() {
833     runOnUiThread(new Runnable() {
834       @Override
835       public void run() {
836         logAndToast("Remote end hung up; dropping PeerConnection");
837         disconnect();
838       }
839     });
840   }
841 
842   @Override
843   public void onChannelError(final String description) {
844     reportError(description);
845   }
846 
847   // -----Implementation of PeerConnectionClient.PeerConnectionEvents.---------
848   // Send local peer connection SDP and ICE candidates to remote party.
849   // All callbacks are invoked from peer connection client looper thread and
850   // are routed to UI thread.
851   @Override
852   public void onLocalDescription(final SessionDescription desc) {
853     final long delta = System.currentTimeMillis() - callStartedTimeMs;
854     runOnUiThread(new Runnable() {
855       @Override
856       public void run() {
857         if (appRtcClient != null) {
858           logAndToast("Sending " + desc.type + ", delay=" + delta + "ms");
859           if (signalingParameters.initiator) {
860             appRtcClient.sendOfferSdp(desc);
861           } else {
862             appRtcClient.sendAnswerSdp(desc);
863           }
864         }
865         if (peerConnectionParameters.videoMaxBitrate > 0) {
866           Log.d(TAG, "Set video maximum bitrate: " + peerConnectionParameters.videoMaxBitrate);
867           peerConnectionClient.setVideoMaxBitrate(peerConnectionParameters.videoMaxBitrate);
868         }
869       }
870     });
871   }
872 
873   @Override
874   public void onIceCandidate(final IceCandidate candidate) {
875     runOnUiThread(new Runnable() {
876       @Override
877       public void run() {
878         if (appRtcClient != null) {
879           appRtcClient.sendLocalIceCandidate(candidate);
880         }
881       }
882     });
883   }
884 
885   @Override
886   public void onIceCandidatesRemoved(final IceCandidate[] candidates) {
887     runOnUiThread(new Runnable() {
888       @Override
889       public void run() {
890         if (appRtcClient != null) {
891           appRtcClient.sendLocalIceCandidateRemovals(candidates);
892         }
893       }
894     });
895   }
896 
897   @Override
898   public void onIceConnected() {
899     final long delta = System.currentTimeMillis() - callStartedTimeMs;
900     runOnUiThread(new Runnable() {
901       @Override
902       public void run() {
903         logAndToast("ICE connected, delay=" + delta + "ms");
904       }
905     });
906   }
907 
908   @Override
909   public void onIceDisconnected() {
910     runOnUiThread(new Runnable() {
911       @Override
912       public void run() {
913         logAndToast("ICE disconnected");
914       }
915     });
916   }
917 
918   @Override
919   public void onConnected() {
920     final long delta = System.currentTimeMillis() - callStartedTimeMs;
921     runOnUiThread(new Runnable() {
922       @Override
923       public void run() {
924         logAndToast("DTLS connected, delay=" + delta + "ms");
925         connected = true;
926         callConnected();
927       }
928     });
929   }
930 
931   @Override
932   public void onDisconnected() {
933     runOnUiThread(new Runnable() {
934       @Override
935       public void run() {
936         logAndToast("DTLS disconnected");
937         connected = false;
938         disconnect();
939       }
940     });
941   }
942 
943   @Override
944   public void onPeerConnectionClosed() {}
945 
946   @Override
947   public void onPeerConnectionStatsReady(final RTCStatsReport report) {
948     runOnUiThread(new Runnable() {
949       @Override
950       public void run() {
951         if (!isError && connected) {
952           hudFragment.updateEncoderStatistics(report);
953         }
954       }
955     });
956   }
957 
958   @Override
959   public void onPeerConnectionError(final String description) {
960     reportError(description);
961   }
962 }
963