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