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