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