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}