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