WebRTCWrapper.java

  1package eu.siacs.conversations.xmpp.jingle;
  2
  3import android.content.Context;
  4import android.os.Build;
  5import android.util.Log;
  6
  7import com.google.common.base.Optional;
  8import com.google.common.base.Preconditions;
  9import com.google.common.collect.ImmutableSet;
 10import com.google.common.util.concurrent.Futures;
 11import com.google.common.util.concurrent.ListenableFuture;
 12import com.google.common.util.concurrent.MoreExecutors;
 13import com.google.common.util.concurrent.SettableFuture;
 14
 15import eu.siacs.conversations.Config;
 16import eu.siacs.conversations.services.XmppConnectionService;
 17
 18import org.webrtc.AudioSource;
 19import org.webrtc.AudioTrack;
 20import org.webrtc.CandidatePairChangeEvent;
 21import org.webrtc.DataChannel;
 22import org.webrtc.DefaultVideoDecoderFactory;
 23import org.webrtc.DefaultVideoEncoderFactory;
 24import org.webrtc.EglBase;
 25import org.webrtc.IceCandidate;
 26import org.webrtc.MediaConstraints;
 27import org.webrtc.MediaStream;
 28import org.webrtc.MediaStreamTrack;
 29import org.webrtc.PeerConnection;
 30import org.webrtc.PeerConnectionFactory;
 31import org.webrtc.RtpReceiver;
 32import org.webrtc.RtpTransceiver;
 33import org.webrtc.SdpObserver;
 34import org.webrtc.SessionDescription;
 35import org.webrtc.VideoTrack;
 36import org.webrtc.audio.JavaAudioDeviceModule;
 37
 38import java.util.LinkedList;
 39import java.util.List;
 40import java.util.Queue;
 41import java.util.Set;
 42import java.util.concurrent.ExecutorService;
 43import java.util.concurrent.Executors;
 44import java.util.concurrent.TimeUnit;
 45import java.util.concurrent.TimeoutException;
 46import java.util.concurrent.atomic.AtomicBoolean;
 47
 48import javax.annotation.Nonnull;
 49import javax.annotation.Nullable;
 50
 51public class WebRTCWrapper {
 52
 53    private static final String EXTENDED_LOGGING_TAG = WebRTCWrapper.class.getSimpleName();
 54
 55    private final ExecutorService executorService = Executors.newSingleThreadExecutor();
 56    private final ExecutorService localDescriptionExecutorService =
 57            Executors.newSingleThreadExecutor();
 58
 59    private static final Set<String> HARDWARE_AEC_BLACKLIST =
 60            new ImmutableSet.Builder<String>()
 61                    .add("Pixel")
 62                    .add("Pixel XL")
 63                    .add("Moto G5")
 64                    .add("Moto G (5S) Plus")
 65                    .add("Moto G4")
 66                    .add("TA-1053")
 67                    .add("Mi A1")
 68                    .add("Mi A2")
 69                    .add("E5823") // Sony z5 compact
 70                    .add("Redmi Note 5")
 71                    .add("FP2") // Fairphone FP2
 72                    .add("FP4") // Fairphone FP4
 73                    .add("MI 5")
 74                    .add("GT-I9515") // Samsung Galaxy S4 Value Edition (jfvelte)
 75                    .add("GT-I9515L") // Samsung Galaxy S4 Value Edition (jfvelte)
 76                    .add("GT-I9505") // Samsung Galaxy S4 (jfltexx)
 77                    .build();
 78
 79    private final EventCallback eventCallback;
 80    private final AtomicBoolean readyToReceivedIceCandidates = new AtomicBoolean(false);
 81    private final Queue<IceCandidate> iceCandidates = new LinkedList<>();
 82    private TrackWrapper<AudioTrack> localAudioTrack = null;
 83    private TrackWrapper<VideoTrack> localVideoTrack = null;
 84    private VideoTrack remoteVideoTrack = null;
 85
 86    private final SettableFuture<Void> iceGatheringComplete = SettableFuture.create();
 87    private final PeerConnection.Observer peerConnectionObserver =
 88            new PeerConnection.Observer() {
 89                @Override
 90                public void onSignalingChange(PeerConnection.SignalingState signalingState) {
 91                    Log.d(EXTENDED_LOGGING_TAG, "onSignalingChange(" + signalingState + ")");
 92                    // this is called after removeTrack or addTrack
 93                    // and should then trigger a content-add or content-remove or something
 94                    // https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/removeTrack
 95                }
 96
 97                @Override
 98                public void onConnectionChange(final PeerConnection.PeerConnectionState newState) {
 99                    eventCallback.onConnectionChange(newState);
100                }
101
102                @Override
103                public void onIceConnectionChange(
104                        PeerConnection.IceConnectionState iceConnectionState) {
105                    Log.d(
106                            EXTENDED_LOGGING_TAG,
107                            "onIceConnectionChange(" + iceConnectionState + ")");
108                }
109
110                @Override
111                public void onSelectedCandidatePairChanged(CandidatePairChangeEvent event) {
112                    Log.d(Config.LOGTAG, "remote candidate selected: " + event.remote);
113                    Log.d(Config.LOGTAG, "local candidate selected: " + event.local);
114                }
115
116                @Override
117                public void onIceConnectionReceivingChange(boolean b) {}
118
119                @Override
120                public void onIceGatheringChange(
121                        final PeerConnection.IceGatheringState iceGatheringState) {
122                    Log.d(EXTENDED_LOGGING_TAG, "onIceGatheringChange(" + iceGatheringState + ")");
123                    if (iceGatheringState == PeerConnection.IceGatheringState.COMPLETE) {
124                        iceGatheringComplete.set(null);
125                    }
126                }
127
128                @Override
129                public void onIceCandidate(IceCandidate iceCandidate) {
130                    if (readyToReceivedIceCandidates.get()) {
131                        eventCallback.onIceCandidate(iceCandidate);
132                    } else {
133                        iceCandidates.add(iceCandidate);
134                    }
135                }
136
137                @Override
138                public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {}
139
140                @Override
141                public void onAddStream(MediaStream mediaStream) {
142                    Log.d(
143                            EXTENDED_LOGGING_TAG,
144                            "onAddStream(numAudioTracks="
145                                    + mediaStream.audioTracks.size()
146                                    + ",numVideoTracks="
147                                    + mediaStream.videoTracks.size()
148                                    + ")");
149                }
150
151                @Override
152                public void onRemoveStream(MediaStream mediaStream) {}
153
154                @Override
155                public void onDataChannel(DataChannel dataChannel) {}
156
157                @Override
158                public void onRenegotiationNeeded() {
159                    Log.d(EXTENDED_LOGGING_TAG, "onRenegotiationNeeded()");
160                    final PeerConnection.PeerConnectionState currentState =
161                            peerConnection == null ? null : peerConnection.connectionState();
162                    if (currentState != null
163                            && currentState != PeerConnection.PeerConnectionState.NEW) {
164                        eventCallback.onRenegotiationNeeded();
165                    }
166                }
167
168                @Override
169                public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
170                    final MediaStreamTrack track = rtpReceiver.track();
171                    Log.d(
172                            EXTENDED_LOGGING_TAG,
173                            "onAddTrack(kind="
174                                    + (track == null ? "null" : track.kind())
175                                    + ",numMediaStreams="
176                                    + mediaStreams.length
177                                    + ")");
178                    if (track instanceof VideoTrack) {
179                        remoteVideoTrack = (VideoTrack) track;
180                    }
181                }
182
183                @Override
184                public void onTrack(final RtpTransceiver transceiver) {
185                    Log.d(
186                            EXTENDED_LOGGING_TAG,
187                            "onTrack(mid="
188                                    + transceiver.getMid()
189                                    + ",media="
190                                    + transceiver.getMediaType()
191                                    + ",direction="
192                                    + transceiver.getDirection()
193                                    + ")");
194                }
195
196                @Override
197                public void onRemoveTrack(final RtpReceiver receiver) {
198                    Log.d(EXTENDED_LOGGING_TAG, "onRemoveTrack(" + receiver.id() + ")");
199                }
200            };
201    @Nullable private PeerConnectionFactory peerConnectionFactory = null;
202    @Nullable private PeerConnection peerConnection = null;
203    private Context context = null;
204    private EglBase eglBase = null;
205    private VideoSourceWrapper videoSourceWrapper;
206
207    WebRTCWrapper(final EventCallback eventCallback) {
208        this.eventCallback = eventCallback;
209    }
210
211    private static void dispose(final PeerConnection peerConnection) {
212        try {
213            peerConnection.dispose();
214        } catch (final IllegalStateException e) {
215            Log.e(Config.LOGTAG, "unable to dispose of peer connection", e);
216        }
217    }
218
219    public void setup(final XmppConnectionService service) throws InitializationException {
220        try {
221            PeerConnectionFactory.initialize(
222                    PeerConnectionFactory.InitializationOptions.builder(service)
223                            .setFieldTrials("WebRTC-BindUsingInterfaceName/Enabled/")
224                            .createInitializationOptions());
225        } catch (final UnsatisfiedLinkError e) {
226            throw new InitializationException("Unable to initialize PeerConnectionFactory", e);
227        }
228        try {
229            this.eglBase = EglBase.create();
230        } catch (final RuntimeException e) {
231            throw new InitializationException("Unable to create EGL base", e);
232        }
233        this.context = service;
234    }
235
236    synchronized void initializePeerConnection(
237            final Set<Media> media,
238            final List<PeerConnection.IceServer> iceServers,
239            final boolean trickle)
240            throws InitializationException {
241        Preconditions.checkState(this.eglBase != null);
242        Preconditions.checkNotNull(media);
243        Preconditions.checkArgument(
244                media.size() > 0, "media can not be empty when initializing peer connection");
245        final boolean setUseHardwareAcousticEchoCanceler =
246                !HARDWARE_AEC_BLACKLIST.contains(Build.MODEL);
247        Log.d(
248                Config.LOGTAG,
249                String.format(
250                        "setUseHardwareAcousticEchoCanceler(%s) model=%s",
251                        setUseHardwareAcousticEchoCanceler, Build.MODEL));
252        this.peerConnectionFactory =
253                PeerConnectionFactory.builder()
254                        .setVideoDecoderFactory(
255                                new DefaultVideoDecoderFactory(eglBase.getEglBaseContext()))
256                        .setVideoEncoderFactory(
257                                new DefaultVideoEncoderFactory(
258                                        eglBase.getEglBaseContext(), true, true))
259                        .setAudioDeviceModule(
260                                JavaAudioDeviceModule.builder(requireContext())
261                                        .setUseHardwareAcousticEchoCanceler(
262                                                setUseHardwareAcousticEchoCanceler)
263                                        .createAudioDeviceModule())
264                        .createPeerConnectionFactory();
265
266        final PeerConnection.RTCConfiguration rtcConfig = buildConfiguration(iceServers, trickle);
267        final PeerConnection peerConnection =
268                requirePeerConnectionFactory()
269                        .createPeerConnection(rtcConfig, peerConnectionObserver);
270        if (peerConnection == null) {
271            throw new InitializationException("Unable to create PeerConnection");
272        }
273
274        if (media.contains(Media.VIDEO)) {
275            addVideoTrack(peerConnection);
276        }
277
278        if (media.contains(Media.AUDIO)) {
279            addAudioTrack(peerConnection);
280        }
281        peerConnection.setAudioPlayout(true);
282        peerConnection.setAudioRecording(true);
283
284        this.peerConnection = peerConnection;
285    }
286
287    private VideoSourceWrapper initializeVideoSourceWrapper() {
288        final VideoSourceWrapper existingVideoSourceWrapper = this.videoSourceWrapper;
289        if (existingVideoSourceWrapper != null) {
290            existingVideoSourceWrapper.startCapture();
291            return existingVideoSourceWrapper;
292        }
293        final VideoSourceWrapper videoSourceWrapper =
294                new VideoSourceWrapper.Factory(requireContext()).create();
295        if (videoSourceWrapper == null) {
296            throw new IllegalStateException("Could not instantiate VideoSourceWrapper");
297        }
298        videoSourceWrapper.initialize(
299                requirePeerConnectionFactory(), requireContext(), eglBase.getEglBaseContext());
300        videoSourceWrapper.startCapture();
301        this.videoSourceWrapper = videoSourceWrapper;
302        return videoSourceWrapper;
303    }
304
305    public synchronized boolean addTrack(final Media media) {
306        if (media == Media.VIDEO) {
307            return addVideoTrack(requirePeerConnection());
308        } else if (media == Media.AUDIO) {
309            return addAudioTrack(requirePeerConnection());
310        }
311        throw new IllegalStateException(String.format("Could not add track for %s", media));
312    }
313
314    public synchronized void removeTrack(final Media media) {
315        if (media == Media.VIDEO) {
316            removeVideoTrack(requirePeerConnection());
317        }
318    }
319
320    private boolean addAudioTrack(final PeerConnection peerConnection) {
321        final AudioSource audioSource =
322                requirePeerConnectionFactory().createAudioSource(new MediaConstraints());
323        final AudioTrack audioTrack =
324                requirePeerConnectionFactory()
325                        .createAudioTrack(TrackWrapper.id(AudioTrack.class), audioSource);
326        this.localAudioTrack = TrackWrapper.addTrack(peerConnection, audioTrack);
327        return true;
328    }
329
330    private boolean addVideoTrack(final PeerConnection peerConnection) {
331        final TrackWrapper<VideoTrack> existing = this.localVideoTrack;
332        if (existing != null) {
333            final RtpTransceiver transceiver =
334                    TrackWrapper.getTransceiver(peerConnection, existing);
335            if (transceiver == null) {
336                Log.w(EXTENDED_LOGGING_TAG, "unable to restart video transceiver");
337                return false;
338            }
339            transceiver.setDirection(RtpTransceiver.RtpTransceiverDirection.SEND_RECV);
340            this.videoSourceWrapper.startCapture();
341            return true;
342        }
343        final VideoSourceWrapper videoSourceWrapper;
344        try {
345            videoSourceWrapper = initializeVideoSourceWrapper();
346        } catch (final IllegalStateException e) {
347            Log.d(Config.LOGTAG, "could not add video track", e);
348            return false;
349        }
350        final VideoTrack videoTrack =
351                requirePeerConnectionFactory()
352                        .createVideoTrack(
353                                TrackWrapper.id(VideoTrack.class),
354                                videoSourceWrapper.getVideoSource());
355        this.localVideoTrack = TrackWrapper.addTrack(peerConnection, videoTrack);
356        return true;
357    }
358
359    private void removeVideoTrack(final PeerConnection peerConnection) {
360        final TrackWrapper<VideoTrack> localVideoTrack = this.localVideoTrack;
361        if (localVideoTrack != null) {
362
363            final RtpTransceiver exactTransceiver =
364                    TrackWrapper.getTransceiver(peerConnection, localVideoTrack);
365            if (exactTransceiver == null) {
366                throw new IllegalStateException();
367            }
368            exactTransceiver.setDirection(RtpTransceiver.RtpTransceiverDirection.INACTIVE);
369        }
370        final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper;
371        if (videoSourceWrapper != null) {
372            try {
373                videoSourceWrapper.stopCapture();
374            } catch (InterruptedException e) {
375                e.printStackTrace();
376            }
377        }
378    }
379
380    public static PeerConnection.RTCConfiguration buildConfiguration(
381            final List<PeerConnection.IceServer> iceServers, final boolean trickle) {
382        final PeerConnection.RTCConfiguration rtcConfig =
383                new PeerConnection.RTCConfiguration(iceServers);
384        rtcConfig.tcpCandidatePolicy =
385                PeerConnection.TcpCandidatePolicy.DISABLED; // XEP-0176 doesn't support tcp
386        if (trickle) {
387            rtcConfig.continualGatheringPolicy =
388                    PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY;
389        } else {
390            rtcConfig.continualGatheringPolicy =
391                    PeerConnection.ContinualGatheringPolicy.GATHER_ONCE;
392        }
393        rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
394        rtcConfig.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.NEGOTIATE;
395        rtcConfig.enableImplicitRollback = true;
396        return rtcConfig;
397    }
398
399    void reconfigurePeerConnection(
400            final List<PeerConnection.IceServer> iceServers, final boolean trickle) {
401        requirePeerConnection().setConfiguration(buildConfiguration(iceServers, trickle));
402    }
403
404    void restartIceAsync() {
405        this.execute(this::restartIce);
406    }
407
408    private void restartIce() {
409        final PeerConnection peerConnection;
410        try {
411            peerConnection = requirePeerConnection();
412        } catch (final PeerConnectionNotInitialized e) {
413            Log.w(EXTENDED_LOGGING_TAG, "PeerConnection vanished before we could execute restart");
414            return;
415        }
416        setIsReadyToReceiveIceCandidates(false);
417        peerConnection.restartIce();
418    }
419
420    public void setIsReadyToReceiveIceCandidates(final boolean ready) {
421        readyToReceivedIceCandidates.set(ready);
422        final int was = iceCandidates.size();
423        while (ready && iceCandidates.peek() != null) {
424            eventCallback.onIceCandidate(iceCandidates.poll());
425        }
426        final int is = iceCandidates.size();
427        Log.d(
428                EXTENDED_LOGGING_TAG,
429                "setIsReadyToReceiveCandidates(" + ready + ") was=" + was + " is=" + is);
430    }
431
432    synchronized void close() {
433        final PeerConnection peerConnection = this.peerConnection;
434        final PeerConnectionFactory peerConnectionFactory = this.peerConnectionFactory;
435        final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper;
436        final EglBase eglBase = this.eglBase;
437        if (peerConnection != null) {
438            this.peerConnection = null;
439            dispose(peerConnection);
440        }
441        this.localVideoTrack = null;
442        this.remoteVideoTrack = null;
443        if (videoSourceWrapper != null) {
444            this.videoSourceWrapper = null;
445            try {
446                videoSourceWrapper.stopCapture();
447            } catch (final InterruptedException e) {
448                Log.e(Config.LOGTAG, "unable to stop capturing");
449            }
450            videoSourceWrapper.dispose();
451        }
452        if (eglBase != null) {
453            eglBase.release();
454            this.eglBase = null;
455        }
456        if (peerConnectionFactory != null) {
457            this.peerConnectionFactory = null;
458            peerConnectionFactory.dispose();
459        }
460    }
461
462    synchronized void verifyClosed() {
463        if (this.peerConnection != null
464                || this.eglBase != null
465                || this.localVideoTrack != null
466                || this.remoteVideoTrack != null) {
467            final AssertionError e =
468                    new AssertionError("WebRTCWrapper hasn't been closed properly");
469            Log.e(Config.LOGTAG, "verifyClosed() failed. Going to throw", e);
470            throw e;
471        }
472    }
473
474    boolean isCameraSwitchable() {
475        final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper;
476        return videoSourceWrapper != null && videoSourceWrapper.isCameraSwitchable();
477    }
478
479    boolean isFrontCamera() {
480        final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper;
481        return videoSourceWrapper == null || videoSourceWrapper.isFrontCamera();
482    }
483
484    ListenableFuture<Boolean> switchCamera() {
485        final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper;
486        if (videoSourceWrapper == null) {
487            return Futures.immediateFailedFuture(
488                    new IllegalStateException("VideoSourceWrapper has not been initialized"));
489        }
490        return videoSourceWrapper.switchCamera();
491    }
492
493    boolean isMicrophoneEnabled() {
494        final Optional<AudioTrack> audioTrack =
495                TrackWrapper.get(peerConnection, this.localAudioTrack);
496        if (audioTrack.isPresent()) {
497            try {
498                return audioTrack.get().enabled();
499            } catch (final IllegalStateException e) {
500                // sometimes UI might still be rendering the buttons when a background thread has
501                // already ended the call
502                return false;
503            }
504        } else {
505            return false;
506        }
507    }
508
509    boolean setMicrophoneEnabled(final boolean enabled) {
510        final Optional<AudioTrack> audioTrack =
511                TrackWrapper.get(peerConnection, this.localAudioTrack);
512        if (audioTrack.isPresent()) {
513            try {
514                audioTrack.get().setEnabled(enabled);
515                return true;
516            } catch (final IllegalStateException e) {
517                Log.d(Config.LOGTAG, "unable to toggle microphone", e);
518                // ignoring race condition in case MediaStreamTrack has been disposed
519                return false;
520            }
521        } else {
522            throw new IllegalStateException("Local audio track does not exist (yet)");
523        }
524    }
525
526    boolean isVideoEnabled() {
527        final Optional<VideoTrack> videoTrack =
528                TrackWrapper.get(peerConnection, this.localVideoTrack);
529        if (videoTrack.isPresent()) {
530            return videoTrack.get().enabled();
531        }
532        return false;
533    }
534
535    void setVideoEnabled(final boolean enabled) {
536        final Optional<VideoTrack> videoTrack =
537                TrackWrapper.get(peerConnection, this.localVideoTrack);
538        if (videoTrack.isPresent()) {
539            videoTrack.get().setEnabled(enabled);
540            return;
541        }
542        throw new IllegalStateException("Local video track does not exist");
543    }
544
545    synchronized ListenableFuture<SessionDescription> setLocalDescription(
546            final boolean waitForCandidates) {
547        this.setIsReadyToReceiveIceCandidates(false);
548        return Futures.transformAsync(
549                getPeerConnectionFuture(),
550                peerConnection -> {
551                    if (peerConnection == null) {
552                        return Futures.immediateFailedFuture(
553                                new IllegalStateException("PeerConnection was null"));
554                    }
555                    final SettableFuture<SessionDescription> future = SettableFuture.create();
556                    peerConnection.setLocalDescription(
557                            new SetSdpObserver() {
558                                @Override
559                                public void onSetSuccess() {
560                                    if (waitForCandidates) {
561                                        final var delay = getIceGatheringCompleteOrTimeout();
562                                        final var delayedSessionDescription =
563                                                Futures.transformAsync(
564                                                        delay,
565                                                        v -> {
566                                                            iceCandidates.clear();
567                                                            return getLocalDescriptionFuture();
568                                                        },
569                                                        MoreExecutors.directExecutor());
570                                        future.setFuture(delayedSessionDescription);
571                                    } else {
572                                        future.setFuture(getLocalDescriptionFuture());
573                                    }
574                                }
575
576                                @Override
577                                public void onSetFailure(final String message) {
578                                    future.setException(
579                                            new FailureToSetDescriptionException(message));
580                                }
581                            });
582                    return future;
583                },
584                MoreExecutors.directExecutor());
585    }
586
587    private ListenableFuture<Void> getIceGatheringCompleteOrTimeout() {
588        return Futures.catching(
589                Futures.withTimeout(
590                        iceGatheringComplete,
591                        2,
592                        TimeUnit.SECONDS,
593                        JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE),
594                TimeoutException.class,
595                ex -> {
596                    Log.d(
597                            EXTENDED_LOGGING_TAG,
598                            "timeout while waiting for ICE gathering to complete");
599                    return null;
600                },
601                MoreExecutors.directExecutor());
602    }
603
604    private ListenableFuture<SessionDescription> getLocalDescriptionFuture() {
605        return Futures.submit(
606                () -> {
607                    final SessionDescription description =
608                            requirePeerConnection().getLocalDescription();
609                    Log.d(EXTENDED_LOGGING_TAG, "local description:");
610                    logDescription(description);
611                    return description;
612                },
613                localDescriptionExecutorService);
614    }
615
616    public static void logDescription(final SessionDescription sessionDescription) {
617        for (final String line :
618                sessionDescription.description.split(
619                        eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) {
620            Log.d(EXTENDED_LOGGING_TAG, line);
621        }
622    }
623
624    synchronized ListenableFuture<Void> setRemoteDescription(
625            final SessionDescription sessionDescription) {
626        Log.d(EXTENDED_LOGGING_TAG, "setting remote description:");
627        logDescription(sessionDescription);
628        return Futures.transformAsync(
629                getPeerConnectionFuture(),
630                peerConnection -> {
631                    if (peerConnection == null) {
632                        return Futures.immediateFailedFuture(
633                                new IllegalStateException("PeerConnection was null"));
634                    }
635                    final SettableFuture<Void> future = SettableFuture.create();
636                    peerConnection.setRemoteDescription(
637                            new SetSdpObserver() {
638                                @Override
639                                public void onSetSuccess() {
640                                    future.set(null);
641                                }
642
643                                @Override
644                                public void onSetFailure(final String message) {
645                                    future.setException(
646                                            new FailureToSetDescriptionException(message));
647                                }
648                            },
649                            sessionDescription);
650                    return future;
651                },
652                MoreExecutors.directExecutor());
653    }
654
655    @Nonnull
656    private ListenableFuture<PeerConnection> getPeerConnectionFuture() {
657        final PeerConnection peerConnection = this.peerConnection;
658        if (peerConnection == null) {
659            return Futures.immediateFailedFuture(new PeerConnectionNotInitialized());
660        } else {
661            return Futures.immediateFuture(peerConnection);
662        }
663    }
664
665    @Nonnull
666    private PeerConnection requirePeerConnection() {
667        final PeerConnection peerConnection = this.peerConnection;
668        if (peerConnection == null) {
669            throw new PeerConnectionNotInitialized();
670        }
671        return peerConnection;
672    }
673
674    @Nonnull
675    private PeerConnectionFactory requirePeerConnectionFactory() {
676        final PeerConnectionFactory peerConnectionFactory = this.peerConnectionFactory;
677        if (peerConnectionFactory == null) {
678            throw new IllegalStateException("Make sure PeerConnectionFactory is initialized");
679        }
680        return peerConnectionFactory;
681    }
682
683    void addIceCandidate(IceCandidate iceCandidate) {
684        requirePeerConnection().addIceCandidate(iceCandidate);
685    }
686
687    PeerConnection.PeerConnectionState getState() {
688        return requirePeerConnection().connectionState();
689    }
690
691    public PeerConnection.SignalingState getSignalingState() {
692        try {
693            return requirePeerConnection().signalingState();
694        } catch (final IllegalStateException e) {
695            return PeerConnection.SignalingState.CLOSED;
696        }
697    }
698
699    EglBase.Context getEglBaseContext() {
700        return this.eglBase.getEglBaseContext();
701    }
702
703    Optional<VideoTrack> getLocalVideoTrack() {
704        return TrackWrapper.get(peerConnection, this.localVideoTrack);
705    }
706
707    Optional<VideoTrack> getRemoteVideoTrack() {
708        return Optional.fromNullable(this.remoteVideoTrack);
709    }
710
711    private Context requireContext() {
712        final Context context = this.context;
713        if (context == null) {
714            throw new IllegalStateException("call setup first");
715        }
716        return context;
717    }
718
719    void execute(final Runnable command) {
720        this.executorService.execute(command);
721    }
722
723    public interface EventCallback {
724        void onIceCandidate(IceCandidate iceCandidate);
725
726        void onConnectionChange(PeerConnection.PeerConnectionState newState);
727
728        void onRenegotiationNeeded();
729    }
730
731    public abstract static class SetSdpObserver implements SdpObserver {
732
733        @Override
734        public void onCreateSuccess(org.webrtc.SessionDescription sessionDescription) {
735            throw new IllegalStateException("Not able to use SetSdpObserver");
736        }
737
738        @Override
739        public void onCreateFailure(String s) {
740            throw new IllegalStateException("Not able to use SetSdpObserver");
741        }
742    }
743
744    static class InitializationException extends Exception {
745
746        private InitializationException(final String message, final Throwable throwable) {
747            super(message, throwable);
748        }
749
750        private InitializationException(final String message) {
751            super(message);
752        }
753    }
754
755    public static class PeerConnectionNotInitialized extends IllegalStateException {
756
757        public PeerConnectionNotInitialized() {
758            super("initialize PeerConnection first");
759        }
760    }
761
762    public static class FailureToSetDescriptionException extends IllegalArgumentException {
763        public FailureToSetDescriptionException(String message) {
764            super(message);
765        }
766    }
767}