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