1package eu.siacs.conversations.xmpp.jingle;
2
3import android.content.Context;
4import android.os.Build;
5import android.util.Log;
6import com.google.common.base.Optional;
7import com.google.common.base.Preconditions;
8import com.google.common.collect.ImmutableList;
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;
14import eu.siacs.conversations.Config;
15import eu.siacs.conversations.services.XmppConnectionService;
16import java.util.LinkedList;
17import java.util.Queue;
18import java.util.Set;
19import java.util.concurrent.ExecutorService;
20import java.util.concurrent.Executors;
21import java.util.concurrent.TimeUnit;
22import java.util.concurrent.TimeoutException;
23import java.util.concurrent.atomic.AtomicBoolean;
24import javax.annotation.Nonnull;
25import javax.annotation.Nullable;
26import org.webrtc.AudioSource;
27import org.webrtc.AudioTrack;
28import org.webrtc.CandidatePairChangeEvent;
29import org.webrtc.DataChannel;
30import org.webrtc.DefaultVideoDecoderFactory;
31import org.webrtc.DefaultVideoEncoderFactory;
32import org.webrtc.EglBase;
33import org.webrtc.IceCandidate;
34import org.webrtc.MediaConstraints;
35import org.webrtc.MediaStream;
36import org.webrtc.MediaStreamTrack;
37import org.webrtc.PeerConnection;
38import org.webrtc.PeerConnectionFactory;
39import org.webrtc.RtpReceiver;
40import org.webrtc.RtpTransceiver;
41import org.webrtc.SdpObserver;
42import org.webrtc.SessionDescription;
43import org.webrtc.VideoTrack;
44import org.webrtc.audio.JavaAudioDeviceModule;
45
46public class WebRTCWrapper {
47
48 private static final String EXTENDED_LOGGING_TAG = WebRTCWrapper.class.getSimpleName();
49
50 private final ExecutorService executorService = Executors.newSingleThreadExecutor();
51 private final ExecutorService localDescriptionExecutorService =
52 Executors.newSingleThreadExecutor();
53
54 private static final Set<String> HARDWARE_AEC_BLACKLIST =
55 new ImmutableSet.Builder<String>()
56 .add("Pixel")
57 .add("Pixel XL")
58 .add("Moto G5")
59 .add("Moto G (5S) Plus")
60 .add("Moto G4")
61 .add("TA-1053")
62 .add("Mi A1")
63 .add("Mi A2")
64 .add("E5823") // Sony z5 compact
65 .add("Redmi Note 5")
66 .add("FP2") // Fairphone FP2
67 .add("FP4") // Fairphone FP4
68 .add("MI 5")
69 .add("GT-I9515") // Samsung Galaxy S4 Value Edition (jfvelte)
70 .add("GT-I9515L") // Samsung Galaxy S4 Value Edition (jfvelte)
71 .add("GT-I9505") // Samsung Galaxy S4 (jfltexx)
72 .build();
73
74 private final EventCallback eventCallback;
75 private final AtomicBoolean readyToReceivedIceCandidates = new AtomicBoolean(false);
76 private final Queue<IceCandidate> iceCandidates = new LinkedList<>();
77 private TrackWrapper<AudioTrack> localAudioTrack = null;
78 private TrackWrapper<VideoTrack> localVideoTrack = null;
79 private VideoTrack remoteVideoTrack = null;
80
81 private final SettableFuture<Void> iceGatheringComplete = SettableFuture.create();
82 private final PeerConnection.Observer peerConnectionObserver =
83 new PeerConnection.Observer() {
84 @Override
85 public void onSignalingChange(PeerConnection.SignalingState signalingState) {
86 Log.d(EXTENDED_LOGGING_TAG, "onSignalingChange(" + signalingState + ")");
87 // this is called after removeTrack or addTrack
88 // and should then trigger a content-add or content-remove or something
89 // https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/removeTrack
90 }
91
92 @Override
93 public void onConnectionChange(final PeerConnection.PeerConnectionState newState) {
94 eventCallback.onConnectionChange(newState);
95 }
96
97 @Override
98 public void onIceConnectionChange(
99 PeerConnection.IceConnectionState iceConnectionState) {
100 Log.d(
101 EXTENDED_LOGGING_TAG,
102 "onIceConnectionChange(" + iceConnectionState + ")");
103 }
104
105 @Override
106 public void onSelectedCandidatePairChanged(CandidatePairChangeEvent event) {
107 Log.d(Config.LOGTAG, "remote candidate selected: " + event.remote);
108 Log.d(Config.LOGTAG, "local candidate selected: " + event.local);
109 }
110
111 @Override
112 public void onIceConnectionReceivingChange(boolean b) {}
113
114 @Override
115 public void onIceGatheringChange(
116 final PeerConnection.IceGatheringState iceGatheringState) {
117 Log.d(EXTENDED_LOGGING_TAG, "onIceGatheringChange(" + iceGatheringState + ")");
118 if (iceGatheringState == PeerConnection.IceGatheringState.COMPLETE) {
119 iceGatheringComplete.set(null);
120 }
121 }
122
123 @Override
124 public void onIceCandidate(IceCandidate iceCandidate) {
125 if (readyToReceivedIceCandidates.get()) {
126 eventCallback.onIceCandidate(iceCandidate);
127 } else {
128 iceCandidates.add(iceCandidate);
129 }
130 }
131
132 @Override
133 public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) {}
134
135 @Override
136 public void onAddStream(MediaStream mediaStream) {
137 Log.d(
138 EXTENDED_LOGGING_TAG,
139 "onAddStream(numAudioTracks="
140 + mediaStream.audioTracks.size()
141 + ",numVideoTracks="
142 + mediaStream.videoTracks.size()
143 + ")");
144 }
145
146 @Override
147 public void onRemoveStream(MediaStream mediaStream) {}
148
149 @Override
150 public void onDataChannel(DataChannel dataChannel) {}
151
152 @Override
153 public void onRenegotiationNeeded() {
154 Log.d(EXTENDED_LOGGING_TAG, "onRenegotiationNeeded()");
155 final PeerConnection.PeerConnectionState currentState =
156 peerConnection == null ? null : peerConnection.connectionState();
157 if (currentState != null
158 && currentState != PeerConnection.PeerConnectionState.NEW) {
159 eventCallback.onRenegotiationNeeded();
160 }
161 }
162
163 @Override
164 public void onAddTrack(RtpReceiver rtpReceiver, MediaStream[] mediaStreams) {
165 final MediaStreamTrack track = rtpReceiver.track();
166 Log.d(
167 EXTENDED_LOGGING_TAG,
168 "onAddTrack(kind="
169 + (track == null ? "null" : track.kind())
170 + ",numMediaStreams="
171 + mediaStreams.length
172 + ")");
173 if (track instanceof VideoTrack) {
174 remoteVideoTrack = (VideoTrack) track;
175 }
176 }
177
178 @Override
179 public void onTrack(final RtpTransceiver transceiver) {
180 Log.d(
181 EXTENDED_LOGGING_TAG,
182 "onTrack(mid="
183 + transceiver.getMid()
184 + ",media="
185 + transceiver.getMediaType()
186 + ",direction="
187 + transceiver.getDirection()
188 + ")");
189 }
190
191 @Override
192 public void onRemoveTrack(final RtpReceiver receiver) {
193 Log.d(EXTENDED_LOGGING_TAG, "onRemoveTrack(" + receiver.id() + ")");
194 }
195 };
196 @Nullable private PeerConnectionFactory peerConnectionFactory = null;
197 @Nullable private PeerConnection peerConnection = null;
198 private Context context = null;
199 private EglBase eglBase = null;
200 private VideoSourceWrapper videoSourceWrapper;
201
202 WebRTCWrapper(final EventCallback eventCallback) {
203 this.eventCallback = eventCallback;
204 }
205
206 private static void dispose(final PeerConnection peerConnection) {
207 try {
208 peerConnection.dispose();
209 } catch (final IllegalStateException e) {
210 Log.e(Config.LOGTAG, "unable to dispose of peer connection", e);
211 }
212 }
213
214 public void setup(final XmppConnectionService service) throws InitializationException {
215 try {
216 PeerConnectionFactory.initialize(
217 PeerConnectionFactory.InitializationOptions.builder(service)
218 .setFieldTrials("WebRTC-BindUsingInterfaceName/Enabled/")
219 .createInitializationOptions());
220 } catch (final UnsatisfiedLinkError e) {
221 throw new InitializationException("Unable to initialize PeerConnectionFactory", e);
222 }
223 try {
224 this.eglBase = EglBase.create();
225 } catch (final RuntimeException e) {
226 throw new InitializationException("Unable to create EGL base", e);
227 }
228 this.context = service;
229 }
230
231 synchronized void initializePeerConnection(
232 final Set<Media> media,
233 final Set<PeerConnection.IceServer> iceServers,
234 final boolean trickle)
235 throws InitializationException {
236 Preconditions.checkState(this.eglBase != null);
237 Preconditions.checkNotNull(media);
238 Preconditions.checkArgument(
239 !media.isEmpty(), "media can not be empty when initializing peer connection");
240 final boolean setUseHardwareAcousticEchoCanceler =
241 !HARDWARE_AEC_BLACKLIST.contains(Build.MODEL);
242 Log.d(
243 Config.LOGTAG,
244 String.format(
245 "setUseHardwareAcousticEchoCanceler(%s) model=%s",
246 setUseHardwareAcousticEchoCanceler, Build.MODEL));
247 this.peerConnectionFactory =
248 PeerConnectionFactory.builder()
249 .setVideoDecoderFactory(
250 new DefaultVideoDecoderFactory(eglBase.getEglBaseContext()))
251 .setVideoEncoderFactory(
252 new DefaultVideoEncoderFactory(
253 eglBase.getEglBaseContext(), true, true))
254 .setAudioDeviceModule(
255 JavaAudioDeviceModule.builder(requireContext())
256 .setUseHardwareAcousticEchoCanceler(
257 setUseHardwareAcousticEchoCanceler)
258 .createAudioDeviceModule())
259 .createPeerConnectionFactory();
260
261 final PeerConnection.RTCConfiguration rtcConfig = buildConfiguration(iceServers, trickle);
262 final PeerConnection peerConnection =
263 requirePeerConnectionFactory()
264 .createPeerConnection(rtcConfig, peerConnectionObserver);
265 if (peerConnection == null) {
266 throw new InitializationException("Unable to create PeerConnection");
267 }
268
269 if (media.contains(Media.VIDEO)) {
270 addVideoTrack(peerConnection);
271 }
272
273 if (media.contains(Media.AUDIO)) {
274 addAudioTrack(peerConnection);
275 }
276 peerConnection.setAudioPlayout(true);
277 peerConnection.setAudioRecording(true);
278
279 this.peerConnection = peerConnection;
280 }
281
282 private VideoSourceWrapper initializeVideoSourceWrapper() {
283 final VideoSourceWrapper existingVideoSourceWrapper = this.videoSourceWrapper;
284 if (existingVideoSourceWrapper != null) {
285 existingVideoSourceWrapper.startCapture();
286 return existingVideoSourceWrapper;
287 }
288 final VideoSourceWrapper videoSourceWrapper =
289 new VideoSourceWrapper.Factory(requireContext()).create();
290 if (videoSourceWrapper == null) {
291 throw new IllegalStateException("Could not instantiate VideoSourceWrapper");
292 }
293 videoSourceWrapper.initialize(
294 requirePeerConnectionFactory(), requireContext(), eglBase.getEglBaseContext());
295 videoSourceWrapper.startCapture();
296 this.videoSourceWrapper = videoSourceWrapper;
297 return videoSourceWrapper;
298 }
299
300 public synchronized boolean addTrack(final Media media) {
301 if (media == Media.VIDEO) {
302 return addVideoTrack(requirePeerConnection());
303 } else if (media == Media.AUDIO) {
304 return addAudioTrack(requirePeerConnection());
305 }
306 throw new IllegalStateException(String.format("Could not add track for %s", media));
307 }
308
309 public synchronized void removeTrack(final Media media) {
310 if (media == Media.VIDEO) {
311 removeVideoTrack(requirePeerConnection());
312 }
313 }
314
315 private boolean addAudioTrack(final PeerConnection peerConnection) {
316 final AudioSource audioSource =
317 requirePeerConnectionFactory().createAudioSource(new MediaConstraints());
318 final AudioTrack audioTrack =
319 requirePeerConnectionFactory()
320 .createAudioTrack(TrackWrapper.id(AudioTrack.class), audioSource);
321 this.localAudioTrack = TrackWrapper.addTrack(peerConnection, audioTrack);
322 return true;
323 }
324
325 private boolean addVideoTrack(final PeerConnection peerConnection) {
326 final TrackWrapper<VideoTrack> existing = this.localVideoTrack;
327 if (existing != null) {
328 final RtpTransceiver transceiver =
329 TrackWrapper.getTransceiver(peerConnection, existing);
330 if (transceiver == null) {
331 Log.w(EXTENDED_LOGGING_TAG, "unable to restart video transceiver");
332 return false;
333 }
334 transceiver.setDirection(RtpTransceiver.RtpTransceiverDirection.SEND_RECV);
335 this.videoSourceWrapper.startCapture();
336 return true;
337 }
338 final VideoSourceWrapper videoSourceWrapper;
339 try {
340 videoSourceWrapper = initializeVideoSourceWrapper();
341 } catch (final IllegalStateException e) {
342 Log.d(Config.LOGTAG, "could not add video track", e);
343 return false;
344 }
345 final VideoTrack videoTrack =
346 requirePeerConnectionFactory()
347 .createVideoTrack(
348 TrackWrapper.id(VideoTrack.class),
349 videoSourceWrapper.getVideoSource());
350 this.localVideoTrack = TrackWrapper.addTrack(peerConnection, videoTrack);
351 return true;
352 }
353
354 private void removeVideoTrack(final PeerConnection peerConnection) {
355 final TrackWrapper<VideoTrack> localVideoTrack = this.localVideoTrack;
356 if (localVideoTrack != null) {
357
358 final RtpTransceiver exactTransceiver =
359 TrackWrapper.getTransceiver(peerConnection, localVideoTrack);
360 if (exactTransceiver == null) {
361 throw new IllegalStateException();
362 }
363 exactTransceiver.setDirection(RtpTransceiver.RtpTransceiverDirection.INACTIVE);
364 }
365 final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper;
366 if (videoSourceWrapper != null) {
367 try {
368 videoSourceWrapper.stopCapture();
369 } catch (final InterruptedException e) {
370 Log.e(Config.LOGTAG, "could not stop capturing video source", e);
371 }
372 }
373 }
374
375 public static PeerConnection.RTCConfiguration buildConfiguration(
376 final Set<PeerConnection.IceServer> iceServers, final boolean trickle) {
377 final PeerConnection.RTCConfiguration rtcConfig =
378 new PeerConnection.RTCConfiguration(ImmutableList.copyOf(iceServers));
379 rtcConfig.tcpCandidatePolicy =
380 PeerConnection.TcpCandidatePolicy.DISABLED; // XEP-0176 doesn't support tcp
381 if (trickle) {
382 rtcConfig.continualGatheringPolicy =
383 PeerConnection.ContinualGatheringPolicy.GATHER_CONTINUALLY;
384 } else {
385 rtcConfig.continualGatheringPolicy =
386 PeerConnection.ContinualGatheringPolicy.GATHER_ONCE;
387 }
388 rtcConfig.sdpSemantics = PeerConnection.SdpSemantics.UNIFIED_PLAN;
389 rtcConfig.rtcpMuxPolicy = PeerConnection.RtcpMuxPolicy.NEGOTIATE;
390 rtcConfig.enableImplicitRollback = true;
391 return rtcConfig;
392 }
393
394 void reconfigurePeerConnection(
395 final Set<PeerConnection.IceServer> iceServers, final boolean trickle) {
396 requirePeerConnection().setConfiguration(buildConfiguration(iceServers, trickle));
397 }
398
399 void restartIceAsync() {
400 this.execute(this::restartIce);
401 }
402
403 private void restartIce() {
404 final PeerConnection peerConnection;
405 try {
406 peerConnection = requirePeerConnection();
407 } catch (final PeerConnectionNotInitialized e) {
408 Log.w(EXTENDED_LOGGING_TAG, "PeerConnection vanished before we could execute restart");
409 return;
410 }
411 setIsReadyToReceiveIceCandidates(false);
412 peerConnection.restartIce();
413 }
414
415 public void setIsReadyToReceiveIceCandidates(final boolean ready) {
416 readyToReceivedIceCandidates.set(ready);
417 final int was = iceCandidates.size();
418 while (ready && iceCandidates.peek() != null) {
419 eventCallback.onIceCandidate(iceCandidates.poll());
420 }
421 final int is = iceCandidates.size();
422 Log.d(
423 EXTENDED_LOGGING_TAG,
424 "setIsReadyToReceiveCandidates(" + ready + ") was=" + was + " is=" + is);
425 }
426
427 synchronized void close() {
428 final PeerConnection peerConnection = this.peerConnection;
429 final PeerConnectionFactory peerConnectionFactory = this.peerConnectionFactory;
430 final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper;
431 final EglBase eglBase = this.eglBase;
432 if (peerConnection != null) {
433 this.peerConnection = null;
434 dispose(peerConnection);
435 }
436 this.localVideoTrack = null;
437 this.remoteVideoTrack = null;
438 if (videoSourceWrapper != null) {
439 this.videoSourceWrapper = null;
440 try {
441 videoSourceWrapper.stopCapture();
442 } catch (final InterruptedException e) {
443 Log.e(Config.LOGTAG, "unable to stop capturing");
444 }
445 videoSourceWrapper.dispose();
446 }
447 if (eglBase != null) {
448 eglBase.release();
449 this.eglBase = null;
450 }
451 if (peerConnectionFactory != null) {
452 this.peerConnectionFactory = null;
453 peerConnectionFactory.dispose();
454 }
455 }
456
457 synchronized void verifyClosed() {
458 if (this.peerConnection != null
459 || this.eglBase != null
460 || this.localVideoTrack != null
461 || this.remoteVideoTrack != null) {
462 final AssertionError e =
463 new AssertionError("WebRTCWrapper hasn't been closed properly");
464 Log.e(Config.LOGTAG, "verifyClosed() failed. Going to throw", e);
465 throw e;
466 }
467 }
468
469 boolean isCameraSwitchable() {
470 final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper;
471 return videoSourceWrapper != null && videoSourceWrapper.isCameraSwitchable();
472 }
473
474 boolean isFrontCamera() {
475 final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper;
476 return videoSourceWrapper == null || videoSourceWrapper.isFrontCamera();
477 }
478
479 ListenableFuture<Boolean> switchCamera() {
480 final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper;
481 if (videoSourceWrapper == null) {
482 return Futures.immediateFailedFuture(
483 new IllegalStateException("VideoSourceWrapper has not been initialized"));
484 }
485 return videoSourceWrapper.switchCamera();
486 }
487
488 boolean isMicrophoneEnabled() {
489 final Optional<AudioTrack> audioTrack =
490 TrackWrapper.get(peerConnection, this.localAudioTrack);
491 if (audioTrack.isPresent()) {
492 try {
493 return audioTrack.get().enabled();
494 } catch (final IllegalStateException e) {
495 // sometimes UI might still be rendering the buttons when a background thread has
496 // already ended the call
497 return false;
498 }
499 } else {
500 return false;
501 }
502 }
503
504 boolean setMicrophoneEnabledOrThrow(final boolean enabled) {
505 final Optional<AudioTrack> audioTrack =
506 TrackWrapper.get(peerConnection, this.localAudioTrack);
507 if (audioTrack.isPresent()) {
508 return setEnabled(audioTrack.get(), enabled);
509
510 } else {
511 throw new IllegalStateException("Local audio track does not exist (yet)");
512 }
513 }
514
515 private static boolean setEnabled(final AudioTrack audioTrack, final boolean enabled) {
516 try {
517 audioTrack.setEnabled(enabled);
518 return true;
519 } catch (final IllegalStateException e) {
520 Log.d(Config.LOGTAG, "unable to toggle audio track", e);
521 // ignoring race condition in case MediaStreamTrack has been disposed
522 return false;
523 }
524 }
525
526 void setMicrophoneEnabled(final boolean enabled) {
527 final Optional<AudioTrack> audioTrack =
528 TrackWrapper.get(peerConnection, this.localAudioTrack);
529 if (audioTrack.isPresent()) {
530 setEnabled(audioTrack.get(), enabled);
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}