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