diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml new file mode 100644 index 0000000..3b83fb1 --- /dev/null +++ b/.github/workflows/builds.yml @@ -0,0 +1,160 @@ +name: Build examples against latest LiveKit SDK (via CMake) + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + workflow_dispatch: + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + name: linux-x64 + - os: macos-latest + name: macos-arm64 + - os: windows-latest + name: windows-x64 + + name: Build (${{ matrix.name }}) + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + # ---------- deps ---------- + - name: Install deps (Ubuntu) + if: runner.os == 'Linux' + shell: bash + run: | + set -eux + sudo apt-get update + sudo apt-get install -y \ + cmake ninja-build pkg-config \ + protobuf-compiler libprotobuf-dev \ + libssl-dev \ + curl + + - name: Install deps (macOS) + if: runner.os == 'macOS' + shell: bash + run: | + set -eux + brew update + brew install cmake ninja protobuf + + - name: Install deps (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + choco install -y cmake --installargs 'ADD_CMAKE_TO_PATH=System' + choco install -y ninja + + - name: Setup MSVC (Windows) + if: runner.os == 'Windows' + uses: ilammy/msvc-dev-cmd@v1 + with: + arch: x64 + + # ---------- configure + build ---------- + - name: Configure (Unix) + if: runner.os != 'Windows' + shell: bash + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + set -eux + cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release + + - name: Build (Unix) + if: runner.os != 'Windows' + shell: bash + run: | + set -eux + cmake --build build --config Release + + - name: Configure (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + cmake -S . -B build -G Ninja ` + -DCMAKE_BUILD_TYPE=Release ` + -DCMAKE_C_COMPILER=cl ` + -DCMAKE_CXX_COMPILER=cl + + - name: Build (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + cmake --build build --config Release + + # ---------- smoke test ---------- + - name: Smoke test (Linux/macOS) + if: runner.os != 'Windows' + shell: bash + run: | + set -euo pipefail + + sdk_root="$(ls -d build/_deps/livekit-sdk/* | head -n 1)" + echo "SDK root: ${sdk_root}" + ls -la "${sdk_root}/lib" || true + # Locate executable (it may not be directly under build/) + exe="$(find build -type f -name basic_room -perm -111 | head -n 1)" + if [[ -z "${exe}" ]]; then + echo "basic_room executable not found under build/" + find build -maxdepth 3 -type f -print + exit 1 + fi + echo "Running: ${exe} --self-test" + if [[ "$RUNNER_OS" == "Linux" ]]; then + export LD_LIBRARY_PATH="${sdk_root}/lib:${LD_LIBRARY_PATH:-}" + else + export DYLD_LIBRARY_PATH="${sdk_root}/lib:${DYLD_LIBRARY_PATH:-}" + fi + "${exe}" --self-test + + + - name: Smoke test (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + # Locate SDK root + $sdkRoot = Get-ChildItem -Directory "build/_deps/livekit-sdk" | Select-Object -First 1 + if (-not $sdkRoot) { + throw "SDK root not found under build/_deps/livekit-sdk" + } + Write-Host "SDK root: $($sdkRoot.FullName)" + # Make sure DLLs are found at runtime + $env:PATH = "$($sdkRoot.FullName)\bin;$($sdkRoot.FullName)\lib;$env:PATH" + # Locate the built executable + $exe = Get-ChildItem -Recurse build -Filter basic_room.exe | Select-Object -First 1 + if (-not $exe) { + throw "basic_room.exe not found in build directory" + } + Write-Host "Running $($exe.FullName) --help" + # Try to execute it. We only care that it launches. + $out = & $exe.FullName --self-test 2>&1 + $code = $LASTEXITCODE + if ($code -ne 0) { + Write-Host $out + throw "basic_room.exe --self-test failed with exit code $code" + } + Write-Host $out + + # ---------- upload build output ---------- + - name: Upload binary + uses: actions/upload-artifact@v4 + with: + name: basic_room-${{ matrix.name }} + path: | + build/basic_room* + build/basic_room.exe + retention-days: 7 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a0132a0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +.cache/ +CMakeFiles/ +CMakeCache.txt +.DS_Store +Makefile +cmake_install.cmake +out +build/ +build-debug/ +build-release/ +vcpkg_installed/ +received_green.avif +docs/*.bak +docs/html/ +docs/latex/ +.vs/ +.vscode/ +# Compiled output +bin/ +lib/ +*.lib +*.a +*.so +*.dylib +*.dll +*.exe diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..7ba5e70 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.20) +project(livekit_cpp_example_collection LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Make "include(LiveKitSDK)" search in ./cmake +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") + +set(LIVEKIT_SDK_VERSION "latest" CACHE STRING "LiveKit C++ SDK version (e.g. 0.2.0 or latest)") + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") +include(LiveKitSDK) + +livekit_sdk_setup( + VERSION "${LIVEKIT_SDK_VERSION}" + SDK_DIR "${CMAKE_BINARY_DIR}/_deps/livekit-sdk" + GITHUB_TOKEN "$ENV{GITHUB_TOKEN}" +) + +find_package(LiveKit CONFIG REQUIRED) +add_subdirectory(basic_room) diff --git a/README.md b/README.md index a8c50fa..4ddb858 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,73 @@ # cpp-example-collection -A collection of small examples for the LiveKit C++ SDK: https://github.com/livekit/client-sdk-cpp +This repository contains a collection of small, self-contained examples for the +[LiveKit C++ SDK](https://github.com/livekit/client-sdk-cpp). + +The goal of these examples is to demonstrate common usage patterns of the +LiveKit C++ SDK (connecting to a room, publishing tracks, RPC, data streams, +etc.) without requiring users to build the SDK from source. + + +## How the SDK is provided + +These examples **automatically download a prebuilt LiveKit C++ SDK release** +from GitHub at CMake configure time. + +This is handled by the cmake helper: [LiveKitSDK.cmake ](https://github.com/livekit-examples/cpp-example-collection/cmake/LiveKitSDK.cmake) + +## Selecting a LiveKit SDK version + +By default, the examples download the **latest released** LiveKit C++ SDK. + +You can pin a specific SDK version using the `LIVEKIT_SDK_VERSION` CMake option. + +### Examples + +Use the latest release: +```bash +cmake -S . -B build +# Or use a specific version (recommended for reproducibility): +cmake -S . -B build -DLIVEKIT_SDK_VERSION=0.2.0 +``` + +Reconfigure to change versions: +```bash +rm -rf build +cmake -S . -B build -DLIVEKIT_SDK_VERSION=0.3.1 +``` + + +### Building the examples +#### macOS / Linux +```bash +cmake -S . -B build # add -DLIVEKIT_SDK_VERSION=0.2.0 if using a specific version +cmake --build build +``` + +#### Windows (Visual Studio generator) +```powershell +cmake -S . -B build # add -DLIVEKIT_SDK_VERSION=0.2.0 if using a specific version +cmake --build build --config Release +``` + +The Livekit Release SDK is downloaded into **build/_deps/livekit-sdk/** + +### Running the examples + +After building, example binaries are located under: +```bash +build// +``` + +For example: +```bash +./build/basic_room/basic_room --url --token +``` + +### Supported platforms + +Prebuilt SDKs are downloaded automatically for: +* Windows: x64 +* macOS: x64, arm64 (Apple Silicon) +* Linux: x64 + +If no matching SDK is available for your platform, CMake configuration will fail with a clear error. \ No newline at end of file diff --git a/basic_room/CMakeLists.txt b/basic_room/CMakeLists.txt new file mode 100644 index 0000000..59f24f6 --- /dev/null +++ b/basic_room/CMakeLists.txt @@ -0,0 +1,45 @@ +add_executable(basic_room + main.cpp + capture_utils.cpp + capture_utils.h +) + +target_include_directories(basic_room PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +target_link_libraries(basic_room PRIVATE LiveKit::livekit) + +# Make -llivekit_ffi resolvable +get_filename_component(_lk_cmake_dir "${LiveKit_DIR}" DIRECTORY) # .../lib/cmake +get_filename_component(_lk_lib_dir "${_lk_cmake_dir}" DIRECTORY) # .../lib +target_link_directories(basic_room PRIVATE "${_lk_lib_dir}") + + + +# Nice-to-have: copy runtime DLLs next to the exe on Windows for "run from build dir". +# Only do this if your exported package provides these targets. +if(WIN32) + # livekit_ffi.dll + if(TARGET LiveKit::livekit_ffi) + add_custom_command(TARGET basic_room POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "$" + "$" + ) + endif() + + # If you also export protobuf/abseil runtime targets, copy them too (optional). + if(TARGET protobuf::libprotobuf) + add_custom_command(TARGET basic_room POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "$" + "$" + ) + endif() + + if(TARGET absl::abseil_dll) + add_custom_command(TARGET basic_room POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "$" + "$" + ) + endif() +endif() diff --git a/basic_room/capture_utils.cpp b/basic_room/capture_utils.cpp new file mode 100644 index 0000000..08497b0 --- /dev/null +++ b/basic_room/capture_utils.cpp @@ -0,0 +1,125 @@ +/* + * Copyright 2025 LiveKit, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "capture_utils.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "livekit/livekit.h" + +using namespace livekit; + +// Test utils to run a capture loop to publish noisy audio frames to the room +void runNoiseCaptureLoop(const std::shared_ptr &source, + std::atomic &running_flag) { + const int sample_rate = source->sample_rate(); + const int num_channels = source->num_channels(); + const int frame_ms = 10; + const int samples_per_channel = sample_rate * frame_ms / 1000; + + // White noise generator (keep amplitude conservative to avoid clipping) + std::mt19937 rng{std::random_device{}()}; + std::uniform_int_distribution dist(-3000, 3000); + + using Clock = std::chrono::steady_clock; + auto next_deadline = Clock::now(); + + while (running_flag.load(std::memory_order_relaxed)) { + AudioFrame frame = + AudioFrame::create(sample_rate, num_channels, samples_per_channel); + auto &pcm = frame.data(); + for (auto &s : pcm) { + s = static_cast(dist(rng)); + } + + try { + source->captureFrame(frame); + } catch (const std::exception &e) { + std::cerr << "Error in captureFrame (noise): " << e.what() << std::endl; + break; + } + + next_deadline += std::chrono::milliseconds(frame_ms); + std::this_thread::sleep_until(next_deadline); + } + + try { + source->clearQueue(); + } catch (...) { + std::cerr << "Error in clearQueue (noise)" << std::endl; + } +} + +// Fake video source: solid color cycling +void runFakeVideoCaptureLoop(const std::shared_ptr &source, + std::atomic &running_flag) { + auto frame = VideoFrame::create(1280, 720, VideoBufferType::RGBA); + const double framerate = 1.0 / 30.0; + + while (running_flag.load(std::memory_order_relaxed)) { + static auto start = std::chrono::high_resolution_clock::now(); + float t = std::chrono::duration( + std::chrono::high_resolution_clock::now() - start) + .count(); + // Cycle every 4 seconds: 0=red, 1=green, 2=blue, 3=black + int stage = static_cast(t) % 4; + + std::array rgb{}; + switch (stage) { + case 0: // red + rgb = {255, 0, 0, 0}; + break; + case 1: // green + rgb = {0, 255, 0, 0}; + break; + case 2: // blue + rgb = {0, 0, 255, 0}; + break; + case 3: // black + default: + rgb = {0, 0, 0, 0}; + break; + } + + // ARGB + uint8_t *data = frame.data(); + const size_t size = frame.dataSize(); + for (size_t i = 0; i < size; i += 4) { + data[i + 0] = 255; // A + data[i + 1] = rgb[0]; // R + data[i + 2] = rgb[1]; // G + data[i + 3] = rgb[2]; // B + } + + try { + // If VideoSource is ARGB-capable, pass frame. + // If it expects I420, pass i420 instead. + source->captureFrame(frame, 0, VideoRotation::VIDEO_ROTATION_0); + } catch (const std::exception &e) { + std::cerr << "Error in captureFrame (fake video): " << e.what() + << std::endl; + break; + } + + std::this_thread::sleep_for(std::chrono::duration(framerate)); + } +} diff --git a/basic_room/capture_utils.h b/basic_room/capture_utils.h new file mode 100644 index 0000000..e80b243 --- /dev/null +++ b/basic_room/capture_utils.h @@ -0,0 +1,32 @@ +/* + * Copyright 2025 LiveKit, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +namespace livekit { +class AudioSource; +class VideoSource; +} // namespace livekit + +void runNoiseCaptureLoop(const std::shared_ptr &source, + std::atomic &running_flag); + +void runFakeVideoCaptureLoop( + const std::shared_ptr &source, + std::atomic &running_flag); diff --git a/basic_room/main.cpp b/basic_room/main.cpp new file mode 100644 index 0000000..daaa9ca --- /dev/null +++ b/basic_room/main.cpp @@ -0,0 +1,196 @@ +/* + * Copyright 2025 LiveKit, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "capture_utils.h" +#include "livekit/livekit.h" + +using namespace livekit; + +namespace { + +std::atomic g_running{true}; + +void handleSignal(int) { g_running.store(false); } + +void printUsage(const char *prog) { + std::cerr << "Usage:\n" + << " " << prog << " --url --token \n" + << "Env fallbacks:\n" + << " LIVEKIT_URL, LIVEKIT_TOKEN\n"; +} + +bool parseArgs(int argc, char *argv[], std::string &url, std::string &token, bool &self_test) { + for (int i = 1; i < argc; ++i) { + const std::string a = argv[i]; + if (a == "-h" || a == "--help") + return false; + + if (a == "--self-test") { + self_test = true; + return true; + } + + auto take = [&](std::string &out) -> bool { + if (i + 1 >= argc) + return false; + out = argv[++i]; + return true; + }; + + if (a == "--url") { + if (!take(url)) + return false; + } else if (a.rfind("--url=", 0) == 0) { + url = a.substr(std::string("--url=").size()); + } else if (a == "--token") { + if (!take(token)) + return false; + } else if (a.rfind("--token=", 0) == 0) { + token = a.substr(std::string("--token=").size()); + } + } + + if (url.empty()) { + if (const char *e = std::getenv("LIVEKIT_URL")) + url = e; + } + if (token.empty()) { + if (const char *e = std::getenv("LIVEKIT_TOKEN")) + token = e; + } + + return !(url.empty() || token.empty()); +} + +} // namespace + +int main(int argc, char *argv[]) { + std::string url, token; + bool self_test = false; + if (!parseArgs(argc, argv, url, token, self_test)) { + printUsage(argv[0]); + return 1; + } + if (self_test) { + livekit::initialize(livekit::LogSink::kConsole); + livekit::shutdown(); + std::cout << "self-test ok" << std::endl; + return 0; + } + + std::signal(SIGINT, handleSignal); + + // Init LiveKit + livekit::initialize(livekit::LogSink::kConsole); + + auto room = std::make_unique(); + + RoomOptions options; + options.auto_subscribe = true; + options.dynacast = false; + + std::cout << "Connecting to: " << url << "\n"; + if (!room->Connect(url, token, options)) { + std::cerr << "Failed to connect\n"; + livekit::shutdown(); + return 1; + } + + std::cout << "Connected.\n"; + + // ---- Create & publish AUDIO (noise) ---- + // Match your runNoiseCaptureLoop pacing: it assumes frame_ms=10. + auto audioSource = std::make_shared(48000, 1, 10); + auto audioTrack = + LocalAudioTrack::createLocalAudioTrack("noise", audioSource); + + TrackPublishOptions audioOpts; + audioOpts.source = TrackSource::SOURCE_MICROPHONE; + audioOpts.dtx = false; + audioOpts.simulcast = false; + + std::shared_ptr audioPub; + try { + audioPub = room->localParticipant()->publishTrack(audioTrack, audioOpts); + std::cout << "Published audio: sid=" << audioPub->sid() << "\n"; + } catch (const std::exception &e) { + std::cerr << "Failed to publish audio: " << e.what() << "\n"; + } + + // ---- Create & publish VIDEO (fake RGB) ---- + // Your helper uses VideoFrame::create(1280, 720, BGRA), so match that. + auto videoSource = std::make_shared(1280, 720); + auto videoTrack = LocalVideoTrack::createLocalVideoTrack("rgb", videoSource); + + TrackPublishOptions videoOpts; + videoOpts.source = TrackSource::SOURCE_CAMERA; + videoOpts.dtx = false; + videoOpts.simulcast = false; + + std::shared_ptr videoPub; + try { + videoPub = room->localParticipant()->publishTrack(videoTrack, videoOpts); + std::cout << "Published video: sid=" << videoPub->sid() << "\n"; + } catch (const std::exception &e) { + std::cerr << "Failed to publish video: " << e.what() << "\n"; + } + + // ---- Start synthetic capture loops ---- + std::atomic audio_running{true}; + std::atomic video_running{true}; + + std::thread audioThread( + [&] { runNoiseCaptureLoop(audioSource, audio_running); }); + std::thread videoThread( + [&] { runFakeVideoCaptureLoop(videoSource, video_running); }); + + // Keep alive until Ctrl-C + while (g_running.load()) { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + + // Stop loops and join threads + audio_running.store(false); + video_running.store(false); + + if (audioThread.joinable()) + audioThread.join(); + if (videoThread.joinable()) + videoThread.join(); + + // Best-effort unpublish + try { + if (audioPub) + room->localParticipant()->unpublishTrack(audioPub->sid()); + if (videoPub) + room->localParticipant()->unpublishTrack(videoPub->sid()); + } catch (...) { + } + + room.reset(); + livekit::shutdown(); + std::cout << "Exiting.\n"; + return 0; +} diff --git a/cmake/LiveKitSDK.cmake b/cmake/LiveKitSDK.cmake new file mode 100644 index 0000000..02d36fa --- /dev/null +++ b/cmake/LiveKitSDK.cmake @@ -0,0 +1,266 @@ +# LiveKitSDK.cmake +# +# Helper for example repos: +# - Downloads the appropriate prebuilt LiveKit C++ SDK release asset for the host OS/arch +# - Extracts it into a local directory (default: /_deps/livekit-sdk) +# - Prepends the extracted prefix to CMAKE_PREFIX_PATH so: +# find_package(LiveKit CONFIG REQUIRED) +# works out of the box. +# +# Usage: +# list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") +# include(LiveKitSDK) +# livekit_sdk_setup(VERSION "latest" SDK_DIR "${CMAKE_BINARY_DIR}/_deps/livekit-sdk") +# +# Optional: +# livekit_sdk_setup(VERSION "latest" REPO "livekit/client-sdk-cpp" GITHUB_TOKEN "$ENV{GITHUB_TOKEN}") + +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() + + # Use host processor; normalize common variants (case-insensitive) + set(_proc "${CMAKE_HOST_SYSTEM_PROCESSOR}") + 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) + _lk_detect_host(_os _arch) + if(_os STREQUAL "windows") + set(${out_ext} "zip" PARENT_SCOPE) + else() + set(${out_ext} "tar.gz" PARENT_SCOPE) + endif() +endfunction() + +# -------------------- GitHub API helpers -------------------- +# Resolve VERSION="latest" via GitHub API, returning version without leading "v". +function(_lk_resolve_latest_version out_version repo download_dir github_token) + if(NOT download_dir) + set(download_dir "${CMAKE_BINARY_DIR}/_downloads") + endif() + file(MAKE_DIRECTORY "${download_dir}") + + set(_api "https://api.github.com/repos/${repo}/releases/latest") + + # Sanitize repo for filename + string(REPLACE "/" "_" _repo_sanitized "${repo}") + set(_json "${download_dir}/livekit_latest_release_${_repo_sanitized}.json") + + # Build headers as a proper LIST (each element is one full header line) + set(_headers + "User-Agent: cmake-livekit-sdk/1.0" + "Accept: application/vnd.github+json" + "X-GitHub-Api-Version: 2022-11-28" + ) + + # Strip token (defensive: avoids accidental newline causing header splitting) + if(NOT "${github_token}" STREQUAL "") + string(STRIP "${github_token}" github_token) + endif() + + # Use Authorization only if token is non-empty + if(NOT "${github_token}" STREQUAL "") + # "token" is broadly compatible + list(APPEND _headers "Authorization: Bearer ${github_token}") + else() + message(STATUS "LiveKitSDK: no GITHUB_TOKEN provided; GitHub API may rate-limit.") + endif() + + # Capture LOG for actionable failure output + set(_dl_args + TLS_VERIFY ON + STATUS _st + LOG _log + ) + foreach(_h IN LISTS _headers) + list(APPEND _dl_args HTTPHEADER "${_h}") + endforeach() + file(DOWNLOAD "${_api}" "${_json}" ${_dl_args}) + + list(GET _st 0 _code) + list(GET _st 1 _msg) + if(NOT _code EQUAL 0) + message(STATUS "LiveKitSDK: GitHub API download log:\n${_log}") + if(EXISTS "${_json}") + file(READ "${_json}" _body) + message(STATUS "LiveKitSDK: GitHub API response body:\n${_body}") + endif() + message(FATAL_ERROR + "LiveKitSDK: failed to query latest release from GitHub API\n" + "API: ${_api}\n" + "Status: ${_code}\n" + "Message: ${_msg}\n" + "Tip: set GITHUB_TOKEN to avoid rate limits, or use VERSION=." + ) + endif() + + file(READ "${_json}" _content) + + # CMake >= 3.19 supports string(JSON ...) + string(JSON _tag GET "${_content}" tag_name) + if(_tag STREQUAL "") + message(FATAL_ERROR "LiveKitSDK: GitHub API response missing tag_name") + endif() + + # Strip leading "v" if present + string(REGEX REPLACE "^v" "" _ver "${_tag}") + set(${out_version} "${_ver}" PARENT_SCOPE) +endfunction() + +# -------------------- Public entrypoint -------------------- +# livekit_sdk_setup( +# VERSION +# SDK_DIR +# [REPO ] default: livekit/client-sdk-cpp +# [SHA256 ] optional: verify download (only works for fixed VERSION) +# [TRIPLE ] optional override +# [DOWNLOAD_DIR ] default: /_downloads +# [GITHUB_TOKEN ] optional: auth for GitHub API when VERSION=latest +# [NO_DOWNLOAD] error if not already present +# ) +function(livekit_sdk_setup) + set(options NO_DOWNLOAD) + set(oneValueArgs VERSION SDK_DIR REPO SHA256 TRIPLE DOWNLOAD_DIR GITHUB_TOKEN) + cmake_parse_arguments(LK "${options}" "${oneValueArgs}" "" ${ARGN}) + + if(NOT LK_VERSION) + message(FATAL_ERROR "livekit_sdk_setup: VERSION is required (use \"latest\" if desired)") + 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() + + # Resolve latest tag if requested + set(_resolved_version "${LK_VERSION}") + if(LK_VERSION STREQUAL "latest") + if(LK_SHA256) + message(WARNING "LiveKitSDK: SHA256 was provided but VERSION=latest; ignoring SHA256.") + set(LK_SHA256 "") + endif() + + if(NOT LK_GITHUB_TOKEN) + # Common in CI if you set env: GITHUB_TOKEN: ${{ github.token }} + set(LK_GITHUB_TOKEN "$ENV{GITHUB_TOKEN}") + endif() + + _lk_resolve_latest_version(_resolved_version "${LK_REPO}" "${LK_DOWNLOAD_DIR}" "${LK_GITHUB_TOKEN}") + message(STATUS "LiveKitSDK: resolved latest version = ${_resolved_version}") + endif() + + _lk_archive_ext(_ext) + set(_archive "livekit-sdk-${LK_TRIPLE}-${_resolved_version}.${_ext}") + set(_url "https://github.com/${LK_REPO}/releases/download/v${_resolved_version}/${_archive}") + + set(_archive_path "${LK_DOWNLOAD_DIR}/${_archive}") + + # The archive is expected to contain a top-level folder named: + # livekit-sdk--/ + set(_extracted_root "${LK_SDK_DIR}/livekit-sdk-${LK_TRIPLE}-${_resolved_version}") + + file(MAKE_DIRECTORY "${LK_DOWNLOAD_DIR}") + file(MAKE_DIRECTORY "${LK_SDK_DIR}") + + if(NOT EXISTS "${_extracted_root}") + if(LK_NO_DOWNLOAD) + message(FATAL_ERROR + "LiveKitSDK: SDK not found at:\n ${_extracted_root}\n" + "and 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) + message(STATUS "LiveKitSDK: download log:\n${_log}") + message(FATAL_ERROR "LiveKitSDK: download failed\nURL: ${_url}\nStatus: ${_code}\nMessage: ${_msg}") + 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") + message(FATAL_ERROR + "LiveKitSDK: extracted SDK does not look valid (missing lib/cmake)\n" + "Expected: ${_extracted_root}\n" + "If your archive root folder name differs, adjust _extracted_root logic." + ) + endif() + + # Make find_package(LiveKit CONFIG REQUIRED) work. + list(PREPEND CMAKE_PREFIX_PATH "${_extracted_root}") + set(CMAKE_PREFIX_PATH "${CMAKE_PREFIX_PATH}" PARENT_SCOPE) + + # Direct hint to the package config dir + set(LiveKit_DIR "${_extracted_root}/lib/cmake/LiveKit" PARENT_SCOPE) + + # Export a few useful variables for callers (optional). + set(LIVEKIT_SDK_EXTRACTED_ROOT "${_extracted_root}" CACHE PATH "LiveKit SDK extracted root" FORCE) + set(LIVEKIT_SDK_URL_USED "${_url}" CACHE STRING "LiveKit SDK URL used" FORCE) + set(LIVEKIT_SDK_VERSION_RESOLVED "${_resolved_version}" CACHE STRING "LiveKit SDK resolved version" FORCE) + set(LIVEKIT_SDK_TRIPLE_USED "${LK_TRIPLE}" CACHE STRING "LiveKit SDK triple used" FORCE) + + message(STATUS "LiveKitSDK: using SDK at ${_extracted_root}") +endfunction()