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