From 023eaea7a393a9bd3115665872dcc84bd7c06148 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sat, 10 Oct 2026 14:37:18 +0200 Subject: [PATCH 1/2] fix: keep a camera without a udev product name from aborting the process The V4L2 hotplug thread made a std::string from udev's ID_V4L_PRODUCT property, which only udev's v4l_id rule sets. Where that rule is missing, as on minimal systems and in containers, the property is null and the thread, and the process with it, aborted on the first camera plugged in. A null subsystem, action or device node would have crashed strcmp() and open() the same way, and an enumerated device without a node was opened with null too. Such events are now skipped, and a device without a product name is named by its node. A removed device is found by its node alone: its name came from the driver at enumeration and from udev at hotplug, which need not agree, so comparing names could leave an unplugged camera listed with no event. --- .../video/linux/V4l2VideoDeviceManager.h | 2 +- .../video/linux/V4l2VideoDeviceManager.cpp | 33 +++++++++++++------ 2 files changed, 24 insertions(+), 11 deletions(-) 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/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); From 38964536fb287d246b5ffe81f877a3d63c30b6d9 Mon Sep 17 00:00:00 2001 From: Alex Andres Date: Sat, 10 Oct 2026 14:37:19 +0200 Subject: [PATCH 2/2] fix: dispose a track from a sender, receiver, stream or listener as a view RTCRtpSender.getTrack(), RTCRtpReceiver.getTrack(), MediaStream's track arrays and the track passed to a track listener wrapped the native track without taking a reference, while MediaStreamTrack.dispose() released one. Disposing such a track dropped a reference that the sender, receiver or stream held, and it could free the track while WebRTC still used it; the dispose of the factory track it came from failed or crashed later. These objects are now marked as views (MediaStreamTrackView). Disposing a view only detaches the Java object: it releases nothing, throws nothing, and leaves the listeners registered on the track alone. Tracks created by the factory are unchanged, including the error when something still holds them on dispose. Views stay without a reference of their own on purpose: callers do not dispose them today, and a reference held by every view, including one per listener event, would keep tracks alive and make their owner's dispose fail. MediaStreamTrackViewTests disposes the view of a sender's track, of a receiver's track, and the track given to a listener. With the view check taken out of dispose(), the first two crashed the JVM. --- docs/guide/video/camera-capture.md | 3 +- .../cpp/include/media/MediaStreamTrackView.h | 80 ++++++++++++ .../src/main/cpp/src/JNI_MediaStream.cpp | 5 +- .../src/main/cpp/src/JNI_MediaStreamTrack.cpp | 8 ++ .../src/main/cpp/src/JNI_RTCRtpReceiver.cpp | 11 +- .../src/main/cpp/src/JNI_RTCRtpSender.cpp | 11 +- .../src/media/MediaStreamTrackObserver.cpp | 11 +- .../cpp/src/media/MediaStreamTrackView.cpp | 60 +++++++++ .../dev/onvoid/webrtc/RTCRtpReceiver.java | 3 + .../java/dev/onvoid/webrtc/RTCRtpSender.java | 3 + .../dev/onvoid/webrtc/media/MediaStream.java | 6 +- .../onvoid/webrtc/media/MediaStreamTrack.java | 23 ++++ .../webrtc/MediaStreamTrackViewTests.java | 119 ++++++++++++++++++ 13 files changed, 313 insertions(+), 30 deletions(-) create mode 100644 webrtc-jni/src/main/cpp/include/media/MediaStreamTrackView.h create mode 100644 webrtc-jni/src/main/cpp/src/media/MediaStreamTrackView.cpp create mode 100644 webrtc/src/test/java/dev/onvoid/webrtc/MediaStreamTrackViewTests.java 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/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/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()); + } +}