From 5ca21712a18557083bd16c5fdc2fd69af4d9b293 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Tue, 15 Sep 2026 22:59:09 +0200 Subject: [PATCH] fix: give AudioTrackSource a dispose() to release its native reference PeerConnectionFactory.createAudioSource() returns a Java object wrapping a ref-counted native AudioSourceInterface, but AudioTrackSource (unlike every other ref-counted wrapper in the API) exposed no dispose()/release(), so the one reference handed to Java could never be dropped. Every call to createAudioSource() leaked the native audio source for the life of the process, regardless of how the resulting track and peer connection were torn down. --- docs/guide/get-started.md | 1 + .../examples/PeerConnectionExample.java | 8 +++- .../web/connection/PeerConnectionManager.java | 10 +++++ .../main/cpp/include/JNI_AudioTrackSource.h | 21 ++++++++++ .../src/main/cpp/src/JNI_AudioTrackSource.cpp | 39 +++++++++++++++++++ .../webrtc/media/audio/AudioTrackSource.java | 7 ++++ .../onvoid/webrtc/media/MediaSourceTests.java | 16 ++++++++ 7 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 webrtc-jni/src/main/cpp/include/JNI_AudioTrackSource.h create mode 100644 webrtc-jni/src/main/cpp/src/JNI_AudioTrackSource.cpp diff --git a/docs/guide/get-started.md b/docs/guide/get-started.md index 99ed5cd5..969b362d 100644 --- a/docs/guide/get-started.md +++ b/docs/guide/get-started.md @@ -298,6 +298,7 @@ audioTrack.dispose(); // Dispose of sources videoSource.dispose(); +audioSource.dispose(); // Close peer connection and release resources peerConnection.close(); 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 52c411ed..ee7679de 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 @@ -98,6 +98,7 @@ public static void main(String[] args) { private static class LocalPeer implements PeerConnectionObserver { private final RTCPeerConnection peerConnection; + private final AudioTrackSource audioSource; private final AudioTrack audioTrack; private final VideoTrack videoTrack; private final RTCRtpSender audioSender; @@ -124,7 +125,7 @@ public LocalPeer(PeerConnectionFactory factory) { audioOptions.autoGainControl = true; audioOptions.noiseSuppression = true; - AudioTrackSource audioSource = factory.createAudioSource(audioOptions); + audioSource = factory.createAudioSource(audioOptions); audioTrack = factory.createAudioTrack("audio0", audioSource); VideoDeviceSource videoSource = new VideoDeviceSource(); @@ -160,6 +161,11 @@ public void dispose() { if (peerConnection != null) { peerConnection.close(); } + // AudioTrackSource is ref-counted and not owned by the audio + // track; the application must dispose it once no longer needed. + if (audioSource != null) { + audioSource.dispose(); + } } // PeerConnectionObserver implementation. 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 167f8b87..5707f857 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 @@ -69,6 +69,7 @@ public class PeerConnectionManager implements PeerConnectionSignalingHandler { private final PeerConnectionFactory factory; private final RTCPeerConnection peerConnection; private final List senders = new ArrayList<>(); + private final List audioSources = new ArrayList<>(); private Consumer onLocalDescriptionCreated; private Consumer onIceCandidateGenerated; @@ -120,6 +121,10 @@ public void addTrack(MediaStreamTrack track, List streamIds) { public AudioTrack createAudioTrack(AudioOptions options, String label) { AudioTrackSource audioSource = factory.createAudioSource(options); + // Keep the source around so it can be disposed in close(); it is + // ref-counted and not owned by the audio track. + audioSources.add(audioSource); + return factory.createAudioTrack(label, audioSource); } @@ -156,6 +161,11 @@ public void close() { } senders.clear(); + for (AudioTrackSource audioSource : audioSources) { + audioSource.dispose(); + } + audioSources.clear(); + if (peerConnection != null) { peerConnection.close(); } diff --git a/webrtc-jni/src/main/cpp/include/JNI_AudioTrackSource.h b/webrtc-jni/src/main/cpp/include/JNI_AudioTrackSource.h new file mode 100644 index 00000000..34cacdff --- /dev/null +++ b/webrtc-jni/src/main/cpp/include/JNI_AudioTrackSource.h @@ -0,0 +1,21 @@ +/* DO NOT EDIT THIS FILE - it is machine generated */ +#include +/* Header for class dev_onvoid_webrtc_media_audio_AudioTrackSource */ + +#ifndef _Included_dev_onvoid_webrtc_media_audio_AudioTrackSource +#define _Included_dev_onvoid_webrtc_media_audio_AudioTrackSource +#ifdef __cplusplus +extern "C" { +#endif + /* + * Class: dev_onvoid_webrtc_media_audio_AudioTrackSource + * Method: dispose + * Signature: ()V + */ + JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_audio_AudioTrackSource_dispose + (JNIEnv *, jobject); + +#ifdef __cplusplus +} +#endif +#endif diff --git a/webrtc-jni/src/main/cpp/src/JNI_AudioTrackSource.cpp b/webrtc-jni/src/main/cpp/src/JNI_AudioTrackSource.cpp new file mode 100644 index 00000000..f5b37d0e --- /dev/null +++ b/webrtc-jni/src/main/cpp/src/JNI_AudioTrackSource.cpp @@ -0,0 +1,39 @@ +/* + * 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_AudioTrackSource.h" +#include "JavaUtils.h" + +#include "api/media_stream_interface.h" + +#include "rtc_base/logging.h" + +JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_audio_AudioTrackSource_dispose +(JNIEnv * env, jobject caller) +{ + webrtc::AudioSourceInterface * source = GetHandle(env, caller); + CHECK_HANDLE(source); + + webrtc::RefCountReleaseStatus status = source->Release(); + + if (status != webrtc::RefCountReleaseStatus::kDroppedLastRef) { + RTC_LOG(LS_WARNING) << "Native object was not deleted. A reference is still around somewhere."; + } + + SetHandle(env, caller, nullptr); + + source = nullptr; +} diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/media/audio/AudioTrackSource.java b/webrtc/src/main/java/dev/onvoid/webrtc/media/audio/AudioTrackSource.java index f25ee9b5..edcc13a0 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/media/audio/AudioTrackSource.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/media/audio/AudioTrackSource.java @@ -29,4 +29,11 @@ protected AudioTrackSource() { } + /** + * Disposes of the native resources held by this audio source. + * This method should be called when the audio source is no longer needed + * to prevent memory leaks. + */ + public native void dispose(); + } diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/media/MediaSourceTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/media/MediaSourceTests.java index 4a1a7f1b..e6e977f3 100644 --- a/webrtc/src/test/java/dev/onvoid/webrtc/media/MediaSourceTests.java +++ b/webrtc/src/test/java/dev/onvoid/webrtc/media/MediaSourceTests.java @@ -20,6 +20,7 @@ import dev.onvoid.webrtc.TestBase; 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.Test; @@ -32,6 +33,21 @@ void audioSourceStateAfterCreation() { AudioTrackSource audioSource = factory.createAudioSource(audioOptions); assertEquals(MediaSource.State.LIVE, audioSource.getState()); + + audioSource.dispose(); + } + + @Test + void audioSourceDisposeAfterTrackDispose() { + // Regression test: AudioTrackSource used to have no dispose(), so the + // native AudioSourceInterface reference obtained from + // createAudioSource() could never be released by the application. + AudioOptions audioOptions = new AudioOptions(); + AudioTrackSource audioSource = factory.createAudioSource(audioOptions); + AudioTrack audioTrack = factory.createAudioTrack("audio0", audioSource); + + audioTrack.dispose(); + audioSource.dispose(); } }