From 90e2d366a183d9651a089d93c3e895190742e56b Mon Sep 17 00:00:00 2001 From: "RICKPORTABLE\\rosbo" Date: Fri, 16 Oct 2026 17:06:11 -0500 Subject: [PATCH] Fix obs-filter(s) not rendering - Add test to verify filters work properly - Now the filter is no longer skipped by obs_source_skip_video_filter() due to the target && effect entries being null. - Asana ticket: https://app.asana.com/1/1083097041131/project/1207748235152481/task/1217844168779754?focus=true All tests pass- ctest --test-dir build -C RelWithDebInfo -R "obs_studio_server_unit_tests::" --output-on-failure --- obs-studio-server/source/osn-source.cpp | 2 +- obs-studio-server/tests/test-osn-source.cpp | 88 +++++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/obs-studio-server/source/osn-source.cpp b/obs-studio-server/source/osn-source.cpp index 0e1377bdf..a299fc2c9 100644 --- a/obs-studio-server/source/osn-source.cpp +++ b/obs-studio-server/source/osn-source.cpp @@ -207,7 +207,7 @@ void osn::Source::Release(void *data, const int64_t id, const std::vector #include "nodeobs_api.h" #include "osn-error.hpp" +#include "osn-filter.hpp" #include "osn-input.hpp" #include "osn-scene.hpp" #include "osn-source.hpp" @@ -50,6 +51,39 @@ obs_source_info makeTestSourceInfo() return info; } +// A video filter can only be attached to a source that also outputs video. +constexpr char TEST_VIDEO_SOURCE_ID[] = "filter_release_test_video_source"; +constexpr char TEST_VIDEO_FILTER_ID[] = "filter_release_test_video_filter"; + +obs_source_info makeTestVideoSourceInfo() +{ + obs_source_info info = makeTestSourceInfo(); + info.id = TEST_VIDEO_SOURCE_ID; + info.output_flags = OBS_SOURCE_VIDEO; + return info; +} + +void *testFilterCreate(obs_data_t *settings, obs_source_t *source) +{ + obs_source_update(source, settings); + return source; +} + +void testFilterUpdate(void *, obs_data_t *) {} + +obs_source_info makeTestVideoFilterInfo() +{ + obs_source_info info{}; + info.id = TEST_VIDEO_FILTER_ID; + info.type = OBS_SOURCE_TYPE_FILTER; + info.output_flags = OBS_SOURCE_VIDEO; + info.get_name = testSourceGetName; + info.create = testFilterCreate; + info.destroy = testSourceDestroy; + info.update = testFilterUpdate; + return info; +} + // These tests only need the OBS core and its deferred-destruction queue. class ObsCoreSetup { public: @@ -308,3 +342,57 @@ TEST_CASE("Source manager safely promotes references during deferred destruction CHECK(manager.size() == sourceCount); } } + +TEST_CASE("Releasing a filter after attaching it keeps the filter active") +{ + ObsCoreSetup setupOBS; + static const obs_source_info testVideoSourceInfo = makeTestVideoSourceInfo(); + static const obs_source_info testVideoFilterInfo = makeTestVideoFilterInfo(); + obs_register_source(&testVideoSourceInfo); + obs_register_source(&testVideoFilterInfo); + auto &manager = osn::Source::Manager::GetInstance(); + const auto sourceCount = manager.size(); + + OBSSourceAutoRelease input = obs_source_create_private(TEST_VIDEO_SOURCE_ID, "filtered input", nullptr); + REQUIRE(input != nullptr); + const uint64_t inputId = manager.allocate(input); + osn::Source::attach_source_signals(input); + REQUIRE(inputId != UINT64_MAX); + + // Follow the sequence used by Streamlabs Desktop when it adds a filter. + std::vector response; + osn::Filter::Create(nullptr, 0, {ipc::value(TEST_VIDEO_FILTER_ID), ipc::value("released filter")}, response); + REQUIRE(response.size() >= 2); + REQUIRE((ErrorCode)response[0].value_union.ui64 == ErrorCode::Ok); + const uint64_t filterId = response[1].value_union.ui64; + + response.clear(); + osn::Input::AddFilter(nullptr, 0, {ipc::value(inputId), ipc::value(filterId)}, response); + REQUIRE(!response.empty()); + REQUIRE((ErrorCode)response[0].value_union.ui64 == ErrorCode::Ok); + + response.clear(); + osn::Source::Release(nullptr, 0, {ipc::value(filterId)}, response); + REQUIRE(!response.empty()); + CHECK((ErrorCode)response[0].value_union.ui64 == ErrorCode::Ok); + + { + // The input now holds the only reference. The filter must not be marked + // removed since libobs skips removed sources when ticking. + OBSSourceAutoRelease filter = manager.findAndRef(filterId); + REQUIRE(filter != nullptr); + CHECK(!obs_source_removed(filter)); + } + + response.clear(); + osn::Input::RemoveFilter(nullptr, 0, {ipc::value(inputId), ipc::value(filterId)}, response); + REQUIRE(!response.empty()); + CHECK((ErrorCode)response[0].value_union.ui64 == ErrorCode::Ok); + obs_wait_for_destroy_queue(); + CHECK(!manager.findAndRef(filterId)); + CHECK(manager.size() == sourceCount + 1); + + input = nullptr; + obs_wait_for_destroy_queue(); + CHECK(manager.size() == sourceCount); +}