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