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