diff --git a/.github/workflows/make-release.yml b/.github/workflows/make-release.yml new file mode 100644 index 0000000..f9a790a --- /dev/null +++ b/.github/workflows/make-release.yml @@ -0,0 +1,134 @@ +name: C++ SDK +on: + push: + branches: ["main"] + tags: + - "cpp-sdks/livekit-cpp@*" + workflow_dispatch: + workflow_call: + inputs: + tag: + required: false + type: string + +env: + BUILD_TYPE: Release + TAG_NAME: ${{ inputs.tag || github.ref_name }} + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + name: linux-x86_64 + generator: Ninja + - os: windows-latest + name: windows-x86_64 + generator: "Visual Studio 17 2022" + - os: macos-latest + name: macos-arm64 + generator: Ninja + macos_arch: "arm64" + # optionally add x86_64 mac build if you need it: + # - os: macos-latest + # name: macos-x86_64 + # generator: Ninja + # macos_arch: "x86_64" + + name: Build (${{ matrix.name }}) + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Install Protoc + uses: arduino/setup-protoc@v2 + with: + version: "25.2" + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install deps (Ubuntu) + if: startsWith(matrix.os, 'ubuntu') + run: | + sudo apt-get update + sudo apt-get install -y ninja-build cmake pkg-config libprotobuf-dev libssl-dev + + - name: Install deps (macOS) + if: startsWith(matrix.os, 'macos') + run: | + brew update + brew install ninja cmake protobuf openssl abseil + + - name: Install deps (Windows) + if: startsWith(matrix.os, 'windows') + shell: pwsh + run: | + choco install ninja cmake -y + + - name: Build + bundle + shell: bash + run: | + chmod +x ./build.sh + args=(release -G "${{ matrix.generator }}" \ + --version "${{ steps.ver.outputs.version }}" \ + --bundle --prefix "sdk-out/livekit-sdk-${{ matrix.name }}") + if [[ "${{ runner.os }}" == "macOS" && -n "${{ matrix.macos_arch }}" ]]; then + args+=(--macos-arch "${{ matrix.macos_arch }}") + fi + ./build.sh "${args[@]}" + + - name: Archive (Unix) + if: ${{ !startsWith(matrix.os, 'windows') }} + shell: bash + run: | + tar -czf "livekit-sdk-${{ matrix.name }}.tar.gz" -C sdk-out "livekit-sdk-${{ matrix.name }}" + + - name: Archive (Windows) + if: startsWith(matrix.os, 'windows') + shell: pwsh + run: | + Compress-Archive -Path "sdk-out/livekit-sdk-${{ matrix.name }}/*" -DestinationPath "livekit-sdk-${{ matrix.name }}.zip" + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: sdk-builds-${{ matrix.name }} + path: | + livekit-sdk-${{ matrix.name }}.tar.gz + livekit-sdk-${{ matrix.name }}.zip + + release: + name: Release to GH (Draft) + runs-on: ubuntu-latest + needs: build + permissions: + contents: write + if: startsWith(inputs.tag || github.ref_name, 'cpp-sdks/livekit-cpp@') + env: + GH_TOKEN: ${{ github.token }} + steps: + - uses: actions/checkout@v4 + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + pattern: sdk-builds-* + merge-multiple: true + path: ${{ github.workspace }}/sdk-builds + + - name: Create draft release (idempotent) + run: | + gh release view "${{ env.TAG_NAME }}" >/dev/null 2>&1 || \ + gh release create "${{ env.TAG_NAME }}" --draft --title "${{ env.TAG_NAME }}" --generate-notes + + - name: Upload assets + run: | + gh release upload "${{ env.TAG_NAME }}" ${{ github.workspace }}/sdk-builds/*.zip ${{ github.workspace }}/sdk-builds/*.tar.gz + diff --git a/CMakeLists.txt b/CMakeLists.txt index 3e9a47d..f485154 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,11 +1,16 @@ cmake_minimum_required(VERSION 3.31.0) -project(livekit LANGUAGES C CXX) +project(livekit VERSION 0.1.0 LANGUAGES C CXX) # ---- C++ standard ---- set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON) +if(MSVC) + # Release: the prebuilt webrtc is using /MT, so we need to match that + set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>" CACHE STRING "" FORCE) +endif() + ############## Protobuf ################ # ---- Protobuf (FFI protos) ---- @@ -69,19 +74,19 @@ endif() string(TIMESTAMP BUILD_DATE "%Y-%m-%d %H:%M:%S") # Comment shown at the top of the generated header -set(GENERATED_COMMENT "This file was auto-generated by CMake on ${LIVEKIT_BUILD_DATE}. Do NOT edit manually. Edit build.h.in instead.") +set(GENERATED_COMMENT "This file was auto-generated by CMake on ${BUILD_DATE}. Do NOT edit manually. Edit build.h.in instead.") # Generate the header from the template +# Version baked into build.h (CI can override: -DLIVEKIT_VERSION=0.1.0) +if(NOT DEFINED LIVEKIT_VERSION OR LIVEKIT_VERSION STREQUAL "") + set(LIVEKIT_VERSION "0.0.0") +endif() configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/build.h.in" "${GENERATED_INCLUDE_DIR}/build.h" @ONLY ) -# Include the directory for the headers -include_directories("${GENERATED_INCLUDE_DIR}") - - ########### Livekit Rust SDK ####################### # Find cargo @@ -121,39 +126,51 @@ endif() # Imported Rust lib with per-config locations add_library(livekit_ffi STATIC IMPORTED GLOBAL) +if (WIN32) + set(RUST_LIB_DEBUG "${RUST_ROOT}/target/debug/livekit_ffi.lib") + set(RUST_LIB_RELEASE "${RUST_ROOT}/target/release/livekit_ffi.lib") +else() + set(RUST_LIB_DEBUG "${RUST_ROOT}/target/debug/liblivekit_ffi.a") + set(RUST_LIB_RELEASE "${RUST_ROOT}/target/release/liblivekit_ffi.a") +endif() set_target_properties(livekit_ffi PROPERTIES - IMPORTED_LOCATION_DEBUG "${RUST_ROOT}/target/debug/liblivekit_ffi.a" - IMPORTED_LOCATION_RELWITHDEBINFO "${RUST_ROOT}/target/release/liblivekit_ffi.a" - IMPORTED_LOCATION_MINSIZEREL "${RUST_ROOT}/target/release/liblivekit_ffi.a" - IMPORTED_LOCATION_RELEASE "${RUST_ROOT}/target/release/liblivekit_ffi.a" - INTERFACE_INCLUDE_DIRECTORIES "${RUST_ROOT}/livekit-ffi/include" + IMPORTED_CONFIGURATIONS "Debug;Release;RelWithDebInfo;MinSizeRel" + IMPORTED_LOCATION_DEBUG "${RUST_LIB_DEBUG}" + IMPORTED_LOCATION_RELEASE "${RUST_LIB_RELEASE}" + IMPORTED_LOCATION_RELWITHDEBINFO "${RUST_LIB_RELEASE}" + IMPORTED_LOCATION_MINSIZEREL "${RUST_LIB_RELEASE}" ) # Custom target that runs the script; no empty args get passed to cargo -add_custom_target(build_rust_ffi ALL +set(RUST_LIB "$,${RUST_LIB_DEBUG},${RUST_LIB_RELEASE}>") +add_custom_command( + OUTPUT "${RUST_LIB_DEBUG}" "${RUST_LIB_RELEASE}" COMMAND "${CMAKE_COMMAND}" -DCFG=$ -DRUST_ROOT=${RUST_ROOT} -DCARGO=${CARGO_EXECUTABLE} -P "${RUN_CARGO_SCRIPT}" - USES_TERMINAL - COMMENT "Invoking cargo for Rust FFI ($)" + WORKING_DIRECTORY "${RUST_ROOT}" + COMMENT "Building Rust FFI via cargo" VERBATIM ) +add_custom_target(build_rust_ffi ALL + DEPENDS "${RUST_LIB_DEBUG}" "${RUST_LIB_RELEASE}" +) -# A workaround to strip out the protozero_plugin.o symbols which cause our examples fail to link on Linux. -# Make sure CMAKE_AR is defined; if not, you can hardcode "ar" -if(NOT CMAKE_AR) +if (UNIX AND NOT APPLE) + # A workaround to strip out the protozero_plugin.o symbols which cause our examples fail to link on Linux. + # Make sure CMAKE_AR is defined; if not, you can hardcode "ar" find_program(CMAKE_AR ar REQUIRED) + add_custom_command( + TARGET build_rust_ffi + POST_BUILD + COMMAND ${CMAKE_AR} -dv + "$,${RUST_LIB_DEBUG},${RUST_LIB_RELEASE}>" + protozero_plugin.o + COMMENT "Removing protozero_plugin.o from Rust FFI archive") endif() -add_custom_command( - TARGET build_rust_ffi - POST_BUILD - COMMAND ${CMAKE_AR} -dv $ protozero_plugin.o - COMMENT "Removing protozero_plugin.o (stray main) from liblivekit_ffi.a" -) - # ---- C++ wrapper library ---- add_library(livekit include/livekit/audio_frame.h @@ -190,6 +207,7 @@ add_library(livekit src/ffi_handle.cpp src/ffi_client.cpp src/ffi_client.h + src/livekit.cpp src/local_audio_track.cpp src/remote_audio_track.cpp src/room.cpp @@ -218,7 +236,11 @@ add_library(livekit target_sources(livekit PRIVATE $) target_include_directories(livekit PUBLIC - ${CMAKE_SOURCE_DIR}/include + $ + $ +) + +target_include_directories(livekit PRIVATE ${CMAKE_SOURCE_DIR}/src ${RUST_ROOT}/livekit-ffi/include ${PROTO_BINARY_DIR} @@ -226,7 +248,7 @@ target_include_directories(livekit PUBLIC ) target_link_libraries(livekit - PUBLIC + PRIVATE livekit_ffi protobuf::libprotobuf ) @@ -235,7 +257,8 @@ message(STATUS "Protobuf: version=${Protobuf_VERSION}; protoc=${Protobuf_PROTOC_ # Ensure cargo runs before we try to link livekit add_dependencies(livekit build_rust_ffi) - +add_custom_target(livekit_ffi_artifacts DEPENDS "${RUST_LIB_DEBUG}" "${RUST_LIB_RELEASE}") +add_dependencies(livekit livekit_ffi_artifacts) ########### Platform specific settings ########### @@ -290,22 +313,21 @@ endif() # Only Protobuf >= 6 needs Abseil if (Protobuf_VERSION VERSION_GREATER_EQUAL 6.0) - # Try modern package name/namespace - find_package(absl CONFIG QUIET) # Some distros export as "Abseil::" + find_package(absl CONFIG QUIET) if (NOT absl_FOUND) find_package(Abseil QUIET) endif() if (absl_FOUND) - target_link_libraries(livekit PUBLIC + target_link_libraries(livekit PRIVATE absl::log absl::check absl::strings absl::base ) elseif (Abseil_FOUND) - target_link_libraries(livekit PUBLIC + target_link_libraries(livekit PRIVATE Abseil::log Abseil::check Abseil::strings @@ -324,7 +346,7 @@ endif() # On Linux, it needs to link OpenSSL if(UNIX AND NOT APPLE) find_package(OpenSSL REQUIRED) - target_link_libraries(livekit PUBLIC OpenSSL::SSL OpenSSL::Crypto) + target_link_libraries(livekit PRIVATE OpenSSL::SSL OpenSSL::Crypto) endif() @@ -335,6 +357,65 @@ else() target_compile_options(livekit PRIVATE -Wall -Wextra -Wpedantic) endif() +# -------------------- Install / Package (SDK bundle) -------------------- +include(GNUInstallDirs) +include(CMakePackageConfigHelpers) + +set(LIVEKIT_PACKAGE_NAME "LiveKit") +set(LIVEKIT_EXPORT_NAMESPACE "LiveKit::") + +# Install the library target +install(TARGETS livekit + EXPORT LiveKitTargets + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} # Windows .dll + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} # .so/.dylib + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR} # .a/.lib + INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} +) + +# Install public headers +install(DIRECTORY "${CMAKE_SOURCE_DIR}/include/" + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}" + FILES_MATCHING PATTERN "*.h" PATTERN "*.hpp" +) + +# Install generated build.h if it is part of the public headers +install(FILES "${GENERATED_INCLUDE_DIR}/build.h" + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/livekit" +) + +# ---- Create Config + Version + Targets for find_package(LiveKit CONFIG) ---- + +# cmake/LiveKitConfig.cmake.in must exist in your repo +configure_package_config_file( + "${CMAKE_SOURCE_DIR}/cmake/LiveKitConfig.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/LiveKitConfig.cmake" + INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/${LIVEKIT_PACKAGE_NAME}" +) + +# Generate a version file automatically from project(VERSION ...) +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/LiveKitConfigVersion.cmake" + VERSION "${PROJECT_VERSION}" + COMPATIBILITY SameMajorVersion +) + +# Export targets +install(EXPORT LiveKitTargets + FILE LiveKitTargets.cmake + NAMESPACE ${LIVEKIT_EXPORT_NAMESPACE} + DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/${LIVEKIT_PACKAGE_NAME}" +) + +# Install config files +install(FILES + "${CMAKE_CURRENT_BINARY_DIR}/LiveKitConfig.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/LiveKitConfigVersion.cmake" + DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/${LIVEKIT_PACKAGE_NAME}" +) + +# ------------------------------------------------------------------------ + # ---- Examples ---- add_subdirectory(examples) @@ -362,17 +443,17 @@ add_custom_target(cargo_clean add_custom_target(clean_all # 1) CMake clean (object files, libs, exes) COMMAND ${CMAKE_COMMAND} -E echo "==> CMake clean in: ${CMAKE_BINARY_DIR}" - COMMAND ${CMAKE_COMMAND} --build "${CMAKE_BINARY_DIR}" --target clean || true + COMMAND ${CMAKE_COMMAND} --build "${CMAKE_BINARY_DIR}" --target clean # 2) Cargo clean (Rust target/) COMMAND ${CMAKE_COMMAND} -E echo "==> cargo clean in: ${RUST_ROOT}" - COMMAND ${CMAKE_COMMAND} -E chdir "${RUST_ROOT}" "${CARGO_EXECUTABLE}" clean || true + COMMAND ${CMAKE_COMMAND} -E chdir "${RUST_ROOT}" "${CARGO_EXECUTABLE}" clean # 3) Remove generated protobufs (lives under build/) COMMAND ${CMAKE_COMMAND} -E echo "==> removing generated protobufs: ${PROTO_BINARY_DIR}" - COMMAND ${CMAKE_COMMAND} -E rm -rf "${PROTO_BINARY_DIR}" || true + COMMAND ${CMAKE_COMMAND} -E rm -rf "${PROTO_BINARY_DIR}" # 4) Remove the entire build directory (like `rm -rf build`) # Switch to SOURCE dir first so removing BINARY dir is always safe. COMMAND ${CMAKE_COMMAND} -E echo "==> removing build directory: ${CMAKE_BINARY_DIR}" - COMMAND ${CMAKE_COMMAND} -E chdir "${CMAKE_SOURCE_DIR}" ${CMAKE_COMMAND} -E rm -rf "${CMAKE_BINARY_DIR}" || true + COMMAND ${CMAKE_COMMAND} -E chdir "${CMAKE_SOURCE_DIR}" ${CMAKE_COMMAND} -E rm -rf "${CMAKE_BINARY_DIR}" COMMENT "Full clean: CMake outputs + Rust target + generated protos + delete build/" VERBATIM ) \ No newline at end of file diff --git a/README.md b/README.md index c3da39b..4d9125b 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ This SDK enables native C++ applications to connect to LiveKit servers for real- - **Rust / Cargo** (latest stable toolchain) - **Protobuf** compiler (`protoc`) - **macOS** users: System frameworks (CoreAudio, AudioToolbox, etc.) are automatically linked via CMake. +- **windows** users: Ninja, Visual Studio 2022 Build Tools (MSVC toolset + Windows SDK), Rust stable (MSVC toolchain) + Cargo, + vcpkg (recommended dependency manager on Windows) and install protobuf package via vcpkg - **Git LFS** (required for examples) Some example data files (e.g., audio assets) are stored using Git LFS. You must install Git LFS before cloning or pulling the repo if you want to run the examples. @@ -31,6 +33,7 @@ git submodule update --init --recursive ## ⚙️ BUILD All build actions are managed by the provided build.sh script. +**UNIX** ```bash ./build.sh clean # Clean CMake build artifacts ./build.sh clean-all # Deep clean (C++ + Rust + generated files) @@ -38,6 +41,21 @@ All build actions are managed by the provided build.sh script. ./build.sh release # Build Release version ./build.sh verbose # Verbose build output ``` +**Windows** +```bash +cmake -S . -B build -DCMAKE_TOOLCHAIN_FILE="$PWD/vcpkg/scripts/buildsystems/vcpkg.cmake" # Generate Makefiles in build folder +# Build (Release or Debug) +cmake --build build --config Release +# or: +cmake --build build --config Debug +# Clean CMake build artifacts +Remove-Item -Recurse -Force build +``` +Note (Windows), This assumes vcpkg is checked out in the repo root at .\vcpkg\. +You must install protobuf via vcpkg (so CMake can find ProtobufConfig.cmake and protoc), for example: +```bash +.\vcpkg\vcpkg install protobuf:x64-windows +``` ## 🧪 Run Example diff --git a/build.h.in b/build.h.in index eb7e8c3..68d8e81 100644 --- a/build.h.in +++ b/build.h.in @@ -4,7 +4,8 @@ #pragma once -#define LIVEKIT_BUILD_VERSION "0.1.0" // Manually maintained +// Version injected by CMake (CI should set it). Defaults to 0.0.0 for dev builds. +#define LIVEKIT_BUILD_VERSION "@LIVEKIT_VERSION@" #define LIVEKIT_BUILD_FLAVOR "cpp" #ifdef DEBUG diff --git a/build.sh b/build.sh index 0a41604..baca0e4 100755 --- a/build.sh +++ b/build.sh @@ -5,33 +5,151 @@ PROJECT_ROOT="$(cd "$(dirname "$0")" && pwd)" BUILD_DIR="${PROJECT_ROOT}/build" BUILD_TYPE="Release" VERBOSE="" -TARGET="" +GENERATOR="" # optional, e.g. Ninja +PREFIX="" # install prefix for bundling +DO_BUNDLE="0" # whether to run cmake --install +DO_ARCHIVE="0" # whether to create .tar.gz/.zip +ARCHIVE_NAME="" # optional override +MACOS_ARCH="" # optional: arm64 or x86_64 (for macOS only) usage() { cat < [options] Commands: debug Configure + build Debug version release Configure + build Release version + verbose Build with verbose output (uses last configured build) clean Run CMake's built-in clean target - clean-all Run clean_all (clears C++ + Rust targets) - verbose Build with verbose output (implies last configured type) - help Show this help + clean-all Run full clean (C++ build + Rust targets + generated files) + help Show this help message + +Options (for debug / release / verbose): + --bundle Install the SDK bundle using 'cmake --install' + --prefix Install prefix for --bundle + (default: ./sdk-out/livekit-sdk) + --archive After --bundle, create an archive of the SDK bundle + (.zip if available, otherwise .tar.gz) + --archive-name Override archive base name (no extension) + --version Inject SDK version into build (sets LIVEKIT_VERSION) + Example: 0.1.0 or 1.0.0-rc1 + -G CMake generator (e.g. Ninja, "Unix Makefiles") + --macos-arch macOS architecture override (arm64 or x86_64) + Sets CMAKE_OSX_ARCHITECTURES Examples: - ./build.sh debug ./build.sh release - ./build.sh clean - ./build.sh clean-all - ./build.sh verbose + ./build.sh release --bundle + ./build.sh release --bundle --archive + ./build.sh release --bundle --prefix ./sdk-out/livekit-sdk-macos-arm64 + ./build.sh debug --bundle --prefix /tmp/livekit-sdk-debug + ./build.sh release --version 0.1.0 --bundle --archive + ./build.sh release -G Ninja --macos-arch arm64 --bundle \\ + --archive-name livekit-sdk-0.1.0-macos-arm64 + +Notes: + - '--bundle' installs a consumable SDK layout containing: + * headers under include/ + * libraries under lib/ (and bin/ if shared) + * CMake package files under lib/cmake/LiveKit/ + - '--archive' requires '--bundle' + - CI builds should use '--version' to ensure build.h matches the release tag EOF } + +parse_opts() { + shift || true + while [[ $# -gt 0 ]]; do + case "$1" in + --bundle) + DO_BUNDLE="1" + shift + ;; + --prefix) + PREFIX="${2:-}" + if [[ -z "${PREFIX}" ]]; then + echo "ERROR: --prefix requires a value" + exit 1 + fi + shift 2 + ;; + --archive) + DO_ARCHIVE="1" + shift + ;; + --archive-name) + ARCHIVE_NAME="${2:-}" + if [[ -z "${ARCHIVE_NAME}" ]]; then + echo "ERROR: --archive-name requires a value" + exit 1 + fi + shift 2 + ;; + -G) + GENERATOR="${2:-}" + if [[ -z "${GENERATOR}" ]]; then + echo "ERROR: -G requires a generator name" + exit 1 + fi + shift 2 + ;; + --macos-arch) + MACOS_ARCH="${2:-}" + if [[ -z "${MACOS_ARCH}" ]]; then + echo "ERROR: --macos-arch requires a value (arm64 or x86_64)" + exit 1 + fi + shift 2 + ;; + --version) + LIVEKIT_VERSION="${2:-}" + [[ -n "${LIVEKIT_VERSION}" ]] || { echo "ERROR: --version requires a value"; exit 1; } + shift 2 + ;; + -h|--help|help) + usage + exit 0 + ;; + *) + echo "ERROR: Unknown option: $1" + usage + exit 1 + ;; + esac + done +} + configure() { echo "==> Configuring CMake (${BUILD_TYPE})..." - cmake -S . -B "${BUILD_DIR}" -DCMAKE_BUILD_TYPE="${BUILD_TYPE}" + + local cmake_args=( + -S "${PROJECT_ROOT}" + -B "${BUILD_DIR}" + ) + + # Generator + if [[ -n "${GENERATOR}" ]]; then + cmake_args+=(-G "${GENERATOR}") + fi + + # Version + if [[ -n "${LIVEKIT_VERSION:-}" ]]; then + cmake_args+=(-DLIVEKIT_VERSION="${LIVEKIT_VERSION}") + fi + + # Build type (single-config generators like Ninja/Unix Makefiles) + # For Visual Studio/Xcode multi-config, this is mostly ignored but harmless. + cmake_args+=(-DCMAKE_BUILD_TYPE="${BUILD_TYPE}") + + # macOS arch override (only if on macOS and provided) + if [[ "$(uname -s)" == "Darwin" && -n "${MACOS_ARCH}" ]]; then + cmake_args+=(-DCMAKE_OSX_ARCHITECTURES="${MACOS_ARCH}") + fi + + cmake "${cmake_args[@]}" } build() { @@ -39,6 +157,70 @@ build() { cmake --build "${BUILD_DIR}" -j ${VERBOSE:+--verbose} } +install_bundle() { + # Default prefix if user asked for --bundle but didn't set one + if [[ -z "${PREFIX}" ]]; then + PREFIX="${PROJECT_ROOT}/sdk-out/livekit-sdk" + fi + + # Make prefix absolute for nicer archives + if [[ "${PREFIX}" != /* ]]; then + PREFIX="${PROJECT_ROOT}/${PREFIX}" + fi + + echo "==> Installing SDK bundle to: ${PREFIX}" + rm -rf "${PREFIX}" + mkdir -p "${PREFIX}" + + # Use --config for safety (works for multi-config too) + cmake --install "${BUILD_DIR}" --config "${BUILD_TYPE}" --prefix "${PREFIX}" + + # Sanity checks (non-fatal, but helpful) + if [[ ! -d "${PREFIX}/include" ]]; then + echo "WARN: ${PREFIX}/include not found. Did you add install(DIRECTORY include/ ...) rules?" + fi + if [[ ! -d "${PREFIX}/lib" && ! -d "${PREFIX}/lib64" ]]; then + echo "WARN: ${PREFIX}/lib or lib64 not found. Did you add install(TARGETS ...) rules?" + fi + if [[ ! -d "${PREFIX}/lib/cmake/LiveKit" && ! -d "${PREFIX}/lib64/cmake/LiveKit" ]]; then + echo "WARN: CMake package files not found under lib/cmake/LiveKit. Did you add install(EXPORT ...) + LiveKitConfig.cmake?" + fi +} + +archive_bundle() { + if [[ "${DO_BUNDLE}" != "1" ]]; then + echo "ERROR: --archive requires --bundle" + exit 1 + fi + + local base + if [[ -n "${ARCHIVE_NAME}" ]]; then + base="${ARCHIVE_NAME}" + else + base="$(basename "${PREFIX}")" + fi + + local out_dir + out_dir="$(dirname "${PREFIX}")" + + echo "==> Archiving bundle..." + pushd "${out_dir}" >/dev/null + + # Prefer zip if available and user is on macOS/Linux too; otherwise tar.gz + if command -v zip >/dev/null 2>&1; then + rm -f "${base}.zip" + # Zip the directory (preserve folder root) + zip -r "${base}.zip" "${base}" >/dev/null + echo "==> Wrote: ${out_dir}/${base}.zip" + else + rm -f "${base}.tar.gz" + tar -czf "${base}.tar.gz" "${base}" + echo "==> Wrote: ${out_dir}/${base}.tar.gz" + fi + + popd >/dev/null +} + clean() { echo "==> Cleaning CMake targets..." if [[ -d "${BUILD_DIR}" ]]; then @@ -62,26 +244,48 @@ clean_all() { echo "==> Clean-all complete." } - if [[ $# -eq 0 ]]; then usage exit 0 fi -case "$1" in +cmd="$1" +case "${cmd}" in debug) BUILD_TYPE="Debug" + parse_opts "$@" configure build + if [[ "${DO_BUNDLE}" == "1" ]]; then + install_bundle + if [[ "${DO_ARCHIVE}" == "1" ]]; then + archive_bundle + fi + fi ;; release) BUILD_TYPE="Release" + parse_opts "$@" configure build + if [[ "${DO_BUNDLE}" == "1" ]]; then + install_bundle + if [[ "${DO_ARCHIVE}" == "1" ]]; then + archive_bundle + fi + fi ;; verbose) VERBOSE="1" + # Optional: allow --bundle with verbose builds as well, but requires configure already ran. + parse_opts "$@" build + if [[ "${DO_BUNDLE}" == "1" ]]; then + install_bundle + if [[ "${DO_ARCHIVE}" == "1" ]]; then + archive_bundle + fi + fi ;; clean) clean @@ -89,14 +293,11 @@ case "$1" in clean-all) clean_all ;; - distclean) - distclean - ;; help|-h|--help) usage ;; *) - echo "Unknown command: $1" + echo "Unknown command: ${cmd}" usage exit 1 ;; diff --git a/cmake/LiveKitConfig.cmake.in b/cmake/LiveKitConfig.cmake.in new file mode 100644 index 0000000..3f53938 --- /dev/null +++ b/cmake/LiveKitConfig.cmake.in @@ -0,0 +1,3 @@ +@PACKAGE_INIT@ + +include("${CMAKE_CURRENT_LIST_DIR}/LiveKitTargets.cmake") diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 020d175..bfac7e4 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,74 +1,91 @@ cmake_minimum_required(VERSION 3.31.0) -project (livekit-examples) +project(livekit-examples) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -#################### SimpleRoom example ########################## +include(FetchContent) -list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") -include(sdl3) +set(SDL_TEST OFF CACHE BOOL "" FORCE) +set(SDL_TESTS OFF CACHE BOOL "" FORCE) +set(SDL_INSTALL OFF CACHE BOOL "" FORCE) +set(SDL_EXAMPLES OFF CACHE BOOL "" FORCE) -add_executable(SimpleRoom - simple_room/main.cpp - simple_room/fallback_capture.cpp - simple_room/fallback_capture.h - simple_room/sdl_media.cpp - simple_room/sdl_media.h - simple_room/sdl_media_manager.cpp - simple_room/sdl_media_manager.h - simple_room/sdl_video_renderer.cpp - simple_room/sdl_video_renderer.h - simple_room/wav_audio_source.cpp - simple_room/wav_audio_source.h +FetchContent_Declare( + SDL3 + GIT_REPOSITORY https://github.com/libsdl-org/SDL.git + GIT_TAG release-3.2.0 ) +FetchContent_MakeAvailable(SDL3) -target_link_libraries(SimpleRoom - PRIVATE - livekit - SDL3::SDL3 +function(livekit_windows_link_deps target) + if (WIN32) + target_link_libraries(${target} PRIVATE + ntdll + userenv + winmm + iphlpapi + ole32 + uuid + msdmo + dmoguids + strmiids + wmcodecdspuuid + ws2_32 + bcrypt + ) + endif() +endfunction() + +add_executable(SimpleRoom + simple_room/main.cpp + simple_room/fallback_capture.cpp + simple_room/fallback_capture.h + simple_room/sdl_media.cpp + simple_room/sdl_media.h + simple_room/sdl_media_manager.cpp + simple_room/sdl_media_manager.h + simple_room/sdl_video_renderer.cpp + simple_room/sdl_video_renderer.h + simple_room/wav_audio_source.cpp + simple_room/wav_audio_source.h ) +target_link_libraries(SimpleRoom PRIVATE livekit SDL3::SDL3) +livekit_windows_link_deps(SimpleRoom) + add_custom_command(TARGET SimpleRoom POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_directory - ${CMAKE_SOURCE_DIR}/data - ${CMAKE_CURRENT_BINARY_DIR}/data + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${CMAKE_SOURCE_DIR}/data + ${CMAKE_CURRENT_BINARY_DIR}/data ) -#################### SimpleRpc example ########################## - -include(FetchContent) FetchContent_Declare( nlohmann_json URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz ) FetchContent_MakeAvailable(nlohmann_json) -add_executable(SimpleRpc - simple_rpc/main.cpp -) - -target_link_libraries(SimpleRpc - PRIVATE - nlohmann_json::nlohmann_json - livekit -) +add_executable(SimpleRpc simple_rpc/main.cpp) +target_link_libraries(SimpleRpc PRIVATE nlohmann_json::nlohmann_json livekit) +livekit_windows_link_deps(SimpleRpc) -#################### SimpleDataStream example ########################## - -add_executable(SimpleDataStream - simple_data_stream/main.cpp -) +add_executable(SimpleDataStream simple_data_stream/main.cpp) +target_link_libraries(SimpleDataStream PRIVATE livekit) +livekit_windows_link_deps(SimpleDataStream) -target_link_libraries(SimpleDataStream - PRIVATE - livekit -) - -add_custom_command( - TARGET SimpleDataStream - POST_BUILD +add_custom_command(TARGET SimpleDataStream POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/data $/data -) \ No newline at end of file +) + +if (WIN32 AND TARGET SDL3::SDL3) + foreach(tgt SimpleRoom SimpleRpc SimpleDataStream) + add_custom_command(TARGET ${tgt} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ + $ + ) + endforeach() +endif() \ No newline at end of file diff --git a/examples/simple_data_stream/main.cpp b/examples/simple_data_stream/main.cpp index b5ecbce..f9db640 100644 --- a/examples/simple_data_stream/main.cpp +++ b/examples/simple_data_stream/main.cpp @@ -7,13 +7,12 @@ #include #include #include +#include #include #include #include #include "livekit/livekit.h" -// TODO, remove the ffi_client from the public usage. -#include "ffi_client.h" using namespace livekit; @@ -210,6 +209,8 @@ int main(int argc, char *argv[]) { std::signal(SIGTERM, handleSignal); #endif + // Initialize the livekit with logging to console. + livekit::initialize(livekit::LogSink::kConsole); auto room = std::make_unique(); RoomOptions options; options.auto_subscribe = true; @@ -219,7 +220,7 @@ int main(int argc, char *argv[]) { std::cout << "[DataStream] Connect result: " << std::boolalpha << ok << "\n"; if (!ok) { std::cerr << "[DataStream] Failed to connect to room\n"; - FfiClient::instance().shutdown(); + livekit::shutdown(); return 1; } @@ -278,6 +279,6 @@ int main(int argc, char *argv[]) { // It is important to clean up the delegate and room in order. room->setDelegate(nullptr); room.reset(); - FfiClient::instance().shutdown(); + livekit::shutdown(); return 0; } diff --git a/examples/simple_room/fallback_capture.cpp b/examples/simple_room/fallback_capture.cpp index e0c3c8c..52a3721 100644 --- a/examples/simple_room/fallback_capture.cpp +++ b/examples/simple_room/fallback_capture.cpp @@ -16,7 +16,11 @@ #include "fallback_capture.h" +#include +#include #include +#include +#include #include #include "livekit/livekit.h" diff --git a/examples/simple_room/main.cpp b/examples/simple_room/main.cpp index b4bfcce..71b0aff 100644 --- a/examples/simple_room/main.cpp +++ b/examples/simple_room/main.cpp @@ -29,11 +29,6 @@ #include "livekit/livekit.h" #include "sdl_media_manager.h" #include "wav_audio_source.h" -// TODO, remove the ffi_client from the public usage. -#include "ffi_client.h" - -// Consider expose this video_utils.h to public ? -#include "video_utils.h" using namespace livekit; @@ -269,6 +264,8 @@ int main(int argc, char *argv[]) { // Handle Ctrl-C to exit the idle loop std::signal(SIGINT, handleSignal); + // Initialize the livekit with logging to console. + livekit::initialize(livekit::LogSink::kConsole); auto room = std::make_unique(); SimpleRoomDelegate delegate(media); room->setDelegate(&delegate); @@ -299,7 +296,7 @@ int main(int argc, char *argv[]) { std::cout << "Connect result is " << std::boolalpha << res << std::endl; if (!res) { std::cerr << "Failed to connect to room\n"; - FfiClient::instance().shutdown(); + livekit::shutdown(); return 1; } @@ -404,7 +401,7 @@ int main(int argc, char *argv[]) { room.reset(); - FfiClient::instance().shutdown(); + livekit::shutdown(); std::cout << "Exiting.\n"; return 0; } diff --git a/examples/simple_room/sdl_media_manager.cpp b/examples/simple_room/sdl_media_manager.cpp index 58d3bd5..aef067f 100644 --- a/examples/simple_room/sdl_media_manager.cpp +++ b/examples/simple_room/sdl_media_manager.cpp @@ -18,165 +18,12 @@ #include "fallback_capture.h" #include "livekit/livekit.h" +#include "sdl_media.h" #include "sdl_video_renderer.h" #include #include #include - -// ---------------- SDLMicSource ---------------- - -class SDLMicSource { -public: - using AudioCallback = - std::function; - - SDLMicSource(int sample_rate, int channels, int frame_samples, - AudioCallback cb) - : sample_rate_(sample_rate), channels_(channels), - frame_samples_(frame_samples), callback_(std::move(cb)) {} - - ~SDLMicSource() { - if (stream_) { - SDL_DestroyAudioStream(stream_); - stream_ = nullptr; - } - } - - bool init() { - SDL_zero(spec_); - spec_.format = SDL_AUDIO_S16; - spec_.channels = static_cast(channels_); - spec_.freq = sample_rate_; - - stream_ = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_RECORDING, - &spec_, nullptr, nullptr); - - if (!stream_) { - std::cerr << "Failed to open recording stream: " << SDL_GetError() - << std::endl; - return false; - } - - if (!SDL_ResumeAudioStreamDevice(stream_)) { - std::cerr << "Failed to resume recording device: " << SDL_GetError() - << std::endl; - return false; - } - - return true; - } - - void pump() { - if (!stream_ || !callback_) - return; - - const int totalSamples = frame_samples_ * channels_; - const int bytes_per_frame = - totalSamples * static_cast(sizeof(int16_t)); - - const int available = SDL_GetAudioStreamAvailable(stream_); - if (available < bytes_per_frame) - return; - - std::vector buffer(totalSamples); - - const int got_bytes = - SDL_GetAudioStreamData(stream_, buffer.data(), bytes_per_frame); - if (got_bytes <= 0) - return; - - const int samplesTotal = got_bytes / static_cast(sizeof(int16_t)); - const int samplesPerChannel = samplesTotal / channels_; - - callback_(buffer.data(), samplesPerChannel, sample_rate_, channels_); - } - -private: - SDL_AudioStream *stream_ = nullptr; - SDL_AudioSpec spec_{}; - int sample_rate_; - int channels_; - int frame_samples_; - AudioCallback callback_; -}; - -// ---------------- SDLCamSource ---------------- - -class SDLCamSource { -public: - using VideoCallback = std::function; - - SDLCamSource(int desired_width, int desired_height, int desired_fps, - SDL_PixelFormat pixelFormat, VideoCallback cb) - : width_(desired_width), height_(desired_height), fps_(desired_fps), - format_(pixelFormat), callback_(std::move(cb)) {} - - ~SDLCamSource() { - if (camera_) { - SDL_CloseCamera(camera_); - camera_ = nullptr; - } - } - - bool init() { - int count = 0; - SDL_CameraID *cams = SDL_GetCameras(&count); - if (!cams || count == 0) { - std::cerr << "No camera devices found (SDL): " << SDL_GetError() - << std::endl; - if (cams) - SDL_free(cams); - return false; - } - - SDL_CameraID camId = cams[0]; - SDL_free(cams); - - SDL_zero(spec_); - spec_.format = format_; - spec_.width = width_; - spec_.height = height_; - spec_.framerate_numerator = fps_; - spec_.framerate_denominator = 1; - - camera_ = SDL_OpenCamera(camId, &spec_); - if (!camera_) { - std::cerr << "Failed to open camera: " << SDL_GetError() << std::endl; - return false; - } - - return true; - } - - void pump() { - if (!camera_ || !callback_) - return; - - Uint64 tsNS = 0; - SDL_Surface *surf = SDL_AcquireCameraFrame(camera_, &tsNS); - if (!surf) - return; - - callback_(static_cast(surf->pixels), surf->pitch, surf->w, - surf->h, surf->format, tsNS); - - SDL_ReleaseCameraFrame(camera_, surf); - } - -private: - SDL_Camera *camera_ = nullptr; - SDL_CameraSpec spec_{}; - int width_; - int height_; - int fps_; - SDL_PixelFormat format_; - VideoCallback callback_; -}; - -// ---------------- SDLMediaManager implementation ---------------- +using namespace livekit; SDLMediaManager::SDLMediaManager() = default; diff --git a/examples/simple_rpc/main.cpp b/examples/simple_rpc/main.cpp index b3ed6d4..f0d34b2 100644 --- a/examples/simple_rpc/main.cpp +++ b/examples/simple_rpc/main.cpp @@ -33,8 +33,6 @@ #include #include "livekit/livekit.h" -// TODO, remove the ffi_client from the public usage. -#include "ffi_client.h" using namespace livekit; using namespace std::chrono_literals; @@ -453,6 +451,8 @@ int main(int argc, char *argv[]) { // Ctrl-C to quit the program std::signal(SIGINT, handleSignal); + // Initialize the livekit with logging to console. + livekit::initialize(livekit::LogSink::kConsole); auto room = std::make_unique(); RoomOptions options; options.auto_subscribe = true; @@ -462,7 +462,7 @@ int main(int argc, char *argv[]) { std::cout << "Connect result is " << std::boolalpha << res << "\n"; if (!res) { std::cerr << "Failed to connect to room\n"; - FfiClient::instance().shutdown(); + livekit::shutdown(); return 1; } @@ -542,6 +542,6 @@ int main(int argc, char *argv[]) { // It is important to clean up the delegate and room in order. room->setDelegate(nullptr); room.reset(); - FfiClient::instance().shutdown(); + livekit::shutdown(); return 0; } diff --git a/include/livekit/livekit.h b/include/livekit/livekit.h index 9666d13..391bf2e 100644 --- a/include/livekit/livekit.h +++ b/include/livekit/livekit.h @@ -14,6 +14,8 @@ * limitations under the License. */ +#pragma once + #include "audio_frame.h" #include "audio_source.h" #include "audio_stream.h" @@ -31,4 +33,33 @@ #include "track_publication.h" #include "video_frame.h" #include "video_source.h" -#include "video_stream.h" \ No newline at end of file +#include "video_stream.h" + +namespace livekit { + +/// Where LiveKit logs should go. +enum class LogSink { + /// Logs are printed to the default console output (FFI prints directly). + kConsole = 0, + + /// Logs are delivered to the application's FFI callback for capturing. + kCallback = 1, +}; + +/// Initialize the LiveKit SDK. +/// +/// This **must be the first LiveKit API called** in the process. +/// It configures global SDK state, including log routing. +/// +/// If LiveKit APIs are used before calling this function, the log +/// configuration may not take effect as expected. +/// Returns true if initialization happened on this call, false if it was +/// already initialized. +bool initialize(LogSink log_sink = LogSink::kConsole); + +/// Shut down the LiveKit SDK. +/// +/// After shutdown, you may call initialize() again. +void shutdown(); + +} // namespace livekit \ No newline at end of file diff --git a/include/livekit/participant.h b/include/livekit/participant.h index 9b82b28..5987963 100644 --- a/include/livekit/participant.h +++ b/include/livekit/participant.h @@ -23,7 +23,6 @@ #include "livekit/ffi_handle.h" #include "livekit/room_delegate.h" -#include "livekit_ffi.h" namespace livekit { diff --git a/src/ffi_client.cpp b/src/ffi_client.cpp index 6892fce..efb7840 100644 --- a/src/ffi_client.cpp +++ b/src/ffi_client.cpp @@ -39,12 +39,33 @@ std::string bytesToString(const std::vector &b) { } // namespace -FfiClient::FfiClient() { - livekit_ffi_initialize(&LivekitFfiCallback, false, LIVEKIT_BUILD_FLAVOR, - LIVEKIT_BUILD_VERSION_FULL); +FfiClient::~FfiClient() { + assert(!initialized_.load() && + "LiveKit SDK was not shut down before process exit. " + "Call livekit::shutdown()."); } -void FfiClient::shutdown() noexcept { livekit_ffi_dispose(); } +void FfiClient::shutdown() noexcept { + if (!isInitialized()) { + return; + } + initialized_.store(false, std::memory_order_release); + livekit_ffi_dispose(); +} + +bool FfiClient::initialize(bool capture_logs) { + if (isInitialized()) { + return false; + } + initialized_.store(true, std::memory_order_release); + livekit_ffi_initialize(&LivekitFfiCallback, capture_logs, + LIVEKIT_BUILD_FLAVOR, LIVEKIT_BUILD_VERSION_FULL); + return true; +} + +bool FfiClient::isInitialized() const noexcept { + return initialized_.load(std::memory_order_acquire); +} FfiClient::ListenerId FfiClient::AddListener(const FfiClient::Listener &listener) { diff --git a/src/ffi_client.h b/src/ffi_client.h index 9851af1..36cf72c 100644 --- a/src/ffi_client.h +++ b/src/ffi_client.h @@ -60,6 +60,7 @@ class FfiClient { using Listener = std::function; using AsyncId = std::uint64_t; + ~FfiClient(); FfiClient(const FfiClient &) = delete; FfiClient &operator=(const FfiClient &) = delete; FfiClient(FfiClient &&) = delete; @@ -70,10 +71,15 @@ class FfiClient { return instance; } + // Must be called before any other FFI usage + bool initialize(bool capture_logs); + // Called only once. After calling shutdown(), no further calls into FfiClient // are valid. void shutdown() noexcept; + bool isInitialized() const noexcept; + ListenerId AddListener(const Listener &listener); void RemoveListener(ListenerId id); @@ -137,6 +143,8 @@ class FfiClient { proto::FfiResponse sendRequest(const proto::FfiRequest &request) const; private: + FfiClient() = default; + // Base class for type-erased pending ops struct PendingBase { virtual ~PendingBase() = default; @@ -167,11 +175,9 @@ class FfiClient { mutable std::mutex lock_; mutable std::vector> pending_; - FfiClient(); - ~FfiClient() = default; - void PushEvent(const proto::FfiEvent &event) const; friend void LivekitFfiCallback(const uint8_t *buf, size_t len); + std::atomic initialized_{false}; }; } // namespace livekit diff --git a/src/livekit.cpp b/src/livekit.cpp new file mode 100644 index 0000000..d8756e0 --- /dev/null +++ b/src/livekit.cpp @@ -0,0 +1,32 @@ +/* + * Copyright 2023 LiveKit + * + * 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 "livekit/livekit.h" +#include "ffi_client.h" + +namespace livekit { + +bool initialize(LogSink log_sink) { + auto &ffi_client = FfiClient::instance(); + return ffi_client.initialize(log_sink == LogSink::kCallback); +} + +void shutdown() { + auto &ffi_client = FfiClient::instance(); + ffi_client.shutdown(); +} + +} // namespace livekit \ No newline at end of file diff --git a/src/room.cpp b/src/room.cpp index a14a4d9..91f19a7 100644 --- a/src/room.cpp +++ b/src/room.cpp @@ -30,6 +30,7 @@ #include "ffi.pb.h" #include "ffi_client.h" +#include "livekit_ffi.h" #include "room.pb.h" #include "room_proto_converter.h" #include "track.pb.h"