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