diff --git a/webrtc-jni/src/main/cpp/include/JNI_RTCDtlsTransport.h b/webrtc-jni/src/main/cpp/include/JNI_RTCDtlsTransport.h index 86cbf7c9..cce7a70d 100644 --- a/webrtc-jni/src/main/cpp/include/JNI_RTCDtlsTransport.h +++ b/webrtc-jni/src/main/cpp/include/JNI_RTCDtlsTransport.h @@ -47,6 +47,14 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCDtlsTransport_registerObserver JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCDtlsTransport_unregisterObserver (JNIEnv *, jobject); +/* + * Class: dev_onvoid_webrtc_RTCDtlsTransport + * Method: dispose + * Signature: ()V + */ +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCDtlsTransport_dispose + (JNIEnv *, jobject); + #ifdef __cplusplus } #endif diff --git a/webrtc-jni/src/main/cpp/include/api/SetSessionDescriptionObserver.h b/webrtc-jni/src/main/cpp/include/api/SetSessionDescriptionObserver.h index 2030f822..edc6bb82 100644 --- a/webrtc-jni/src/main/cpp/include/api/SetSessionDescriptionObserver.h +++ b/webrtc-jni/src/main/cpp/include/api/SetSessionDescriptionObserver.h @@ -21,17 +21,23 @@ #include "JavaRef.h" #include "api/jsep.h" +#include "api/rtc_error.h" #include #include namespace jni { + // Reports the outcome of setting a local or remote description to a Java + // SetSessionDescriptionObserver, exactly once. class SetSessionDescriptionObserver : public webrtc::SetSessionDescriptionObserver { public: explicit SetSessionDescriptionObserver(JNIEnv * env, const JavaGlobalRef & observer); - ~SetSessionDescriptionObserver() = default; + ~SetSessionDescriptionObserver(); + + SetSessionDescriptionObserver(const SetSessionDescriptionObserver &) = delete; + SetSessionDescriptionObserver & operator=(const SetSessionDescriptionObserver &) = delete; // SetSessionDescriptionObserver implementation. void OnSuccess() override; @@ -47,6 +53,8 @@ namespace jni jmethodID onFailure; }; + void Notify(const char * error) noexcept; + private: JavaGlobalRef observer; diff --git a/webrtc-jni/src/main/cpp/src/JNI_RTCDtlsTransport.cpp b/webrtc-jni/src/main/cpp/src/JNI_RTCDtlsTransport.cpp index 1c1d8bf9..5b66bfc9 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_RTCDtlsTransport.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_RTCDtlsTransport.cpp @@ -119,4 +119,25 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCDtlsTransport_unregisterObserve transport->UnregisterObserver(); ClearNativeObserver(env, caller, "observerHandle"); +} + +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCDtlsTransport_dispose +(JNIEnv * env, jobject caller) +{ + webrtc::DtlsTransportInterface * transport = GetHandle(env, caller); + CHECK_HANDLE(transport); + + // WebRTC must let go of an observer registered through this object before + // it is deleted. + if (GetHandle(env, caller, "observerHandle") != nullptr) { + transport->UnregisterObserver(); + + ClearNativeObserver(env, caller, "observerHandle"); + } + + // The connection, its senders and receivers, and other Java objects keep + // their own references, so this one need not be the last. + transport->Release(); + + SetHandle(env, caller, nullptr); } \ No newline at end of file diff --git a/webrtc-jni/src/main/cpp/src/JNI_RTCPeerConnection.cpp b/webrtc-jni/src/main/cpp/src/JNI_RTCPeerConnection.cpp index 9330cd2e..bfa8a5dd 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_RTCPeerConnection.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_RTCPeerConnection.cpp @@ -39,15 +39,18 @@ #include "JavaRuntimeException.h" #include "JavaString.h" #include "JavaUtils.h" +#include "JNI_WebRTC.h" #include "api/peer_connection_interface.h" #include "api/transport/bitrate_settings.h" #include "api/rtp_receiver_interface.h" #include "api/rtp_sender_interface.h" +#include "rtc_base/thread.h" #include #include #include +#include #include JNIEXPORT jobjectArray JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getSenders @@ -838,15 +841,37 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_close CHECK_HANDLE(pc); try { - pc->Close(); + jni::JavaLocalRef factory(env, env->GetObjectField(caller, + GetFieldID(env, caller, "factory", "L" PKG "PeerConnectionFactory;"))); + webrtc::Thread * signalingThread = factory.get() != nullptr + ? GetHandle(env, factory.get(), "signalingThreadHandle") + : nullptr; + + webrtc::PeerConnectionObserver * observer = GetHandle(env, caller, "observerHandle"); SetHandle(env, caller, nullptr); + SetHandle(env, caller, "observerHandle", nullptr); + + auto close = [pc, observer]() { + pc->Close(); + + // The connection lets go of its observer once closed. + delete observer; - ClearNativeObserver(env, caller, "observerHandle"); + // Drop the owning reference taken when the PeerConnection was handed + // to Java in PeerConnectionFactory::createPeerConnection. + pc->Release(); + }; - // Drop the owning reference taken when the PeerConnection was handed - // to Java in PeerConnectionFactory::createPeerConnection. - pc->Release(); + if (signalingThread != nullptr && signalingThread->IsCurrent()) { + // Called from a callback of this or another connection of the same + // factory. WebRTC goes on with the connection once the callback has + // returned, so it must not be closed and freed under its feet. + signalingThread->PostTask(std::move(close)); + } + else { + close(); + } } catch (...) { ThrowCxxJavaException(env); diff --git a/webrtc-jni/src/main/cpp/src/JNI_RTCRtpReceiver.cpp b/webrtc-jni/src/main/cpp/src/JNI_RTCRtpReceiver.cpp index 848cb440..d4e80509 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_RTCRtpReceiver.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_RTCRtpReceiver.cpp @@ -53,7 +53,13 @@ JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCRtpReceiver_getTransport auto transport = receiver->dtls_transport(); - return jni::JavaFactories::create(env, transport.get()).release(); + if (!transport) { + return nullptr; + } + + // The Java object holds a reference until it is disposed, since the + // connection frees the transport once it closes or drops it. + return jni::JavaFactories::create(env, transport.release()).release(); } catch (...) { ThrowCxxJavaException(env); diff --git a/webrtc-jni/src/main/cpp/src/JNI_RTCRtpSender.cpp b/webrtc-jni/src/main/cpp/src/JNI_RTCRtpSender.cpp index 03769f71..a64c7f33 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_RTCRtpSender.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_RTCRtpSender.cpp @@ -53,7 +53,13 @@ JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCRtpSender_getTransport auto transport = sender->dtls_transport(); - return jni::JavaFactories::create(env, transport.get()).release(); + if (!transport) { + return nullptr; + } + + // The Java object holds a reference until it is disposed, since the + // connection frees the transport once it closes or drops it. + return jni::JavaFactories::create(env, transport.release()).release(); } catch (...) { ThrowCxxJavaException(env); diff --git a/webrtc-jni/src/main/cpp/src/api/SetSessionDescriptionObserver.cpp b/webrtc-jni/src/main/cpp/src/api/SetSessionDescriptionObserver.cpp index 28adbb8d..1e3032ac 100644 --- a/webrtc-jni/src/main/cpp/src/api/SetSessionDescriptionObserver.cpp +++ b/webrtc-jni/src/main/cpp/src/api/SetSessionDescriptionObserver.cpp @@ -20,6 +20,8 @@ #include "JNI_WebRTC.h" #include "JavaUtils.h" +#include + namespace jni { SetSessionDescriptionObserver::SetSessionDescriptionObserver(JNIEnv * env, const JavaGlobalRef & observer) : @@ -28,36 +30,54 @@ namespace jni { } + SetSessionDescriptionObserver::~SetSessionDescriptionObserver() + { + // WebRTC posts the result to the signaling thread and drops it, together + // with this observer, when the peer connection goes away before it runs. + Notify("[INVALID_STATE] Setting the session description was discarded before completion"); + } + void SetSessionDescriptionObserver::OnSuccess() { - JNIEnv * env = AttachCurrentThread(); + Notify(nullptr); + } - if (env == nullptr) { - return; + void SetSessionDescriptionObserver::OnFailure(webrtc::RTCError error) + { + try { + Notify(RTCErrorToString(error).c_str()); + } + catch (...) { + Notify("[INTERNAL_ERROR] Could not report the session description result"); } - - env->CallVoidMethod(observer, javaClass->onSuccess); - - ReportPendingException(env); } - void SetSessionDescriptionObserver::OnFailure(webrtc::RTCError error) + void SetSessionDescriptionObserver::Notify(const char * error) noexcept { + JavaGlobalRef callback(std::move(observer)); + + if (!callback.get()) { + return; + } + JNIEnv * env = AttachCurrentThread(); if (env == nullptr) { return; } - try { - JavaLocalRef errorMessage = JavaString::toJava(env, RTCErrorToString(error)); - - env->CallVoidMethod(observer, javaClass->onFailure, errorMessage.get()); + if (error == nullptr) { + env->CallVoidMethod(callback.get(), javaClass->onSuccess); } - catch (...) { - ThrowCxxJavaException(env); + else { + JavaLocalRef message(env, env->NewStringUTF(error)); + + if (!env->ExceptionCheck()) { + env->CallVoidMethod(callback.get(), javaClass->onFailure, message.get()); + } } + // A Java exception must not escape into WebRTC's signaling thread. ReportPendingException(env); } diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/RTCDtlsTransport.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCDtlsTransport.java index ca1b4906..fe86728f 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/RTCDtlsTransport.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/RTCDtlsTransport.java @@ -18,7 +18,7 @@ import java.util.List; -import dev.onvoid.webrtc.internal.NativeObject; +import dev.onvoid.webrtc.internal.DisposableNativeObject; /** * Allows an application to access information about the Datagram Transport @@ -31,16 +31,22 @@ * transport layer for the RTP or RTCP component of a specific {@link * RTCRtpTransceiver}, or a group of RTCRtpTransceivers if such a group has been * negotiated via [BUNDLE]. + *

+ * Each instance holds a reference to the native transport, which keeps it + * readable after the connection has closed or dropped it; the transport then + * reports {@link RTCDtlsTransportState#CLOSED}. Release it with {@link + * #dispose()} once it is no longer needed. * * @author Alex Andres */ -public class RTCDtlsTransport extends NativeObject { +public class RTCDtlsTransport extends DisposableNativeObject { /** * The native observer registered via {@link #registerObserver}. The * RTCDtlsTransport does not take ownership of the Java observer, but it - * owns the native observer wrapper; it is freed when replaced and when - * {@link #unregisterObserver()} is called. + * owns the native observer wrapper; it is freed when replaced, when + * {@link #unregisterObserver()} is called, and when this transport is + * disposed. */ @SuppressWarnings("unused") private long observerHandle; @@ -84,6 +90,20 @@ public class RTCDtlsTransport extends NativeObject { */ public native void unregisterObserver(); + /** + * Unregisters the observer registered through this instance, if any, and + * releases the native reference held by this instance. + *

+ * An RTCDtlsTransport is not exclusively owned by this instance: the + * connection, its senders and receivers, and other RTCDtlsTransport + * instances obtained via separate calls to {@link + * RTCRtpSender#getTransport()} or {@link RTCRtpReceiver#getTransport()} + * keep their own references. Disposing this instance does not affect the + * transport itself or any other instance referring to it. + */ + @Override + public native void dispose(); + /** * Two RTCDtlsTransport instances are equal if they are bound to the same * native transport, e.g. when obtained from two separate calls to {@link diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/RTCPeerConnection.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCPeerConnection.java index 01e5624a..1742300e 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/RTCPeerConnection.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/RTCPeerConnection.java @@ -507,6 +507,11 @@ public native void getStats(RTCRtpSender sender, /** * Closes the peer connection, terminates all media and releases any used * resources. + *

+ * When called from an observer callback of a connection created by the + * same factory, the connection is closed once that callback has returned, + * since WebRTC goes on with it afterwards. Until then, the {@link + * PeerConnectionObserver} may still hear of the operation in progress. */ public native void close(); diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpReceiver.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpReceiver.java index a09d7a47..cbb3bcab 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpReceiver.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpReceiver.java @@ -86,9 +86,12 @@ private RTCRtpReceiver() { * the form of RTP packets. When bundling is used, multiple RTCRtpReceivers * will share one transport and will all receive RTP and RTCP over the same * transport. + *

+ * Each call returns a new instance holding its own reference to the + * transport, to be released with {@link RTCDtlsTransport#dispose()}. * * @return The transport over which media for the receiver's track is - * received. + * received, or null if the receiver has none yet. */ public native RTCDtlsTransport getTransport(); diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpSender.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpSender.java index 55f34f1d..d8a54851 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpSender.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpSender.java @@ -89,8 +89,12 @@ private RTCRtpSender() { * form of RTP packets. When bundling is used, multiple RTCRtpSenders will * share one transport and will all send RTP and RTCP over the same * transport. + *

+ * Each call returns a new instance holding its own reference to the + * transport, to be released with {@link RTCDtlsTransport#dispose()}. * - * @return The transport over which media from the MediaStreamTrack is sent. + * @return The transport over which media from the MediaStreamTrack is sent, + * or null if the sender has none yet. */ public native RTCDtlsTransport getTransport(); diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/RTCDtlsTransportTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/RTCDtlsTransportTests.java index 02d2c1d0..2bc10edd 100644 --- a/webrtc/src/test/java/dev/onvoid/webrtc/RTCDtlsTransportTests.java +++ b/webrtc/src/test/java/dev/onvoid/webrtc/RTCDtlsTransportTests.java @@ -20,6 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.ArrayList; @@ -45,6 +46,8 @@ class RTCDtlsTransportTests extends TestBase { private TestPeerConnection caller; private TestPeerConnection callee; + private RTCRtpSender sender; + private RTCDtlsTransport transport; @@ -59,7 +62,7 @@ void init() throws Exception { List streamIds = new ArrayList<>(); streamIds.add("stream1"); - RTCRtpSender sender = caller.getPeerConnection().addTrack(audioTrack, streamIds); + sender = caller.getPeerConnection().addTrack(audioTrack, streamIds); caller.setRemotePeerConnection(callee); callee.setRemotePeerConnection(caller); @@ -77,6 +80,27 @@ void init() throws Exception { void dispose() { caller.close(); callee.close(); + + if (transport != null) { + transport.dispose(); + } + } + + @Test + void noTransportBeforeNegotiation() { + TestPeerConnection offerer = new TestPeerConnection(factory); + + try { + AudioTrackSource audioSource = factory.createAudioSource(new AudioOptions()); + AudioTrack audioTrack = factory.createAudioTrack("audioTrack", audioSource); + RTCRtpSender audioSender = offerer.getPeerConnection() + .addTrack(audioTrack, Collections.singletonList("stream1")); + + assertNull(audioSender.getTransport()); + } + finally { + offerer.close(); + } } @Test @@ -160,12 +184,47 @@ void remoteCertificatesBeforeHandshake() throws Exception { assertEquals(RTCDtlsTransportState.NEW, dtlsTransport.getState()); assertTrue(dtlsTransport.getRemoteCertificates().isEmpty()); + + dtlsTransport.dispose(); } finally { offerer.close(); } } + @Test + void transportOutlivesClosedConnection() throws Exception { + CompletableFuture closedState = new CompletableFuture<>(); + + transport.registerObserver(new RTCDtlsTransportObserver() { + @Override + public void onStateChange(RTCDtlsTransportState state) { + if (state == RTCDtlsTransportState.CLOSED) { + closedState.complete(state); + } + } + + @Override + public void onError(String error) { } + }); + + // Closing frees the connection's transports, and with the sender gone + // nothing else refers to this one. The wrapper must keep the transport + // it refers to alive, rather than point to freed memory. + caller.close(); + callee.close(); + sender.dispose(); + + assertEquals(RTCDtlsTransportState.CLOSED, closedState.get(5, TimeUnit.SECONDS)); + + for (int i = 0; i < 1000; i++) { + assertEquals(RTCDtlsTransportState.CLOSED, transport.getState()); + assertTrue(transport.getRemoteCertificates().isEmpty()); + } + + assertDoesNotThrow(transport::unregisterObserver); + } + @Test void observerCanUseRtpSender() throws Exception { // WebRTC reports DTLS states on the network thread, which is also the @@ -209,6 +268,7 @@ public void onError(String error) { assertNotNull(parameters.get(10, TimeUnit.SECONDS)); dtlsTransport.unregisterObserver(); + dtlsTransport.dispose(); } finally { offerer.close(); diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/RTCPeerConnectionSignalingTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/RTCPeerConnectionSignalingTests.java index e6aa1bcb..6097c2f6 100644 --- a/webrtc/src/test/java/dev/onvoid/webrtc/RTCPeerConnectionSignalingTests.java +++ b/webrtc/src/test/java/dev/onvoid/webrtc/RTCPeerConnectionSignalingTests.java @@ -19,12 +19,15 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -199,6 +202,51 @@ void remoteChannelHasAnnouncedPriority() throws Exception { remote.dispose(); } + @Test + void closeBeforeDescriptionResultFailsObserver() throws Exception { + RTCPeerConnection connection = factory.createPeerConnection( + new RTCConfiguration(), candidate -> { }); + connection.createDataChannel("data", new RTCDataChannelInit()); + + TestSetDescObserver applied = new TestSetDescObserver(); + CountDownLatch closed = new CountDownLatch(1); + AtomicReference failure = new AtomicReference<>(); + + connection.createOffer(new RTCOfferOptions(), new CreateSessionDescriptionObserver() { + @Override + public void onSuccess(RTCSessionDescription description) { + // This runs on the signaling thread. Setting the description + // waits for the offer, whose result is posted to that thread. + // Closing from here takes effect once this returns, before the + // result of setting the description is posted in turn, and + // WebRTC drops that result with the connection. + try { + connection.createOffer(new RTCOfferOptions(), new TestCreateDescObserver()); + connection.setLocalDescription(applied); + connection.close(); + } + catch (Throwable e) { + failure.set(e); + } + closed.countDown(); + } + + @Override + public void onFailure(String error) { + failure.set(new IllegalStateException(error)); + closed.countDown(); + } + }); + + assertTrue(closed.await(5, TimeUnit.SECONDS), "Offer was not created"); + assertNull(failure.get()); + + ExecutionException e = assertThrows(ExecutionException.class, + () -> applied.get(5, TimeUnit.SECONDS)); + + assertTrue(e.getMessage().startsWith("[INVALID_STATE]"), e.getMessage()); + } + /** * Offer and answer through the implicit form of setLocalDescription. */ diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/RTCPeerConnectionTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/RTCPeerConnectionTests.java index 10255d05..be9be63a 100644 --- a/webrtc/src/test/java/dev/onvoid/webrtc/RTCPeerConnectionTests.java +++ b/webrtc/src/test/java/dev/onvoid/webrtc/RTCPeerConnectionTests.java @@ -29,6 +29,7 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -564,4 +565,93 @@ void closeManyConnectionsDoesNotCrash() { connection.close(); } } + + @Test + void closeFromConnectionStateCallback() throws Exception { + CountDownLatch closed = new CountDownLatch(1); + AtomicReference failure = new AtomicReference<>(); + + TestPeerConnection callee = new TestPeerConnection(factory); + TestPeerConnection caller = new TestPeerConnection(factory) { + + @Override + public void onConnectionChange(RTCPeerConnectionState state) { + super.onConnectionChange(state); + + // WebRTC goes on with the connection once this callback has + // returned, so closing must not free it right away. + if (state == RTCPeerConnectionState.CONNECTED) { + try { + getPeerConnection().close(); + } + catch (Throwable e) { + failure.set(e); + } + closed.countDown(); + } + } + }; + + try { + caller.setRemotePeerConnection(callee); + callee.setRemotePeerConnection(caller); + + callee.setRemoteDescription(caller.createOffer()); + caller.setRemoteDescription(callee.createAnswer()); + + assertTrue(closed.await(10, TimeUnit.SECONDS), "Caller did not connect"); + assertNull(failure.get()); + } + finally { + callee.close(); + } + + // The factory keeps working. + RTCPeerConnection connection = factory.createPeerConnection( + new RTCConfiguration(), candidate -> { }); + connection.close(); + } + + @Test + void closeFromSignalingStateCallback() throws Exception { + AtomicReference failure = new AtomicReference<>(); + + TestPeerConnection caller = new TestPeerConnection(factory); + TestPeerConnection callee = new TestPeerConnection(factory) { + + @Override + public void onSignalingChange(RTCSignalingState state) { + // Called while the remote description is being applied, which + // WebRTC goes on with once this callback has returned. + if (state == RTCSignalingState.HAVE_REMOTE_OFFER) { + try { + getPeerConnection().close(); + } + catch (Throwable e) { + failure.set(e); + } + } + } + }; + + try { + RTCSessionDescription offer = caller.createOffer(); + + TestSetDescObserver applied = new TestSetDescObserver(); + callee.getPeerConnection().setRemoteDescription(offer, applied); + + // Either outcome is fine, as long as there is one. + try { + applied.get(5, TimeUnit.SECONDS); + } + catch (ExecutionException e) { + assertTrue(e.getMessage().startsWith("[INVALID_STATE]"), e.getMessage()); + } + + assertNull(failure.get()); + } + finally { + caller.close(); + } + } }