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