WebRTCWrapper.java

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