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