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