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
51 lines
1.3 KiB
CMake
51 lines
1.3 KiB
CMake
# streamer-tools OBS Camera Plugin - core library
|
|
#
|
|
# No OBS dependency by design (see docs/superpowers/specs/
|
|
# 2026-09-06-obs-camera-plugin-design.md in the streamer-tools repo) --
|
|
# this must build and test headlessly on every platform.
|
|
|
|
set(STPLUGIN_CORE_SOURCES
|
|
src/core.cpp
|
|
src/json.cpp
|
|
src/http_common.cpp
|
|
src/api_client.cpp
|
|
src/session_types.cpp
|
|
src/session.cpp
|
|
)
|
|
|
|
# HTTP backend, one per platform. See core/include/stplugin/http.h for why
|
|
# this is split rather than using libcurl everywhere.
|
|
if(WIN32)
|
|
list(APPEND STPLUGIN_CORE_SOURCES src/http_winhttp.cpp)
|
|
else()
|
|
list(APPEND STPLUGIN_CORE_SOURCES src/http_curl.cpp)
|
|
endif()
|
|
|
|
add_library(stplugin_core STATIC ${STPLUGIN_CORE_SOURCES})
|
|
|
|
target_include_directories(stplugin_core
|
|
PUBLIC
|
|
${CMAKE_CURRENT_SOURCE_DIR}/include
|
|
)
|
|
|
|
target_link_libraries(stplugin_core
|
|
PUBLIC
|
|
LiveKit::livekit
|
|
)
|
|
|
|
if(WIN32)
|
|
target_link_libraries(stplugin_core PRIVATE winhttp)
|
|
else()
|
|
find_package(CURL REQUIRED)
|
|
target_link_libraries(stplugin_core PRIVATE CURL::libcurl)
|
|
endif()
|
|
|
|
find_package(Threads REQUIRED)
|
|
target_link_libraries(stplugin_core PUBLIC Threads::Threads)
|
|
|
|
set_target_properties(stplugin_core PROPERTIES
|
|
POSITION_INDEPENDENT_CODE ON
|
|
)
|
|
|
|
add_subdirectory(tests)
|