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