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
6 changed files with 417 additions and 16 deletions
Showing only changes of commit e595173049 - Show all commits
+35 -14
View File
@@ -1,8 +1,8 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.19)
project(obs-streamer-tools-plugin project(obs-streamer-tools-plugin
VERSION 0.0.1 VERSION 0.1.0
DESCRIPTION "OBS Studio source plugin for streamer-tools camera feeds (scaffold, no LiveKit integration yet)" DESCRIPTION "OBS Studio source plugin for streamer-tools camera feeds"
LANGUAGES C CXX LANGUAGES C CXX
) )
@@ -11,19 +11,40 @@ set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_STANDARD_REQUIRED ON)
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()
enable_testing() enable_testing()
# --- Scaffolding note ------------------------------------------------------ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
# This deliberately does NOT use the full obsproject/obs-plugintemplate
# build system (its cmake/common/bootstrap.cmake + buildspec.json, which # --- LiveKit C++ client SDK -------------------------------------------------
# download complete OBS source archives and prebuilt dependency bundles # Pinned, prebuilt release of livekit/client-sdk-cpp, downloaded and unpacked
# for macOS/Windows). That machinery is real and may be worth adopting # by cmake/LiveKitSDK.cmake, then consumed through its own CMake package
# wholesale in a later phase; for this scaffolding pass the goal is a much # config as the LiveKit::livekit imported target. See the design doc's
# simpler CMakeLists.txt that proves out find_package(libobs) plus the # "Resolved (2026-09-07)" section: an exact pin, never "latest".
# core/adapter split on the platform we can actually verify locally set(STPLUGIN_LIVEKIT_SDK_VERSION "1.10.1" CACHE STRING
# (Linux, via the system libobs-dev package). See README.md for the full "Pinned livekit/client-sdk-cpp release version")
# writeup of what was verified vs. what remains. set(STPLUGIN_LIVEKIT_SDK_TRIPLE "" CACHE STRING
# ---------------------------------------------------------------------------- "Override the client-sdk-cpp release triple (e.g. ubuntu-24.04-x64); empty = autodetect")
set(STPLUGIN_LIVEKIT_SDK_DIR "${CMAKE_BINARY_DIR}/_deps/livekit-sdk" CACHE PATH
"Directory the client-sdk-cpp release archive is extracted into (point at a persistent path to cache it across CI builds)")
include(LiveKitSDK)
if(STPLUGIN_LIVEKIT_SDK_TRIPLE)
livekit_sdk_setup(
VERSION "${STPLUGIN_LIVEKIT_SDK_VERSION}"
SDK_DIR "${STPLUGIN_LIVEKIT_SDK_DIR}"
TRIPLE "${STPLUGIN_LIVEKIT_SDK_TRIPLE}"
)
else()
livekit_sdk_setup(
VERSION "${STPLUGIN_LIVEKIT_SDK_VERSION}"
SDK_DIR "${STPLUGIN_LIVEKIT_SDK_DIR}"
)
endif()
find_package(LiveKit CONFIG REQUIRED)
add_subdirectory(core) add_subdirectory(core)
+209
View File
@@ -0,0 +1,209 @@
# LiveKitSDK.cmake
#
# Downloads the prebuilt LiveKit C++ SDK (livekit/client-sdk-cpp) release
# asset for the host OS/arch, extracts it, and points
# `find_package(LiveKit CONFIG REQUIRED)` at it.
#
# Adapted from livekit-examples/cpp-example-collection's cmake/LiveKitSDK.cmake
# (fetched 2026-09-06). Deliberate changes from the upstream reference:
#
# 1. VERSION must be an exact release number. The upstream helper accepted
# VERSION "latest" and resolved it through the GitHub releases API at
# configure time. That is exactly what this project must not do: the
# design doc calls for a pinned release tag, treating version bumps as
# deliberate work (the SDK is young and ships roughly weekly). Dropping
# the "latest" path also removes a GitHub-API call -- and its rate
# limiting / GITHUB_TOKEN plumbing -- from every CI configure.
# 2. Exports LIVEKIT_SDK_RUNTIME_LIBS: the shared libraries that must be
# staged next to the built OBS plugin module for it to load at runtime
# (liblivekit + liblivekit_ffi). Upstream examples run out of the build
# tree and never needed this; a redistributable OBS plugin does.
# 3. Exports LIVEKIT_SDK_INCLUDE_DIR / _LIB_DIR / _BIN_DIR for packaging.
#
# Usage:
# list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
# include(LiveKitSDK)
# livekit_sdk_setup(VERSION "1.10.1" SDK_DIR "${CMAKE_BINARY_DIR}/_deps/livekit-sdk")
# find_package(LiveKit CONFIG REQUIRED)
include_guard(GLOBAL)
# -------------------- Host detection --------------------
function(_lk_detect_host out_os out_arch)
if(WIN32)
set(_os "windows")
elseif(APPLE)
set(_os "macos")
elseif(UNIX)
set(_os "linux")
else()
message(FATAL_ERROR "LiveKitSDK: unsupported host OS")
endif()
# Prefer the *target* processor when cross-compiling is expressed through
# CMAKE_OSX_ARCHITECTURES (macOS CI runners are arm64 but may target x64).
set(_proc "${CMAKE_HOST_SYSTEM_PROCESSOR}")
if(APPLE AND CMAKE_OSX_ARCHITECTURES)
list(LENGTH CMAKE_OSX_ARCHITECTURES _n_arch)
if(_n_arch GREATER 1)
message(FATAL_ERROR
"LiveKitSDK: CMAKE_OSX_ARCHITECTURES lists ${_n_arch} architectures "
"(${CMAKE_OSX_ARCHITECTURES}). client-sdk-cpp ships single-arch "
"dylibs only, so universal binaries are not supported. Build one "
"architecture at a time.")
endif()
list(GET CMAKE_OSX_ARCHITECTURES 0 _proc)
endif()
string(TOLOWER "${_proc}" _proc_l)
if(_proc_l MATCHES "^(x86_64|amd64)$")
set(_arch "x64")
elseif(_proc_l MATCHES "^(arm64|aarch64)$")
set(_arch "arm64")
else()
message(FATAL_ERROR "LiveKitSDK: unsupported host arch: ${_proc}")
endif()
set(${out_os} "${_os}" PARENT_SCOPE)
set(${out_arch} "${_arch}" PARENT_SCOPE)
endfunction()
function(_lk_default_triple out_triple)
_lk_detect_host(_os _arch)
set(${out_triple} "${_os}-${_arch}" PARENT_SCOPE)
endfunction()
function(_lk_archive_ext out_ext)
if(WIN32)
set(${out_ext} "zip" PARENT_SCOPE)
else()
set(${out_ext} "tar.gz" PARENT_SCOPE)
endif()
endfunction()
# -------------------- Public entrypoint --------------------
# livekit_sdk_setup(
# VERSION <x.y.z> REQUIRED, exact -- "latest" is rejected
# SDK_DIR <dir> REQUIRED, where the archive is extracted
# [REPO <org/repo>] default: livekit/client-sdk-cpp
# [SHA256 <hash>] optional: verify the downloaded archive
# [TRIPLE <os-arch>] optional override (e.g. ubuntu-24.04-x64)
# [DOWNLOAD_DIR <dir>] default: <build>/_downloads
# [NO_DOWNLOAD] fail instead of downloading if absent
# )
function(livekit_sdk_setup)
set(options NO_DOWNLOAD)
set(oneValueArgs VERSION SDK_DIR REPO SHA256 TRIPLE DOWNLOAD_DIR)
cmake_parse_arguments(LK "${options}" "${oneValueArgs}" "" ${ARGN})
if(NOT LK_VERSION)
message(FATAL_ERROR "livekit_sdk_setup: VERSION is required")
endif()
if(LK_VERSION STREQUAL "latest")
message(FATAL_ERROR
"livekit_sdk_setup: VERSION=\"latest\" is deliberately not supported. "
"This project pins an exact client-sdk-cpp release; see the comment at "
"the top of cmake/LiveKitSDK.cmake.")
endif()
if(NOT LK_VERSION MATCHES "^[0-9]+\\.[0-9]+\\.[0-9]+$")
message(FATAL_ERROR "livekit_sdk_setup: VERSION must be x.y.z, got '${LK_VERSION}'")
endif()
if(NOT LK_SDK_DIR)
message(FATAL_ERROR "livekit_sdk_setup: SDK_DIR is required")
endif()
if(NOT LK_REPO)
set(LK_REPO "livekit/client-sdk-cpp")
endif()
if(NOT LK_DOWNLOAD_DIR)
set(LK_DOWNLOAD_DIR "${CMAKE_BINARY_DIR}/_downloads")
endif()
if(NOT LK_TRIPLE)
_lk_default_triple(LK_TRIPLE)
endif()
_lk_archive_ext(_ext)
set(_archive "livekit-sdk-${LK_TRIPLE}-${LK_VERSION}.${_ext}")
set(_url "https://github.com/${LK_REPO}/releases/download/v${LK_VERSION}/${_archive}")
set(_archive_path "${LK_DOWNLOAD_DIR}/${_archive}")
# The archive contains a single top-level folder named after the asset.
set(_extracted_root "${LK_SDK_DIR}/livekit-sdk-${LK_TRIPLE}-${LK_VERSION}")
file(MAKE_DIRECTORY "${LK_DOWNLOAD_DIR}")
file(MAKE_DIRECTORY "${LK_SDK_DIR}")
if(NOT EXISTS "${_extracted_root}/lib/cmake")
if(LK_NO_DOWNLOAD)
message(FATAL_ERROR
"LiveKitSDK: SDK not found at:\n ${_extracted_root}\nand NO_DOWNLOAD was set.")
endif()
message(STATUS "LiveKitSDK: downloading ${_url}")
if(LK_SHA256)
file(DOWNLOAD "${_url}" "${_archive_path}"
SHOW_PROGRESS TLS_VERIFY ON
EXPECTED_HASH "SHA256=${LK_SHA256}"
STATUS _st LOG _log)
else()
file(DOWNLOAD "${_url}" "${_archive_path}"
SHOW_PROGRESS TLS_VERIFY ON
STATUS _st LOG _log)
endif()
list(GET _st 0 _code)
list(GET _st 1 _msg)
if(NOT _code EQUAL 0)
file(REMOVE "${_archive_path}")
message(STATUS "LiveKitSDK: download log:\n${_log}")
message(FATAL_ERROR
"LiveKitSDK: download failed\nURL: ${_url}\nStatus: ${_code}\nMessage: ${_msg}\n"
"If this triple has no release asset, pass TRIPLE explicitly.")
endif()
# Remove any previous partial extraction.
file(REMOVE_RECURSE "${_extracted_root}")
message(STATUS "LiveKitSDK: extracting ${_archive_path}")
file(ARCHIVE_EXTRACT INPUT "${_archive_path}" DESTINATION "${LK_SDK_DIR}")
endif()
if(NOT EXISTS "${_extracted_root}/lib/cmake/LiveKit/LiveKitConfig.cmake")
message(FATAL_ERROR
"LiveKitSDK: extracted SDK does not look valid (missing "
"lib/cmake/LiveKit/LiveKitConfig.cmake)\nExpected under: ${_extracted_root}")
endif()
# Make find_package(LiveKit CONFIG REQUIRED) work in the caller's scope.
list(PREPEND CMAKE_PREFIX_PATH "${_extracted_root}")
set(CMAKE_PREFIX_PATH "${CMAKE_PREFIX_PATH}" PARENT_SCOPE)
set(LiveKit_DIR "${_extracted_root}/lib/cmake/LiveKit" PARENT_SCOPE)
# --- Runtime libraries, for staging next to the plugin module ------------
# Windows keeps the DLLs in bin/ and the import libs in lib/; the Unix
# platforms put the shared objects directly in lib/.
if(WIN32)
file(GLOB _runtime_libs "${_extracted_root}/bin/*.dll")
elseif(APPLE)
file(GLOB _runtime_libs "${_extracted_root}/lib/*.dylib")
else()
file(GLOB _runtime_libs "${_extracted_root}/lib/*.so" "${_extracted_root}/lib/*.so.*")
endif()
if(NOT _runtime_libs)
message(FATAL_ERROR
"LiveKitSDK: found no runtime shared libraries under ${_extracted_root}. "
"The release layout may have changed for version ${LK_VERSION}.")
endif()
set(LIVEKIT_SDK_EXTRACTED_ROOT "${_extracted_root}" CACHE PATH "LiveKit SDK extracted root" FORCE)
set(LIVEKIT_SDK_INCLUDE_DIR "${_extracted_root}/include" CACHE PATH "LiveKit SDK include dir" FORCE)
set(LIVEKIT_SDK_LIB_DIR "${_extracted_root}/lib" CACHE PATH "LiveKit SDK lib dir" FORCE)
set(LIVEKIT_SDK_BIN_DIR "${_extracted_root}/bin" CACHE PATH "LiveKit SDK bin dir" FORCE)
set(LIVEKIT_SDK_RUNTIME_LIBS "${_runtime_libs}" CACHE STRING "LiveKit SDK runtime shared libraries" FORCE)
set(LIVEKIT_SDK_URL_USED "${_url}" CACHE STRING "LiveKit SDK URL used" FORCE)
set(LIVEKIT_SDK_VERSION_RESOLVED "${LK_VERSION}" CACHE STRING "LiveKit SDK version" FORCE)
set(LIVEKIT_SDK_TRIPLE_USED "${LK_TRIPLE}" CACHE STRING "LiveKit SDK triple" FORCE)
message(STATUS "LiveKitSDK: using SDK ${LK_VERSION} (${LK_TRIPLE}) at ${_extracted_root}")
endfunction()
+5
View File
@@ -13,6 +13,11 @@ target_include_directories(stplugin_core
${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/include
) )
target_link_libraries(stplugin_core
PUBLIC
LiveKit::livekit
)
set_target_properties(stplugin_core PROPERTIES set_target_properties(stplugin_core PROPERTIES
POSITION_INDEPENDENT_CODE ON POSITION_INDEPENDENT_CODE ON
) )
+13 -2
View File
@@ -1,7 +1,18 @@
add_executable(stplugin_core_tests add_executable(stplugin_core_tests
test_core.cpp test_core.cpp
) )
target_link_libraries(stplugin_core_tests PRIVATE stplugin_core) target_link_libraries(stplugin_core_tests PRIVATE stplugin_core)
add_test(NAME stplugin_core_tests COMMAND stplugin_core_tests) add_test(NAME stplugin_core_tests COMMAND stplugin_core_tests)
# Smoke test for the LiveKit SDK link: initialize()/shutdown() must succeed
# in-process. This is the cheapest possible proof that LiveKit::livekit is
# not just linked but loadable and callable (it dlopen-chains into
# liblivekit_ffi, which is where a broken RPATH would show up).
add_executable(stplugin_livekit_smoke
test_livekit_smoke.cpp
)
target_link_libraries(stplugin_livekit_smoke PRIVATE stplugin_core)
target_compile_definitions(stplugin_livekit_smoke PRIVATE
STPLUGIN_EXPECTED_LIVEKIT_VERSION="${LIVEKIT_SDK_VERSION_RESOLVED}"
)
add_test(NAME stplugin_livekit_smoke COMMAND stplugin_livekit_smoke)
+64
View File
@@ -0,0 +1,64 @@
/*
streamer-tools OBS Camera Plugin - LiveKit SDK link smoke test
Copyright (C) 2026 CyberCoveLLC <jknapp85@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
// Proves the pinned client-sdk-cpp release is genuinely linked and callable:
// the SDK's own global init/shutdown runs in-process without crashing, and
// the header-reported build version matches the version CMake pinned.
//
// This does NOT touch the network -- livekit::initialize() only sets up
// global SDK state and log routing.
#include <cstdio>
#include <cstring>
#include <livekit/build.h>
#include <livekit/livekit.h>
#include <livekit/logging.h>
#include "test_util.h"
int main()
{
// The pin CMake resolved is passed in as a define; the SDK's own
// generated build.h reports what was actually unpacked. A mismatch means
// a stale extracted SDK directory is being reused.
ST_ASSERT_EQ(std::string(LIVEKIT_BUILD_VERSION), std::string(STPLUGIN_EXPECTED_LIVEKIT_VERSION));
// initialize() returns true when this call performed the init, false if
// the SDK was already initialized. Either way it must not crash, and a
// second call must report "already initialized".
bool first = livekit::initialize(livekit::LogLevel::Error);
ST_ASSERT(first);
bool second = livekit::initialize(livekit::LogLevel::Error);
ST_ASSERT(!second);
// Log level round-trips through the SDK's global state.
livekit::setLogLevel(livekit::LogLevel::Warn);
ST_ASSERT(livekit::getLogLevel() == livekit::LogLevel::Warn);
livekit::shutdown();
// The SDK documents that initialize() may be called again after
// shutdown(); exercise that so a half-torn-down global state would show
// up here rather than in OBS.
ST_ASSERT(livekit::initialize(livekit::LogLevel::Error));
livekit::shutdown();
return st_test_report("livekit_smoke");
}
+91
View File
@@ -0,0 +1,91 @@
/*
streamer-tools OBS Camera Plugin - minimal test harness
Copyright (C) 2026 CyberCoveLLC <jknapp85@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program. If not, see <https://www.gnu.org/licenses/>
*/
#pragma once
// Deliberately dependency-free (no gtest/catch2) so the core library's test
// targets add no package-manager or network step to any of the three CI
// platforms. Unlike bare assert(), this keeps running after a failure and
// prints a real pass/fail count, so CI output says how much actually ran.
#include <cstdio>
#include <string>
namespace st_test_detail {
inline int &checks_run()
{
static int n = 0;
return n;
}
inline int &checks_failed()
{
static int n = 0;
return n;
}
inline std::string to_display(const std::string &v)
{
return "\"" + v + "\"";
}
inline std::string to_display(const char *v)
{
return v ? ("\"" + std::string(v) + "\"") : std::string("(null)");
}
inline std::string to_display(bool v)
{
return v ? "true" : "false";
}
template<typename T> inline std::string to_display(const T &v)
{
return std::to_string(v);
}
} // namespace st_test_detail
#define ST_ASSERT(expr) \
do { \
++st_test_detail::checks_run(); \
if (!(expr)) { \
++st_test_detail::checks_failed(); \
std::fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #expr); \
} \
} while (0)
#define ST_ASSERT_EQ(actual, expected) \
do { \
++st_test_detail::checks_run(); \
auto _st_a = (actual); \
auto _st_e = (expected); \
if (!(_st_a == _st_e)) { \
++st_test_detail::checks_failed(); \
std::fprintf(stderr, "FAIL %s:%d: %s\n actual: %s\n expected: %s\n", \
__FILE__, __LINE__, #actual " == " #expected, \
st_test_detail::to_display(_st_a).c_str(), \
st_test_detail::to_display(_st_e).c_str()); \
} \
} while (0)
inline int st_test_report(const char *suite)
{
const int run = st_test_detail::checks_run();
const int failed = st_test_detail::checks_failed();
if (failed == 0) {
std::printf("%s: %d checks passed\n", suite, run);
return 0;
}
std::printf("%s: %d/%d checks FAILED\n", suite, failed, run);
return 1;
}