LiveKit integration: real camera feed pipeline #1

Merged
jknapp merged 17 commits from feat/livekit-integration into main 2026-09-07 06:06:50 +00:00
5 changed files with 80 additions and 85 deletions
Showing only changes of commit 6b12859887 - Show all commits
+28 -29
View File
@@ -68,24 +68,29 @@ function(_check_deps_version version)
return(PROPAGATE found CMAKE_PREFIX_PATH)
endfunction()
# _resolve_versioned_macos_sdk: return an SDK path whose *filename* carries the
# version number, e.g. .../MacOSX15.5.sdk.
# _resolve_versioned_macos_sdk: return an SDK path that satisfies OBS's own
# macOS SDK version check.
#
# Not upstream. OBS's own cmake/macos/compilerconfig.cmake reads the SDK
# version by regex-matching "MacOSX<major>.<minor>.sdk" out of
# CMAKE_OSX_SYSROOT, and hard-fails if that does not match:
# Not upstream. OBS 30.0.2's cmake/macos/compilerconfig.cmake reads the SDK
# version straight out of CMAKE_OSX_SYSROOT with this regex:
#
# string sub-command REGEX, mode MATCH needs at least 5 arguments
# Your macOS SDK version () is too low.
# ".+/MacOSX.platform/Developer/SDKs/MacOSX([0-9]+\\.[0-9])+\\.sdk$"
#
# With upstream's Xcode generator CMAKE_OSX_SYSROOT stays the literal string
# "macosx" and Xcode resolves it late, so the regex never runs against a real
# path. With Ninja -- which this project uses because CI has no Xcode -- CMake
# resolves it eagerly to whatever `xcrun --show-sdk-path` returns, and on a
# Command-Line-Tools-only install that is the UNVERSIONED symlink
# /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk. Hence this: prefer a
# versioned sibling if the toolchain ships one, and otherwise synthesise a
# correctly-named symlink to the same SDK.
# and hard-fails if it does not match ("Your macOS SDK version () is too low",
# with an empty version, which is the tell). That pattern only ever matches a
# full-Xcode SDK path; a Command-Line-Tools-only install has its SDK at
# /Library/Developer/CommandLineTools/SDKs/MacOSX<ver>.sdk, with no
# MacOSX.platform/Developer/SDKs segment at all, and can never match.
#
# Upstream never hits this because it uses the Xcode generator, where
# CMAKE_OSX_SYSROOT stays the literal string "macosx" and Xcode resolves it
# late. This project uses Ninja (the CI runner has no xcodebuild), so CMake
# resolves the sysroot eagerly and the regex runs against a real path.
#
# So: if the toolchain's own SDK path already matches, use it untouched.
# Otherwise build a symlink tree under .deps/ whose shape matches the regex
# and which points at exactly the same SDK. Nothing about the compilation
# changes -- only the spelling of the path, which is all the check reads.
function(_resolve_versioned_macos_sdk out_path)
set(${out_path} "" PARENT_SCOPE)
@@ -97,12 +102,11 @@ function(_resolve_versioned_macos_sdk out_path)
ERROR_QUIET
)
if(NOT _rc EQUAL 0 OR NOT _sdk)
message(WARNING "Could not determine the macOS SDK path via xcrun; leaving CMAKE_OSX_SYSROOT alone.")
return()
endif()
# Already versioned: nothing to do.
get_filename_component(_sdk_name "${_sdk}" NAME)
if(_sdk_name MATCHES "^MacOSX[0-9]+\\.[0-9]+\\.sdk$")
if(_sdk MATCHES "/MacOSX\\.platform/Developer/SDKs/MacOSX[0-9]+\\.[0-9]+\\.sdk$")
set(${out_path} "${_sdk}" PARENT_SCOPE)
return()
endif()
@@ -115,27 +119,22 @@ function(_resolve_versioned_macos_sdk out_path)
ERROR_QUIET
)
if(NOT _rc EQUAL 0 OR NOT _sdk_version MATCHES "^([0-9]+)\\.([0-9]+)")
message(WARNING "Could not determine the macOS SDK version via xcrun (got '${_sdk_version}').")
return()
endif()
set(_short "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}")
# A versioned sibling next to the symlink is the common layout.
get_filename_component(_sdk_dir "${_sdk}" DIRECTORY)
if(EXISTS "${_sdk_dir}/MacOSX${_short}.sdk")
set(${out_path} "${_sdk_dir}/MacOSX${_short}.sdk" PARENT_SCOPE)
return()
endif()
# Otherwise make one, inside our own dependency directory.
set(_link_dir "${dependencies_dir}/sdk")
file(MAKE_DIRECTORY "${_link_dir}")
set(_link_dir "${dependencies_dir}/sdk/MacOSX.platform/Developer/SDKs")
set(_link "${_link_dir}/MacOSX${_short}.sdk")
file(MAKE_DIRECTORY "${_link_dir}")
if(NOT EXISTS "${_link}")
file(CREATE_LINK "${_sdk}" "${_link}" SYMBOLIC)
endif()
if(EXISTS "${_link}")
message(STATUS "Using synthesised versioned macOS SDK path: ${_link}")
message(STATUS "macOS SDK ${_short} at ${_sdk}; presenting it to OBS as ${_link}")
set(${out_path} "${_link}" PARENT_SCOPE)
else()
message(WARNING "Could not create the versioned macOS SDK symlink at ${_link}.")
endif()
endfunction()
+12 -21
View File
@@ -20,32 +20,23 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#include <string>
// C++ API for the core library. Per the design doc
// (docs/superpowers/specs/2026-09-06-obs-camera-plugin-design.md in the
// streamer-tools repo), this library will eventually own: streamer-tools
// API auth, LiveKit FFI session management (connect, subscribe, decode,
// reconnect), and frame callbacks -- all with zero OBS dependency, so it
// can be built and tested headlessly.
//
// THIS IS SCAFFOLDING. Nothing below talks to a real server or to
// livekit-ffi yet. It exists to prove the core-library/OBS-adapter split
// builds, links, and is unit-testable, ahead of a later phase that
// implements the real logic.
// The small shared pieces of the core library: its version string, and the
// streamer-tools connection settings that both the API client and the OBS
// adapter pass around. Everything substantial lives in its own header --
// api_client.h, session.h, http.h, json.h -- and none of it depends on OBS,
// so the whole library builds and tests headlessly on all three platforms.
namespace stplugin {
// Returns the core library's version string. Placeholder for a real
// version scheme once the library does something.
// The core library's version string, injected by CMake from the top-level
// project() version, so what OBS logs on load is the actual build.
const char *core_version();
// Minimal connection configuration the future core library will use to
// authenticate against the streamer-tools API
// (see apps/server/src/rooms/join.routes.ts and
// apps/server/src/livekit/tokens.ts in the streamer-tools repo for the
// existing read-key-authed token pattern this will follow) and mint a
// scoped LiveKit subscriber token. Validation here is intentionally
// trivial -- it exists to prove the core library is unit-testable
// headlessly, not to implement the real API client.
// What an operator types into the source's properties, and what ApiClient
// needs to reach the two read-key-scoped endpoints in
// apps/server/src/obs/plugin.routes.ts (streamer-tools repo). The read key is
// a credential: it is masked in the properties UI and never logged (see
// ApiClient::redactedUrl).
struct ConnectionConfig {
std::string server_url;
std::string room_slug;
+6 -8
View File
@@ -18,15 +18,13 @@ with this program. If not, see <https://www.gnu.org/licenses/>
#pragma once
// Minimal C ABI surface of the core library, for the OBS adapter (plain
// C, per the obs-plugintemplate convention) to call into the core
// library (C++) without needing a C++ compiler in that translation unit.
// A minimal C ABI over the core library's version string.
//
// This mirrors the boundary the real integration will cross in the
// other direction: livekit-ffi is a Rust library exposing a C ABI that
// the C++ core library will link against. Proving a small, deliberate
// C ABI seam works cleanly here is part of what this scaffold is for.
// The OBS adapter is C++ and calls stplugin::core_version() directly, so
// nothing in this repository needs this header today. It is kept because it
// is the seam a plain-C consumer would use, and because the unit tests assert
// the two entry points agree -- which is a cheap check that the C++ library
// really is linkable from a C translation unit.
#ifdef __cplusplus
extern "C" {
#endif
+3
View File
@@ -15,6 +15,9 @@ function(stplugin_add_test name)
endfunction()
stplugin_add_test(test_core)
target_compile_definitions(test_core PRIVATE
STPLUGIN_EXPECTED_CORE_VERSION="${PROJECT_VERSION}"
)
stplugin_add_test(test_json)
stplugin_add_test(test_api_client)
stplugin_add_test(test_session)
+31 -27
View File
@@ -16,44 +16,48 @@ You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
// Deliberately dependency-free (no gtest/catch2 etc.) so this test target
// has no network fetch or package-manager step in CI -- proving the
// "core library builds and tests headlessly, no OBS required" claim
// without adding another moving part to this scaffolding pass.
// NOTE on why this file uses ST_ASSERT and not assert(): CI builds Release,
// which defines NDEBUG, which compiles every bare assert() out entirely. This
// suite previously passed unconditionally for exactly that reason. The
// ST_ASSERT macros in test_util.h are always live and report a pass/fail
// count.
#include <cassert>
#include <cstdio>
#include <cstring>
#include <string>
#include "stplugin/core.h"
#include "stplugin/core_c.h"
#include "test_util.h"
int main() {
// core_version() should return a non-empty string, via both the
// C++ API and the C ABI wrapper the OBS adapter will actually call.
int main()
{
// The version string comes through both the C++ API and the C ABI
// wrapper, and the two must agree.
const char *cpp_version = stplugin::core_version();
assert(cpp_version != nullptr);
assert(std::strlen(cpp_version) > 0);
ST_ASSERT(cpp_version != nullptr);
ST_ASSERT(cpp_version != nullptr && std::strlen(cpp_version) > 0);
const char *c_version = stplugin_core_version();
assert(c_version != nullptr);
assert(std::strcmp(cpp_version, c_version) == 0);
ST_ASSERT(c_version != nullptr);
ST_ASSERT_EQ(std::string(cpp_version ? cpp_version : ""), std::string(c_version ? c_version : ""));
// ConnectionConfig::is_valid() -- trivial non-empty checks, but
// exercised through all four combinations to prove the core library
// is genuinely testable in isolation.
stplugin::ConnectionConfig valid{"https://streamers.example.com", "main-room", "readkey123"};
assert(valid.is_valid());
// It is the version CMake injected, not a hand-maintained literal.
ST_ASSERT_EQ(std::string(cpp_version ? cpp_version : ""), std::string(STPLUGIN_EXPECTED_CORE_VERSION));
stplugin::ConnectionConfig missing_url{"", "main-room", "readkey123"};
assert(!missing_url.is_valid());
// ConnectionConfig::is_valid(): all three fields are required. Named
// locals rather than braced temporaries inline, because the commas inside
// a braced initialiser would be read as macro argument separators.
const stplugin::ConnectionConfig complete{"https://streamers.example.com", "main-room", "readkey123"};
const stplugin::ConnectionConfig no_url{"", "main-room", "readkey123"};
const stplugin::ConnectionConfig no_slug{"https://streamers.example.com", "", "readkey123"};
const stplugin::ConnectionConfig no_key{"https://streamers.example.com", "main-room", ""};
const stplugin::ConnectionConfig empty;
stplugin::ConnectionConfig missing_slug{"https://streamers.example.com", "", "readkey123"};
assert(!missing_slug.is_valid());
ST_ASSERT(complete.is_valid());
ST_ASSERT(!no_url.is_valid());
ST_ASSERT(!no_slug.is_valid());
ST_ASSERT(!no_key.is_valid());
ST_ASSERT(!empty.is_valid());
stplugin::ConnectionConfig missing_key{"https://streamers.example.com", "main-room", ""};
assert(!missing_key.is_valid());
std::printf("core: all tests passed\n");
return 0;
return st_test_report("core");
}