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 setMicrophoneEnabledOrThrow(final boolean enabled) {
510        final Optional<AudioTrack> audioTrack =
511                TrackWrapper.get(peerConnection, this.localAudioTrack);
512        if (audioTrack.isPresent()) {
513            return setEnabled(audioTrack.get(), enabled);
514
515        } else {
516            throw new IllegalStateException("Local audio track does not exist (yet)");
517        }
518    }
519
520    private static boolean setEnabled(final AudioTrack audioTrack, final boolean enabled) {
521        try {
522            audioTrack.setEnabled(enabled);
523            return true;
524        } catch (final IllegalStateException e) {
525            Log.d(Config.LOGTAG, "unable to toggle audio track", e);
526            // ignoring race condition in case MediaStreamTrack has been disposed
527            return false;
528        }
529    }
530
531    void setMicrophoneEnabled(final boolean enabled) {
532        final Optional<AudioTrack> audioTrack =
533                TrackWrapper.get(peerConnection, this.localAudioTrack);
534        if (audioTrack.isPresent()) {
535            setEnabled(audioTrack.get(), enabled);
536        }
537    }
538
539    boolean isVideoEnabled() {
540        final Optional<VideoTrack> videoTrack =
541                TrackWrapper.get(peerConnection, this.localVideoTrack);
542        if (videoTrack.isPresent()) {
543            return videoTrack.get().enabled();
544        }
545        return false;
546    }
547
548    void setVideoEnabled(final boolean enabled) {
549        final Optional<VideoTrack> videoTrack =
550                TrackWrapper.get(peerConnection, this.localVideoTrack);
551        if (videoTrack.isPresent()) {
552            videoTrack.get().setEnabled(enabled);
553            return;
554        }
555        throw new IllegalStateException("Local video track does not exist");
556    }
557
558    synchronized ListenableFuture<SessionDescription> setLocalDescription(
559            final boolean waitForCandidates) {
560        this.setIsReadyToReceiveIceCandidates(false);
561        return Futures.transformAsync(
562                getPeerConnectionFuture(),
563                peerConnection -> {
564                    if (peerConnection == null) {
565                        return Futures.immediateFailedFuture(
566                                new IllegalStateException("PeerConnection was null"));
567                    }
568                    final SettableFuture<SessionDescription> future = SettableFuture.create();
569                    peerConnection.setLocalDescription(
570                            new SetSdpObserver() {
571                                @Override
572                                public void onSetSuccess() {
573                                    if (waitForCandidates) {
574                                        final var delay = getIceGatheringCompleteOrTimeout();
575                                        final var delayedSessionDescription =
576                                                Futures.transformAsync(
577                                                        delay,
578                                                        v -> {
579                                                            iceCandidates.clear();
580                                                            return getLocalDescriptionFuture();
581                                                        },
582                                                        MoreExecutors.directExecutor());
583                                        future.setFuture(delayedSessionDescription);
584                                    } else {
585                                        future.setFuture(getLocalDescriptionFuture());
586                                    }
587                                }
588
589                                @Override
590                                public void onSetFailure(final String message) {
591                                    future.setException(
592                                            new FailureToSetDescriptionException(message));
593                                }
594                            });
595                    return future;
596                },
597                MoreExecutors.directExecutor());
598    }
599
600    private ListenableFuture<Void> getIceGatheringCompleteOrTimeout() {
601        return Futures.catching(
602                Futures.withTimeout(
603                        iceGatheringComplete,
604                        2,
605                        TimeUnit.SECONDS,
606                        JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE),
607                TimeoutException.class,
608                ex -> {
609                    Log.d(
610                            EXTENDED_LOGGING_TAG,
611                            "timeout while waiting for ICE gathering to complete");
612                    return null;
613                },
614                MoreExecutors.directExecutor());
615    }
616
617    private ListenableFuture<SessionDescription> getLocalDescriptionFuture() {
618        return Futures.submit(
619                () -> {
620                    final SessionDescription description =
621                            requirePeerConnection().getLocalDescription();
622                    Log.d(EXTENDED_LOGGING_TAG, "local description:");
623                    logDescription(description);
624                    return description;
625                },
626                localDescriptionExecutorService);
627    }
628
629    public static void logDescription(final SessionDescription sessionDescription) {
630        for (final String line :
631                sessionDescription.description.split(
632                        eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) {
633            Log.d(EXTENDED_LOGGING_TAG, line);
634        }
635    }
636
637    synchronized ListenableFuture<Void> setRemoteDescription(
638            final SessionDescription sessionDescription) {
639        Log.d(EXTENDED_LOGGING_TAG, "setting remote description:");
640        logDescription(sessionDescription);
641        return Futures.transformAsync(
642                getPeerConnectionFuture(),
643                peerConnection -> {
644                    if (peerConnection == null) {
645                        return Futures.immediateFailedFuture(
646                                new IllegalStateException("PeerConnection was null"));
647                    }
648                    final SettableFuture<Void> future = SettableFuture.create();
649                    peerConnection.setRemoteDescription(
650                            new SetSdpObserver() {
651                                @Override
652                                public void onSetSuccess() {
653                                    future.set(null);
654                                }
655
656                                @Override
657                                public void onSetFailure(final String message) {
658                                    future.setException(
659                                            new FailureToSetDescriptionException(message));
660                                }
661                            },
662                            sessionDescription);
663                    return future;
664                },
665                MoreExecutors.directExecutor());
666    }
667
668    @Nonnull
669    private ListenableFuture<PeerConnection> getPeerConnectionFuture() {
670        final PeerConnection peerConnection = this.peerConnection;
671        if (peerConnection == null) {
672            return Futures.immediateFailedFuture(new PeerConnectionNotInitialized());
673        } else {
674            return Futures.immediateFuture(peerConnection);
675        }
676    }
677
678    @Nonnull
679    private PeerConnection requirePeerConnection() {
680        final PeerConnection peerConnection = this.peerConnection;
681        if (peerConnection == null) {
682            throw new PeerConnectionNotInitialized();
683        }
684        return peerConnection;
685    }
686
687    @Nonnull
688    private PeerConnectionFactory requirePeerConnectionFactory() {
689        final PeerConnectionFactory peerConnectionFactory = this.peerConnectionFactory;
690        if (peerConnectionFactory == null) {
691            throw new IllegalStateException("Make sure PeerConnectionFactory is initialized");
692        }
693        return peerConnectionFactory;
694    }
695
696    void addIceCandidate(IceCandidate iceCandidate) {
697        requirePeerConnection().addIceCandidate(iceCandidate);
698    }
699
700    PeerConnection.PeerConnectionState getState() {
701        return requirePeerConnection().connectionState();
702    }
703
704    public PeerConnection.SignalingState getSignalingState() {
705        try {
706            return requirePeerConnection().signalingState();
707        } catch (final IllegalStateException e) {
708            return PeerConnection.SignalingState.CLOSED;
709        }
710    }
711
712    EglBase.Context getEglBaseContext() {
713        return this.eglBase.getEglBaseContext();
714    }
715
716    Optional<VideoTrack> getLocalVideoTrack() {
717        return TrackWrapper.get(peerConnection, this.localVideoTrack);
718    }
719
720    Optional<VideoTrack> getRemoteVideoTrack() {
721        return Optional.fromNullable(this.remoteVideoTrack);
722    }
723
724    private Context requireContext() {
725        final Context context = this.context;
726        if (context == null) {
727            throw new IllegalStateException("call setup first");
728        }
729        return context;
730    }
731
732    void execute(final Runnable command) {
733        this.executorService.execute(command);
734    }
735
736    public interface EventCallback {
737        void onIceCandidate(IceCandidate iceCandidate);
738
739        void onConnectionChange(PeerConnection.PeerConnectionState newState);
740
741        void onRenegotiationNeeded();
742    }
743
744    public abstract static class SetSdpObserver implements SdpObserver {
745
746        @Override
747        public void onCreateSuccess(org.webrtc.SessionDescription sessionDescription) {
748            throw new IllegalStateException("Not able to use SetSdpObserver");
749        }
750
751        @Override
752        public void onCreateFailure(String s) {
753            throw new IllegalStateException("Not able to use SetSdpObserver");
754        }
755    }
756
757    static class InitializationException extends Exception {
758
759        private InitializationException(final String message, final Throwable throwable) {
760            super(message, throwable);
761        }
762
763        private InitializationException(final String message) {
764            super(message);
765        }
766    }
767
768    public static class PeerConnectionNotInitialized extends IllegalStateException {
769
770        public PeerConnectionNotInitialized() {
771            super("initialize PeerConnection first");
772        }
773    }
774
775    public static class FailureToSetDescriptionException extends IllegalArgumentException {
776        public FailureToSetDescriptionException(String message) {
777            super(message);
778        }
779    }
780}