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