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