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