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 sdp) { 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 " + sdp.type + ", delay=" + delta + "ms"); 799 peerConnectionClient.setRemoteDescription(sdp); 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 sdp) { 860 final long delta = System.currentTimeMillis() - callStartedTimeMs; 861 runOnUiThread(new Runnable() { 862 @Override 863 public void run() { 864 if (appRtcClient != null) { 865 logAndToast("Sending " + sdp.type + ", delay=" + delta + "ms"); 866 if (signalingParameters.initiator) { 867 appRtcClient.sendOfferSdp(sdp); 868 } else { 869 appRtcClient.sendAnswerSdp(sdp); 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