Commit Graph
41 Commits
Author SHA1 Message Date
shadowdaoandClaude Opus 5 abd4dc9aca docs: record the first confirmed OBS GUI load (Windows)
Build / macOS (macos-latest) (pull_request) Successful in 26s
Build / Linux (ubuntu-24.04) (pull_request) Successful in 59s
Build / macOS (macos-latest) (push) Successful in 37s
Build / Linux (ubuntu-24.04) (push) Successful in 55s
Build / Windows (windows-latest) (pull_request) Successful in 3m45s
Build / Windows (windows-latest) (push) Successful in 3m48s
The v0.1.0 Windows artifact loaded into OBS 32.2.2 on Windows 11 (build
26200) on a director's machine on 2026-09-09, from
C:\ProgramData\obs-studio\plugins\streamer-tools-camera\bin\64bit\. That
retires "has not been run in the OBS GUI on any platform", which this README
asserted in four places.

Scoped deliberately to what was actually observed: the module loads and
registers its source type. Whether video renders, the colours, A/V sync,
latency, and mid-show publisher restart are all still unverified, and Linux
and macOS have still never been opened in the GUI. The "Not verified
anywhere" list now says so explicitly rather than being deleted.

Also drops the release-draft rationale that pointed at the no-GUI-load state,
and carries the ProgramData install-path warning up into the status section,
since that is what stopped the first GUI load from happening sooner.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AzGnvQ6wfD7bw7PZN35ft9
2026-09-09 16:47:00 -07:00
shadowdaoandClaude Opus 5 bfc38f45ca docs: the Windows plugin dir is ProgramData, not the config dir
Build / macOS (macos-latest) (push) Successful in 36s
Build / Linux (ubuntu-24.04) (push) Successful in 55s
Build / macOS (macos-latest) (pull_request) Successful in 33s
Build / Linux (ubuntu-24.04) (pull_request) Successful in 1m9s
Build / Windows (windows-latest) (push) Successful in 10m51s
Build / Windows (windows-latest) (pull_request) Successful in 10m27s
The packaging section claimed `<config>/obs-studio/plugins/<name>/bin/64bit`
is "exactly the layout OBS searches on Linux and Windows". The layout is
right; the base directory is not the same on both. obs-studio's
`AddExtraModulePaths()` uses `GetAppConfigPath` on Linux/macOS but
`GetProgramDataPath` (`CSIDL_COMMON_APPDATA`) on Windows, so Windows scans
`C:\ProgramData\obs-studio\plugins\`, never `%APPDATA%\obs-studio\`
(`CSIDL_APPDATA`) — which on Windows is where OBS keeps its config.

This bit a director on 2026-09-09: a correctly-shaped install under
`AppData\Roaming` produced an OBS log with zero mention of the module. Noted
here because the failure is silent — OBS names any module it finds but cannot
use, so an absence of any line means the file is not on a scanned path at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AzGnvQ6wfD7bw7PZN35ft9
2026-09-09 16:44:06 -07:00
shadowdaoandClaude Sonnet 5 76b2942db1 ci: bump the OBS deps cache key -- it was stuck on a permanently bad entry
Build / macOS (macos-latest) (push) Successful in 45s
Build / Linux (ubuntu-24.04) (push) Successful in 53s
Build / Windows (windows-latest) (push) Successful in 6m0s
Release / macOS (macos-latest) (push) Successful in 38s
Release / Linux (ubuntu-24.04) (push) Successful in 57s
Release / Windows (windows-latest) (push) Successful in 3m28s
Release / Create Gitea Release (draft) (push) Successful in 19s
actions/cache never overwrites an existing key: once a key has a saved
entry, later jobs' save steps are always skipped as a no-op regardless
of cache hit/miss. The very first job to ever populate this cache did
so before the "Drop non-relocatable OBS build tree" cleanup step
existed, so that entry included the bad, path-baked build_x86
directory -- and every run since has kept restoring that same bad
entry, not self-healing after "one more transitional run" the way it
looked when this was last checked. Confirmed live: a run just now hit
the identical stale-path CMakeCache.txt error against the same
original job hash (f72837d12c75c64a) from hours ago.

Bumping the key (obs-deps- -> obs-deps-v2-) is what actually forces a
fresh save under the cleanup step's watch, since the key content
(buildspec files) hasn't changed and nothing else would invalidate it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-07 10:47:56 -07:00
shadowdaoandClaude Sonnet 5 af7d2c6d24 fix: pin video quality to stop OBS source resizing; add audio-only mode
Build / macOS (macos-latest) (push) Successful in 34s
Build / Linux (ubuntu-24.04) (push) Successful in 46s
Build / Windows (windows-latest) (push) Failing after 2m44s
Live testing (2026-09-07) showed two real problems in one root cause:
LiveKit's default subscriber behavior lets the SFU switch simulcast
layers on its own bandwidth/adaptive logic, and this plugin never told
it not to. For a real camera, that showed up as the OBS source's
received frame size visibly hopping between 320x180/640x360/1280x720
mid-show -- OBS's async video source resizes to match, breaking any
manual crop/position a director had set up. For the soundboard (a
Camera-source track that exists only to satisfy RTMP's video
requirement -- Soundboard.tsx -- with no real visual content), the
same instability, plus the video showing at all, was pure noise: there
was no way to pull just its audio.

Both come from RemoteTrackPublication (livekit/remote_track_publication.h
in the pinned SDK), on the exact publication object TrackSubscribedEvent
and attachExistingTracks already hand this code:

  - setVideoQuality(VideoQuality::HIGH) on every wanted video track,
    unconditionally, so the SFU always sends the top simulcast layer
    instead of switching layers underneath a source with no
    rendered-size hint to give it (this is a native subscriber, not a
    sized <video> element).
  - A new SessionConfig::subscribe_video (mirrors subscribe_audio):
    when false, the wanted video track is never attached, and its
    publication is explicitly setEnabled(false) -- the SFU stops
    sending it, not just "decoded and discarded here". Wired to a new
    "Audio only (no video)" checkbox in the source's properties.

Both call sites (a fresh TrackSubscribedEvent, and attachExistingTracks
sweeping tracks already up when the session starts watching) go
through one new handleWantedVideoTrack() so they can't drift apart.

Not unit-testable without a real LiveKit connection (RemoteTrackPublication
isn't fakeable, matching why test_integration_livekit.cpp already needs a
real server) -- verified instead by a full local build against real
libobs-dev + the pinned SDK (clean compile, all 6 existing tests still
pass) and CI. The actual behavioral fix -- stable resolution, no video
for an audio-only source -- needs the same real-OBS verification every
other claim in this repo's "What is verified, and how" section does.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-07 10:43:54 -07:00
shadowdaoandClaude Sonnet 5 104326d05a docs/release: drop the C1 licensing gate, simplify install instructions
Build / macOS (macos-latest) (push) Successful in 29s
Build / Linux (ubuntu-24.04) (push) Successful in 49s
Build / Windows (windows-latest) (push) Successful in 2m50s
Release / macOS (macos-latest) (push) Successful in 33s
Release / Linux (ubuntu-24.04) (push) Successful in 56s
Release / Windows (windows-latest) (push) Successful in 3m20s
Release / Create Gitea Release (draft) (push) Successful in 19s
The WebRTC/OpenH264 attribution question tracked as "C1" throughout
README, third_party/livekit/README.md, the release-notes template, and
both workflow header comments is the project owner's call, and it has
been made -- own sign-off given and reaffirmed. Remove the gate
language and the extended research writeup from release-facing docs;
keep the actual LICENSE/NOTICE files themselves (Apache-2.0 requires
shipping those regardless of any of this).

Also simplify the release notes' install instructions per owner
request: point at each platform's default OBS plugins folder rather
than walking through verbose per-platform copy/extract instructions --
the archives already extract straight into place (prior commit), so a
short pointer is all that's needed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-07 09:40:49 -07:00
shadowdaoandClaude Sonnet 5 e050b81d6c ci: stop re-downloading CMake/Ninja on every Windows run
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
2026-09-07 09:07:23 -07:00
shadowdaoandClaude Sonnet 5 18610bd9dc ci: stop build.yml from also running on tag pushes
Build / macOS (macos-latest) (push) Successful in 32s
Build / Linux (ubuntu-24.04) (push) Successful in 48s
Build / Windows (windows-latest) (push) Successful in 10m32s
A bare `push:` trigger matches every ref push, tags included -- so
tagging a release triggered this workflow's full 3-platform build
(Windows and all) at the same time as release.yml's own, both
competing for the Windows runner's capacity:1 and roughly doubling
Windows CI time for every tag push. Scope this workflow's push trigger
to branches only; release.yml already covers this same build (plus
packaging) on every `v*` tag.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-07 09:03:41 -07:00
shadowdaoandClaude Sonnet 5 43a501c06c release: package archives as direct drop-ins to OBS plugin dirs
Release / macOS (macos-latest) (push) Successful in 43s
Release / Linux (ubuntu-24.04) (push) Successful in 51s
Build / Windows (windows-latest) (push) Successful in 10m43s
Build / Linux (ubuntu-24.04) (push) Successful in 52s
Build / macOS (macos-latest) (push) Successful in 28s
Release / Windows (windows-latest) (push) Failing after 16m46s
Release / Create Gitea Release (draft) (push) Skipped
Previously each release zip mirrored build/package/ verbatim (bin/ +
data/ at the archive root), which needed a manual mkdir + cp -r into
the right OBS plugin subdirectory per the install instructions. Wrap
Linux and Windows archives in a top-level streamer-tools-camera/
directory instead -- matching AddExtraModulePaths()'s expected
<config>/obs-studio/plugins/<name>/bin/64bit + data layout -- so
`unzip -d ~/.config/obs-studio/plugins/` (or Expand-Archive to
%APPDATA%\obs-studio\plugins\ on Windows) is the entire install step.

macOS already produced the right shape (the .plugin bundle itself at
the archive's top level, since OBS wants the whole bundle directly
under plugins/, not nested under a named subdirectory) -- no packaging
change needed there, just an install-instructions rewrite plus making
that text properly conditional on MACOS_BUNDLE_FOUND like MACOS_NOTE
already was, since the old hard-coded "not yet that bundle shape" text
was stale once the macOS bundle packaging fix landed.

Verified locally: staged a fake build/package/ tree, ran the new
packaging logic, and confirmed extracting the resulting zip into a
plugins directory produces <name>/bin/64bit/... and <name>/data/...
directly. Also dry-ran publish-release.sh's notes generation for both
MACOS_BUNDLE_FOUND values to confirm the conditional install text
renders correctly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-07 08:41:44 -07:00
shadowdaoandClaude Sonnet 5 14fa00af3f docs: OpenH264/MPEG-LA royalty finding sharpens C1 -- not resolved
Build / macOS (macos-latest) (push) Successful in 28s
Build / Linux (ubuntu-24.04) (push) Successful in 59s
Build / Windows (windows-latest) (push) Successful in 10m45s
Cisco's own OpenH264 FAQ (openh264.org/faq.html) is explicit: they cover
MPEG-LA/AVC patent-pool royalties only for their own prebuilt binary,
downloaded at install time. Anyone who compiles OpenH264 from source
and redistributes it inside their own binary takes on "all applicable
license fees" themselves -- Cisco "will not be liable for any licensing
fees incurred by other parties" in that case.

LiveKit's client-sdk-cpp links Google libwebrtc via the webrtc-sdk
org's fork, whose documented build args (rtc_use_h264=true,
ffmpeg_branding="Chrome") are the standard Chromium/WebRTC recipe --
which links a from-source, statically-compiled copy of OpenH264 (from
Google's mirror, not Cisco's runtime binary) into libwebrtc. That is
exactly the shape of case Cisco's FAQ says voids their coverage. Not
independently confirmed against LiveKit's actual pinned v1.10.1 build
(their release archives ship only compiled output, no build manifest)
-- this is webrtc-sdk/libwebrtc's documented default, not a verified
fact about this specific artifact.

This sharpens C1 into a concrete mechanism instead of a general open
question. It does not resolve C1 -- if anything it strengthens the case
for treating it as unresolved -- and none of this is a substitute for
an actual legal opinion. Recorded in third_party/livekit/README.md (the
full writeup), README.md's Status section, and the release-notes
template in publish-release.sh so it reaches whoever opens a draft
release next, not just this one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-07 07:45:36 -07:00
shadowdaoandClaude Sonnet 5 d287a90048 ci: strip the non-relocatable OBS build tree before caching .deps
Build / macOS (macos-latest) (push) Successful in 33s
Build / Linux (ubuntu-24.04) (push) Successful in 56s
Release / macOS (macos-latest) (push) Successful in 32s
Release / Linux (ubuntu-24.04) (push) Successful in 52s
Build / Windows (windows-latest) (push) Successful in 10m35s
Release / Windows (windows-latest) (push) Successful in 11m7s
Release / Create Gitea Release (draft) (push) Successful in 20s
The obs-studio sub-build's out-of-source CMakeCache.txt
(.deps/obs-studio-*/build_*) bakes in the job's absolute checkout
path. Caching it as-is (from the previous .deps caching commit)
meant every run's first configure attempt hard-failed with:

  CMake Error: The current CMakeCache.txt directory ... is different
  than the directory ... where CMakeCache.txt was created.

Confirmed live in run 3091 (commit 1f342b1): this was silently
self-healing, since windows-build.ps1's existing fallback
(-DSTPLUGIN_BOOTSTRAP_OBS=OFF) still found the already-installed,
path-independent libobs package and the build succeeded anyway -- but
every run paid for one guaranteed-failed configure and printed a
misleading "::warning::OBS SDK bootstrap failed" that isn't actually
true. Delete only the build tree, after libobs is already built and
installed from it, so nothing path-dependent survives into the cache.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-07 06:10:55 -07:00
shadowdaoandClaude Sonnet 5 1f342b1971 docs: Windows CI is green, log-verified; scope the w32-pthreads fallback
Build / macOS (macos-latest) (push) Successful in 32s
Build / Linux (ubuntu-24.04) (push) Successful in 52s
Build / Windows (windows-latest) (push) Successful in 10m36s
Records the outcome of f27b1c0 against the actual job log rather than the
job status: the from-source libobs bootstrap configures/builds/installs,
find_package(libobs) resolves through .deps/cmake/libobs, all 6 CTest suites
pass, and build\package\bin\64bit\streamer-tools-camera.dll (136,192 bytes)
is staged beside livekit.dll and livekit_ffi.dll. First completed green
Windows run on this repository. That also retires three separately-unproven
items at once -- the -A x64 fix, the PowerShell rewrite of the Windows steps,
and the add_subdirectory(deps/w32-pthreads) bootstrap patch.

The log also settles which branch of the new code ran: the "w32-pthreads_DIR
not set" message means OBS's own exported package was at
.deps/cmake/w32-pthreads/ the whole time and the fallback find module was
never loaded. The export was never missing, only unfindable -- worth
recording, because "w32-pthreads has no install rules" was the first
hypothesis and it was wrong.

Also narrows when that fallback is exposed at all. It reads artifacts out of
.deps/ and nowhere else, so it is only appended to CMAKE_MODULE_PATH when
this repo's own bootstrap actually produced an obs-studio tree there. A
developer building against an external OBS SDK (STPLUGIN_BOOTSTRAP_OBS=OFF,
or an explicit -Dlibobs_DIR=...) stays on CMake's normal search, where
whatever w32-pthreads package their SDK provides can still win -- a find
module that could only ever fail would have made their error worse, not
better. Linux reconfigures unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-07 05:56:18 -07:00
shadowdaoandClaude Sonnet 5 f27b1c0b45 Fix Windows configure: point find_package at OBS's exported w32-pthreads
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
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
shadowdaoandClaude Sonnet 5 1a8255230f ci: cache the OBS SDK bootstrap deps across runs
Build / Linux (ubuntu-24.04) (push) Successful in 1m5s
Build / macOS (macos-latest) (push) Successful in 1m10s
Build / Windows (windows-latest) (push) Failing after 9m18s
cmake/{windows,macos}/buildspec.cmake download the pinned obs-deps
bundle + obs-studio source into .deps/, with its own idempotent
skip-if-present logic keyed on SHA256 marker files. That logic never
gets a chance to fire because .deps/ lives inside the checkout and a
fresh `actions/checkout` wipes it on every single push -- this is what
has made every CI iteration tonight re-pay the several-minute cold
download instead of only paying it once.

Add an actions/cache step for .deps/ in both jobs, keyed on the files
that actually pin dependency versions (buildspec.json +
buildspec_common.cmake + the platform buildspec.cmake), so a version
bump still invalidates the cache correctly. No runner-side config
change needed -- the act_runner instance already has its built-in
cache server on by default (cache.enabled: true, confirmed via
`gitea-runner.exe generate-config` on winvm-builder).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-07 05:19:16 -07:00
shadowdaoandClaude Sonnet 5 b5c91c72e5 macOS: real .plugin bundle packaging, libobs @rpath fixup, hard-fail CI check
Build / macOS (macos-latest) (push) Successful in 37s
Build / Linux (ubuntu-24.04) (push) Successful in 53s
Build / Windows (windows-latest) (push) Canceled after 1m16s
Closes the "macOS packaging gap" documented in README.md: CI built a real
adapter module on macOS but staged it as a bare streamer-tools-camera.so,
which OBS.app cannot load (it needs a <name>.plugin bundle), and otool -L
showed the libobs dependency as the relative path
libobs/libobs.framework/Versions/A/libobs instead of an @rpath reference.

cmake/macos/helpers.cmake adapts (not vendors as-is) obs-plugintemplate's
cmake/macos/helpers.cmake: upstream builds the bundle almost entirely
through XCODE_ATTRIBUTE_* properties and its own CI drives that with
`xcodebuild -project <name>.xcodeproj`, but this project's home-mac runner
has only the Command Line Tools, not Xcode.app (already established by
buildspec_common.cmake's CI-iteration-1 comment), and the whole project
builds with Ninja end to end. So this reimplements the same outcome --
Contents/MacOS, Contents/Resources, a real Info.plist -- with CMake's own
generator-agnostic BUNDLE/BUNDLE_EXTENSION/MACOSX_BUNDLE_INFO_PLIST/
MACOSX_PACKAGE_LOCATION target properties (verified working under Ninja via
a dry-run configure with APPLE spoofed), and does by hand what upstream gets
from Xcode's embed/codesign build phases:
  - copies obs-adapter/data/** into Contents/Resources/**, since OBS's
    AddExtraModulePaths() (UI/window-basic-main.cpp) passes
    Contents/Resources as a macOS module's *data path* -- not a sibling
    data/ the way Linux/Windows work -- so the locale ini has to land at
    Contents/Resources/locale/en-US.ini for OBS_MODULE_USE_DEFAULT_LOCALE
    to find it
  - copies the LiveKit runtime dylibs into Contents/Frameworks
  - fixes up the libobs dependency: cmake/macos/fixup-libobs-rpath.sh
    rewrites the relative install name the from-source libobs build records
    to @rpath/libobs.framework/Versions/A/libobs (LiveKit's own dylibs
    already record @rpath references, confirmed in prior CI otool -L
    output, so only libobs needs the rewrite)
  - gives the plugin binary two LC_RPATH entries via INSTALL_RPATH:
    @loader_path/../Frameworks (this bundle's own Frameworks, for LiveKit)
    and @executable_path/../Frameworks (OBS.app/Contents/Frameworks, for
    libobs.framework -- @executable_path is always relative to the host
    process's main executable, not this dlopen'd bundle)

obs-adapter/CMakeLists.txt: calls stplugin_macos_finalize_bundle() before
staging, and corrects the staged layout for macOS -- OBS's module search
wants the whole <name>.plugin dropped directly into .../obs-studio/plugins/,
not nested under a bin/ subdirectory the way Linux/Windows are, so
build/package/ now holds the bundle at its own top level on macOS instead of
build/package/bin/<name>.plugin.

.gitea/scripts/macos-build.sh (this project's macOS steps were factored out
of build.yml into this shared script, used by both build.yml and
release.yml, in a concurrent commit -- rebased onto that): the "Show what
was built" section now hard-fails unless a real .plugin bundle (with
Info.plist, bundled LiveKit dylibs, and locale data) was produced, the
libobs dependency resolves via @rpath rather than the old relative path,
and both LC_RPATH entries are present -- closing the same
false-positive-green gap this project's Windows find_package(libobs)
incident just exposed, which this check previously did not cover (it only
checked for a bare .so). release.yml's macOS packaging step already globs
for any `*.plugin` under build/ (added in the concurrent commit,
anticipating this fix), so it picks up the new layout with no change
needed.

Verified: Linux configure/build/ctest (STPLUGIN_BOOTSTRAP_OBS=OFF) still
passes 6/6, unaffected -- the new macOS CMake logic is fully guarded by
if(APPLE). The bundle/Info.plist/Resources-mapping logic itself was dry-run
verified by configuring a throwaway CMake project with APPLE spoofed to
TRUE, confirming buildspec.json values substitute correctly into Info.plist
and obs-adapter/data/locale/en-US.ini maps to Resources/locale/en-US.ini.
Not verified: an actual macOS build/link/otool pass, or loading the result
in real OBS.app -- this is a Linux sandbox with no way to do either: this
commit is going to the home-mac CI runner to get that verification next.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-07 05:16:57 -07:00
shadowdaoandClaude Sonnet 5 85e2d08f18 ci: fix Windows step shell -- use powershell, not pwsh
Build / macOS (macos-latest) (push) Successful in 36s
Build / Linux (ubuntu-24.04) (push) Successful in 1m1s
Build / Windows (windows-latest) (push) Failing after 9m20s
Real verification push (2b216d3) caught this: explicitly setting
shell: pwsh made the Windows job fail immediately with "Cannot find:
pwsh in PATH". winvm-builder only has the in-box Windows PowerShell 5.1
(powershell.exe), not PowerShell Core -- which is exactly why the original
inline build.yml steps never set `shell:` at all and just relied on the
implicit default. Switched both workflows' Windows steps to
`shell: powershell` explicitly instead of leaving it implicit, so future
edits can't reintroduce this by copying a pwsh example from elsewhere.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-07 05:15:35 -07:00
shadowdaoandClaude Sonnet 5 2b216d3d75 ci: add tag-triggered release packaging workflow
Build / macOS (macos-latest) (push) Successful in 33s
Build / Linux (ubuntu-24.04) (push) Successful in 54s
Build / Windows (windows-latest) (push) Failing after 7m50s
Adds .gitea/workflows/release.yml, triggered only on a pushed v* tag, which
builds all three platforms (reusing the exact same configure/build/test
commands as build.yml, now factored out into .gitea/scripts/ so the two
workflows can't drift), zips each platform's build/package/ output, and
creates a draft Gitea Release with the archives attached.

This is packaging automation only -- it does not resolve or bypass the C1
WebRTC/OpenH264 release gate documented in README.md's Status section.
Nothing publishes until a human deliberately pushes a version tag (which
should not happen before owner sign-off) and then explicitly publishes the
resulting draft. The generated release notes lead with a restatement of the
open C1 question specifically so that second step can't be taken by
accident.

build.yml is refactored (not rewritten) to call the same shared scripts;
its job/step behavior is otherwise unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-07 05:06:25 -07:00
shadowdaoandClaude Sonnet 5 79de5e8f23 Fix Windows find_package(libobs) and make CI hard-fail when the OBS module is missing
Build / macOS (macos-latest) (push) Successful in 32s
Build / Linux (ubuntu-24.04) (push) Successful in 46s
Build / Windows (windows-latest) (push) Failing after 9m20s
Root cause of the find_package(libobs) failure on Windows: OBS's own
modern-CMake Windows layout (obs-studio/cmake/windows/defaults.cmake sets
OBS_CMAKE_DESTINATION=cmake) installs libobs's CMake package config to
<prefix>/cmake/libobs/ -- a shape find_package(libobs CONFIG) never
searches under CMAKE_PREFIX_PATH. Verified empirically with
--debug-find-pkg=libobs: CMake's Config-mode search suffixes try
<prefix>/cmake/libobsConfig.cmake (no <name> subdirectory) and
<prefix>/libobs*/cmake/... (a <name>-prefixed dir first), never
<prefix>/cmake/<name>*/. This has nothing to do with the earlier "file
INSTALL cannot find obs-frontend-api.dll" error the bootstrap already
tolerates -- libobs is the first subdirectory obs-studio's modern
top-level CMakeLists.txt adds (well before UI), so libobs's own
install(EXPORT ...) rules already completed by the time that later,
unrelated install error aborts the script. macOS is unaffected
(OBS_CMAKE_DESTINATION=lib/cmake there, matching the standard
<prefix>/lib*/cmake/<name>*/ suffix), and so is Linux's libobs-dev
(/usr/lib/<arch>/cmake/libobs/, same standard suffix).

Fix: point libobs_DIR directly at the from-source Windows install when it
exists, bypassing find_package's path-search heuristics entirely. Also
fixed a secondary bug found while tracing this: the existing WIN32
locationless-OBS::libobs repair looked for obs.dll under
"<deps>/bin" instead of the actual OBS_EXECUTABLE_DESTINATION,
"<deps>/bin/64bit".

Also make the Windows and macOS "Show what was built" CI steps hard-fail
when the OBS adapter module is missing, instead of only printing a
message. Several recent "green" Windows runs silently shipped a
core-library-only build because of the bug above; nothing in CI caught
it, only a human reading the raw log by hand. Left Linux untouched (its
check is already a hard, non-"|| true" verification). Applied the same
hard-fail treatment to macOS: its bootstrap has been reliably building
the real module in CI (6/6 tests, per README), so there's no longer a
known legitimate reason for a silent core-only fallback there either --
the documented macOS packaging/bundle-loadability gap is a separate,
already-visible issue this check doesn't touch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-07 04:53:03 -07:00
shadowdaoandClaude Sonnet 5 969b8db94a license: relicense first-party code from GPL-2.0-or-later to Apache-2.0
Build / macOS (macos-latest) (push) Successful in 33s
Build / Linux (ubuntu-24.04) (push) Successful in 54s
Build / Windows (windows-latest) (push) Successful in 12m10s
Owner sign-off: replace root LICENSE with Apache License 2.0, add a root
NOTICE file, and swap the GPL-2.0 boilerplate header in every first-party
core/ and obs-adapter/ source file for a short Apache-2.0 notice.

This resolves review finding C2 (GPLv2 top-level LICENSE vs. the vendored
Apache-2.0 LiveKit SDK is a license-compatibility violation): the whole
repo is now Apache-2.0, matching LiveKit, so there's no GPL/Apache clash
left. Updated the README Status gate and the CI workflow comment to reflect
that C2 is resolved, while leaving the C1 WebRTC/OpenH264 patent/royalty
gate untouched -- that question is still open and still blocks release.

third_party/ stays under its own upstream licenses; only this project's own
code changed hands. All 6 CTest suites still pass after the header swap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-07 04:44:16 -07:00
shadowdaoandClaude Sonnet 5 c4db99414d ci: copy LiveKit runtime DLLs next to Windows test executables
Build / macOS (macos-latest) (push) Successful in 35s
Build / Linux (ubuntu-24.04) (push) Successful in 48s
Build / Windows (windows-latest) (push) Successful in 12m5s
Windows CI now gets through CMake configure/build (the w32-pthreads
bootstrap patch landed), but CTest immediately failed 3/6 suites with
exit 0xc0000135 (STATUS_DLL_NOT_FOUND): test_session,
test_integration_livekit, test_livekit_smoke.

stplugin_core links LiveKit::livekit PUBLICly (core/CMakeLists.txt), so
every test executable under core/tests depends on livekit.dll /
livekit_ffi.dll at runtime. Unlike the $ORIGIN/@loader_path RPATH
handling obs-adapter/CMakeLists.txt already sets up for Linux/macOS,
Windows has no relative-to-the-exe DLL search path -- the DLLs must
physically sit next to the .exe (or be on PATH) when the process starts,
or the loader fails before main() runs. core/tests/CMakeLists.txt had no
equivalent staging step at all.

Added a POST_BUILD copy_if_different in stplugin_add_test(), guarded by
WIN32, that copies the same LIVEKIT_SDK_RUNTIME_LIBS list
(cmake/LiveKitSDK.cmake, already resolves to *.dll on Windows) into
$<TARGET_FILE_DIR:...> for each test binary -- mirroring the pattern
obs-adapter/CMakeLists.txt already uses for its own staged package.
Applied to all six tests rather than only the three known to reference
LiveKit symbols today: harmless for the other three, and avoids
re-diagnosing this if a future test starts touching stplugin_core's
LiveKit-dependent code paths.

Verified locally on Linux (WIN32 branch inert there, but confirms the
rest of the configure/build/test cycle is untouched):
  cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DSTPLUGIN_BOOTSTRAP_OBS=OFF
  cmake --build build
  ctest --test-dir build --output-on-failure   # 6/6 passed

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-07 00:57:36 -07:00
shadowdaoandClaude Sonnet 5 a3579d4fd5 docs: bash-on-Windows is proven broken (WSL as local system), with the log
Build / macOS (macos-latest) (push) Successful in 32s
Build / Linux (ubuntu-24.04) (push) Successful in 54s
Build / Windows (windows-latest) (push) Failing after 11m21s
The 58f4832 Windows run completed and failed, but not on the bootstrap: its
Windows steps still used `shell: bash`, and on winvm-builder bash resolves to
WSL, which refuses to run under the runner's service account:

  Running WSL as local system is not supported.
  Error code: Bash/WSL_E_LOCAL_SYSTEM_NOT_SUPPORTED
  ##[error]Process completed with exit code 1.

The step died on the shell without ever invoking cmake. So the PowerShell
rewrite in 524e78c -- which was written pre-emptively from reading the
workflow back, on the reasoning that bash "cannot be assumed present" -- is
now confirmed necessary by a real log rather than by argument. It is still
unproven in the other direction: no completed run has yet included it.

Equally important for reading the record correctly: this failure says nothing
about the OBS::w32-pthreads blocker. That result stands on edb0c02 alone, and
the README now says so explicitly so a later reader does not count 58f4832 as
corroboration.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
2026-09-06 23:12:57 -07:00
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