diff --git a/docs/guide/video/camera-capture.md b/docs/guide/video/camera-capture.md index 2fffe0697..0bb69f4e4 100644 --- a/docs/guide/video/camera-capture.md +++ b/docs/guide/video/camera-capture.md @@ -192,7 +192,8 @@ public class MyPeerConnectionObserver implements PeerConnectionObserver { // The receiver and transceiver are query results you own; dispose // them once the track has been retrieved. The track itself is - // unaffected and keeps delivering frames to its sink. + // unaffected and keeps delivering frames to its sink. It is a view + // of the receiver's track and needs no dispose() of its own. receiver.dispose(); transceiver.dispose(); } diff --git a/webrtc-jni/src/main/cpp/include/media/MediaStreamTrackView.h b/webrtc-jni/src/main/cpp/include/media/MediaStreamTrackView.h new file mode 100644 index 000000000..7611c846d --- /dev/null +++ b/webrtc-jni/src/main/cpp/include/media/MediaStreamTrackView.h @@ -0,0 +1,80 @@ +/* + * Copyright 2026 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. + */ + +#ifndef JNI_WEBRTC_MEDIA_STREAM_TRACK_VIEW_H_ +#define JNI_WEBRTC_MEDIA_STREAM_TRACK_VIEW_H_ + +#include "Exception.h" +#include "JavaClass.h" +#include "JavaFactories.h" +#include "JavaRef.h" + +#include "api/media_stream_interface.h" +#include "api/scoped_refptr.h" + +#include +#include +#include + +namespace jni +{ + // A Java MediaStreamTrack made for a track that something else owns (a + // sender, a receiver, a stream, or the track a listener is called for) + // holds no reference to it. It is marked as a view, so that disposing it + // detaches the Java object without releasing a reference it never had. + // Tracks the factory creates hand their reference to the Java object and + // are not views. + class MediaStreamTrackView + { + public: + static JavaLocalRef create(JNIEnv * env, const webrtc::MediaStreamTrackInterface * track); + + template + static JavaLocalRef createArray(JNIEnv * env, const std::vector> & tracks) + { + jsize size = static_cast(tracks.size()); + + JavaLocalRef array = JavaFactories::createArray(env, size); + + if (array.get() == nullptr) { + throw Exception("Create track array of type [%s] failed", typeid(T).name()); + } + + for (jsize i = 0; i < size; i++) { + JavaLocalRef view = create(env, tracks[i].get()); + + env->SetObjectArrayElement(array, i, view.get()); + } + + return array; + } + + static bool isView(JNIEnv * env, jobject javaTrack); + + private: + static JavaLocalRef markView(JNIEnv * env, JavaLocalRef && javaTrack); + + class JavaMediaStreamTrackClass : public JavaClass + { + public: + explicit JavaMediaStreamTrackClass(JNIEnv * env); + + jfieldID view; + }; + }; +} + +#endif diff --git a/webrtc-jni/src/main/cpp/include/media/video/linux/V4l2VideoDeviceManager.h b/webrtc-jni/src/main/cpp/include/media/video/linux/V4l2VideoDeviceManager.h index 454a38676..f52d57dfc 100644 --- a/webrtc-jni/src/main/cpp/include/media/video/linux/V4l2VideoDeviceManager.h +++ b/webrtc-jni/src/main/cpp/include/media/video/linux/V4l2VideoDeviceManager.h @@ -38,7 +38,7 @@ namespace jni private: void addDevice(const std::string & name, const std::string & descriptor); - void removeDevice(const std::string & name, const std::string & descriptor); + void removeDevice(const std::string & descriptor); void run(); bool checkDevice(const std::string & descriptor); int ioctlDevice(int fh, int request, void * arg); diff --git a/webrtc-jni/src/main/cpp/src/JNI_MediaStream.cpp b/webrtc-jni/src/main/cpp/src/JNI_MediaStream.cpp index 520684510..ce48406bd 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_MediaStream.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_MediaStream.cpp @@ -20,6 +20,7 @@ #include "JavaFactories.h" #include "JavaString.h" #include "JavaUtils.h" +#include "media/MediaStreamTrackView.h" #include "api/media_stream_interface.h" #include "rtc_base/logging.h" @@ -42,7 +43,7 @@ JNIEXPORT jobjectArray JNICALL Java_dev_onvoid_webrtc_media_MediaStream_getAudio jni::JavaLocalRef objectArray; try { - objectArray = jni::createObjectArray(env, stream->GetAudioTracks()); + objectArray = jni::MediaStreamTrackView::createArray(env, stream->GetAudioTracks()); } catch (...) { ThrowCxxJavaException(env); @@ -60,7 +61,7 @@ JNIEXPORT jobjectArray JNICALL Java_dev_onvoid_webrtc_media_MediaStream_getVideo jni::JavaLocalRef objectArray; try { - objectArray = jni::createObjectArray(env, stream->GetVideoTracks()); + objectArray = jni::MediaStreamTrackView::createArray(env, stream->GetVideoTracks()); } catch (...) { ThrowCxxJavaException(env); diff --git a/webrtc-jni/src/main/cpp/src/JNI_MediaStreamTrack.cpp b/webrtc-jni/src/main/cpp/src/JNI_MediaStreamTrack.cpp index 1ab3bf3e9..517f32b37 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_MediaStreamTrack.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_MediaStreamTrack.cpp @@ -23,6 +23,7 @@ #include "WebRTCContext.h" #include "media/MediaStreamTrackObserver.h" +#include "media/MediaStreamTrackView.h" #include "api/media_stream_interface.h" #include "rtc_base/logging.h" @@ -33,6 +34,13 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_MediaStreamTrack_dispose webrtc::MediaStreamTrackInterface * track = GetHandle(env, caller); CHECK_HANDLE(track); + // A view holds no reference to the track and does not own its listeners: + // the track lives on with whatever owns it, so only this object detaches. + if (jni::MediaStreamTrackView::isView(env, caller)) { + SetHandle(env, caller, nullptr); + return; + } + // Listeners still registered are taken off the track before it goes. Their // entries are keyed by this track's address, which a later track can be // given, and an observer left registered would be freed with its entry diff --git a/webrtc-jni/src/main/cpp/src/JNI_RTCRtpReceiver.cpp b/webrtc-jni/src/main/cpp/src/JNI_RTCRtpReceiver.cpp index 154da0b47..813690015 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_RTCRtpReceiver.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_RTCRtpReceiver.cpp @@ -26,6 +26,7 @@ #include "api/media_stream_interface.h" #include "api/rtp_receiver_interface.h" +#include "media/MediaStreamTrackView.h" #include @@ -37,14 +38,8 @@ JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCRtpReceiver_getTrack webrtc::scoped_refptr track = receiver->track(); - if (webrtc::AudioTrackInterface * t = dynamic_cast(track.get())) { - return jni::JavaFactories::create(env, t).release(); - } - else if (webrtc::VideoTrackInterface * t = dynamic_cast(track.get())) { - return jni::JavaFactories::create(env, t).release(); - } - - return nullptr; + // The receiver keeps the track; the Java object is only a view of it. + return jni::MediaStreamTrackView::create(env, track.get()).release(); } JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCRtpReceiver_getTransport diff --git a/webrtc-jni/src/main/cpp/src/JNI_RTCRtpSender.cpp b/webrtc-jni/src/main/cpp/src/JNI_RTCRtpSender.cpp index a70435f1b..7a17ecc68 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_RTCRtpSender.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_RTCRtpSender.cpp @@ -26,6 +26,7 @@ #include "api/EncodedFrameTransformer.h" #include "api/RTCDtmfSender.h" #include "api/rtp_sender_interface.h" +#include "media/MediaStreamTrackView.h" JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCRtpSender_getTrack (JNIEnv * env, jobject caller) @@ -35,14 +36,8 @@ JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCRtpSender_getTrack webrtc::scoped_refptr track = sender->track(); - if (webrtc::AudioTrackInterface * t = dynamic_cast(track.get())) { - return jni::JavaFactories::create(env, t).release(); - } - else if (webrtc::VideoTrackInterface * t = dynamic_cast(track.get())) { - return jni::JavaFactories::create(env, t).release(); - } - - return nullptr; + // The sender keeps the track; the Java object is only a view of it. + return jni::MediaStreamTrackView::create(env, track.get()).release(); } JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_RTCRtpSender_getTransport diff --git a/webrtc-jni/src/main/cpp/src/media/MediaStreamTrackObserver.cpp b/webrtc-jni/src/main/cpp/src/media/MediaStreamTrackObserver.cpp index dcf36809b..7cd275293 100644 --- a/webrtc-jni/src/main/cpp/src/media/MediaStreamTrackObserver.cpp +++ b/webrtc-jni/src/main/cpp/src/media/MediaStreamTrackObserver.cpp @@ -15,6 +15,7 @@ */ #include "media/MediaStreamTrackObserver.h" +#include "media/MediaStreamTrackView.h" #include "api/WebRTCUtils.h" #include "JavaFactories.h" #include "JavaUtils.h" @@ -70,15 +71,7 @@ namespace jni JavaLocalRef MediaStreamTrackObserver::createJavaTrack(JNIEnv * env) { - if (const webrtc::AudioTrackInterface * t = dynamic_cast(track)) { - return jni::JavaFactories::create(env, t); - } - else if (const webrtc::VideoTrackInterface * t = dynamic_cast(track)) { - return jni::JavaFactories::create(env, t); - } - else { - return jni::JavaLocalRef(env, nullptr); - } + return MediaStreamTrackView::create(env, track); } MediaStreamTrackObserver::JavaMediaStreamTrackListenerClass::JavaMediaStreamTrackListenerClass(JNIEnv * env) diff --git a/webrtc-jni/src/main/cpp/src/media/MediaStreamTrackView.cpp b/webrtc-jni/src/main/cpp/src/media/MediaStreamTrackView.cpp new file mode 100644 index 000000000..263522afe --- /dev/null +++ b/webrtc-jni/src/main/cpp/src/media/MediaStreamTrackView.cpp @@ -0,0 +1,60 @@ +/* + * Copyright 2026 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 "media/MediaStreamTrackView.h" +#include "JavaClasses.h" +#include "JavaUtils.h" +#include "JNI_WebRTC.h" + +#include + +namespace jni +{ + JavaLocalRef MediaStreamTrackView::create(JNIEnv * env, const webrtc::MediaStreamTrackInterface * track) + { + if (const webrtc::AudioTrackInterface * t = dynamic_cast(track)) { + return markView(env, JavaFactories::create(env, t)); + } + else if (const webrtc::VideoTrackInterface * t = dynamic_cast(track)) { + return markView(env, JavaFactories::create(env, t)); + } + + return JavaLocalRef(env, nullptr); + } + + JavaLocalRef MediaStreamTrackView::markView(JNIEnv * env, JavaLocalRef && javaTrack) + { + const auto javaClass = JavaClasses::get(env); + + env->SetBooleanField(javaTrack, javaClass->view, JNI_TRUE); + + return std::move(javaTrack); + } + + bool MediaStreamTrackView::isView(JNIEnv * env, jobject javaTrack) + { + const auto javaClass = JavaClasses::get(env); + + return env->GetBooleanField(javaTrack, javaClass->view) == JNI_TRUE; + } + + MediaStreamTrackView::JavaMediaStreamTrackClass::JavaMediaStreamTrackClass(JNIEnv * env) + { + jclass cls = FindClass(env, PKG_MEDIA"MediaStreamTrack"); + + view = GetFieldID(env, cls, "view", "Z"); + } +} diff --git a/webrtc-jni/src/main/cpp/src/media/video/linux/V4l2VideoDeviceManager.cpp b/webrtc-jni/src/main/cpp/src/media/video/linux/V4l2VideoDeviceManager.cpp index b8d9f29f4..1e19be2e9 100644 --- a/webrtc-jni/src/main/cpp/src/media/video/linux/V4l2VideoDeviceManager.cpp +++ b/webrtc-jni/src/main/cpp/src/media/video/linux/V4l2VideoDeviceManager.cpp @@ -104,6 +104,12 @@ namespace jni } const char * node = lib.udev_device_get_devnode(dev); + + if (!node) { + lib.udev_device_unref(dev); + continue; + } + bool error = false; int v4l2_fd = open(node, O_RDONLY); @@ -238,21 +244,25 @@ namespace jni } const char * subsystem = lib.udev_device_get_subsystem(dev); + const char * action = lib.udev_device_get_action(dev); + const char * node = lib.udev_device_get_devnode(dev); - if (strcmp(subsystem, UDEV_SUBSYSTEM) != 0) { + // Any of these can be missing, and a std::string made from + // a null pointer aborts this thread and the process with it. + if (!subsystem || !action || !node || strcmp(subsystem, UDEV_SUBSYSTEM) != 0) { lib.udev_device_unref(dev); continue; } - const char * action = lib.udev_device_get_action(dev); - const char * node = lib.udev_device_get_devnode(dev); - const char * name = lib.udev_device_get_property_value(dev, "ID_V4L_PRODUCT"); - if (strcmp(action, UDEV_ADD) == 0 && checkDevice(node)) { - addDevice(name, node); + // The product name is set by udev's v4l_id rule, which + // not every system has. + const char * name = lib.udev_device_get_property_value(dev, "ID_V4L_PRODUCT"); + + addDevice(name ? name : node, node); } else if (strcmp(action, UDEV_REMOVE) == 0) { - removeDevice(name, node); + removeDevice(node); } lib.udev_device_unref(dev); @@ -270,10 +280,13 @@ namespace jni notifyDeviceConnected(device); } - void V4l2VideoDeviceManager::removeDevice(const std::string & name, const std::string & descriptor) + void V4l2VideoDeviceManager::removeDevice(const std::string & descriptor) { - auto predicate = [name, descriptor](const VideoDevicePtr & dev) { - return dev->getName() == name && dev->getDescriptor() == descriptor; + // The device node identifies the device. Its name may have come + // from the driver (enumeration) or from udev (hotplug), which need + // not agree, so it is not compared. + auto predicate = [descriptor](const VideoDevicePtr & dev) { + return dev->getDescriptor() == descriptor; }; VideoDevicePtr removed = captureDevices.removeDevice(predicate); diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpReceiver.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpReceiver.java index 7e120f76d..368bba617 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpReceiver.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpReceiver.java @@ -41,6 +41,9 @@ private RTCRtpReceiver() { * track.stop() is final, although clones are not affected. Since * receiver.track.stop() does not implicitly stop receiver, Receiver Reports * continue to be sent. + *

+ * The returned object is a view of the receiver's track; see + * {@link MediaStreamTrack#dispose()}. * * @return The media track associated with this receiver. */ diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpSender.java b/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpSender.java index b30c010ee..5e1ac58b5 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpSender.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/RTCRtpSender.java @@ -45,6 +45,9 @@ private RTCRtpSender() { * (video) or a zero-information-content equivalent. In the case of video, * the RTCRtpSender SHOULD send one black frame per second. If track is * {@code null} then the RTCRtpSender does not send. + *

+ * The returned object is a view of the sender's track; see + * {@link MediaStreamTrack#dispose()}. * * @return The media track associated with this sender. */ diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/media/MediaStream.java b/webrtc/src/main/java/dev/onvoid/webrtc/media/MediaStream.java index 690439211..fb3db0153 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/media/MediaStream.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/media/MediaStream.java @@ -43,7 +43,8 @@ private MediaStream() { /** * Returns an array of MediaStreamTrack objects representing the audio - * tracks in this stream. + * tracks in this stream. They are views of the stream's tracks; see + * {@link MediaStreamTrack#dispose()}. * * @return The audio tracks in this stream. */ @@ -51,7 +52,8 @@ private MediaStream() { /** * Returns an array of MediaStreamTrack objects representing the video - * tracks in this stream. + * tracks in this stream. They are views of the stream's tracks; see + * {@link MediaStreamTrack#dispose()}. * * @return The video tracks in this stream. */ diff --git a/webrtc/src/main/java/dev/onvoid/webrtc/media/MediaStreamTrack.java b/webrtc/src/main/java/dev/onvoid/webrtc/media/MediaStreamTrack.java index c17478318..68a34dae3 100644 --- a/webrtc/src/main/java/dev/onvoid/webrtc/media/MediaStreamTrack.java +++ b/webrtc/src/main/java/dev/onvoid/webrtc/media/MediaStreamTrack.java @@ -35,6 +35,13 @@ public abstract class MediaStreamTrack extends DisposableNativeObject { @SuppressWarnings("unused") private long listenerNativeHandle; + /** + * Set by native code when this object is a view of a track that something + * else owns, and holds no reference to it. Don't modify this value. + */ + @SuppressWarnings("unused") + private boolean view; + /** * If this object represents an audio track. */ @@ -86,6 +93,22 @@ public void removeTrackEndedListener(MediaStreamTrackEndedListener listener) { removeEndedEventListener(listener); } + /** + * Releases the native track held by this object. + *

+ * A track created by the {@link dev.onvoid.webrtc.PeerConnectionFactory} + * is owned by the object returned there: disposing it removes its + * remaining listeners and releases the track, and an {@link Error} is + * thrown if something else, such as an {@link dev.onvoid.webrtc.RTCRtpSender}, + * still holds it. + *

+ * A track obtained from {@link dev.onvoid.webrtc.RTCRtpSender#getTrack()}, + * {@link dev.onvoid.webrtc.RTCRtpReceiver#getTrack()}, a {@link MediaStream} + * or a track listener is a view of a track owned elsewhere. Disposing it + * only detaches this object; the track and the listeners registered on it + * are left as they are. A view is valid for as long as its owner keeps + * the track. + */ @Override public native void dispose(); diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/MediaStreamTrackViewTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/MediaStreamTrackViewTests.java new file mode 100644 index 000000000..efaba0b89 --- /dev/null +++ b/webrtc/src/test/java/dev/onvoid/webrtc/MediaStreamTrackViewTests.java @@ -0,0 +1,119 @@ +/* + * Copyright 2026 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.*; + +import dev.onvoid.webrtc.media.MediaStreamTrack; +import dev.onvoid.webrtc.media.MediaStreamTrackMuteListener; +import dev.onvoid.webrtc.media.audio.AudioOptions; +import dev.onvoid.webrtc.media.audio.AudioTrack; +import dev.onvoid.webrtc.media.audio.AudioTrackSource; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +/** + * Tracks obtained from a sender, a receiver or a track listener are views of + * a track owned elsewhere: disposing one must not release the track. + */ +@Execution(ExecutionMode.SAME_THREAD) +class MediaStreamTrackViewTests extends TestBase { + + private TestPeerConnection connection; + + private AudioTrackSource audioSource; + + private AudioTrack audioTrack; + + + @BeforeEach + void init() { + connection = new TestPeerConnection(factory); + audioSource = factory.createAudioSource(new AudioOptions()); + audioTrack = factory.createAudioTrack("audioTrack", audioSource); + } + + @AfterEach + void dispose() { + // The track goes after the connection, whose sender holds it. Its + // disposal fails if a view released a reference it did not own. + connection.close(); + audioTrack.dispose(); + audioSource.dispose(); + } + + @Test + void disposeSenderTrackView() { + RTCRtpSender sender = connection.getPeerConnection() + .addTrack(audioTrack, Collections.singletonList("stream")); + + MediaStreamTrack view = sender.getTrack(); + + assertDoesNotThrow(view::dispose); + assertThrows(NullPointerException.class, view::getId); + + // The track itself is untouched. + assertEquals(audioTrack.getId(), sender.getTrack().getId()); + assertTrue(audioTrack.isEnabled()); + + sender.dispose(); + } + + @Test + void disposeReceiverTrackView() { + RTCRtpTransceiver transceiver = connection.getPeerConnection() + .addTransceiver(audioTrack, new RTCRtpTransceiverInit()); + RTCRtpReceiver receiver = transceiver.getReceiver(); + + MediaStreamTrack view = receiver.getTrack(); + String id = view.getId(); + + assertDoesNotThrow(view::dispose); + assertEquals(id, receiver.getTrack().getId()); + + receiver.dispose(); + transceiver.dispose(); + } + + @Test + void disposeListenerTrackView() { + List errors = Collections.synchronizedList(new ArrayList<>()); + MediaStreamTrackMuteListener listener = (track, muted) -> { + try { + track.dispose(); + } + catch (Throwable e) { + errors.add(e); + } + }; + + audioTrack.addTrackMuteListener(listener); + audioTrack.setEnabled(false); + audioTrack.removeTrackMuteListener(listener); + + assertTrue(errors.isEmpty(), () -> "Errors: " + errors); + assertFalse(audioTrack.isEnabled()); + } +}