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
11package org.appspot.apprtc;
12
13import android.content.Context;
14import android.os.ParcelFileDescriptor;
15import android.os.Environment;
16import android.util.Log;
17
18import org.appspot.apprtc.AppRTCClient.SignalingParameters;
19import org.appspot.apprtc.util.LooperExecutor;
20import org.webrtc.CameraEnumerationAndroid;
21import org.webrtc.DataChannel;
22import org.webrtc.EglBase;
23import org.webrtc.IceCandidate;
24import org.webrtc.Logging;
25import org.webrtc.MediaCodecVideoEncoder;
26import org.webrtc.MediaConstraints;
27import org.webrtc.MediaConstraints.KeyValuePair;
28import org.webrtc.MediaStream;
29import org.webrtc.PeerConnection;
30import org.webrtc.PeerConnection.IceConnectionState;
31import org.webrtc.PeerConnectionFactory;
32import org.webrtc.SdpObserver;
33import org.webrtc.SessionDescription;
34import org.webrtc.StatsObserver;
35import org.webrtc.StatsReport;
36import org.webrtc.VideoCapturerAndroid;
37import org.webrtc.VideoRenderer;
38import org.webrtc.VideoSource;
39import org.webrtc.VideoTrack;
40import org.webrtc.voiceengine.WebRtcAudioManager;
41
42import java.io.File;
43import java.io.IOException;
44import java.util.EnumSet;
45import java.util.LinkedList;
46import java.util.Timer;
47import java.util.TimerTask;
48import java.util.regex.Matcher;
49import 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 */
58public 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
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     */
179    public void onLocalDescription(final SessionDescription sdp);
180
181    /**
182     * Callback fired once local Ice candidate is generated.
183     */
184    public void onIceCandidate(final IceCandidate candidate);
185
186    /**
187     * Callback fired once connection is established (IceConnectionState is
188     * CONNECTED).
189     */
190    public void onIceConnected();
191
192    /**
193     * Callback fired once connection is closed (IceConnectionState is
194     * DISCONNECTED).
195     */
196    public void onIceDisconnected();
197
198    /**
199     * Callback fired once peer connection is closed.
200     */
201    public void onPeerConnectionClosed();
202
203    /**
204     * Callback fired once peer connection statistics is ready.
205     */
206    public void onPeerConnectionStatsReady(final StatsReport[] reports);
207
208    /**
209     * Callback fired once peer connection error happened.
210     */
211    public void onPeerConnectionError(final String description);
212  }
213
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
222  public static PeerConnectionClient getInstance() {
223    return instance;
224  }
225
226  public void setPeerConnectionFactoryOptions(PeerConnectionFactory.Options options) {
227    this.options = options;
228  }
229
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
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
281  public void close() {
282    executor.execute(new Runnable() {
283      @Override
284      public void run() {
285        closeInternal();
286      }
287    });
288  }
289
290  public boolean isVideoCallEnabled() {
291    return videoCallEnabled;
292  }
293
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
888  public void switchCamera() {
889    executor.execute(new Runnable() {
890      @Override
891      public void run() {
892        switchCameraInternal();
893      }
894    });
895  }
896
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
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
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
928    public void onSignalingChange(
929        PeerConnection.SignalingState newState) {
930      Log.d(TAG, "SignalingState: " + newState);
931    }
932
933    @Override
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
952    public void onIceGatheringChange(
953      PeerConnection.IceGatheringState newState) {
954      Log.d(TAG, "IceGatheringState: " + newState);
955    }
956
957    @Override
958    public void onIceConnectionReceivingChange(boolean receiving) {
959      Log.d(TAG, "IceConnectionReceiving changed to " + receiving);
960    }
961
962    @Override
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
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
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
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
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
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
1077    public void onCreateFailure(final String error) {
1078      reportError("createSDP error: " + error);
1079    }
1080
1081    @Override
1082    public void onSetFailure(final String error) {
1083      reportError("setSDP error: " + error);
1084    }
1085  }
1086}
1087