shadowdaoandClaude Sonnet 5 80904a3e85
Build / macOS (macos-latest) (push) Successful in 14s
Build / Linux (ubuntu-latest) (push) Successful in 41s
Build / Windows (windows-latest) (push) Failing after 8m18s
Add the LiveKit session wrapper, verified end-to-end against a real room
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

obs-streamer-tools-plugin

Native OBS Studio source plugin that will pull streamer-tools camera feeds directly from LiveKit over WebRTC (via LiveKit's livekit-ffi), replacing the current SRT/RTSP-via-VLC-or-Media-Source path for directors. Full design: docs/superpowers/specs/2026-09-06-obs-camera-plugin-design.md in the streamer-tools repo (as of this writing, that doc lives on the worktree-obs-plugin-server-api branch there, not yet merged to main).

Status: scaffolding only

This repository does not talk to LiveKit or streamer-tools yet. This first pass exists to prove the CMake toolchain, the core-library/OBS-adapter split, and the three-platform Gitea Actions CI pipeline all actually work, so the next phase (real livekit-ffi integration) can be planned against verified facts instead of assumptions. See the design doc's "Components" and "CI / build pipeline" sections for the target architecture this scaffold is standing up.

Layout

core/           - core library (C++17, no OBS dependency, headless-testable)
  include/stplugin/
    core.h        C++ API (ConnectionConfig, core_version())
    core_c.h       C ABI wrapper the OBS adapter calls into
  src/core.cpp
  tests/           dependency-free CTest unit tests

obs-adapter/    - thin OBS glue (C, adapted from obsproject/obs-plugintemplate)
  src/plugin-main.c       obs_module_load/unload + a stub source registration
  src/plugin-support.{h,c.in}
  data/locale/en-US.ini

.gitea/workflows/build.yml   - 3-platform CI matrix (see below)

Everything in core/ is real, working, unit-tested code -- it just doesn't do anything useful yet (a version string, a config struct with non-empty validation). Everything in obs-adapter/ is real OBS module code -- it registers an actual obs_source_info and builds as a real, dynamically loadable OBS module (see "Verified" below) -- but the source is a stub: create/destroy allocate/free a dummy blob, there is no properties UI, and no frames are ever pushed. That's the boundary this task was scoped to.

What's real vs. deliberately deferred

Deferred, per the task that produced this scaffold (out of scope for this pass, in scope for the next one):

  • No livekit-ffi linkage of any kind.
  • No streamer-tools API client (auth, slot-listing, token minting).
  • No properties UI (server URL / room slug / read key / camera dropdown).
  • No frame output (obs_source_output_video/_audio).
  • No packaging/release step (the design doc's "on a version tag" job).

Toolchain notes (verified on this machine: Ubuntu 24.04 / Linux)

  • CMake 3.28.3, Ninja 1.11.1, GCC 13.3.0 -- all installed via apt-get install cmake ninja-build. Top-level CMakeLists.txt requires CMake >= 3.16 (deliberately lower than the official obsproject/obs-plugintemplate's 3.28...3.30 floor -- see below).
  • OBS plugin template used as reference: obsproject/obs-plugintemplate, commit 3e7d7ac3b5342cd7d9b88890b9c70b472d1520fc (2025-12-09, "Fix typo of Visual Studio in README"), fetched fresh from GitHub. src/plugin-main.c, src/plugin-support.{h,c.in}, and the empty data/locale/en-US.ini in obs-adapter/ are adapted directly from it.
  • Deliberate deviation from the template's own build system: the official template's CMakeLists.txt chains into cmake/common/bootstrap.cmake, which in turn reads buildspec.json and downloads full OBS source archives (pinned to OBS 31.1.1) plus prebuilt dependency bundles for macOS and Windows. That machinery is real, actively maintained, and probably the right long-term answer for cross-platform reproducible builds -- but it's heavy (multi-hundred-MB downloads, a whole cmake/{macos,windows,common} support tree, Qt6, code-signing hooks) and out of scope to stand up and debug in one pass. This scaffold instead uses a much simpler hand-written top-level CMakeLists.txt that calls find_package(libobs) directly.
  • On Linux, this actually works far better than expected: Ubuntu ships a real libobs-dev package (30.0.2+dfsg-3build1 on 24.04, i.e. not the 31.1.1 the template's buildspec.json pins -- worth reconciling before the next phase if API surface matters) with genuine CMake package config files (/usr/lib/x86_64-linux-gnu/cmake/libobs/libobsConfig.cmake, libobsTargets.cmake) that export an OBS::libobs imported target -- the exact target name the official template expects. find_package(libobs QUIET) finds it with zero extra plumbing. This means the OBS adapter in this repo links against real OBS headers and a real libobs.so, not a stub -- confirmed by ldd showing libobs.so.0 and nm -D showing real obs_module_* exports (see "Verified" below). Install via apt-get install libobs-dev (pulls in Qt6 as a dependency chain, ~seconds on a fast mirror).
  • macOS/Windows have no equivalent system package (there's no Homebrew formula or winget package that ships libobsConfig.cmake the way Ubuntu's libobs-dev does). For those platforms the choices are: (a) adopt the template's full buildspec-driven source/prebuilt-deps download, or (b) find/produce a lighter prebuilt SDK bundle. This is now a concrete, scoped decision for the next phase, not a guess -- the CI workflow in this repo currently takes option (c) for this pass only: skip building the OBS adapter on macOS/Windows and build+test just the core library, via the same find_package(libobs QUIET) fallback the top-level CMakeLists.txt already has for exactly this situation.
  • ENABLE_QT/ENABLE_FRONTEND_API template options were not carried over -- this scaffold's properties-UI-free stub doesn't need Qt yet; the real adapter will need to revisit this once the properties UI (server URL / room slug / read key / camera dropdown) is built.

What actually builds, and how it was verified

$ cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
-- libobs found (/usr/lib/x86_64-linux-gnu/cmake/libobs) -- building OBS adapter module
-- Configuring done
-- Generating done

$ cmake --build build
[1/7] Building C object obs-adapter/CMakeFiles/streamer-tools-camera.dir/plugin-support.c.o
[2/7] Building C object obs-adapter/CMakeFiles/streamer-tools-camera.dir/src/plugin-main.c.o
[3/7] Building CXX object core/CMakeFiles/stplugin_core.dir/src/core.cpp.o
[4/7] Linking CXX static library core/libstplugin_core.a
[5/7] Building CXX object core/tests/CMakeFiles/stplugin_core_tests.dir/test_core.cpp.o
[6/7] Linking CXX shared module obs-adapter/streamer-tools-camera.so
[7/7] Linking CXX executable core/tests/stplugin_core_tests

$ ctest --test-dir build --output-on-failure
1/1 Test #1: stplugin_core_tests ..............   Passed    0.00 sec
100% tests passed, 0 tests failed out of 1

$ ldd build/obs-adapter/streamer-tools-camera.so | grep obs
	libobs.so.0 => /lib/x86_64-linux-gnu/libobs.so.0 (...)

$ nm -D build/obs-adapter/streamer-tools-camera.so | grep obs_module
0000000000001430 T obs_module_free_locale
0000000000001450 T obs_module_load
...
0000000000001490 T obs_module_unload

This is a genuine, dynamically-linked OBS module -- not the "standalone shared library without linking OBS" fallback the scaffolding task's scope explicitly allowed as an acceptable compromise. That fallback path is still exercised (and needed) on macOS/Windows CI for now; see above.

CI

.gitea/workflows/build.yml runs on every push/PR, matrixed across the three runners confirmed available to this repo by living under the CyberCoveLLC org (see the design doc's "CI / build pipeline" section):

Job runs-on Runner
linux ubuntu-latest gitea-runner.internal.cloud-hosting.io (Global) or localhost.localdomain (org-scoped; note: now online with ubuntu-latest/ubuntu-24.04/ubuntu-22.04 labels -- the design doc recorded it as offline, that's since changed)
macos macos-latest home-mac (Global)
windows windows-latest winvm-builder (org-scoped to CyberCoveLLC)

The Linux job installs libobs-dev and builds the real OBS adapter module plus the core library, then runs ctest. The macOS/Windows jobs build and test only the core library for now (see toolchain notes above for why).

No packaging/release step yet -- out of scope for this pass.

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