shadowdaoandClaude Sonnet 5 f27b1c0b45
Build / macOS (macos-latest) (push) Successful in 28s
Build / Linux (ubuntu-24.04) (push) Successful in 46s
Build / Windows (windows-latest) (push) Successful in 13m44s
Fix Windows configure: point find_package at OBS's exported w32-pthreads
The libobs_DIR fix in 79de5e8f worked -- libobsConfig.cmake now loads on the
Windows runner -- and immediately exposed the next link in the same chain.
obs-studio's libobsConfig.cmake.in carries, under if(MSVC), a hard

    find_dependency(w32-pthreads REQUIRED)

because libobs/cmake/os-windows.cmake links PUBLIC OBS::w32-pthreads. That
lookup failed and aborted the whole configure.

The cause is not a missing export, which was the first hypothesis:
deps/w32-pthreads/CMakeLists.txt ends in target_export(w32-pthreads), the
same helper libobs itself uses, so the install(TARGETS ... EXPORT),
install(EXPORT ... NAMESPACE OBS::) and generated w32-pthreadsConfig.cmake
all exist and all run -- CMake would have hard-errored at generate time if
w32-pthreads were in no export set, and our own patch adds it under the
libobs subtree, which installs before the tolerated obs-frontend-api install
error stops the rest.

It is the same search-path mismatch libobs itself had: target_export installs
to <prefix>/${OBS_CMAKE_DESTINATION}/<target>/, which on Windows is
<prefix>/cmake/w32-pthreads/ -- a shape find_package's Config-mode suffixes
never search. So apply the same remedy CMake's own error message suggests,
and apply it before the find_package(libobs) call that transitively triggers
the find_dependency.

Also adds a fallback Findw32-pthreads.cmake, reached only when that export is
genuinely absent, which reconstructs OBS::w32-pthreads from the bootstrap's
artifacts, and a repair for a locationless imported target mirroring the
existing OBS::libobs one. The fallback deliberately lives in
cmake/windows/find-fallback/ rather than cmake/windows/, which osconfig.cmake
already puts on CMAKE_MODULE_PATH: a find module there would shadow OBS's
real exported package on every build, since MODULE mode is tried first.

Verified on Linux (this is a Linux sandbox) by reconstructing the exact
failure -- a stub libobsConfig.cmake with the same find_dependency, reached
through libobs_DIR -- and confirming it fails without this block and passes
with it, and separately that deleting the package routes find_package through
the fallback module instead. A full Linux configure of the repo is unchanged;
both new blocks are inside if(OS_WINDOWS) and the upstream find_dependency is
inside if(MSVC), so macOS and Linux are no-ops.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-07 05:40:38 -07:00

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. (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 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 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.

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.

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: every completed Windows CI run on this branch has 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 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:

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.

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.

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.

S
Description
Native OBS Studio source plugin pulling streamer-tools camera feeds directly from LiveKit over WebRTC (via livekit-ffi). Core library + thin OBS adapter, cross-platform.
Readme Apache-2.0
984 KiB
v0.1.0
Latest
2026-09-07 18:18:35 +00:00
Languages
C++ 66.2%
CMake 26.4%
Shell 5.2%
Python 1%
PowerShell 0.6%
Other 0.6%