From da0b304584808b4aebdc835a54fd3ebdaea8e8bc Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Mon, 26 Oct 2026 16:38:18 +0100 Subject: [PATCH 1/3] fix: report session descriptions that WebRTC discards on close WebRTC posts the result of setting a description and drops it, together with the observer, when the connection goes away first; the observer was never called. Like AddIceCandidateObserver, it now reports INVALID_STATE from its destructor if it has not reported anything yet. --- .../api/SetSessionDescriptionObserver.h | 10 +++- .../src/api/SetSessionDescriptionObserver.cpp | 48 +++++++++++++------ .../RTCPeerConnectionSignalingTests.java | 48 +++++++++++++++++++ 3 files changed, 91 insertions(+), 15 deletions(-) diff --git a/webrtc-jni/src/main/cpp/include/api/SetSessionDescriptionObserver.h b/webrtc-jni/src/main/cpp/include/api/SetSessionDescriptionObserver.h index 2030f8222..edc6bb824 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/api/SetSessionDescriptionObserver.cpp b/webrtc-jni/src/main/cpp/src/api/SetSessionDescriptionObserver.cpp index 28adbb8d0..1e3032ac5 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/test/java/dev/onvoid/webrtc/RTCPeerConnectionSignalingTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/RTCPeerConnectionSignalingTests.java index e6aa1bcb8..6097c2f6a 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. */ From e3cf327c91c2e20d342219efc2e6cb352b88d575 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Mon, 26 Oct 2026 16:38:26 +0100 Subject: [PATCH 2/3] fix: keep a DTLS transport alive while its Java object refers to it getTransport() handed out a pointer it held no reference on, so once the connection had closed and the sender or receiver was gone, reading the transport or unregistering its observer touched freed memory. The Java object now holds a reference, released by the new dispose(), and getTransport() returns null while there is no transport. --- .../main/cpp/include/JNI_RTCDtlsTransport.h | 8 +++ .../src/main/cpp/src/JNI_RTCDtlsTransport.cpp | 21 +++++++ .../src/main/cpp/src/JNI_RTCRtpReceiver.cpp | 8 ++- .../src/main/cpp/src/JNI_RTCRtpSender.cpp | 8 ++- .../dev/onvoid/webrtc/RTCDtlsTransport.java | 28 +++++++-- .../dev/onvoid/webrtc/RTCRtpReceiver.java | 5 +- .../java/dev/onvoid/webrtc/RTCRtpSender.java | 6 +- .../onvoid/webrtc/RTCDtlsTransportTests.java | 62 ++++++++++++++++++- 8 files changed, 137 insertions(+), 9 deletions(-) diff --git a/webrtc-jni/src/main/cpp/include/JNI_RTCDtlsTransport.h b/webrtc-jni/src/main/cpp/include/JNI_RTCDtlsTransport.h index 86cbf7c99..cce7a70d7 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/src/JNI_RTCDtlsTransport.cpp b/webrtc-jni/src/main/cpp/src/JNI_RTCDtlsTransport.cpp index 1c1d8bf99..5b66bfc9c 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_RTCRtpReceiver.cpp b/webrtc-jni/src/main/cpp/src/JNI_RTCRtpReceiver.cpp index 848cb4404..d4e805094 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 03769f71b..a64c7f335 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/src/main/java/dev/onvoid/webrtc/RTCDtlsTransport.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCDtlsTransport.java index ca1b49064..fe86728fc 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/RTCRtpReceiver.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpReceiver.java index a09d7a479..cbb3bcab0 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 55f34f1d9..d8a54851f 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 02d2c1d0f..2bc10edd5 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(); From 48d1f8c1d6e46bce684dec903d44acc27d38197a Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Mon, 26 Oct 2026 16:38:26 +0100 Subject: [PATCH 3/3] fix: close a peer connection from its own callbacks without crashing close() closed and freed the connection right away, while WebRTC goes on with it after an observer callback returns, e.g. after reporting CONNECTED or while applying a remote description; the VM crashed. Called on the factory's signaling thread, close() now posts the close to run once the callback has returned. --- .../main/cpp/src/JNI_RTCPeerConnection.cpp | 35 ++++++-- .../dev/onvoid/webrtc/RTCPeerConnection.java | 5 ++ .../onvoid/webrtc/RTCPeerConnectionTests.java | 90 +++++++++++++++++++ 3 files changed, 125 insertions(+), 5 deletions(-) diff --git a/webrtc-jni/src/main/cpp/src/JNI_RTCPeerConnection.cpp b/webrtc-jni/src/main/cpp/src/JNI_RTCPeerConnection.cpp index 9330cd2ea..bfa8a5dd3 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/src/main/java/dev/onvoid/webrtc/RTCPeerConnection.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCPeerConnection.java index 01e5624a9..1742300ec 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/test/java/dev/onvoid/webrtc/RTCPeerConnectionTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/RTCPeerConnectionTests.java index 10255d054..be9be63af 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(); + } + } }