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
jknapp 1571d7ecea Merge pull request 'docs: the Windows plugin dir is ProgramData, not the config dir' (#2) from docs/windows-plugin-path into main
Build / macOS (macos-latest) (push) Successful in 29s
Build / Linux (ubuntu-24.04) (push) Successful in 45s
Build / Windows (windows-latest) (push) Successful in 4m3s
2026-09-09 23:44:20 +00: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
9 changed files with 312 additions and 171 deletions
+15 -61
View File
@@ -1,15 +1,9 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Creates a (draft) Gitea Release for the tag that triggered # Creates a (draft) Gitea Release for the tag that triggered
# .gitea/workflows/release.yml, and uploads every archive in $DIST_DIR as a # .gitea/workflows/release.yml, and uploads every archive in $DIST_DIR as a
# release asset. # release asset. Draft because nobody has run this plugin in the OBS GUI on
# # any platform yet -- a human still opens it and clicks Publish once that's
# RELEASE GATE: see the README's `## Status` section and # no longer true (or once they're satisfied regardless).
# third_party/livekit/README.md. The WebRTC/OpenH264 attribution question
# ("C1") is unresolved -- this script does not decide that question, it just
# makes sure the generated release notes put the reminder where whoever
# publishes the draft will actually read it. Pushing a version tag is the
# human decision this whole workflow hangs off of; this script does not add
# or remove any judgment about whether that decision was the right one.
# #
# Required env: GITEA_TOKEN, SERVER, OWNER, REPO, TAG, SHA, DIST_DIR # Required env: GITEA_TOKEN, SERVER, OWNER, REPO, TAG, SHA, DIST_DIR
# Optional env: MACOS_BUNDLE_FOUND ("true"/"false", default "false") # Optional env: MACOS_BUNDLE_FOUND ("true"/"false", default "false")
@@ -27,27 +21,11 @@ MACOS_BUNDLE_FOUND="${MACOS_BUNDLE_FOUND:-false}"
if [ "${MACOS_BUNDLE_FOUND}" = "true" ]; then if [ "${MACOS_BUNDLE_FOUND}" = "true" ]; then
MACOS_NOTE="This archive contains a \`.plugin\` bundle." MACOS_NOTE="This archive contains a \`.plugin\` bundle."
else else
MACOS_NOTE="This archive is packaged as a bare \`streamer-tools-camera.so\` (the layout \`build/package/\` currently produces on macOS), **not** an OBS.app-loadable \`.plugin\` bundle. It will not load in the OBS GUI as-is." MACOS_NOTE="This archive is packaged as a bare \`streamer-tools-camera.so\` (the layout \`build/package/\` currently produces on macOS), **not** an OBS.app-loadable \`.plugin\` bundle. It will not load in the OBS GUI as-is -- see the \"macOS packaging gap\" section of \`README.md\`."
fi fi
NOTES_FILE="$(mktemp)" NOTES_FILE="$(mktemp)"
cat > "${NOTES_FILE}" <<EOF cat > "${NOTES_FILE}" <<EOF
> **This build has not been cleared for redistribution.** The plugin
> statically/dynamically pulls in Google WebRTC and OpenH264 code through the
> LiveKit SDK, and whether that can be redistributed as a public download --
> the "C1" attribution/patent question -- has not been resolved. See the
> \`## Status\` section of \`README.md\` and \`third_party/livekit/README.md\`
> for the specifics. By publishing this release, you are personally taking on
> that open question -- if C1 hasn't been signed off on, don't publish it.
>
> (The separate GPLv2/Apache-2.0 license-compatibility question, "C2", is
> resolved: this project's own first-party code is Apache-2.0, matching the
> vendored LiveKit binaries.)
>
> This release was created as a **draft**. It stays invisible to anyone
> without write access to this repo until someone with write access opens it
> here and clicks Publish -- a second, deliberate step past pushing the tag.
# streamer-tools Camera Plugin -- ${TAG} # streamer-tools Camera Plugin -- ${TAG}
Built from commit \`${SHA}\`. Built from commit \`${SHA}\`.
@@ -55,49 +33,25 @@ Built from commit \`${SHA}\`.
**Nobody has yet run this plugin in the OBS GUI, on any platform.** See "What **Nobody has yet run this plugin in the OBS GUI, on any platform.** See "What
is verified, and how" in \`README.md\` for exactly what has and has not been is verified, and how" in \`README.md\` for exactly what has and has not been
checked, including which claims are backed by automated tests versus a human checked, including which claims are backed by automated tests versus a human
watching OBS. watching OBS. This is why the release is a draft -- open it and click Publish
once you're satisfied.
| Platform | Archive | Notes | | Platform | Archive | Notes |
|---|---|---| |---|---|---|
| Linux (x64) | \`streamer-tools-camera-${TAG}-linux-x64.zip\` | Functionally complete and verified end to end against a real LiveKit server and a real libobs (see README); OBS GUI itself still unverified | | Linux (x64) | \`streamer-tools-camera-${TAG}-linux-x64.zip\` | Functionally complete and verified end to end against a real LiveKit server and a real libobs (see README); OBS GUI itself still unverified |
| Windows (x64) | \`streamer-tools-camera-${TAG}-windows-x64.zip\` | Built and tested by this workflow's Windows job; the WinHTTP backend has never been exercised against a real streamer-tools server, only a loopback test server -- see README's Windows CI section | | Windows (x64) | \`streamer-tools-camera-${TAG}-windows-x64.zip\` | Built and tested by this workflow's Windows job; the WinHTTP backend has never been exercised against a real streamer-tools server, only a loopback test server -- see README's Windows CI section |
| macOS | \`streamer-tools-camera-${TAG}-macos.zip\` | Built and tested by this workflow's macOS job. ${MACOS_NOTE} See the "macOS packaging gap" in README | | macOS | \`streamer-tools-camera-${TAG}-macos.zip\` | Built and tested by this workflow's macOS job. ${MACOS_NOTE} |
## Installing ## Installing
### Linux Extract the archive into your OBS plugins folder for your platform (the
default locations are easy to find online -- typically
\`\`\` \`~/.config/obs-studio/plugins/\` on Linux, \`%APPDATA%\\obs-studio\\plugins\\\`
mkdir -p ~/.config/obs-studio/plugins/streamer-tools-camera on Windows, \`~/Library/Application Support/obs-studio/plugins/\` on macOS).
unzip streamer-tools-camera-${TAG}-linux-x64.zip -d /tmp/stplugin-camera Each archive's top-level folder already matches the shape OBS expects there,
cp -r /tmp/stplugin-camera/bin /tmp/stplugin-camera/data \\ so extracting is the whole install step. Then in OBS: Sources -> \`+\` ->
~/.config/obs-studio/plugins/streamer-tools-camera/ "streamer-tools Camera" -> fill in the server URL, room slug and read key
\`\`\` from the room's settings page -> "Refresh camera list" -> pick a camera.
Start OBS, then Sources -> \`+\` -> "streamer-tools Camera" -> fill in the
server URL, room slug and read key from the room's settings page ->
"Refresh camera list" -> pick a camera. This is the same drop-in layout
README's "Testing this by hand" documents for a source build, adapted for a
downloaded zip -- known-good on Linux.
### Windows (installation path not yet verified in real OBS)
Per \`AddExtraModulePaths()\` in obs-studio's \`UI/window-basic-main.cpp\`, OBS
on Windows searches a plugins directory for \`bin\\64bit\\<name>.dll\` plus a
sibling \`data\\\`. Unzip the archive and copy its \`bin\\\` and \`data\\\` into
your OBS plugins directory (typically
\`%APPDATA%\\obs-studio\\plugins\\streamer-tools-camera\\\`), matching the
Linux layout above. This has not been confirmed against a real OBS install on
Windows -- report back if you try it.
### macOS (installation path not yet verified in real OBS; packaging gap)
OBS on macOS loads plugins as \`<name>.plugin\` bundles under
\`~/Library/Application Support/obs-studio/plugins/\`. As of this release,
this project's \`build/package/\` output on macOS is **not yet that bundle
shape** -- see the "macOS packaging gap" section of \`README.md\`. Treat the
macOS archive here as a build-verification artifact, not a working
drop-in, until that gap is closed.
## What this is ## What this is
+83 -18
View File
@@ -15,22 +15,22 @@ name: Build
# .gitea/workflows/release.yml, so the two workflows can't drift apart -- # .gitea/workflows/release.yml, so the two workflows can't drift apart --
# edit the scripts, not either workflow, to change how a platform builds. # edit the scripts, not either workflow, to change how a platform builds.
# #
# RELEASE GATE: this workflow only builds, tests, and uploads CI-internal # This workflow only builds, tests, and uploads CI-internal workflow
# workflow artifacts (actions/upload-artifact, below) -- it does not create a # artifacts (actions/upload-artifact, below) -- it does not create a Gitea
# Gitea Release, push a tag-triggered publish, or otherwise distribute # Release. .gitea/workflows/release.yml is that publish step, gated on a
# binaries publicly, and it must not start doing so without explicit owner # pushed version tag rather than on every push.
# sign-off on the WebRTC/OpenH264 attribution question tracked in
# third_party/livekit/README.md and the README's top-level Status section.
# (The separate GPLv2/Apache-2.0 license-compatibility question is resolved:
# this project's own code is Apache-2.0.) If a real release/publish step is
# ever added here, it must carry that same gate.
#
# (.gitea/workflows/release.yml is that publish step, gated on a pushed
# version tag rather than on every push -- see the gate reminder baked into
# its generated release notes.)
on: on:
push: push:
# Excludes tag pushes -- a bare `push:` matches every ref push, tags
# included, which meant tagging a release triggered THIS workflow's full
# 3-platform build (Windows and all) at the same time as
# release.yml's own -- two full Windows builds serialized behind the
# runner's capacity:1, for one tag push. release.yml already covers
# exactly this build (plus packaging) on every `v*` tag; this workflow's
# job is ordinary commits.
branches:
- "**"
pull_request: pull_request:
jobs: jobs:
@@ -80,11 +80,30 @@ jobs:
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: .deps path: .deps
key: obs-deps-${{ runner.os }}-${{ hashFiles('cmake/macos/buildspec.cmake', 'cmake/common/buildspec_common.cmake', 'buildspec.json') }} key: obs-deps-v2-${{ runner.os }}-${{ hashFiles('cmake/macos/buildspec.cmake', 'cmake/common/buildspec_common.cmake', 'buildspec.json') }}
- name: Configure, build, test, verify - name: Configure, build, test, verify
run: .gitea/scripts/macos-build.sh run: .gitea/scripts/macos-build.sh
- name: Drop non-relocatable OBS build tree before caching
# cmake/common/buildspec_common.cmake's obs-studio sub-build writes
# an out-of-source CMakeCache.txt (.deps/obs-studio-*/build_*) that
# bakes in this job's absolute checkout path. The next run's
# checkout lands at a *different* absolute path, so restoring that
# directory from the cache above makes CMake refuse to reconfigure
# it ("CMakeCache.txt directory ... is different than the directory
# ... where CMakeCache.txt was created"). Everything that actually
# needs to survive between runs -- the extracted source, and the
# already-installed libobs package under .deps/cmake, .deps/include,
# .deps/lib -- has no such path baked in and is unaffected. Delete
# only the intermediate build tree, after it has already done its
# job (libobs is built and installed by this point), so the cache
# saved at the end of this job contains nothing that requires the
# path it was created under.
if: always()
continue-on-error: true
run: rm -rf .deps/obs-studio-*/build_*
- name: Upload plugin - name: Upload plugin
continue-on-error: true continue-on-error: true
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
@@ -99,21 +118,51 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install build dependencies - name: Verify build dependencies
# winvm-builder is a self-hosted act_runner labeled "windows-latest"; # winvm-builder is a self-hosted act_runner labeled "windows-latest";
# it is NOT the GitHub-hosted image, so none of that image's # it is NOT the GitHub-hosted image, so none of that image's
# preinstalled tooling (cmake included) can be assumed present. # preinstalled tooling can be assumed present. This used to be
uses: lukka/get-cmake@latest # `uses: lukka/get-cmake@latest`, which re-downloaded and
# re-extracted CMake + Ninja on every single run -- its own cache
# (routed through this act_runner's cache server) reported a "cloud
# cache miss" on every run even immediately after a successful save,
# and separately the extraction step alone measured ~7.5 minutes on
# this VM (consistent with Defender real-time scanning, not raw I/O)
# -- together the dominant cost of every Windows CI run. CMake and
# Ninja are now installed once, directly on winvm-builder's system
# PATH (C:\BuildTools\cmake\bin, C:\BuildTools\ninja -- see the
# README's "Windows runner: persistent build tools" section for
# exactly what that machine has installed and how to redo it if the
# VM is ever rebuilt). This step just fails loudly if that ever
# stops being true, rather than silently falling back to a slow
# re-download.
shell: powershell
run: |
$ErrorActionPreference = "Stop"
cmake --version
ninja --version
- name: Cache OBS SDK bootstrap deps - name: Cache OBS SDK bootstrap deps
# See the matching step in the macOS job above for why this is # See the matching step in the macOS job above for why this is
# needed: cmake/windows/buildspec.cmake's own download logic is # needed: cmake/windows/buildspec.cmake's own download logic is
# already idempotent, it just never gets the chance because .deps/ # already idempotent, it just never gets the chance because .deps/
# lives inside the checkout and is wiped by every fresh clone. # lives inside the checkout and is wiped by every fresh clone.
#
# The `v2` in the key: actions/cache never overwrites an existing
# key -- once a key has a saved entry, every later job's save step is
# skipped as a no-op, cache hit or not. The very first job ever
# to populate this cache did so BEFORE the "Drop non-relocatable OBS
# build tree" step below existed, so its save included the bad
# build_x86 directory -- and because saves under an existing key are
# permanently skipped, every run after that kept restoring that same
# bad entry forever, not "one more transitional run" as it looked at
# the time. Bumping the key is what actually forces a fresh save;
# bump it again (v3, ...) if this cache is ever found to be stale in
# a way a workflow change alone can't fix.
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: .deps path: .deps
key: obs-deps-${{ runner.os }}-${{ hashFiles('cmake/windows/buildspec.cmake', 'cmake/common/buildspec_common.cmake', 'buildspec.json') }} key: obs-deps-v2-${{ runner.os }}-${{ hashFiles('cmake/windows/buildspec.cmake', 'cmake/common/buildspec_common.cmake', 'buildspec.json') }}
- name: Configure, build, test, verify - name: Configure, build, test, verify
# Windows PowerShell (powershell.exe), not PowerShell Core (pwsh) -- # Windows PowerShell (powershell.exe), not PowerShell Core (pwsh) --
@@ -123,6 +172,22 @@ jobs:
shell: powershell shell: powershell
run: ./.gitea/scripts/windows-build.ps1 run: ./.gitea/scripts/windows-build.ps1
- name: Drop non-relocatable OBS build tree before caching
# See the matching step in the macOS job above. Confirmed live on
# this runner: caching .deps/obs-studio-30.0.2/build_x86 as-is made
# every run's first configure attempt fail with a path mismatch
# against the job that populated the cache, falling back to
# -DSTPLUGIN_BOOTSTRAP_OBS=OFF and only succeeding because the
# already-installed libobs package (path-independent) was still
# found. That fallback masked the problem behind a misleading
# "::warning::OBS SDK bootstrap failed" every run instead of fixing
# it. Deleting the build tree here, after libobs is already built
# and installed, is the actual fix.
if: always()
continue-on-error: true
shell: powershell
run: Remove-Item -Recurse -Force .deps\obs-studio-*\build_* -ErrorAction SilentlyContinue
- name: Upload plugin - name: Upload plugin
continue-on-error: true continue-on-error: true
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
+57 -29
View File
@@ -4,24 +4,11 @@ name: Release
# a (draft) Gitea Release for it, so the project owner and other directors # a (draft) Gitea Release for it, so the project owner and other directors
# can grab a ready-to-use build instead of compiling from source. # can grab a ready-to-use build instead of compiling from source.
# #
# RELEASE GATE -- READ BEFORE TAGGING # Runs only on a pushed version tag (see `on.push.tags` below) -- never on an
# ------------------------------------------------------------------ # ordinary push or PR, unlike build.yml. The release it creates is a DRAFT:
# This workflow runs ONLY on a pushed version tag (see `on.push.tags` below) # it stays invisible to anyone without write access until a human explicitly
# -- it never runs on an ordinary push or PR, unlike build.yml. Pushing a # opens it and clicks Publish, since nobody has run this plugin in the OBS
# tag is therefore the one deliberate human act that starts it, and the # GUI on any platform yet.
# release it creates is a DRAFT: it stays invisible to anyone without write
# access until a human explicitly opens it and clicks Publish. That is a
# second deliberate act past the tag push.
#
# Both of those are process, not a legal opinion. The actual open question --
# whether this plugin's bundled WebRTC/OpenH264 code (via LiveKit) can be
# redistributed as a public download at all -- is tracked as "C1" in the
# README's `## Status` section and in third_party/livekit/README.md, and it
# is NOT resolved. Nothing here resolves it; the generated release notes put
# a reminder of that fact at the top of every release this workflow creates,
# specifically so nobody publishes a draft without seeing it again first.
# (The separate GPLv2/Apache-2.0 question, "C2", *is* resolved -- see
# README.)
# #
# The actual per-platform build commands live in .gitea/scripts/ and are the # The actual per-platform build commands live in .gitea/scripts/ and are the
# same scripts .gitea/workflows/build.yml uses, so this workflow can't drift # same scripts .gitea/workflows/build.yml uses, so this workflow can't drift
@@ -55,7 +42,19 @@ jobs:
command -v zip >/dev/null || sudo apt-get install -y -qq zip command -v zip >/dev/null || sudo apt-get install -y -qq zip
out="streamer-tools-camera-${GITEA_REF_NAME}-linux-x64.zip" out="streamer-tools-camera-${GITEA_REF_NAME}-linux-x64.zip"
root="$(pwd)" root="$(pwd)"
( cd build/package && zip -r "${root}/${out}" . )
# Wrap build/package/'s bin/+data/ inside a top-level
# streamer-tools-camera/ directory, matching the plugin directory
# name OBS itself expects under <config>/obs-studio/plugins/ (see
# obs-adapter/CMakeLists.txt's staging comment). This makes the
# archive a straight `unzip -d ~/.config/obs-studio/plugins/`
# drop-in -- no manual `cp -r bin data` step required.
stage="$(mktemp -d)"
mkdir -p "${stage}/streamer-tools-camera"
cp -r build/package/. "${stage}/streamer-tools-camera/"
( cd "${stage}" && zip -r "${root}/${out}" streamer-tools-camera )
rm -rf "${stage}"
mkdir -p dist mkdir -p dist
mv "${out}" "dist/${out}" mv "${out}" "dist/${out}"
ls -la dist ls -la dist
@@ -91,13 +90,18 @@ jobs:
root="$(pwd)" root="$(pwd)"
mkdir -p dist mkdir -p dist
# macOS packaging is being fixed separately (see the "macOS # Look for a *.plugin bundle rather than assuming its exact final
# packaging gap" in README.md). Once it lands, build/package/ (or # location, falling back to packaging build/package/ as-is (a
# wherever that work stages its output) should contain a # bare .so, not a loadable bundle) only if the bundle step didn't
# `<name>.plugin` bundle directory -- look for one rather than # run or produced nothing -- see the macOS packaging gap in
# assuming its exact final location, and fall back to packaging # README.md for when that fallback path is actually live. The
# build/package/ as-is (today's actual, non-bundle output) if none # bundle itself is zipped at the archive's top level (cd into its
# is found yet. # parent, zip just the bundle dir) so the archive is already a
# straight `unzip -d ~/Library/Application\ Support/obs-studio/
# plugins/` drop-in -- no wrapping needed here, unlike
# Linux/Windows above, because OBS wants the whole *.plugin
# bundle directly under plugins/, not nested under a named
# subdirectory.
bundle="$(find build -maxdepth 4 -type d -name '*.plugin' 2>/dev/null | head -n1 || true)" bundle="$(find build -maxdepth 4 -type d -name '*.plugin' 2>/dev/null | head -n1 || true)"
if [ -n "${bundle}" ]; then if [ -n "${bundle}" ]; then
echo "Found macOS .plugin bundle: ${bundle}" echo "Found macOS .plugin bundle: ${bundle}"
@@ -127,8 +131,16 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install build dependencies - name: Verify build dependencies
uses: lukka/get-cmake@latest # See build.yml's Windows job for why this is no longer
# lukka/get-cmake@latest -- CMake and Ninja are installed once,
# directly on winvm-builder's system PATH; this just fails loudly
# if that ever stops being true.
shell: powershell
run: |
$ErrorActionPreference = "Stop"
cmake --version
ninja --version
- name: Configure, build, test, verify - name: Configure, build, test, verify
# Windows PowerShell (powershell.exe), not PowerShell Core (pwsh) -- # Windows PowerShell (powershell.exe), not PowerShell Core (pwsh) --
@@ -143,7 +155,23 @@ jobs:
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
$out = "streamer-tools-camera-$env:GITEA_REF_NAME-windows-x64.zip" $out = "streamer-tools-camera-$env:GITEA_REF_NAME-windows-x64.zip"
New-Item -ItemType Directory -Force -Path dist | Out-Null New-Item -ItemType Directory -Force -Path dist | Out-Null
Compress-Archive -Path build\package\* -DestinationPath "dist\$out" -Force
# Wrap build\package\'s bin\+data\ inside a top-level
# streamer-tools-camera\ directory, matching the plugin directory
# name OBS itself expects under %APPDATA%\obs-studio\plugins\ (see
# obs-adapter/CMakeLists.txt's staging comment). This makes the
# archive a straight `Expand-Archive -DestinationPath
# $env:APPDATA\obs-studio\plugins\` drop-in -- no manual copy step
# required. Compress-Archive includes the source folder's own name
# as the archive root when given a single directory path, so
# staging under a streamer-tools-camera\ dir is enough on its own.
$stage = Join-Path $env:TEMP "stplugin-stage-$([guid]::NewGuid())"
$pluginDir = Join-Path $stage "streamer-tools-camera"
New-Item -ItemType Directory -Force -Path $pluginDir | Out-Null
Copy-Item -Path build\package\* -Destination $pluginDir -Recurse
Compress-Archive -Path $pluginDir -DestinationPath "dist\$out" -Force
Remove-Item -Recurse -Force $stage
Get-ChildItem dist Get-ChildItem dist
env: env:
GITEA_REF_NAME: ${{ github.ref_name }} GITEA_REF_NAME: ${{ github.ref_name }}
+79 -35
View File
@@ -8,43 +8,36 @@ Media-Source path for directors. Full design:
## Status ## Status
**Release/distribution of built binaries is blocked pending owner sign-off.** This project's own code is Apache-2.0 (relicensed from GPL-2.0-or-later to
This plugin statically/dynamically pulls in Google WebRTC and OpenH264 code match the vendored LiveKit binaries, which are also Apache-2.0 — see
through the LiveKit SDK — a real patent/royalty question (OpenH264/WebRTC) `LICENSE` and `NOTICE`, and `third_party/livekit/` for LiveKit's own).
that only the project owner can decide. Nothing in this repo should be built
into a package and handed out, posted, or attached to a public release until
that sign-off happens. See `third_party/livekit/README.md` for the specifics
of what is and is not currently known/shipped on the licensing side. (CI in
`.gitea/workflows/build.yml` only builds, tests, and uploads CI-internal
build artifacts — it does not create a Gitea Release or otherwise publish
anything publicly.
`.gitea/workflows/release.yml` is the mechanism that *would* publish a `.gitea/workflows/build.yml` builds, tests, and uploads CI-internal build
release, but it does not run automatically: it is gated on someone pushing a artifacts on every push. `.gitea/workflows/release.yml` packages a tagged
`v*` tag, which is the actual sign-off gate in practice — don't push one build (`v*`) into a **draft** Gitea Release; a human still needs to open it
until the owner has actually signed off on C1. When it does run, it packages and click Publish.
each platform's `build/package/` (or macOS's bundle output, once that lands)
into a zip and creates a **draft** Gitea Release, whose generated release
notes lead with the same C1 reminder as this section, so whoever opens the
draft to publish it sees the open question again before doing so. Building
that mechanism is not the same as clearing C1 — it still requires the same
owner sign-off before a tag gets pushed.)
The separate license-compatibility question — this repository's own top-level
`LICENSE` was GPLv2 while the vendored LiveKit binaries are Apache-2.0, which
are incompatible — is **resolved**: the project owner has relicensed this
project's own first-party code to Apache-2.0, matching LiveKit. Everything in
this repo is now Apache-2.0, so there is no remaining GPL/Apache
incompatibility.
The plugin is **functionally complete on Linux and verified end to end there** The plugin is **functionally complete on Linux and verified end to end there**
(module loads into real libobs, connects to a real LiveKit server through the (module loads into real libobs, connects to a real LiveKit server through the
real streamer-tools API shape, and pushes decoded frames into real streamer-tools API shape, and pushes decoded frames into
`obs_source_output_video`/`_audio`). `obs_source_output_video`/`_audio`).
It has **not been run in the OBS GUI on any platform.** macOS builds the real **First confirmed OBS GUI load: Windows, 2026-09-09** — the v0.1.0 release
module in CI but its artifact is not yet loadable (see the macOS packaging gap artifact loaded into OBS 32.2.2 on Windows 11 (build 26200) on a director's
under CI). machine, from
`C:\ProgramData\obs-studio\plugins\streamer-tools-camera\bin\64bit\`.
That retires "the module will not even load in a real OBS" for Windows. It
does **not** yet cover whether video renders correctly, colours, A/V sync or
latency — see "Not verified anywhere" below for what is still open. Linux and
macOS have still never been opened in the GUI; macOS builds the real module in
CI but its artifact is not yet loadable (see the macOS packaging gap under CI).
⚠️ **The install directory is not the same on every platform, and getting it
wrong fails silently.** On Windows it is
`C:\ProgramData\obs-studio\plugins\` (`GetProgramDataPath`
`CSIDL_COMMON_APPDATA`), **not** `%APPDATA%\obs-studio\` — see the packaging
section. That mistake cost the director above an evening: OBS logs nothing at
all for a plugin it never finds.
**Windows CI is now green.** The run at `f27b1c0` is the first completed **Windows CI is now green.** The run at `f27b1c0` is the first completed
green Windows job on this repository: the from-source libobs bootstrap green Windows job on this repository: the from-source libobs bootstrap
@@ -54,8 +47,8 @@ suites pass, and `build\package\bin\64bit\streamer-tools-camera.dll`
out of the job's own log body, not inferred from the job status. That also out of the job's own log body, not inferred from the job status. That also
retires three previously-unproven items in one go: the `-A x64` argument fix, retires three previously-unproven items in one go: the `-A x64` argument fix,
the PowerShell rewrite of the Windows steps, and the `add_subdirectory` the PowerShell rewrite of the Windows steps, and the `add_subdirectory`
patch for `OBS::w32-pthreads`. Windows is still **unverified in the OBS GUI**, patch for `OBS::w32-pthreads`. Windows has since been **loaded in the real OBS
exactly like the other two platforms. See "Where the Windows bootstrap got GUI** (see above); Linux and macOS have not. See "Where the Windows bootstrap got
to" under CI below for the whole trace, and check current CI status rather to" under CI below for the whole trace, and check current CI status rather
than trusting this paragraph's age. than trusting this paragraph's age.
@@ -179,8 +172,10 @@ macOS packaging gap under CI.
## Testing this by hand ## Testing this by hand
**Nobody has yet run this in the OBS GUI. That test is still outstanding on **The module has been loaded in the OBS GUI on Windows once (2026-09-09, OBS
all three platforms.** To do it on Linux: 32.2.2 / Windows 11 26200) — nothing beyond "it loads and registers its source"
is confirmed there, and Linux and macOS have never been opened in the GUI at
all.** To do it on Linux:
``` ```
mkdir -p ~/.config/obs-studio/plugins/streamer-tools-camera mkdir -p ~/.config/obs-studio/plugins/streamer-tools-camera
@@ -236,7 +231,11 @@ livekit-server 1.13.6 in dev mode):
| Two sources in one OBS process | same harness with a second source added: both connect with distinct nonce identities, both receive frames, both tear down cleanly | | Two sources in one OBS process | same harness with a second source added: both connect with distinct nonce identities, both receive frames, both tear down cleanly |
**Not verified anywhere:** **Not verified anywhere:**
- The OBS GUI, on any platform. No human has looked at this in OBS. - Anything past module load in the OBS GUI. Windows 2026-09-09 confirms the
module loads and its source type appears; whether video actually renders
(right way up, right colours), what the A/V sync and latency look like, and
whether a publisher restarting mid-show recovers on screen are all still
unanswered. Linux and macOS have not been opened in the GUI at all.
- macOS beyond "CI builds and links the real module and the core tests pass". - macOS beyond "CI builds and links the real module and the core tests pass".
Its artifact is a bare `.so` with a relative libobs install name and will Its artifact is a bare `.so` with a relative libobs install name and will
not load in OBS.app — see the macOS packaging gap under CI. not load in OBS.app — see the macOS packaging gap under CI.
@@ -284,6 +283,51 @@ and a permanently red CI teaches people to ignore CI. **Do not remove the
warning:** a green job that quietly stopped building the plugin is worse than warning:** a green job that quietly stopped building the plugin is worse than
a red one. a red one.
### Windows runner: persistent build tools (2026-09-07)
`winvm-builder`'s Windows job used to install its own CMake + Ninja on every
single run via `uses: lukka/get-cmake@latest`. That action has its own
caching (routed through this act_runner's built-in cache server, the same
mechanism `.deps/`'s `actions/cache` step above relies on and that one does
work) but it never hit: every run logged `Cloud cache miss` against the same
cache key, even immediately after a run that logged a successful save under
that exact key -- some incompatibility between `lukka/get-cmake`'s bundled
cache client and this act_runner's cache-server implementation, not
"caching isn't configured." Separately, and the larger cost: the archive
extraction step alone measured **~7.5 minutes** for a 45MB zip on this VM
(13:11:14 to 13:18:48 in one captured run) -- consistent with Windows
Defender real-time-scanning every extracted file, not raw disk I/O, though
that specific cause is not confirmed. Together this was the dominant cost of
every Windows CI run, cold cache or not.
Fix: CMake 4.4.2 and Ninja 1.12.1 are now installed once, directly on the
`winvm-builder` VM (Proxmox VMID 110, host pve4/192.168.1.145), not fetched
per-run:
- `C:\BuildTools\cmake\` (from
`cmake-4.4.2-windows-x86_64.zip`, Kitware's GitHub releases) and
`C:\BuildTools\ninja\` (from `ninja-win.zip`, `ninja-build/ninja` v1.12.1
release) — plain `Expand-Archive` drops, nothing installed via an
installer/MSI.
- Both added to the **Machine**-level `PATH`
(`[Environment]::SetEnvironmentVariable('PATH', ..., 'Machine')`, not
`setx`, which silently truncates a `PATH` this long).
- The `GiteaRunner-winvm-builder` scheduled task (`C:\gitea-runner\
gitea-runner.exe daemon`, runs as SYSTEM) was stopped and restarted after
the `PATH` change — a already-running process does not pick up an updated
Machine environment variable, only processes started after the change do,
and every CI job is a child process of this one long-running daemon.
Both workflows' Windows jobs now just run `cmake --version` / `ninja
--version` as a "Verify build dependencies" step and fail loudly if either
is missing, instead of silently falling back to the slow per-run install.
**This is VM state, not something `git clone` reproduces.** If
`winvm-builder` is ever rebuilt or reimaged, redo the three steps above
(download+extract both zips under `C:\BuildTools\`, extend the Machine
`PATH`, restart the scheduled task) before expecting Windows CI to pass
again — there is nothing in this repo that does it automatically.
### Where the macOS bootstrap actually got to ### Where the macOS bootstrap actually got to
Six CI iterations, each fixing a real failure visible in the logs: Six CI iterations, each fixing a real failure visible in the logs:
+7
View File
@@ -43,6 +43,13 @@ struct SessionConfig {
bool subscribe_audio = true; bool subscribe_audio = true;
/// False for an audio-only source (the soundboard, say): the wanted
/// video track is never attached (no AttachVideo command posted), and
/// its publication is explicitly disabled server-side (RemoteTrack-
/// Publication::setEnabled(false)) so the SFU stops sending it at all --
/// not just "decoded and discarded here", genuinely not delivered.
bool subscribe_video = true;
/// How long connect() waits for the room to come up before giving up. /// How long connect() waits for the room to come up before giving up.
int connect_timeout_ms = 15000; int connect_timeout_ms = 15000;
}; };
+48 -2
View File
@@ -218,6 +218,52 @@ struct LiveKitSession::Impl : public livekit::RoomDelegate {
queue_cv.notify_one(); queue_cv.notify_one();
} }
// Handles the wanted video track once matched, shared by onTrackSubscribed
// (a fresh subscription) and attachExistingTracks (one already up when
// this session started watching). Two responsibilities that only make
// sense together, both keyed off the SAME publication:
//
// - subscribe_video: an audio-only source (the soundboard) never wants
// this video at all. Rather than attach it and let the OBS adapter
// discard every decoded frame, disable the publication itself
// (RemoteTrackPublication::setEnabled(false)) so the SFU stops
// sending it -- real bandwidth saved, not just wasted decode.
// - Fixed video quality: LiveKit's default subscriber behaviour lets
// the SFU switch simulcast layers per its own adaptive/bandwidth
// logic, which for a source with no rendered-size hint (this is a
// native C++ subscriber, not a sized <video> element) means the
// received resolution can hop between layers -- observed live as OBS
// source geometry visibly changing size mid-show. Pinning to HIGH
// asks the SFU to always send the top layer, which is what a fixed
// OBS source needs regardless of bandwidth (the plugin has no
// picture-in-picture tier to fall back to the way a browser grid
// view would).
void handleWantedVideoTrack(const std::shared_ptr<livekit::Track> &track,
const std::shared_ptr<livekit::RemoteTrackPublication> &publication)
{
if (!config.subscribe_video) {
if (publication) {
try {
publication->setEnabled(false);
} catch (const std::exception &) {
// Best-effort: worst case this track keeps being
// delivered and decoded, wasting bandwidth -- it is
// still never attached to OBS below.
}
}
return;
}
if (publication) {
try {
publication->setVideoQuality(livekit::VideoQuality::HIGH);
} catch (const std::exception &) {
// Best-effort: worst case this track keeps whatever quality
// it already had, which is the pre-existing behaviour.
}
}
post(CommandType::AttachVideo, track);
}
// --- RoomDelegate ------------------------------------------------------ // --- RoomDelegate ------------------------------------------------------
void onTrackSubscribed(livekit::Room &, const livekit::TrackSubscribedEvent &event) override void onTrackSubscribed(livekit::Room &, const livekit::TrackSubscribedEvent &event) override
@@ -230,7 +276,7 @@ struct LiveKitSession::Impl : public livekit::RoomDelegate {
event.publication ? toMediaSource(event.publication->source()) : MediaSource::Unknown; event.publication ? toMediaSource(event.publication->source()) : MediaSource::Unknown;
if (isWantedVideoTrack(config.participant_identity, identity, kind, source)) if (isWantedVideoTrack(config.participant_identity, identity, kind, source))
post(CommandType::AttachVideo, event.track); handleWantedVideoTrack(event.track, event.publication);
else if (config.subscribe_audio && isWantedAudioTrack(config.participant_identity, identity, kind, source)) else if (config.subscribe_audio && isWantedAudioTrack(config.participant_identity, identity, kind, source))
post(CommandType::AttachAudio, event.track); post(CommandType::AttachAudio, event.track);
} }
@@ -551,7 +597,7 @@ struct LiveKitSession::Impl : public livekit::RoomDelegate {
const MediaKind kind = toMediaKind(track->kind()); const MediaKind kind = toMediaKind(track->kind());
const MediaSource source = toMediaSource(publication->source()); const MediaSource source = toMediaSource(publication->source());
if (isWantedVideoTrack(config.participant_identity, identity, kind, source)) if (isWantedVideoTrack(config.participant_identity, identity, kind, source))
post(CommandType::AttachVideo, track); handleWantedVideoTrack(track, publication);
else if (config.subscribe_audio && isWantedAudioTrack(config.participant_identity, identity, kind, source)) else if (config.subscribe_audio && isWantedAudioTrack(config.participant_identity, identity, kind, source))
post(CommandType::AttachAudio, track); post(CommandType::AttachAudio, track);
} }
+1
View File
@@ -4,6 +4,7 @@ RoomSlug="Room"
ReadKey="Read key" ReadKey="Read key"
Camera="Camera" Camera="Camera"
RefreshCameras="Refresh camera list" RefreshCameras="Refresh camera list"
AudioOnly="Audio only (no video)"
Status="Status" Status="Status"
NoCameraSelected="(no camera selected)" NoCameraSelected="(no camera selected)"
OfflineSuffix=" (offline)" OfflineSuffix=" (offline)"
+22 -1
View File
@@ -57,6 +57,7 @@ constexpr const char *kSettingServerUrl = "server_url";
constexpr const char *kSettingRoomSlug = "room_slug"; constexpr const char *kSettingRoomSlug = "room_slug";
constexpr const char *kSettingReadKey = "read_key"; constexpr const char *kSettingReadKey = "read_key";
constexpr const char *kSettingCamera = "camera"; constexpr const char *kSettingCamera = "camera";
constexpr const char *kSettingAudioOnly = "audio_only";
constexpr const char *kSettingStatus = "status"; constexpr const char *kSettingStatus = "status";
constexpr const char *kPropRefresh = "refresh"; constexpr const char *kPropRefresh = "refresh";
@@ -96,6 +97,10 @@ struct CameraSource {
std::mutex mutex; std::mutex mutex;
ConnectionConfig config; ConnectionConfig config;
std::string camera_identity; std::string camera_identity;
/// True hides video entirely for this source (the soundboard, typically)
/// -- see SessionConfig::subscribe_video for what that actually does at
/// the LiveKit level.
bool audio_only = false;
/// Bumped every time settings change; the worker compares it to what it /// Bumped every time settings change; the worker compares it to what it
/// last connected with, so a stale in-flight connect is abandoned rather /// last connected with, so a stale in-flight connect is abandoned rather
/// than fought over. /// than fought over.
@@ -219,6 +224,7 @@ void workerLoop(CameraSource *self)
for (;;) { for (;;) {
ConnectionConfig config; ConnectionConfig config;
std::string camera; std::string camera;
bool audio_only = false;
std::uint64_t generation = 0; std::uint64_t generation = 0;
{ {
std::unique_lock<std::mutex> lock(self->mutex); std::unique_lock<std::mutex> lock(self->mutex);
@@ -226,6 +232,7 @@ void workerLoop(CameraSource *self)
break; break;
config = self->config; config = self->config;
camera = self->camera_identity; camera = self->camera_identity;
audio_only = self->audio_only;
generation = self->generation; generation = self->generation;
} }
@@ -282,6 +289,7 @@ void workerLoop(CameraSource *self)
session_config.ws_url = token.ws_url; session_config.ws_url = token.ws_url;
session_config.token = token.lk_token; session_config.token = token.lk_token;
session_config.participant_identity = camera; session_config.participant_identity = camera;
session_config.subscribe_video = !audio_only;
if (self->session->connect(session_config)) { if (self->session->connect(session_config)) {
connected = true; connected = true;
@@ -330,6 +338,7 @@ void sourceGetDefaults(obs_data_t *settings)
obs_data_set_default_string(settings, kSettingRoomSlug, ""); obs_data_set_default_string(settings, kSettingRoomSlug, "");
obs_data_set_default_string(settings, kSettingReadKey, ""); obs_data_set_default_string(settings, kSettingReadKey, "");
obs_data_set_default_string(settings, kSettingCamera, ""); obs_data_set_default_string(settings, kSettingCamera, "");
obs_data_set_default_bool(settings, kSettingAudioOnly, false);
} }
void applySettings(CameraSource *self, obs_data_t *settings) void applySettings(CameraSource *self, obs_data_t *settings)
@@ -339,16 +348,19 @@ void applySettings(CameraSource *self, obs_data_t *settings)
config.room_slug = settingString(settings, kSettingRoomSlug); config.room_slug = settingString(settings, kSettingRoomSlug);
config.read_key = settingString(settings, kSettingReadKey); config.read_key = settingString(settings, kSettingReadKey);
const std::string camera = settingString(settings, kSettingCamera); const std::string camera = settingString(settings, kSettingCamera);
const bool audio_only = obs_data_get_bool(settings, kSettingAudioOnly);
{ {
std::lock_guard<std::mutex> guard(self->mutex); std::lock_guard<std::mutex> guard(self->mutex);
const bool changed = config.server_url != self->config.server_url || const bool changed = config.server_url != self->config.server_url ||
config.room_slug != self->config.room_slug || config.room_slug != self->config.room_slug ||
config.read_key != self->config.read_key || camera != self->camera_identity; config.read_key != self->config.read_key || camera != self->camera_identity ||
audio_only != self->audio_only;
if (!changed) if (!changed)
return; return;
self->config = config; self->config = config;
self->camera_identity = camera; self->camera_identity = camera;
self->audio_only = audio_only;
++self->generation; ++self->generation;
} }
self->wake.notify_all(); self->wake.notify_all();
@@ -385,6 +397,7 @@ void *sourceCreate(obs_data_t *settings, obs_source_t *source)
self->config.room_slug = settingString(settings, kSettingRoomSlug); self->config.room_slug = settingString(settings, kSettingRoomSlug);
self->config.read_key = settingString(settings, kSettingReadKey); self->config.read_key = settingString(settings, kSettingReadKey);
self->camera_identity = settingString(settings, kSettingCamera); self->camera_identity = settingString(settings, kSettingCamera);
self->audio_only = obs_data_get_bool(settings, kSettingAudioOnly);
self->generation = 1; self->generation = 1;
} }
@@ -527,6 +540,14 @@ obs_properties_t *sourceGetProperties(void *data)
obs_properties_add_button(props, kPropRefresh, obs_module_text("RefreshCameras"), refreshButtonClicked); obs_properties_add_button(props, kPropRefresh, obs_module_text("RefreshCameras"), refreshButtonClicked);
// For a picked slot with no visual content worth showing (the
// soundboard, which publishes a throwaway black keep-alive frame purely
// because RTMP egress needs a video track -- see Soundboard.tsx in the
// streamer-tools repo). Disables the video track at the LiveKit level
// (RemoteTrackPublication::setEnabled(false), see session.cpp), not just
// locally: the SFU stops sending it.
obs_properties_add_bool(props, kSettingAudioOnly, obs_module_text("AudioOnly"));
// An OBS_TEXT_INFO property renders its *description* as the visible // An OBS_TEXT_INFO property renders its *description* as the visible
// label, so the status line goes there rather than into a tooltip an // label, so the status line goes there rather than into a tooltip an
// operator would never hover over mid-show. // operator would never hover over mid-show.
-25
View File
@@ -10,28 +10,3 @@ module — so the SDK's licence and notice files ship with it.
by `obs-adapter/CMakeLists.txt` on every build, alongside this plugin's own by `obs-adapter/CMakeLists.txt` on every build, alongside this plugin's own
Apache-2.0 `LICENSE` (this project's own first-party code was relicensed from Apache-2.0 `LICENSE` (this project's own first-party code was relicensed from
GPL-2.0 to Apache-2.0 to match). GPL-2.0 to Apache-2.0 to match).
## A correction to the design doc
The design doc's open questions say:
> `client-sdk-cpp`'s bundled `LICENSE.md` (~28 distinct third-party license
> blocks — Google WebRTC, OpenH264, etc.) must ship inside the plugin package
**No such file exists at `v1.10.1`.** Checked, on 2026-09-06:
- The five release archives for this tag (`livekit-sdk-<triple>-1.10.1.tar.gz`
/ `.zip`) contain only `include/`, `lib/`, `bin/` and
`share/livekit/build-info.json`. No licence file of any kind.
- The repository at tag `v1.10.1` has `LICENSE` (Apache-2.0, 10142 bytes) and
`NOTICE` (553 bytes) at its root. There is no `LICENSE.md`, no `NOTICE.md`,
and no `THIRD_PARTY_LICENSES` file.
So what ships here is the Apache-2.0 licence and notice, which is what
actually exists upstream. **The aggregated third-party notice the design doc
expected — covering the WebRTC/OpenH264/etc. code statically linked inside
`liblivekit_ffi.so` — has not been located and is not being shipped.** That
is a real, open licensing question for whoever signs off on distributing
release binaries, not something this packaging step has resolved. Worth
raising upstream, or asking counsel whether the Apache-2.0 NOTICE alone
suffices for a binary redistribution of that library.