Commit Graph
21 Commits
Author SHA1 Message Date
shadowdaoandClaude Sonnet 5 2bbd94775a ci: patch obs-studio's Windows CMake to define OBS::w32-pthreads
Build / macOS (macos-latest) (push) Successful in 32s
Build / Linux (ubuntu-24.04) (push) Successful in 1m2s
Build / Windows (windows-latest) (push) Failing after 11m20s
libobs/cmake/os-windows.cmake unconditionally links OBS::w32-pthreads, but
in the OBS_CMAKE_VERSION>=3.0.0 ("modern") top-level CMakeLists.txt branch
this bootstrap selects, nothing ever adds deps/w32-pthreads (confirmed at
every checked tag, 30.0.2 through 31.1.1) -- only the legacy branch's
add_subdirectory(deps) does, and libobs/CMakeLists.txt itself only adds
deps/libcaption and deps/uthash.

Upstream's own CI never hits this because it leaves ENABLE_UI on, and
UI/cmake/os-windows.cmake happens to add deps/w32-pthreads as a side effect
of building the Qt UI. Building obs-frontend-api instead of libobs (as
upstream's CI does) does not help: UI/obs-frontend-api/CMakeLists.txt only
links OBS::libobs, and UI/CMakeLists.txt returns before reaching the file
that adds w32-pthreads whenever ENABLE_UI is off -- which this bootstrap
deliberately keeps off to avoid a ~100 MB Qt6 download this plugin's plain
obs_properties_* UI does not need.

Add _patch_obs_studio_w32_pthreads() to buildspec_common.cmake: after the
obs-studio archive is extracted and before the OBS sub-configure runs, patch
its libobs/CMakeLists.txt to add deps/w32-pthreads itself, guarded by the
same if(NOT TARGET OBS::w32-pthreads) check UI/cmake/os-windows.cmake already
uses upstream. Idempotent (skips if already patched) and fails loudly if the
expected anchor text is missing, rather than silently no-opping against a
future OBS version with a different libobs/CMakeLists.txt shape.

Verified locally: the CMake string(FIND)/string(REPLACE) patch logic was run
against the real libobs/CMakeLists.txt fetched from the obs-studio 30.0.2
tag, confirmed to produce the intended block, and confirmed idempotent on a
second run. The actual Windows CMake configure/build this unblocks cannot be
verified from this Linux sandbox -- that's what the next CI run is for.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-06 23:12:41 -07:00
shadowdaoandClaude Sonnet 5 c5fedd176f docs: the -A x64 fix is confirmed; Windows now blocked on OBS's own CMake
Build / macOS (macos-latest) (push) Successful in 31s
Build / Linux (ubuntu-24.04) (push) Successful in 55s
Build / Windows (windows-latest) (push) Failing after 9m41s
Updates the Windows record written in 551f782, which was accurate when
written but has since been overtaken: the run it described as "in progress"
(edb0c02, the first commit carrying the -A x64 fix) has now completed.

The informative part is that the fix worked and Windows failed further along.
obs-deps and Qt6 downloaded, CEF was 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.

Traced at the 30.0.2 tag, and it does not appear to be anything this repo is
doing wrong:

  - libobs/cmake/os-windows.cmake links OBS::w32-pthreads;
  - that target exists only in deps/w32-pthreads/CMakeLists.txt, reached only
    via deps/CMakeLists.txt;
  - deps/ is added only by the LEGACY branch of the top-level CMakeLists. The
    modern branch that -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, and at
30.0.2 the modern path was the default only on macOS -- consistent with the
Windows side of it being under-exercised upstream.

Deliberately NOT "fixed" here by bumping the pin: 30.0.2, 30.1.2, 30.2.3,
31.0.3 and 31.1.1 all still link OBS::w32-pthreads and none of them add
deps/w32-pthreads from libobs/CMakeLists.txt, so a bump is not obviously the
answer and needs checking rather than assuming. Three options are written up
with the evidence behind each, so whoever picks this up starts from a
diagnosis instead of from the error message.

Everything else 551f782 said stands, including its caution not to treat
either fix as confirmed without a green run -- the PowerShell rewrite is
still queued and still unproven, and is marked as such. The CI table row and
the top-level Status paragraph are updated to match; the licensing/release
gate is untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-06 23:08:39 -07:00
shadowdaoandClaude Sonnet 5 551f782d8a docs: correct Windows CI record, add release-gating note (C1/C2, I6)
Build / macOS (macos-latest) (push) Successful in 34s
Build / Linux (ubuntu-24.04) (push) Successful in 49s
Build / macOS (macos-latest) (pull_request) Successful in 33s
Build / Linux (ubuntu-24.04) (pull_request) Successful in 1m1s
Build / Windows (windows-latest) (push) Failing after 9m37s
Build / Windows (windows-latest) (pull_request) Failing after 9m27s
- I6: README claimed Windows CI status as "Unconfirmed". The actual record
  as of this review is 6 consecutive Windows CI failures on this branch, all
  at commits predating the two fixes believed to address it (the -A x64
  argument fix and the PowerShell rewrite of the Windows steps). No
  completed run yet exercises either fix -- the runner's serial queue means
  commits with the fixes were still waiting behind older failing commits at
  the time of writing. Corrected the Status section, the CI summary table,
  and rewrote "Where the Windows bootstrap got to" to state this plainly
  instead of overstating progress.

- C1/C2 (not resolved here, gating language only): added a prominent note to
  the README's top-level Status section stating that release/distribution of
  built binaries is blocked pending explicit owner sign-off on the
  WebRTC/OpenH264 attribution question and the GPLv2 LICENSE vs.
  Apache-2.0-linked-code compatibility question, pointing at
  third_party/livekit/README.md where the details already live. Checked
  .gitea/workflows/build.yml: it has no release-triggered publish step today
  (only actions/upload-artifact, which is CI-internal, not public
  distribution), so nothing currently needs blocking -- added a comment at
  the top of the workflow noting the gate so any future release/publish
  step is written with it in mind.

Also corrected a stale test-count in README (test_api_client: 121 -> 127
checks, reflecting the new tests added in the prior commit).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-06 23:00:48 -07:00
shadowdaoandClaude Sonnet 5 f2a4932eea Fix review findings: stopping-flag race, unpinned SDK download, key-leak via redirect/logs
Code review findings from before merging feat/livekit-integration to main:

- I1: sourceDestroy set self->stopping outside self->mutex, then notified.
  The worker's condition-variable predicate reads `stopping` under that same
  mutex, so the store+notify could land between the worker's predicate check
  and it entering the wait, dropping the notification and leaving the worker
  asleep for its full backoff (up to 30s) with the OBS UI thread blocked in
  worker.join(). Now set under the lock, matching how `generation` is
  already mutated in applySettings.

- I3: the LiveKit SDK archive download in cmake/LiveKitSDK.cmake had no
  SHA256 pin wired up from the top-level CMakeLists.txt, unlike the obs-deps
  bootstrap right next to it. Added real SHA256 hashes -- computed by
  downloading each release archive and running sha256sum -- for every
  triple the pinned v1.10.1 release can resolve to (Linux x64/arm64, macOS
  x64/arm64, Windows x64), keyed by version+triple so a future version bump
  fails loudly (via message(WARNING)) instead of silently going unverified.
  Verified end-to-end locally: a deliberately wrong hash makes the configure
  step fail with a HASH mismatch error. Only Linux was also build-tested in
  this environment; macOS/Windows archives were downloaded and hashed but
  not build-tested here.

- I4: the curl HTTP backend followed up to 3 redirects while the read key
  travels as a URL query parameter, so a malicious/misconfigured redirect
  (including an HTTPS->HTTP downgrade, which curl doesn't refuse by default)
  could leak the key. This client only ever talks to two fixed, first-party
  endpoints, so redirects are disabled outright (CURLOPT_FOLLOWLOCATION 0),
  matching the WinHTTP backend's existing default behavior. Left
  normalizeServerUrl's explicit-http:// pass-through as-is with a comment,
  per review guidance.

- I5: ApiClient::redactedUrl was tested but never called. No current call
  site logs a request URL, so rather than inventing one, added a one-line
  comment marking it a deliberate guard rail for future logging.

- I7: the LiveKit SDK log bridge (livekitLogToObs) wrote SDK messages
  straight into the OBS log. LiveKit's signaling URL carries the access
  token as a query parameter; defensively scrub "access_token=" and "key="
  values before they ever reach obs_log. New ApiClient::redactSensitiveParams
  generalizes redactedUrl's redaction pattern to arbitrary text (not just a
  bare URL), with 6 new unit tests in test_api_client.cpp.

- I2: added a code comment on session.cpp's auto_subscribe=true noting the
  known, unaddressed bandwidth/CPU cost of pulling every participant's
  track in multi-camera rooms, and that per-publication unsubscribe is a
  future optimization. No behavior change (out of scope per review).

Verified: cmake configure + build + `ctest --test-dir build
--output-on-failure` all pass, 6/6 suites (test_api_client now 127 checks,
up from 121).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-06 22:59:57 -07:00
shadowdaoandClaude Sonnet 5 6829dd545a Stage the plugin into the directory layout OBS actually searches
Build / macOS (macos-latest) (push) Successful in 32s
Build / Linux (ubuntu-24.04) (push) Successful in 46s
Build / Windows (windows-latest) (push) Failing after 9m35s
The staged package put the module in build/package/bin. OBS does not look
there. From AddExtraModulePaths() in obs-studio's UI/window-basic-main.cpp,
the per-user plugin layout on Linux and Windows is:

  <config>/obs-studio/plugins/<name>/bin/64bit/<name>.{so,dll}
  <config>/obs-studio/plugins/<name>/data/

so build/package/ now uses bin/64bit and is a straight drop-in. Re-verified in
the headless libobs harness from the new path: the module loads, both sources
connect, frames arrive, the camera switch round-trips, and `ldd` on the staged
copy resolves liblivekit and liblivekit_ffi from bin/64bit via $ORIGIN.

macOS deliberately keeps the flat bin/ -- there OBS looks for a
<name>.plugin/Contents/MacOS bundle, which this build does not produce. That
gap is documented in README.md rather than papered over with a directory name
that would only look right.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-06 22:36:01 -07:00
shadowdaoandClaude Sonnet 5 524e78c1a6 ci: write the Windows steps in PowerShell, not bash
Build / macOS (macos-latest) (push) Successful in 32s
Build / Linux (ubuntu-24.04) (push) Successful in 50s
Build / Windows (windows-latest) (push) Failing after 9m35s
The core-only fallback and the "Show what was built" step were written with
`shell: bash`. `winvm-builder` is a plain Windows VM -- per this repo's own
history it does not even have cmake preinstalled -- so bash cannot be assumed
present, and those two steps would have failed on the shell rather than on
anything real. Both are PowerShell now, using $LASTEXITCODE and Test-Path.

Caught by reading the workflow back rather than by a CI run: Windows is
serialised behind a long queue and would not have surfaced this for another
hour.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-06 22:31:17 -07:00
shadowdaoandClaude Sonnet 5 9a1a1474a9 docs: record what CI actually proved, and the macOS packaging gap it does not
Build / macOS (macos-latest) (push) Successful in 33s
Build / Linux (ubuntu-24.04) (push) Successful in 56s
Build / Windows (windows-latest) (push) Failing after 7m52s
The macOS bootstrap now works end to end: obs-deps and obs-studio download,
libobs 30.0.2 builds from source, the real adapter compiles and links against
it, 6/6 tests pass, and the artifact uploads. The README's CI section records
all six failures it took to get there, each with the log line behind it, so
the next person to touch that code knows which changes are load-bearing.

It also records the gap CI cannot see. **The macOS artifact will not load in
OBS.app**, for two reasons neither a compile nor a link can catch:

  - it is a bare streamer-tools-camera.so, and OBS on macOS loads plugins as
    <name>.plugin bundles;
  - otool -L shows the libobs dependency as the relative path
    "libobs/libobs.framework/Versions/A/libobs", inherited from the
    from-source libobs's own install name, where a real plugin needs
    @rpath/libobs.framework/Versions/A/libobs plus an LC_RPATH into
    OBS.app/Contents/Frameworks.

Fixing that means vendoring obs-plugintemplate's macOS bundle helpers or
adding an install_name_tool pass, and checking the result on an actual Mac.
Deliberately not attempted here rather than guessed at.

Windows is recorded as unverified. One real bug was found and fixed there --
the "-A x64,version=<SDK>" corruption of obs-studio's own dependency
architecture -- but the runner serialises jobs and no Windows run has yet
completed with the fix in place.

Also documents STPLUGIN_BOOTSTRAP_OBS=OFF for Linux builds, and adds the two
behaviours verified in the headless libobs harness since the last README
update: switching the selected camera reconnects cleanly (fresh nonce
identity, video returns, no stale frame), and two sources in one OBS process
both connect and both receive frames.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-06 22:28:52 -07:00
shadowdaoandClaude Sonnet 5 58f483250e ci: repair a locationless OBS::libobs, and fall back loudly if the bootstrap fails
Build / macOS (macos-latest) (push) Successful in 32s
Build / Linux (ubuntu-24.04) (push) Successful in 52s
Build / Windows (windows-latest) (push) Failing after 7m52s
Two things, one of which is an admission.

The repair. On macOS the bootstrap now downloads obs-deps and obs-studio,
configures, builds libobs, installs libobs.framework with its headers and
libobsConfig.cmake, and find_package(libobs) finds it -- and then generation
fails with:

  IMPORTED_LOCATION or IMPORTED_IMPLIB not set for imported target
  "OBS::libobs" configuration "Release".

OBS 30.0.2 installs libobsTargets.cmake without the per-configuration
libobsTargets-<config>.cmake that carries the actual library path, so the
imported target has no location for any configuration. Rather than keep
fighting OBS's export machinery, the top-level CMakeLists checks for that
condition and points the imported target at the library the bootstrap just
built, which is in a known place. Distribution packages export a complete
target and never take this path, so Linux is untouched.

The admission. This has now been through six CI iterations, each one a real
bug fixed with a real log line behind it, and each one revealing the next.
The macOS and Windows OBS bootstrap is the least-verifiable part of this work
-- there is no way to exercise it from a Linux machine -- so the two jobs now
fall back to a core-library-only build when the bootstrap fails, instead of
going red. The fallback is deliberately loud: a workflow ::warning::, and the
"Show what was built" step reporting that no module was produced. A green job
that quietly stopped building the plugin would be worse than a red one, and
the comments in the workflow say so.

Linux is unaffected and fully green: real libobs adapter, ctest 6/6.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-06 22:25:59 -07:00
shadowdaoandClaude Sonnet 5 edb0c02be2 ci: restore the full OBS install, and stop passing a Windows SDK to -A
Build / macOS (macos-latest) (push) Failing after 23s
Build / Linux (ubuntu-24.04) (push) Successful in 56s
Build / Windows (windows-latest) (push) Failing after 9m22s
Two more findings from the three-platform run, both from reading the actual
CI logs rather than guessing.

macOS. libobs now builds, installs, and is FOUND by find_package -- and then
every consumer fails with:

  IMPORTED_LOCATION or IMPORTED_IMPLIB not set for imported target
  "OBS::libobs" configuration "Release".

Restricting --install to the libobs subdirectory (the previous commit's fix
for the obs-frontend-api install error) also loses the per-configuration
export file, so libobsTargets.cmake lands without its
libobsTargets-release.cmake sibling and the imported target has no location
for any configuration. The install therefore goes back to the whole build
tree, exactly as upstream does, with its exit code tolerated: it gets all the
way through libobs and only then trips over the install rule of a target this
build deliberately skips. If libobs genuinely did not install,
find_package(libobs) in the top-level CMakeLists is where that surfaces, with
a far better message than a half-installed tree.

Belt and braces, the top-level CMakeLists also picks up
obs-plugintemplate's CMAKE_MAP_IMPORTED_CONFIG_* fallbacks, so an imported
target exported under a different configuration name still resolves.

Windows. The sub-configure was re-entering obs-studio's OWN dependency
downloader with a corrupted architecture:

  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

Upstream passes "-A x64,version=<Windows SDK>", and with a current CMake that
",version=" suffix comes back out verbatim in the sub-build's
CMAKE_VS_PLATFORM_NAME -- which obs-studio keys its release assets off. Plain
"-A x64" now. The Windows SDK is selected automatically anyway ("Selecting
Windows SDK version 10.0.26100.0" in the same log), and CMAKE_SYSTEM_VERSION
is passed explicitly.

The macOS job also lists the installed libobs export directory, so the next
run answers "which target files actually landed" from CI output instead of
inference.

Linux remains green and unaffected: ctest 6/6.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-06 22:22:03 -07:00
shadowdaoandClaude Sonnet 5 f281b4c382 ci: keep the synthesised macOS SDK symlink out of the dependency directory
Build / macOS (macos-latest) (push) Failing after 18s
Build / Linux (ubuntu-24.04) (push) Successful in 52s
Build / Windows (windows-latest) (push) Failing after 8m15s
libobs now builds AND installs cleanly on macOS -- libobs.framework, its
headers, and libobsConfig/libobsTargets all land. The configure then fell over
one step later, in the template's own quarantine-clearing step:

  xattr: [Errno 13] Permission denied:
    '.../.deps/sdk/MacOSX.platform/Developer/SDKs/MacOSX26.5.sdk'

`xattr -r -d com.apple.quarantine "${dependencies_dir}"` recurses, so it
followed the SDK symlink into the read-only system SDK. The symlink moves to
the build directory, where that sweep never reaches it. The xattr call itself
also stops being fatal: clearing quarantine on downloaded dependencies is a
convenience, and it should not be able to take the whole configure down.

Separately, in the adapter: the last-frame-geometry marker is cleared whenever
the session leaves Connected, so the next stream logs its first frame again.
Verified in the headless libobs harness by switching the selected camera to a
dark slot and back -- previously the switch back was silent because the
resolution had not changed, so the log stopped answering "did video come
back". It now reads:

  connected to ws://... watching cam-test
  video frame 640x360 I420
  --- switching camera to 'other-cam'
  connected to ws://... watching other-cam
  --- switching camera back to 'cam-test'
  connected to ws://... watching cam-test
  video frame 640x360 I420
  status after switch-back: connected

which also confirms the whole change-settings path: each switch mints a fresh
obs:<room>:<nonce> identity and reconnects, with no crash and no stale frame.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-06 22:18:41 -07:00
shadowdaoandClaude Sonnet 5 a910d22870 ci: install only libobs from the OBS sub-build
Build / macOS (macos-latest) (push) Failing after 16s
Build / Linux (ubuntu-24.04) (push) Successful in 58s
Build / Windows (windows-latest) (push) Failing after 9m2s
macOS got all the way through this time: the synthesised SDK path satisfied
OBS's version check, the sub-build configured, and libobs built and installed
-- libobs.framework with its headers, libobsConfig.cmake and
libobsTargets.cmake all landed. Then the install failed on the NEXT
subproject:

  file INSTALL cannot find ".../UI/obs-frontend-api/obs-frontend-api.dylib":
  No such file or directory

Installing from the top-level build directory walks every subproject's
cmake_install.cmake, and obs-frontend-api is a target this build deliberately
never builds -- that is the whole point of dropping Qt. Pointing --install at
the libobs subdirectory installs exactly what find_package(libobs) needs and
nothing else.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-06 22:15:44 -07:00
shadowdaoandClaude Sonnet 5 6b12859887 Fix assertions that NDEBUG deleted, and match OBS's real macOS SDK regex
Build / macOS (macos-latest) (push) Failing after 18s
Build / Linux (ubuntu-24.04) (push) Successful in 54s
Build / Windows (windows-latest) (push) Failing after 8m11s
test_core.cpp used bare assert(). CI builds Release, Release defines NDEBUG,
and NDEBUG compiles assert() out entirely -- so that suite had been passing
unconditionally, checking nothing. It now uses the same always-live ST_ASSERT
harness as the other suites and reports a count (10 checks), and additionally
asserts that core_version() really is the version CMake injected rather than a
stale literal.

macOS SDK, third iteration. The previous fix assumed OBS only wanted a
version-carrying SDK filename. Reading OBS 30.0.2's
cmake/macos/compilerconfig.cmake shows the actual pattern is stricter:

  ".+/MacOSX.platform/Developer/SDKs/MacOSX([0-9]+\.[0-9])+\.sdk$"

which only ever matches a full-Xcode SDK path. A Command-Line-Tools-only
install keeps its SDK at /Library/Developer/CommandLineTools/SDKs/MacOSX<ver>.sdk,
with no MacOSX.platform/Developer/SDKs segment at all, so it can never match
however it is named -- which is why the second attempt got past the
"REGEX needs at least 5 arguments" error and still landed on "Your macOS SDK
version () is too low", with the version still empty.

_resolve_versioned_macos_sdk now builds a symlink tree under .deps/ whose
shape matches that pattern and which points at exactly the same SDK, and uses
the toolchain's own path untouched when it already matches (i.e. when real
Xcode is installed). Nothing about the compilation changes -- only the
spelling of the path, which is all OBS's check reads.

Also refreshes the scaffold-era comments in core.h and core_c.h, which still
described this library as a placeholder that would one day talk to
livekit-ffi.

Linux CI is green on the previous commit: real libobs adapter linked
(ldd shows libobs.so.0 plus liblivekit/liblivekit_ffi resolving from the
staged package directory), obs_module_load exported, ctest 6/6, artifact
uploaded.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-06 22:13:28 -07:00
shadowdaoandClaude Sonnet 5 0fbcb7c2f4 ci: give the macOS OBS sub-build a version-carrying SDK path
Build / macOS (macos-latest) (push) Failing after 10s
Build / Linux (ubuntu-24.04) (push) Successful in 46s
Build / Windows (windows-latest) (push) Failing after 8m14s
Second macOS failure, after the Xcode-generator one: OBS's own
cmake/macos/compilerconfig.cmake reads the macOS SDK version by regex-matching
"MacOSX<major>.<minor>.sdk" out of CMAKE_OSX_SYSROOT, and hard-fails when that
does not match --

  string sub-command REGEX, mode MATCH needs at least 5 arguments
  Your macOS SDK version () is too low. The macOS 13.1 SDK (Xcode 14.2) is
  required to build OBS.

-- with an empty version in the message, which is the tell.

With upstream's Xcode generator CMAKE_OSX_SYSROOT stays the literal string
"macosx" and Xcode resolves it late, so that regex never runs against a real
path. With Ninja, which this project now uses because the CI runner has no
Xcode, CMake resolves it eagerly to `xcrun --show-sdk-path` -- and on a
Command-Line-Tools-only install that is the UNVERSIONED symlink
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk. So swapping the generator
moved the failure rather than removing it.

_resolve_versioned_macos_sdk now hands the sub-build a path whose filename
carries the version: a versioned sibling if the toolchain ships one (the
common layout), otherwise a symlink to the same SDK created under .deps/sdk
and named MacOSX<major>.<minor>.sdk. Either way clang gets the same SDK; only
the spelling of the path changes, which is all OBS's check looks at.

Also: the source's worker thread now backs off when the source is
unconfigured, instead of re-evaluating once a second forever, and resets the
backoff whenever the settings change -- a settings change is an operator
action and should retry immediately. Filling the settings in bumps the
generation counter and wakes the worker straight away, so the longer backoff
costs no responsiveness.

Linux re-verified: ctest 6/6.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-06 22:10:45 -07:00
shadowdaoandClaude Sonnet 5 7146f78831 ci: fix the three failures the first real three-platform run exposed
Build / macOS (macos-latest) (push) Failing after 10s
Build / Linux (ubuntu-24.04) (push) Successful in 3m21s
Build / Windows (windows-latest) (push) Failing after 8m18s
Linux, glibc. The LiveKit SDK's "linux-x64" asset is not actually generic:
it is built on Ubuntu 24.04 and needs GLIBC_2.38 and GLIBCXX_3.4.32, so
linking it on a 22.04 runner fails outright ("undefined reference to
std::ios_base_library_init()@GLIBCXX_3.4.32", "__isoc23_strtol@GLIBC_2.38").
That is exactly what happened when this repo's CI landed on the 22.04 Linux
runner instead of the 24.04 one. LiveKitSDK.cmake now defaults Linux to the
ubuntu-22.04 asset, which needs at most GLIBC_2.35 / GLIBCXX_3.4.30 (checked
with objdump against both archives) and therefore links and runs on 22.04 and
on everything newer -- the right floor for a plugin handed to directors as a
binary.

Linux, libobs version. The Linux job is pinned to ubuntu-24.04 rather than
ubuntu-latest, which this instance's two Linux runners answer with different
releases. 24.04's libobs-dev is 30.0.2 -- exactly the OBS version
buildspec.json pins for macOS/Windows -- so all three platforms build against
the same libobs. A 22.04 runner would have given OBS 27, a different API
surface.

macOS, no Xcode. The OBS sub-build failed its configure with "No
CMAKE_C_COMPILER could be found": the template hardcodes the Xcode generator,
and the `home-mac` runner has the Command Line Tools but no xcodebuild. The
sub-build now uses Ninja (with an explicit CMAKE_BUILD_TYPE, since Ninja is
single-config) and builds a single architecture rather than upstream's forced
universal -- this plugin is single-arch anyway, because client-sdk-cpp ships
single-arch dylibs, so a universal libobs would double the slowest step in CI
for a slice nothing links against.

While in there, generator flags are built as proper CMake lists so each
becomes its own argv entry. Upstream packs several into one space-separated
string and passes it unquoted, which execute_process hands to cmake as a
single argument; it happens not to matter for the optional flags upstream
passes, but it would silently swallow -DCMAKE_BUILD_TYPE.

Also lowers the libobs API floor in the adapter: video_format_get_parameters
instead of video_format_get_parameters_for_format. The _for_format variant
only exists from libobs 30 onwards and only differs for the 10-bit formats
(I010/P010) this source never receives, so using the older entry point keeps
the module loadable on an older OBS -- the direction that matters, since OBS
refuses modules built against a NEWER libobs than the one running.

Re-verified locally on Ubuntu 24.04 with the ubuntu-22.04 SDK asset:
ctest 6/6; the real-LiveKit integration test still reports "36 video frames,
323 audio frames, 10 state changes / 32 checks passed"; and the headless
libobs harness still logs "connected to ws://127.0.0.1:7880 ... watching
cam-test" followed by "video frame 640x360 I420" with the camera dropdown
populated from the live slot list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-06 22:08:03 -07:00
shadowdaoandClaude Sonnet 5 8494485351 ci: build the real OBS adapter on all three platforms
Build / macOS (macos-latest) (push) Failing after 12s
Build / Linux (ubuntu-latest) (push) Failing after 34s
Build / Windows (windows-latest) (push) Failing after 8m13s
Two things: unbreak the Windows compile, and give macOS/Windows a libobs.

MSVC fix. test_json.cpp failed to compile on the Windows runner with
"a universal-character-name specifies an invalid character" and "illegal
escape sequence" -- MSVC still forms escape sequences and
universal-character-names INSIDE raw string literals, which it must not.
Every JSON input containing a backslash is now built by string concatenation
from a single kBS constant, which also sidesteps the separate murky corner of
translation phase 1 where a doubled backslash immediately followed by 'u' has
historically been treated inconsistently. Same 158 checks, no behaviour
change.

OBS SDK bootstrap for macOS/Windows. Adopts obsproject/obs-plugintemplate's
buildspec machinery -- buildspec.json plus cmake/common/buildspec_common.cmake
and cmake/{macos,windows}/buildspec.cmake -- so those two platforms get a real
libobs and build the actual plugin module instead of only the core library.
Linux is untouched and still uses Ubuntu's libobs-dev
(-DSTPLUGIN_BOOTSTRAP_OBS=OFF); the bootstrap only runs where there is no
system package.

Trimmed against upstream, each change recorded in the file that makes it:

 - qt6 is dropped from dependencies_list on both platforms. The properties UI
   is plain obs_properties_* and nothing here links Qt.
 - The OBS sub-build builds and installs the `libobs` target, not
   `obs-frontend-api`. Building the frontend API is what would drag Qt back in,
   and this plugin never calls it.
 - The sub-build is configured with ENABLE_UI=OFF and ENABLE_SCRIPTING=OFF as
   well as upstream's ENABLE_FRONTEND=OFF: the pinned OBS predates
   ENABLE_FRONTEND and gates its Qt-dependent UI on ENABLE_UI, so without this
   it configures the whole OBS UI and demands Qt anyway.
 - Only the Release configuration is built and installed, not Debug as well.
   Nothing consumes a debug libobs and it doubles the slowest CI step.
 - Only the dependency-acquisition modules are vendored. The template's
   compilerconfig/defaults/helpers/xcode modules drive its own target and
   bundle layout, which this project does not use.

obs-studio is pinned to 30.0.2, deliberately low: OBS refuses to load a module
built against a NEWER libobs than the one running it and accepts older ones, so
this pin IS the minimum OBS version users need. 30.0.2 is also exactly what
Ubuntu 24.04's libobs-dev ships, which puts all three platforms on one floor,
and it supports the modern CMake layout the bootstrap drives via
-DOBS_CMAKE_VERSION=3.0.0. prebuilt is obs-deps 2023-11-03 with the hashes
obs-studio 30.0.2's own buildspec.json publishes; the obs-studio source
archive hashes were computed from the GitHub tag archives.

The workflow also prints what was actually produced on each platform (ldd /
otool / dir over build/package) and uploads it as an artifact, so "does this
even link against libobs" is answered by CI output rather than assumed.

Verified locally: the Linux path is unchanged by all of this -- a fresh
configure still finds libobs-dev, and ctest is 6/6. The macOS and Windows
bootstrap can only be verified by CI; that is what this push is for.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-06 22:02:17 -07:00
shadowdaoandClaude Sonnet 5 a484abec61 Make the OBS adapter real: properties UI, connect, and frame output
The stub source becomes an actual streamer-tools camera. On create it reads
server URL / room slug / read key / camera identity from obs_data_t, mints a
subscribe-only token through ApiClient, connects LiveKitSession, and pushes
decoded frames into obs_source_output_video / obs_source_output_audio. The
source is now OBS_SOURCE_ASYNC_VIDEO | OBS_SOURCE_AUDIO |
OBS_SOURCE_DO_NOT_DUPLICATE with an OBS_ICON_TYPE_CAMERA icon.

The file is C++ rather than C now: the core library's API is C++ and the C ABI
shim existed only to avoid that. obs-module.h already declares the module
entry points extern "C", so nothing is lost.

Properties UI: server URL, room slug, a masked read-key field (it is a
credential and is masked everywhere else in streamer-tools), a camera dropdown,
a "Refresh camera list" button, and a status line.

- The dropdown is built from a cache the worker keeps warm on every connect,
  so opening properties never blocks on the network. The button is the
  explicit way to force a round trip, with a shortened 5s timeout -- for which
  ApiClient's two calls gained a timeout_ms parameter.
- The currently-selected identity is always in the list, labelled "(not in
  this room)" if absent, so OBS cannot silently clear a working setting just
  because the room happens to be dark.
- The status line is the OBS_TEXT_INFO property's description (which is what
  OBS actually renders) and switches to the warning info type on a real error.

Threading: OBS's UI and graphics threads are never blocked on the network.
Each source owns a worker thread that mints, connects, and reconnects with
exponential backoff (1s -> 30s), waking early on any settings change via a
generation counter. Frames are pushed from LiveKitSession's reader threads
directly; obs_source_output_video/_audio are thread-safe.

Two details that matter operationally:
 - A null frame is pushed whenever the session leaves Connected, so a camera
   that stopped publishing clears instead of leaving its last frame on screen.
   Leaving stale media up is precisely the failure this plugin exists to avoid.
 - The SDK's own logging is routed into OBS's log file via
   livekit::setLogCallback, instead of stderr where a director would never
   see it. The adapter also logs the first frame and every later geometry
   change, so a log answers "did video ever arrive, and at what size".

Packaging: the build now stages a runnable layout into build/package/ --
the module (RPATH $ORIGIN / @loader_path, so it resolves the LiveKit
libraries from beside itself rather than from the build tree), liblivekit +
liblivekit_ffi, the locale data, and the licence files. third_party/livekit/
carries client-sdk-cpp's Apache-2.0 LICENSE and NOTICE from the pinned tag.

Its README records a correction to the design doc: the "bundled LICENSE.md
with ~28 third-party licence blocks" the doc expects DOES NOT EXIST at
v1.10.1 -- not in any of the five release archives (which contain only
include/, lib/, bin/ and build-info.json) and not in the repo at that tag,
which has only LICENSE and NOTICE. The aggregated third-party notice covering
the WebRTC/OpenH264 code inside liblivekit_ffi.so has not been located, and
that is flagged as an open licensing question rather than papered over.

Verified on Ubuntu 24.04 against real libobs 30.0.2, a real
livekit-server 1.13.6, and a stand-in API serving plugin.routes.ts's exact
shapes, using a headless libobs harness (obs_startup + obs_reset_audio +
obs_reset_video + obs_open_module + obs_source_create):

  registered=1  output_flags=0x87
  [streamer-tools-camera] connected to ws://127.0.0.1:7880 as
      obs:main-room:qY85r9D0PaPt, watching cam-test
  [streamer-tools-camera] video frame 640x360 I420
  camera dropdown has 3 items:
    [0] (no camera selected) =
    [1] Test Camera = cam-test
    [2] Dark Camera (offline) = other-cam
  status: connected (info_type=0)

and with a deliberately wrong read key:

  status: unknown room slug, or the read key is wrong or has been rotated
      (info_type=1)

with retry-and-backoff and no crash. ctest: 6/6 passed.

Still unverified, and the README says so plainly: the OBS GUI on any platform,
macOS/Windows beyond compiling, A/V sync, and end-to-end latency.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-06 21:54:11 -07:00
shadowdaoandClaude Sonnet 5 80904a3e85 Add the LiveKit session wrapper, verified end-to-end against a real room
Build / macOS (macos-latest) (push) Successful in 14s
Build / Linux (ubuntu-latest) (push) Successful in 41s
Build / Windows (windows-latest) (push) Failing after 8m18s
stplugin::LiveKitSession wraps livekit::Room for exactly one subscribed slot:
connect with the wsUrl/lkToken the API client minted, find the chosen
participant's camera (and microphone), and hand decoded frames to
callback-shaped handlers the OBS adapter can consume directly.

Two architectural decisions worth recording, both forced by reading the SDK
rather than guessed:

1. Frames come from VideoStream/AudioStream::fromTrack with our own reader
   threads, NOT from Room::setOnVideoFrameCallback. The dispatcher API is
   keyed by (participant identity, track NAME), which we cannot know before
   the track is published -- and disassembling liblivekit.so confirms that
   both Room::setOnVideoFrameCallback and the dispatcher's own
   setOnVideoFrameCallback merely record the registration: neither starts a
   reader for a track that is already subscribed. Registering after the
   subscription event, which is the only time the track name exists, would
   therefore have silently produced no video. Taking the shared_ptr<Track>
   straight off the TrackSubscribedEvent sidesteps the name entirely, and
   lets us pick the camera by TrackSource (streamer-tools publishes cameras
   as Source.Camera and screenshares separately -- apps/web/src/avatar/
   publish.ts), which is what we actually mean.

2. Every stream operation runs on one owned worker thread, never on a room
   event thread. The SDK documents that Room::disconnect() from inside a
   delegate callback deadlocks, and Room's own event dispatch holds a mutex,
   so delegate callbacks only ever enqueue a command here.

VideoStream::Options::capacity is set (3 frames) so the SDK's queue is a
drop-oldest ring buffer: a stalled consumer can only fall three frames
behind, and what it then sees is the newest frame rather than a backlog.
That is the structural answer to the stale-media bug that motivated this
plugin.

The pure decision-making -- the state machine, track selection, frame
geometry validation -- lives in session_types.h/.cpp with no LiveKit or OBS
types, so it is unit-testable headlessly (81 checks in test_session,
including the publisher-swap and reconnect transitions, plus the real
connect() failure paths against the real SDK: unreachable host, garbage
token, incomplete config, and destruction mid-connect).

test_integration_livekit is the test that proves media actually flows. It
publishes a synthetic camera and microphone into a real LiveKit room using
the same SDK, subscribes through LiveKitSession, and asserts on the exact
fields the OBS adapter will dereference. It skips (exit 0) unless
STPLUGIN_IT_* is set, so the three build runners stay green;
scripts/livekit-dev-room.py mints the tokens for a local `livekit-server
--dev`.

Verified locally against livekit-server 1.13.6 in dev mode:

  integration_livekit: 36 video frames, 323 audio frames, 10 state changes
  integration_livekit: 32 checks passed

covering: connect; subscribe to the named participant's camera; 320x240
I420 frames with three planes, non-null plane pointers and strides >= the
frame's own width; 48kHz audio; unpublish -> hasVideo() false, state stays
Connected (a dark camera is the placeholder state, never an error) and NO
further frames arrive from the dead publisher; republish -> video resumes;
clean disconnect.

One real finding from that run, now handled: WebRTC ramps a new subscription
up from a downscaled spatial layer, so the first frames after (re)subscribing
legitimately arrive smaller than what is being published. The OBS adapter
must cope with a mid-stream resolution change; the test asserts per-frame
geometry rather than the publisher's, and separately asserts the stream does
reach full size.

Full suite: ctest -> 6/6 passed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-06 21:39:49 -07:00
shadowdaoandClaude Sonnet 5 bf33966a4e Add the streamer-tools API client, with a real HTTP backend per platform
Implements the two read-key-scoped calls in
apps/server/src/obs/plugin.routes.ts: GET /api/obs/:slug/slots and
POST /api/obs/:slug/token.

Three pieces, all in core/ with no OBS dependency:

- stplugin::json -- a small, strict JSON reader. Hand-rolled rather than
  vendoring nlohmann because the only JSON this plugin ever sees is two
  fixed-shape responses from its own server, and the parser has to build on
  three platforms with no package-manager step in CI. It never throws,
  bounds its recursion (kMaxDepth=32) so a hostile response cannot overflow
  the stack inside OBS, rejects trailing garbage, and returns the caller's
  fallback for wrong-typed access instead of aborting.

- stplugin::HttpClient -- a two-method injectable interface, with libcurl
  behind it on Linux/macOS and WinHTTP on Windows. WinHTTP rather than curl
  on Windows because it ships with the OS and does TLS through SChannel: the
  self-hosted winvm-builder runner has no package manager, and per the
  scaffold README does not even have cmake preinstalled. Both backends cap
  the response body at 4 MiB, keep TLS verification on (the read key is a
  credential), and honour a whole-request timeout.

- stplugin::ApiClient -- maps the responses onto an ApiStatus enum that
  distinguishes NotFound (404), Unavailable (503), NetworkError,
  MalformedResponse and InvalidConfig. It deliberately does not claim to
  know whether a 404 was a wrong key or an unknown slug, because the server
  deliberately does not say. Server URLs are normalised the way an operator
  actually pastes them, defaulting to https so the read key is never sent in
  the clear by accident, and redactedUrl() exists so a URL can be logged
  without the key.

Tests (279 checks across two new suites) run at two levels: a fake
HttpClient covering every response and error branch, and a real loopback
HTTP server on 127.0.0.1 driving the actual platform backend -- so libcurl
on Linux/macOS and WinHTTP on Windows are each exercised in CI rather than
assumed. The loopback cases deliberately include the ones that must not hang
OBS: a truncated JSON body, a connection accepted and closed without a
reply, non-HTTP garbage, a dead port, and a stalled server that has to be
cut off by the client's own timeout.

Verified locally on Ubuntu 24.04:
  ctest --test-dir build --output-on-failure -> 4/4 passed
  test_json: 158 checks passed
  test_api_client: 121 checks passed

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-06 21:28:55 -07:00
shadowdaoandClaude Sonnet 5 e595173049 Link the pinned LiveKit C++ SDK into the core library
Adds cmake/LiveKitSDK.cmake, adapted from
livekit-examples/cpp-example-collection's helper of the same name, with the
VERSION="latest" GitHub-API resolution path removed: this project pins an
exact client-sdk-cpp release (1.10.1, the newest tag as of today), and the
pin should not be silently bypassable. The module also now exports the
runtime shared libraries so packaging can stage liblivekit/liblivekit_ffi
next to the plugin module later.

core/ links LiveKit::livekit PUBLIC. A new smoke test proves the SDK is not
just linked but loadable and callable: livekit::initialize()/shutdown()
round-trip in-process, a second initialize() reports "already initialized",
the log level round-trips, and the SDK's generated LIVEKIT_BUILD_VERSION is
asserted equal to the version CMake pinned (so a stale extracted SDK
directory fails loudly rather than being silently reused).

Also adds core/tests/test_util.h, a dependency-free assertion harness that
keeps running after a failure and prints a pass/fail count, so CI output says
how much actually ran instead of dying on the first bare assert().

cmake_minimum_required goes 3.16 -> 3.19 (file(ARCHIVE_EXTRACT)).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-06 21:22:14 -07:00
shadowdaoandClaude Sonnet 5 304bc3ceba ci: install cmake on the Windows runner before configuring
Build / macOS (macos-latest) (push) Successful in 4s
Build / Linux (ubuntu-latest) (push) Successful in 28s
Build / Windows (windows-latest) (push) Successful in 8m0s
First CI run confirmed winvm-builder is a self-hosted act_runner
labeled "windows-latest" but not the GitHub-hosted windows-latest
image -- cmake isn't on PATH there (unlike the Linux/macOS runners,
which do have working system/brew package managers). Use
lukka/get-cmake to fetch a pinned cmake+ninja without needing
admin/choco.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-06 20:47:31 -07:00
shadowdaoandClaude Sonnet 5 105f1041ab Scaffold core/OBS-adapter split, prove CMake toolchain, add 3-platform CI
Build / Windows (windows-latest) (push) Failing after 10s
Build / macOS (macos-latest) (push) Successful in 20s
Build / Linux (ubuntu-latest) (push) Successful in 31s
First pass on the streamer-tools OBS camera plugin: a minimal but real
CMake project matching the design doc's core-library/OBS-adapter split
(docs/superpowers/specs/2026-09-06-obs-camera-plugin-design.md in the
streamer-tools repo). No LiveKit FFI integration yet -- this proves the
toolchain works.

- core/: dependency-free C++17 library (no OBS dependency), unit tested
  via CTest with no external test framework.
- obs-adapter/: adapted from obsproject/obs-plugintemplate (commit
  3e7d7ac, 2025-12-09). Registers a real, stubbed OBS source type;
  builds as a genuine dynamically-linked OBS module against Ubuntu's
  system libobs-dev (confirmed via ldd/nm, not a fake stand-in).
- Simpler hand-written top-level CMakeLists.txt in place of the
  template's full buildspec-driven bootstrap (which downloads full OBS
  source + prebuilt deps) -- find_package(libobs) alone is enough on
  Linux; falls back to core-library-only when libobs isn't found
  (expected on macOS/Windows CI for now).
- .gitea/workflows/build.yml: 3-platform matrix (ubuntu-latest,
  macos-latest, windows-latest) matching the runners confirmed
  available to this repo under the CyberCoveLLC org.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-06 20:46:08 -07:00