From 81c832a59ea96ede30d81626bb13f2d1006876c6 Mon Sep 17 00:00:00 2001 From: Richard Osborne Date: Wed, 6 May 2026 14:23:10 -0500 Subject: [PATCH 1/2] Revert "settings: revert 01b09d90 / fix device list (#1609)" This reverts commit 4434fda1cfc294013bba93763f283978fa365da3. --- obs-studio-server/source/nodeobs_settings.cpp | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/obs-studio-server/source/nodeobs_settings.cpp b/obs-studio-server/source/nodeobs_settings.cpp index c691c371a..5183e7a14 100644 --- a/obs-studio-server/source/nodeobs_settings.cpp +++ b/obs-studio-server/source/nodeobs_settings.cpp @@ -3877,24 +3877,17 @@ void getDevices(const char *source_id, const char *property_name, std::vector Date: Fri, 8 May 2026 18:54:15 -0500 Subject: [PATCH 2/2] set target 12.0, add video test, replay-buffer test skip CI, getDevices() * rename legacy getDevices() -> getDevicesUsingDummySource(). Adding more error checking * set CI flag for MacOS test runner so it can use obs.isCI() --- .github/workflows/main.yml | 1 + CMakeLists.txt | 8 +- ci/configure-osn-osx.sh | 2 +- obs-studio-server/source/nodeobs_settings.cpp | 97 +++++++++++++------ .../src/test_osn_advanced_replayBuffer.ts | 6 ++ tests/osn-tests/src/test_osn_audio.ts | 5 +- tests/osn-tests/src/test_osn_video.ts | 11 +++ tests/osn-tests/util/error_messages.ts | 2 + tests/osn-tests/util/obs_handler.ts | 2 +- 9 files changed, 100 insertions(+), 34 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e803aa57d..32bf046f5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -151,6 +151,7 @@ jobs: OSN_ACCESS_KEY_ID: ${{secrets.AWS_RELEASE_ACCESS_KEY_ID}} OSN_SECRET_ACCESS_KEY: ${{secrets.AWS_RELEASE_SECRET_ACCESS_KEY}} RELEASE_NAME: ${{matrix.ReleaseName}} + CI: true # Run even after test failures so the PR still gets the flaky summary. - name: Publish flaky test check if: ${{ always() }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 93805110f..d0f76767e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,15 +29,15 @@ ENDIF() # Comfigure macos architecture and min version iF(APPLE) - set(CMAKE_XCODE_ATTRIBUTE_MACOSX_DEPLOYMENT_TARGET[arch=arm64] "11.0") - set(CMAKE_XCODE_ATTRIBUTE_MACOSX_DEPLOYMENT_TARGET[arch=x86_64] "10.15") + set(CMAKE_XCODE_ATTRIBUTE_MACOSX_DEPLOYMENT_TARGET[arch=arm64] "12.0") + set(CMAKE_XCODE_ATTRIBUTE_MACOSX_DEPLOYMENT_TARGET[arch=x86_64] "12.0") if (NOT CMAKE_OSX_ARCHITECTURES) set(CMAKE_OSX_ARCHITECTURES "${CMAKE_HOST_SYSTEM_PROCESSOR}") endif() if ("${CMAKE_OSX_ARCHITECTURES}" STREQUAL "arm64") - set(CMAKE_OSX_DEPLOYMENT_TARGET "11.0") + set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0") else() - set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15") + set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0") endif() # See CMake issue # 21854 # https://gitlab.kitware.com/cmake/cmake/-/issues/21854 diff --git a/ci/configure-osn-osx.sh b/ci/configure-osn-osx.sh index d5fef7920..bae1d6e62 100755 --- a/ci/configure-osn-osx.sh +++ b/ci/configure-osn-osx.sh @@ -8,7 +8,7 @@ cd .. [ -n "${ARCHITECTURE}" ] && CMAKE_OSX_ARCHITECTURES_PARAM="-DCMAKE_OSX_ARCHITECTURES=${ARCHITECTURE}" || CMAKE_OSX_ARCHITECTURES_PARAM="" cmake .. \ --DCMAKE_OSX_DEPLOYMENT_TARGET=10.15 \ +-DCMAKE_OSX_DEPLOYMENT_TARGET=12.0 \ -DCMAKE_INSTALL_PREFIX=$PWD/../${SLFullDistributePath}/${InstallPath} \ -DSTREAMLABS_BUILD=OFF \ -DNODEJS_NAME=${RuntimeName} \ diff --git a/obs-studio-server/source/nodeobs_settings.cpp b/obs-studio-server/source/nodeobs_settings.cpp index 5183e7a14..143b073be 100644 --- a/obs-studio-server/source/nodeobs_settings.cpp +++ b/obs-studio-server/source/nodeobs_settings.cpp @@ -20,6 +20,7 @@ #include "osn-error.hpp" #include "nodeobs_api.h" #include "shared.hpp" +#include #include "memory-manager.h" #include "osn-video.hpp" #include "osn-encoders.hpp" @@ -3864,39 +3865,87 @@ void OBS_settings::saveGenericSettings(std::vector genericSettings, config_save_safe(config, "tmp", nullptr); } -void getDevices(const char *source_id, const char *property_name, std::vector &rval) +// Gets the devices using a "dummy" source object that does not exist. For most +// plugins this is fine but should be avoided for plugins like mac-coreaudio +// because it will create a reconnect_thread. Triggers the following callbacks +// on the obs_source_info: get_properties, get_defaults, and get_updates. +void getDevicesUsingDummySource(const char *source_id, const char *property_name, std::vector &rval) { - auto settings = obs_get_source_defaults(source_id); - if (!settings) - return; + OBSDataAutoRelease settings = obs_get_source_defaults(source_id); + if (!settings) { + std::ostringstream ss; + ss << "Could not get settings for source id: " << source_id; + PRETTY_ERROR_RETURN(ErrorCode::Error, ss.str()); + } const char *dummy_device_name = "does_not_exist"; obs_data_set_string(settings, property_name, dummy_device_name); - if (strcmp(source_id, "dshow_input") == 0) { - obs_data_set_string(settings, "video_device_id", dummy_device_name); - obs_data_set_string(settings, "audio_device_id", dummy_device_name); + + // Create a dummy source so that the "device" property can be init + OBSSourceAutoRelease dummy_source = obs_source_create(source_id, dummy_device_name, settings, nullptr); + if (!dummy_source) { + std::ostringstream ss; + ss << "Could not create source: " << source_id; + PRETTY_ERROR_RETURN(ErrorCode::Error, ss.str()); + } + + auto props = obs_source_properties(dummy_source); + if (!props) { + std::ostringstream ss; + ss << "Could not get source properties for source id: " << source_id; + PRETTY_ERROR_RETURN(ErrorCode::Error, ss.str()); + } + + auto prop = obs_properties_get(props, property_name); + if (!prop) { + obs_properties_destroy(props); + std::ostringstream ss; + ss << "Could not get source property " << property_name << " for source id: " << source_id; + PRETTY_ERROR_RETURN(ErrorCode::Error, ss.str()); + } + + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); + size_t items = obs_property_list_item_count(prop); + rval.push_back(ipc::value((uint64_t)items)); + + for (size_t idx = 0; idx < items; idx++) { + const char *description = obs_property_list_item_name(prop, idx); + const char *device_id = obs_property_list_item_string(prop, idx); + + if (!description || !strcmp(description, "") || !device_id || !strcmp(device_id, "")) { + rval[1].value_union.ui64--; + continue; + } + + rval.push_back(ipc::value(description)); + rval.push_back(ipc::value(device_id)); } + obs_properties_destroy(props); +} + +// Gets the devices from the source using a property list without triggering +// obs_source_info.get_updates callback. +void getDevices(const char *source_id, const char *property_name, std::vector &rval) +{ auto props = obs_get_source_properties(source_id); if (!props) { - obs_data_release(settings); - blog(LOG_WARNING, "Could not get source properties for source id: %s", source_id); - return; + std::ostringstream ss; + ss << "Could not get source properties for source id: " << source_id; + PRETTY_ERROR_RETURN(ErrorCode::Error, ss.str()); } auto prop = obs_properties_get(props, property_name); if (!prop) { - blog(LOG_WARNING, "Could not get the property [%s] for source id: %s", property_name, source_id); obs_properties_destroy(props); - obs_data_release(settings); - return; + std::ostringstream ss; + ss << "Could not get source property " << property_name << " for source id: " << source_id; + PRETTY_ERROR_RETURN(ErrorCode::Error, ss.str()); } + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); size_t items = obs_property_list_item_count(prop); - if (rval.size() > 1) - rval[1].value_union.ui64 += items; - else - rval.push_back(ipc::value((uint64_t)items)); + rval.push_back(ipc::value((uint64_t)items)); for (size_t idx = 0; idx < items; idx++) { const char *description = obs_property_list_item_name(prop, idx); @@ -3912,7 +3961,6 @@ void getDevices(const char *source_id, const char *property_name, std::vector &rval, EDataFlow dataFlow) void OBS_settings::OBS_settings_getInputAudioDevices(void *data, const int64_t id, const std::vector &args, std::vector &rval) { - rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - #ifdef WIN32 + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); rval.push_back(ipc::value((uint32_t)1)); rval.push_back(ipc::value("Default")); rval.push_back(ipc::value("default")); @@ -4066,9 +4113,8 @@ void OBS_settings::OBS_settings_getInputAudioDevices(void *data, const int64_t i void OBS_settings::OBS_settings_getOutputAudioDevices(void *data, const int64_t id, const std::vector &args, std::vector &rval) { - rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - #ifdef WIN32 + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); rval.push_back(ipc::value((uint32_t)1)); rval.push_back(ipc::value("Default")); rval.push_back(ipc::value("default")); @@ -4083,15 +4129,12 @@ void OBS_settings::OBS_settings_getOutputAudioDevices(void *data, const int64_t void OBS_settings::OBS_settings_getVideoDevices(void *data, const int64_t id, const std::vector &args, std::vector &rval) { - rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); - #ifdef WIN32 + rval.push_back(ipc::value((uint64_t)ErrorCode::Ok)); rval.push_back(ipc::value((uint32_t)0)); enumVideoDevices(rval); #elif __APPLE__ - const char *source_id = "macos_avcapture"; - const char *property_name = "device"; - getDevices(source_id, property_name, rval); + getDevicesUsingDummySource("macos_avcapture", "device", rval); #endif AUTO_DEBUG; diff --git a/tests/osn-tests/src/test_osn_advanced_replayBuffer.ts b/tests/osn-tests/src/test_osn_advanced_replayBuffer.ts index 53a8b290a..8f912ca62 100644 --- a/tests/osn-tests/src/test_osn_advanced_replayBuffer.ts +++ b/tests/osn-tests/src/test_osn_advanced_replayBuffer.ts @@ -131,6 +131,9 @@ describe(testName, () => { }); it('Start advanced replay buffer - Use Recording', async function() { + if (obs.isDarwin() && obs.isCI()) { + this.skip(); + } replayBuffer = osn.AdvancedReplayBufferFactory.create(); replayBuffer.path = path.join(path.normalize(__dirname), '..', 'osnData'); replayBuffer.format = osn.ERecordingFormat.MP4; @@ -258,6 +261,9 @@ describe(testName, () => { }); it('Start advanced replay buffer - Use Stream through Recording', async function() { + if (obs.isDarwin() && obs.isCI()) { + this.skip(); + } replayBuffer = osn.AdvancedReplayBufferFactory.create(); replayBuffer.path = path.join(path.normalize(__dirname), '..', 'osnData'); replayBuffer.format = osn.ERecordingFormat.MP4; diff --git a/tests/osn-tests/src/test_osn_audio.ts b/tests/osn-tests/src/test_osn_audio.ts index b3a75c586..ad69dee0d 100644 --- a/tests/osn-tests/src/test_osn_audio.ts +++ b/tests/osn-tests/src/test_osn_audio.ts @@ -70,7 +70,10 @@ describe(testName, () => { foundDefaultDevice = true; } } - if (!obs.isDarwin()) { // On virtual mac the default output device is not included in the list of output devices + // On Darwin CI, skip the default-device assertion only when there are no audio devices. + // mac-coreaudio only returns the default device when there is at least one audio device, but on CI there may be no audio devices, so the default device is not returned. + const shouldSkipDefaultDeviceAssertion = obs.isDarwin() && obs.isCI() && devices.length === 0; + if (!shouldSkipDefaultDeviceAssertion) { expect(foundDefaultDevice).to.equal(true, GetErrorMessage(ETestErrorMsg.DefaultDeviceNotFound)); } }); diff --git a/tests/osn-tests/src/test_osn_video.ts b/tests/osn-tests/src/test_osn_video.ts index 167a2b964..163c2625d 100644 --- a/tests/osn-tests/src/test_osn_video.ts +++ b/tests/osn-tests/src/test_osn_video.ts @@ -191,4 +191,15 @@ describe(testName, () => { context.destroy(); }); + + it('Get video capture devices', function() { + const devices = osn.NodeObs.OBS_settings_getVideoDevices(); + expect(devices).to.not.equal(undefined, GetErrorMessage(ETestErrorMsg.VideoDevices)); + expect(Array.isArray(devices)).to.equal(true, GetErrorMessage(ETestErrorMsg.VideoDevicesIsArray)); + for (const device of devices) { + expect(device).to.have.property('id'); + expect(device).to.have.property('description'); + logInfo(testName, `Video Capture Device Found: ${device.description} with id: ${device.id}`); + } + }); }); diff --git a/tests/osn-tests/util/error_messages.ts b/tests/osn-tests/util/error_messages.ts index 470f28864..76c26e55e 100644 --- a/tests/osn-tests/util/error_messages.ts +++ b/tests/osn-tests/util/error_messages.ts @@ -203,6 +203,8 @@ export const enum ETestErrorMsg { AudioDevices = 'Failed to get audio devices', AudioDevicesIsArray = 'Returned audio devices value is not an array', DefaultDeviceNotFound = 'Did not find the default audio device in the list of audio devices', + VideoDevices = 'Failed to get video devices', + VideoDevicesIsArray = 'Returned video devices value is not an array', } export function GetErrorMessage(message: string, value1?: string, value2?: string, value3?: string): string { diff --git a/tests/osn-tests/util/obs_handler.ts b/tests/osn-tests/util/obs_handler.ts index d44168b8b..a3b8de811 100644 --- a/tests/osn-tests/util/obs_handler.ts +++ b/tests/osn-tests/util/obs_handler.ts @@ -553,7 +553,7 @@ export class OBSHandler { isDarwin() { - // Wrapped this in a function- just incase we want to add more conditions later or disable only within the build agent. + // Wrapped this in a function- just in case we want to add more conditions later or disable only within the build agent. return this.os === 'darwin'; }