WebRTCWrapper.java

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