Link the pinned LiveKit C++ SDK into the core library

Adds cmake/LiveKitSDK.cmake, adapted from
livekit-examples/cpp-example-collection's helper of the same name, with the
VERSION="latest" GitHub-API resolution path removed: this project pins an
exact client-sdk-cpp release (1.10.1, the newest tag as of today), and the
pin should not be silently bypassable. The module also now exports the
runtime shared libraries so packaging can stage liblivekit/liblivekit_ffi
next to the plugin module later.

core/ links LiveKit::livekit PUBLIC. A new smoke test proves the SDK is not
just linked but loadable and callable: livekit::initialize()/shutdown()
round-trip in-process, a second initialize() reports "already initialized",
the log level round-trips, and the SDK's generated LIVEKIT_BUILD_VERSION is
asserted equal to the version CMake pinned (so a stale extracted SDK
directory fails loudly rather than being silently reused).

Also adds core/tests/test_util.h, a dependency-free assertion harness that
keeps running after a failure and prints a pass/fail count, so CI output says
how much actually ran instead of dying on the first bare assert().

cmake_minimum_required goes 3.16 -> 3.19 (file(ARCHIVE_EXTRACT)).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL8abRmgFXkVASHkkqiJbE
This commit is contained in:
2026-09-06 21:22:14 -07:00
co-authored by Claude Sonnet 5
parent 304bc3ceba
commit e595173049
6 changed files with 417 additions and 16 deletions
+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;
}