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.Collection;
45import java.util.LinkedList;
46import java.util.List;
47import java.util.Queue;
48import java.util.Map;
49import java.util.Set;
50import java.util.concurrent.ExecutorService;
51import java.util.concurrent.Executors;
52import java.util.concurrent.atomic.AtomicBoolean;
53
54import javax.annotation.Nonnull;
55import javax.annotation.Nullable;
56
57@SuppressWarnings("UnstableApiUsage")
58public class WebRTCWrapper {
59
60 private static final String EXTENDED_LOGGING_TAG = WebRTCWrapper.class.getSimpleName();
61
62 private final ExecutorService executorService = Executors.newSingleThreadExecutor();
63
64 private static final int TONE_DURATION = 500;
65 private static final Map<String,Integer> TONE_CODES;
66 static {
67 ImmutableMap.Builder<String,Integer> builder = new ImmutableMap.Builder<>();
68 builder.put("0", ToneGenerator.TONE_DTMF_0);
69 builder.put("1", ToneGenerator.TONE_DTMF_1);
70 builder.put("2", ToneGenerator.TONE_DTMF_2);
71 builder.put("3", ToneGenerator.TONE_DTMF_3);
72 builder.put("4", ToneGenerator.TONE_DTMF_4);
73 builder.put("5", ToneGenerator.TONE_DTMF_5);
74 builder.put("6", ToneGenerator.TONE_DTMF_6);
75 builder.put("7", ToneGenerator.TONE_DTMF_7);
76 builder.put("8", ToneGenerator.TONE_DTMF_8);
77 builder.put("9", ToneGenerator.TONE_DTMF_9);
78 builder.put("*", ToneGenerator.TONE_DTMF_S);
79 builder.put("#", ToneGenerator.TONE_DTMF_P);
80 TONE_CODES = builder.build();
81 }
82 private final ExecutorService localDescriptionExecutorService =
83 Executors.newSingleThreadExecutor();
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("MI 5")
99 .add("GT-I9515") // Samsung Galaxy S4 Value Edition (jfvelte)
100 .add("GT-I9515L") // Samsung Galaxy S4 Value Edition (jfvelte)
101 .add("GT-I9505") // Samsung Galaxy S4 (jfltexx)
102 .build();
103
104 private final EventCallback eventCallback;
105 private final AtomicBoolean readyToReceivedIceCandidates = new AtomicBoolean(false);
106 private final AtomicBoolean rfc3264 = new AtomicBoolean(false);
107 private final Queue<IceCandidate> iceCandidates = new LinkedList<>();
108 private final AppRTCAudioManager.AudioManagerEvents audioManagerEvents =
109 new AppRTCAudioManager.AudioManagerEvents() {
110 @Override
111 public void onAudioDeviceChanged(
112 AppRTCAudioManager.AudioDevice selectedAudioDevice,
113 Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
114 eventCallback.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices);
115 }
116 };
117 private final Handler mainHandler = new Handler(Looper.getMainLooper());
118 private TrackWrapper<AudioTrack> localAudioTrack = null;
119 private TrackWrapper<VideoTrack> localVideoTrack = null;
120 private VideoTrack remoteVideoTrack = null;
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 PeerConnection.IceGatheringState iceGatheringState) {
156 Log.d(EXTENDED_LOGGING_TAG, "onIceGatheringChange(" + iceGatheringState + ")");
157 if (iceGatheringState == PeerConnection.IceGatheringState.COMPLETE) {
158 execute(() -> eventCallback.onIceGatheringComplete(iceCandidates));
159 }
160 }
161
162 @Override
163 public void onIceCandidate(IceCandidate iceCandidate) {
164 if (readyToReceivedIceCandidates.get() && !rfc3264.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 AppRTCAudioManager appRTCAudioManager = null;
238 private ToneManager toneManager = null;
239 private Context context = null;
240 private EglBase eglBase = null;
241 private VideoSourceWrapper videoSourceWrapper;
242
243 WebRTCWrapper(final EventCallback eventCallback) {
244 this.eventCallback = eventCallback;
245 }
246
247 private static void dispose(final PeerConnection peerConnection) {
248 try {
249 peerConnection.dispose();
250 } catch (final IllegalStateException e) {
251 Log.e(Config.LOGTAG, "unable to dispose of peer connection", e);
252 }
253 }
254
255 public void setup(
256 final XmppConnectionService service,
257 @Nonnull final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference)
258 throws InitializationException {
259 try {
260 PeerConnectionFactory.initialize(
261 PeerConnectionFactory.InitializationOptions.builder(service)
262 .setFieldTrials("WebRTC-BindUsingInterfaceName/Enabled/")
263 .createInitializationOptions());
264 } catch (final UnsatisfiedLinkError e) {
265 throw new InitializationException("Unable to initialize PeerConnectionFactory", e);
266 }
267 try {
268 this.eglBase = EglBase.create();
269 } catch (final RuntimeException e) {
270 throw new InitializationException("Unable to create EGL base", e);
271 }
272 this.context = service;
273 this.toneManager = service.getJingleConnectionManager().toneManager;
274 mainHandler.post(
275 () -> {
276 appRTCAudioManager = AppRTCAudioManager.create(service, speakerPhonePreference);
277 toneManager.setAppRtcAudioManagerHasControl(true);
278 appRTCAudioManager.start(audioManagerEvents);
279 eventCallback.onAudioDeviceChanged(
280 appRTCAudioManager.getSelectedAudioDevice(),
281 appRTCAudioManager.getAudioDevices());
282 });
283 }
284
285 synchronized void initializePeerConnection(
286 final Set<Media> media, final List<PeerConnection.IceServer> iceServers)
287 throws InitializationException {
288 Preconditions.checkState(this.eglBase != null);
289 Preconditions.checkNotNull(media);
290 Preconditions.checkArgument(
291 media.size() > 0, "media can not be empty when initializing peer connection");
292 final boolean setUseHardwareAcousticEchoCanceler =
293 !HARDWARE_AEC_BLACKLIST.contains(Build.MODEL);
294 Log.d(
295 Config.LOGTAG,
296 String.format(
297 "setUseHardwareAcousticEchoCanceler(%s) model=%s",
298 setUseHardwareAcousticEchoCanceler, Build.MODEL));
299 this.peerConnectionFactory =
300 PeerConnectionFactory.builder()
301 .setVideoDecoderFactory(
302 new DefaultVideoDecoderFactory(eglBase.getEglBaseContext()))
303 .setVideoEncoderFactory(
304 new DefaultVideoEncoderFactory(
305 eglBase.getEglBaseContext(), true, true))
306 .setAudioDeviceModule(
307 JavaAudioDeviceModule.builder(requireContext())
308 .setUseHardwareAcousticEchoCanceler(
309 setUseHardwareAcousticEchoCanceler)
310 .createAudioDeviceModule())
311 .createPeerConnectionFactory();
312
313 final PeerConnection.RTCConfiguration rtcConfig = buildConfiguration(iceServers, rfc3264.get());
314 final PeerConnection peerConnection =
315 requirePeerConnectionFactory()
316 .createPeerConnection(rtcConfig, peerConnectionObserver);
317 if (peerConnection == null) {
318 throw new InitializationException("Unable to create PeerConnection");
319 }
320
321 if (media.contains(Media.VIDEO)) {
322 addVideoTrack(peerConnection);
323 }
324
325 if (media.contains(Media.AUDIO)) {
326 addAudioTrack(peerConnection);
327 }
328 peerConnection.setAudioPlayout(true);
329 peerConnection.setAudioRecording(true);
330
331 this.peerConnection = peerConnection;
332 }
333
334 private VideoSourceWrapper initializeVideoSourceWrapper() {
335 final VideoSourceWrapper existingVideoSourceWrapper = this.videoSourceWrapper;
336 if (existingVideoSourceWrapper != null) {
337 existingVideoSourceWrapper.startCapture();
338 return existingVideoSourceWrapper;
339 }
340 final VideoSourceWrapper videoSourceWrapper =
341 new VideoSourceWrapper.Factory(requireContext()).create();
342 if (videoSourceWrapper == null) {
343 throw new IllegalStateException("Could not instantiate VideoSourceWrapper");
344 }
345 videoSourceWrapper.initialize(
346 requirePeerConnectionFactory(), requireContext(), eglBase.getEglBaseContext());
347 videoSourceWrapper.startCapture();
348 this.videoSourceWrapper = videoSourceWrapper;
349 return videoSourceWrapper;
350 }
351
352 public synchronized boolean addTrack(final Media media) {
353 if (media == Media.VIDEO) {
354 return addVideoTrack(requirePeerConnection());
355 } else if (media == Media.AUDIO) {
356 return addAudioTrack(requirePeerConnection());
357 }
358 throw new IllegalStateException(String.format("Could not add track for %s", media));
359 }
360
361 public synchronized void removeTrack(final Media media) {
362 if (media == Media.VIDEO) {
363 removeVideoTrack(requirePeerConnection());
364 }
365 }
366
367 private boolean addAudioTrack(final PeerConnection peerConnection) {
368 final AudioSource audioSource =
369 requirePeerConnectionFactory().createAudioSource(new MediaConstraints());
370 final AudioTrack audioTrack =
371 requirePeerConnectionFactory()
372 .createAudioTrack(TrackWrapper.id(AudioTrack.class), audioSource);
373 this.localAudioTrack = TrackWrapper.addTrack(peerConnection, audioTrack);
374 return true;
375 }
376
377 private boolean addVideoTrack(final PeerConnection peerConnection) {
378 final TrackWrapper<VideoTrack> existing = this.localVideoTrack;
379 if (existing != null) {
380 final RtpTransceiver transceiver =
381 TrackWrapper.getTransceiver(peerConnection, existing);
382 if (transceiver == null) {
383 Log.w(EXTENDED_LOGGING_TAG, "unable to restart video transceiver");
384 return false;
385 }
386 transceiver.setDirection(RtpTransceiver.RtpTransceiverDirection.SEND_RECV);
387 this.videoSourceWrapper.startCapture();
388 return true;
389 }
390 final VideoSourceWrapper videoSourceWrapper;
391 try {
392 videoSourceWrapper = initializeVideoSourceWrapper();
393 } catch (final IllegalStateException e) {
394 Log.d(Config.LOGTAG, "could not add video track", e);
395 return false;
396 }
397 final VideoTrack videoTrack =
398 requirePeerConnectionFactory()
399 .createVideoTrack(
400 TrackWrapper.id(VideoTrack.class),
401 videoSourceWrapper.getVideoSource());
402 this.localVideoTrack = TrackWrapper.addTrack(peerConnection, videoTrack);
403 return true;
404 }
405
406 private void removeVideoTrack(final PeerConnection peerConnection) {
407 final TrackWrapper<VideoTrack> localVideoTrack = this.localVideoTrack;
408 if (localVideoTrack != null) {
409
410 final RtpTransceiver exactTransceiver =
411 TrackWrapper.getTransceiver(peerConnection, localVideoTrack);
412 if (exactTransceiver == null) {
413 throw new IllegalStateException();
414 }
415 exactTransceiver.setDirection(RtpTransceiver.RtpTransceiverDirection.INACTIVE);
416 }
417 final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper;
418 if (videoSourceWrapper != null) {
419 try {
420 videoSourceWrapper.stopCapture();
421 } catch (InterruptedException e) {
422 e.printStackTrace();
423 }
424 }
425 }
426
427 private static PeerConnection.RTCConfiguration buildConfiguration(
428 final List<PeerConnection.IceServer> iceServers, boolean rfc3264) {
429 final PeerConnection.RTCConfiguration rtcConfig =
430 new PeerConnection.RTCConfiguration(iceServers);
431 rtcConfig.tcpCandidatePolicy =
432 PeerConnection.TcpCandidatePolicy.DISABLED; // XEP-0176 doesn't support tcp
433 if (rfc3264) {
434 rtcConfig.continualGatheringPolicy =
435 PeerConnection.ContinualGatheringPolicy.GATHER_ONCE;
436 } else {
437 rtcConfig.continualGatheringPolicy =
438 PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY;
439 }
440 rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
441 rtcConfig.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.NEGOTIATE;
442 rtcConfig.enableImplicitRollback = true;
443 return rtcConfig;
444 }
445
446 void reconfigurePeerConnection(final List<PeerConnection.IceServer> iceServers) {
447 requirePeerConnection().setConfiguration(buildConfiguration(iceServers, rfc3264.get()));
448 }
449
450 void restartIceAsync() {
451 this.execute(this::restartIce);
452 }
453
454 private void restartIce() {
455 final PeerConnection peerConnection;
456 try {
457 peerConnection = requirePeerConnection();
458 } catch (final PeerConnectionNotInitialized e) {
459 Log.w(EXTENDED_LOGGING_TAG, "PeerConnection vanished before we could execute restart");
460 return;
461 }
462 setIsReadyToReceiveIceCandidates(false);
463 peerConnection.restartIce();
464 }
465
466 public void setIsReadyToReceiveIceCandidates(final boolean ready) {
467 readyToReceivedIceCandidates.set(ready);
468 if (this.rfc3264.get()) return;
469 final int was = iceCandidates.size();
470 while (ready && iceCandidates.peek() != null) {
471 eventCallback.onIceCandidate(iceCandidates.poll());
472 }
473 final int is = iceCandidates.size();
474 Log.d(
475 EXTENDED_LOGGING_TAG,
476 "setIsReadyToReceiveCandidates(" + ready + ") was=" + was + " is=" + is);
477 }
478
479 public void setRFC3264(final boolean rfc3264) {
480 // When this feature is enabled, do not trickle candidates
481 this.rfc3264.set(rfc3264);
482 }
483
484 public boolean getRFC3264() {
485 return this.rfc3264.get();
486 }
487
488 synchronized void close() {
489 final PeerConnection peerConnection = this.peerConnection;
490 final PeerConnectionFactory peerConnectionFactory = this.peerConnectionFactory;
491 final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper;
492 final AppRTCAudioManager audioManager = this.appRTCAudioManager;
493 final EglBase eglBase = this.eglBase;
494 if (peerConnection != null) {
495 this.peerConnection = null;
496 dispose(peerConnection);
497 }
498 if (audioManager != null) {
499 toneManager.setAppRtcAudioManagerHasControl(false);
500 mainHandler.post(audioManager::stop);
501 }
502 this.localVideoTrack = null;
503 this.remoteVideoTrack = null;
504 if (videoSourceWrapper != null) {
505 this.videoSourceWrapper = null;
506 try {
507 videoSourceWrapper.stopCapture();
508 } catch (final InterruptedException e) {
509 Log.e(Config.LOGTAG, "unable to stop capturing");
510 }
511 videoSourceWrapper.dispose();
512 }
513 if (eglBase != null) {
514 eglBase.release();
515 this.eglBase = null;
516 }
517 if (peerConnectionFactory != null) {
518 this.peerConnectionFactory = null;
519 peerConnectionFactory.dispose();
520 }
521 }
522
523 synchronized void verifyClosed() {
524 if (this.peerConnection != null
525 || this.eglBase != null
526 || this.localVideoTrack != null
527 || this.remoteVideoTrack != null) {
528 final IllegalStateException e =
529 new IllegalStateException("WebRTCWrapper hasn't been closed properly");
530 Log.e(Config.LOGTAG, "verifyClosed() failed. Going to throw", e);
531 throw e;
532 }
533 }
534
535 boolean isCameraSwitchable() {
536 final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper;
537 return videoSourceWrapper != null && videoSourceWrapper.isCameraSwitchable();
538 }
539
540 boolean isFrontCamera() {
541 final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper;
542 return videoSourceWrapper == null || videoSourceWrapper.isFrontCamera();
543 }
544
545 ListenableFuture<Boolean> switchCamera() {
546 final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper;
547 if (videoSourceWrapper == null) {
548 return Futures.immediateFailedFuture(
549 new IllegalStateException("VideoSourceWrapper has not been initialized"));
550 }
551 return videoSourceWrapper.switchCamera();
552 }
553
554 boolean isMicrophoneEnabled() {
555 Optional<AudioTrack> audioTrack = null;
556 try {
557 audioTrack = TrackWrapper.get(peerConnection, this.localAudioTrack);
558 } catch (final IllegalStateException e) {
559 Log.d(Config.LOGTAG, "unable to check microphone", e);
560 // ignoring race condition in case sender has been disposed
561 return false;
562 }
563 if (audioTrack.isPresent()) {
564 try {
565 return audioTrack.get().enabled();
566 } catch (final IllegalStateException e) {
567 // sometimes UI might still be rendering the buttons when a background thread has
568 // already ended the call
569 return false;
570 }
571 } else {
572 return false;
573 }
574 }
575
576 boolean setMicrophoneEnabled(final boolean enabled) {
577 Optional<AudioTrack> audioTrack = null;
578 try {
579 audioTrack = TrackWrapper.get(peerConnection, this.localAudioTrack);
580 } catch (final IllegalStateException e) {
581 Log.d(Config.LOGTAG, "unable to toggle microphone", e);
582 // ignoring race condition in case sender has been disposed
583 return false;
584 }
585 if (audioTrack.isPresent()) {
586 try {
587 audioTrack.get().setEnabled(enabled);
588 return true;
589 } catch (final IllegalStateException e) {
590 Log.d(Config.LOGTAG, "unable to toggle microphone", e);
591 // ignoring race condition in case MediaStreamTrack has been disposed
592 return false;
593 }
594 } else {
595 throw new IllegalStateException("Local audio track does not exist (yet)");
596 }
597 }
598
599 boolean isVideoEnabled() {
600 final Optional<VideoTrack> videoTrack =
601 TrackWrapper.get(peerConnection, this.localVideoTrack);
602 if (videoTrack.isPresent()) {
603 return videoTrack.get().enabled();
604 }
605 return false;
606 }
607
608 void setVideoEnabled(final boolean enabled) {
609 final Optional<VideoTrack> videoTrack =
610 TrackWrapper.get(peerConnection, this.localVideoTrack);
611 if (videoTrack.isPresent()) {
612 videoTrack.get().setEnabled(enabled);
613 return;
614 }
615 throw new IllegalStateException("Local video track does not exist");
616 }
617
618 synchronized SessionDescription getLocalDescription() {
619 return peerConnection.getLocalDescription();
620 }
621
622 synchronized ListenableFuture<SessionDescription> setLocalDescription() {
623 this.setIsReadyToReceiveIceCandidates(false);
624 return Futures.transformAsync(
625 getPeerConnectionFuture(),
626 peerConnection -> {
627 if (peerConnection == null) {
628 return Futures.immediateFailedFuture(
629 new IllegalStateException("PeerConnection was null"));
630 }
631 final SettableFuture<SessionDescription> future = SettableFuture.create();
632 peerConnection.setLocalDescription(
633 new SetSdpObserver() {
634 @Override
635 public void onSetSuccess() {
636 future.setFuture(getLocalDescriptionFuture());
637 }
638
639 @Override
640 public void onSetFailure(final String message) {
641 future.setException(
642 new FailureToSetDescriptionException(message));
643 }
644 });
645 return future;
646 },
647 MoreExecutors.directExecutor());
648 }
649
650 private ListenableFuture<SessionDescription> getLocalDescriptionFuture() {
651 return Futures.submit(
652 () -> {
653 final SessionDescription description =
654 requirePeerConnection().getLocalDescription();
655 Log.d(EXTENDED_LOGGING_TAG, "local description:");
656 logDescription(description);
657 return description;
658 },
659 localDescriptionExecutorService);
660 }
661
662 public static void logDescription(final SessionDescription sessionDescription) {
663 for (final String line :
664 sessionDescription.description.split(
665 eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) {
666 Log.d(EXTENDED_LOGGING_TAG, line);
667 }
668 }
669
670 synchronized ListenableFuture<Void> setRemoteDescription(
671 final SessionDescription sessionDescription) {
672 Log.d(EXTENDED_LOGGING_TAG, "setting remote description:");
673 logDescription(sessionDescription);
674 return Futures.transformAsync(
675 getPeerConnectionFuture(),
676 peerConnection -> {
677 if (peerConnection == null) {
678 return Futures.immediateFailedFuture(
679 new IllegalStateException("PeerConnection was null"));
680 }
681 final SettableFuture<Void> future = SettableFuture.create();
682 peerConnection.setRemoteDescription(
683 new SetSdpObserver() {
684 @Override
685 public void onSetSuccess() {
686 future.set(null);
687 }
688
689 @Override
690 public void onSetFailure(final String message) {
691 future.setException(
692 new FailureToSetDescriptionException(message));
693 }
694 },
695 sessionDescription);
696 return future;
697 },
698 MoreExecutors.directExecutor());
699 }
700
701 @Nonnull
702 private ListenableFuture<PeerConnection> getPeerConnectionFuture() {
703 final PeerConnection peerConnection = this.peerConnection;
704 if (peerConnection == null) {
705 return Futures.immediateFailedFuture(new PeerConnectionNotInitialized());
706 } else {
707 return Futures.immediateFuture(peerConnection);
708 }
709 }
710
711 @Nonnull
712 private PeerConnection requirePeerConnection() {
713 final PeerConnection peerConnection = this.peerConnection;
714 if (peerConnection == null) {
715 throw new PeerConnectionNotInitialized();
716 }
717 return peerConnection;
718 }
719
720 public boolean applyDtmfTone(String tone) {
721 if (toneManager == null || peerConnection == null || localAudioTrack == null) {
722 return false;
723 }
724 localAudioTrack.rtpSender.dtmf().insertDtmf(tone, TONE_DURATION, 100);
725 toneManager.startTone(TONE_CODES.get(tone), TONE_DURATION);
726 return true;
727 }
728
729 @Nonnull
730 private PeerConnectionFactory requirePeerConnectionFactory() {
731 final PeerConnectionFactory peerConnectionFactory = this.peerConnectionFactory;
732 if (peerConnectionFactory == null) {
733 throw new IllegalStateException("Make sure PeerConnectionFactory is initialized");
734 }
735 return peerConnectionFactory;
736 }
737
738 void addIceCandidate(IceCandidate iceCandidate) {
739 requirePeerConnection().addIceCandidate(iceCandidate);
740 }
741
742 PeerConnection.PeerConnectionState getState() {
743 return requirePeerConnection().connectionState();
744 }
745
746 public PeerConnection.SignalingState getSignalingState() {
747 try {
748 return requirePeerConnection().signalingState();
749 } catch (final IllegalStateException e) {
750 return PeerConnection.SignalingState.CLOSED;
751 }
752 }
753
754 EglBase.Context getEglBaseContext() {
755 return this.eglBase.getEglBaseContext();
756 }
757
758 Optional<VideoTrack> getLocalVideoTrack() {
759 return TrackWrapper.get(peerConnection, this.localVideoTrack);
760 }
761
762 Optional<VideoTrack> getRemoteVideoTrack() {
763 return Optional.fromNullable(this.remoteVideoTrack);
764 }
765
766 private Context requireContext() {
767 final Context context = this.context;
768 if (context == null) {
769 throw new IllegalStateException("call setup first");
770 }
771 return context;
772 }
773
774 AppRTCAudioManager getAudioManager() {
775 return appRTCAudioManager;
776 }
777
778 void execute(final Runnable command) {
779 this.executorService.execute(command);
780 }
781
782 public void switchSpeakerPhonePreference(AppRTCAudioManager.SpeakerPhonePreference preference) {
783 mainHandler.post(() -> appRTCAudioManager.switchSpeakerPhonePreference(preference));
784 }
785
786 public interface EventCallback {
787 void onIceCandidate(IceCandidate iceCandidate);
788
789 void onConnectionChange(PeerConnection.PeerConnectionState newState);
790
791 void onAudioDeviceChanged(
792 AppRTCAudioManager.AudioDevice selectedAudioDevice,
793 Set<AppRTCAudioManager.AudioDevice> availableAudioDevices);
794
795 void onRenegotiationNeeded();
796
797 void onIceGatheringComplete(Collection<IceCandidate> iceCandidates);
798 }
799
800 private abstract static class SetSdpObserver implements SdpObserver {
801
802 @Override
803 public void onCreateSuccess(org.webrtc.SessionDescription sessionDescription) {
804 throw new IllegalStateException("Not able to use SetSdpObserver");
805 }
806
807 @Override
808 public void onCreateFailure(String s) {
809 throw new IllegalStateException("Not able to use SetSdpObserver");
810 }
811 }
812
813 static class InitializationException extends Exception {
814
815 private InitializationException(final String message, final Throwable throwable) {
816 super(message, throwable);
817 }
818
819 private InitializationException(final String message) {
820 super(message);
821 }
822 }
823
824 public static class PeerConnectionNotInitialized extends IllegalStateException {
825
826 private PeerConnectionNotInitialized() {
827 super("initialize PeerConnection first");
828 }
829 }
830
831 private static class FailureToSetDescriptionException extends IllegalArgumentException {
832 public FailureToSetDescriptionException(String message) {
833 super(message);
834 }
835 }
836}