ab455a9cfe03707f47f3bdb03293c2f77c845e93
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
af7d2c6d24 |
fix: pin video quality to stop OBS source resizing; add audio-only mode
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
|
||
|
|
969b8db94a |
license: relicense first-party code from GPL-2.0-or-later to Apache-2.0
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 |
||
|
|
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 |
||
|
|
80904a3e85 |
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 |