From 6efcd0b8c48317f0d3be2ea09c2de205a13b3d6b Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sun, 13 Sep 2026 11:37:09 +0200 Subject: [PATCH 01/15] fix: release the owning PeerConnection reference on close RTCPeerConnection.close() called pc->Close() and cleared the Java handle, but never called pc->Release() on the raw pointer that PeerConnectionFactory::createPeerConnection() had handed to Java via pc.release(). Every closed connection leaked the native PeerConnectionInterface and everything it still owned. --- webrtc-jni/src/main/cpp/src/JNI_RTCPeerConnection.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/webrtc-jni/src/main/cpp/src/JNI_RTCPeerConnection.cpp b/webrtc-jni/src/main/cpp/src/JNI_RTCPeerConnection.cpp index 82215244..5c941456 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_RTCPeerConnection.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_RTCPeerConnection.cpp @@ -584,6 +584,10 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_close SetHandle(env, caller, "observerHandle", nullptr); delete observer; } + + // Drop the owning reference taken when the PeerConnection was handed + // to Java in PeerConnectionFactory::createPeerConnection. + pc->Release(); } catch (...) { ThrowCxxJavaException(env); From b1b42f8d7155997332fa874fd8d9ab36512aabca Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sun, 13 Sep 2026 11:37:19 +0200 Subject: [PATCH 02/15] fix: free native observer wrappers on replace, unregister, and dispose registerObserver() on RTCDataChannel, RTCDtlsTransport, and RTCDtmfSender heap-allocated a native observer and handed it to WebRTC, but never kept a reference to free it: every call leaked the observer plus the JNI global reference it held to the Java listener, and replacing an observer leaked the previous one outright. Track the native observer the same way RTCPeerConnection already tracks its own via an "observerHandle" field, and free it whenever it's replaced, explicitly unregistered, or (for RTCDataChannel) the channel is disposed. --- .../src/main/cpp/src/JNI_RTCDataChannel.cpp | 28 ++++++++++++++++++- .../src/main/cpp/src/JNI_RTCDtlsTransport.cpp | 19 ++++++++++++- .../src/main/cpp/src/JNI_RTCDtmfSender.cpp | 19 ++++++++++++- .../dev/onvoid/webrtc/RTCDataChannel.java | 10 +++++++ .../dev/onvoid/webrtc/RTCDtlsTransport.java | 9 ++++++ .../java/dev/onvoid/webrtc/RTCDtmfSender.java | 9 ++++++ 6 files changed, 91 insertions(+), 3 deletions(-) diff --git a/webrtc-jni/src/main/cpp/src/JNI_RTCDataChannel.cpp b/webrtc-jni/src/main/cpp/src/JNI_RTCDataChannel.cpp index 53b21e6c..0163f566 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_RTCDataChannel.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_RTCDataChannel.cpp @@ -35,7 +35,17 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCDataChannel_registerObserver webrtc::DataChannelInterface * channel = GetHandle(env, caller); CHECK_HANDLE(channel); - channel->RegisterObserver(new jni::RTCDataChannelObserver(env, jni::JavaGlobalRef(env, jObserver))); + // Unregister and free a previously registered observer before replacing + // it, otherwise it would leak along with its JNI global reference. + channel->UnregisterObserver(); + + auto oldObserver = GetHandle(env, caller, "observerHandle"); + delete oldObserver; + + auto observer = new jni::RTCDataChannelObserver(env, jni::JavaGlobalRef(env, jObserver)); + SetHandle(env, caller, "observerHandle", observer); + + channel->RegisterObserver(observer); } JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCDataChannel_unregisterObserver @@ -45,6 +55,13 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCDataChannel_unregisterObserver CHECK_HANDLE(channel); channel->UnregisterObserver(); + + auto observer = GetHandle(env, caller, "observerHandle"); + + if (observer) { + SetHandle(env, caller, "observerHandle", nullptr); + delete observer; + } } JNIEXPORT jstring JNICALL Java_dev_onvoid_webrtc_RTCDataChannel_getLabel @@ -152,6 +169,15 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCDataChannel_dispose webrtc::DataChannelInterface * channel = GetHandle(env, caller); CHECK_HANDLE(channel); + channel->UnregisterObserver(); + + auto observer = GetHandle(env, caller, "observerHandle"); + + if (observer) { + SetHandle(env, caller, "observerHandle", nullptr); + delete observer; + } + webrtc::RefCountReleaseStatus status = channel->Release(); if (status != webrtc::RefCountReleaseStatus::kDroppedLastRef) { diff --git a/webrtc-jni/src/main/cpp/src/JNI_RTCDtlsTransport.cpp b/webrtc-jni/src/main/cpp/src/JNI_RTCDtlsTransport.cpp index 04454fb2..3f42a272 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_RTCDtlsTransport.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_RTCDtlsTransport.cpp @@ -76,7 +76,17 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCDtlsTransport_registerObserver webrtc::DtlsTransportInterface * transport = GetHandle(env, caller); CHECK_HANDLE(transport); - transport->RegisterObserver(new jni::RTCDtlsTransportObserver(env, jni::JavaGlobalRef(env, observer))); + // Unregister and free a previously registered observer before replacing + // it, otherwise it would leak along with its JNI global reference. + transport->UnregisterObserver(); + + auto oldObserver = GetHandle(env, caller, "observerHandle"); + delete oldObserver; + + auto newObserver = new jni::RTCDtlsTransportObserver(env, jni::JavaGlobalRef(env, observer)); + SetHandle(env, caller, "observerHandle", newObserver); + + transport->RegisterObserver(newObserver); } JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCDtlsTransport_unregisterObserver @@ -86,4 +96,11 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCDtlsTransport_unregisterObserve CHECK_HANDLE(transport); transport->UnregisterObserver(); + + auto observer = GetHandle(env, caller, "observerHandle"); + + if (observer) { + SetHandle(env, caller, "observerHandle", nullptr); + delete observer; + } } \ No newline at end of file diff --git a/webrtc-jni/src/main/cpp/src/JNI_RTCDtmfSender.cpp b/webrtc-jni/src/main/cpp/src/JNI_RTCDtmfSender.cpp index e6dd5c7b..d6ff4201 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_RTCDtmfSender.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_RTCDtmfSender.cpp @@ -74,7 +74,17 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCDtmfSender_registerObserver webrtc::DtmfSenderInterface * sender = GetHandle(env, caller); CHECK_HANDLE(sender); - sender->RegisterObserver(new jni::RTCDtmfSenderObserver(env, jni::JavaGlobalRef(env, jObserver))); + // Unregister and free a previously registered observer before replacing + // it, otherwise it would leak along with its JNI global reference. + sender->UnregisterObserver(); + + auto oldObserver = GetHandle(env, caller, "observerHandle"); + delete oldObserver; + + auto observer = new jni::RTCDtmfSenderObserver(env, jni::JavaGlobalRef(env, jObserver)); + SetHandle(env, caller, "observerHandle", observer); + + sender->RegisterObserver(observer); } JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCDtmfSender_unregisterObserver @@ -84,4 +94,11 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCDtmfSender_unregisterObserver CHECK_HANDLE(sender); sender->UnregisterObserver(); + + auto observer = GetHandle(env, caller, "observerHandle"); + + if (observer) { + SetHandle(env, caller, "observerHandle", nullptr); + delete observer; + } } \ No newline at end of file diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/RTCDataChannel.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCDataChannel.java index 97fe9cfd..46b06ac0 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/RTCDataChannel.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/RTCDataChannel.java @@ -29,6 +29,16 @@ */ public class RTCDataChannel extends DisposableNativeObject { + /** + * The native observer registered via {@link #registerObserver}. The + * RTCDataChannel does not take ownership of the Java observer, but it + * owns the native observer wrapper; it is freed when replaced, when + * {@link #unregisterObserver()} is called, and when this channel is + * disposed. + */ + @SuppressWarnings("unused") + private long observerHandle; + /** * Used by the native api. */ diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/RTCDtlsTransport.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCDtlsTransport.java index 5cce5904..f3bcf592 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/RTCDtlsTransport.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/RTCDtlsTransport.java @@ -36,6 +36,15 @@ */ public class RTCDtlsTransport extends NativeObject { + /** + * 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. + */ + @SuppressWarnings("unused") + private long observerHandle; + /** * Returns the underlying transport that is used to send and receive * packets. The underlying transport may not be shared between multiple diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/RTCDtmfSender.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCDtmfSender.java index dc9412f1..d2ee24dd 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/RTCDtmfSender.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/RTCDtmfSender.java @@ -36,6 +36,15 @@ */ public class RTCDtmfSender extends NativeObject { + /** + * The native observer registered via {@link #registerObserver}. The + * RTCDtmfSender 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. + */ + @SuppressWarnings("unused") + private long observerHandle; + RTCDtmfSender() { // Default constructor for native object instantiation. } From 1c5cb0bfd2011e9dbc162472319b3d16e2ea4876 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sun, 13 Sep 2026 11:37:39 +0200 Subject: [PATCH 03/15] fix: guard against a null JNIEnv after a failed thread attach AttachCurrentThread() can return nullptr when attaching the calling native thread to the JVM fails (e.g. during JVM shutdown), a case already handled in LogSink and RTCDataChannelSendObserver. Every other native callback/observer dereferenced the JNIEnv unconditionally, crashing the process instead of just dropping the callback. --- .../src/main/cpp/src/api/AudioTrackSink.cpp | 4 ++ .../api/CreateSessionDescriptionObserver.cpp | 8 +++ .../cpp/src/api/PeerConnectionObserver.cpp | 52 +++++++++++++++++++ .../cpp/src/api/RTCDataChannelObserver.cpp | 12 +++++ .../cpp/src/api/RTCDtlsTransportObserver.cpp | 8 +++ .../cpp/src/api/RTCDtmfSenderObserver.cpp | 4 ++ .../cpp/src/api/RTCStatsCollectorCallback.cpp | 4 ++ .../src/api/SetSessionDescriptionObserver.cpp | 8 +++ .../cpp/src/media/DeviceChangeListener.cpp | 10 ++++ .../src/media/MediaStreamTrackObserver.cpp | 4 ++ .../src/media/audio/AudioTransportSink.cpp | 4 ++ .../src/media/audio/AudioTransportSource.cpp | 5 ++ 12 files changed, 123 insertions(+) diff --git a/webrtc-jni/src/main/cpp/src/api/AudioTrackSink.cpp b/webrtc-jni/src/main/cpp/src/api/AudioTrackSink.cpp index 54705428..957668f9 100644 --- a/webrtc-jni/src/main/cpp/src/api/AudioTrackSink.cpp +++ b/webrtc-jni/src/main/cpp/src/api/AudioTrackSink.cpp @@ -30,6 +30,10 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + return; + } + const jbyte * buffer = static_cast(data); jsize dataSize = static_cast(frames * channels * (bitsPerSample / 8)); diff --git a/webrtc-jni/src/main/cpp/src/api/CreateSessionDescriptionObserver.cpp b/webrtc-jni/src/main/cpp/src/api/CreateSessionDescriptionObserver.cpp index e01591e5..52d89755 100644 --- a/webrtc-jni/src/main/cpp/src/api/CreateSessionDescriptionObserver.cpp +++ b/webrtc-jni/src/main/cpp/src/api/CreateSessionDescriptionObserver.cpp @@ -32,6 +32,10 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + return; + } + JavaLocalRef javaDesc = jni::RTCSessionDescription::toJava(env, desc); env->CallVoidMethod(observer, javaClass->onSuccess, javaDesc.get()); @@ -43,6 +47,10 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + return; + } + JavaLocalRef errorMessage = JavaString::toJava(env, RTCErrorToString(error)); env->CallVoidMethod(observer, javaClass->onFailure, errorMessage.get()); diff --git a/webrtc-jni/src/main/cpp/src/api/PeerConnectionObserver.cpp b/webrtc-jni/src/main/cpp/src/api/PeerConnectionObserver.cpp index 6c752129..fc936506 100644 --- a/webrtc-jni/src/main/cpp/src/api/PeerConnectionObserver.cpp +++ b/webrtc-jni/src/main/cpp/src/api/PeerConnectionObserver.cpp @@ -40,6 +40,10 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + return; + } + auto jState = JavaEnums::toJava(env, state); env->CallVoidMethod(observer, javaClass->onConnectionChange, jState.get()); @@ -51,6 +55,10 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + return; + } + auto jState = JavaEnums::toJava(env, state); env->CallVoidMethod(observer, javaClass->onSignalingChange, jState.get()); @@ -62,6 +70,10 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + return; + } + auto jTransceiver = JavaFactories::create(env, transceiver.get()); env->CallVoidMethod(observer, javaClass->onTrack, jTransceiver.get()); @@ -73,6 +85,10 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + return; + } + JavaLocalRef streamArray; try { @@ -93,6 +109,10 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + return; + } + auto jReceiver = JavaFactories::create(env, receiver.get()); env->CallVoidMethod(observer, javaClass->onRemoveTrack, jReceiver.get()); @@ -104,6 +124,10 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + return; + } + auto jDataChannel = JavaFactories::create(env, channel.release()); env->CallVoidMethod(observer, javaClass->onDataChannel, jDataChannel.get()); @@ -115,6 +139,10 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + return; + } + env->CallVoidMethod(observer, javaClass->onRenegotiationNeeded); ExceptionCheck(env); @@ -124,6 +152,10 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + return; + } + auto jState = JavaEnums::toJava(env, state); env->CallVoidMethod(observer, javaClass->onIceConnectionChange, jState.get()); @@ -135,6 +167,10 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + return; + } + auto jState = JavaEnums::toJava(env, state); env->CallVoidMethod(observer, javaClass->onIceGatheringChange, jState.get()); @@ -146,6 +182,10 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + return; + } + JavaLocalRef jCandidate = RTCIceCandidate::toJava(env, candidate); env->CallVoidMethod(observer, javaClass->onIceCandidate, jCandidate.get()); @@ -157,6 +197,10 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + return; + } + JavaLocalRef event = RTCPeerConnectionIceErrorEvent::toJava(env, address, port, url, error_code, error_text); env->CallVoidMethod(observer, javaClass->onIceCandidateError, event.get()); @@ -168,6 +212,10 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + return; + } + env->CallVoidMethod(observer, javaClass->onIceConnectionReceivingChange, receiving); ExceptionCheck(env); @@ -177,6 +225,10 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + return; + } + const webrtc::Candidate & remote = event.selected_candidate_pair.remote_candidate(); std::string ip = remote.address().ipaddr().ToString(); diff --git a/webrtc-jni/src/main/cpp/src/api/RTCDataChannelObserver.cpp b/webrtc-jni/src/main/cpp/src/api/RTCDataChannelObserver.cpp index 5c0258ea..6333bd06 100644 --- a/webrtc-jni/src/main/cpp/src/api/RTCDataChannelObserver.cpp +++ b/webrtc-jni/src/main/cpp/src/api/RTCDataChannelObserver.cpp @@ -32,6 +32,10 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + return; + } + env->CallVoidMethod(observer, javaClass->onStateChange); ExceptionCheck(env); @@ -41,6 +45,10 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + return; + } + JavaLocalRef jBuffer = bufferFactory->create(env, &buffer); env->CallVoidMethod(observer, javaClass->onMessage, jBuffer.get()); @@ -52,6 +60,10 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + return; + } + env->CallVoidMethod(observer, javaClass->onBufferedAmountChange, static_cast(sent_data_size)); ExceptionCheck(env); diff --git a/webrtc-jni/src/main/cpp/src/api/RTCDtlsTransportObserver.cpp b/webrtc-jni/src/main/cpp/src/api/RTCDtlsTransportObserver.cpp index 7ebdf739..f72d350f 100644 --- a/webrtc-jni/src/main/cpp/src/api/RTCDtlsTransportObserver.cpp +++ b/webrtc-jni/src/main/cpp/src/api/RTCDtlsTransportObserver.cpp @@ -34,6 +34,10 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + return; + } + auto state = JavaEnums::toJava(env, info.state()); env->CallVoidMethod(observer, javaClass->onStateChange, state.get()); @@ -45,6 +49,10 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + return; + } + JavaLocalRef errorMessage = JavaString::toJava(env, RTCErrorToString(error)); env->CallVoidMethod(observer, javaClass->onError, errorMessage.get()); diff --git a/webrtc-jni/src/main/cpp/src/api/RTCDtmfSenderObserver.cpp b/webrtc-jni/src/main/cpp/src/api/RTCDtmfSenderObserver.cpp index 92cc9986..a0a54960 100644 --- a/webrtc-jni/src/main/cpp/src/api/RTCDtmfSenderObserver.cpp +++ b/webrtc-jni/src/main/cpp/src/api/RTCDtmfSenderObserver.cpp @@ -32,6 +32,10 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + return; + } + JavaLocalRef jTone = JavaString::toJava(env, tone); JavaLocalRef jToneBuffer = JavaString::toJava(env, tone_buffer); diff --git a/webrtc-jni/src/main/cpp/src/api/RTCStatsCollectorCallback.cpp b/webrtc-jni/src/main/cpp/src/api/RTCStatsCollectorCallback.cpp index c6e152b2..5bfe61c0 100644 --- a/webrtc-jni/src/main/cpp/src/api/RTCStatsCollectorCallback.cpp +++ b/webrtc-jni/src/main/cpp/src/api/RTCStatsCollectorCallback.cpp @@ -31,6 +31,10 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + return; + } + JavaLocalRef javaReport = jni::RTCStatsReport::toJava(env, report); env->CallVoidMethod(callback, javaClass->onStatsDelivered, javaReport.get()); diff --git a/webrtc-jni/src/main/cpp/src/api/SetSessionDescriptionObserver.cpp b/webrtc-jni/src/main/cpp/src/api/SetSessionDescriptionObserver.cpp index d7bb1a22..1ea9ac00 100644 --- a/webrtc-jni/src/main/cpp/src/api/SetSessionDescriptionObserver.cpp +++ b/webrtc-jni/src/main/cpp/src/api/SetSessionDescriptionObserver.cpp @@ -32,6 +32,10 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + return; + } + env->CallVoidMethod(observer, javaClass->onSuccess); ExceptionCheck(env); @@ -41,6 +45,10 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + return; + } + JavaLocalRef errorMessage = JavaString::toJava(env, RTCErrorToString(error)); env->CallVoidMethod(observer, javaClass->onFailure, errorMessage.get()); diff --git a/webrtc-jni/src/main/cpp/src/media/DeviceChangeListener.cpp b/webrtc-jni/src/main/cpp/src/media/DeviceChangeListener.cpp index ea5b8513..a7abaa7a 100644 --- a/webrtc-jni/src/main/cpp/src/media/DeviceChangeListener.cpp +++ b/webrtc-jni/src/main/cpp/src/media/DeviceChangeListener.cpp @@ -33,6 +33,11 @@ namespace jni void DeviceChangeListener::deviceConnected(avdev::DevicePtr device) { JNIEnv * env = AttachCurrentThread(); + + if (env == nullptr) { + return; + } + JavaLocalRef jdevice = nullptr; if (dynamic_cast(device.get())) { @@ -51,6 +56,11 @@ namespace jni void DeviceChangeListener::deviceDisconnected(avdev::DevicePtr device) { JNIEnv * env = AttachCurrentThread(); + + if (env == nullptr) { + return; + } + JavaLocalRef jdevice = nullptr; if (dynamic_cast(device.get())) { diff --git a/webrtc-jni/src/main/cpp/src/media/MediaStreamTrackObserver.cpp b/webrtc-jni/src/main/cpp/src/media/MediaStreamTrackObserver.cpp index 762683a4..faba1f5d 100644 --- a/webrtc-jni/src/main/cpp/src/media/MediaStreamTrackObserver.cpp +++ b/webrtc-jni/src/main/cpp/src/media/MediaStreamTrackObserver.cpp @@ -35,6 +35,10 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + return; + } + // Check state changes. if (eventType == MediaStreamTrackEvent::mute) { diff --git a/webrtc-jni/src/main/cpp/src/media/audio/AudioTransportSink.cpp b/webrtc-jni/src/main/cpp/src/media/audio/AudioTransportSink.cpp index 1a517690..48fb6170 100644 --- a/webrtc-jni/src/main/cpp/src/media/audio/AudioTransportSink.cpp +++ b/webrtc-jni/src/main/cpp/src/media/audio/AudioTransportSink.cpp @@ -39,6 +39,10 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + return 0; + } + const jbyte * buffer = static_cast(audioSamples); jsize dataSize = static_cast(nSamples * nBytesPerSample); diff --git a/webrtc-jni/src/main/cpp/src/media/audio/AudioTransportSource.cpp b/webrtc-jni/src/main/cpp/src/media/audio/AudioTransportSource.cpp index 3b1672db..dca821b8 100644 --- a/webrtc-jni/src/main/cpp/src/media/audio/AudioTransportSource.cpp +++ b/webrtc-jni/src/main/cpp/src/media/audio/AudioTransportSource.cpp @@ -40,6 +40,11 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + nSamplesOut = 0; + return 0; + } + *elapsed_time_ms = 0; *ntp_time_ms = 0; From de41049315b174221d6ade005ec69a842e2c4e4b Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sun, 13 Sep 2026 11:37:48 +0200 Subject: [PATCH 04/15] fix: remove double local reference deletion in video frame callbacks VideoTrackSink::OnFrame and DesktopCaptureCallback::OnCaptureResult manually called env->DeleteLocalRef() on jBuffer, a JavaLocalRef that already deletes its own local reference on scope exit. JavaLocalRef's destructor doesn't know the reference was already deleted, so it deletes it again -- undefined behavior per the JNI spec, hit on every video frame and desktop capture frame. Also guard both callbacks against a null JNIEnv after a failed thread attach, matching the rest of the native callback classes. --- webrtc-jni/src/main/cpp/src/api/VideoTrackSink.cpp | 7 ++++++- .../cpp/src/media/video/desktop/DesktopCaptureCallback.cpp | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/webrtc-jni/src/main/cpp/src/api/VideoTrackSink.cpp b/webrtc-jni/src/main/cpp/src/api/VideoTrackSink.cpp index 97d4e7e8..0c343d04 100644 --- a/webrtc-jni/src/main/cpp/src/api/VideoTrackSink.cpp +++ b/webrtc-jni/src/main/cpp/src/api/VideoTrackSink.cpp @@ -35,6 +35,10 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + return; + } + webrtc::scoped_refptr buffer = frame.video_frame_buffer(); webrtc::scoped_refptr i420Buffer = buffer->ToI420(); @@ -53,7 +57,8 @@ namespace jni env->CallVoidMethod(sink, javaClass->onFrame, jFrame); ExceptionCheck(env); - env->DeleteLocalRef(jBuffer); + // jBuffer is a JavaLocalRef and deletes its own local reference on + // scope exit; deleting it again here would be a double free. env->DeleteLocalRef(jFrame); } diff --git a/webrtc-jni/src/main/cpp/src/media/video/desktop/DesktopCaptureCallback.cpp b/webrtc-jni/src/main/cpp/src/media/video/desktop/DesktopCaptureCallback.cpp index ac29ddb5..2abebde3 100644 --- a/webrtc-jni/src/main/cpp/src/media/video/desktop/DesktopCaptureCallback.cpp +++ b/webrtc-jni/src/main/cpp/src/media/video/desktop/DesktopCaptureCallback.cpp @@ -43,6 +43,10 @@ namespace jni { JNIEnv * env = AttachCurrentThread(); + if (env == nullptr) { + return; + } + if (result != webrtc::DesktopCapturer::Result::SUCCESS) { // Propagate the failure to Java instead of silently dropping it — // callers waiting for a frame otherwise have to rely on timeouts. @@ -115,7 +119,8 @@ namespace jni env->CallVoidMethod(callback, javaClass->onCaptureResult, jresult.get(), jFrame); ExceptionCheck(env); - env->DeleteLocalRef(jBuffer); + // jBuffer is a JavaLocalRef and deletes its own local reference on + // scope exit; deleting it again here would be a double free. env->DeleteLocalRef(jFrame); } From b8e774d5d311cc78605521a2f3a8dacc2d7e3c95 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sun, 13 Sep 2026 11:56:23 +0200 Subject: [PATCH 05/15] test: add regression tests for the observer and reference leak fixes - RTCPeerConnection: repeated create/close does not crash, guarding the pc->Release() fix on close(). - RTCDataChannel and RTCDtlsTransport: replacing a registered observer stops the previous one from receiving further events, and disposing a channel with an observer still registered does not throw. - RTCDtmfSender: replacing a registered observer stops the previous one from receiving further events. None of these previously had coverage for the observer-replace path, which is what silently leaked the old native observer wrapper. --- .../onvoid/webrtc/RTCDataChannelTests.java | 107 +++++++++++++++ .../onvoid/webrtc/RTCDtlsTransportTests.java | 127 ++++++++++++++++++ .../dev/onvoid/webrtc/RTCDtmfSenderTests.java | 21 +++ .../onvoid/webrtc/RTCPeerConnectionTests.java | 17 +++ 4 files changed, 272 insertions(+) create mode 100644 webrtc/src/test/java/dev/onvoid/webrtc/RTCDtlsTransportTests.java diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/RTCDataChannelTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/RTCDataChannelTests.java index 183b378f..8841f459 100644 --- a/webrtc/src/test/java/dev/onvoid/webrtc/RTCDataChannelTests.java +++ b/webrtc/src/test/java/dev/onvoid/webrtc/RTCDataChannelTests.java @@ -17,6 +17,7 @@ package dev.onvoid.webrtc; import static java.util.Objects.nonNull; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -28,6 +29,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -117,6 +119,111 @@ void textMessage() throws Exception { callee.close(); } + @Test + void replaceObserverStopsPreviousObserver() throws Exception { + TestPeerConnection caller = new TestPeerConnection(factory); + TestPeerConnection callee = new TestPeerConnection(factory); + + caller.setRemotePeerConnection(callee); + callee.setRemotePeerConnection(caller); + + RTCDataChannel channel = caller.getPeerConnection() + .createDataChannel("replace-observer", new RTCDataChannelInit()); + + callee.setRemoteDescription(caller.createOffer()); + caller.setRemoteDescription(callee.createAnswer()); + + caller.waitUntilConnected(); + callee.waitUntilConnected(); + + AtomicInteger firstObserverCalls = new AtomicInteger(); + AtomicInteger secondObserverCalls = new AtomicInteger(); + + channel.registerObserver(new RTCDataChannelObserver() { + @Override + public void onStateChange() { + firstObserverCalls.incrementAndGet(); + } + + @Override + public void onMessage(RTCDataChannelBuffer buffer) { } + + @Override + public void onBufferedAmountChange(long sentDataSize) { } + }); + + // Replacing the observer must unregister and free the previous + // native observer wrapper instead of leaking it and leaving it + // registered alongside the new one. + channel.registerObserver(new RTCDataChannelObserver() { + @Override + public void onStateChange() { + secondObserverCalls.incrementAndGet(); + } + + @Override + public void onMessage(RTCDataChannelBuffer buffer) { } + + @Override + public void onBufferedAmountChange(long sentDataSize) { } + }); + + int firstCallsAfterReplace = firstObserverCalls.get(); + + channel.close(); + + long deadline = System.currentTimeMillis() + 2000; + while (secondObserverCalls.get() == 0 && System.currentTimeMillis() < deadline) { + Thread.sleep(20); + } + + assertEquals(firstCallsAfterReplace, firstObserverCalls.get(), + "The replaced observer must not receive further events"); + assertTrue(secondObserverCalls.get() > 0, + "The active observer must receive the close state change"); + + channel.dispose(); + caller.close(); + callee.close(); + } + + @Test + void disposeFreesStillRegisteredObserver() throws Exception { + TestPeerConnection caller = new TestPeerConnection(factory); + TestPeerConnection callee = new TestPeerConnection(factory); + + caller.setRemotePeerConnection(callee); + callee.setRemotePeerConnection(caller); + + RTCDataChannel channel = caller.getPeerConnection() + .createDataChannel("dispose-observer", new RTCDataChannelInit()); + + callee.setRemoteDescription(caller.createOffer()); + caller.setRemoteDescription(callee.createAnswer()); + + caller.waitUntilConnected(); + callee.waitUntilConnected(); + + channel.registerObserver(new RTCDataChannelObserver() { + @Override + public void onStateChange() { } + + @Override + public void onMessage(RTCDataChannelBuffer buffer) { } + + @Override + public void onBufferedAmountChange(long sentDataSize) { } + }); + + // dispose() must free the still-registered native observer itself + // instead of leaking it, without calling unregisterObserver() first. + channel.close(); + assertDoesNotThrow(channel::dispose); + + caller.close(); + callee.close(); + } + private static class DataPeerConnection extends TestPeerConnection { diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/RTCDtlsTransportTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/RTCDtlsTransportTests.java new file mode 100644 index 00000000..45b31931 --- /dev/null +++ b/webrtc/src/test/java/dev/onvoid/webrtc/RTCDtlsTransportTests.java @@ -0,0 +1,127 @@ +/* + * Copyright 2019 Alex Andres + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.onvoid.webrtc; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import dev.onvoid.webrtc.media.audio.AudioOptions; +import dev.onvoid.webrtc.media.audio.AudioTrack; +import dev.onvoid.webrtc.media.audio.AudioTrackSource; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for the {@link RTCDtlsTransport} class. + */ +class RTCDtlsTransportTests extends TestBase { + + private TestPeerConnection caller; + private TestPeerConnection callee; + + private RTCDtlsTransport transport; + + + @BeforeEach + void init() throws Exception { + caller = new TestPeerConnection(factory); + callee = new TestPeerConnection(factory); + + AudioTrackSource audioSource = factory.createAudioSource(new AudioOptions()); + AudioTrack audioTrack = factory.createAudioTrack("audioTrack", audioSource); + + List streamIds = new ArrayList<>(); + streamIds.add("stream1"); + + RTCRtpSender sender = caller.getPeerConnection().addTrack(audioTrack, streamIds); + + caller.setRemotePeerConnection(callee); + callee.setRemotePeerConnection(caller); + + callee.setRemoteDescription(caller.createOffer()); + caller.setRemoteDescription(callee.createAnswer()); + + caller.waitUntilConnected(); + callee.waitUntilConnected(); + + transport = sender.getTransport(); + } + + @AfterEach + void dispose() { + caller.close(); + callee.close(); + } + + @Test + void replaceObserverStopsPreviousObserver() { + assertNotNull(transport); + + AtomicInteger firstObserverCalls = new AtomicInteger(); + AtomicInteger secondObserverCalls = new AtomicInteger(); + + RTCDtlsTransportObserver first = new RTCDtlsTransportObserver() { + @Override + public void onStateChange(RTCDtlsTransportState info) { + firstObserverCalls.incrementAndGet(); + } + + @Override + public void onError(String error) { } + }; + RTCDtlsTransportObserver second = new RTCDtlsTransportObserver() { + @Override + public void onStateChange(RTCDtlsTransportState info) { + secondObserverCalls.incrementAndGet(); + } + + @Override + public void onError(String error) { } + }; + + transport.registerObserver(first); + + // Replacing the observer must unregister and free the previous + // native observer wrapper instead of leaking it and leaving it + // registered alongside the new one. + assertDoesNotThrow(() -> transport.registerObserver(second)); + + int firstCallsAfterReplace = firstObserverCalls.get(); + + // Registering and unregistering repeatedly must not leak or crash; + // this used to leak the previous native observer on every call. + for (int i = 0; i < 50; i++) { + transport.registerObserver(second); + } + + transport.unregisterObserver(); + + // Unregistering with nothing registered anymore must be a safe no-op. + assertDoesNotThrow(transport::unregisterObserver); + + assertEquals(firstCallsAfterReplace, firstObserverCalls.get(), + "The replaced observer must not receive further events"); + } + +} diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/RTCDtmfSenderTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/RTCDtmfSenderTests.java index d8e352ba..b4a069ce 100644 --- a/webrtc/src/test/java/dev/onvoid/webrtc/RTCDtmfSenderTests.java +++ b/webrtc/src/test/java/dev/onvoid/webrtc/RTCDtmfSenderTests.java @@ -186,6 +186,27 @@ void registerAndUnregisterObserver() throws InterruptedException { assertEquals(Arrays.asList("1", "2", "3", null), observer.getTones()); // No new events } + @Test + void replaceObserverStopsPreviousObserver() throws InterruptedException { + TestDtmfSenderObserver first = new TestDtmfSenderObserver(); + TestDtmfSenderObserver second = new TestDtmfSenderObserver(); + + dtmfSender.registerObserver(first); + + // Replacing the observer must unregister and free the previous + // native observer wrapper instead of leaking it and leaving it + // registered alongside the new one. + dtmfSender.registerObserver(second); + + assertTrue(dtmfSender.insertDtmf("1", 100, 70)); + assertTrue(second.awaitCompletion(), "Timed out waiting for DTMF sequence to complete"); + + assertEquals(Arrays.asList("1", null), second.getTones()); + assertTrue(first.getTones().isEmpty(), "The replaced observer must not receive events"); + + dtmfSender.unregisterObserver(); + } + private void insertTones(List tones, int duration, int interToneGap) throws InterruptedException { TestDtmfSenderObserver observer = new TestDtmfSenderObserver(); diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/RTCPeerConnectionTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/RTCPeerConnectionTests.java index 751ccbc0..c0727b5f 100644 --- a/webrtc/src/test/java/dev/onvoid/webrtc/RTCPeerConnectionTests.java +++ b/webrtc/src/test/java/dev/onvoid/webrtc/RTCPeerConnectionTests.java @@ -394,4 +394,21 @@ void statesWhenClosed() { assertEquals(RTCIceGatheringState.NEW, peerConnection.getIceGatheringState()); assertEquals(RTCIceConnectionState.CLOSED, peerConnection.getIceConnectionState()); } + + @Test + void closeManyConnectionsDoesNotCrash() { + // Regression guard: close() previously never released the native + // reference taken when the connection was created (pc->Release() + // was missing), leaking the native PeerConnectionInterface on every + // close(). A leak itself is not observable from Java, but repeating + // create/close guards against the fix (calling Release() on the raw + // pointer) crashing or corrupting memory. + for (int i = 0; i < 200; i++) { + RTCConfiguration config = new RTCConfiguration(); + PeerConnectionObserver observer = candidate -> { }; + + RTCPeerConnection connection = factory.createPeerConnection(config, observer); + connection.close(); + } + } } From 6b0ec83e87af311d577a6e29614aee6432f0a680 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sun, 13 Sep 2026 14:03:08 +0200 Subject: [PATCH 06/15] fix: invalidate the MediaStreamTrack handle even if a reference remains dispose() only zeroed the native handle when Release() reported that it dropped the last reference; if another reference happened to be alive elsewhere, the handle was left pointing at an object this Java wrapper no longer owns. A retried dispose() (or any other call) would then Release() a reference count unit that isn't this wrapper's to release, corrupting whatever else still legitimately holds a reference to the track. --- webrtc-jni/src/main/cpp/src/JNI_MediaStreamTrack.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/webrtc-jni/src/main/cpp/src/JNI_MediaStreamTrack.cpp b/webrtc-jni/src/main/cpp/src/JNI_MediaStreamTrack.cpp index 011e6ac0..e23695df 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_MediaStreamTrack.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_MediaStreamTrack.cpp @@ -35,13 +35,16 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_MediaStreamTrack_dispose webrtc::RefCountReleaseStatus status = track->Release(); + // Our reference is gone the moment Release() returns, regardless of + // whether it was the last one, so the handle must not be reused either + // way -- otherwise a retry would Release() a reference this object no + // longer owns. + SetHandle(env, caller, nullptr); + track = nullptr; + if (status != webrtc::RefCountReleaseStatus::kDroppedLastRef) { env->Throw(jni::JavaError(env, "Native object was not deleted. A reference is still around somewhere.")); } - else { - SetHandle(env, caller, nullptr); - track = nullptr; - } } JNIEXPORT jstring JNICALL Java_dev_onvoid_webrtc_media_MediaStreamTrack_getKind From 6f2fbff5ec42920c1e556ea521058cd35d6cbf7a Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sun, 13 Sep 2026 14:04:00 +0200 Subject: [PATCH 07/15] fix: keep a real reference for queried RTP senders, receivers, and transceivers RTCPeerConnection.getSenders()/getReceivers()/getTransceivers(), RTCRtpTransceiver.getSender()/getReceiver(), and the OnTrack/ OnAddTrack/OnRemoveTrack observer callbacks all constructed their Java wrapper from a raw pointer (scoped_refptr::get()) without ever keeping the reference that pointer came from. The temporary scoped_refptr's destructor released that reference at the end of the same statement, so the Java wrapper ended up backed by no owned reference at all -- relying entirely on WebRTC's internal transceiver list to keep the object alive. That list entry is not guaranteed to survive: WebRTC actually erases a stopped transceiver from PeerConnection's own list during a later SetLocalDescription/SetRemoteDescription that recycles its m= section, or during rollback of a not-yet-negotiated transceiver (confirmed against M152's pc/sdp_offer_answer.cc). A Java RTCRtpSender/ RTCRtpReceiver/RTCRtpTransceiver obtained before that point can end up holding a dangling native pointer. Transfer the reference properly instead (scoped_refptr::release(), or a new createOwningObjectArray() for the array-returning queries), and give RTCRtpSender/RTCRtpReceiver/RTCRtpTransceiver a dispose() to release it -- these types are not exclusively owned the way e.g. MediaStreamTrack is (the owning transceiver/connection, and any other independently queried wrapper, keep their own reference), so unlike other dispose() implementations in this codebase, dropping our reference here is normal and is not reported as an error. --- .../src/main/cpp/include/JNI_RTCRtpReceiver.h | 8 + .../src/main/cpp/include/JNI_RTCRtpSender.h | 8 + .../main/cpp/include/JNI_RTCRtpTransceiver.h | 8 + .../src/main/cpp/include/api/WebRTCUtils.h | 27 + .../main/cpp/src/JNI_RTCPeerConnection.cpp | 1188 ++++++++--------- .../src/main/cpp/src/JNI_RTCRtpReceiver.cpp | 16 + .../src/main/cpp/src/JNI_RTCRtpSender.cpp | 16 + .../main/cpp/src/JNI_RTCRtpTransceiver.cpp | 26 +- .../cpp/src/api/PeerConnectionObserver.cpp | 18 +- .../dev/onvoid/webrtc/RTCRtpReceiver.java | 45 +- .../java/dev/onvoid/webrtc/RTCRtpSender.java | 45 +- .../dev/onvoid/webrtc/RTCRtpTransceiver.java | 44 +- .../onvoid/webrtc/RTCPeerConnectionTests.java | 57 + .../onvoid/webrtc/RTCRtpTransceiverTests.java | 36 + 14 files changed, 937 insertions(+), 605 deletions(-) diff --git a/webrtc-jni/src/main/cpp/include/JNI_RTCRtpReceiver.h b/webrtc-jni/src/main/cpp/include/JNI_RTCRtpReceiver.h index 3ff3658f..2a16ba70 100644 --- a/webrtc-jni/src/main/cpp/include/JNI_RTCRtpReceiver.h +++ b/webrtc-jni/src/main/cpp/include/JNI_RTCRtpReceiver.h @@ -47,6 +47,14 @@ extern "C" { JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCRtpReceiver_getSynchronizationSources (JNIEnv *, jobject); + /* + * Class: dev_onvoid_webrtc_RTCRtpReceiver + * Method: dispose + * Signature: ()V + */ + JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCRtpReceiver_dispose + (JNIEnv *, jobject); + #ifdef __cplusplus } #endif diff --git a/webrtc-jni/src/main/cpp/include/JNI_RTCRtpSender.h b/webrtc-jni/src/main/cpp/include/JNI_RTCRtpSender.h index 83f23ffc..eec94f3f 100644 --- a/webrtc-jni/src/main/cpp/include/JNI_RTCRtpSender.h +++ b/webrtc-jni/src/main/cpp/include/JNI_RTCRtpSender.h @@ -63,6 +63,14 @@ extern "C" { JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCRtpSender_getDtmfSender (JNIEnv *, jobject); + /* + * Class: dev_onvoid_webrtc_RTCRtpSender + * Method: dispose + * Signature: ()V + */ + JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCRtpSender_dispose + (JNIEnv *, jobject); + #ifdef __cplusplus } #endif diff --git a/webrtc-jni/src/main/cpp/include/JNI_RTCRtpTransceiver.h b/webrtc-jni/src/main/cpp/include/JNI_RTCRtpTransceiver.h index d0b74192..3379b6fc 100644 --- a/webrtc-jni/src/main/cpp/include/JNI_RTCRtpTransceiver.h +++ b/webrtc-jni/src/main/cpp/include/JNI_RTCRtpTransceiver.h @@ -87,6 +87,14 @@ extern "C" { JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCRtpTransceiver_setCodecPreferences (JNIEnv *, jobject, jobject); + /* + * Class: dev_onvoid_webrtc_RTCRtpTransceiver + * Method: dispose + * Signature: ()V + */ + JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCRtpTransceiver_dispose + (JNIEnv *, jobject); + #ifdef __cplusplus } #endif diff --git a/webrtc-jni/src/main/cpp/include/api/WebRTCUtils.h b/webrtc-jni/src/main/cpp/include/api/WebRTCUtils.h index 87f0f931..166f8144 100644 --- a/webrtc-jni/src/main/cpp/include/api/WebRTCUtils.h +++ b/webrtc-jni/src/main/cpp/include/api/WebRTCUtils.h @@ -49,6 +49,33 @@ namespace jni return objectArray; } + // Same as createObjectArray, but transfers the reference each element of + // `vector` holds into the corresponding Java wrapper instead of letting + // it evaporate when `vector` goes out of scope. Use this when the Java + // wrapper type owns/releases its native reference (e.g. is disposable) + // and nothing else already guarantees the pointer outlives the call -- + // callers of the returned array elements are then responsible for + // disposing them. + template + JavaLocalRef createOwningObjectArray(JNIEnv * env, std::vector> vector) + { + jsize vectorSize = static_cast(vector.size()); + + JavaLocalRef objectArray = JavaFactories::createArray(env, vectorSize); + + if (objectArray.get() == nullptr) { + throw Exception("Create object array of type [%s] failed", typeid(T).name()); + } + + for (jsize i = 0; i < vectorSize; i++) { + JavaLocalRef obj = JavaFactories::create(env, vector[i].release()); + + env->SetObjectArrayElement(objectArray, i, obj.get()); + } + + return objectArray; + } + std::string RTCErrorToString(const webrtc::RTCError & error); } diff --git a/webrtc-jni/src/main/cpp/src/JNI_RTCPeerConnection.cpp b/webrtc-jni/src/main/cpp/src/JNI_RTCPeerConnection.cpp index 5c941456..04821b2f 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_RTCPeerConnection.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_RTCPeerConnection.cpp @@ -1,595 +1,595 @@ -/* - * Copyright 2019 Alex Andres - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -#include "JNI_RTCPeerConnection.h" -#include "api/CreateSessionDescriptionObserver.h" -#include "api/SetSessionDescriptionObserver.h" -#include "api/RTCAnswerOptions.h" -#include "api/RTCConfiguration.h" -#include "api/RTCDataChannelInit.h" -#include "api/RTCIceCandidate.h" -#include "api/RTCOfferOptions.h" -#include "api/RTCRtpTransceiverInit.h" -#include "api/RTCSessionDescription.h" -#include "api/RTCStatsCollectorCallback.h" -#include "api/WebRTCUtils.h" -#include "JavaArray.h" -#include "JavaEnums.h" -#include "JavaError.h" -#include "JavaFactories.h" -#include "JavaIterable.h" -#include "JavaList.h" -#include "JavaNullPointerException.h" -#include "JavaRef.h" -#include "JavaRuntimeException.h" -#include "JavaString.h" -#include "JavaUtils.h" - -#include "api/peer_connection_interface.h" -#include "api/rtp_receiver_interface.h" -#include "api/rtp_sender_interface.h" - -#include -#include - -JNIEXPORT jobjectArray JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getSenders -(JNIEnv * env, jobject caller) -{ - webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); - CHECK_HANDLEV(pc, nullptr); - - jni::JavaLocalRef objectArray; - - try { - objectArray = jni::createObjectArray(env, pc->GetSenders()); - } - catch (...) { - ThrowCxxJavaException(env); - } - - return objectArray.release(); -} - -JNIEXPORT jobjectArray JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getReceivers -(JNIEnv * env, jobject caller) -{ - webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); - CHECK_HANDLEV(pc, nullptr); - - jni::JavaLocalRef objectArray; - - try { - objectArray = jni::createObjectArray(env, pc->GetReceivers()); - } - catch (...) { - ThrowCxxJavaException(env); - } - - return objectArray.release(); -} - -JNIEXPORT jobjectArray JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getTransceivers -(JNIEnv * env, jobject caller) -{ - webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); - CHECK_HANDLEV(pc, nullptr); - - jni::JavaLocalRef objectArray; - - try { - objectArray = jni::createObjectArray(env, pc->GetTransceivers()); - } - catch (...) { - ThrowCxxJavaException(env); - } - - return objectArray.release(); -} - -JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_addTrack -(JNIEnv * env, jobject caller, jobject jTrack, jobject jStreamIds) -{ - if (jTrack == nullptr) { - env->Throw(jni::JavaNullPointerException(env, "MediaStreamTrack must not be null")); - return nullptr; - } - if (jStreamIds == nullptr) { - env->Throw(jni::JavaNullPointerException(env, "Stream IDs must not be null")); - return nullptr; - } - - webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); - CHECK_HANDLEV(pc, nullptr); - - webrtc::MediaStreamTrackInterface * track = GetHandle(env, jTrack); - CHECK_HANDLEV(track, nullptr); - - std::vector streamIDs = jni::JavaList::toStringVector(env, jni::JavaLocalRef(env, jStreamIds)); - - auto result = pc->AddTrack(webrtc::scoped_refptr(track), streamIDs); - - if (result.ok()) { - auto sender = result.MoveValue(); - - return jni::JavaFactories::create(env, sender.release()).release(); - } - - env->Throw(jni::JavaRuntimeException(env, jni::RTCErrorToString(result.error()).c_str())); - - return nullptr; -} - -JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_removeTrack -(JNIEnv * env, jobject caller, jobject jSender) -{ - if (jSender == nullptr) { - env->Throw(jni::JavaNullPointerException(env, "RTCRtpSender must not be null")); - return; - } - - webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); - CHECK_HANDLE(pc); - - webrtc::RtpSenderInterface * sender = GetHandle(env, jSender); - CHECK_HANDLE(sender); - - auto result = pc->RemoveTrackOrError(webrtc::scoped_refptr(sender)); - - if (!result.ok()) { - env->Throw(jni::JavaRuntimeException(env, "Remove track (RTCRtpSender) failed: %s %s", - ToString(result.type()), result.message())); - } -} - -JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_addTransceiver -(JNIEnv * env, jobject caller, jobject jTrack, jobject jTransceiverInit) -{ - if (jTrack == nullptr) { - env->Throw(jni::JavaNullPointerException(env, "MediaStreamTrack must not be null")); - return nullptr; - } - - webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); - CHECK_HANDLEV(pc, nullptr); - - webrtc::MediaStreamTrackInterface * track = GetHandle(env, jTrack); - CHECK_HANDLEV(track, nullptr); - - webrtc::RTCErrorOr> result; - - if (jTransceiverInit != nullptr) { - auto init = jni::RTCRtpTransceiverInit::toNative(env, jni::JavaLocalRef(env, jTransceiverInit)); - - result = pc->AddTransceiver(webrtc::scoped_refptr(track), init); - } - else { - result = pc->AddTransceiver(webrtc::scoped_refptr(track)); - } - - if (result.ok()) { - auto transceiver = result.MoveValue(); - - return jni::JavaFactories::create(env, transceiver.release()).release(); - } - - env->Throw(jni::JavaRuntimeException(env, jni::RTCErrorToString(result.error()).c_str())); - - return nullptr; -} - -JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_createDataChannel -(JNIEnv * env, jobject caller, jstring jLabel, jobject jDict) -{ - if (jLabel == nullptr) { - env->Throw(jni::JavaNullPointerException(env, "Label must not be null")); - return nullptr; - } - if (jDict == nullptr) { - env->Throw(jni::JavaNullPointerException(env, "RTCDataChannelInit must not be null")); - return nullptr; - } - - webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); - CHECK_HANDLEV(pc, nullptr); - - std::string label = jni::JavaString::toNative(env, jni::JavaLocalRef(env, jLabel)); - webrtc::DataChannelInit dict = jni::RTCDataChannelInit::toNative(env, jni::JavaLocalRef(env, jDict)); - - try { - auto result = pc->CreateDataChannelOrError(label, &dict); - - if (!result.ok()) { - env->Throw(jni::JavaRuntimeException(env, "Create DataChannel failed: %s %s", - ToString(result.error().type()), result.error().message())); - - return nullptr; - } - - auto dataChannel = result.MoveValue(); - - return jni::JavaFactories::create(env, dataChannel.release()).release(); - } - catch (...) { - ThrowCxxJavaException(env); - return nullptr; - } -} - -JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_createOffer -(JNIEnv * env, jobject caller, jobject jOptions, jobject jObserver) -{ - if (jOptions == nullptr) { - env->Throw(jni::JavaNullPointerException(env, "RTCOfferOptions must not be null")); - return; - } - if (jObserver == nullptr) { - env->Throw(jni::JavaNullPointerException(env, "CreateSessionDescriptionObserver must not be null")); - return; - } - - webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); - CHECK_HANDLE(pc); - - try { - auto options = jni::RTCOfferOptions::toNative(env, jni::JavaLocalRef(env, jOptions)); - auto observer = new webrtc::RefCountedObject(env, jni::JavaGlobalRef(env, jObserver)); - - pc->CreateOffer(observer, options); - } - catch (...) { - ThrowCxxJavaException(env); - } -} - -JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_createAnswer -(JNIEnv * env, jobject caller, jobject jOptions, jobject jObserver) -{ - if (jOptions == nullptr) { - env->Throw(jni::JavaNullPointerException(env, "RTCAnswerOptions must not be null")); - return; - } - if (jObserver == nullptr) { - env->Throw(jni::JavaNullPointerException(env, "CreateSessionDescriptionObserver must not be null")); - return; - } - - webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); - CHECK_HANDLE(pc); - - try { - auto options = jni::RTCAnswerOptions::toNative(env, jni::JavaLocalRef(env, jOptions)); - auto observer = new webrtc::RefCountedObject(env, jni::JavaGlobalRef(env, jObserver)); - - pc->CreateAnswer(observer, options); - } - catch (...) { - ThrowCxxJavaException(env); - } -} - -JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getCurrentLocalDescription -(JNIEnv * env, jobject caller) -{ - webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); - CHECK_HANDLEV(pc, nullptr); - - if (!pc->current_local_description()) { - return nullptr; - } - - return jni::RTCSessionDescription::toJava(env, pc->current_local_description()).release(); -} - -JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getLocalDescription -(JNIEnv * env, jobject caller) -{ - webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); - CHECK_HANDLEV(pc, nullptr); - - if (!pc->local_description()) { - return nullptr; - } - - return jni::RTCSessionDescription::toJava(env, pc->local_description()).release(); -} - -JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getPendingLocalDescription -(JNIEnv * env, jobject caller) -{ - webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); - CHECK_HANDLEV(pc, nullptr); - - if (!pc->pending_local_description()) { - return nullptr; - } - - return jni::RTCSessionDescription::toJava(env, pc->pending_local_description()).release(); -} - -JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getCurrentRemoteDescription -(JNIEnv * env, jobject caller) -{ - webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); - CHECK_HANDLEV(pc, nullptr); - - if (!pc->current_remote_description()) { - return nullptr; - } - - return jni::RTCSessionDescription::toJava(env, pc->current_remote_description()).release(); -} - -JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getRemoteDescription -(JNIEnv * env, jobject caller) -{ - webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); - CHECK_HANDLEV(pc, nullptr); - - if (!pc->remote_description()) { - return nullptr; - } - - return jni::RTCSessionDescription::toJava(env, pc->remote_description()).release(); -} - -JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getPendingRemoteDescription -(JNIEnv * env, jobject caller) -{ - webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); - CHECK_HANDLEV(pc, nullptr); - - if (!pc->pending_remote_description()) { - return nullptr; - } - - return jni::RTCSessionDescription::toJava(env, pc->pending_remote_description()).release(); -} - -JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_setLocalDescription -(JNIEnv * env, jobject caller, jobject jSessionDesc, jobject jobserver) -{ - if (jSessionDesc == nullptr) { - env->Throw(jni::JavaNullPointerException(env, "RTCSessionDescription must not be null")); - return; - } - if (jobserver == nullptr) { - env->Throw(jni::JavaNullPointerException(env, "SetSessionDescriptionObserver must not be null")); - return; - } - - webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); - CHECK_HANDLE(pc); - - try { - auto desc = jni::RTCSessionDescription::toNative(env, jni::JavaLocalRef(env, jSessionDesc)); - auto observer = new webrtc::RefCountedObject(env, jni::JavaGlobalRef(env, jobserver)); - - pc->SetLocalDescription(observer, desc.release()); - } - catch (...) { - ThrowCxxJavaException(env); - } -} - -JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_setRemoteDescription -(JNIEnv * env, jobject caller, jobject jSessionDesc, jobject jobserver) -{ - if (jSessionDesc == nullptr) { - env->Throw(jni::JavaNullPointerException(env, "RTCSessionDescription must not be null")); - return; - } - if (jobserver == nullptr) { - env->Throw(jni::JavaNullPointerException(env, "SetSessionDescriptionObserver must not be null")); - return; - } - - webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); - CHECK_HANDLE(pc); - - try { - auto desc = jni::RTCSessionDescription::toNative(env, jni::JavaLocalRef(env, jSessionDesc)); - auto observer = new webrtc::RefCountedObject(env, jni::JavaGlobalRef(env, jobserver)); - - pc->SetRemoteDescription(observer, desc.release()); - } - catch (...) { - ThrowCxxJavaException(env); - } -} - -JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_addIceCandidate -(JNIEnv * env, jobject caller, jobject jCandidate) -{ - if (jCandidate == nullptr) { - env->Throw(jni::JavaNullPointerException(env, "RTCIceCandidate must not be null")); - return; - } - - webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); - CHECK_HANDLE(pc); - - try { - auto candidate = jni::RTCIceCandidate::toNative(env, jni::JavaLocalRef(env, jCandidate)); - - pc->AddIceCandidate(candidate.get()); - } - catch (...) { - ThrowCxxJavaException(env); - } -} - -JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getSignalingState -(JNIEnv * env, jobject caller) -{ - webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); - CHECK_HANDLE_DEFAULT(pc, jni::JavaEnums::toJava(env, webrtc::PeerConnectionInterface::SignalingState::kClosed).release()); - - return jni::JavaEnums::toJava(env, pc->signaling_state()).release(); -} - -JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getIceGatheringState -(JNIEnv * env, jobject caller) -{ - webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); - CHECK_HANDLE_DEFAULT(pc, jni::JavaEnums::toJava(env, webrtc::PeerConnectionInterface::IceGatheringState::kIceGatheringNew).release()); - - return jni::JavaEnums::toJava(env, pc->ice_gathering_state()).release(); -} - -JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getIceConnectionState -(JNIEnv * env, jobject caller) -{ - webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); - CHECK_HANDLE_DEFAULT(pc, jni::JavaEnums::toJava(env, webrtc::PeerConnectionInterface::IceConnectionState::kIceConnectionClosed).release()); - - return jni::JavaEnums::toJava(env, pc->ice_connection_state()).release(); -} - -JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getConnectionState -(JNIEnv * env, jobject caller) -{ - webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); - CHECK_HANDLE_DEFAULT(pc, jni::JavaEnums::toJava(env, webrtc::PeerConnectionInterface::PeerConnectionState::kClosed).release()); - - return jni::JavaEnums::toJava(env, pc->peer_connection_state()).release(); -} - -JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getConfiguration -(JNIEnv * env, jobject caller) -{ - webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); - CHECK_HANDLEV(pc, nullptr); - - return jni::RTCConfiguration::toJava(env, pc->GetConfiguration()).release(); -} - -JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_setConfiguration -(JNIEnv * env, jobject caller, jobject jConfig) -{ - if (jConfig == nullptr) { - env->Throw(jni::JavaNullPointerException(env, "RTCConfiguration must not be null")); - return; - } - - webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); - CHECK_HANDLE(pc); - - auto config = jni::RTCConfiguration::toNative(env, jni::JavaLocalRef(env, jConfig)); - - webrtc::RTCError error = pc->SetConfiguration(config); - - if (!error.ok()) { - env->Throw(jni::JavaRuntimeException(env, jni::RTCErrorToString(error).c_str())); - } -} - -JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getStats__Ldev_onvoid_webrtc_RTCStatsCollectorCallback_2 -(JNIEnv * env, jobject caller, jobject jcallback) -{ - webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); - CHECK_HANDLE(pc); - - if (jcallback == nullptr) { - env->Throw(jni::JavaNullPointerException(env, "RTCStatsCollectorCallback is null")); - return; - } - - auto callback = new webrtc::RefCountedObject(env, jni::JavaGlobalRef(env, jcallback)); - - pc->GetStats(callback); -} - -JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getStats__Ldev_onvoid_webrtc_RTCRtpReceiver_2Ldev_onvoid_webrtc_RTCStatsCollectorCallback_2 -(JNIEnv * env, jobject caller, jobject jreceiver, jobject jcallback) -{ - webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); - CHECK_HANDLE(pc); - - if (jreceiver == nullptr) { - env->Throw(jni::JavaNullPointerException(env, "RTCRtpReceiver is null")); - return; - } - if (jcallback == nullptr) { - env->Throw(jni::JavaNullPointerException(env, "RTCStatsCollectorCallback is null")); - return; - } - - webrtc::RtpReceiverInterface * receiver = GetHandle(env, jreceiver); - CHECK_HANDLE(receiver); - - auto callback = new webrtc::RefCountedObject(env, jni::JavaGlobalRef(env, jcallback)); - - pc->GetStats(webrtc::scoped_refptr(receiver), webrtc::scoped_refptr(callback)); -} - -JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getStats__Ldev_onvoid_webrtc_RTCRtpSender_2Ldev_onvoid_webrtc_RTCStatsCollectorCallback_2 -(JNIEnv * env, jobject caller, jobject jsender, jobject jcallback) -{ - webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); - CHECK_HANDLE(pc); - - if (jsender == nullptr) { - env->Throw(jni::JavaNullPointerException(env, "RTCRtpSender is null")); - return; - } - if (jcallback == nullptr) { - env->Throw(jni::JavaNullPointerException(env, "RTCStatsCollectorCallback is null")); - return; - } - - webrtc::RtpSenderInterface * sender = GetHandle(env, jsender); - CHECK_HANDLE(sender); - - auto callback = new webrtc::RefCountedObject(env, jni::JavaGlobalRef(env, jcallback)); - - pc->GetStats(webrtc::scoped_refptr(sender), webrtc::scoped_refptr(callback)); -} - -JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_restartIce -(JNIEnv * env, jobject caller) -{ - webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); - CHECK_HANDLE(pc); - - pc->RestartIce(); -} - -JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_close -(JNIEnv * env, jobject caller) -{ - webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); - CHECK_HANDLE(pc); - - try { - pc->Close(); - - SetHandle(env, caller, nullptr); - - auto observer = GetHandle(env, caller, "observerHandle"); - - if (observer) { - SetHandle(env, caller, "observerHandle", nullptr); - delete observer; - } - - // Drop the owning reference taken when the PeerConnection was handed - // to Java in PeerConnectionFactory::createPeerConnection. - pc->Release(); - } - catch (...) { - ThrowCxxJavaException(env); - } +/* + * Copyright 2019 Alex Andres + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "JNI_RTCPeerConnection.h" +#include "api/CreateSessionDescriptionObserver.h" +#include "api/SetSessionDescriptionObserver.h" +#include "api/RTCAnswerOptions.h" +#include "api/RTCConfiguration.h" +#include "api/RTCDataChannelInit.h" +#include "api/RTCIceCandidate.h" +#include "api/RTCOfferOptions.h" +#include "api/RTCRtpTransceiverInit.h" +#include "api/RTCSessionDescription.h" +#include "api/RTCStatsCollectorCallback.h" +#include "api/WebRTCUtils.h" +#include "JavaArray.h" +#include "JavaEnums.h" +#include "JavaError.h" +#include "JavaFactories.h" +#include "JavaIterable.h" +#include "JavaList.h" +#include "JavaNullPointerException.h" +#include "JavaRef.h" +#include "JavaRuntimeException.h" +#include "JavaString.h" +#include "JavaUtils.h" + +#include "api/peer_connection_interface.h" +#include "api/rtp_receiver_interface.h" +#include "api/rtp_sender_interface.h" + +#include +#include + +JNIEXPORT jobjectArray JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getSenders +(JNIEnv * env, jobject caller) +{ + webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); + CHECK_HANDLEV(pc, nullptr); + + jni::JavaLocalRef objectArray; + + try { + objectArray = jni::createOwningObjectArray(env, pc->GetSenders()); + } + catch (...) { + ThrowCxxJavaException(env); + } + + return objectArray.release(); +} + +JNIEXPORT jobjectArray JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getReceivers +(JNIEnv * env, jobject caller) +{ + webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); + CHECK_HANDLEV(pc, nullptr); + + jni::JavaLocalRef objectArray; + + try { + objectArray = jni::createOwningObjectArray(env, pc->GetReceivers()); + } + catch (...) { + ThrowCxxJavaException(env); + } + + return objectArray.release(); +} + +JNIEXPORT jobjectArray JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getTransceivers +(JNIEnv * env, jobject caller) +{ + webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); + CHECK_HANDLEV(pc, nullptr); + + jni::JavaLocalRef objectArray; + + try { + objectArray = jni::createOwningObjectArray(env, pc->GetTransceivers()); + } + catch (...) { + ThrowCxxJavaException(env); + } + + return objectArray.release(); +} + +JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_addTrack +(JNIEnv * env, jobject caller, jobject jTrack, jobject jStreamIds) +{ + if (jTrack == nullptr) { + env->Throw(jni::JavaNullPointerException(env, "MediaStreamTrack must not be null")); + return nullptr; + } + if (jStreamIds == nullptr) { + env->Throw(jni::JavaNullPointerException(env, "Stream IDs must not be null")); + return nullptr; + } + + webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); + CHECK_HANDLEV(pc, nullptr); + + webrtc::MediaStreamTrackInterface * track = GetHandle(env, jTrack); + CHECK_HANDLEV(track, nullptr); + + std::vector streamIDs = jni::JavaList::toStringVector(env, jni::JavaLocalRef(env, jStreamIds)); + + auto result = pc->AddTrack(webrtc::scoped_refptr(track), streamIDs); + + if (result.ok()) { + auto sender = result.MoveValue(); + + return jni::JavaFactories::create(env, sender.release()).release(); + } + + env->Throw(jni::JavaRuntimeException(env, jni::RTCErrorToString(result.error()).c_str())); + + return nullptr; +} + +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_removeTrack +(JNIEnv * env, jobject caller, jobject jSender) +{ + if (jSender == nullptr) { + env->Throw(jni::JavaNullPointerException(env, "RTCRtpSender must not be null")); + return; + } + + webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); + CHECK_HANDLE(pc); + + webrtc::RtpSenderInterface * sender = GetHandle(env, jSender); + CHECK_HANDLE(sender); + + auto result = pc->RemoveTrackOrError(webrtc::scoped_refptr(sender)); + + if (!result.ok()) { + env->Throw(jni::JavaRuntimeException(env, "Remove track (RTCRtpSender) failed: %s %s", + ToString(result.type()), result.message())); + } +} + +JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_addTransceiver +(JNIEnv * env, jobject caller, jobject jTrack, jobject jTransceiverInit) +{ + if (jTrack == nullptr) { + env->Throw(jni::JavaNullPointerException(env, "MediaStreamTrack must not be null")); + return nullptr; + } + + webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); + CHECK_HANDLEV(pc, nullptr); + + webrtc::MediaStreamTrackInterface * track = GetHandle(env, jTrack); + CHECK_HANDLEV(track, nullptr); + + webrtc::RTCErrorOr> result; + + if (jTransceiverInit != nullptr) { + auto init = jni::RTCRtpTransceiverInit::toNative(env, jni::JavaLocalRef(env, jTransceiverInit)); + + result = pc->AddTransceiver(webrtc::scoped_refptr(track), init); + } + else { + result = pc->AddTransceiver(webrtc::scoped_refptr(track)); + } + + if (result.ok()) { + auto transceiver = result.MoveValue(); + + return jni::JavaFactories::create(env, transceiver.release()).release(); + } + + env->Throw(jni::JavaRuntimeException(env, jni::RTCErrorToString(result.error()).c_str())); + + return nullptr; +} + +JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_createDataChannel +(JNIEnv * env, jobject caller, jstring jLabel, jobject jDict) +{ + if (jLabel == nullptr) { + env->Throw(jni::JavaNullPointerException(env, "Label must not be null")); + return nullptr; + } + if (jDict == nullptr) { + env->Throw(jni::JavaNullPointerException(env, "RTCDataChannelInit must not be null")); + return nullptr; + } + + webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); + CHECK_HANDLEV(pc, nullptr); + + std::string label = jni::JavaString::toNative(env, jni::JavaLocalRef(env, jLabel)); + webrtc::DataChannelInit dict = jni::RTCDataChannelInit::toNative(env, jni::JavaLocalRef(env, jDict)); + + try { + auto result = pc->CreateDataChannelOrError(label, &dict); + + if (!result.ok()) { + env->Throw(jni::JavaRuntimeException(env, "Create DataChannel failed: %s %s", + ToString(result.error().type()), result.error().message())); + + return nullptr; + } + + auto dataChannel = result.MoveValue(); + + return jni::JavaFactories::create(env, dataChannel.release()).release(); + } + catch (...) { + ThrowCxxJavaException(env); + return nullptr; + } +} + +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_createOffer +(JNIEnv * env, jobject caller, jobject jOptions, jobject jObserver) +{ + if (jOptions == nullptr) { + env->Throw(jni::JavaNullPointerException(env, "RTCOfferOptions must not be null")); + return; + } + if (jObserver == nullptr) { + env->Throw(jni::JavaNullPointerException(env, "CreateSessionDescriptionObserver must not be null")); + return; + } + + webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); + CHECK_HANDLE(pc); + + try { + auto options = jni::RTCOfferOptions::toNative(env, jni::JavaLocalRef(env, jOptions)); + auto observer = new webrtc::RefCountedObject(env, jni::JavaGlobalRef(env, jObserver)); + + pc->CreateOffer(observer, options); + } + catch (...) { + ThrowCxxJavaException(env); + } +} + +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_createAnswer +(JNIEnv * env, jobject caller, jobject jOptions, jobject jObserver) +{ + if (jOptions == nullptr) { + env->Throw(jni::JavaNullPointerException(env, "RTCAnswerOptions must not be null")); + return; + } + if (jObserver == nullptr) { + env->Throw(jni::JavaNullPointerException(env, "CreateSessionDescriptionObserver must not be null")); + return; + } + + webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); + CHECK_HANDLE(pc); + + try { + auto options = jni::RTCAnswerOptions::toNative(env, jni::JavaLocalRef(env, jOptions)); + auto observer = new webrtc::RefCountedObject(env, jni::JavaGlobalRef(env, jObserver)); + + pc->CreateAnswer(observer, options); + } + catch (...) { + ThrowCxxJavaException(env); + } +} + +JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getCurrentLocalDescription +(JNIEnv * env, jobject caller) +{ + webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); + CHECK_HANDLEV(pc, nullptr); + + if (!pc->current_local_description()) { + return nullptr; + } + + return jni::RTCSessionDescription::toJava(env, pc->current_local_description()).release(); +} + +JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getLocalDescription +(JNIEnv * env, jobject caller) +{ + webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); + CHECK_HANDLEV(pc, nullptr); + + if (!pc->local_description()) { + return nullptr; + } + + return jni::RTCSessionDescription::toJava(env, pc->local_description()).release(); +} + +JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getPendingLocalDescription +(JNIEnv * env, jobject caller) +{ + webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); + CHECK_HANDLEV(pc, nullptr); + + if (!pc->pending_local_description()) { + return nullptr; + } + + return jni::RTCSessionDescription::toJava(env, pc->pending_local_description()).release(); +} + +JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getCurrentRemoteDescription +(JNIEnv * env, jobject caller) +{ + webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); + CHECK_HANDLEV(pc, nullptr); + + if (!pc->current_remote_description()) { + return nullptr; + } + + return jni::RTCSessionDescription::toJava(env, pc->current_remote_description()).release(); +} + +JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getRemoteDescription +(JNIEnv * env, jobject caller) +{ + webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); + CHECK_HANDLEV(pc, nullptr); + + if (!pc->remote_description()) { + return nullptr; + } + + return jni::RTCSessionDescription::toJava(env, pc->remote_description()).release(); +} + +JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getPendingRemoteDescription +(JNIEnv * env, jobject caller) +{ + webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); + CHECK_HANDLEV(pc, nullptr); + + if (!pc->pending_remote_description()) { + return nullptr; + } + + return jni::RTCSessionDescription::toJava(env, pc->pending_remote_description()).release(); +} + +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_setLocalDescription +(JNIEnv * env, jobject caller, jobject jSessionDesc, jobject jobserver) +{ + if (jSessionDesc == nullptr) { + env->Throw(jni::JavaNullPointerException(env, "RTCSessionDescription must not be null")); + return; + } + if (jobserver == nullptr) { + env->Throw(jni::JavaNullPointerException(env, "SetSessionDescriptionObserver must not be null")); + return; + } + + webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); + CHECK_HANDLE(pc); + + try { + auto desc = jni::RTCSessionDescription::toNative(env, jni::JavaLocalRef(env, jSessionDesc)); + auto observer = new webrtc::RefCountedObject(env, jni::JavaGlobalRef(env, jobserver)); + + pc->SetLocalDescription(observer, desc.release()); + } + catch (...) { + ThrowCxxJavaException(env); + } +} + +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_setRemoteDescription +(JNIEnv * env, jobject caller, jobject jSessionDesc, jobject jobserver) +{ + if (jSessionDesc == nullptr) { + env->Throw(jni::JavaNullPointerException(env, "RTCSessionDescription must not be null")); + return; + } + if (jobserver == nullptr) { + env->Throw(jni::JavaNullPointerException(env, "SetSessionDescriptionObserver must not be null")); + return; + } + + webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); + CHECK_HANDLE(pc); + + try { + auto desc = jni::RTCSessionDescription::toNative(env, jni::JavaLocalRef(env, jSessionDesc)); + auto observer = new webrtc::RefCountedObject(env, jni::JavaGlobalRef(env, jobserver)); + + pc->SetRemoteDescription(observer, desc.release()); + } + catch (...) { + ThrowCxxJavaException(env); + } +} + +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_addIceCandidate +(JNIEnv * env, jobject caller, jobject jCandidate) +{ + if (jCandidate == nullptr) { + env->Throw(jni::JavaNullPointerException(env, "RTCIceCandidate must not be null")); + return; + } + + webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); + CHECK_HANDLE(pc); + + try { + auto candidate = jni::RTCIceCandidate::toNative(env, jni::JavaLocalRef(env, jCandidate)); + + pc->AddIceCandidate(candidate.get()); + } + catch (...) { + ThrowCxxJavaException(env); + } +} + +JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getSignalingState +(JNIEnv * env, jobject caller) +{ + webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); + CHECK_HANDLE_DEFAULT(pc, jni::JavaEnums::toJava(env, webrtc::PeerConnectionInterface::SignalingState::kClosed).release()); + + return jni::JavaEnums::toJava(env, pc->signaling_state()).release(); +} + +JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getIceGatheringState +(JNIEnv * env, jobject caller) +{ + webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); + CHECK_HANDLE_DEFAULT(pc, jni::JavaEnums::toJava(env, webrtc::PeerConnectionInterface::IceGatheringState::kIceGatheringNew).release()); + + return jni::JavaEnums::toJava(env, pc->ice_gathering_state()).release(); +} + +JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getIceConnectionState +(JNIEnv * env, jobject caller) +{ + webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); + CHECK_HANDLE_DEFAULT(pc, jni::JavaEnums::toJava(env, webrtc::PeerConnectionInterface::IceConnectionState::kIceConnectionClosed).release()); + + return jni::JavaEnums::toJava(env, pc->ice_connection_state()).release(); +} + +JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getConnectionState +(JNIEnv * env, jobject caller) +{ + webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); + CHECK_HANDLE_DEFAULT(pc, jni::JavaEnums::toJava(env, webrtc::PeerConnectionInterface::PeerConnectionState::kClosed).release()); + + return jni::JavaEnums::toJava(env, pc->peer_connection_state()).release(); +} + +JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getConfiguration +(JNIEnv * env, jobject caller) +{ + webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); + CHECK_HANDLEV(pc, nullptr); + + return jni::RTCConfiguration::toJava(env, pc->GetConfiguration()).release(); +} + +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_setConfiguration +(JNIEnv * env, jobject caller, jobject jConfig) +{ + if (jConfig == nullptr) { + env->Throw(jni::JavaNullPointerException(env, "RTCConfiguration must not be null")); + return; + } + + webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); + CHECK_HANDLE(pc); + + auto config = jni::RTCConfiguration::toNative(env, jni::JavaLocalRef(env, jConfig)); + + webrtc::RTCError error = pc->SetConfiguration(config); + + if (!error.ok()) { + env->Throw(jni::JavaRuntimeException(env, jni::RTCErrorToString(error).c_str())); + } +} + +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getStats__Ldev_onvoid_webrtc_RTCStatsCollectorCallback_2 +(JNIEnv * env, jobject caller, jobject jcallback) +{ + webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); + CHECK_HANDLE(pc); + + if (jcallback == nullptr) { + env->Throw(jni::JavaNullPointerException(env, "RTCStatsCollectorCallback is null")); + return; + } + + auto callback = new webrtc::RefCountedObject(env, jni::JavaGlobalRef(env, jcallback)); + + pc->GetStats(callback); +} + +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getStats__Ldev_onvoid_webrtc_RTCRtpReceiver_2Ldev_onvoid_webrtc_RTCStatsCollectorCallback_2 +(JNIEnv * env, jobject caller, jobject jreceiver, jobject jcallback) +{ + webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); + CHECK_HANDLE(pc); + + if (jreceiver == nullptr) { + env->Throw(jni::JavaNullPointerException(env, "RTCRtpReceiver is null")); + return; + } + if (jcallback == nullptr) { + env->Throw(jni::JavaNullPointerException(env, "RTCStatsCollectorCallback is null")); + return; + } + + webrtc::RtpReceiverInterface * receiver = GetHandle(env, jreceiver); + CHECK_HANDLE(receiver); + + auto callback = new webrtc::RefCountedObject(env, jni::JavaGlobalRef(env, jcallback)); + + pc->GetStats(webrtc::scoped_refptr(receiver), webrtc::scoped_refptr(callback)); +} + +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_getStats__Ldev_onvoid_webrtc_RTCRtpSender_2Ldev_onvoid_webrtc_RTCStatsCollectorCallback_2 +(JNIEnv * env, jobject caller, jobject jsender, jobject jcallback) +{ + webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); + CHECK_HANDLE(pc); + + if (jsender == nullptr) { + env->Throw(jni::JavaNullPointerException(env, "RTCRtpSender is null")); + return; + } + if (jcallback == nullptr) { + env->Throw(jni::JavaNullPointerException(env, "RTCStatsCollectorCallback is null")); + return; + } + + webrtc::RtpSenderInterface * sender = GetHandle(env, jsender); + CHECK_HANDLE(sender); + + auto callback = new webrtc::RefCountedObject(env, jni::JavaGlobalRef(env, jcallback)); + + pc->GetStats(webrtc::scoped_refptr(sender), webrtc::scoped_refptr(callback)); +} + +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_restartIce +(JNIEnv * env, jobject caller) +{ + webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); + CHECK_HANDLE(pc); + + pc->RestartIce(); +} + +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_close +(JNIEnv * env, jobject caller) +{ + webrtc::PeerConnectionInterface * pc = GetHandle(env, caller); + CHECK_HANDLE(pc); + + try { + pc->Close(); + + SetHandle(env, caller, nullptr); + + auto observer = GetHandle(env, caller, "observerHandle"); + + if (observer) { + SetHandle(env, caller, "observerHandle", nullptr); + delete observer; + } + + // Drop the owning reference taken when the PeerConnection was handed + // to Java in PeerConnectionFactory::createPeerConnection. + pc->Release(); + } + catch (...) { + ThrowCxxJavaException(env); + } } \ 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 7e74734d..41a9ca0d 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_RTCRtpReceiver.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_RTCRtpReceiver.cpp @@ -96,4 +96,20 @@ JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCRtpReceiver_getSynchronizati auto list = jni::JavaList::toArrayList(env, ssrc, jni::RTCRtpSynchronizationSource::toJava); return list.release(); +} + +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCRtpReceiver_dispose +(JNIEnv * env, jobject caller) +{ + webrtc::RtpReceiverInterface * receiver = GetHandle(env, caller); + CHECK_HANDLE(receiver); + + // Unlike e.g. MediaStreamTrack, an RTCRtpReceiver is not exclusively + // owned by one Java wrapper: the owning RtpTransceiver keeps its own + // reference, and other Java wrappers may have been obtained via separate + // getReceivers()/getReceiver() calls. Dropping our reference here is + // expected to leave others around, so it is not reported as an error. + receiver->Release(); + + SetHandle(env, caller, nullptr); } \ No newline at end of file diff --git a/webrtc-jni/src/main/cpp/src/JNI_RTCRtpSender.cpp b/webrtc-jni/src/main/cpp/src/JNI_RTCRtpSender.cpp index 2899121e..634abae1 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_RTCRtpSender.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_RTCRtpSender.cpp @@ -120,4 +120,20 @@ JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCRtpSender_getDtmfSender SetHandle(env, jDtmfSender, dtmfSender.get()); return jDtmfSender.release(); +} + +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCRtpSender_dispose +(JNIEnv * env, jobject caller) +{ + webrtc::RtpSenderInterface * sender = GetHandle(env, caller); + CHECK_HANDLE(sender); + + // Unlike e.g. MediaStreamTrack, an RTCRtpSender is not exclusively owned + // by one Java wrapper: the owning RtpTransceiver keeps its own reference, + // and other Java wrappers may have been obtained via separate + // getSenders()/getSender() calls. Dropping our reference here is + // expected to leave others around, so it is not reported as an error. + sender->Release(); + + SetHandle(env, caller, nullptr); } \ No newline at end of file diff --git a/webrtc-jni/src/main/cpp/src/JNI_RTCRtpTransceiver.cpp b/webrtc-jni/src/main/cpp/src/JNI_RTCRtpTransceiver.cpp index d752bb6e..be116e24 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_RTCRtpTransceiver.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_RTCRtpTransceiver.cpp @@ -45,7 +45,10 @@ JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCRtpTransceiver_getSender webrtc::scoped_refptr sender = transceiver->sender(); - return jni::JavaFactories::create(env, sender.get()).release(); + // Transfer the reference this local scoped_refptr holds into the Java + // wrapper, which owns and disposes it -- otherwise the wrapper would be + // left with a pointer nothing on the Java/JNI side keeps alive. + return jni::JavaFactories::create(env, sender.release()).release(); } JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCRtpTransceiver_getReceiver @@ -56,7 +59,10 @@ JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCRtpTransceiver_getReceiver webrtc::scoped_refptr receiver = transceiver->receiver(); - return jni::JavaFactories::create(env, receiver.get()).release(); + // Transfer the reference this local scoped_refptr holds into the Java + // wrapper, which owns and disposes it -- otherwise the wrapper would be + // left with a pointer nothing on the Java/JNI side keeps alive. + return jni::JavaFactories::create(env, receiver.release()).release(); } JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCRtpTransceiver_getDirection @@ -150,4 +156,20 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCRtpTransceiver_setCodecPreferen catch (...) { ThrowCxxJavaException(env); } +} + +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCRtpTransceiver_dispose +(JNIEnv * env, jobject caller) +{ + webrtc::RtpTransceiverInterface * transceiver = GetHandle(env, caller); + CHECK_HANDLE(transceiver); + + // Unlike e.g. MediaStreamTrack, an RTCRtpTransceiver is not exclusively + // owned by one Java wrapper: the owning PeerConnection keeps its own + // reference, and other Java wrappers may have been obtained via separate + // getTransceivers() calls. Dropping our reference here is expected to + // leave others around, so it is not reported as an error. + transceiver->Release(); + + SetHandle(env, caller, nullptr); } \ No newline at end of file diff --git a/webrtc-jni/src/main/cpp/src/api/PeerConnectionObserver.cpp b/webrtc-jni/src/main/cpp/src/api/PeerConnectionObserver.cpp index fc936506..c89f88bf 100644 --- a/webrtc-jni/src/main/cpp/src/api/PeerConnectionObserver.cpp +++ b/webrtc-jni/src/main/cpp/src/api/PeerConnectionObserver.cpp @@ -74,7 +74,11 @@ namespace jni return; } - auto jTransceiver = JavaFactories::create(env, transceiver.get()); + // transceiver is passed by value, so this call owns one reference on + // it; transfer that reference into the Java wrapper, which disposes + // it, instead of releasing it right back when transceiver goes out + // of scope and leaving the wrapper's pointer unowned. + auto jTransceiver = JavaFactories::create(env, transceiver.release()); env->CallVoidMethod(observer, javaClass->onTrack, jTransceiver.get()); @@ -94,7 +98,11 @@ namespace jni try { streamArray = createObjectArray(env, streams); - auto jReceiver = JavaFactories::create(env, receiver.get()); + // receiver is passed by value, so this call owns one reference on + // it; transfer that reference into the Java wrapper, which + // disposes it, instead of releasing it right back when receiver + // goes out of scope and leaving the wrapper's pointer unowned. + auto jReceiver = JavaFactories::create(env, receiver.release()); env->CallVoidMethod(observer, javaClass->onAddTrack, jReceiver.get(), streamArray.get()); } @@ -113,7 +121,11 @@ namespace jni return; } - auto jReceiver = JavaFactories::create(env, receiver.get()); + // receiver is passed by value, so this call owns one reference on + // it; transfer that reference into the Java wrapper, which disposes + // it, instead of releasing it right back when receiver goes out of + // scope and leaving the wrapper's pointer unowned. + auto jReceiver = JavaFactories::create(env, receiver.release()); env->CallVoidMethod(observer, javaClass->onRemoveTrack, jReceiver.get()); diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpReceiver.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpReceiver.java index 0a4754bd..03778a79 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpReceiver.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpReceiver.java @@ -18,7 +18,7 @@ import java.util.List; -import dev.onvoid.webrtc.internal.NativeObject; +import dev.onvoid.webrtc.internal.DisposableNativeObject; import dev.onvoid.webrtc.media.MediaStreamTrack; /** @@ -27,7 +27,7 @@ * * @author Alex Andres */ -public class RTCRtpReceiver extends NativeObject { +public class RTCRtpReceiver extends DisposableNativeObject { /** * Constructor to be used by the native api. @@ -83,4 +83,45 @@ private RTCRtpReceiver() { */ public native List getSynchronizationSources(); + /** + * Releases the native reference held by this RTCRtpReceiver instance. + *

+ * An RTCRtpReceiver is not exclusively owned by this instance: the + * underlying receiver may be kept alive by its {@link RTCRtpTransceiver} + * or by other RTCRtpReceiver instances obtained via separate calls to + * {@link RTCPeerConnection#getReceivers()} or {@link + * RTCRtpTransceiver#getReceiver()}. Disposing this instance only drops + * the reference it holds and does not affect the receiver itself or any + * other instance referring to it. + */ + @Override + public native void dispose(); + + /** + * Two RTCRtpReceiver instances are equal if they are bound to the same + * native receiver, e.g. when obtained from two separate calls to {@link + * RTCPeerConnection#getReceivers()}. A disposed instance is never equal + * to anything but itself. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof RTCRtpReceiver)) { + return false; + } + + long handle = getNativeHandle(); + + return handle != 0 && handle == ((RTCRtpReceiver) obj).getNativeHandle(); + } + + @Override + public int hashCode() { + long handle = getNativeHandle(); + + return handle == 0 ? System.identityHashCode(this) : Long.hashCode(handle); + } + } diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpSender.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpSender.java index 5c053a02..155bb896 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpSender.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpSender.java @@ -18,7 +18,7 @@ import java.util.List; -import dev.onvoid.webrtc.internal.NativeObject; +import dev.onvoid.webrtc.internal.DisposableNativeObject; import dev.onvoid.webrtc.media.MediaStreamTrack; /** @@ -29,7 +29,7 @@ * * @author Alex Andres */ -public class RTCRtpSender extends NativeObject { +public class RTCRtpSender extends DisposableNativeObject { /** * Constructor to be used by the native api. @@ -112,4 +112,45 @@ private RTCRtpSender() { */ public native RTCDtmfSender getDtmfSender(); + /** + * Releases the native reference held by this RTCRtpSender instance. + *

+ * An RTCRtpSender is not exclusively owned by this instance: the + * underlying sender may be kept alive by its {@link RTCRtpTransceiver} + * or by other RTCRtpSender instances obtained via separate calls to + * {@link RTCPeerConnection#getSenders()} or {@link + * RTCRtpTransceiver#getSender()}. Disposing this instance only drops the + * reference it holds and does not affect the sender itself or any other + * instance referring to it. + */ + @Override + public native void dispose(); + + /** + * Two RTCRtpSender instances are equal if they are bound to the same + * native sender, e.g. when obtained from two separate calls to {@link + * RTCPeerConnection#getSenders()}. A disposed instance is never equal to + * anything but itself. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof RTCRtpSender)) { + return false; + } + + long handle = getNativeHandle(); + + return handle != 0 && handle == ((RTCRtpSender) obj).getNativeHandle(); + } + + @Override + public int hashCode() { + long handle = getNativeHandle(); + + return handle == 0 ? System.identityHashCode(this) : Long.hashCode(handle); + } + } diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpTransceiver.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpTransceiver.java index 6f2b040d..efc5702b 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpTransceiver.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpTransceiver.java @@ -16,7 +16,7 @@ package dev.onvoid.webrtc; -import dev.onvoid.webrtc.internal.NativeObject; +import dev.onvoid.webrtc.internal.DisposableNativeObject; import java.util.List; @@ -26,7 +26,7 @@ * * @author Alex Andres */ -public class RTCRtpTransceiver extends NativeObject { +public class RTCRtpTransceiver extends DisposableNativeObject { /** * Constructor to be used by the native api. @@ -139,4 +139,44 @@ private RTCRtpTransceiver() { public native void setCodecPreferences( List preferences); + /** + * Releases the native reference held by this RTCRtpTransceiver instance. + *

+ * An RTCRtpTransceiver is not exclusively owned by this instance: the + * underlying transceiver may be kept alive by its {@link + * RTCPeerConnection} or by other RTCRtpTransceiver instances obtained via + * separate calls to {@link RTCPeerConnection#getTransceivers()}. + * Disposing this instance only drops the reference it holds and does not + * affect the transceiver itself or any other instance referring to it. + */ + @Override + public native void dispose(); + + /** + * Two RTCRtpTransceiver instances are equal if they are bound to the + * same native transceiver, e.g. when obtained from two separate calls to + * {@link RTCPeerConnection#getTransceivers()}. A disposed instance is + * never equal to anything but itself. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof RTCRtpTransceiver)) { + return false; + } + + long handle = getNativeHandle(); + + return handle != 0 && handle == ((RTCRtpTransceiver) obj).getNativeHandle(); + } + + @Override + public int hashCode() { + long handle = getNativeHandle(); + + return handle == 0 ? System.identityHashCode(this) : Long.hashCode(handle); + } + } diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/RTCPeerConnectionTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/RTCPeerConnectionTests.java index c0727b5f..3885a7ec 100644 --- a/webrtc/src/test/java/dev/onvoid/webrtc/RTCPeerConnectionTests.java +++ b/webrtc/src/test/java/dev/onvoid/webrtc/RTCPeerConnectionTests.java @@ -130,6 +130,63 @@ void addTrack() { assertEquals(2, receivers.length); } + @Test + void queriedRefsDisposeIndependently() { + // Regression guard: getSenders()/getReceivers()/getTransceivers() + // used to hand out Java wrappers backed by no owned native + // reference at all (relying entirely on WebRTC's internal + // bookkeeping to keep the pointer valid), which could dangle once a + // transceiver was stopped and the connection later renegotiated + // with m-line recycling, or a pending offer was rolled back. Each + // call now transfers a real, independent reference into the + // returned wrappers, so disposing one instance must not affect a + // separately queried instance of the same underlying sender, + // receiver, or transceiver. + AudioTrackSource audioSource = factory.createAudioSource(new AudioOptions()); + AudioTrack audioTrack = factory.createAudioTrack("audioTrack", audioSource); + + List streamIds = new ArrayList<>(); + streamIds.add("stream-0"); + + peerConnection.addTrack(audioTrack, streamIds); + + RTCRtpSender[] firstSenders = peerConnection.getSenders(); + RTCRtpSender[] secondSenders = peerConnection.getSenders(); + RTCRtpReceiver[] firstReceivers = peerConnection.getReceivers(); + RTCRtpReceiver[] secondReceivers = peerConnection.getReceivers(); + RTCRtpTransceiver[] firstTransceivers = peerConnection.getTransceivers(); + RTCRtpTransceiver[] secondTransceivers = peerConnection.getTransceivers(); + + assertEquals(1, firstSenders.length); + assertEquals(1, firstReceivers.length); + assertEquals(1, firstTransceivers.length); + + // Independently queried wrappers around the same underlying object + // must compare equal and hash consistently. + assertEquals(firstSenders[0], secondSenders[0]); + assertEquals(firstSenders[0].hashCode(), secondSenders[0].hashCode()); + assertEquals(firstReceivers[0], secondReceivers[0]); + assertEquals(firstTransceivers[0], secondTransceivers[0]); + + assertDoesNotThrow(() -> firstSenders[0].dispose()); + assertDoesNotThrow(() -> firstReceivers[0].dispose()); + assertDoesNotThrow(() -> firstTransceivers[0].dispose()); + + // A disposed instance is never equal to anything but itself, even a + // previously-equal wrapper around the same (still-live) object. + assertNotEquals(firstSenders[0], secondSenders[0]); + + // The second set of wrappers must remain fully usable after the + // first set was disposed. + assertNotNull(secondSenders[0].getTrack()); + assertNotNull(secondTransceivers[0].getSender()); + assertNotNull(secondTransceivers[0].getReceiver()); + + assertDoesNotThrow(() -> secondSenders[0].dispose()); + assertDoesNotThrow(() -> secondReceivers[0].dispose()); + assertDoesNotThrow(() -> secondTransceivers[0].dispose()); + } + @Test void removeTrack() { AudioTrackSource audioSource = factory.createAudioSource(new AudioOptions()); diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/RTCRtpTransceiverTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/RTCRtpTransceiverTests.java index a743e5a5..7adb2bb4 100644 --- a/webrtc/src/test/java/dev/onvoid/webrtc/RTCRtpTransceiverTests.java +++ b/webrtc/src/test/java/dev/onvoid/webrtc/RTCRtpTransceiverTests.java @@ -1,5 +1,9 @@ package dev.onvoid.webrtc; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + import dev.onvoid.webrtc.media.MediaType; import dev.onvoid.webrtc.media.audio.AudioOptions; import dev.onvoid.webrtc.media.audio.AudioTrackSource; @@ -58,4 +62,36 @@ void setCodecCapabilities() { videoTransceiver.setCodecPreferences(videoPreferences); } + @Test + void senderAndReceiverAreIndependentlyDisposable() { + AudioTrackSource audioSource = factory.createAudioSource(new AudioOptions()); + AudioTrack audioTrack = factory.createAudioTrack("audioTrack", audioSource); + + RTCPeerConnection peerConnection = connection.getPeerConnection(); + RTCRtpTransceiver transceiver = peerConnection.addTransceiver(audioTrack, + new RTCRtpTransceiverInit()); + + // getSender()/getReceiver() used to hand out Java wrappers backed by + // no owned native reference at all. Querying twice must yield + // independently disposable instances, and disposing one must not + // affect the other or the transceiver itself. + RTCRtpSender firstSender = transceiver.getSender(); + RTCRtpSender secondSender = transceiver.getSender(); + RTCRtpReceiver firstReceiver = transceiver.getReceiver(); + RTCRtpReceiver secondReceiver = transceiver.getReceiver(); + + assertNotNull(firstSender); + assertNotNull(firstReceiver); + + assertDoesNotThrow(firstSender::dispose); + assertDoesNotThrow(firstReceiver::dispose); + + assertEquals(audioTrack.getId(), secondSender.getTrack().getId()); + assertNotNull(secondReceiver.getParameters()); + + assertDoesNotThrow(secondSender::dispose); + assertDoesNotThrow(secondReceiver::dispose); + assertDoesNotThrow(transceiver::dispose); + } + } From 8e4dec4d77cee1dd3e727b6e3a42cee8c2b4762f Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sun, 13 Sep 2026 14:04:30 +0200 Subject: [PATCH 08/15] feat: add identity-based equals/hashCode to native handle wrapper classes RTCPeerConnection, RTCDtlsTransport, and RTCDtmfSender inherited Object's reference-equality equals()/hashCode(), so two Java wrappers bound to the same native object (e.g. an RTCDtlsTransport queried via two different RTCRtpSenders sharing a bundled transport) compared as unequal -- surprising in Set/Map usage. Compare by native handle instead, via a new protected accessor on NativeObject; a disposed instance (handle 0) falls back to identity so that unrelated disposed objects don't all compare equal to each other. (RTCRtpSender/RTCRtpReceiver/RTCRtpTransceiver already got the same treatment in the previous commit, since they needed touching anyway for their reference-ownership fix.) --- .../dev/onvoid/webrtc/RTCDtlsTransport.java | 26 +++++++++++++++++++ .../java/dev/onvoid/webrtc/RTCDtmfSender.java | 26 +++++++++++++++++++ .../dev/onvoid/webrtc/RTCPeerConnection.java | 26 +++++++++++++++++++ .../onvoid/webrtc/internal/NativeObject.java | 13 ++++++++++ 4 files changed, 91 insertions(+) diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/RTCDtlsTransport.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCDtlsTransport.java index f3bcf592..14b7315f 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/RTCDtlsTransport.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/RTCDtlsTransport.java @@ -83,4 +83,30 @@ public class RTCDtlsTransport extends NativeObject { */ public native void unregisterObserver(); + /** + * Two RTCDtlsTransport instances are equal if they are bound to the same + * native transport, e.g. when obtained from two separate calls to {@link + * RTCRtpSender#getTransport()}/{@link RTCRtpReceiver#getTransport()}. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof RTCDtlsTransport)) { + return false; + } + + long handle = getNativeHandle(); + + return handle != 0 && handle == ((RTCDtlsTransport) obj).getNativeHandle(); + } + + @Override + public int hashCode() { + long handle = getNativeHandle(); + + return handle == 0 ? System.identityHashCode(this) : Long.hashCode(handle); + } + } diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/RTCDtmfSender.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCDtmfSender.java index d2ee24dd..a3b31976 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/RTCDtmfSender.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/RTCDtmfSender.java @@ -151,4 +151,30 @@ public class RTCDtmfSender extends NativeObject { */ public native void unregisterObserver(); + /** + * Two RTCDtmfSender instances are equal if they are bound to the same + * native sender, e.g. when obtained from two separate calls to {@link + * RTCRtpSender#getDtmfSender()}. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof RTCDtmfSender)) { + return false; + } + + long handle = getNativeHandle(); + + return handle != 0 && handle == ((RTCDtmfSender) obj).getNativeHandle(); + } + + @Override + public int hashCode() { + long handle = getNativeHandle(); + + return handle == 0 ? System.identityHashCode(this) : Long.hashCode(handle); + } + } diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/RTCPeerConnection.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCPeerConnection.java index bc0d904e..80c2af10 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/RTCPeerConnection.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/RTCPeerConnection.java @@ -330,4 +330,30 @@ public native void getStats(RTCRtpSender sender, */ public native void close(); + /** + * Two RTCPeerConnection instances are equal if they are bound to the + * same native connection. A closed instance is never equal to anything + * but itself. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof RTCPeerConnection)) { + return false; + } + + long handle = getNativeHandle(); + + return handle != 0 && handle == ((RTCPeerConnection) obj).getNativeHandle(); + } + + @Override + public int hashCode() { + long handle = getNativeHandle(); + + return handle == 0 ? System.identityHashCode(this) : Long.hashCode(handle); + } + } diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/internal/NativeObject.java b/webrtc/src/main/java/dev/onvoid/webrtc/internal/NativeObject.java index fa170b5c..695f1a74 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/internal/NativeObject.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/internal/NativeObject.java @@ -31,4 +31,17 @@ public abstract class NativeObject { @SuppressWarnings("unused") private long nativeHandle; + + /** + * Returns the native pointer bound to this object, so that subclasses + * can implement identity-based {@code equals()}/{@code hashCode()}. A + * value of {@code 0} means no native object is currently bound (e.g. + * after disposal). + * + * @return the native handle value. + */ + protected final long getNativeHandle() { + return nativeHandle; + } + } From 54006011828362bd0cabca99f7f56debcaa40df5 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sun, 13 Sep 2026 14:04:38 +0200 Subject: [PATCH 09/15] docs: correct VideoCapture's disposed-instance exception type The javadoc promised IllegalStateException after disposal, but the native implementation's CHECK_HANDLE guard -- the same one every other disposable class in this codebase relies on -- throws NullPointerException. Document the actual, already-consistent behavior instead of a type nothing throws. --- .../onvoid/webrtc/media/video/VideoCapture.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/media/video/VideoCapture.java b/webrtc/src/main/java/dev/onvoid/webrtc/media/video/VideoCapture.java index 4aadf354..2552063e 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/media/video/VideoCapture.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/media/video/VideoCapture.java @@ -59,7 +59,7 @@ public VideoCapture() { * @param device Non-null device descriptor to bind. Passing {@code null} * is invalid and may raise a {@link NullPointerException}. * - * @throws IllegalStateException if called after disposal. + * @throws NullPointerException if called after disposal. */ public native void setVideoCaptureDevice(VideoDevice device); @@ -73,7 +73,7 @@ public VideoCapture() { * @param capability Desired capture capability (must be non-null). * * @throws IllegalArgumentException if unsupported or invalid. - * @throws IllegalStateException if called after disposal. + * @throws NullPointerException if called after disposal. */ public native void setVideoCaptureCapability(VideoCaptureCapability capability); @@ -87,7 +87,7 @@ public VideoCapture() { * * @param sink The consumer of captured frames. * - * @throws IllegalStateException if called after disposal. + * @throws NullPointerException if called after disposal. */ public native void setVideoSink(VideoTrackSink sink); @@ -97,8 +97,8 @@ public VideoCapture() { * Idempotency: * Calling start() while already started should be a no-op (implementation-dependent). * - * @throws IllegalStateException if prerequisites (device/capability) are missing - * or the instance is disposed. + * @throws NullPointerException if the instance is disposed. + * @throws IllegalStateException if prerequisites (device/capability) are missing. */ public native void start(); @@ -109,7 +109,7 @@ public VideoCapture() { *

  • Drains or discards in-flight frames (implementation-dependent).
  • *
  • Safe to call multiple times (idempotent).
  • * - * @throws IllegalStateException if the instance is disposed. + * @throws NullPointerException if the instance is disposed. */ public native void stop(); @@ -118,8 +118,8 @@ public VideoCapture() { *

    * Lifecycle: *

  • Implicitly stops capture if currently running.
  • - *
  • After disposal, further method calls (other than additional dispose attempts) - * are invalid and may throw {@link IllegalStateException}.
  • + *
  • After disposal, any further method call, including another call to + * {@code dispose()} itself, throws {@link NullPointerException}.
  • *

    * Best Practice: * Always invoke in a finally block or use a higher-level resource management From 83b790c8a04dc7e76bcd500a5444f686f5a72622 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sun, 13 Sep 2026 14:39:06 +0200 Subject: [PATCH 10/15] docs: dispose queried RTCRtpSender/Receiver/Transceiver in the examples RTCRtpSender, RTCRtpReceiver, and RTCRtpTransceiver are now disposable (previous commit), so the examples need to actually dispose the instances they obtain from addTrack()/addTransceiver() and the onAddTrack/onRemoveTrack/onTrack observer callbacks, instead of leaking them, to demonstrate correct API usage. PeerConnectionManager gained a senders list, disposed in close(), since its addTrack() wrapper previously discarded the returned RTCRtpSender. --- .../webrtc/examples/DesktopVideoExample.java | 23 +++++++++++-- .../examples/PeerConnectionExample.java | 30 ++++++++++++++-- .../onvoid/webrtc/examples/WhepExample.java | 13 ++++++- .../web/connection/PeerConnectionManager.java | 34 +++++++++++++++---- 4 files changed, 88 insertions(+), 12 deletions(-) diff --git a/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/DesktopVideoExample.java b/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/DesktopVideoExample.java index ee95938e..0fad48c0 100644 --- a/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/DesktopVideoExample.java +++ b/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/DesktopVideoExample.java @@ -32,6 +32,7 @@ import dev.onvoid.webrtc.RTCPeerConnection; import dev.onvoid.webrtc.RTCPeerConnectionState; import dev.onvoid.webrtc.RTCRtpReceiver; +import dev.onvoid.webrtc.RTCRtpSender; import dev.onvoid.webrtc.RTCRtpTransceiver; import dev.onvoid.webrtc.RTCSignalingState; import dev.onvoid.webrtc.media.MediaStream; @@ -95,6 +96,7 @@ private static class LocalPeer implements PeerConnectionObserver { private final RTCPeerConnection peerConnection; private final VideoDesktopSource videoSource; + private final RTCRtpSender videoSender; public LocalPeer(PeerConnectionFactory factory) { @@ -172,7 +174,7 @@ else if (!windows.isEmpty()) { // Add the tracks to the peer connection. List streamIds = new ArrayList<>(); streamIds.add("stream1"); - peerConnection.addTrack(videoTrack, streamIds); + videoSender = peerConnection.addTrack(videoTrack, streamIds); System.out.println("LocalPeer: Created with a desktop video track"); } @@ -186,6 +188,11 @@ public void dispose() { videoSource.stop(); videoSource.dispose(); } + // RTCRtpSender is not owned by the peer connection; the + // application must dispose it once it is no longer needed. + if (videoSender != null) { + videoSender.dispose(); + } if (peerConnection != null) { peerConnection.close(); } @@ -234,18 +241,30 @@ public void onRenegotiationNeeded() { @Override public void onAddTrack(RTCRtpReceiver receiver, MediaStream[] mediaStreams) { System.out.println("LocalPeer: Track added: " + receiver.getTrack().getKind()); + + // The receiver is a query result the application owns; dispose it + // once its track has been retrieved. + receiver.dispose(); } @Override public void onRemoveTrack(RTCRtpReceiver receiver) { System.out.println("LocalPeer: Track removed: " + receiver.getTrack().getKind()); + + receiver.dispose(); } @Override public void onTrack(RTCRtpTransceiver transceiver) { - MediaStreamTrack track = transceiver.getReceiver().getTrack(); + RTCRtpReceiver receiver = transceiver.getReceiver(); + MediaStreamTrack track = receiver.getTrack(); System.out.println("LocalPeer: Transceiver track added: " + track.getKind()); + + // The receiver and transceiver are query results the application + // owns; dispose them once the track has been retrieved. + receiver.dispose(); + transceiver.dispose(); } } } \ No newline at end of file diff --git a/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/PeerConnectionExample.java b/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/PeerConnectionExample.java index 2c670cdf..52c411ed 100644 --- a/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/PeerConnectionExample.java +++ b/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/PeerConnectionExample.java @@ -32,6 +32,7 @@ import dev.onvoid.webrtc.RTCPeerConnection; import dev.onvoid.webrtc.RTCPeerConnectionState; import dev.onvoid.webrtc.RTCRtpReceiver; +import dev.onvoid.webrtc.RTCRtpSender; import dev.onvoid.webrtc.RTCRtpTransceiver; import dev.onvoid.webrtc.RTCSignalingState; import dev.onvoid.webrtc.media.MediaStream; @@ -99,6 +100,8 @@ private static class LocalPeer implements PeerConnectionObserver { private final RTCPeerConnection peerConnection; private final AudioTrack audioTrack; private final VideoTrack videoTrack; + private final RTCRtpSender audioSender; + private final RTCRtpSender videoSender; private final AudioFrameLogger audioFrameLogger = new AudioFrameLogger(); private final VideoFrameLogger videoFrameLogger = new VideoFrameLogger(); @@ -130,8 +133,8 @@ public LocalPeer(PeerConnectionFactory factory) { // Add the tracks to the peer connection. List streamIds = new ArrayList<>(); streamIds.add("stream1"); - peerConnection.addTrack(audioTrack, streamIds); - peerConnection.addTrack(videoTrack, streamIds); + audioSender = peerConnection.addTrack(audioTrack, streamIds); + videoSender = peerConnection.addTrack(videoTrack, streamIds); System.out.println("LocalPeer: Created with audio and video tracks"); } @@ -146,6 +149,14 @@ public void dispose() { if (videoTrack != null) { videoTrack.removeSink(videoFrameLogger); } + // RTCRtpSender instances are not owned by the peer connection; the + // application must dispose them once they are no longer needed. + if (audioSender != null) { + audioSender.dispose(); + } + if (videoSender != null) { + videoSender.dispose(); + } if (peerConnection != null) { peerConnection.close(); } @@ -194,16 +205,23 @@ public void onRenegotiationNeeded() { @Override public void onAddTrack(RTCRtpReceiver receiver, MediaStream[] mediaStreams) { System.out.println("LocalPeer: Track added: " + receiver.getTrack().getKind()); + + // The receiver is a query result the application owns; dispose it + // once its track has been retrieved. + receiver.dispose(); } @Override public void onRemoveTrack(RTCRtpReceiver receiver) { System.out.println("LocalPeer: Track removed: " + receiver.getTrack().getKind()); + + receiver.dispose(); } @Override public void onTrack(RTCRtpTransceiver transceiver) { - MediaStreamTrack track = transceiver.getReceiver().getTrack(); + RTCRtpReceiver receiver = transceiver.getReceiver(); + MediaStreamTrack track = receiver.getTrack(); String kind = track.getKind(); if (kind.equals(MediaStreamTrack.AUDIO_TRACK_KIND)) { @@ -216,6 +234,12 @@ public void onTrack(RTCRtpTransceiver transceiver) { } System.out.println("LocalPeer: Transceiver track added: " + kind); + + // The receiver and transceiver are query results the application + // owns; dispose them once the track has been retrieved. The track + // itself is unaffected and keeps delivering frames to its sink. + receiver.dispose(); + transceiver.dispose(); } } diff --git a/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/WhepExample.java b/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/WhepExample.java index 46a49c9c..f8fdae3d 100644 --- a/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/WhepExample.java +++ b/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/WhepExample.java @@ -161,7 +161,8 @@ private void createPeerConnection() { RTCRtpTransceiver transceiver = peerConnection.addTransceiver(videoTrack, transceiverInit); // Set up a sink to handle incoming video frames. - MediaStreamTrack track = transceiver.getReceiver().getTrack(); + RTCRtpReceiver receiver = transceiver.getReceiver(); + MediaStreamTrack track = receiver.getTrack(); if (track instanceof VideoTrack vTrack) { vTrack.addSink(videoFrame -> { System.out.println("Received video frame: " + videoFrame); @@ -170,6 +171,12 @@ private void createPeerConnection() { videoFrame.release(); }); } + + // The receiver and transceiver are query results this example owns; + // dispose them once the track has been retrieved. The track itself + // is unaffected and keeps delivering frames to its sink. + receiver.dispose(); + transceiver.dispose(); } private void createOffer() { @@ -314,6 +321,10 @@ public void onRenegotiationNeeded() { @Override public void onAddTrack(RTCRtpReceiver receiver, MediaStream[] mediaStreams) { System.out.println("Track added."); + + // The receiver is a query result the application owns; dispose it + // once it is no longer needed. + receiver.dispose(); } } } \ No newline at end of file diff --git a/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/web/connection/PeerConnectionManager.java b/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/web/connection/PeerConnectionManager.java index fbece89c..167f8b87 100644 --- a/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/web/connection/PeerConnectionManager.java +++ b/webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/web/connection/PeerConnectionManager.java @@ -16,6 +16,7 @@ package dev.onvoid.webrtc.examples.web.connection; +import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; @@ -33,6 +34,7 @@ import dev.onvoid.webrtc.RTCPeerConnection; import dev.onvoid.webrtc.RTCPeerConnectionState; import dev.onvoid.webrtc.RTCRtpReceiver; +import dev.onvoid.webrtc.RTCRtpSender; import dev.onvoid.webrtc.RTCRtpTransceiver; import dev.onvoid.webrtc.RTCSdpType; import dev.onvoid.webrtc.RTCSessionDescription; @@ -66,6 +68,7 @@ public class PeerConnectionManager implements PeerConnectionSignalingHandler { private final PeerConnectionFactory factory; private final RTCPeerConnection peerConnection; + private final List senders = new ArrayList<>(); private Consumer onLocalDescriptionCreated; private Consumer onIceCandidateGenerated; @@ -99,7 +102,9 @@ public PeerConnectionManager() { * @param streamIds The stream IDs to associate with the track. */ public void addTrack(MediaStreamTrack track, List streamIds) { - peerConnection.addTrack(track, streamIds); + // Keep the returned RTCRtpSender around so it can be disposed in + // close(); it is not owned by the peer connection. + senders.add(peerConnection.addTrack(track, streamIds)); LOG.info("Added track: {}", track.getKind()); } @@ -146,6 +151,11 @@ public VideoTrack createVideoTrack(VideoTrackSource source, String label) { * Closes the peer connection. */ public void close() { + for (RTCRtpSender sender : senders) { + sender.dispose(); + } + senders.clear(); + if (peerConnection != null) { peerConnection.close(); } @@ -407,23 +417,35 @@ public void onRenegotiationNeeded() { @Override public void onAddTrack(RTCRtpReceiver receiver, MediaStream[] mediaStreams) { LOG.info("Track added: {}", receiver.getTrack().getKind()); + + // The receiver is a query result the application owns; dispose it + // once its track has been retrieved. + receiver.dispose(); } - + @Override public void onRemoveTrack(RTCRtpReceiver receiver) { LOG.info("Track removed: {}", receiver.getTrack().getKind()); + + receiver.dispose(); } - + @Override public void onTrack(RTCRtpTransceiver transceiver) { - MediaStreamTrack track = transceiver.getReceiver().getTrack(); + RTCRtpReceiver receiver = transceiver.getReceiver(); + MediaStreamTrack track = receiver.getTrack(); String kind = track.getKind(); - + LOG.info("{} track added to transceiver", kind); - + if (onTrackReceived != null) { onTrackReceived.accept(track); } + + // The receiver and transceiver are query results the application + // owns; dispose them once the track has been retrieved. + receiver.dispose(); + transceiver.dispose(); } } } \ No newline at end of file From e90644b6c833be1f7769d34f5258918496fdbcae Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sun, 13 Sep 2026 15:21:23 +0200 Subject: [PATCH 11/15] refactor: extract ReplaceNativeObserver/ClearNativeObserver helpers RTCDataChannel, RTCDtlsTransport, RTCDtmfSender, and RTCPeerConnection each hand-rolled the same get-old/delete/store-new sequence for freeing a heap-allocated native observer on replace, unregister, or dispose -- the exact pattern a missing step in silently leaked before. Centralize it in jni-voithos so the next JNI wrapper that registers an observer against a "replace the previous one" API gets this for free instead of reimplementing (and potentially forgetting a step of) the same logic. --- .../jni-voithos/include/JavaUtils.h | 32 +++++++++++++++++++ .../src/main/cpp/src/JNI_RTCDataChannel.cpp | 19 ++--------- .../src/main/cpp/src/JNI_RTCDtlsTransport.cpp | 12 ++----- .../src/main/cpp/src/JNI_RTCDtmfSender.cpp | 12 ++----- .../main/cpp/src/JNI_RTCPeerConnection.cpp | 7 +--- 5 files changed, 40 insertions(+), 42 deletions(-) diff --git a/webrtc-jni/src/main/cpp/dependencies/jni-voithos/include/JavaUtils.h b/webrtc-jni/src/main/cpp/dependencies/jni-voithos/include/JavaUtils.h index db287978..dd273ef5 100644 --- a/webrtc-jni/src/main/cpp/dependencies/jni-voithos/include/JavaUtils.h +++ b/webrtc-jni/src/main/cpp/dependencies/jni-voithos/include/JavaUtils.h @@ -117,4 +117,36 @@ void SetHandle(JNIEnv * env, jobject obj, T * t) SetHandle(env, obj, "nativeHandle", t); } +// Replaces the native observer stored in the `handleName` field on `obj` +// with `observer`, deleting whatever was stored there before. Use this +// together with ClearNativeObserver() wherever a Java object registers a +// heap-allocated native observer against a "replace the previous one" +// register() API, so the previous observer's lifetime can never be +// forgotten and leaked -- as opposed to reimplementing the same +// get-old/delete/store-new sequence by hand in each JNI_Foo.cpp. +template +void ReplaceNativeObserver(JNIEnv * env, jobject obj, const std::string & handleName, T * observer) +{ + T * old = GetHandle(env, obj, handleName); + + delete old; + + SetHandle(env, obj, handleName, observer); +} + +// Deletes and clears the native observer stored in the `handleName` field +// on `obj`, if any. Safe to call when no observer is currently registered +// (e.g. from an unregisterObserver()/dispose() implementation that isn't +// sure whether one was ever set). +template +void ClearNativeObserver(JNIEnv * env, jobject obj, const std::string & handleName) +{ + T * observer = GetHandle(env, obj, handleName); + + if (observer) { + SetHandle(env, obj, handleName, nullptr); + delete observer; + } +} + #endif \ No newline at end of file diff --git a/webrtc-jni/src/main/cpp/src/JNI_RTCDataChannel.cpp b/webrtc-jni/src/main/cpp/src/JNI_RTCDataChannel.cpp index 0163f566..eef0a8a1 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_RTCDataChannel.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_RTCDataChannel.cpp @@ -39,11 +39,8 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCDataChannel_registerObserver // it, otherwise it would leak along with its JNI global reference. channel->UnregisterObserver(); - auto oldObserver = GetHandle(env, caller, "observerHandle"); - delete oldObserver; - auto observer = new jni::RTCDataChannelObserver(env, jni::JavaGlobalRef(env, jObserver)); - SetHandle(env, caller, "observerHandle", observer); + ReplaceNativeObserver(env, caller, "observerHandle", observer); channel->RegisterObserver(observer); } @@ -56,12 +53,7 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCDataChannel_unregisterObserver channel->UnregisterObserver(); - auto observer = GetHandle(env, caller, "observerHandle"); - - if (observer) { - SetHandle(env, caller, "observerHandle", nullptr); - delete observer; - } + ClearNativeObserver(env, caller, "observerHandle"); } JNIEXPORT jstring JNICALL Java_dev_onvoid_webrtc_RTCDataChannel_getLabel @@ -171,12 +163,7 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCDataChannel_dispose channel->UnregisterObserver(); - auto observer = GetHandle(env, caller, "observerHandle"); - - if (observer) { - SetHandle(env, caller, "observerHandle", nullptr); - delete observer; - } + ClearNativeObserver(env, caller, "observerHandle"); webrtc::RefCountReleaseStatus status = channel->Release(); diff --git a/webrtc-jni/src/main/cpp/src/JNI_RTCDtlsTransport.cpp b/webrtc-jni/src/main/cpp/src/JNI_RTCDtlsTransport.cpp index 3f42a272..4d331dc9 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_RTCDtlsTransport.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_RTCDtlsTransport.cpp @@ -80,11 +80,8 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCDtlsTransport_registerObserver // it, otherwise it would leak along with its JNI global reference. transport->UnregisterObserver(); - auto oldObserver = GetHandle(env, caller, "observerHandle"); - delete oldObserver; - auto newObserver = new jni::RTCDtlsTransportObserver(env, jni::JavaGlobalRef(env, observer)); - SetHandle(env, caller, "observerHandle", newObserver); + ReplaceNativeObserver(env, caller, "observerHandle", newObserver); transport->RegisterObserver(newObserver); } @@ -97,10 +94,5 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCDtlsTransport_unregisterObserve transport->UnregisterObserver(); - auto observer = GetHandle(env, caller, "observerHandle"); - - if (observer) { - SetHandle(env, caller, "observerHandle", nullptr); - delete observer; - } + ClearNativeObserver(env, caller, "observerHandle"); } \ No newline at end of file diff --git a/webrtc-jni/src/main/cpp/src/JNI_RTCDtmfSender.cpp b/webrtc-jni/src/main/cpp/src/JNI_RTCDtmfSender.cpp index d6ff4201..8db7b672 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_RTCDtmfSender.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_RTCDtmfSender.cpp @@ -78,11 +78,8 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCDtmfSender_registerObserver // it, otherwise it would leak along with its JNI global reference. sender->UnregisterObserver(); - auto oldObserver = GetHandle(env, caller, "observerHandle"); - delete oldObserver; - auto observer = new jni::RTCDtmfSenderObserver(env, jni::JavaGlobalRef(env, jObserver)); - SetHandle(env, caller, "observerHandle", observer); + ReplaceNativeObserver(env, caller, "observerHandle", observer); sender->RegisterObserver(observer); } @@ -95,10 +92,5 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCDtmfSender_unregisterObserver sender->UnregisterObserver(); - auto observer = GetHandle(env, caller, "observerHandle"); - - if (observer) { - SetHandle(env, caller, "observerHandle", nullptr); - delete observer; - } + ClearNativeObserver(env, caller, "observerHandle"); } \ 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 04821b2f..785fa77b 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_RTCPeerConnection.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_RTCPeerConnection.cpp @@ -578,12 +578,7 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_RTCPeerConnection_close SetHandle(env, caller, nullptr); - auto observer = GetHandle(env, caller, "observerHandle"); - - if (observer) { - SetHandle(env, caller, "observerHandle", nullptr); - delete observer; - } + ClearNativeObserver(env, caller, "observerHandle"); // Drop the owning reference taken when the PeerConnection was handed // to Java in PeerConnectionFactory::createPeerConnection. From def5be31e93aadd177186a9f72a71755a3be83f4 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sun, 13 Sep 2026 15:30:52 +0200 Subject: [PATCH 12/15] fix: avoid using an already-deleted local ref in PortAllocatorConfig toJava() wrapped the same raw jobject in two separate JavaLocalRef instances: one temporary handed to the JavaObject constructor, and another to build the return value. The first temporary's destructor deleted the local ref at the end of its full expression, so the second one -- and everything the caller (RTCConfiguration::toJava) did with it afterward, including RTCPeerConnection.getConfiguration() -- used an already-deleted reference. Found via -Xcheck:jni, which reported "FATAL ERROR in native method: Bad global or local ref passed to JNI" and aborted the JVM; not something a normal test run would necessarily catch, since a freed local ref doesn't always misbehave immediately. Keep one named JavaLocalRef alive for the whole function instead. --- .../src/main/cpp/src/api/PortAllocatorConfig.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/webrtc-jni/src/main/cpp/src/api/PortAllocatorConfig.cpp b/webrtc-jni/src/main/cpp/src/api/PortAllocatorConfig.cpp index 8f5f9a6a..e4c24cf7 100644 --- a/webrtc-jni/src/main/cpp/src/api/PortAllocatorConfig.cpp +++ b/webrtc-jni/src/main/cpp/src/api/PortAllocatorConfig.cpp @@ -28,14 +28,19 @@ namespace jni { const auto javaClass = JavaClasses::get(env); - jobject jpac = env->NewObject(javaClass->cls, javaClass->ctor); + JavaLocalRef jpac(env, env->NewObject(javaClass->cls, javaClass->ctor)); - JavaObject obj(env, JavaLocalRef(env, jpac)); + // Keep jpac alive for the whole function instead of wrapping the + // raw jobject in two separate JavaLocalRef instances: the first + // one's destructor would delete the local ref at the end of its + // full expression, leaving the second (returned) one wrapping an + // already-deleted reference. + JavaObject obj(env, jpac); obj.setInt(javaClass->minPort, cfg.min_port); obj.setInt(javaClass->maxPort, cfg.max_port); obj.setInt(javaClass->flags, cfg.flags); - return JavaLocalRef(env, jpac); + return jpac; } JavaPortAllocatorConfigClass::JavaPortAllocatorConfigClass(JNIEnv * env) From 3d4a1a6a12a4f05afdbe672024acf5b4c23389dd Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sun, 13 Sep 2026 15:31:02 +0200 Subject: [PATCH 13/15] build: add an opt-in -Xcheck:jni profile for JNI reference checks mvn -pl webrtc test -Pjni-check reruns the test suite under -Xcheck:jni, which validates every JNI call's local/global refs and aborts with "FATAL ERROR in native method: ..." on a real violation -- the exact bug class this branch has fixed several times over (a double-freed local ref, an already-deleted one, a dangling reference relied on without an owned ref). It just caught another one (PortAllocatorConfig, previous commit). Not wired into the default test run or CI: -Xcheck:jni also emits a "WARNING in native method: JNI call made without checking exceptions" for every native call in this codebase that doesn't immediately check for a pending exception -- thousands of them, pre-existing, harmless on their own, but far too noisy to gate a build on before someone works through fixing them. The profile also switches Surefire's fork communication to TCP sockets (forkNode=SurefireForkNodeFactory), which is required: -Xcheck:jni writes its diagnostics straight to the process's native stdout, the same stream Surefire's default pipe-based protocol uses, corrupting it ("Corrupted channel by directly writing to native stream") instead of reporting results. --- webrtc/pom.xml | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/webrtc/pom.xml b/webrtc/pom.xml index 00946f51..a758f783 100644 --- a/webrtc/pom.xml +++ b/webrtc/pom.xml @@ -31,6 +31,53 @@ + + + jni-check + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + -Xcheck:jni + --add-opens webrtc.java/dev.onvoid.webrtc=ALL-UNNAMED + --add-opens webrtc.java/dev.onvoid.webrtc.logging=ALL-UNNAMED + --add-opens webrtc.java/dev.onvoid.webrtc.media=ALL-UNNAMED + --add-opens webrtc.java/dev.onvoid.webrtc.media.audio=ALL-UNNAMED + --add-opens webrtc.java/dev.onvoid.webrtc.media.video=ALL-UNNAMED + + + + + + +