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