WebRTCWrapper.java

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