2026-09-06 20:46:08 -07:00
# obs-streamer-tools-plugin
2026-09-06 21:54:11 -07:00
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
2026-09-06 23:00:06 -07:00
**Release/distribution of built binaries is blocked pending owner sign-off.**
This plugin statically/dynamically pulls in Google WebRTC and OpenH264 code
2026-09-07 04:44:16 -07:00
through the LiveKit SDK — a real patent/royalty question (OpenH264/WebRTC)
that only the project owner can decide. Nothing in this repo should be built
into a package and handed out, posted, or attached to a public release until
that sign-off happens. See `third_party/livekit/README.md` for the specifics
of what is and is not currently known/shipped on the licensing side. (CI in
2026-09-07 05:06:25 -07:00
`.gitea/workflows/build.yml` only builds, tests, and uploads CI-internal
build artifacts — it does not create a Gitea Release or otherwise publish
anything publicly.
`.gitea/workflows/release.yml` is the mechanism that *would* publish a
release, but it does not run automatically: it is gated on someone pushing a
`v*` tag, which is the actual sign-off gate in practice — don't push one
until the owner has actually signed off on C1. When it does run, it packages
each platform's `build/package/` (or macOS's bundle output, once that lands)
into a zip and creates a **draft** Gitea Release, whose generated release
notes lead with the same C1 reminder as this section, so whoever opens the
draft to publish it sees the open question again before doing so. Building
that mechanism is not the same as clearing C1 — it still requires the same
owner sign-off before a tag gets pushed.)
2026-09-07 04:44:16 -07:00
The separate license-compatibility question — this repository's own top-level
`LICENSE` was GPLv2 while the vendored LiveKit binaries are Apache-2.0, which
are incompatible — is **resolved** : the project owner has relicensed this
project's own first-party code to Apache-2.0, matching LiveKit. Everything in
this repo is now Apache-2.0, so there is no remaining GPL/Apache
incompatibility.
2026-09-06 23:00:06 -07:00
2026-09-06 21:54:11 -07:00
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
2026-09-06 22:28:52 -07:00
`obs_source_output_video` /`_audio` ).
It has **not been run in the OBS GUI on any platform.** macOS builds the real
module in CI but its artifact is not yet loadable (see the macOS packaging gap
2026-09-06 23:07:04 -07:00
under CI). Windows CI has **failed on every completed run so far** . The
`-A x64` argument fix is now confirmed working — the run carrying it got as
far as building libobs — but it exposed a deeper blocker: OBS 30.0.2's
opt-in modern CMake path never defines the `OBS::w32-pthreads` target its own
2026-09-06 23:12:41 -07:00
Windows libobs links against. A targeted bootstrap patch for that gap has been
pushed but not yet confirmed by a completed green run. The PowerShell rewrite
of the Windows steps is also still unproven. See "Where the Windows bootstrap
got to" under CI below for the trace and the options, and check current CI
status rather than trusting this paragraph's age.
2026-09-06 22:28:52 -07:00
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.
2026-09-06 20:46:08 -07:00
## Layout
```
2026-09-06 21:54:11 -07:00
cmake/LiveKitSDK.cmake - downloads + unpacks the pinned client-sdk-cpp release
core/ - core library (C++17, no OBS dependency, headless-testable)
2026-09-06 20:46:08 -07:00
include/stplugin/
2026-09-06 21:54:11 -07:00
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
2026-09-06 20:46:08 -07:00
data/locale/en-US.ini
2026-09-06 21:54:11 -07:00
scripts/livekit-dev-room.py - mints tokens for the integration test
third_party/livekit/ - redistribution notices for the LiveKit binaries
2026-09-07 05:06:25 -07:00
.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 + creates a draft Gitea Release (only on a `v*` tag push; see Status above)
2026-09-06 21:54:11 -07:00
```
## How it works
1. The operator fills in the streamer-tools server URL, room slug and read key,
and picks a camera from the dropdown.
2. The source's own worker thread calls `POST /api/obs/:slug/token?key=…` to
mint a hidden, subscribe-only LiveKit token
(identity `obs:<slug>:<nonce>` — a fresh nonce per mint, so two OBS
installs watching the same room can never kick each other).
3. `LiveKitSession` connects `livekit::Room` to the returned `wsUrl` , waits for
the chosen participant's `Source.Camera` video track (and their microphone),
and reads decoded frames off `VideoStream` /`AudioStream` .
4. 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::fromTrack` with our own reader threads, not
from `Room::setOnVideoFrameCallback` .** The dispatcher API is keyed by
(participant identity, track *name* ), which is only knowable once the track
is published — and disassembly of `liblivekit.so` 1.10.1 confirms that
neither `Room::setOnVideoFrameCallback` nor 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::capacity` is 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
2026-09-06 22:28:52 -07:00
Linux (the platform that is fully verified). `STPLUGIN_BOOTSTRAP_OBS=OFF`
skips the macOS/Windows OBS-SDK bootstrap, which Linux does not need:
2026-09-06 21:54:11 -07:00
```
sudo apt-get install -y cmake ninja-build libobs-dev libcurl4-openssl-dev
2026-09-06 22:28:52 -07:00
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DSTPLUGIN_BOOTSTRAP_OBS=OFF
2026-09-06 21:54:11 -07:00
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/` :
```
2026-09-06 22:36:01 -07:00
build/package/bin/64bit/streamer-tools-camera.so (RPATH=$ORIGIN)
build/package/bin/64bit/liblivekit.so
build/package/bin/64bit/liblivekit_ffi.so
2026-09-06 21:54:11 -07:00
build/package/data/locale/en-US.ini
build/package/licenses/...
```
2026-09-06 22:36:01 -07:00
That is exactly the layout OBS searches on Linux and Windows —
`<config>/obs-studio/plugins/<name>/bin/64bit` plus a sibling `data/` , per
`AddExtraModulePaths()` in obs-studio's `UI/window-basic-main.cpp` — so
`build/package/` is a straight drop-in. 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.
2026-09-06 21:54:11 -07:00
## Testing this by hand
2026-09-06 20:46:08 -07:00
2026-09-06 21:54:11 -07:00
**Nobody has yet run this in the OBS GUI. That test is still outstanding on
all three platforms.** 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
2026-09-06 20:46:08 -07:00
```
2026-09-06 22:36:01 -07:00
(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.)
2026-09-06 21:54:11 -07:00
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:
2026-09-06 20:46:08 -07:00
```
2026-09-06 21:54:11 -07:00
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
2026-09-06 20:46:08 -07:00
```
2026-09-06 21:54:11 -07:00
## 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 |
2026-09-06 23:00:06 -07:00
| 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 |
2026-09-06 21:54:11 -07:00
| 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 |
2026-09-06 22:28:52 -07:00
| 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 |
2026-09-06 21:54:11 -07:00
**Not verified anywhere:**
- The OBS GUI, on any platform. No human has looked at this in OBS.
2026-09-06 22:28:52 -07:00
- macOS beyond "CI builds and links the real module and the core tests pass".
Its artifact is a bare `.so` with 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` .
2026-09-06 21:54:11 -07:00
- 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-cpp` 1.10.1 exposes no way to hand a live `Room` a new token.
2026-09-06 20:46:08 -07:00
## CI
2026-09-06 21:54:11 -07:00
`.gitea/workflows/build.yml` runs on every push, matrixed across the three
runners available to this repo under the `CyberCoveLLC` org.
2026-09-06 20:46:08 -07:00
2026-09-06 22:28:52 -07:00
| 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 |
2026-09-06 23:12:41 -07:00
| `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 |
2026-09-06 22:28:52 -07:00
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.
### Where the macOS bootstrap actually got to
Six CI iterations, each fixing a real failure visible in the logs:
1. Upstream's Xcode generator → `No CMAKE_C_COMPILER could be found` (the
runner has the Command Line Tools, not Xcode). Switched to Ninja.
2. OBS's SDK version regex only matches a full-Xcode SDK path. Synthesised a
`MacOSX.platform/Developer/SDKs/MacOSX<ver>.sdk` symlink to the same SDK.
3. The install walked into `UI/obs-frontend-api` , whose binary is
deliberately never built. The install's exit code is now tolerated.
4. Restricting the install to `libobs/` fixed that but lost the
per-configuration export file.
5. `xattr -r -d com.apple.quarantine` followed the SDK symlink into the
read-only system SDK. Symlink moved to the build directory; the xattr step
is no longer fatal.
6. `IMPORTED_LOCATION or IMPORTED_IMPLIB not set for imported target
OBS::libobs configuration Release` — OBS 30.0.2 installs
` libobsTargets.cmake` without the per-config file that carries the library
path. The top-level ` CMakeLists.txt` now detects a locationless
` OBS::libobs` and 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:
1. It is a bare ` streamer-tools-camera.so`. OBS on macOS loads plugins as
` <name>.plugin` bundles (` Contents/MacOS/<name>`, ` Contents/Resources/`,
an ` Info.plist`), which is what obs-plugintemplate's
` cmake/macos/helpers.cmake` builds and which this project deliberately did
not vendor.
2. ` otool -L` shows the libobs dependency recorded as the relative path
` libobs/libobs.framework/Versions/A/libobs`, inherited from the
from-source libobs's own install name. A real plugin needs
` @rpath/libobs .framework/Versions/A/libobs` plus an ` LC_RPATH` pointing at
` OBS.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
2026-09-06 23:00:06 -07:00
pushes leaves a queue that takes an hour to drain.
**The honest record: every completed Windows CI run on this branch has
2026-09-06 23:07:04 -07:00
failed.** 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
2026-09-06 23:11:46 -07:00
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.
2026-09-06 23:07:04 -07:00
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.cmake` links ` OBS::w32-pthreads`;
- that target is defined only by ` deps/w32-pthreads/CMakeLists.txt`, reached
only through ` deps/CMakeLists.txt`;
- ` deps/` is added only by the **legacy** branch of the top-level
` CMakeLists.txt`. The modern branch — the one ` -DOBS_CMAKE_VERSION=3.0.0`
selects — adds ` libobs`, ` libobs-d3d11`, ` libobs-winrt`, ` libobs-opengl`,
` plugins`, ` test/test-input` and ` UI`, never ` deps`;
- ` libobs/CMakeLists.txt` adds only ` deps/libcaption` and ` deps/uthash`;
- ` plugins/CMakeLists.txt` returns immediately under ` ENABLE_PLUGINS=OFF`, and
does not add ` deps/` 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.
2026-09-06 23:12:41 -07:00
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:
` ``cmake
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.
2026-09-06 23:07:04 -07:00
Two bugs of its own were found; the first is now proven fixed by ` edb0c02`
getting past it, the second is still unproven:
2026-09-06 23:00:06 -07:00
1. 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's ` CMAKE_VS_PLATFORM_NAME` — which obs-studio's own dependency
downloader uses as the architecture, sending it after
` windows-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.zip
` ``
2026-09-06 23:07:04 -07:00
Plain ` -A x64` now. **Confirmed fixed:** the ` edb0c02` run got past this
and downloaded ` windows-deps-2023-11-03-x64.zip` correctly.
2026-09-06 23:11:46 -07:00
2. 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 the ` 58f4832` run: on ` winvm-builder`, ` bash` resolves 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 the ` w32-pthreads` blocker; that
result comes from ` edb0c02` alone.
2026-09-06 23:07:04 -07:00
Until a Windows run completes green, Windows should be treated as unverified
beyond "the core library and the WinHTTP backend compile and their tests
pass", which earlier (failing-job) runs did show before failing later in the
job. The ` w32-pthreads` blocker above is the next thing to solve, and it is
upstream's problem to work around rather than a defect in this repo's
bootstrap.
2026-09-07 05:40:38 -07:00
### 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:
1. **` find_package(libobs)` could not locate the package it had just
installed.** obs-studio's ` cmake/windows/defaults.cmake` sets
` OBS_CMAKE_DESTINATION=cmake`, and ` target_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 in ` 79de5e8f` by setting
` libobs_DIR` explicitly; the long comment above that block in
` CMakeLists.txt` has the full reasoning.
2. **…and then ` libobsConfig.cmake` could not locate ` w32-pthreads` for
exactly the same reason.** ` libobs/cmake/os-windows.cmake` links
` PUBLIC OBS::w32-pthreads`, so upstream's ` libobsConfig.cmake.in` carries
a hard ` find_dependency(w32-pthreads REQUIRED)` under ` if(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.txt` ends with
` target_export(w32-pthreads)`, the same helper ` libobs` itself uses, so
it does emit ` install(TARGETS … EXPORT w32-pthreadsTargets)`,
` install(EXPORT … NAMESPACE OBS::)` and a generated
` w32-pthreadsConfig.cmake`, all ` COMPONENT 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 adds ` deps/w32-pthreads`
from ` libobs/CMakeLists.txt`, making it part of the ` libobs` subtree —
which installs *before* the tolerated ` UI/obs-frontend-api` install error
aborts the rest. The package really is at ` .deps/cmake/w32-pthreads/`;
` find_package` was 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: set ` w32-pthreads_DIR` before
the ` find_package(libobs)` call that transitively triggers the
` find_dependency`. Both ` _DIR` blocks now sit next to each other in
` CMakeLists.txt`.
Behind that sits ` cmake/windows/find-fallback/Findw32-pthreads.cmake`,
used only if that export is genuinely absent from ` .deps/`. It rebuilds
` OBS::w32-pthreads` by hand from the bootstrap's own artifacts. It
deliberately does *not* live in ` cmake/windows/`, which
` cmake/common/osconfig.cmake` already puts on ` CMAKE_MODULE_PATH` for
every Windows configure — a find module there would shadow OBS's real
exported package on every build, since ` find_package` tries MODULE mode
before CONFIG mode. ` CMakeLists.txt` appends the ` find-fallback/`
directory to ` CMAKE_MODULE_PATH` only 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.h` does ` #include <pthread.h>`, and on Windows
that header only exists because ` target_export(w32-pthreads)` installs
` pthread.h`/` sched.h` as ` PUBLIC_HEADER` into ` .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). Whether the real Windows runner now gets to a
` streamer-tools-camera.dll` is what the CI run on this commit has to show.