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