obs-streamer-tools-plugin
Native OBS Studio source plugin that pulls streamer-tools camera feeds
directly from LiveKit over WebRTC, replacing the current SRT/RTSP-via-VLC-or-
Media-Source path for directors. Full design:
docs/superpowers/specs/2026-09-06-obs-camera-plugin-design.md in the
streamer-tools repo.
Status
This project's own code is Apache-2.0 (relicensed from GPL-2.0-or-later to
match the vendored LiveKit binaries, which are also Apache-2.0 — see
LICENSE and NOTICE, and third_party/livekit/ for LiveKit's own).
.gitea/workflows/build.yml builds, tests, and uploads CI-internal build
artifacts on every push. .gitea/workflows/release.yml packages a tagged
build (v*) into a published Gitea Release. It created drafts until
2026-09-09, gated on a human clicking Publish because nobody had run the
plugin in the OBS GUI; the first confirmed GUI load retired that gate, and the
remaining caveats live in the generated release notes instead.
The plugin is functionally complete on Linux and verified end to end there
(module loads into real libobs, connects to a real LiveKit server through the
real streamer-tools API shape, and pushes decoded frames into
obs_source_output_video/_audio).
First confirmed OBS GUI load: Windows, 2026-09-09 — the v0.1.0 release
artifact loaded into OBS 32.2.2 on Windows 11 (build 26200) on a director's
machine, from
C:\ProgramData\obs-studio\plugins\streamer-tools-camera\bin\64bit\.
That retires "the module will not even load in a real OBS" for Windows. It
does not yet cover whether video renders correctly, colours, A/V sync or
latency — see "Not verified anywhere" below for what is still open. Linux and
macOS have still never been opened in the GUI; macOS builds the real module in
CI but its artifact is not yet loadable (see the macOS packaging gap under CI).
⚠️ The install directory is not the same on every platform, and getting it
wrong fails silently. On Windows it is
C:\ProgramData\obs-studio\plugins\ (GetProgramDataPath →
CSIDL_COMMON_APPDATA), not %APPDATA%\obs-studio\ — see the packaging
section. That mistake cost the director above an evening: OBS logs nothing at
all for a plugin it never finds.
Windows CI is now green. The run at f27b1c0 is the first completed
green Windows job on this repository: the from-source libobs bootstrap
configures, builds and installs, find_package(libobs) resolves, all 6 CTest
suites pass, and build\package\bin\64bit\streamer-tools-camera.dll
(136,192 bytes) is staged next to livekit.dll and livekit_ffi.dll — read
out of the job's own log body, not inferred from the job status. That also
retires three previously-unproven items in one go: the -A x64 argument fix,
the PowerShell rewrite of the Windows steps, and the add_subdirectory
patch for OBS::w32-pthreads. Windows has since been loaded in the real OBS
GUI (see above); Linux and macOS have not. See "Where the Windows bootstrap got
to" under CI below for the whole trace, and check current CI status rather
than trusting this paragraph's age.
See "What is verified, and how" below for exactly what has and has not been checked, and "Testing this by hand" for what a human still needs to do.
Layout
cmake/LiveKitSDK.cmake - downloads + unpacks the pinned client-sdk-cpp release
core/ - core library (C++17, no OBS dependency, headless-testable)
include/stplugin/
core.h version + ConnectionConfig
json.h small strict JSON reader
http.h injectable HTTP client interface
api_client.h the two /api/obs/:slug/* calls
session_types.h media/state types + the pure session logic
session.h LiveKitSession, the livekit::Room wrapper
src/
http_curl.cpp libcurl backend (Linux/macOS)
http_winhttp.cpp WinHTTP backend (Windows)
tests/ dependency-free CTest suites
obs-adapter/ - thin OBS glue (C++)
src/plugin-main.cpp obs_source_info, properties UI, frame output
data/locale/en-US.ini
scripts/livekit-dev-room.py - mints tokens for the integration test
third_party/livekit/ - redistribution notices for the LiveKit binaries
.gitea/scripts/ - the actual per-platform build commands, shared by build.yml and release.yml
.gitea/workflows/build.yml - 3-platform CI matrix (every push/PR; never publishes)
.gitea/workflows/release.yml - packages + publishes a Gitea Release (only on a `v*` tag push; see Status above)
How it works
- The operator fills in the streamer-tools server URL, room slug and read key, and picks a camera from the dropdown.
- The source's own worker thread calls
POST /api/obs/:slug/token?key=…to mint a hidden, subscribe-only LiveKit token (identityobs:<slug>:<nonce>— a fresh nonce per mint, so two OBS installs watching the same room can never kick each other). LiveKitSessionconnectslivekit::Roomto the returnedwsUrl, waits for the chosen participant'sSource.Cameravideo track (and their microphone), and reads decoded frames offVideoStream/AudioStream.- The adapter hands those straight to
obs_source_output_video/obs_source_output_audio.
Nothing on the OBS UI thread ever blocks on the network. The one deliberate exception is the "Refresh camera list" button, which the operator pressed and is waiting on; it uses a shortened 5s timeout.
Design decisions worth knowing before changing this
- Frames come from
VideoStream::fromTrackwith our own reader threads, not fromRoom::setOnVideoFrameCallback. The dispatcher API is keyed by (participant identity, track name), which is only knowable once the track is published — and disassembly ofliblivekit.so1.10.1 confirms that neitherRoom::setOnVideoFrameCallbacknor the dispatcher's own version starts a reader for an already-subscribed track; they only record the registration. Registering at the only moment the name exists would therefore have silently produced no video. - Every stream operation runs on one owned worker thread, never on a
LiveKit room event thread:
Room::disconnect()from inside a delegate callback is documented to deadlock. VideoStream::Options::capacityis 3, making the SDK queue a drop-oldest ring buffer. A stalled consumer can only fall three frames behind and then sees the newest frame, not a backlog — the structural answer to the stale-media bug that motivated this plugin.- Video and audio are both timestamped with
os_gettime_ns()at arrival. The SDK gives video a WebRTC capture timestamp and audio none; mixing two epochs inside one OBS source would guarantee A/V drift. This relies on the SDK's jitter buffering having already aligned them — the assumption the design doc flags for verification on real hardware. Still unverified. - WebRTC changes resolution mid-stream. Observed directly in the integration test: the first frames after (re)subscribing arrive at a downscaled spatial layer before ramping to the published size. The adapter passes each frame's own geometry through, and logs geometry changes.
Building
Linux (the platform that is fully verified). STPLUGIN_BOOTSTRAP_OBS=OFF
skips the macOS/Windows OBS-SDK bootstrap, which Linux does not need:
sudo apt-get install -y cmake ninja-build libobs-dev libcurl4-openssl-dev
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DSTPLUGIN_BOOTSTRAP_OBS=OFF
cmake --build build
ctest --test-dir build --output-on-failure
The configure step downloads the pinned client-sdk-cpp release (~13 MB) into
build/_deps/livekit-sdk. Point -DSTPLUGIN_LIVEKIT_SDK_DIR=<path> at a
persistent directory to cache it across builds; -DSTPLUGIN_LIVEKIT_SDK_VERSION
and -DSTPLUGIN_LIVEKIT_SDK_TRIPLE override the pin and the release triple.
The build stages a runnable layout into build/package/:
build/package/bin/64bit/streamer-tools-camera.so (RPATH=$ORIGIN)
build/package/bin/64bit/liblivekit.so
build/package/bin/64bit/liblivekit_ffi.so
build/package/data/locale/en-US.ini
build/package/licenses/...
That is exactly the layout OBS searches on Linux and Windows —
<base>/obs-studio/plugins/<name>/bin/64bit plus a sibling data/, per
AddExtraModulePaths() in obs-studio (UI/window-basic-main.cpp in 30.x,
frontend/widgets/OBSBasic.cpp in 32.x) — so build/package/ is a straight
drop-in. <base> is NOT the same directory on every platform, and getting
this wrong is silent: OBS logs nothing at all for a plugin it never finds.
Linux uses the user config dir (GetAppConfigPath → ~/.config), but Windows
uses GetProgramDataPath (CSIDL_COMMON_APPDATA) — i.e.
C:\ProgramData\obs-studio\plugins\, not %APPDATA%\obs-studio\
(CSIDL_APPDATA), which on Windows holds OBS's config and is never scanned for
plugins. This bit a director on 2026-09-09: a correctly-shaped install under
AppData\Roaming produced a log with zero mention of the module.
The module resolves the LiveKit libraries from $ORIGIN (verified: ldd on the staged copy resolves both
to bin/64bit/), not from the build tree. macOS is not this shape; see the
macOS packaging gap under CI.
Testing this by hand
The module has been loaded in the OBS GUI on Windows once (2026-09-09, OBS 32.2.2 / Windows 11 26200) — nothing beyond "it loads and registers its source" is confirmed there, and Linux and macOS have never been opened in the GUI at all. To do it on Linux:
mkdir -p ~/.config/obs-studio/plugins/streamer-tools-camera
cp -r build/package/bin build/package/data \
~/.config/obs-studio/plugins/streamer-tools-camera/
obs
(That yields .../streamer-tools-camera/bin/64bit/streamer-tools-camera.so
and .../streamer-tools-camera/data/locale/en-US.ini, which is what OBS
looks for.)
Then: Sources → + → "streamer-tools Camera" → fill in the server URL, room
slug and read key from the room's settings page → "Refresh camera list" → pick
a camera. Check ~/.config/obs-studio/logs/ for
[streamer-tools-camera] connected to … and [streamer-tools-camera] video frame WxH I420.
What to look for that automated testing could not answer:
- Does video actually appear, right way up, right colours?
- Is A/V sync acceptable? (see the timestamp caveat above)
- What is the end-to-end latency versus the existing egress path?
- Does a publisher restarting mid-show recover cleanly on screen?
Running the LiveKit integration test
core/tests/test_integration_livekit publishes a synthetic camera into a real
room and subscribes to it through the wrapper. It skips unless
STPLUGIN_IT_* is set:
livekit-server --dev --bind 127.0.0.1 &
eval "$(python3 scripts/livekit-dev-room.py)"
ctest --test-dir build -R test_integration_livekit --output-on-failure
What is verified, and how
Verified on Ubuntu 24.04 (libobs 30.0.2, client-sdk-cpp 1.10.1, livekit-server 1.13.6 in dev mode):
| Claim | How it was checked |
|---|---|
| The pinned LiveKit SDK links and is callable | test_livekit_smoke: initialize()/shutdown() round-trip, header version asserted equal to the CMake pin |
| The JSON reader handles real and hostile input | test_json, 158 checks, including truncated bodies, HTML error pages, binary garbage, lone surrogates, and a depth-limit case |
| The API client parses the real response shapes and every error branch | test_api_client, 127 checks, against a fake HTTP client and a real loopback HTTP server driving the actual platform backend |
| A dead/stalled/garbage server cannot hang or crash the plugin | loopback cases: truncated JSON, connection closed with no reply, non-HTTP bytes, dead port, stalled server cut off by the client timeout |
| Session state transitions, track selection, frame geometry | test_session, 81 checks, plus real connect() failures against the real SDK |
| Media actually flows | test_integration_livekit against a real LiveKit server: 36 video frames + 323 audio frames, correct I420 geometry and plane pointers, publisher unpublish → hasVideo() false with no further frames from the dead publisher, republish → video resumes |
| The module loads into real libobs and pushes frames | a headless libobs harness (obs_startup + obs_reset_audio/obs_reset_video + obs_open_module) driving the built module against a stand-in streamer-tools API in front of a real LiveKit server. Log: connected to ws://… watching cam-test then video frame 640x360 I420; the camera dropdown populated as Test Camera / Dark Camera (offline); status connected; clean destroy and unload |
| A wrong read key is reported, not silently swallowed | same harness with a bad key: status unknown room slug, or the read key is wrong or has been rotated, warning info type, retry with backoff, no crash |
| Changing the selected camera reconnects cleanly | same harness: switch to a dark slot and back. Each switch mints a fresh obs:<room>:<nonce> identity and reconnects; video returns; status stays connected; no crash, no stale frame |
| Two sources in one OBS process | same harness with a second source added: both connect with distinct nonce identities, both receive frames, both tear down cleanly |
Not verified anywhere:
- Anything past module load in the OBS GUI. Windows 2026-09-09 confirms the module loads and its source type appears; whether video actually renders (right way up, right colours), what the A/V sync and latency look like, and whether a publisher restarting mid-show recovers on screen are all still unanswered. Linux and macOS have not been opened in the GUI at all.
- macOS beyond "CI builds and links the real module and the core tests pass".
Its artifact is a bare
.sowith a relative libobs install name and will not load in OBS.app — see the macOS packaging gap under CI. - Windows beyond "the core library and the WinHTTP backend compile and their
tests pass", from runs predating the current fixes. The WinHTTP backend has
never run against a real streamer-tools server, only against the loopback
test server in
test_api_client. - A/V sync and end-to-end latency against the existing egress path.
- Behaviour against the real production streamer-tools server (only against a stand-in serving the same shapes).
- Token expiry after an hour. Expiry is handled reactively: a fatal
disconnect makes the worker mint a fresh token and reconnect. The design
doc's "proactively refreshed before expiry" is not implemented —
client-sdk-cpp1.10.1 exposes no way to hand a liveRooma new token.
CI
.gitea/workflows/build.yml runs on every push, matrixed across the three
runners available to this repo under the CyberCoveLLC org.
| Job | runs-on |
Runner | State |
|---|---|---|---|
linux |
ubuntu-24.04 |
localhost.localdomain |
Green. Builds the real adapter against Ubuntu's libobs-dev 30.0.2, runs all six test suites, uploads build/package as an artifact |
macos |
macos-latest |
home-mac (Global) |
Green. Builds libobs 30.0.2 from source, then the real adapter; 6/6 tests; artifact uploaded. But see the macOS packaging gap below |
windows |
windows-latest |
winvm-builder (org-scoped) |
Failing, fix pushed and awaiting a completed run. Every completed run so far has failed; the latest got as far as building libobs and stopped on an OBS-side OBS::w32-pthreads target that its own modern CMake path never defines. A bootstrap patch for that gap has been pushed but not yet confirmed by a green run; see below |
The Linux job is pinned to ubuntu-24.04 rather than ubuntu-latest: this
instance's two Linux runners answer ubuntu-latest with different releases,
and 22.04's libobs-dev is OBS 27 — a different API surface, and the LiveKit
SDK's own linux-x64 asset does not even link there (hence the
ubuntu-22.04 SDK triple; see cmake/LiveKitSDK.cmake).
macOS and Windows use the obsproject/obs-plugintemplate buildspec
bootstrap, trimmed to drop qt6 (this plugin's properties UI is plain
obs_properties_*), with obs-studio.version pinned to 30.0.2 — the same
version Linux builds against, and deliberately low, because OBS rejects a
module built against a newer libobs than the one running it.
Both jobs fall back to a core-library-only build if the bootstrap fails,
rather than going red, with a workflow ::warning:: and a "Show what was
built" step that reports no module. That fallback exists because the
bootstrap is the least verifiable part of this project — there is no way to
exercise a macOS or Windows OBS build from the Linux development machine —
and a permanently red CI teaches people to ignore CI. Do not remove the
warning: a green job that quietly stopped building the plugin is worse than
a red one.
Windows runner: persistent build tools (2026-09-07)
winvm-builder's Windows job used to install its own CMake + Ninja on every
single run via uses: lukka/get-cmake@latest. That action has its own
caching (routed through this act_runner's built-in cache server, the same
mechanism .deps/'s actions/cache step above relies on and that one does
work) but it never hit: every run logged Cloud cache miss against the same
cache key, even immediately after a run that logged a successful save under
that exact key -- some incompatibility between lukka/get-cmake's bundled
cache client and this act_runner's cache-server implementation, not
"caching isn't configured." Separately, and the larger cost: the archive
extraction step alone measured ~7.5 minutes for a 45MB zip on this VM
(13:11:14 to 13:18:48 in one captured run) -- consistent with Windows
Defender real-time-scanning every extracted file, not raw disk I/O, though
that specific cause is not confirmed. Together this was the dominant cost of
every Windows CI run, cold cache or not.
Fix: CMake 4.4.2 and Ninja 1.12.1 are now installed once, directly on the
winvm-builder VM (Proxmox VMID 110, host pve4/192.168.1.145), not fetched
per-run:
C:\BuildTools\cmake\(fromcmake-4.4.2-windows-x86_64.zip, Kitware's GitHub releases) andC:\BuildTools\ninja\(fromninja-win.zip,ninja-build/ninjav1.12.1 release) — plainExpand-Archivedrops, nothing installed via an installer/MSI.- Both added to the Machine-level
PATH([Environment]::SetEnvironmentVariable('PATH', ..., 'Machine'), notsetx, which silently truncates aPATHthis long). - The
GiteaRunner-winvm-builderscheduled task (C:\gitea-runner\ gitea-runner.exe daemon, runs as SYSTEM) was stopped and restarted after thePATHchange — a already-running process does not pick up an updated Machine environment variable, only processes started after the change do, and every CI job is a child process of this one long-running daemon.
Both workflows' Windows jobs now just run cmake --version / ninja --version as a "Verify build dependencies" step and fail loudly if either
is missing, instead of silently falling back to the slow per-run install.
This is VM state, not something git clone reproduces. If
winvm-builder is ever rebuilt or reimaged, redo the three steps above
(download+extract both zips under C:\BuildTools\, extend the Machine
PATH, restart the scheduled task) before expecting Windows CI to pass
again — there is nothing in this repo that does it automatically.
Where the macOS bootstrap actually got to
Six CI iterations, each fixing a real failure visible in the logs:
- Upstream's Xcode generator →
No CMAKE_C_COMPILER could be found(the runner has the Command Line Tools, not Xcode). Switched to Ninja. - OBS's SDK version regex only matches a full-Xcode SDK path. Synthesised a
MacOSX.platform/Developer/SDKs/MacOSX<ver>.sdksymlink to the same SDK. - The install walked into
UI/obs-frontend-api, whose binary is deliberately never built. The install's exit code is now tolerated. - Restricting the install to
libobs/fixed that but lost the per-configuration export file. xattr -r -d com.apple.quarantinefollowed the SDK symlink into the read-only system SDK. Symlink moved to the build directory; the xattr step is no longer fatal.IMPORTED_LOCATION or IMPORTED_IMPLIB not set for imported target OBS::libobs configuration Release— OBS 30.0.2 installslibobsTargets.cmakewithout the per-config file that carries the library path. The top-levelCMakeLists.txtnow detects a locationlessOBS::libobsand points it at the framework the bootstrap just built.
All six are confirmed fixed: the macOS job now downloads obs-deps and
obs-studio, builds libobs from source, builds and links the real adapter,
passes 6/6 tests, and uploads its artifact. otool -L on the result shows it
linked against libobs and @rpath/liblivekit.dylib.
macOS packaging gap (known, unfixed)
The macOS artifact will not load in OBS.app as it stands. Two reasons, neither of which CI can catch, because CI only proves it compiles and links:
- It is a bare
streamer-tools-camera.so. OBS on macOS loads plugins as<name>.pluginbundles (Contents/MacOS/<name>,Contents/Resources/, anInfo.plist), which is what obs-plugintemplate'scmake/macos/helpers.cmakebuilds and which this project deliberately did not vendor. otool -Lshows the libobs dependency recorded as the relative pathlibobs/libobs.framework/Versions/A/libobs, inherited from the from-source libobs's own install name. A real plugin needs@rpath/libobs.framework/Versions/A/libobsplus anLC_RPATHpointing atOBS.app/Contents/Frameworks.
Fixing this means either vendoring the template's macOS bundle helpers or
adding an install_name_tool pass and a bundle layout — bounded work, but
work that has to be done and checked on an actual Mac. It is deliberately not
attempted here rather than guessed at.
Where the Windows bootstrap got to
Windows is by far the slowest job — the lukka/get-cmake step alone takes
7-15 minutes on winvm-builder, and the runner serialises jobs, so a burst of
pushes leaves a queue that takes an hour to drain.
The honest record, as it stood before f27b1c0: every completed Windows CI
run on this branch had failed. (f27b1c0 is the one that finally went
green — see "After the w32-pthreads target" at the end of this section. The
history below is kept because each dead end in it is a real constraint
someone will otherwise rediscover.) The first 7 failures were all at commits
predating the -A x64 fix
below. The 8th, at edb0c02 — the first commit that actually carries that fix
— has since completed, and it is the informative one: the -A x64 fix
worked, and Windows failed further along, on something else. A later run
(58f4832) then failed for an unrelated reason — its Windows steps still used
shell: bash, which on this runner is WSL and cannot run as local system —
so it never reached cmake at all and tells us nothing about the bootstrap.
The PowerShell rewrite that fixes that is still queued and remains unproven.
Do not read any fix below as "confirmed" beyond what is stated; check current
CI status rather than trusting this paragraph's age.
What edb0c02 showed: obs-deps and Qt6 downloaded, CEF skipped, the OBS
sub-configure ran for 53s and correctly reported "Plugin Support" and "User
Interface" disabled — then failed at generate time:
CMake Error at libobs/cmake/os-windows.cmake:46 (target_link_libraries):
Target "libobs" links to:
OBS::w32-pthreads
but the target was not found.
That looks like a genuine hole in OBS 30.0.2's opt-in modern CMake path on Windows, not something this repo is doing wrong. Traced at the 30.0.2 tag:
libobs/cmake/os-windows.cmakelinksOBS::w32-pthreads;- that target is defined only by
deps/w32-pthreads/CMakeLists.txt, reached only throughdeps/CMakeLists.txt; deps/is added only by the legacy branch of the top-levelCMakeLists.txt. The modern branch — the one-DOBS_CMAKE_VERSION=3.0.0selects — addslibobs,libobs-d3d11,libobs-winrt,libobs-opengl,plugins,test/test-inputandUI, neverdeps;libobs/CMakeLists.txtadds onlydeps/libcaptionanddeps/uthash;plugins/CMakeLists.txtreturns immediately underENABLE_PLUGINS=OFF, and does not adddeps/even when enabled.
macOS is unaffected because its libobs does not link w32-pthreads. At 30.0.2
the modern path was the default only on macOS
(if(CMAKE_HOST_SYSTEM_NAME MATCHES "(Darwin)" OR OBS_CMAKE_VERSION ...)),
which is consistent with the Windows side of it being under-exercised
upstream.
Deliberately not "fixed" by bumping the pin: 30.0.2, 30.1.2, 30.2.3,
31.0.3 and 31.1.1 were all checked, and every one still links
OBS::w32-pthreads from libobs/cmake/os-windows.cmake while none of them
add deps/w32-pthreads from libobs/CMakeLists.txt. A version bump is
therefore not obviously the answer and needs checking rather than assuming.
Option 1 (build obs-frontend-api, matching obs-plugintemplate's own CI, and
accept the Qt dependency on Windows only) was investigated and rejected:
UI/obs-frontend-api/CMakeLists.txt only links OBS::libobs, nothing else —
it does not itself pull in deps/w32-pthreads. What actually satisfies the
target upstream is that UI/CMakeLists.txt returns early when
ENABLE_UI=OFF, before reaching include(cmake/os-windows.cmake) — the
file that (via its own if(NOT TARGET OBS::w32-pthreads) guard) adds
deps/w32-pthreads. Upstream's CI never sets ENABLE_UI=OFF, so that
add-as-a-side-effect-of-Qt always happens for them. Building
obs-frontend-api instead of libobs would not change any of that; the only
way to get the same side effect is to stop passing -DENABLE_UI:BOOL=OFF,
which is exactly the ~100 MB Qt6 download this bootstrap was trimmed to avoid
(see the Windows buildspec.cmake comment) and buys this plugin nothing,
since its properties UI is plain obs_properties_*.
Went with Option 2 instead: cmake/common/buildspec_common.cmake now carries
_patch_obs_studio_w32_pthreads(), called for OS_WINDOWS right before
_setup_obs_studio(). It patches the freshly-extracted
libobs/CMakeLists.txt to add the one missing subdirectory itself, using the
exact same if(NOT TARGET OBS::w32-pthreads) guard
UI/cmake/os-windows.cmake already relies on upstream:
if(OS_WINDOWS)
if(NOT TARGET OBS::w32-pthreads)
add_subdirectory("${CMAKE_SOURCE_DIR}/deps/w32-pthreads" "${CMAKE_BINARY_DIR}/deps/w32-pthreads")
endif()
include(cmake/os-windows.cmake)
...
It is idempotent (checks for deps/w32-pthreads already present in the file
before patching, so re-running against a previously-patched extraction is a
no-op) and fails loudly with FATAL_ERROR if the anchor text it expects to
find is not there, rather than silently doing nothing on a future OBS version
whose libobs/CMakeLists.txt has changed shape. Verified locally (this is a
Linux sandbox, so only the CMake string-patching logic itself could be
checked, not a real Windows configure/build): ran the same string(FIND)
/string(REPLACE) sequence against the real libobs/CMakeLists.txt fetched
from the obs-studio 30.0.2 tag, confirmed it produces the intended
if(OS_WINDOWS) / if(NOT TARGET ...) / add_subdirectory(...) / endif() / include(...) block, and confirmed a second run against the already-patched
file is a no-op. Whether this actually gets libobs through CMake generate and
building on a real Windows runner is the thing the next CI run needs to
prove — option 3 (dropping the from-source libobs on Windows for a prebuilt
SDK) remains the fallback if it does not.
Two bugs of its own were found; the first is now proven fixed by edb0c02
getting past it, the second is still unproven:
-
Upstream passes
-A x64,version=<Windows SDK>to the OBS sub-configure, and with a current CMake that,version=suffix reappears verbatim in the sub-build'sCMAKE_VS_PLATFORM_NAME— which obs-studio's own dependency downloader uses as the architecture, sending it afterwindows-deps-2023-11-03-x64,version=10.0.26100.0.zip:string sub-command JSON member 'hashes windows-x64,version=10.0.26100.0' not found Unable to download .../windows-deps-2023-11-03-x64,version=10.0.26100.0.zipPlain
-A x64now. Confirmed fixed: theedb0c02run got past this and downloadedwindows-deps-2023-11-03-x64.zipcorrectly. -
The Windows CI steps were originally written in bash (via
shell: bash) and were rewritten in PowerShell. The bash version is now proven broken by the58f4832run: onwinvm-builder,bashresolves to WSL, and WSL refuses to run under the service account the runner uses —Running WSL as local system is not supported. Error code: Bash/WSL_E_LOCAL_SYSTEM_NOT_SUPPORTED ##[error]Process completed with exit code 1.so the step died on the shell without ever invoking cmake. The PowerShell rewrite is therefore necessary, but still unproven in the other direction — no completed run has yet included it. Note this also means
58f4832's failure says nothing about thew32-pthreadsblocker; that result comes fromedb0c02alone.
(Superseded by the next subsection: the w32-pthreads blocker described above
was the next thing solved, and the add_subdirectory patch and the
PowerShell rewrite are both now proven by a completed green run. What stands
from this subsection is its reasoning — why ENABLE_UI=OFF exposes the gap,
and why re-enabling Qt or bumping the OBS pin is not the answer.)
After the w32-pthreads target: the w32-pthreads package
The add_subdirectory(deps/w32-pthreads) patch above did its job — the
edb0c02-era generate error is gone, obs-studio 30.0.2 configures, builds
w32-pthreads.dll and obs.dll, and installs. Two further Windows-only
blockers then surfaced behind it, both the same underlying upstream mismatch
and neither one a defect in this repo:
-
find_package(libobs)could not locate the package it had just installed. obs-studio'scmake/windows/defaults.cmakesetsOBS_CMAKE_DESTINATION=cmake, andtarget_export()installs each package to<prefix>/${OBS_CMAKE_DESTINATION}/<target>/— i.e..deps/cmake/libobs/. That is not one of CMake's Config-mode search suffixes (<prefix>/cmake/is, but only for a config file sitting directly in it;<prefix>/<name>*/cmake/is, but the<name>directory has to come first). Fixed in79de5e8fby settinglibobs_DIRexplicitly; the long comment above that block inCMakeLists.txthas the full reasoning. -
…and then
libobsConfig.cmakecould not locatew32-pthreadsfor exactly the same reason.libobs/cmake/os-windows.cmakelinksPUBLIC OBS::w32-pthreads, so upstream'slibobsConfig.cmake.incarries a hardfind_dependency(w32-pthreads REQUIRED)underif(MSVC). Once fix 1 finally got that config file loaded, the dependency lookup inside it failed and killed the configure:By not providing "Findw32-pthreads.cmake" in CMAKE_MODULE_PATH this project has asked CMake to find a package configuration file provided by "w32-pthreads", but CMake did not find one. .deps/cmake/libobs/libobsConfig.cmake:30 (find_dependency)This is not a missing export, which was the first hypothesis and is worth recording as wrong:
deps/w32-pthreads/CMakeLists.txtends withtarget_export(w32-pthreads), the same helperlibobsitself uses, so it does emitinstall(TARGETS … EXPORT w32-pthreadsTargets),install(EXPORT … NAMESPACE OBS::)and a generatedw32-pthreadsConfig.cmake, allCOMPONENT Development. Two independent arguments say those rules ran: CMake hard-errors at generate time if an exported target links a target that is in no export set at all (and OBS's generate step succeeded), and this repo's patch addsdeps/w32-pthreadsfromlibobs/CMakeLists.txt, making it part of thelibobssubtree — which installs before the toleratedUI/obs-frontend-apiinstall error aborts the rest. The package really is at.deps/cmake/w32-pthreads/;find_packagewas simply never going to look there.So the fix is the same one-liner as for
libobs, and it is literally what CMake's own error message suggests: setw32-pthreads_DIRbefore thefind_package(libobs)call that transitively triggers thefind_dependency. Both_DIRblocks now sit next to each other inCMakeLists.txt.Behind that sits
cmake/windows/find-fallback/Findw32-pthreads.cmake, used only if that export is genuinely absent from.deps/. It rebuildsOBS::w32-pthreadsby hand from the bootstrap's own artifacts. It deliberately does not live incmake/windows/, whichcmake/common/osconfig.cmakealready puts onCMAKE_MODULE_PATHfor every Windows configure — a find module there would shadow OBS's real exported package on every build, sincefind_packagetries MODULE mode before CONFIG mode.CMakeLists.txtappends thefind-fallback/directory toCMAKE_MODULE_PATHonly after it has established the real export is missing, and logs what is under.deps/cmake/when it does, so a future failure of this shape is answered by the CI log rather than by another run.Note this package is load-bearing for more than the dependency check:
libobs/util/threading.hdoes#include <pthread.h>, and on Windows that header only exists becausetarget_export(w32-pthreads)installspthread.h/sched.hasPUBLIC_HEADERinto.deps/include/.
Verified before pushing, on Linux, since this is a Linux sandbox: a
reconstruction of the exact failure — a stub libobsConfig.cmake containing
find_dependency(w32-pthreads REQUIRED), reached through libobs_DIR, with
the package installed at .deps/cmake/w32-pthreads/ — reproduces the CI
error without the w32-pthreads_DIR block and passes with it; and the
fallback find module was exercised separately by deleting that package, with
find_package resolving through MODULE mode to it instead. A full Linux
configure of this repo is unchanged (both blocks are inside if(OS_WINDOWS),
and the upstream find_dependency is inside if(MSVC), so macOS and Linux
are pure no-ops).
Confirmed on the real runner. The Windows job for f27b1c0 completed
green, and its log body — not just its status — shows the whole chain:
-- w32-pthreads_DIR not set; libobsConfig.cmake's find_dependency(w32-pthreads
REQUIRED) hits the same OBS_CMAKE_DESTINATION=cmake search-suffix problem as
libobs itself, so pointing it directly at the from-source install:
...\.deps\cmake\w32-pthreads
-- libobs found (...\.deps\cmake\libobs) -- building OBS adapter module
streamer-tools-camera.vcxproj -> ...\build\obs-adapter\Release\streamer-tools-camera.dll
100% tests passed out of 6
Directory: ...\build\package\bin\64bit
-a---- 3078656 livekit.dll
-a---- 25008640 livekit_ffi.dll
-a---- 136192 streamer-tools-camera.dll
Note which branch that log took: the w32-pthreads_DIR message means the
package config really was sitting at .deps/cmake/w32-pthreads/ all along and
the fallback find module was never loaded. The export was never missing — only
unfindable. Linux and macOS were green in the same run, confirming the no-op.
The one piece of noise left in that log is the tolerated
UI/obs-frontend-api/cmake_install.cmake error, which now repeats once per
CMake re-configure because Visual Studio's ZERO_CHECK target re-runs the
bootstrap during the build. It is cosmetic and pre-dates this change, but it
makes the Windows log harder to read than it should be.