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