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