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}