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 setMicrophoneEnabled(final boolean enabled) {
542 Optional<AudioTrack> audioTrack = null;
543 try {
544 audioTrack = TrackWrapper.get(peerConnection, this.localAudioTrack);
545 } catch (final IllegalStateException e) {
546 Log.d(Config.LOGTAG, "unable to toggle microphone", e);
547 // ignoring race condition in case sender has been disposed
548 return false;
549 }
550 if (audioTrack.isPresent()) {
551 try {
552 audioTrack.get().setEnabled(enabled);
553 return true;
554 } catch (final IllegalStateException e) {
555 Log.d(Config.LOGTAG, "unable to toggle microphone", e);
556 // ignoring race condition in case MediaStreamTrack has been disposed
557 return false;
558 }
559 } else {
560 throw new IllegalStateException("Local audio track does not exist (yet)");
561 }
562 }
563
564 boolean isVideoEnabled() {
565 final Optional<VideoTrack> videoTrack =
566 TrackWrapper.get(peerConnection, this.localVideoTrack);
567 if (videoTrack.isPresent()) {
568 return videoTrack.get().enabled();
569 }
570 return false;
571 }
572
573 void setVideoEnabled(final boolean enabled) {
574 final Optional<VideoTrack> videoTrack =
575 TrackWrapper.get(peerConnection, this.localVideoTrack);
576 if (videoTrack.isPresent()) {
577 videoTrack.get().setEnabled(enabled);
578 return;
579 }
580 throw new IllegalStateException("Local video track does not exist");
581 }
582
583 synchronized ListenableFuture<SessionDescription> setLocalDescription(
584 final boolean waitForCandidates) {
585 this.setIsReadyToReceiveIceCandidates(false);
586 return Futures.transformAsync(
587 getPeerConnectionFuture(),
588 peerConnection -> {
589 if (peerConnection == null) {
590 return Futures.immediateFailedFuture(
591 new IllegalStateException("PeerConnection was null"));
592 }
593 final SettableFuture<SessionDescription> future = SettableFuture.create();
594 peerConnection.setLocalDescription(
595 new SetSdpObserver() {
596 @Override
597 public void onSetSuccess() {
598 if (waitForCandidates) {
599 final var delay = getIceGatheringCompleteOrTimeout();
600 final var delayedSessionDescription =
601 Futures.transformAsync(
602 delay,
603 v -> {
604 iceCandidates.clear();
605 return getLocalDescriptionFuture();
606 },
607 MoreExecutors.directExecutor());
608 future.setFuture(delayedSessionDescription);
609 } else {
610 future.setFuture(getLocalDescriptionFuture());
611 }
612 }
613
614 @Override
615 public void onSetFailure(final String message) {
616 future.setException(
617 new FailureToSetDescriptionException(message));
618 }
619 });
620 return future;
621 },
622 MoreExecutors.directExecutor());
623 }
624
625 private ListenableFuture<Void> getIceGatheringCompleteOrTimeout() {
626 return Futures.catching(
627 Futures.withTimeout(
628 iceGatheringComplete,
629 2,
630 TimeUnit.SECONDS,
631 JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE),
632 TimeoutException.class,
633 ex -> {
634 Log.d(
635 EXTENDED_LOGGING_TAG,
636 "timeout while waiting for ICE gathering to complete");
637 return null;
638 },
639 MoreExecutors.directExecutor());
640 }
641
642 private ListenableFuture<SessionDescription> getLocalDescriptionFuture() {
643 return Futures.submit(
644 () -> {
645 final SessionDescription description =
646 requirePeerConnection().getLocalDescription();
647 Log.d(EXTENDED_LOGGING_TAG, "local description:");
648 logDescription(description);
649 return description;
650 },
651 localDescriptionExecutorService);
652 }
653
654 public static void logDescription(final SessionDescription sessionDescription) {
655 for (final String line :
656 sessionDescription.description.split(
657 eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) {
658 Log.d(EXTENDED_LOGGING_TAG, line);
659 }
660 }
661
662 synchronized ListenableFuture<Void> setRemoteDescription(
663 final SessionDescription sessionDescription) {
664 Log.d(EXTENDED_LOGGING_TAG, "setting remote description:");
665 logDescription(sessionDescription);
666 return Futures.transformAsync(
667 getPeerConnectionFuture(),
668 peerConnection -> {
669 if (peerConnection == null) {
670 return Futures.immediateFailedFuture(
671 new IllegalStateException("PeerConnection was null"));
672 }
673 final SettableFuture<Void> future = SettableFuture.create();
674 peerConnection.setRemoteDescription(
675 new SetSdpObserver() {
676 @Override
677 public void onSetSuccess() {
678 future.set(null);
679 }
680
681 @Override
682 public void onSetFailure(final String message) {
683 future.setException(
684 new FailureToSetDescriptionException(message));
685 }
686 },
687 sessionDescription);
688 return future;
689 },
690 MoreExecutors.directExecutor());
691 }
692
693 @Nonnull
694 private ListenableFuture<PeerConnection> getPeerConnectionFuture() {
695 final PeerConnection peerConnection = this.peerConnection;
696 if (peerConnection == null) {
697 return Futures.immediateFailedFuture(new PeerConnectionNotInitialized());
698 } else {
699 return Futures.immediateFuture(peerConnection);
700 }
701 }
702
703 @Nonnull
704 private PeerConnection requirePeerConnection() {
705 final PeerConnection peerConnection = this.peerConnection;
706 if (peerConnection == null) {
707 throw new PeerConnectionNotInitialized();
708 }
709 return peerConnection;
710 }
711
712 public boolean applyDtmfTone(String tone) {
713 localAudioTrack.rtpSender.dtmf().insertDtmf(tone, TONE_DURATION, 100);
714 final var toneGenerator =
715 new ToneGenerator(AudioManager.STREAM_VOICE_CALL, DEFAULT_TONE_VOLUME);
716 toneGenerator.startTone(TONE_CODES.get(tone), TONE_DURATION);
717 return true;
718 }
719
720 @Nonnull
721 private PeerConnectionFactory requirePeerConnectionFactory() {
722 final PeerConnectionFactory peerConnectionFactory = this.peerConnectionFactory;
723 if (peerConnectionFactory == null) {
724 throw new IllegalStateException("Make sure PeerConnectionFactory is initialized");
725 }
726 return peerConnectionFactory;
727 }
728
729 void addIceCandidate(IceCandidate iceCandidate) {
730 requirePeerConnection().addIceCandidate(iceCandidate);
731 }
732
733 PeerConnection.PeerConnectionState getState() {
734 return requirePeerConnection().connectionState();
735 }
736
737 public PeerConnection.SignalingState getSignalingState() {
738 try {
739 return requirePeerConnection().signalingState();
740 } catch (final IllegalStateException e) {
741 return PeerConnection.SignalingState.CLOSED;
742 }
743 }
744
745 EglBase.Context getEglBaseContext() {
746 return this.eglBase.getEglBaseContext();
747 }
748
749 Optional<VideoTrack> getLocalVideoTrack() {
750 try {
751 return TrackWrapper.get(peerConnection, this.localVideoTrack);
752 } catch (IllegalStateException e) {
753 return Optional.absent();
754 }
755 }
756
757 Optional<VideoTrack> getRemoteVideoTrack() {
758 return Optional.fromNullable(this.remoteVideoTrack);
759 }
760
761 private Context requireContext() {
762 final Context context = this.context;
763 if (context == null) {
764 throw new IllegalStateException("call setup first");
765 }
766 return context;
767 }
768
769 void execute(final Runnable command) {
770 this.executorService.execute(command);
771 }
772
773 public interface EventCallback {
774 void onIceCandidate(IceCandidate iceCandidate);
775
776 void onConnectionChange(PeerConnection.PeerConnectionState newState);
777
778 void onRenegotiationNeeded();
779 }
780
781 public abstract static class SetSdpObserver implements SdpObserver {
782
783 @Override
784 public void onCreateSuccess(org.webrtc.SessionDescription sessionDescription) {
785 throw new IllegalStateException("Not able to use SetSdpObserver");
786 }
787
788 @Override
789 public void onCreateFailure(String s) {
790 throw new IllegalStateException("Not able to use SetSdpObserver");
791 }
792 }
793
794 static class InitializationException extends Exception {
795
796 private InitializationException(final String message, final Throwable throwable) {
797 super(message, throwable);
798 }
799
800 private InitializationException(final String message) {
801 super(message);
802 }
803 }
804
805 public static class PeerConnectionNotInitialized extends IllegalStateException {
806
807 public PeerConnectionNotInitialized() {
808 super("initialize PeerConnection first");
809 }
810 }
811
812 public static class FailureToSetDescriptionException extends IllegalArgumentException {
813 public FailureToSetDescriptionException(String message) {
814 super(message);
815 }
816 }
817}