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