1 /* 2 * Copyright 2014 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.content.Context; 14 import android.os.ParcelFileDescriptor; 15 import android.os.Environment; 16 import android.util.Log; 17 18 import org.appspot.apprtc.AppRTCClient.SignalingParameters; 19 import org.appspot.apprtc.util.LooperExecutor; 20 import org.webrtc.CameraEnumerationAndroid; 21 import org.webrtc.DataChannel; 22 import org.webrtc.EglBase; 23 import org.webrtc.IceCandidate; 24 import org.webrtc.Logging; 25 import org.webrtc.MediaCodecVideoEncoder; 26 import org.webrtc.MediaConstraints; 27 import org.webrtc.MediaConstraints.KeyValuePair; 28 import org.webrtc.MediaStream; 29 import org.webrtc.PeerConnection; 30 import org.webrtc.PeerConnection.IceConnectionState; 31 import org.webrtc.PeerConnectionFactory; 32 import org.webrtc.SdpObserver; 33 import org.webrtc.SessionDescription; 34 import org.webrtc.StatsObserver; 35 import org.webrtc.StatsReport; 36 import org.webrtc.VideoCapturerAndroid; 37 import org.webrtc.VideoRenderer; 38 import org.webrtc.VideoSource; 39 import org.webrtc.VideoTrack; 40 import org.webrtc.voiceengine.WebRtcAudioManager; 41 42 import java.io.File; 43 import java.io.IOException; 44 import java.util.EnumSet; 45 import java.util.LinkedList; 46 import java.util.Timer; 47 import java.util.TimerTask; 48 import java.util.regex.Matcher; 49 import java.util.regex.Pattern; 50 51 /** 52 * Peer connection client implementation. 53 * 54 * <p>All public methods are routed to local looper thread. 55 * All PeerConnectionEvents callbacks are invoked from the same looper thread. 56 * This class is a singleton. 57 */ 58 public class PeerConnectionClient { 59 public static final String VIDEO_TRACK_ID = "ARDAMSv0"; 60 public static final String AUDIO_TRACK_ID = "ARDAMSa0"; 61 private static final String TAG = "PCRTCClient"; 62 private static final String FIELD_TRIAL_AUTOMATIC_RESIZE = 63 "WebRTC-MediaCodecVideoEncoder-AutomaticResize/Enabled/"; 64 private static final String VIDEO_CODEC_VP8 = "VP8"; 65 private static final String VIDEO_CODEC_VP9 = "VP9"; 66 private static final String VIDEO_CODEC_H264 = "H264"; 67 private static final String AUDIO_CODEC_OPUS = "opus"; 68 private static final String AUDIO_CODEC_ISAC = "ISAC"; 69 private static final String VIDEO_CODEC_PARAM_START_BITRATE = 70 "x-google-start-bitrate"; 71 private static final String AUDIO_CODEC_PARAM_BITRATE = "maxaveragebitrate"; 72 private static final String AUDIO_ECHO_CANCELLATION_CONSTRAINT = "googEchoCancellation"; 73 private static final String AUDIO_AUTO_GAIN_CONTROL_CONSTRAINT= "googAutoGainControl"; 74 private static final String AUDIO_HIGH_PASS_FILTER_CONSTRAINT = "googHighpassFilter"; 75 private static final String AUDIO_NOISE_SUPPRESSION_CONSTRAINT = "googNoiseSuppression"; 76 private static final String MAX_VIDEO_WIDTH_CONSTRAINT = "maxWidth"; 77 private static final String MIN_VIDEO_WIDTH_CONSTRAINT = "minWidth"; 78 private static final String MAX_VIDEO_HEIGHT_CONSTRAINT = "maxHeight"; 79 private static final String MIN_VIDEO_HEIGHT_CONSTRAINT = "minHeight"; 80 private static final String MAX_VIDEO_FPS_CONSTRAINT = "maxFrameRate"; 81 private static final String MIN_VIDEO_FPS_CONSTRAINT = "minFrameRate"; 82 private static final String DTLS_SRTP_KEY_AGREEMENT_CONSTRAINT = "DtlsSrtpKeyAgreement"; 83 private static final int HD_VIDEO_WIDTH = 1280; 84 private static final int HD_VIDEO_HEIGHT = 720; 85 private static final int MAX_VIDEO_WIDTH = 1280; 86 private static final int MAX_VIDEO_HEIGHT = 1280; 87 private static final int MAX_VIDEO_FPS = 30; 88 89 private static final PeerConnectionClient instance = new PeerConnectionClient(); 90 private final PCObserver pcObserver = new PCObserver(); 91 private final SDPObserver sdpObserver = new SDPObserver(); 92 private final LooperExecutor executor; 93 94 private PeerConnectionFactory factory; 95 private PeerConnection peerConnection; 96 PeerConnectionFactory.Options options = null; 97 private VideoSource videoSource; 98 private boolean videoCallEnabled; 99 private boolean preferIsac; 100 private String preferredVideoCodec; 101 private boolean videoSourceStopped; 102 private boolean isError; 103 private Timer statsTimer; 104 private VideoRenderer.Callbacks localRender; 105 private VideoRenderer.Callbacks remoteRender; 106 private SignalingParameters signalingParameters; 107 private MediaConstraints pcConstraints; 108 private MediaConstraints videoConstraints; 109 private MediaConstraints audioConstraints; 110 private ParcelFileDescriptor aecDumpFileDescriptor; 111 private MediaConstraints sdpMediaConstraints; 112 private PeerConnectionParameters peerConnectionParameters; 113 // Queued remote ICE candidates are consumed only after both local and 114 // remote descriptions are set. Similarly local ICE candidates are sent to 115 // remote peer after both local and remote description are set. 116 private LinkedList<IceCandidate> queuedRemoteCandidates; 117 private PeerConnectionEvents events; 118 private boolean isInitiator; 119 private SessionDescription localSdp; // either offer or answer SDP 120 private MediaStream mediaStream; 121 private int numberOfCameras; 122 private VideoCapturerAndroid videoCapturer; 123 // enableVideo is set to true if video should be rendered and sent. 124 private boolean renderVideo; 125 private VideoTrack localVideoTrack; 126 private VideoTrack remoteVideoTrack; 127 128 /** 129 * Peer connection parameters. 130 */ 131 public static class PeerConnectionParameters { 132 public final boolean videoCallEnabled; 133 public final boolean loopback; 134 public final boolean tracing; 135 public final int videoWidth; 136 public final int videoHeight; 137 public final int videoFps; 138 public final int videoStartBitrate; 139 public final String videoCodec; 140 public final boolean videoCodecHwAcceleration; 141 public final boolean captureToTexture; 142 public final int audioStartBitrate; 143 public final String audioCodec; 144 public final boolean noAudioProcessing; 145 public final boolean aecDump; 146 public final boolean useOpenSLES; 147 PeerConnectionParameters( boolean videoCallEnabled, boolean loopback, boolean tracing, int videoWidth, int videoHeight, int videoFps, int videoStartBitrate, String videoCodec, boolean videoCodecHwAcceleration, boolean captureToTexture, int audioStartBitrate, String audioCodec, boolean noAudioProcessing, boolean aecDump, boolean useOpenSLES)148 public PeerConnectionParameters( 149 boolean videoCallEnabled, boolean loopback, boolean tracing, 150 int videoWidth, int videoHeight, int videoFps, int videoStartBitrate, 151 String videoCodec, boolean videoCodecHwAcceleration, boolean captureToTexture, 152 int audioStartBitrate, String audioCodec, 153 boolean noAudioProcessing, boolean aecDump, boolean useOpenSLES) { 154 this.videoCallEnabled = videoCallEnabled; 155 this.loopback = loopback; 156 this.tracing = tracing; 157 this.videoWidth = videoWidth; 158 this.videoHeight = videoHeight; 159 this.videoFps = videoFps; 160 this.videoStartBitrate = videoStartBitrate; 161 this.videoCodec = videoCodec; 162 this.videoCodecHwAcceleration = videoCodecHwAcceleration; 163 this.captureToTexture = captureToTexture; 164 this.audioStartBitrate = audioStartBitrate; 165 this.audioCodec = audioCodec; 166 this.noAudioProcessing = noAudioProcessing; 167 this.aecDump = aecDump; 168 this.useOpenSLES = useOpenSLES; 169 } 170 } 171 172 /** 173 * Peer connection events. 174 */ 175 public static interface PeerConnectionEvents { 176 /** 177 * Callback fired once local SDP is created and set. 178 */ onLocalDescription(final SessionDescription sdp)179 public void onLocalDescription(final SessionDescription sdp); 180 181 /** 182 * Callback fired once local Ice candidate is generated. 183 */ onIceCandidate(final IceCandidate candidate)184 public void onIceCandidate(final IceCandidate candidate); 185 186 /** 187 * Callback fired once connection is established (IceConnectionState is 188 * CONNECTED). 189 */ onIceConnected()190 public void onIceConnected(); 191 192 /** 193 * Callback fired once connection is closed (IceConnectionState is 194 * DISCONNECTED). 195 */ onIceDisconnected()196 public void onIceDisconnected(); 197 198 /** 199 * Callback fired once peer connection is closed. 200 */ onPeerConnectionClosed()201 public void onPeerConnectionClosed(); 202 203 /** 204 * Callback fired once peer connection statistics is ready. 205 */ onPeerConnectionStatsReady(final StatsReport[] reports)206 public void onPeerConnectionStatsReady(final StatsReport[] reports); 207 208 /** 209 * Callback fired once peer connection error happened. 210 */ onPeerConnectionError(final String description)211 public void onPeerConnectionError(final String description); 212 } 213 PeerConnectionClient()214 private PeerConnectionClient() { 215 executor = new LooperExecutor(); 216 // Looper thread is started once in private ctor and is used for all 217 // peer connection API calls to ensure new peer connection factory is 218 // created on the same thread as previously destroyed factory. 219 executor.requestStart(); 220 } 221 getInstance()222 public static PeerConnectionClient getInstance() { 223 return instance; 224 } 225 setPeerConnectionFactoryOptions(PeerConnectionFactory.Options options)226 public void setPeerConnectionFactoryOptions(PeerConnectionFactory.Options options) { 227 this.options = options; 228 } 229 createPeerConnectionFactory( final Context context, final PeerConnectionParameters peerConnectionParameters, final PeerConnectionEvents events)230 public void createPeerConnectionFactory( 231 final Context context, 232 final PeerConnectionParameters peerConnectionParameters, 233 final PeerConnectionEvents events) { 234 this.peerConnectionParameters = peerConnectionParameters; 235 this.events = events; 236 videoCallEnabled = peerConnectionParameters.videoCallEnabled; 237 // Reset variables to initial states. 238 factory = null; 239 peerConnection = null; 240 preferIsac = false; 241 videoSourceStopped = false; 242 isError = false; 243 queuedRemoteCandidates = null; 244 localSdp = null; // either offer or answer SDP 245 mediaStream = null; 246 videoCapturer = null; 247 renderVideo = true; 248 localVideoTrack = null; 249 remoteVideoTrack = null; 250 statsTimer = new Timer(); 251 252 executor.execute(new Runnable() { 253 @Override 254 public void run() { 255 createPeerConnectionFactoryInternal(context); 256 } 257 }); 258 } 259 createPeerConnection( final EglBase.Context renderEGLContext, final VideoRenderer.Callbacks localRender, final VideoRenderer.Callbacks remoteRender, final SignalingParameters signalingParameters)260 public void createPeerConnection( 261 final EglBase.Context renderEGLContext, 262 final VideoRenderer.Callbacks localRender, 263 final VideoRenderer.Callbacks remoteRender, 264 final SignalingParameters signalingParameters) { 265 if (peerConnectionParameters == null) { 266 Log.e(TAG, "Creating peer connection without initializing factory."); 267 return; 268 } 269 this.localRender = localRender; 270 this.remoteRender = remoteRender; 271 this.signalingParameters = signalingParameters; 272 executor.execute(new Runnable() { 273 @Override 274 public void run() { 275 createMediaConstraintsInternal(); 276 createPeerConnectionInternal(renderEGLContext); 277 } 278 }); 279 } 280 close()281 public void close() { 282 executor.execute(new Runnable() { 283 @Override 284 public void run() { 285 closeInternal(); 286 } 287 }); 288 } 289 isVideoCallEnabled()290 public boolean isVideoCallEnabled() { 291 return videoCallEnabled; 292 } 293 createPeerConnectionFactoryInternal(Context context)294 private void createPeerConnectionFactoryInternal(Context context) { 295 PeerConnectionFactory.initializeInternalTracer(); 296 if (peerConnectionParameters.tracing) { 297 PeerConnectionFactory.startInternalTracingCapture( 298 Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator 299 + "webrtc-trace.txt"); 300 } 301 Log.d(TAG, "Create peer connection factory. Use video: " + 302 peerConnectionParameters.videoCallEnabled); 303 isError = false; 304 305 // Initialize field trials. 306 PeerConnectionFactory.initializeFieldTrials(FIELD_TRIAL_AUTOMATIC_RESIZE); 307 308 // Check preferred video codec. 309 preferredVideoCodec = VIDEO_CODEC_VP8; 310 if (videoCallEnabled && peerConnectionParameters.videoCodec != null) { 311 if (peerConnectionParameters.videoCodec.equals(VIDEO_CODEC_VP9)) { 312 preferredVideoCodec = VIDEO_CODEC_VP9; 313 } else if (peerConnectionParameters.videoCodec.equals(VIDEO_CODEC_H264)) { 314 preferredVideoCodec = VIDEO_CODEC_H264; 315 } 316 } 317 Log.d(TAG, "Pereferred video codec: " + preferredVideoCodec); 318 319 // Check if ISAC is used by default. 320 preferIsac = false; 321 if (peerConnectionParameters.audioCodec != null 322 && peerConnectionParameters.audioCodec.equals(AUDIO_CODEC_ISAC)) { 323 preferIsac = true; 324 } 325 326 // Enable/disable OpenSL ES playback. 327 if (!peerConnectionParameters.useOpenSLES) { 328 Log.d(TAG, "Disable OpenSL ES audio even if device supports it"); 329 WebRtcAudioManager.setBlacklistDeviceForOpenSLESUsage(true /* enable */); 330 } else { 331 Log.d(TAG, "Allow OpenSL ES audio if device supports it"); 332 WebRtcAudioManager.setBlacklistDeviceForOpenSLESUsage(false); 333 } 334 335 // Create peer connection factory. 336 if (!PeerConnectionFactory.initializeAndroidGlobals(context, true, true, 337 peerConnectionParameters.videoCodecHwAcceleration)) { 338 events.onPeerConnectionError("Failed to initializeAndroidGlobals"); 339 } 340 factory = new PeerConnectionFactory(); 341 if (options != null) { 342 Log.d(TAG, "Factory networkIgnoreMask option: " + options.networkIgnoreMask); 343 factory.setOptions(options); 344 } 345 Log.d(TAG, "Peer connection factory created."); 346 } 347 createMediaConstraintsInternal()348 private void createMediaConstraintsInternal() { 349 // Create peer connection constraints. 350 pcConstraints = new MediaConstraints(); 351 // Enable DTLS for normal calls and disable for loopback calls. 352 if (peerConnectionParameters.loopback) { 353 pcConstraints.optional.add( 354 new MediaConstraints.KeyValuePair(DTLS_SRTP_KEY_AGREEMENT_CONSTRAINT, "false")); 355 } else { 356 pcConstraints.optional.add( 357 new MediaConstraints.KeyValuePair(DTLS_SRTP_KEY_AGREEMENT_CONSTRAINT, "true")); 358 } 359 360 // Check if there is a camera on device and disable video call if not. 361 numberOfCameras = CameraEnumerationAndroid.getDeviceCount(); 362 if (numberOfCameras == 0) { 363 Log.w(TAG, "No camera on device. Switch to audio only call."); 364 videoCallEnabled = false; 365 } 366 // Create video constraints if video call is enabled. 367 if (videoCallEnabled) { 368 videoConstraints = new MediaConstraints(); 369 int videoWidth = peerConnectionParameters.videoWidth; 370 int videoHeight = peerConnectionParameters.videoHeight; 371 372 // If VP8 HW video encoder is supported and video resolution is not 373 // specified force it to HD. 374 if ((videoWidth == 0 || videoHeight == 0) 375 && peerConnectionParameters.videoCodecHwAcceleration 376 && MediaCodecVideoEncoder.isVp8HwSupported()) { 377 videoWidth = HD_VIDEO_WIDTH; 378 videoHeight = HD_VIDEO_HEIGHT; 379 } 380 381 // Add video resolution constraints. 382 if (videoWidth > 0 && videoHeight > 0) { 383 videoWidth = Math.min(videoWidth, MAX_VIDEO_WIDTH); 384 videoHeight = Math.min(videoHeight, MAX_VIDEO_HEIGHT); 385 videoConstraints.mandatory.add(new KeyValuePair( 386 MIN_VIDEO_WIDTH_CONSTRAINT, Integer.toString(videoWidth))); 387 videoConstraints.mandatory.add(new KeyValuePair( 388 MAX_VIDEO_WIDTH_CONSTRAINT, Integer.toString(videoWidth))); 389 videoConstraints.mandatory.add(new KeyValuePair( 390 MIN_VIDEO_HEIGHT_CONSTRAINT, Integer.toString(videoHeight))); 391 videoConstraints.mandatory.add(new KeyValuePair( 392 MAX_VIDEO_HEIGHT_CONSTRAINT, Integer.toString(videoHeight))); 393 } 394 395 // Add fps constraints. 396 int videoFps = peerConnectionParameters.videoFps; 397 if (videoFps > 0) { 398 videoFps = Math.min(videoFps, MAX_VIDEO_FPS); 399 videoConstraints.mandatory.add(new KeyValuePair( 400 MIN_VIDEO_FPS_CONSTRAINT, Integer.toString(videoFps))); 401 videoConstraints.mandatory.add(new KeyValuePair( 402 MAX_VIDEO_FPS_CONSTRAINT, Integer.toString(videoFps))); 403 } 404 } 405 406 // Create audio constraints. 407 audioConstraints = new MediaConstraints(); 408 // added for audio performance measurements 409 if (peerConnectionParameters.noAudioProcessing) { 410 Log.d(TAG, "Disabling audio processing"); 411 audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair( 412 AUDIO_ECHO_CANCELLATION_CONSTRAINT, "false")); 413 audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair( 414 AUDIO_AUTO_GAIN_CONTROL_CONSTRAINT, "false")); 415 audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair( 416 AUDIO_HIGH_PASS_FILTER_CONSTRAINT, "false")); 417 audioConstraints.mandatory.add(new MediaConstraints.KeyValuePair( 418 AUDIO_NOISE_SUPPRESSION_CONSTRAINT , "false")); 419 } 420 // Create SDP constraints. 421 sdpMediaConstraints = new MediaConstraints(); 422 sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair( 423 "OfferToReceiveAudio", "true")); 424 if (videoCallEnabled || peerConnectionParameters.loopback) { 425 sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair( 426 "OfferToReceiveVideo", "true")); 427 } else { 428 sdpMediaConstraints.mandatory.add(new MediaConstraints.KeyValuePair( 429 "OfferToReceiveVideo", "false")); 430 } 431 } 432 createPeerConnectionInternal(EglBase.Context renderEGLContext)433 private void createPeerConnectionInternal(EglBase.Context renderEGLContext) { 434 if (factory == null || isError) { 435 Log.e(TAG, "Peerconnection factory is not created"); 436 return; 437 } 438 Log.d(TAG, "Create peer connection."); 439 440 Log.d(TAG, "PCConstraints: " + pcConstraints.toString()); 441 if (videoConstraints != null) { 442 Log.d(TAG, "VideoConstraints: " + videoConstraints.toString()); 443 } 444 queuedRemoteCandidates = new LinkedList<IceCandidate>(); 445 446 if (videoCallEnabled) { 447 Log.d(TAG, "EGLContext: " + renderEGLContext); 448 factory.setVideoHwAccelerationOptions(renderEGLContext, renderEGLContext); 449 } 450 451 PeerConnection.RTCConfiguration rtcConfig = 452 new PeerConnection.RTCConfiguration(signalingParameters.iceServers); 453 // TCP candidates are only useful when connecting to a server that supports 454 // ICE-TCP. 455 rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED; 456 rtcConfig.bundlePolicy = PeerConnection.BundlePolicy.MAXBUNDLE; 457 rtcConfig.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.REQUIRE; 458 // Use ECDSA encryption. 459 rtcConfig.keyType = PeerConnection.KeyType.ECDSA; 460 461 peerConnection = factory.createPeerConnection( 462 rtcConfig, pcConstraints, pcObserver); 463 isInitiator = false; 464 465 // Set default WebRTC tracing and INFO libjingle logging. 466 // NOTE: this _must_ happen while |factory| is alive! 467 Logging.enableTracing( 468 "logcat:", 469 EnumSet.of(Logging.TraceLevel.TRACE_DEFAULT), 470 Logging.Severity.LS_INFO); 471 472 mediaStream = factory.createLocalMediaStream("ARDAMS"); 473 if (videoCallEnabled) { 474 String cameraDeviceName = CameraEnumerationAndroid.getDeviceName(0); 475 String frontCameraDeviceName = 476 CameraEnumerationAndroid.getNameOfFrontFacingDevice(); 477 if (numberOfCameras > 1 && frontCameraDeviceName != null) { 478 cameraDeviceName = frontCameraDeviceName; 479 } 480 Log.d(TAG, "Opening camera: " + cameraDeviceName); 481 videoCapturer = VideoCapturerAndroid.create(cameraDeviceName, null, 482 peerConnectionParameters.captureToTexture ? renderEGLContext : null); 483 if (videoCapturer == null) { 484 reportError("Failed to open camera"); 485 return; 486 } 487 mediaStream.addTrack(createVideoTrack(videoCapturer)); 488 } 489 490 mediaStream.addTrack(factory.createAudioTrack( 491 AUDIO_TRACK_ID, 492 factory.createAudioSource(audioConstraints))); 493 peerConnection.addStream(mediaStream); 494 495 if (peerConnectionParameters.aecDump) { 496 try { 497 aecDumpFileDescriptor = ParcelFileDescriptor.open( 498 new File("/sdcard/Download/audio.aecdump"), 499 ParcelFileDescriptor.MODE_READ_WRITE | 500 ParcelFileDescriptor.MODE_CREATE | 501 ParcelFileDescriptor.MODE_TRUNCATE); 502 factory.startAecDump(aecDumpFileDescriptor.getFd()); 503 } catch(IOException e) { 504 Log.e(TAG, "Can not open aecdump file", e); 505 } 506 } 507 508 Log.d(TAG, "Peer connection created."); 509 } 510 closeInternal()511 private void closeInternal() { 512 if (factory != null && peerConnectionParameters.aecDump) { 513 factory.stopAecDump(); 514 } 515 Log.d(TAG, "Closing peer connection."); 516 statsTimer.cancel(); 517 if (peerConnection != null) { 518 peerConnection.dispose(); 519 peerConnection = null; 520 } 521 Log.d(TAG, "Closing video source."); 522 if (videoSource != null) { 523 videoSource.dispose(); 524 videoSource = null; 525 } 526 Log.d(TAG, "Closing peer connection factory."); 527 if (factory != null) { 528 factory.dispose(); 529 factory = null; 530 } 531 options = null; 532 Log.d(TAG, "Closing peer connection done."); 533 events.onPeerConnectionClosed(); 534 PeerConnectionFactory.stopInternalTracingCapture(); 535 PeerConnectionFactory.shutdownInternalTracer(); 536 } 537 isHDVideo()538 public boolean isHDVideo() { 539 if (!videoCallEnabled) { 540 return false; 541 } 542 int minWidth = 0; 543 int minHeight = 0; 544 for (KeyValuePair keyValuePair : videoConstraints.mandatory) { 545 if (keyValuePair.getKey().equals("minWidth")) { 546 try { 547 minWidth = Integer.parseInt(keyValuePair.getValue()); 548 } catch (NumberFormatException e) { 549 Log.e(TAG, "Can not parse video width from video constraints"); 550 } 551 } else if (keyValuePair.getKey().equals("minHeight")) { 552 try { 553 minHeight = Integer.parseInt(keyValuePair.getValue()); 554 } catch (NumberFormatException e) { 555 Log.e(TAG, "Can not parse video height from video constraints"); 556 } 557 } 558 } 559 if (minWidth * minHeight >= 1280 * 720) { 560 return true; 561 } else { 562 return false; 563 } 564 } 565 getStats()566 private void getStats() { 567 if (peerConnection == null || isError) { 568 return; 569 } 570 boolean success = peerConnection.getStats(new StatsObserver() { 571 @Override 572 public void onComplete(final StatsReport[] reports) { 573 events.onPeerConnectionStatsReady(reports); 574 } 575 }, null); 576 if (!success) { 577 Log.e(TAG, "getStats() returns false!"); 578 } 579 } 580 enableStatsEvents(boolean enable, int periodMs)581 public void enableStatsEvents(boolean enable, int periodMs) { 582 if (enable) { 583 try { 584 statsTimer.schedule(new TimerTask() { 585 @Override 586 public void run() { 587 executor.execute(new Runnable() { 588 @Override 589 public void run() { 590 getStats(); 591 } 592 }); 593 } 594 }, 0, periodMs); 595 } catch (Exception e) { 596 Log.e(TAG, "Can not schedule statistics timer", e); 597 } 598 } else { 599 statsTimer.cancel(); 600 } 601 } 602 setVideoEnabled(final boolean enable)603 public void setVideoEnabled(final boolean enable) { 604 executor.execute(new Runnable() { 605 @Override 606 public void run() { 607 renderVideo = enable; 608 if (localVideoTrack != null) { 609 localVideoTrack.setEnabled(renderVideo); 610 } 611 if (remoteVideoTrack != null) { 612 remoteVideoTrack.setEnabled(renderVideo); 613 } 614 } 615 }); 616 } 617 createOffer()618 public void createOffer() { 619 executor.execute(new Runnable() { 620 @Override 621 public void run() { 622 if (peerConnection != null && !isError) { 623 Log.d(TAG, "PC Create OFFER"); 624 isInitiator = true; 625 peerConnection.createOffer(sdpObserver, sdpMediaConstraints); 626 } 627 } 628 }); 629 } 630 createAnswer()631 public void createAnswer() { 632 executor.execute(new Runnable() { 633 @Override 634 public void run() { 635 if (peerConnection != null && !isError) { 636 Log.d(TAG, "PC create ANSWER"); 637 isInitiator = false; 638 peerConnection.createAnswer(sdpObserver, sdpMediaConstraints); 639 } 640 } 641 }); 642 } 643 addRemoteIceCandidate(final IceCandidate candidate)644 public void addRemoteIceCandidate(final IceCandidate candidate) { 645 executor.execute(new Runnable() { 646 @Override 647 public void run() { 648 if (peerConnection != null && !isError) { 649 if (queuedRemoteCandidates != null) { 650 queuedRemoteCandidates.add(candidate); 651 } else { 652 peerConnection.addIceCandidate(candidate); 653 } 654 } 655 } 656 }); 657 } 658 setRemoteDescription(final SessionDescription sdp)659 public void setRemoteDescription(final SessionDescription sdp) { 660 executor.execute(new Runnable() { 661 @Override 662 public void run() { 663 if (peerConnection == null || isError) { 664 return; 665 } 666 String sdpDescription = sdp.description; 667 if (preferIsac) { 668 sdpDescription = preferCodec(sdpDescription, AUDIO_CODEC_ISAC, true); 669 } 670 if (videoCallEnabled) { 671 sdpDescription = preferCodec(sdpDescription, preferredVideoCodec, false); 672 } 673 if (videoCallEnabled && peerConnectionParameters.videoStartBitrate > 0) { 674 sdpDescription = setStartBitrate(VIDEO_CODEC_VP8, true, 675 sdpDescription, peerConnectionParameters.videoStartBitrate); 676 sdpDescription = setStartBitrate(VIDEO_CODEC_VP9, true, 677 sdpDescription, peerConnectionParameters.videoStartBitrate); 678 sdpDescription = setStartBitrate(VIDEO_CODEC_H264, true, 679 sdpDescription, peerConnectionParameters.videoStartBitrate); 680 } 681 if (peerConnectionParameters.audioStartBitrate > 0) { 682 sdpDescription = setStartBitrate(AUDIO_CODEC_OPUS, false, 683 sdpDescription, peerConnectionParameters.audioStartBitrate); 684 } 685 Log.d(TAG, "Set remote SDP."); 686 SessionDescription sdpRemote = new SessionDescription( 687 sdp.type, sdpDescription); 688 peerConnection.setRemoteDescription(sdpObserver, sdpRemote); 689 } 690 }); 691 } 692 stopVideoSource()693 public void stopVideoSource() { 694 executor.execute(new Runnable() { 695 @Override 696 public void run() { 697 if (videoSource != null && !videoSourceStopped) { 698 Log.d(TAG, "Stop video source."); 699 videoSource.stop(); 700 videoSourceStopped = true; 701 } 702 } 703 }); 704 } 705 startVideoSource()706 public void startVideoSource() { 707 executor.execute(new Runnable() { 708 @Override 709 public void run() { 710 if (videoSource != null && videoSourceStopped) { 711 Log.d(TAG, "Restart video source."); 712 videoSource.restart(); 713 videoSourceStopped = false; 714 } 715 } 716 }); 717 } 718 reportError(final String errorMessage)719 private void reportError(final String errorMessage) { 720 Log.e(TAG, "Peerconnection error: " + errorMessage); 721 executor.execute(new Runnable() { 722 @Override 723 public void run() { 724 if (!isError) { 725 events.onPeerConnectionError(errorMessage); 726 isError = true; 727 } 728 } 729 }); 730 } 731 createVideoTrack(VideoCapturerAndroid capturer)732 private VideoTrack createVideoTrack(VideoCapturerAndroid capturer) { 733 videoSource = factory.createVideoSource(capturer, videoConstraints); 734 735 localVideoTrack = factory.createVideoTrack(VIDEO_TRACK_ID, videoSource); 736 localVideoTrack.setEnabled(renderVideo); 737 localVideoTrack.addRenderer(new VideoRenderer(localRender)); 738 return localVideoTrack; 739 } 740 setStartBitrate(String codec, boolean isVideoCodec, String sdpDescription, int bitrateKbps)741 private static String setStartBitrate(String codec, boolean isVideoCodec, 742 String sdpDescription, int bitrateKbps) { 743 String[] lines = sdpDescription.split("\r\n"); 744 int rtpmapLineIndex = -1; 745 boolean sdpFormatUpdated = false; 746 String codecRtpMap = null; 747 // Search for codec rtpmap in format 748 // a=rtpmap:<payload type> <encoding name>/<clock rate> [/<encoding parameters>] 749 String regex = "^a=rtpmap:(\\d+) " + codec + "(/\\d+)+[\r]?$"; 750 Pattern codecPattern = Pattern.compile(regex); 751 for (int i = 0; i < lines.length; i++) { 752 Matcher codecMatcher = codecPattern.matcher(lines[i]); 753 if (codecMatcher.matches()) { 754 codecRtpMap = codecMatcher.group(1); 755 rtpmapLineIndex = i; 756 break; 757 } 758 } 759 if (codecRtpMap == null) { 760 Log.w(TAG, "No rtpmap for " + codec + " codec"); 761 return sdpDescription; 762 } 763 Log.d(TAG, "Found " + codec + " rtpmap " + codecRtpMap 764 + " at " + lines[rtpmapLineIndex]); 765 766 // Check if a=fmtp string already exist in remote SDP for this codec and 767 // update it with new bitrate parameter. 768 regex = "^a=fmtp:" + codecRtpMap + " \\w+=\\d+.*[\r]?$"; 769 codecPattern = Pattern.compile(regex); 770 for (int i = 0; i < lines.length; i++) { 771 Matcher codecMatcher = codecPattern.matcher(lines[i]); 772 if (codecMatcher.matches()) { 773 Log.d(TAG, "Found " + codec + " " + lines[i]); 774 if (isVideoCodec) { 775 lines[i] += "; " + VIDEO_CODEC_PARAM_START_BITRATE 776 + "=" + bitrateKbps; 777 } else { 778 lines[i] += "; " + AUDIO_CODEC_PARAM_BITRATE 779 + "=" + (bitrateKbps * 1000); 780 } 781 Log.d(TAG, "Update remote SDP line: " + lines[i]); 782 sdpFormatUpdated = true; 783 break; 784 } 785 } 786 787 StringBuilder newSdpDescription = new StringBuilder(); 788 for (int i = 0; i < lines.length; i++) { 789 newSdpDescription.append(lines[i]).append("\r\n"); 790 // Append new a=fmtp line if no such line exist for a codec. 791 if (!sdpFormatUpdated && i == rtpmapLineIndex) { 792 String bitrateSet; 793 if (isVideoCodec) { 794 bitrateSet = "a=fmtp:" + codecRtpMap + " " 795 + VIDEO_CODEC_PARAM_START_BITRATE + "=" + bitrateKbps; 796 } else { 797 bitrateSet = "a=fmtp:" + codecRtpMap + " " 798 + AUDIO_CODEC_PARAM_BITRATE + "=" + (bitrateKbps * 1000); 799 } 800 Log.d(TAG, "Add remote SDP line: " + bitrateSet); 801 newSdpDescription.append(bitrateSet).append("\r\n"); 802 } 803 804 } 805 return newSdpDescription.toString(); 806 } 807 preferCodec( String sdpDescription, String codec, boolean isAudio)808 private static String preferCodec( 809 String sdpDescription, String codec, boolean isAudio) { 810 String[] lines = sdpDescription.split("\r\n"); 811 int mLineIndex = -1; 812 String codecRtpMap = null; 813 // a=rtpmap:<payload type> <encoding name>/<clock rate> [/<encoding parameters>] 814 String regex = "^a=rtpmap:(\\d+) " + codec + "(/\\d+)+[\r]?$"; 815 Pattern codecPattern = Pattern.compile(regex); 816 String mediaDescription = "m=video "; 817 if (isAudio) { 818 mediaDescription = "m=audio "; 819 } 820 for (int i = 0; (i < lines.length) 821 && (mLineIndex == -1 || codecRtpMap == null); i++) { 822 if (lines[i].startsWith(mediaDescription)) { 823 mLineIndex = i; 824 continue; 825 } 826 Matcher codecMatcher = codecPattern.matcher(lines[i]); 827 if (codecMatcher.matches()) { 828 codecRtpMap = codecMatcher.group(1); 829 continue; 830 } 831 } 832 if (mLineIndex == -1) { 833 Log.w(TAG, "No " + mediaDescription + " line, so can't prefer " + codec); 834 return sdpDescription; 835 } 836 if (codecRtpMap == null) { 837 Log.w(TAG, "No rtpmap for " + codec); 838 return sdpDescription; 839 } 840 Log.d(TAG, "Found " + codec + " rtpmap " + codecRtpMap + ", prefer at " 841 + lines[mLineIndex]); 842 String[] origMLineParts = lines[mLineIndex].split(" "); 843 if (origMLineParts.length > 3) { 844 StringBuilder newMLine = new StringBuilder(); 845 int origPartIndex = 0; 846 // Format is: m=<media> <port> <proto> <fmt> ... 847 newMLine.append(origMLineParts[origPartIndex++]).append(" "); 848 newMLine.append(origMLineParts[origPartIndex++]).append(" "); 849 newMLine.append(origMLineParts[origPartIndex++]).append(" "); 850 newMLine.append(codecRtpMap); 851 for (; origPartIndex < origMLineParts.length; origPartIndex++) { 852 if (!origMLineParts[origPartIndex].equals(codecRtpMap)) { 853 newMLine.append(" ").append(origMLineParts[origPartIndex]); 854 } 855 } 856 lines[mLineIndex] = newMLine.toString(); 857 Log.d(TAG, "Change media description: " + lines[mLineIndex]); 858 } else { 859 Log.e(TAG, "Wrong SDP media description format: " + lines[mLineIndex]); 860 } 861 StringBuilder newSdpDescription = new StringBuilder(); 862 for (String line : lines) { 863 newSdpDescription.append(line).append("\r\n"); 864 } 865 return newSdpDescription.toString(); 866 } 867 drainCandidates()868 private void drainCandidates() { 869 if (queuedRemoteCandidates != null) { 870 Log.d(TAG, "Add " + queuedRemoteCandidates.size() + " remote candidates"); 871 for (IceCandidate candidate : queuedRemoteCandidates) { 872 peerConnection.addIceCandidate(candidate); 873 } 874 queuedRemoteCandidates = null; 875 } 876 } 877 switchCameraInternal()878 private void switchCameraInternal() { 879 if (!videoCallEnabled || numberOfCameras < 2 || isError || videoCapturer == null) { 880 Log.e(TAG, "Failed to switch camera. Video: " + videoCallEnabled + ". Error : " 881 + isError + ". Number of cameras: " + numberOfCameras); 882 return; // No video is sent or only one camera is available or error happened. 883 } 884 Log.d(TAG, "Switch camera"); 885 videoCapturer.switchCamera(null); 886 } 887 switchCamera()888 public void switchCamera() { 889 executor.execute(new Runnable() { 890 @Override 891 public void run() { 892 switchCameraInternal(); 893 } 894 }); 895 } 896 changeCaptureFormat(final int width, final int height, final int framerate)897 public void changeCaptureFormat(final int width, final int height, final int framerate) { 898 executor.execute(new Runnable() { 899 @Override 900 public void run() { 901 changeCaptureFormatInternal(width, height, framerate); 902 } 903 }); 904 } 905 changeCaptureFormatInternal(int width, int height, int framerate)906 private void changeCaptureFormatInternal(int width, int height, int framerate) { 907 if (!videoCallEnabled || isError || videoCapturer == null) { 908 Log.e(TAG, "Failed to change capture format. Video: " + videoCallEnabled + ". Error : " 909 + isError); 910 return; 911 } 912 videoCapturer.onOutputFormatRequest(width, height, framerate); 913 } 914 915 // Implementation detail: observe ICE & stream changes and react accordingly. 916 private class PCObserver implements PeerConnection.Observer { 917 @Override onIceCandidate(final IceCandidate candidate)918 public void onIceCandidate(final IceCandidate candidate){ 919 executor.execute(new Runnable() { 920 @Override 921 public void run() { 922 events.onIceCandidate(candidate); 923 } 924 }); 925 } 926 927 @Override onSignalingChange( PeerConnection.SignalingState newState)928 public void onSignalingChange( 929 PeerConnection.SignalingState newState) { 930 Log.d(TAG, "SignalingState: " + newState); 931 } 932 933 @Override onIceConnectionChange( final PeerConnection.IceConnectionState newState)934 public void onIceConnectionChange( 935 final PeerConnection.IceConnectionState newState) { 936 executor.execute(new Runnable() { 937 @Override 938 public void run() { 939 Log.d(TAG, "IceConnectionState: " + newState); 940 if (newState == IceConnectionState.CONNECTED) { 941 events.onIceConnected(); 942 } else if (newState == IceConnectionState.DISCONNECTED) { 943 events.onIceDisconnected(); 944 } else if (newState == IceConnectionState.FAILED) { 945 reportError("ICE connection failed."); 946 } 947 } 948 }); 949 } 950 951 @Override onIceGatheringChange( PeerConnection.IceGatheringState newState)952 public void onIceGatheringChange( 953 PeerConnection.IceGatheringState newState) { 954 Log.d(TAG, "IceGatheringState: " + newState); 955 } 956 957 @Override onIceConnectionReceivingChange(boolean receiving)958 public void onIceConnectionReceivingChange(boolean receiving) { 959 Log.d(TAG, "IceConnectionReceiving changed to " + receiving); 960 } 961 962 @Override onAddStream(final MediaStream stream)963 public void onAddStream(final MediaStream stream){ 964 executor.execute(new Runnable() { 965 @Override 966 public void run() { 967 if (peerConnection == null || isError) { 968 return; 969 } 970 if (stream.audioTracks.size() > 1 || stream.videoTracks.size() > 1) { 971 reportError("Weird-looking stream: " + stream); 972 return; 973 } 974 if (stream.videoTracks.size() == 1) { 975 remoteVideoTrack = stream.videoTracks.get(0); 976 remoteVideoTrack.setEnabled(renderVideo); 977 remoteVideoTrack.addRenderer(new VideoRenderer(remoteRender)); 978 } 979 } 980 }); 981 } 982 983 @Override onRemoveStream(final MediaStream stream)984 public void onRemoveStream(final MediaStream stream){ 985 executor.execute(new Runnable() { 986 @Override 987 public void run() { 988 remoteVideoTrack = null; 989 } 990 }); 991 } 992 993 @Override onDataChannel(final DataChannel dc)994 public void onDataChannel(final DataChannel dc) { 995 reportError("AppRTC doesn't use data channels, but got: " + dc.label() 996 + " anyway!"); 997 } 998 999 @Override onRenegotiationNeeded()1000 public void onRenegotiationNeeded() { 1001 // No need to do anything; AppRTC follows a pre-agreed-upon 1002 // signaling/negotiation protocol. 1003 } 1004 } 1005 1006 // Implementation detail: handle offer creation/signaling and answer setting, 1007 // as well as adding remote ICE candidates once the answer SDP is set. 1008 private class SDPObserver implements SdpObserver { 1009 @Override onCreateSuccess(final SessionDescription origSdp)1010 public void onCreateSuccess(final SessionDescription origSdp) { 1011 if (localSdp != null) { 1012 reportError("Multiple SDP create."); 1013 return; 1014 } 1015 String sdpDescription = origSdp.description; 1016 if (preferIsac) { 1017 sdpDescription = preferCodec(sdpDescription, AUDIO_CODEC_ISAC, true); 1018 } 1019 if (videoCallEnabled) { 1020 sdpDescription = preferCodec(sdpDescription, preferredVideoCodec, false); 1021 } 1022 final SessionDescription sdp = new SessionDescription( 1023 origSdp.type, sdpDescription); 1024 localSdp = sdp; 1025 executor.execute(new Runnable() { 1026 @Override 1027 public void run() { 1028 if (peerConnection != null && !isError) { 1029 Log.d(TAG, "Set local SDP from " + sdp.type); 1030 peerConnection.setLocalDescription(sdpObserver, sdp); 1031 } 1032 } 1033 }); 1034 } 1035 1036 @Override onSetSuccess()1037 public void onSetSuccess() { 1038 executor.execute(new Runnable() { 1039 @Override 1040 public void run() { 1041 if (peerConnection == null || isError) { 1042 return; 1043 } 1044 if (isInitiator) { 1045 // For offering peer connection we first create offer and set 1046 // local SDP, then after receiving answer set remote SDP. 1047 if (peerConnection.getRemoteDescription() == null) { 1048 // We've just set our local SDP so time to send it. 1049 Log.d(TAG, "Local SDP set succesfully"); 1050 events.onLocalDescription(localSdp); 1051 } else { 1052 // We've just set remote description, so drain remote 1053 // and send local ICE candidates. 1054 Log.d(TAG, "Remote SDP set succesfully"); 1055 drainCandidates(); 1056 } 1057 } else { 1058 // For answering peer connection we set remote SDP and then 1059 // create answer and set local SDP. 1060 if (peerConnection.getLocalDescription() != null) { 1061 // We've just set our local SDP so time to send it, drain 1062 // remote and send local ICE candidates. 1063 Log.d(TAG, "Local SDP set succesfully"); 1064 events.onLocalDescription(localSdp); 1065 drainCandidates(); 1066 } else { 1067 // We've just set remote SDP - do nothing for now - 1068 // answer will be created soon. 1069 Log.d(TAG, "Remote SDP set succesfully"); 1070 } 1071 } 1072 } 1073 }); 1074 } 1075 1076 @Override onCreateFailure(final String error)1077 public void onCreateFailure(final String error) { 1078 reportError("createSDP error: " + error); 1079 } 1080 1081 @Override onSetFailure(final String error)1082 public void onSetFailure(final String error) { 1083 reportError("setSDP error: " + error); 1084 } 1085 } 1086 } 1087