Build / macOS (macos-latest) (push) Successful in 32s
Build / Linux (ubuntu-24.04) (push) Successful in 56s
Build / Windows (windows-latest) (push) Successful in 2m52s
Release / macOS (macos-latest) (push) Successful in 43s
Release / Linux (ubuntu-24.04) (push) Successful in 56s
Release / Windows (windows-latest) (push) Successful in 3m23s
Release / Create Gitea Release (draft) (push) Successful in 19s
lukka/get-cmake@latest re-fetched and re-extracted CMake + Ninja on every single Windows CI run. Its own cache (routed through this act_runner's built-in cache server) reported "Cloud cache miss" on every run, even one immediately after a run that logged a successful save under the exact same key -- an incompatibility between its bundled cache client and this act_runner's cache-server implementation, not a config gap. Separately and more importantly: the archive extraction step alone measured ~7.5 minutes for a 45MB zip on this VM, consistent with Defender real-time scanning rather than raw disk I/O. Together this was the dominant cost of every Windows CI run. CMake 4.4.2 and Ninja 1.12.1 are now installed once, directly on the winvm-builder VM's system PATH (C:\BuildTools\cmake, C:\BuildTools\ ninja), sidestepping the third-party action's cache entirely rather than debugging its internals further. Both Windows jobs now just verify cmake/ninja are present and fail loudly if not, instead of silently falling back to a slow reinstall. Full detail, including how to redo this if the VM is ever rebuilt, is in README's new "Windows runner: persistent build tools" section -- this is VM state, not something git reproduces. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
630 lines
35 KiB
Markdown
630 lines
35 KiB
Markdown
# 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
|
|
|
|
**Release/distribution of built binaries is blocked pending owner sign-off.**
|
|
This plugin statically/dynamically pulls in Google WebRTC and OpenH264 code
|
|
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 —
|
|
including a 2026-09-07 finding, sourced from Cisco's own OpenH264 FAQ, that
|
|
the way LiveKit's WebRTC build normally links OpenH264 (statically, from
|
|
source, not Cisco's runtime-downloaded binary) is specifically the case Cisco
|
|
says voids their MPEG-LA royalty coverage and shifts it to the redistributor.
|
|
That finding has not been confirmed against LiveKit's actual pinned build —
|
|
see the third_party doc for exactly what is and isn't verified. (CI in
|
|
`.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.)
|
|
|
|
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.
|
|
|
|
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`).
|
|
|
|
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
|
|
under CI).
|
|
|
|
**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 is still **unverified in the OBS GUI**,
|
|
exactly like the other two platforms. 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 + creates a draft Gitea Release (only on a `v*` tag push; see Status above)
|
|
```
|
|
|
|
## 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
|
|
|
|
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 —
|
|
`<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.
|
|
|
|
## Testing this by hand
|
|
|
|
**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
|
|
```
|
|
|
|
(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:**
|
|
- The OBS GUI, on any platform. No human has looked at this in OBS.
|
|
- 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`.
|
|
- 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.
|
|
|
|
## 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\` (from
|
|
`cmake-4.4.2-windows-x86_64.zip`, Kitware's GitHub releases) and
|
|
`C:\BuildTools\ninja\` (from `ninja-win.zip`, `ninja-build/ninja` v1.12.1
|
|
release) — plain `Expand-Archive` drops, nothing installed via an
|
|
installer/MSI.
|
|
- Both added to the **Machine**-level `PATH`
|
|
(`[Environment]::SetEnvironmentVariable('PATH', ..., 'Machine')`, not
|
|
`setx`, which silently truncates a `PATH` this long).
|
|
- The `GiteaRunner-winvm-builder` scheduled task (`C:\gitea-runner\
|
|
gitea-runner.exe daemon`, runs as SYSTEM) was stopped and restarted after
|
|
the `PATH` change — 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:
|
|
|
|
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
|
|
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.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.
|
|
|
|
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.
|
|
|
|
Two bugs of its own were found; the first is now proven fixed by `edb0c02`
|
|
getting past it, the second is still unproven:
|
|
|
|
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
|
|
```
|
|
|
|
Plain `-A x64` now. **Confirmed fixed:** the `edb0c02` run got past this
|
|
and downloaded `windows-deps-2023-11-03-x64.zip` correctly.
|
|
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.
|
|
|
|
(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:
|
|
|
|
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).
|
|
|
|
**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.
|