diff --git a/webrtc-jni/src/main/cpp/dependencies/jni-voithos/include/JavaContext.h b/webrtc-jni/src/main/cpp/dependencies/jni-voithos/include/JavaContext.h index 724ecaedd..41bfb4000 100644 --- a/webrtc-jni/src/main/cpp/dependencies/jni-voithos/include/JavaContext.h +++ b/webrtc-jni/src/main/cpp/dependencies/jni-voithos/include/JavaContext.h @@ -15,9 +15,11 @@ #include #include #include +#include #include #include #include +#include namespace jni { @@ -39,39 +41,81 @@ namespace jni JavaVM * getVM(); - void addNativeRef(JNIEnv * env, const JavaLocalRef & javaRef, const std::shared_ptr & nativeRef); + // Keeps a native object (a listener's native peer) alive for as long as + // the Java object stays registered. An entry is found again by the Java + // object together with its owner and kind: the same Java listener can be + // registered with several owners (tracks), or as several kinds of + // listener with one owner, and each registration has its own native + // object. + void addNativeRef(JNIEnv * env, const JavaLocalRef & javaRef, const std::shared_ptr & nativeRef, const void * owner = nullptr, int kind = 0); template - std::shared_ptr removeNativeRef(JNIEnv * env, const JavaLocalRef & javaRef) + std::shared_ptr removeNativeRef(JNIEnv * env, const JavaLocalRef & javaRef, const void * owner = nullptr, int kind = 0) { auto className = JavaClassUtils::toNativeClassName(env, javaRef); + + std::lock_guard lock(objectMapMutex); + auto it = objectMap.find(className); if (it == objectMap.end()) { return nullptr; } - auto globalRef = JavaGlobalRef(env, javaRef.get()); auto & list = it->second; std::shared_ptr nativeRef = nullptr; for (auto it = list.begin(); it != list.end(); ++it) { - if (env->IsSameObject(it->first, globalRef)) { - nativeRef = std::static_pointer_cast(it->second); + if (it->owner == owner && it->kind == kind && env->IsSameObject(it->javaRef, javaRef)) { + nativeRef = std::static_pointer_cast(it->nativeRef); list.erase(it); break; } } - env->DeleteGlobalRef(globalRef); - return nativeRef; } + // Removes every entry of the owner, of all kinds, which must all hold a + // native object of type T. + template + std::vector> removeNativeRefs(const void * owner) + { + std::vector> nativeRefs; + + std::lock_guard lock(objectMapMutex); + + for (auto & entry : objectMap) { + auto & list = entry.second; + + for (auto it = list.begin(); it != list.end();) { + if (it->owner == owner) { + nativeRefs.push_back(std::static_pointer_cast(it->nativeRef)); + it = list.erase(it); + } + else { + ++it; + } + } + } + + return nativeRefs; + } + private: + struct NativeRef + { + JavaGlobalRef javaRef; + const void * owner; + int kind; + std::shared_ptr nativeRef; + }; + JavaVM * vm; - // Java object class name mapped to a Java object reference and its native object reference. - std::unordered_map, std::shared_ptr>>> objectMap; + // Java object class name mapped to the registrations of Java objects of that class. + std::unordered_map> objectMap; + // Listeners are added and removed on any Java thread. + std::mutex objectMapMutex; }; } diff --git a/webrtc-jni/src/main/cpp/dependencies/jni-voithos/src/JavaContext.cpp b/webrtc-jni/src/main/cpp/dependencies/jni-voithos/src/JavaContext.cpp index 0f3813b09..a1dffc586 100644 --- a/webrtc-jni/src/main/cpp/dependencies/jni-voithos/src/JavaContext.cpp +++ b/webrtc-jni/src/main/cpp/dependencies/jni-voithos/src/JavaContext.cpp @@ -19,20 +19,13 @@ namespace jni return vm; } - void JavaContext::addNativeRef(JNIEnv * env, const JavaLocalRef & javaRef, const std::shared_ptr & nativeRef) + void JavaContext::addNativeRef(JNIEnv * env, const JavaLocalRef & javaRef, const std::shared_ptr & nativeRef, const void * owner, int kind) { auto className = JavaClassUtils::toNativeClassName(env, javaRef); - auto globalRef = JavaGlobalRef(env, javaRef.get()); - auto pair = std::make_pair(globalRef, nativeRef); - auto it = objectMap.find(className); + NativeRef entry = { JavaGlobalRef(env, javaRef.get()), owner, kind, nativeRef }; - if (it == objectMap.end()) { - std::list, std::shared_ptr>> list = { pair }; + std::lock_guard lock(objectMapMutex); - objectMap[className] = list; - } - else { - it->second.push_back(pair); - } + objectMap[className].push_back(std::move(entry)); } } \ No newline at end of file diff --git a/webrtc-jni/src/main/cpp/include/media/DeviceChangeListener.h b/webrtc-jni/src/main/cpp/include/media/DeviceChangeListener.h index 65a5a9f6d..d3230b488 100644 --- a/webrtc-jni/src/main/cpp/include/media/DeviceChangeListener.h +++ b/webrtc-jni/src/main/cpp/include/media/DeviceChangeListener.h @@ -38,6 +38,9 @@ namespace jni void deviceConnected(avdev::DevicePtr device) override; void deviceDisconnected(avdev::DevicePtr device) override; + private: + void notify(const avdev::DevicePtr & device, jmethodID method); + private: class JavaDeviceChangeListenerClass : public JavaClass { diff --git a/webrtc-jni/src/main/cpp/include/media/DeviceManager.h b/webrtc-jni/src/main/cpp/include/media/DeviceManager.h index a4482ff23..ce8bef388 100644 --- a/webrtc-jni/src/main/cpp/include/media/DeviceManager.h +++ b/webrtc-jni/src/main/cpp/include/media/DeviceManager.h @@ -19,6 +19,7 @@ #include #include +#include #include "media/DeviceHotplugListener.h" @@ -48,6 +49,7 @@ namespace jni void notifyListeners(DevicePtr device, const DeviceEvent event); std::list> hotplugListeners; + std::mutex listenerMutex; }; } } diff --git a/webrtc-jni/src/main/cpp/src/JNI_MediaDevices.cpp b/webrtc-jni/src/main/cpp/src/JNI_MediaDevices.cpp index 125ec53bb..f78aa1cb5 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_MediaDevices.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_MediaDevices.cpp @@ -68,6 +68,10 @@ JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_media_MediaDevices_getDefaultAu try { auto device = context->getAudioDeviceManager()->getDefaultAudioPlaybackDevice(); + if (device == nullptr) { + return nullptr; + } + return jni::AudioDevice::toJavaAudioDevice(env, device).release(); } catch (...) { @@ -85,6 +89,10 @@ JNIEXPORT jobject JNICALL Java_dev_onvoid_webrtc_media_MediaDevices_getDefaultAu try { auto device = context->getAudioDeviceManager()->getDefaultAudioCaptureDevice(); + if (device == nullptr) { + return nullptr; + } + return jni::AudioDevice::toJavaAudioDevice(env, device).release(); } catch (...) { diff --git a/webrtc-jni/src/main/cpp/src/JNI_MediaStreamTrack.cpp b/webrtc-jni/src/main/cpp/src/JNI_MediaStreamTrack.cpp index e23695dfa..1ab3bf3e9 100644 --- a/webrtc-jni/src/main/cpp/src/JNI_MediaStreamTrack.cpp +++ b/webrtc-jni/src/main/cpp/src/JNI_MediaStreamTrack.cpp @@ -33,6 +33,14 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_MediaStreamTrack_dispose webrtc::MediaStreamTrackInterface * track = GetHandle(env, caller); CHECK_HANDLE(track); + // 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 + // while the track (if something else still holds it) can still call it. + for (const auto & observer : javaContext->removeNativeRefs(track)) { + track->UnregisterObserver(observer.get()); + } + webrtc::RefCountReleaseStatus status = track->Release(); // Our reference is gone the moment Release() returns, regardless of @@ -104,7 +112,7 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_MediaStreamTrack_addEndedEve track->RegisterObserver(observer); - javaContext->addNativeRef(env, jni::JavaLocalRef(env, jListener), observerPtr); + javaContext->addNativeRef(env, jni::JavaLocalRef(env, jListener), observerPtr, track, static_cast(jni::MediaStreamTrackEvent::ended)); } catch (...) { ThrowCxxJavaException(env); @@ -118,7 +126,7 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_MediaStreamTrack_removeEnded CHECK_HANDLE(track); try { - auto observerPtr = javaContext->removeNativeRef(env, jni::JavaLocalRef(env, jListener)); + auto observerPtr = javaContext->removeNativeRef(env, jni::JavaLocalRef(env, jListener), track, static_cast(jni::MediaStreamTrackEvent::ended)); if (observerPtr) { track->UnregisterObserver(observerPtr.get()); @@ -141,7 +149,7 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_MediaStreamTrack_addMuteEven track->RegisterObserver(observer); - javaContext->addNativeRef(env, jni::JavaLocalRef(env, jListener), observerPtr); + javaContext->addNativeRef(env, jni::JavaLocalRef(env, jListener), observerPtr, track, static_cast(jni::MediaStreamTrackEvent::mute)); } catch (...) { ThrowCxxJavaException(env); @@ -155,7 +163,7 @@ JNIEXPORT void JNICALL Java_dev_onvoid_webrtc_media_MediaStreamTrack_removeMuteE CHECK_HANDLE(track); try { - auto observerPtr = javaContext->removeNativeRef(env, jni::JavaLocalRef(env, jListener)); + auto observerPtr = javaContext->removeNativeRef(env, jni::JavaLocalRef(env, jListener), track, static_cast(jni::MediaStreamTrackEvent::mute)); if (observerPtr) { track->UnregisterObserver(observerPtr.get()); diff --git a/webrtc-jni/src/main/cpp/src/media/DeviceChangeListener.cpp b/webrtc-jni/src/main/cpp/src/media/DeviceChangeListener.cpp index a7abaa7a7..a452644ce 100644 --- a/webrtc-jni/src/main/cpp/src/media/DeviceChangeListener.cpp +++ b/webrtc-jni/src/main/cpp/src/media/DeviceChangeListener.cpp @@ -22,6 +22,10 @@ #include "JavaUtils.h" #include "JNI_WebRTC.h" +#include "rtc_base/logging.h" + +#include + namespace jni { DeviceChangeListener::DeviceChangeListener(JNIEnv * env, const JavaGlobalRef & listener) : @@ -32,28 +36,15 @@ namespace jni void DeviceChangeListener::deviceConnected(avdev::DevicePtr device) { - JNIEnv * env = AttachCurrentThread(); - - if (env == nullptr) { - return; - } - - JavaLocalRef jdevice = nullptr; - - if (dynamic_cast(device.get())) { - jdevice = AudioDevice::toJavaAudioDevice(env, device); - } - else if (dynamic_cast(device.get())) { - const auto dev = dynamic_cast(device.get()); - jdevice = VideoDevice::toJavaVideoDevice(env, *dev); - } - - if (jdevice) { - env->CallVoidMethod(listener, javaClass->deviceConnected, jdevice.get()); - } + notify(device, javaClass->deviceConnected); } void DeviceChangeListener::deviceDisconnected(avdev::DevicePtr device) + { + notify(device, javaClass->deviceDisconnected); + } + + void DeviceChangeListener::notify(const avdev::DevicePtr & device, jmethodID method) { JNIEnv * env = AttachCurrentThread(); @@ -61,18 +52,36 @@ namespace jni return; } - JavaLocalRef jdevice = nullptr; + // This runs on a thread the operating system owns (a CoreAudio queue, the MMDevice + // notification thread, the PulseAudio main loop, ...), which has no handler for a C++ + // exception and never returns to Java. So nothing may be thrown from here, and what the + // Java listener throws is reported and cleared: left pending, it would be there for the + // next JNI call on this thread, which may be the next listener or the next event. + try { + JavaLocalRef jdevice = nullptr; - if (dynamic_cast(device.get())) { - jdevice = AudioDevice::toJavaAudioDevice(env, device); + if (dynamic_cast(device.get())) { + jdevice = AudioDevice::toJavaAudioDevice(env, device); + } + else if (dynamic_cast(device.get())) { + const auto dev = dynamic_cast(device.get()); + jdevice = VideoDevice::toJavaVideoDevice(env, *dev); + } + + if (jdevice) { + env->CallVoidMethod(listener, method, jdevice.get()); + } + } + catch (const std::exception & e) { + RTC_LOG(LS_ERROR) << "Device change notification failed: " << e.what(); } - else if (dynamic_cast(device.get())) { - const auto dev = dynamic_cast(device.get()); - jdevice = VideoDevice::toJavaVideoDevice(env, *dev); + catch (...) { + RTC_LOG(LS_ERROR) << "Device change notification failed"; } - if (jdevice) { - env->CallVoidMethod(listener, javaClass->deviceDisconnected, jdevice.get()); + if (env->ExceptionCheck()) { + env->ExceptionDescribe(); + env->ExceptionClear(); } } diff --git a/webrtc-jni/src/main/cpp/src/media/DeviceManager.cpp b/webrtc-jni/src/main/cpp/src/media/DeviceManager.cpp index ed1be5193..e689d5222 100644 --- a/webrtc-jni/src/main/cpp/src/media/DeviceManager.cpp +++ b/webrtc-jni/src/main/cpp/src/media/DeviceManager.cpp @@ -16,17 +16,23 @@ #include "media/DeviceManager.h" +#include + namespace jni { namespace avdev { void DeviceManager::attachHotplugListener(PDeviceHotplugListener listener) { + std::lock_guard lock(listenerMutex); + hotplugListeners.push_back(listener); } void DeviceManager::detachHotplugListener(PDeviceHotplugListener listener) { + std::lock_guard lock(listenerMutex); + hotplugListeners.remove_if([listener](std::weak_ptr p) { return !(p.owner_before(listener) || listener.owner_before(p)); }); @@ -44,21 +50,33 @@ namespace jni void DeviceManager::notifyListeners(DevicePtr device, const DeviceEvent event) { - for (auto i = hotplugListeners.begin(); i != hotplugListeners.end();) { - if ((*i).expired()) { - i = hotplugListeners.erase(i); - } - else { + // Listeners are attached and detached on Java threads while the operating system + // reports device changes on its own. The listeners are called on a copy, outside the + // lock, so that one may detach itself (or another) from within its callback. + std::vector listeners; + + { + std::lock_guard lock(listenerMutex); + + for (auto i = hotplugListeners.begin(); i != hotplugListeners.end();) { PDeviceHotplugListener listener = (*i).lock(); - if (event == DeviceEvent::Connected) { - listener->deviceConnected(device); + if (listener) { + listeners.push_back(listener); + ++i; } - else if (event == DeviceEvent::Disconnected) { - listener->deviceDisconnected(device); + else { + i = hotplugListeners.erase(i); } + } + } - ++i; + for (const PDeviceHotplugListener & listener : listeners) { + if (event == DeviceEvent::Connected) { + listener->deviceConnected(device); + } + else if (event == DeviceEvent::Disconnected) { + listener->deviceDisconnected(device); } } } diff --git a/webrtc-jni/src/main/cpp/src/media/audio/macos/CoreAudioDeviceManager.cpp b/webrtc-jni/src/main/cpp/src/media/audio/macos/CoreAudioDeviceManager.cpp index 97f03c7ff..66ebbca11 100644 --- a/webrtc-jni/src/main/cpp/src/media/audio/macos/CoreAudioDeviceManager.cpp +++ b/webrtc-jni/src/main/cpp/src/media/audio/macos/CoreAudioDeviceManager.cpp @@ -23,6 +23,7 @@ #include #include +#include #if !defined(MAC_OS_VERSION_12_0) || MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_VERSION_12_0 #define kAudioObjectPropertyElementMain kAudioObjectPropertyElementMaster @@ -94,12 +95,14 @@ namespace jni OSStatus status = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &pa, 0, nullptr, &dataSize); ThrowIfFailed(status, "CoreAudio: Enumerate audio endpoints failed"); - const int numDevices = dataSize / sizeof(AudioDeviceID); - AudioDeviceID * devIDs = new AudioDeviceID[numDevices]; + std::vector devIDs(dataSize / sizeof(AudioDeviceID)); - status = AudioObjectGetPropertyData(kAudioObjectSystemObject, &pa, 0, nullptr, &dataSize, devIDs); + status = AudioObjectGetPropertyData(kAudioObjectSystemObject, &pa, 0, nullptr, &dataSize, devIDs.data()); ThrowIfFailed(status, "CoreAudio: Enumerate audio endpoints failed"); + // The list may have shrunk between the two calls. + const int numDevices = dataSize / sizeof(AudioDeviceID); + AudioDeviceID defaultID = getDefaultDeviceID(scope); for (int i = 0; i < numDevices; i++) { @@ -119,8 +122,6 @@ namespace jni } } } - - delete[] devIDs; } void CoreAudioDeviceManager::onDevicesChanged() @@ -136,12 +137,14 @@ namespace jni OSStatus status = AudioObjectGetPropertyDataSize(kAudioObjectSystemObject, &pa, 0, nullptr, &dataSize); ThrowIfFailed(status, "CoreAudio: Enumerate audio endpoints failed"); - const int numDevices = dataSize / sizeof(AudioDeviceID); - AudioDeviceID * devIDs = new AudioDeviceID[numDevices]; + std::vector devIDs(dataSize / sizeof(AudioDeviceID)); - status = AudioObjectGetPropertyData(kAudioObjectSystemObject, &pa, 0, nullptr, &dataSize, devIDs); + status = AudioObjectGetPropertyData(kAudioObjectSystemObject, &pa, 0, nullptr, &dataSize, devIDs.data()); ThrowIfFailed(status, "CoreAudio: Enumerate audio endpoints failed"); + // The list may have shrunk between the two calls. + const int numDevices = dataSize / sizeof(AudioDeviceID); + // Check, if a new device is available. for (int i = 0; i < numDevices; i++) { bool found = false; @@ -201,10 +204,8 @@ namespace jni } // Check, if a device was disconnected. - checkDeviceGone(captureDevices, devIDs, numDevices, kAudioObjectPropertyScopeInput); - checkDeviceGone(playbackDevices, devIDs, numDevices, kAudioObjectPropertyScopeOutput); - - delete[] devIDs; + checkDeviceGone(captureDevices, devIDs.data(), numDevices, kAudioObjectPropertyScopeInput); + checkDeviceGone(playbackDevices, devIDs.data(), numDevices, kAudioObjectPropertyScopeOutput); } void CoreAudioDeviceManager::onDefaultDeviceChanged(const AudioObjectPropertyScope & scope, DeviceList & devices, const AudioDevicePtr & device) @@ -256,19 +257,20 @@ namespace jni AudioDevicePtr removed = devices.removeDevice(predicate); if (removed != nullptr) { + // Compare with the default this manager holds, not with a newly read one: a new + // device object never equals the removed one, and reading a device that has just + // gone fails. The system's new default, if any, is looked up in the list. if (scope == kAudioObjectPropertyScopeInput) { - AudioDevicePtr def = getDefaultAudioCaptureDevice(); - - if (removed == def) { - onDefaultDeviceChanged(scope, captureDevices, def); + if (removed == getDefaultCaptureDevice()) { + setDefaultCaptureDevice(nullptr); + onDefaultDeviceChanged(scope, captureDevices, nullptr); } removed->directionType = AudioDeviceDirectionType::adtCapture; } else if (scope == kAudioObjectPropertyScopeOutput) { - AudioDevicePtr def = getDefaultAudioPlaybackDevice(); - - if (removed == def) { - onDefaultDeviceChanged(scope, playbackDevices, def); + if (removed == getDefaultPlaybackDevice()) { + setDefaultPlaybackDevice(nullptr); + onDefaultDeviceChanged(scope, playbackDevices, nullptr); } removed->directionType = AudioDeviceDirectionType::adtRender; } @@ -281,6 +283,10 @@ namespace jni { AudioDeviceID defaultID = getDefaultDeviceID(scope); + if (defaultID == kAudioObjectUnknown) { + return nullptr; + } + return createAudioDevice(defaultID, scope); } @@ -295,7 +301,13 @@ namespace jni pa.mElement = kAudioObjectPropertyElementMain; OSStatus status = AudioObjectGetPropertyData(deviceID, &pa, 0, nullptr, &dataSize, &devNameRef); - ThrowIfFailed(status, "CoreAudio: Get device name failed"); + + // A device can go away between reading the device list and reading its properties. + // It is skipped, so that one such device does not cut short the whole enumeration. + if (status != noErr || devNameRef == nullptr) { + RTC_LOG(LS_WARNING) << "CoreAudio: Get device name failed: Device = " << deviceID << ", Status = " << status; + return nullptr; + } std::string name = CFStringRefToUTF8(devNameRef); std::string id = std::to_string(deviceID); diff --git a/webrtc-jni/src/main/cpp/src/media/audio/windows/WindowsAudioDeviceManager.cpp b/webrtc-jni/src/main/cpp/src/media/audio/windows/WindowsAudioDeviceManager.cpp index 2a46bf1b1..216c08ed9 100644 --- a/webrtc-jni/src/main/cpp/src/media/audio/windows/WindowsAudioDeviceManager.cpp +++ b/webrtc-jni/src/main/cpp/src/media/audio/windows/WindowsAudioDeviceManager.cpp @@ -168,16 +168,22 @@ namespace jni hr = deviceEnumerator->GetDevice(wDeviceID, &pDevice); delete[] wDeviceID; - THROW_IF_FAILED(hr, "Audio Device Manager: Enumerator get device with ID: %S failed", device->getDescriptor().c_str()); - - hr = pDevice->QueryInterface(__uuidof(IMMEndpoint), (void**)&endpoint); - THROW_IF_FAILED(hr, "Audio Device Manager: Device get endpoint failed"); - - hr = pDevice->OpenPropertyStore(STGM_READ, &propertyStore); - THROW_IF_FAILED(hr, "Audio Device Manager: Device open property store failed"); - - hr = propertyStore->GetValue(PKEY_AudioEndpoint_FormFactor, &ff); - THROW_IF_FAILED(hr, "Audio Device Manager: PropertyStore get form factor property name failed"); + // The transport and form factor are optional details: an endpoint that cannot be + // read (one that has just gone away) keeps the unknown ones, so that it does not + // fail the enumeration of all the other devices. + if (SUCCEEDED(hr)) { + hr = pDevice->QueryInterface(__uuidof(IMMEndpoint), (void**)&endpoint); + } + if (SUCCEEDED(hr)) { + hr = pDevice->OpenPropertyStore(STGM_READ, &propertyStore); + } + if (SUCCEEDED(hr)) { + hr = propertyStore->GetValue(PKEY_AudioEndpoint_FormFactor, &ff); + } + if (FAILED(hr)) { + RTC_LOG(LS_WARNING) << "MMF: Get form factor of device " << device->getDescriptor() << " failed: HRESULT = " << hr; + return; + } EndpointFormFactor formFactor = static_cast(ff.uintVal); PropVariantClear(&ff); @@ -223,6 +229,12 @@ namespace jni PropVariantInit(&pv); HRESULT hr = deviceEnumerator->GetDefaultAudioEndpoint(dataFlow, eMultimedia, &defaultDevice); + + // No device of this kind, so no default either. + if (hr == HRESULT_FROM_WIN32(ERROR_NOT_FOUND)) { + return nullptr; + } + THROW_IF_FAILED(hr, "MMF: Get default audio endpoint failed"); hr = defaultDevice->GetId(&defaultDeviceId); diff --git a/webrtc/src/test/java/dev/onvoid/webrtc/media/audio/AudioTrackTests.java b/webrtc/src/test/java/dev/onvoid/webrtc/media/audio/AudioTrackTests.java index 961b53773..f7b68948d 100644 --- a/webrtc/src/test/java/dev/onvoid/webrtc/media/audio/AudioTrackTests.java +++ b/webrtc/src/test/java/dev/onvoid/webrtc/media/audio/AudioTrackTests.java @@ -19,6 +19,14 @@ import static org.junit.jupiter.api.Assertions.*; import dev.onvoid.webrtc.TestBase; +import dev.onvoid.webrtc.media.MediaStreamTrack; +import dev.onvoid.webrtc.media.MediaStreamTrackEndedListener; +import dev.onvoid.webrtc.media.MediaStreamTrackMuteListener; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -71,4 +79,135 @@ void addRemoveSink() { audioTrack.removeSink(sink); } + @Test + void addRemoveListeners() { + MediaStreamTrackMuteListener muteListener = (track, muted) -> { }; + MediaStreamTrackEndedListener endedListener = track -> { }; + + audioTrack.addTrackMuteListener(muteListener); + audioTrack.addTrackEndedListener(endedListener); + + audioTrack.removeTrackMuteListener(muteListener); + audioTrack.removeTrackEndedListener(endedListener); + } + + @Test + void removeListenerFromOneOfTwoTracks() { + // Mute events are delivered before setEnabled() returns. + AudioTrack otherTrack = factory.createAudioTrack("otherTrack", + factory.createAudioSource(new AudioOptions())); + List mutedTracks = Collections.synchronizedList(new ArrayList<>()); + MediaStreamTrackMuteListener listener = (track, muted) -> mutedTracks.add(track.getId()); + + audioTrack.addTrackMuteListener(listener); + otherTrack.addTrackMuteListener(listener); + + // Removing the second registration must not take the first one. + otherTrack.removeTrackMuteListener(listener); + + otherTrack.setEnabled(false); + audioTrack.setEnabled(false); + + audioTrack.removeTrackMuteListener(listener); + otherTrack.dispose(); + + assertEquals(Collections.singletonList("audioTrack"), mutedTracks); + } + + @Test + void removeListenerOfOneKind() { + // One object registered as both a mute and an ended listener. + List muteEvents = Collections.synchronizedList(new ArrayList<>()); + + class Listener implements MediaStreamTrackMuteListener, MediaStreamTrackEndedListener { + + @Override + public void onTrackMute(MediaStreamTrack track, boolean muted) { + muteEvents.add(muted); + } + + @Override + public void onTrackEnd(MediaStreamTrack track) { + } + } + + Listener listener = new Listener(); + + audioTrack.addTrackMuteListener(listener); + audioTrack.addTrackEndedListener(listener); + + audioTrack.removeTrackEndedListener(listener); + + audioTrack.setEnabled(false); + + audioTrack.removeTrackMuteListener(listener); + + assertEquals(Collections.singletonList(true), muteEvents); + } + + @Test + void disposeWithListeners() { + AudioTrack track = factory.createAudioTrack("otherTrack", + factory.createAudioSource(new AudioOptions())); + + track.addTrackMuteListener((t, muted) -> { }); + track.addTrackEndedListener(t -> { }); + + assertDoesNotThrow(track::dispose); + } + + @Test + void addRemoveListenersConcurrently() throws Exception { + // Each thread has a track of its own and new listeners (a lambda without + // captures would be one shared instance), so that the only state the + // threads share is the native registry of Java listeners. + final int threadCount = 8; + List tracks = new ArrayList<>(); + + for (int i = 0; i < threadCount; i++) { + AudioTrackSource source = factory.createAudioSource(new AudioOptions()); + tracks.add(factory.createAudioTrack("audioTrack" + i, source)); + } + + CountDownLatch start = new CountDownLatch(1); + List errors = Collections.synchronizedList(new ArrayList<>()); + List threads = new ArrayList<>(); + + for (AudioTrack track : tracks) { + Thread thread = new Thread(() -> { + try { + start.await(); + + for (int i = 0; i < 200; i++) { + MediaStreamTrackEndedListener listener = new MediaStreamTrackEndedListener() { + + @Override + public void onTrackEnd(MediaStreamTrack t) { + } + }; + + track.addTrackEndedListener(listener); + track.removeTrackEndedListener(listener); + } + } + catch (Throwable e) { + errors.add(e); + } + }); + thread.start(); + threads.add(thread); + } + + start.countDown(); + + for (Thread thread : threads) { + thread.join(); + } + for (AudioTrack track : tracks) { + track.dispose(); + } + + assertTrue(errors.isEmpty(), () -> "Errors: " + errors); + } + }