WebRTCWrapper.java

  1package eu.siacs.conversations.xmpp.jingle;
  2
  3import android.content.Context;
  4import android.media.ToneGenerator;
  5import android.os.Build;
  6import android.os.Handler;
  7import android.os.Looper;
  8import android.util.Log;
  9
 10import com.google.common.base.Optional;
 11import com.google.common.base.Preconditions;
 12import com.google.common.collect.ImmutableMap;
 13import com.google.common.collect.ImmutableSet;
 14import com.google.common.collect.Iterables;
 15import com.google.common.util.concurrent.Futures;
 16import com.google.common.util.concurrent.ListenableFuture;
 17import com.google.common.util.concurrent.MoreExecutors;
 18import com.google.common.util.concurrent.SettableFuture;
 19
 20import org.webrtc.AudioSource;
 21import org.webrtc.AudioTrack;
 22import org.webrtc.Camera1Enumerator;
 23import org.webrtc.Camera2Enumerator;
 24import org.webrtc.CameraEnumerationAndroid;
 25import org.webrtc.CameraEnumerator;
 26import org.webrtc.CameraVideoCapturer;
 27import org.webrtc.CandidatePairChangeEvent;
 28import org.webrtc.DataChannel;
 29import org.webrtc.DefaultVideoDecoderFactory;
 30import org.webrtc.DefaultVideoEncoderFactory;
 31import org.webrtc.DtmfSender;
 32import org.webrtc.EglBase;
 33import org.webrtc.IceCandidate;
 34import org.webrtc.MediaConstraints;
 35import org.webrtc.MediaStream;
 36import org.webrtc.MediaStreamTrack;
 37import org.webrtc.PeerConnection;
 38import org.webrtc.PeerConnectionFactory;
 39import org.webrtc.RtpReceiver;
 40import org.webrtc.RtpTransceiver;
 41import org.webrtc.SdpObserver;
 42import org.webrtc.SessionDescription;
 43import org.webrtc.SurfaceTextureHelper;
 44import org.webrtc.VideoSource;
 45import org.webrtc.VideoTrack;
 46import org.webrtc.audio.JavaAudioDeviceModule;
 47import org.webrtc.voiceengine.WebRtcAudioEffects;
 48
 49import java.util.ArrayList;
 50import java.util.Collections;
 51import java.util.List;
 52import java.util.Map;
 53import java.util.Set;
 54
 55import javax.annotation.Nonnull;
 56import javax.annotation.Nullable;
 57
 58import eu.siacs.conversations.Config;
 59import eu.siacs.conversations.services.AppRTCAudioManager;
 60import eu.siacs.conversations.services.XmppConnectionService;
 61
 62public class WebRTCWrapper {
 63
 64    private static final String EXTENDED_LOGGING_TAG = WebRTCWrapper.class.getSimpleName();
 65
 66    //we should probably keep this in sync with: https://github.com/signalapp/Signal-Android/blob/master/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java#L296
 67    private static final Set<String> HARDWARE_AEC_BLACKLIST = new ImmutableSet.Builder<String>()
 68            .add("Pixel")
 69            .add("Pixel XL")
 70            .add("Moto G5")
 71            .add("Moto G (5S) Plus")
 72            .add("Moto G4")
 73            .add("TA-1053")
 74            .add("Mi A1")
 75            .add("Mi A2")
 76            .add("E5823") // Sony z5 compact
 77            .add("Redmi Note 5")
 78            .add("FP2") // Fairphone FP2
 79            .add("MI 5")
 80            .build();
 81
 82    private static final int TONE_DURATION = 200;
 83    private static final Map<String,Integer> TONE_CODES;
 84    static {
 85        ImmutableMap.Builder<String,Integer> builder = new ImmutableMap.Builder<>();
 86        builder.put("0", ToneGenerator.TONE_DTMF_0);
 87        builder.put("1", ToneGenerator.TONE_DTMF_1);
 88        builder.put("2", ToneGenerator.TONE_DTMF_2);
 89        builder.put("3", ToneGenerator.TONE_DTMF_3);
 90        builder.put("4", ToneGenerator.TONE_DTMF_4);
 91        builder.put("5", ToneGenerator.TONE_DTMF_5);
 92        builder.put("6", ToneGenerator.TONE_DTMF_6);
 93        builder.put("7", ToneGenerator.TONE_DTMF_7);
 94        builder.put("8", ToneGenerator.TONE_DTMF_8);
 95        builder.put("9", ToneGenerator.TONE_DTMF_9);
 96        builder.put("*", ToneGenerator.TONE_DTMF_S);
 97        builder.put("#", ToneGenerator.TONE_DTMF_P);
 98        TONE_CODES = builder.build();
 99    }
100
101    private static final int CAPTURING_RESOLUTION = 1920;
102    private static final int CAPTURING_MAX_FRAME_RATE = 30;
103
104    private final EventCallback eventCallback;
105    private final AppRTCAudioManager.AudioManagerEvents audioManagerEvents = new AppRTCAudioManager.AudioManagerEvents() {
106        @Override
107        public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
108            eventCallback.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices);
109        }
110    };
111    private final Handler mainHandler = new Handler(Looper.getMainLooper());
112    private VideoTrack localVideoTrack = null;
113    private VideoTrack remoteVideoTrack = null;
114    private final PeerConnection.Observer peerConnectionObserver = new PeerConnection.Observer() {
115        @Override
116        public void onSignalingChange(PeerConnection.SignalingState signalingState) {
117            Log.d(EXTENDED_LOGGING_TAG, "onSignalingChange(" + signalingState + ")");
118            //this is called after removeTrack or addTrack
119            //and should then trigger a content-add or content-remove or something
120            //https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/removeTrack
121        }
122
123        @Override
124        public void onConnectionChange(PeerConnection.PeerConnectionState newState) {
125            eventCallback.onConnectionChange(newState);
126        }
127
128        @Override
129        public void onIceConnectionChange(PeerConnection.IceConnectionState iceConnectionState) {
130
131        }
132
133        @Override
134        public void onSelectedCandidatePairChanged(CandidatePairChangeEvent event) {
135            Log.d(Config.LOGTAG, "remote candidate selected: " + event.remote);
136            Log.d(Config.LOGTAG, "local candidate selected: " + event.local);
137        }
138
139        @Override
140        public void onIceConnectionReceivingChange(boolean b) {
141
142        }
143
144        @Override
145        public void onIceGatheringChange(PeerConnection.IceGatheringState iceGatheringState) {
146            Log.d(EXTENDED_LOGGING_TAG, "onIceGatheringChange(" + iceGatheringState + ")");
147        }
148
149        @Override
150        public void onIceCandidate(IceCandidate iceCandidate) {
151            eventCallback.onIceCandidate(iceCandidate);
152        }
153
154        @Override
155        public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {
156
157        }
158
159        @Override
160        public void onAddStream(MediaStream mediaStream) {
161            Log.d(EXTENDED_LOGGING_TAG, "onAddStream(numAudioTracks=" + mediaStream.audioTracks.size() + ",numVideoTracks=" + mediaStream.videoTracks.size() + ")");
162        }
163
164        @Override
165        public void onRemoveStream(MediaStream mediaStream) {
166
167        }
168
169        @Override
170        public void onDataChannel(DataChannel dataChannel) {
171
172        }
173
174        @Override
175        public void onRenegotiationNeeded() {
176
177        }
178
179        @Override
180        public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
181            final MediaStreamTrack track = rtpReceiver.track();
182            Log.d(EXTENDED_LOGGING_TAG, "onAddTrack(kind=" + (track == null ? "null" : track.kind()) + ",numMediaStreams=" + mediaStreams.length + ")");
183            if (track instanceof VideoTrack) {
184                remoteVideoTrack = (VideoTrack) track;
185            }
186        }
187
188        @Override
189        public void onTrack(RtpTransceiver transceiver) {
190            Log.d(EXTENDED_LOGGING_TAG, "onTrack(mid=" + transceiver.getMid() + ",media=" + transceiver.getMediaType() + ")");
191        }
192    };
193    @Nullable
194    private PeerConnection peerConnection = null;
195    private AudioTrack localAudioTrack = null;
196    private AppRTCAudioManager appRTCAudioManager = null;
197    private ToneManager toneManager = null;
198    private Context context = null;
199    private EglBase eglBase = null;
200    private CapturerChoice capturerChoice;
201
202    WebRTCWrapper(final EventCallback eventCallback) {
203        this.eventCallback = eventCallback;
204    }
205
206    private static void dispose(final PeerConnection peerConnection) {
207        try {
208            peerConnection.dispose();
209        } catch (final IllegalStateException e) {
210            Log.e(Config.LOGTAG, "unable to dispose of peer connection", e);
211        }
212    }
213
214    @Nullable
215    private static CapturerChoice of(CameraEnumerator enumerator, final String deviceName, Set<String> availableCameras) {
216        final CameraVideoCapturer capturer = enumerator.createCapturer(deviceName, null);
217        if (capturer == null) {
218            return null;
219        }
220        final ArrayList<CameraEnumerationAndroid.CaptureFormat> choices = new ArrayList<>(enumerator.getSupportedFormats(deviceName));
221        Collections.sort(choices, (a, b) -> b.width - a.width);
222        for (final CameraEnumerationAndroid.CaptureFormat captureFormat : choices) {
223            if (captureFormat.width <= CAPTURING_RESOLUTION) {
224                return new CapturerChoice(capturer, captureFormat, availableCameras);
225            }
226        }
227        return null;
228    }
229
230    private static boolean isFrontFacing(final CameraEnumerator cameraEnumerator, final String deviceName) {
231        try {
232            return cameraEnumerator.isFrontFacing(deviceName);
233        } catch (final NullPointerException e) {
234            return false;
235        }
236    }
237
238    public void setup(final XmppConnectionService service, final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference) throws InitializationException {
239        try {
240            PeerConnectionFactory.initialize(
241                    PeerConnectionFactory.InitializationOptions.builder(service).createInitializationOptions()
242            );
243        } catch (final UnsatisfiedLinkError e) {
244            throw new InitializationException("Unable to initialize PeerConnectionFactory", e);
245        }
246        try {
247            this.eglBase = EglBase.create();
248        } catch (final RuntimeException e) {
249            throw new InitializationException("Unable to create EGL base", e);
250        }
251        this.context = service;
252        this.toneManager = service.getJingleConnectionManager().toneManager;
253        mainHandler.post(() -> {
254            appRTCAudioManager = AppRTCAudioManager.create(service, speakerPhonePreference);
255            toneManager.setAppRtcAudioManagerHasControl(true);
256            appRTCAudioManager.start(audioManagerEvents);
257            eventCallback.onAudioDeviceChanged(appRTCAudioManager.getSelectedAudioDevice(), appRTCAudioManager.getAudioDevices());
258        });
259    }
260
261    synchronized void initializePeerConnection(final Set<Media> media, final List<PeerConnection.IceServer> iceServers) throws InitializationException {
262        Preconditions.checkState(this.eglBase != null);
263        Preconditions.checkNotNull(media);
264        Preconditions.checkArgument(media.size() > 0, "media can not be empty when initializing peer connection");
265        final boolean setUseHardwareAcousticEchoCanceler = WebRtcAudioEffects.canUseAcousticEchoCanceler() && !HARDWARE_AEC_BLACKLIST.contains(Build.MODEL);
266        Log.d(Config.LOGTAG, String.format("setUseHardwareAcousticEchoCanceler(%s) model=%s", setUseHardwareAcousticEchoCanceler, Build.MODEL));
267        PeerConnectionFactory peerConnectionFactory = PeerConnectionFactory.builder()
268                .setVideoDecoderFactory(new DefaultVideoDecoderFactory(eglBase.getEglBaseContext()))
269                .setVideoEncoderFactory(new DefaultVideoEncoderFactory(eglBase.getEglBaseContext(), true, true))
270                .setAudioDeviceModule(JavaAudioDeviceModule.builder(context)
271                        .setUseHardwareAcousticEchoCanceler(setUseHardwareAcousticEchoCanceler)
272                        .createAudioDeviceModule()
273                )
274                .createPeerConnectionFactory();
275
276
277        final PeerConnection.RTCConfiguration rtcConfig = new PeerConnection.RTCConfiguration(iceServers);
278        rtcConfig.tcpCandidatePolicy = PeerConnection.TcpCandidatePolicy.DISABLED; //XEP-0176 doesn't support tcp
279        rtcConfig.continualGatheringPolicy = PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY;
280        rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
281        final PeerConnection peerConnection = peerConnectionFactory.createPeerConnection(rtcConfig, peerConnectionObserver);
282        if (peerConnection == null) {
283            throw new InitializationException("Unable to create PeerConnection");
284        }
285
286        final Optional<CapturerChoice> optionalCapturerChoice = media.contains(Media.VIDEO) ? getVideoCapturer() : Optional.absent();
287
288        if (optionalCapturerChoice.isPresent()) {
289            this.capturerChoice = optionalCapturerChoice.get();
290            final CameraVideoCapturer capturer = this.capturerChoice.cameraVideoCapturer;
291            final VideoSource videoSource = peerConnectionFactory.createVideoSource(false);
292            SurfaceTextureHelper surfaceTextureHelper = SurfaceTextureHelper.create("webrtc", eglBase.getEglBaseContext());
293            capturer.initialize(surfaceTextureHelper, requireContext(), videoSource.getCapturerObserver());
294            Log.d(Config.LOGTAG, String.format("start capturing at %dx%d@%d", capturerChoice.captureFormat.width, capturerChoice.captureFormat.height, capturerChoice.getFrameRate()));
295            capturer.startCapture(capturerChoice.captureFormat.width, capturerChoice.captureFormat.height, capturerChoice.getFrameRate());
296
297            this.localVideoTrack = peerConnectionFactory.createVideoTrack("my-video-track", videoSource);
298
299            peerConnection.addTrack(this.localVideoTrack);
300        }
301
302
303        if (media.contains(Media.AUDIO)) {
304            //set up audio track
305            final AudioSource audioSource = peerConnectionFactory.createAudioSource(new MediaConstraints());
306            this.localAudioTrack = peerConnectionFactory.createAudioTrack("my-audio-track", audioSource);
307            peerConnection.addTrack(this.localAudioTrack);
308        }
309        peerConnection.setAudioPlayout(true);
310        peerConnection.setAudioRecording(true);
311        this.peerConnection = peerConnection;
312    }
313
314    synchronized void close() {
315        final PeerConnection peerConnection = this.peerConnection;
316        final CapturerChoice capturerChoice = this.capturerChoice;
317        final AppRTCAudioManager audioManager = this.appRTCAudioManager;
318        final EglBase eglBase = this.eglBase;
319        if (peerConnection != null) {
320            dispose(peerConnection);
321            this.peerConnection = null;
322        }
323        if (audioManager != null) {
324            toneManager.setAppRtcAudioManagerHasControl(false);
325            mainHandler.post(audioManager::stop);
326        }
327        this.localVideoTrack = null;
328        this.remoteVideoTrack = null;
329        if (capturerChoice != null) {
330            try {
331                capturerChoice.cameraVideoCapturer.stopCapture();
332            } catch (InterruptedException e) {
333                Log.e(Config.LOGTAG, "unable to stop capturing");
334            }
335        }
336        if (eglBase != null) {
337            eglBase.release();
338            this.eglBase = null;
339        }
340    }
341
342    synchronized void verifyClosed() {
343        if (this.peerConnection != null
344                || this.eglBase != null
345                || this.localVideoTrack != null
346                || this.remoteVideoTrack != null) {
347            final IllegalStateException e = new IllegalStateException("WebRTCWrapper hasn't been closed properly");
348            Log.e(Config.LOGTAG, "verifyClosed() failed. Going to throw", e);
349            throw e;
350        }
351    }
352
353    boolean isCameraSwitchable() {
354        final CapturerChoice capturerChoice = this.capturerChoice;
355        return capturerChoice != null && capturerChoice.availableCameras.size() > 1;
356    }
357
358    boolean isFrontCamera() {
359        final CapturerChoice capturerChoice = this.capturerChoice;
360        return capturerChoice == null || capturerChoice.isFrontCamera;
361    }
362
363    ListenableFuture<Boolean> switchCamera() {
364        final CapturerChoice capturerChoice = this.capturerChoice;
365        if (capturerChoice == null) {
366            return Futures.immediateFailedFuture(new IllegalStateException("CameraCapturer has not been initialized"));
367        }
368        final SettableFuture<Boolean> future = SettableFuture.create();
369        capturerChoice.cameraVideoCapturer.switchCamera(new CameraVideoCapturer.CameraSwitchHandler() {
370            @Override
371            public void onCameraSwitchDone(boolean isFrontCamera) {
372                capturerChoice.isFrontCamera = isFrontCamera;
373                future.set(isFrontCamera);
374            }
375
376            @Override
377            public void onCameraSwitchError(final String message) {
378                future.setException(new IllegalStateException(String.format("Unable to switch camera %s", message)));
379            }
380        });
381        return future;
382    }
383
384    boolean isMicrophoneEnabled() {
385        final AudioTrack audioTrack = this.localAudioTrack;
386        if (audioTrack == null) {
387            throw new IllegalStateException("Local audio track does not exist (yet)");
388        }
389        try {
390            return audioTrack.enabled();
391        } catch (final IllegalStateException e) {
392            //sometimes UI might still be rendering the buttons when a background thread has already ended the call
393            return false;
394        }
395    }
396
397    boolean setMicrophoneEnabled(final boolean enabled) {
398        final AudioTrack audioTrack = this.localAudioTrack;
399        if (audioTrack == null) {
400            throw new IllegalStateException("Local audio track does not exist (yet)");
401        }
402        try {
403            audioTrack.setEnabled(enabled);
404            return true;
405        } catch (final IllegalStateException e) {
406            Log.d(Config.LOGTAG, "unable to toggle microphone", e);
407            //ignoring race condition in case MediaStreamTrack has been disposed
408            return false;
409        }
410    }
411
412    boolean isVideoEnabled() {
413        final VideoTrack videoTrack = this.localVideoTrack;
414        if (videoTrack == null) {
415            return false;
416        }
417        return videoTrack.enabled();
418    }
419
420    void setVideoEnabled(final boolean enabled) {
421        final VideoTrack videoTrack = this.localVideoTrack;
422        if (videoTrack == null) {
423            throw new IllegalStateException("Local video track does not exist");
424        }
425        videoTrack.setEnabled(enabled);
426    }
427
428    ListenableFuture<SessionDescription> createOffer() {
429        return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> {
430            final SettableFuture<SessionDescription> future = SettableFuture.create();
431            peerConnection.createOffer(new CreateSdpObserver() {
432                @Override
433                public void onCreateSuccess(SessionDescription sessionDescription) {
434                    future.set(sessionDescription);
435                }
436
437                @Override
438                public void onCreateFailure(String s) {
439                    future.setException(new IllegalStateException("Unable to create offer: " + s));
440                }
441            }, new MediaConstraints());
442            return future;
443        }, MoreExecutors.directExecutor());
444    }
445
446    ListenableFuture<SessionDescription> createAnswer() {
447        return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> {
448            final SettableFuture<SessionDescription> future = SettableFuture.create();
449            peerConnection.createAnswer(new CreateSdpObserver() {
450                @Override
451                public void onCreateSuccess(SessionDescription sessionDescription) {
452                    future.set(sessionDescription);
453                }
454
455                @Override
456                public void onCreateFailure(String s) {
457                    future.setException(new IllegalStateException("Unable to create answer: " + s));
458                }
459            }, new MediaConstraints());
460            return future;
461        }, MoreExecutors.directExecutor());
462    }
463
464    ListenableFuture<Void> setLocalDescription(final SessionDescription sessionDescription) {
465        Log.d(EXTENDED_LOGGING_TAG, "setting local description:");
466        for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) {
467            Log.d(EXTENDED_LOGGING_TAG, line);
468        }
469        return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> {
470            final SettableFuture<Void> future = SettableFuture.create();
471            peerConnection.setLocalDescription(new SetSdpObserver() {
472                @Override
473                public void onSetSuccess() {
474                    future.set(null);
475                }
476
477                @Override
478                public void onSetFailure(final String s) {
479                    future.setException(new IllegalArgumentException("unable to set local session description: " + s));
480
481                }
482            }, sessionDescription);
483            return future;
484        }, MoreExecutors.directExecutor());
485    }
486
487    ListenableFuture<Void> setRemoteDescription(final SessionDescription sessionDescription) {
488        Log.d(EXTENDED_LOGGING_TAG, "setting remote description:");
489        for (final String line : sessionDescription.description.split(eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) {
490            Log.d(EXTENDED_LOGGING_TAG, line);
491        }
492        return Futures.transformAsync(getPeerConnectionFuture(), peerConnection -> {
493            final SettableFuture<Void> future = SettableFuture.create();
494            peerConnection.setRemoteDescription(new SetSdpObserver() {
495                @Override
496                public void onSetSuccess() {
497                    future.set(null);
498                }
499
500                @Override
501                public void onSetFailure(String s) {
502                    future.setException(new IllegalArgumentException("unable to set remote session description: " + s));
503
504                }
505            }, sessionDescription);
506            return future;
507        }, MoreExecutors.directExecutor());
508    }
509
510    @Nonnull
511    private ListenableFuture<PeerConnection> getPeerConnectionFuture() {
512        final PeerConnection peerConnection = this.peerConnection;
513        if (peerConnection == null) {
514            return Futures.immediateFailedFuture(new IllegalStateException("initialize PeerConnection first"));
515        } else {
516            return Futures.immediateFuture(peerConnection);
517        }
518    }
519
520    //TODO: remove - hack to test dtmfSending
521    public DtmfSender getDtmfSender() {
522        return peerConnection.getSenders().get(0).dtmf();
523    }
524
525    public boolean applyDtmfTone(String tone) {
526        if (toneManager == null || peerConnection.getSenders().isEmpty()) {
527            return false;
528        }
529        peerConnection.getSenders().get(0).dtmf().insertDtmf(tone, TONE_DURATION, 100);
530        toneManager.startTone(TONE_CODES.get(tone), TONE_DURATION);
531        return true;
532    }
533
534    void addIceCandidate(IceCandidate iceCandidate) {
535        requirePeerConnection().addIceCandidate(iceCandidate);
536    }
537
538    private CameraEnumerator getCameraEnumerator() {
539        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
540            return new Camera2Enumerator(requireContext());
541        } else {
542            return new Camera1Enumerator();
543        }
544    }
545
546    private Optional<CapturerChoice> getVideoCapturer() {
547        final CameraEnumerator enumerator = getCameraEnumerator();
548        final Set<String> deviceNames = ImmutableSet.copyOf(enumerator.getDeviceNames());
549        for (final String deviceName : deviceNames) {
550            if (isFrontFacing(enumerator, deviceName)) {
551                final CapturerChoice capturerChoice = of(enumerator, deviceName, deviceNames);
552                if (capturerChoice == null) {
553                    return Optional.absent();
554                }
555                capturerChoice.isFrontCamera = true;
556                return Optional.of(capturerChoice);
557            }
558        }
559        if (deviceNames.size() == 0) {
560            return Optional.absent();
561        } else {
562            return Optional.fromNullable(of(enumerator, Iterables.get(deviceNames, 0), deviceNames));
563        }
564    }
565
566    public PeerConnection.PeerConnectionState getState() {
567        return requirePeerConnection().connectionState();
568    }
569
570    EglBase.Context getEglBaseContext() {
571        return this.eglBase.getEglBaseContext();
572    }
573
574    Optional<VideoTrack> getLocalVideoTrack() {
575        return Optional.fromNullable(this.localVideoTrack);
576    }
577
578    Optional<VideoTrack> getRemoteVideoTrack() {
579        return Optional.fromNullable(this.remoteVideoTrack);
580    }
581
582    private PeerConnection requirePeerConnection() {
583        final PeerConnection peerConnection = this.peerConnection;
584        if (peerConnection == null) {
585            throw new PeerConnectionNotInitialized();
586        }
587        return peerConnection;
588    }
589
590    private Context requireContext() {
591        final Context context = this.context;
592        if (context == null) {
593            throw new IllegalStateException("call setup first");
594        }
595        return context;
596    }
597
598    AppRTCAudioManager getAudioManager() {
599        return appRTCAudioManager;
600    }
601
602    public interface EventCallback {
603        void onIceCandidate(IceCandidate iceCandidate);
604
605        void onConnectionChange(PeerConnection.PeerConnectionState newState);
606
607        void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices);
608    }
609
610    private static abstract class SetSdpObserver implements SdpObserver {
611
612        @Override
613        public void onCreateSuccess(org.webrtc.SessionDescription sessionDescription) {
614            throw new IllegalStateException("Not able to use SetSdpObserver");
615        }
616
617        @Override
618        public void onCreateFailure(String s) {
619            throw new IllegalStateException("Not able to use SetSdpObserver");
620        }
621
622    }
623
624    private static abstract class CreateSdpObserver implements SdpObserver {
625
626
627        @Override
628        public void onSetSuccess() {
629            throw new IllegalStateException("Not able to use CreateSdpObserver");
630        }
631
632
633        @Override
634        public void onSetFailure(String s) {
635            throw new IllegalStateException("Not able to use CreateSdpObserver");
636        }
637    }
638
639    static class InitializationException extends Exception {
640
641        private InitializationException(final String message, final Throwable throwable) {
642            super(message, throwable);
643        }
644
645        private InitializationException(final String message) {
646            super(message);
647        }
648    }
649
650    public static class PeerConnectionNotInitialized extends IllegalStateException {
651
652        private PeerConnectionNotInitialized() {
653            super("initialize PeerConnection first");
654        }
655
656    }
657
658    private static class CapturerChoice {
659        private final CameraVideoCapturer cameraVideoCapturer;
660        private final CameraEnumerationAndroid.CaptureFormat captureFormat;
661        private final Set<String> availableCameras;
662        private boolean isFrontCamera = false;
663
664        CapturerChoice(CameraVideoCapturer cameraVideoCapturer, CameraEnumerationAndroid.CaptureFormat captureFormat, Set<String> cameras) {
665            this.cameraVideoCapturer = cameraVideoCapturer;
666            this.captureFormat = captureFormat;
667            this.availableCameras = cameras;
668        }
669
670        int getFrameRate() {
671            return Math.max(captureFormat.framerate.min, Math.min(CAPTURING_MAX_FRAME_RATE, captureFormat.framerate.max));
672        }
673    }
674}