From 3bdf79a5249e918a486c12cbc93940090757d61c Mon Sep 17 00:00:00 2001 From: Stephen DeRosa Date: Thu, 2 Apr 2026 12:27:18 -0600 Subject: [PATCH 1/6] Install spdlog with sdk --- CMakeLists.txt | 2 +- cmake/spdlog.cmake | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3af32d2c..86984eda 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -379,7 +379,7 @@ target_include_directories(livekit target_link_libraries(livekit PUBLIC - $ + spdlog::spdlog PRIVATE livekit_ffi ${LIVEKIT_PROTOBUF_TARGET} diff --git a/cmake/spdlog.cmake b/cmake/spdlog.cmake index b0956b00..25117dd3 100644 --- a/cmake/spdlog.cmake +++ b/cmake/spdlog.cmake @@ -71,7 +71,7 @@ FetchContent_Declare( set(SPDLOG_BUILD_SHARED OFF CACHE BOOL "" FORCE) set(SPDLOG_BUILD_EXAMPLE OFF CACHE BOOL "" FORCE) set(SPDLOG_BUILD_TESTS OFF CACHE BOOL "" FORCE) -set(SPDLOG_INSTALL OFF CACHE BOOL "" FORCE) +set(SPDLOG_INSTALL ON CACHE BOOL "" FORCE) FetchContent_MakeAvailable(livekit_spdlog) From 5cbd1804a78eb7d5382a5b87b5c53f6925d50356 Mon Sep 17 00:00:00 2001 From: Stephen DeRosa Date: Thu, 2 Apr 2026 10:29:56 -0600 Subject: [PATCH 2/6] CI docker build arm image, docker build example collection on linux arm/x64 --- .github/workflows/builds.yml | 117 +++++++++++++++++++++++++++++++++-- docker/Dockerfile | 18 ++++++ 2 files changed, 131 insertions(+), 4 deletions(-) diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index a9309805..ce6e27ab 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -33,6 +33,9 @@ on: - .github/workflows/** workflow_dispatch: +permissions: + contents: read + env: CARGO_TERM_COLOR: always # vcpkg binary caching for Windows @@ -252,7 +255,7 @@ jobs: ./build.sh clean-all || true fi - docker-build: + docker-build-x64: name: Build (docker-linux-x64) runs-on: ubuntu-latest @@ -267,9 +270,115 @@ jobs: uses: docker/setup-buildx-action@v3 - name: Build Docker image - run: docker build -t livekit-cpp-sdk:${{ github.sha }} . -f docker/Dockerfile + run: | + docker buildx build \ + --platform linux/amd64 \ + --load \ + -t livekit-cpp-sdk-x64:${{ github.sha }} \ + . \ + -f docker/Dockerfile + + - name: Verify installed SDK inside image + run: | + docker run --rm livekit-cpp-sdk-x64:${{ github.sha }} bash -c \ + 'test -f /opt/livekit-sdk/lib/cmake/LiveKit/LiveKitConfig.cmake' - - name: Build SDK inside Docker + - name: Save Docker image artifact + run: | + docker save livekit-cpp-sdk-x64:${{ github.sha }} | gzip > livekit-cpp-sdk-x64.tar.gz + + - name: Upload Docker image artifact + uses: actions/upload-artifact@v4 + with: + name: livekit-cpp-sdk-docker-x64 + path: livekit-cpp-sdk-x64.tar.gz + retention-days: 7 + + docker-build-linux-arm64: + name: Build (docker-linux-arm64) + runs-on: ubuntu-24.04-arm + + steps: + - name: Checkout (with submodules) + uses: actions/checkout@v4 + with: + submodules: recursive + fetch-depth: 0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + run: | + docker buildx build \ + --platform linux/arm64 \ + --load \ + -t livekit-cpp-sdk:${{ github.sha }} \ + . \ + -f docker/Dockerfile + + - name: Verify installed SDK inside image run: | docker run --rm livekit-cpp-sdk:${{ github.sha }} bash -c \ - 'cd /client-sdk-cpp && chmod +x build.sh && ./build.sh release-examples' + 'test -f /opt/livekit-sdk/lib/cmake/LiveKit/LiveKitConfig.cmake' + + - name: Save Docker image artifact + run: | + docker save livekit-cpp-sdk:${{ github.sha }} | gzip > livekit-cpp-sdk-arm64.tar.gz + + - name: Upload Docker image artifact + uses: actions/upload-artifact@v4 + with: + name: livekit-cpp-sdk-docker-arm64 + path: livekit-cpp-sdk-arm64.tar.gz + retention-days: 7 + + build-collections-linux-arm64: + name: Build (cpp-example-collection-linux-arm64) + runs-on: ubuntu-24.04-arm + needs: docker-build-linux-arm64 + + steps: + - name: Download Docker image artifact + uses: actions/download-artifact@v4 + with: + name: livekit-cpp-sdk-docker-arm64 + + - name: Load Docker image + run: gzip -dc livekit-cpp-sdk-arm64.tar.gz | docker load + + - name: Build cpp-example-collection against installed SDK + run: | + docker run --rm livekit-cpp-sdk:${{ github.sha }} bash -lc ' + set -euxo pipefail + git clone https://github.com/livekit-examples/cpp-example-collection.git /tmp/cpp-example-collection + cd /tmp/cpp-example-collection + git checkout sderosa/examples-migration + cmake -S . -B build -DLIVEKIT_LOCAL_SDK_DIR=/opt/livekit-sdk + cmake --build build --parallel + ' + + build-collections-x64: + name: Build (cpp-example-collection-x64) + runs-on: ubuntu-latest + needs: docker-build-x64 + + steps: + - name: Download Docker image artifact + uses: actions/download-artifact@v4 + with: + name: livekit-cpp-sdk-docker-x64 + + - name: Load Docker image + run: gzip -dc livekit-cpp-sdk-x64.tar.gz | docker load + + - name: Build cpp-example-collection against installed SDK + run: | + docker run --rm livekit-cpp-sdk-x64:${{ github.sha }} bash -lc ' + set -euxo pipefail + git clone https://github.com/livekit-examples/cpp-example-collection.git /tmp/cpp-example-collection + cd /tmp/cpp-example-collection + git checkout sderosa/examples-migration + cmake -S . -B build -DLIVEKIT_LOCAL_SDK_DIR=/opt/livekit-sdk + cmake --build build --parallel + ' diff --git a/docker/Dockerfile b/docker/Dockerfile index 0f42ff0b..ccdc1c7d 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -22,6 +22,7 @@ FROM ubuntu:22.04 ENV DEBIAN_FRONTEND=noninteractive ENV HOME=/root +ENV SDK_INSTALL_PREFIX=/opt/livekit-sdk # Install make, pkg-config, and base build deps (pkg-config + libglib2.0-dev for Rust glib-sys, libasound2-dev for alsa-sys) RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -29,18 +30,25 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ curl \ git \ libasound2-dev \ + libabsl-dev \ libclang-dev \ libdrm-dev \ libglib2.0-dev \ + libprotobuf-dev \ + libdecor-0-dev \ libspdlog-dev \ libssl-dev \ libunwind-dev \ libusb-1.0-0-dev \ libva-dev \ + libwayland-dev \ make \ ninja-build \ pkg-config \ + protobuf-compiler \ wget \ + llvm-dev \ + clang \ xz-utils \ && rm -rf /var/lib/apt/lists/* @@ -122,3 +130,13 @@ RUN mkdir -p /client-sdk-cpp/client-sdk-rust/.cargo \ && printf '%s\n' '[target.x86_64-unknown-linux-gnu]' 'linker = "/root/gcc-14/bin/g++"' \ '[target.aarch64-unknown-linux-gnu]' 'linker = "/root/gcc-14/bin/g++"' > /client-sdk-cpp/client-sdk-rust/.cargo/config.toml +# Build and install the SDK into a fixed prefix so downstream projects can +# consume the image as a prebuilt LiveKit SDK environment. +RUN LLVM_VERSION="$(llvm-config --version | cut -d. -f1)" \ + && export LIBCLANG_PATH="/usr/lib/llvm-${LLVM_VERSION}/lib" \ + && export CXXFLAGS="-Wno-deprecated-declarations" \ + && export CFLAGS="-Wno-deprecated-declarations" \ + && chmod +x /client-sdk-cpp/build.sh \ + && cd /client-sdk-cpp \ + && ./build.sh release --bundle --prefix "${SDK_INSTALL_PREFIX}" \ + && test -f "${SDK_INSTALL_PREFIX}/lib/cmake/LiveKit/LiveKitConfig.cmake" From 8fa26b367891d24aff9f297fa1beb8cc7a377a30 Mon Sep 17 00:00:00 2001 From: Stephen DeRosa Date: Thu, 2 Apr 2026 23:29:04 -0600 Subject: [PATCH 3/6] append -docker to docker image .gz files --- .github/workflows/builds.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index ce6e27ab..1a7dfef4 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -285,13 +285,13 @@ jobs: - name: Save Docker image artifact run: | - docker save livekit-cpp-sdk-x64:${{ github.sha }} | gzip > livekit-cpp-sdk-x64.tar.gz + docker save livekit-cpp-sdk-x64:${{ github.sha }} | gzip > livekit-cpp-sdk-x64-docker.tar.gz - name: Upload Docker image artifact uses: actions/upload-artifact@v4 with: name: livekit-cpp-sdk-docker-x64 - path: livekit-cpp-sdk-x64.tar.gz + path: livekit-cpp-sdk-x64-docker.tar.gz retention-days: 7 docker-build-linux-arm64: @@ -324,13 +324,13 @@ jobs: - name: Save Docker image artifact run: | - docker save livekit-cpp-sdk:${{ github.sha }} | gzip > livekit-cpp-sdk-arm64.tar.gz + docker save livekit-cpp-sdk:${{ github.sha }} | gzip > livekit-cpp-sdk-arm64-docker.tar.gz - name: Upload Docker image artifact uses: actions/upload-artifact@v4 with: name: livekit-cpp-sdk-docker-arm64 - path: livekit-cpp-sdk-arm64.tar.gz + path: livekit-cpp-sdk-arm64-docker.tar.gz retention-days: 7 build-collections-linux-arm64: @@ -345,7 +345,7 @@ jobs: name: livekit-cpp-sdk-docker-arm64 - name: Load Docker image - run: gzip -dc livekit-cpp-sdk-arm64.tar.gz | docker load + run: gzip -dc livekit-cpp-sdk-arm64-docker.tar.gz | docker load - name: Build cpp-example-collection against installed SDK run: | @@ -370,7 +370,7 @@ jobs: name: livekit-cpp-sdk-docker-x64 - name: Load Docker image - run: gzip -dc livekit-cpp-sdk-x64.tar.gz | docker load + run: gzip -dc livekit-cpp-sdk-x64-docker.tar.gz | docker load - name: Build cpp-example-collection against installed SDK run: | From 07b8036fc73b13edd18d7ffce35995ac333e2075 Mon Sep 17 00:00:00 2001 From: Stephen DeRosa Date: Tue, 7 Apr 2026 20:58:05 -0600 Subject: [PATCH 4/6] include/livekit/lk_log.h -> src/lk_log.h rm examples, fix bridge --- bridge/CMakeLists.txt | 7 - .../include/livekit_bridge/livekit_bridge.h | 1 - bridge/src/bridge_audio_track.cpp | 11 +- bridge/src/bridge_video_track.cpp | 11 +- bridge/src/livekit_bridge.cpp | 11 +- examples/CMakeLists.txt | 476 ------------- examples/bridge_human_robot/human.cpp | 414 ----------- examples/bridge_human_robot/robot.cpp | 661 ------------------ examples/bridge_mute_unmute/README.md | 94 --- examples/bridge_mute_unmute/caller.cpp | 337 --------- examples/bridge_mute_unmute/receiver.cpp | 266 ------- examples/bridge_rpc/README.md | 105 --- examples/bridge_rpc/custom_caller.cpp | 122 ---- examples/bridge_rpc/custom_receiver.cpp | 113 --- examples/cmake/sdl3.cmake | 52 -- examples/common/sdl_media.cpp | 226 ------ examples/common/sdl_media.h | 128 ---- examples/common/sdl_media_manager.cpp | 402 ----------- examples/common/sdl_media_manager.h | 109 --- examples/common/sdl_video_renderer.cpp | 176 ----- examples/common/sdl_video_renderer.h | 53 -- examples/common/wav_audio_source.cpp | 162 ----- examples/common/wav_audio_source.h | 56 -- examples/hello_livekit/receiver.cpp | 130 ---- examples/hello_livekit/sender.cpp | 142 ---- examples/logging_levels/README.md | 160 ----- examples/logging_levels/basic_usage.cpp | 163 ----- examples/logging_levels/custom_sinks.cpp | 292 -------- examples/ping_pong/constants.h | 36 - examples/ping_pong/json_converters.cpp | 69 -- examples/ping_pong/json_converters.h | 31 - examples/ping_pong/messages.h | 48 -- examples/ping_pong/ping.cpp | 209 ------ examples/ping_pong/pong.cpp | 147 ---- examples/ping_pong/utils.h | 45 -- examples/simple_data_stream/main.cpp | 284 -------- examples/simple_joystick/json_utils.cpp | 46 -- examples/simple_joystick/json_utils.h | 38 - examples/simple_joystick/receiver.cpp | 126 ---- examples/simple_joystick/sender.cpp | 268 ------- examples/simple_joystick/utils.cpp | 87 --- examples/simple_joystick/utils.h | 31 - examples/simple_room/fallback_capture.cpp | 119 ---- examples/simple_room/fallback_capture.h | 35 - examples/simple_room/main.cpp | 416 ----------- examples/simple_rpc/README.md | 157 ----- examples/simple_rpc/main.cpp | 547 --------------- examples/tokens/README.md | 8 - examples/tokens/gen_and_set.bash | 169 ----- .../tokens/set_integration_test_tokens.bash | 138 ---- {include/livekit => src}/lk_log.h | 0 51 files changed, 17 insertions(+), 7917 deletions(-) delete mode 100644 examples/CMakeLists.txt delete mode 100644 examples/bridge_human_robot/human.cpp delete mode 100644 examples/bridge_human_robot/robot.cpp delete mode 100644 examples/bridge_mute_unmute/README.md delete mode 100644 examples/bridge_mute_unmute/caller.cpp delete mode 100644 examples/bridge_mute_unmute/receiver.cpp delete mode 100644 examples/bridge_rpc/README.md delete mode 100644 examples/bridge_rpc/custom_caller.cpp delete mode 100644 examples/bridge_rpc/custom_receiver.cpp delete mode 100644 examples/cmake/sdl3.cmake delete mode 100644 examples/common/sdl_media.cpp delete mode 100644 examples/common/sdl_media.h delete mode 100644 examples/common/sdl_media_manager.cpp delete mode 100644 examples/common/sdl_media_manager.h delete mode 100644 examples/common/sdl_video_renderer.cpp delete mode 100644 examples/common/sdl_video_renderer.h delete mode 100644 examples/common/wav_audio_source.cpp delete mode 100644 examples/common/wav_audio_source.h delete mode 100644 examples/hello_livekit/receiver.cpp delete mode 100644 examples/hello_livekit/sender.cpp delete mode 100644 examples/logging_levels/README.md delete mode 100644 examples/logging_levels/basic_usage.cpp delete mode 100644 examples/logging_levels/custom_sinks.cpp delete mode 100644 examples/ping_pong/constants.h delete mode 100644 examples/ping_pong/json_converters.cpp delete mode 100644 examples/ping_pong/json_converters.h delete mode 100644 examples/ping_pong/messages.h delete mode 100644 examples/ping_pong/ping.cpp delete mode 100644 examples/ping_pong/pong.cpp delete mode 100644 examples/ping_pong/utils.h delete mode 100644 examples/simple_data_stream/main.cpp delete mode 100644 examples/simple_joystick/json_utils.cpp delete mode 100644 examples/simple_joystick/json_utils.h delete mode 100644 examples/simple_joystick/receiver.cpp delete mode 100644 examples/simple_joystick/sender.cpp delete mode 100644 examples/simple_joystick/utils.cpp delete mode 100644 examples/simple_joystick/utils.h delete mode 100644 examples/simple_room/fallback_capture.cpp delete mode 100644 examples/simple_room/fallback_capture.h delete mode 100644 examples/simple_room/main.cpp delete mode 100644 examples/simple_rpc/README.md delete mode 100644 examples/simple_rpc/main.cpp delete mode 100644 examples/tokens/README.md delete mode 100755 examples/tokens/gen_and_set.bash delete mode 100755 examples/tokens/set_integration_test_tokens.bash rename {include/livekit => src}/lk_log.h (100%) diff --git a/bridge/CMakeLists.txt b/bridge/CMakeLists.txt index 369c98df..549e86cb 100644 --- a/bridge/CMakeLists.txt +++ b/bridge/CMakeLists.txt @@ -32,13 +32,6 @@ target_include_directories(livekit_bridge target_link_libraries(livekit_bridge PUBLIC livekit - PRIVATE - spdlog::spdlog -) - -target_compile_definitions(livekit_bridge - PRIVATE - SPDLOG_ACTIVE_LEVEL=${_SPDLOG_ACTIVE_LEVEL} ) if(MSVC) diff --git a/bridge/include/livekit_bridge/livekit_bridge.h b/bridge/include/livekit_bridge/livekit_bridge.h index 47f13d46..1f2d44a6 100644 --- a/bridge/include/livekit_bridge/livekit_bridge.h +++ b/bridge/include/livekit_bridge/livekit_bridge.h @@ -23,7 +23,6 @@ #include "livekit_bridge/bridge_video_track.h" #include "livekit_bridge/rpc_constants.h" -#include "livekit/lk_log.h" #include "livekit/local_participant.h" #include "livekit/room.h" #include "livekit/rpc_error.h" diff --git a/bridge/src/bridge_audio_track.cpp b/bridge/src/bridge_audio_track.cpp index 1fa58cd2..5816cfce 100644 --- a/bridge/src/bridge_audio_track.cpp +++ b/bridge/src/bridge_audio_track.cpp @@ -24,10 +24,9 @@ #include "livekit/local_audio_track.h" #include "livekit/local_participant.h" +#include #include -#include "livekit/lk_log.h" - namespace livekit_bridge { BridgeAudioTrack::BridgeAudioTrack( @@ -56,7 +55,7 @@ bool BridgeAudioTrack::pushFrame(const std::vector &data, try { source_->captureFrame(frame, timeout_ms); } catch (const std::exception &e) { - LK_LOG_ERROR("BridgeAudioTrack captureFrame error: {}", e.what()); + std::cerr << "[error] BridgeAudioTrack captureFrame error: " << e.what() << "\n"; return false; } return true; @@ -77,7 +76,7 @@ bool BridgeAudioTrack::pushFrame(const std::int16_t *data, try { source_->captureFrame(frame, timeout_ms); } catch (const std::exception &e) { - LK_LOG_ERROR("BridgeAudioTrack captureFrame error: {}", e.what()); + std::cerr << "[error] BridgeAudioTrack captureFrame error: " << e.what() << "\n"; return false; } return true; @@ -115,8 +114,8 @@ void BridgeAudioTrack::release() { participant_->unpublishTrack(track_->publication()->sid()); } catch (...) { // Best-effort cleanup; ignore errors during teardown - LK_LOG_WARN("BridgeAudioTrack unpublishTrack error, continuing with " - "cleanup"); + std::cerr << "[warn] BridgeAudioTrack unpublishTrack error, continuing " + "with cleanup\n"; } } diff --git a/bridge/src/bridge_video_track.cpp b/bridge/src/bridge_video_track.cpp index 7b8a6cc8..7a66155f 100644 --- a/bridge/src/bridge_video_track.cpp +++ b/bridge/src/bridge_video_track.cpp @@ -24,10 +24,9 @@ #include "livekit/video_frame.h" #include "livekit/video_source.h" +#include #include -#include "livekit/lk_log.h" - namespace livekit_bridge { BridgeVideoTrack::BridgeVideoTrack( @@ -56,7 +55,7 @@ bool BridgeVideoTrack::pushFrame(const std::vector &rgba, try { source_->captureFrame(frame, timestamp_us); } catch (const std::exception &e) { - LK_LOG_ERROR("BridgeVideoTrack captureFrame error: {}", e.what()); + std::cerr << "[error] BridgeVideoTrack captureFrame error: " << e.what() << "\n"; return false; } return true; @@ -76,7 +75,7 @@ bool BridgeVideoTrack::pushFrame(const std::uint8_t *rgba, try { source_->captureFrame(frame, timestamp_us); } catch (const std::exception &e) { - LK_LOG_ERROR("BridgeVideoTrack captureFrame error: {}", e.what()); + std::cerr << "[error] BridgeVideoTrack captureFrame error: " << e.what() << "\n"; return false; } return true; @@ -114,8 +113,8 @@ void BridgeVideoTrack::release() { participant_->unpublishTrack(track_->publication()->sid()); } catch (...) { // Best-effort cleanup; ignore errors during teardown - LK_LOG_WARN("BridgeVideoTrack unpublishTrack error, continuing with " - "cleanup"); + std::cerr << "[warn] BridgeVideoTrack unpublishTrack error, continuing " + "with cleanup\n"; } } diff --git a/bridge/src/livekit_bridge.cpp b/bridge/src/livekit_bridge.cpp index 9f782904..b15587ec 100644 --- a/bridge/src/livekit_bridge.cpp +++ b/bridge/src/livekit_bridge.cpp @@ -34,6 +34,7 @@ #include "livekit/video_source.h" #include +#include #include namespace livekit_bridge { @@ -122,8 +123,8 @@ void LiveKitBridge::disconnect() { std::lock_guard lock(mutex_); if (!connected_) { - LK_LOG_WARN("Attempting to disconnect an already disconnected bridge. " - "Things may not disconnect properly."); + std::cerr << "[warn] Attempting to disconnect an already disconnected " + "bridge. Things may not disconnect properly.\n"; } connected_ = false; @@ -241,7 +242,8 @@ void LiveKitBridge::setOnAudioFrameCallback( AudioFrameCallback callback) { std::lock_guard lock(mutex_); if (!room_) { - LK_LOG_WARN("setOnAudioFrameCallback called before connect(); ignored"); + std::cerr << "[warn] setOnAudioFrameCallback called before connect(); " + "ignored\n"; return; } room_->setOnAudioFrameCallback(participant_identity, source, @@ -253,7 +255,8 @@ void LiveKitBridge::setOnVideoFrameCallback( VideoFrameCallback callback) { std::lock_guard lock(mutex_); if (!room_) { - LK_LOG_WARN("setOnVideoFrameCallback called before connect(); ignored"); + std::cerr << "[warn] setOnVideoFrameCallback called before connect(); " + "ignored\n"; return; } room_->setOnVideoFrameCallback(participant_identity, source, diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt deleted file mode 100644 index 6d7ab0a5..00000000 --- a/examples/CMakeLists.txt +++ /dev/null @@ -1,476 +0,0 @@ -cmake_minimum_required(VERSION 3.20) -project(livekit-examples) - -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -# Set RPATH for examples to find shared libraries in executable directory -# This ensures SDL3 and other shared libs are loaded from bin/ not _deps/ -if(UNIX) - if(APPLE) - set(CMAKE_BUILD_RPATH "@loader_path;@loader_path/../lib") - set(CMAKE_INSTALL_RPATH "@loader_path;@loader_path/../lib") - else() - set(CMAKE_BUILD_RPATH "$ORIGIN:$ORIGIN/../lib") - set(CMAKE_INSTALL_RPATH "$ORIGIN:$ORIGIN/../lib") - endif() - set(CMAKE_BUILD_RPATH_USE_ORIGIN TRUE) - set(CMAKE_SKIP_BUILD_RPATH FALSE) - set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE) -endif() - -# Always use FetchContent for SDL3 (vcpkg doesn't have it in stable baseline) -list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") -include(sdl3) - -# Common include directories for examples that need private headers -# TODO: These should be refactored to use only public headers -set(EXAMPLES_PRIVATE_INCLUDE_DIRS - ${LIVEKIT_ROOT_DIR}/src - ${LIVEKIT_BINARY_DIR}/generated -) - -# Propagate the compile-time log level to all example targets. -add_compile_definitions(SPDLOG_ACTIVE_LEVEL=${_SPDLOG_ACTIVE_LEVEL}) - -# Shared example code (SDL media, wav source, etc.) -set(EXAMPLES_COMMON_DIR ${CMAKE_CURRENT_SOURCE_DIR}/common) - -# All example executables (used for copying livekit_ffi DLL/shared lib) -set(EXAMPLES_ALL - SimpleRoom - SimpleRpc - SimpleJoystickSender - SimpleJoystickReceiver - SimpleDataStream - PingPongPing - PingPongPong - HelloLivekitSender - HelloLivekitReceiver - LoggingLevelsBasicUsage - LoggingLevelsCustomSinks - BridgeRobot - BridgeHuman - BridgeMuteCaller - BridgeMuteReceiver - BridgeRpcCaller - BridgeRpcReceiver -) - -# Bridge examples (need livekit_bridge DLL/shared lib in addition to livekit_ffi) -set(EXAMPLES_BRIDGE - BridgeRobot - BridgeMuteCaller - BridgeMuteReceiver - BridgeRpcCaller - BridgeRpcReceiver -) - -# Examples that use SDL3 (need SDL3 lib copied on Linux; SimpleRoom is handled separately above) -set(EXAMPLES_NEED_SDL3 - BridgeRobot - BridgeHuman - BridgeMuteCaller - BridgeMuteReceiver -) - -add_executable(SimpleRoom - simple_room/main.cpp - simple_room/fallback_capture.cpp - simple_room/fallback_capture.h - ${EXAMPLES_COMMON_DIR}/sdl_media.cpp - ${EXAMPLES_COMMON_DIR}/sdl_media.h - ${EXAMPLES_COMMON_DIR}/sdl_media_manager.cpp - ${EXAMPLES_COMMON_DIR}/sdl_media_manager.h - ${EXAMPLES_COMMON_DIR}/sdl_video_renderer.cpp - ${EXAMPLES_COMMON_DIR}/sdl_video_renderer.h - ${EXAMPLES_COMMON_DIR}/wav_audio_source.cpp - ${EXAMPLES_COMMON_DIR}/wav_audio_source.h -) - -target_include_directories(SimpleRoom PRIVATE - ${EXAMPLES_PRIVATE_INCLUDE_DIRS} - ${EXAMPLES_COMMON_DIR} - ${CMAKE_CURRENT_SOURCE_DIR}/simple_room -) - -target_link_libraries(SimpleRoom - PRIVATE - livekit - spdlog::spdlog - SDL3::SDL3 -) - -add_custom_command(TARGET SimpleRoom POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_directory - ${LIVEKIT_ROOT_DIR}/data - ${CMAKE_CURRENT_BINARY_DIR}/data -) - -# Copy SDL3 shared library to SimpleRoom output directory -# On Linux, we also need to create the SONAME symlink (libSDL3.so.0 -> libSDL3.so.0.x.x) -# macOS doesn't need SONAME symlink (dylib versioning works differently) -if(UNIX AND NOT APPLE) - add_custom_command(TARGET SimpleRoom POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - "$" - "$" - COMMAND ${CMAKE_COMMAND} -E create_symlink - "$" - "$/$" - COMMENT "Copying SDL3 shared library and SONAME symlink to SimpleRoom output directory" - VERBATIM - ) -else() - # Windows and macOS: just copy the library file - add_custom_command(TARGET SimpleRoom POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - "$" - "$" - COMMENT "Copying SDL3 shared library to SimpleRoom output directory" - VERBATIM - ) -endif() - -find_package(nlohmann_json CONFIG QUIET) -if(NOT nlohmann_json_FOUND) - include(FetchContent) - FetchContent_Declare( - nlohmann_json - URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz - ) - FetchContent_MakeAvailable(nlohmann_json) -endif() - -add_executable(SimpleRpc - simple_rpc/main.cpp -) - -target_include_directories(SimpleRpc PRIVATE ${EXAMPLES_PRIVATE_INCLUDE_DIRS}) - -target_link_libraries(SimpleRpc - PRIVATE - nlohmann_json::nlohmann_json - livekit - spdlog::spdlog -) - -# --- SimpleJoystick example (sender + receiver executables with shared json_utils) --- - -add_library(simple_joystick_json_utils STATIC - simple_joystick/json_utils.cpp - simple_joystick/json_utils.h - simple_joystick/utils.cpp - simple_joystick/utils.h -) - -target_include_directories(simple_joystick_json_utils PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/simple_joystick -) - -target_link_libraries(simple_joystick_json_utils - PUBLIC - nlohmann_json::nlohmann_json -) - -add_executable(SimpleJoystickReceiver - simple_joystick/receiver.cpp -) - -target_include_directories(SimpleJoystickReceiver PRIVATE ${EXAMPLES_PRIVATE_INCLUDE_DIRS}) - -target_link_libraries(SimpleJoystickReceiver - PRIVATE - simple_joystick_json_utils - livekit - spdlog::spdlog -) - -add_executable(SimpleJoystickSender - simple_joystick/sender.cpp -) - -target_include_directories(SimpleJoystickSender PRIVATE ${EXAMPLES_PRIVATE_INCLUDE_DIRS}) - -target_link_libraries(SimpleJoystickSender - PRIVATE - simple_joystick_json_utils - livekit - spdlog::spdlog -) - -# --- LoggingLevelsBasicUsage example --- - -add_executable(LoggingLevelsBasicUsage - logging_levels/basic_usage.cpp -) - -target_include_directories(LoggingLevelsBasicUsage PRIVATE ${EXAMPLES_PRIVATE_INCLUDE_DIRS}) - -target_link_libraries(LoggingLevelsBasicUsage - PRIVATE - livekit - spdlog::spdlog -) - -# --- LoggingLevelsCustomSinks example --- - -add_executable(LoggingLevelsCustomSinks - logging_levels/custom_sinks.cpp -) - -target_link_libraries(LoggingLevelsCustomSinks - PRIVATE - livekit -) - -# --- SimpleDataStream example --- - -add_executable(SimpleDataStream - simple_data_stream/main.cpp -) - -target_include_directories(SimpleDataStream PRIVATE ${EXAMPLES_PRIVATE_INCLUDE_DIRS}) - -target_link_libraries(SimpleDataStream - PRIVATE - livekit - spdlog::spdlog -) - -add_custom_command( - TARGET SimpleDataStream - POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_directory - ${LIVEKIT_ROOT_DIR}/data - $/data -) - -# --- ping_pong (request/response latency measurement over data tracks) --- - -add_library(ping_pong_support STATIC - ping_pong/json_converters.cpp - ping_pong/json_converters.h - ping_pong/constants.h - ping_pong/messages.h - ping_pong/utils.h -) - -target_include_directories(ping_pong_support PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/ping_pong -) - -target_link_libraries(ping_pong_support - PRIVATE - nlohmann_json::nlohmann_json -) - -add_executable(PingPongPing - ping_pong/ping.cpp -) - -target_include_directories(PingPongPing PRIVATE ${EXAMPLES_PRIVATE_INCLUDE_DIRS}) - -target_link_libraries(PingPongPing - PRIVATE - ping_pong_support - livekit - spdlog::spdlog -) - -add_executable(PingPongPong - ping_pong/pong.cpp -) - -target_include_directories(PingPongPong PRIVATE ${EXAMPLES_PRIVATE_INCLUDE_DIRS}) - -target_link_libraries(PingPongPong - PRIVATE - ping_pong_support - livekit - spdlog::spdlog -) - -# --- hello_livekit (minimal synthetic video + data publish / subscribe) --- - -add_executable(HelloLivekitSender - hello_livekit/sender.cpp -) - -target_include_directories(HelloLivekitSender PRIVATE ${EXAMPLES_PRIVATE_INCLUDE_DIRS}) - -target_link_libraries(HelloLivekitSender - PRIVATE - livekit - spdlog::spdlog -) - -add_executable(HelloLivekitReceiver - hello_livekit/receiver.cpp -) - -target_include_directories(HelloLivekitReceiver PRIVATE ${EXAMPLES_PRIVATE_INCLUDE_DIRS}) - -target_link_libraries(HelloLivekitReceiver - PRIVATE - livekit - spdlog::spdlog -) - -# --- bridge_human_robot examples (robot + human; use livekit_bridge and SDL3) --- - -add_executable(BridgeRobot - bridge_human_robot/robot.cpp - ${EXAMPLES_COMMON_DIR}/sdl_media.cpp - ${EXAMPLES_COMMON_DIR}/sdl_media.h -) -target_include_directories(BridgeRobot PRIVATE - ${EXAMPLES_PRIVATE_INCLUDE_DIRS} - ${EXAMPLES_COMMON_DIR} -) -target_link_libraries(BridgeRobot PRIVATE livekit_bridge spdlog::spdlog SDL3::SDL3) - -add_executable(BridgeHuman - bridge_human_robot/human.cpp - ${EXAMPLES_COMMON_DIR}/sdl_media.cpp - ${EXAMPLES_COMMON_DIR}/sdl_media.h -) -target_include_directories(BridgeHuman PRIVATE - ${EXAMPLES_PRIVATE_INCLUDE_DIRS} - ${EXAMPLES_COMMON_DIR} -) -target_link_libraries(BridgeHuman PRIVATE livekit spdlog::spdlog SDL3::SDL3) - -# --- bridge_rpc examples (headless custom RPC caller + receiver) --- - -add_executable(BridgeRpcCaller - bridge_rpc/custom_caller.cpp -) -target_link_libraries(BridgeRpcCaller PRIVATE livekit_bridge) - -add_executable(BridgeRpcReceiver - bridge_rpc/custom_receiver.cpp -) -target_link_libraries(BridgeRpcReceiver PRIVATE livekit_bridge) - -# --- bridge_mute_unmute examples (caller uses SDL3 for A/V playback; receiver is headless) --- - -add_executable(BridgeMuteCaller - bridge_mute_unmute/caller.cpp - ${EXAMPLES_COMMON_DIR}/sdl_media.cpp - ${EXAMPLES_COMMON_DIR}/sdl_media.h -) -target_include_directories(BridgeMuteCaller PRIVATE - ${EXAMPLES_PRIVATE_INCLUDE_DIRS} - ${EXAMPLES_COMMON_DIR} -) -target_link_libraries(BridgeMuteCaller PRIVATE livekit_bridge spdlog::spdlog SDL3::SDL3) - -add_executable(BridgeMuteReceiver - bridge_mute_unmute/receiver.cpp - ${EXAMPLES_COMMON_DIR}/sdl_media.cpp - ${EXAMPLES_COMMON_DIR}/sdl_media.h -) -target_include_directories(BridgeMuteReceiver PRIVATE - ${EXAMPLES_PRIVATE_INCLUDE_DIRS} - ${EXAMPLES_COMMON_DIR} -) -target_link_libraries(BridgeMuteReceiver PRIVATE livekit_bridge spdlog::spdlog SDL3::SDL3) - -# Copy SDL3 shared library to bridge example output directories -if(UNIX AND NOT APPLE) - foreach(_target ${EXAMPLES_NEED_SDL3}) - add_custom_command(TARGET ${_target} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - "$" - "$" - COMMAND ${CMAKE_COMMAND} -E create_symlink - "$" - "$/$" - COMMENT "Copying SDL3 shared library and SONAME symlink to ${_target} output directory" - VERBATIM - ) - endforeach() -else() - foreach(_target ${EXAMPLES_NEED_SDL3}) - add_custom_command(TARGET ${_target} POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - "$" - "$" - COMMENT "Copying SDL3 shared library to ${_target} output directory" - VERBATIM - ) - endforeach() -endif() - -# Windows: Copy required DLLs to examples output directory (single copy to avoid parallel POST_BUILD races) -if(WIN32) - set(REQUIRED_DLLS "livekit_ffi.dll") - - # One copy of each DLL to bin/; all examples depend on this target - add_custom_command(OUTPUT ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/livekit_ffi.dll - COMMAND ${CMAKE_COMMAND} -E copy_if_different - $/livekit_ffi.dll - ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/livekit_ffi.dll - DEPENDS livekit - COMMENT "Copying livekit_ffi.dll to examples output directory" - ) - add_custom_target(copy_ffi_dll_to_bin ALL DEPENDS ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/livekit_ffi.dll) - foreach(EXAMPLE ${EXAMPLES_ALL}) - add_dependencies(${EXAMPLE} copy_ffi_dll_to_bin) - endforeach() - - # Bridge examples also need livekit_bridge.dll (single copy to bin/) - set(BRIDGE_DLL "livekit_bridge.dll") - add_custom_command(OUTPUT ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${BRIDGE_DLL} - COMMAND ${CMAKE_COMMAND} -E copy_if_different - $ - ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${BRIDGE_DLL} - DEPENDS livekit_bridge - COMMENT "Copying livekit_bridge DLL to examples output directory" - ) - add_custom_target(copy_bridge_dll_to_bin ALL DEPENDS ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${BRIDGE_DLL}) - foreach(EXAMPLE ${EXAMPLES_BRIDGE}) - add_dependencies(${EXAMPLE} copy_bridge_dll_to_bin) - endforeach() -endif() - -# Linux/macOS: Copy shared library to examples output directory (single copy to avoid parallel POST_BUILD races) -if(UNIX) - if(APPLE) - set(FFI_SHARED_LIB "liblivekit_ffi.dylib") - else() - set(FFI_SHARED_LIB "liblivekit_ffi.so") - endif() - - # One copy to bin/; all examples depend on this target so the copy runs once - add_custom_command(OUTPUT ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${FFI_SHARED_LIB} - COMMAND ${CMAKE_COMMAND} -E copy_if_different - $/${FFI_SHARED_LIB} - ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${FFI_SHARED_LIB} - DEPENDS livekit - COMMENT "Copying ${FFI_SHARED_LIB} to examples output directory" - ) - add_custom_target(copy_ffi_to_bin ALL DEPENDS ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${FFI_SHARED_LIB}) - foreach(EXAMPLE ${EXAMPLES_ALL}) - add_dependencies(${EXAMPLE} copy_ffi_to_bin) - endforeach() - - # Bridge examples also need livekit_bridge shared library (single copy to bin/) - if(APPLE) - set(BRIDGE_SHARED_LIB "liblivekit_bridge.dylib") - else() - set(BRIDGE_SHARED_LIB "liblivekit_bridge.so") - endif() - add_custom_command(OUTPUT ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${BRIDGE_SHARED_LIB} - COMMAND ${CMAKE_COMMAND} -E copy_if_different - $ - ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${BRIDGE_SHARED_LIB} - DEPENDS livekit_bridge - COMMENT "Copying livekit_bridge to examples output directory" - ) - add_custom_target(copy_bridge_to_bin ALL DEPENDS ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${BRIDGE_SHARED_LIB}) - foreach(EXAMPLE ${EXAMPLES_BRIDGE}) - add_dependencies(${EXAMPLE} copy_bridge_to_bin) - endforeach() -endif() diff --git a/examples/bridge_human_robot/human.cpp b/examples/bridge_human_robot/human.cpp deleted file mode 100644 index 3e8c553d..00000000 --- a/examples/bridge_human_robot/human.cpp +++ /dev/null @@ -1,414 +0,0 @@ -/* - * Copyright 2025 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. - */ - -/* - * Human example -- receives audio and video frames from a robot in a - * LiveKit room and renders them using SDL3. - * - * This example demonstrates the base SDK's convenience frame callback API - * (Room::setOnAudioFrameCallback / Room::setOnVideoFrameCallback) which - * eliminates the need for a RoomDelegate subclass, manual AudioStream/ - * VideoStream creation, and reader threads. - * - * The robot publishes two video tracks and two audio tracks: - * - "robot-cam" (SOURCE_CAMERA) -- webcam or placeholder - * - "robot-sim-frame" (SOURCE_SCREENSHARE) -- simulated diagnostic - * frame - * - "robot-mic" (SOURCE_MICROPHONE) -- real microphone or - * silence - * - "robot-sim-audio" (SOURCE_SCREENSHARE_AUDIO) -- simulated siren tone - * - * Press 'w' to play the webcam feed + real mic, or 's' for sim frame + siren. - * The selection controls both video and audio simultaneously. - * - * Usage: - * human [--no-audio] - * LIVEKIT_URL=... LIVEKIT_TOKEN=... human [--no-audio] - * - * --no-audio Subscribe to audio tracks but suppress local playback. - * - * The token must grant identity "human". Generate one with: - * lk token create --api-key --api-secret \ - * --join --room my-room --identity human \ - * --valid-for 24h - * - * Run alongside the "robot" example (which publishes with identity "robot"). - */ - -#include "livekit/audio_frame.h" -#include "livekit/livekit.h" -#include "livekit/track.h" -#include "livekit/video_frame.h" -#include "sdl_media.h" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -static std::atomic g_running{true}; -static void handleSignal(int) { g_running.store(false); } - -// ---- Video source selection ---- -enum class SelectedSource : int { Webcam = 0, SimFrame = 1 }; -static std::atomic g_selected_source{ - static_cast(SelectedSource::Webcam)}; - -// ---- Thread-safe video frame slot ---- -struct LatestVideoFrame { - std::mutex mutex; - std::vector data; - int width = 0; - int height = 0; - bool dirty = false; -}; - -static LatestVideoFrame g_latest_video; - -static void renderFrame(const livekit::VideoFrame &frame) { - const std::uint8_t *src = frame.data(); - const std::size_t size = frame.dataSize(); - if (!src || size == 0) - return; - - std::lock_guard lock(g_latest_video.mutex); - g_latest_video.data.assign(src, src + size); - g_latest_video.width = frame.width(); - g_latest_video.height = frame.height(); - g_latest_video.dirty = true; -} - -// ---- Counters for periodic status ---- -static std::atomic g_audio_frames{0}; -static std::atomic g_video_frames{0}; - -constexpr const char *kRobotMicTrackName = "robot-mic"; -constexpr const char *kRobotSimAudioTrackName = "robot-sim-audio"; -constexpr const char *kRobotCamTrackName = "robot-cam"; -constexpr const char *kRobotSimVideoTrackName = "robot-sim-frame"; - -int main(int argc, char *argv[]) { - // ----- Parse args / env ----- - bool no_audio = false; - std::vector positional; - for (int i = 1; i < argc; ++i) { - if (std::string(argv[i]) == "--no-audio") { - no_audio = true; - } else { - positional.push_back(argv[i]); - } - } - - std::string url, token; - auto is_ws_url = [](const std::string &s) { - return (s.size() >= 5 && s.compare(0, 5, "ws://") == 0) || - (s.size() >= 6 && s.compare(0, 6, "wss://") == 0); - }; - if (positional.size() >= 2) { - for (const auto &arg : positional) { - if (is_ws_url(arg)) { - url = arg; - break; - } - } - for (const auto &arg : positional) { - if (arg != url) { - token = arg; - break; - } - } - if (url.empty()) - url = positional[0], token = positional[1]; - } else { - const char *e = std::getenv("LIVEKIT_URL"); - if (e) - url = e; - e = std::getenv("LIVEKIT_TOKEN"); - if (e) - token = e; - } - if (url.empty() || token.empty()) { - std::cerr - << "Usage: human [--no-audio] \n" - << " or: LIVEKIT_URL=... LIVEKIT_TOKEN=... human [--no-audio]\n"; - return 1; - } - if (no_audio) { - std::cout << "[human] --no-audio: audio playback disabled.\n"; - } - - std::signal(SIGINT, handleSignal); - - // ----- Initialize SDL3 ----- - if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) { - LK_LOG_ERROR("[human] SDL_Init failed: {}", SDL_GetError()); - return 1; - } - - // ----- Create SDL window + renderer ----- - constexpr int kWindowWidth = 1280; - constexpr int kWindowHeight = 720; - - SDL_Window *window = SDL_CreateWindow("Human - Robot Camera Feed", - kWindowWidth, kWindowHeight, 0); - if (!window) { - LK_LOG_ERROR("[human] SDL_CreateWindow failed: {}", SDL_GetError()); - SDL_Quit(); - return 1; - } - - SDL_Renderer *renderer = SDL_CreateRenderer(window, nullptr); - if (!renderer) { - LK_LOG_ERROR("[human] SDL_CreateRenderer failed: {}", SDL_GetError()); - SDL_DestroyWindow(window); - SDL_Quit(); - return 1; - } - - SDL_Texture *texture = nullptr; - int tex_width = 0; - int tex_height = 0; - - // ----- SDL speaker for audio playback ----- - std::unique_ptr speaker; - std::mutex speaker_mutex; - - // ----- Connect to LiveKit using the base SDK ----- - livekit::initialize(); - - auto room = std::make_unique(); - std::cout << "[human] Connecting to " << url << " ...\n"; - livekit::RoomOptions options; - options.auto_subscribe = true; - if (!room->Connect(url, token, options)) { - LK_LOG_ERROR("[human] Failed to connect."); - livekit::shutdown(); - SDL_DestroyRenderer(renderer); - SDL_DestroyWindow(window); - SDL_Quit(); - return 1; - } - std::cout << "[human] Connected. Waiting for robot...\n"; - - // Helper: enqueue audio to the speaker (lazily initializes on first call) - auto playAudio = [&speaker, - &speaker_mutex](const livekit::AudioFrame &frame) { - const auto &samples = frame.data(); - if (samples.empty()) - return; - - std::lock_guard lock(speaker_mutex); - - if (!speaker) { - speaker = std::make_unique(frame.sample_rate(), - frame.num_channels()); - if (!speaker->init()) { - LK_LOG_ERROR("[human] Failed to init SDL speaker."); - speaker.reset(); - return; - } - std::cout << "[human] Speaker opened: " << frame.sample_rate() << " Hz, " - << frame.num_channels() << " ch.\n"; - } - - speaker->enqueue(samples.data(), frame.samples_per_channel()); - }; - - // ----- Set audio callbacks using Room::setOnAudioFrameCallback ----- - room->setOnAudioFrameCallback( - "robot", kRobotMicTrackName, - [playAudio, no_audio](const livekit::AudioFrame &frame) { - g_audio_frames.fetch_add(1, std::memory_order_relaxed); - if (!no_audio && g_selected_source.load(std::memory_order_relaxed) == - static_cast(SelectedSource::Webcam)) { - playAudio(frame); - } - }); - - room->setOnAudioFrameCallback( - "robot", kRobotSimAudioTrackName, - [playAudio, no_audio](const livekit::AudioFrame &frame) { - g_audio_frames.fetch_add(1, std::memory_order_relaxed); - if (!no_audio && g_selected_source.load(std::memory_order_relaxed) == - static_cast(SelectedSource::SimFrame)) { - playAudio(frame); - } - }); - - // ----- Set video callbacks using Room::setOnVideoFrameCallback ----- - room->setOnVideoFrameCallback( - "robot", kRobotCamTrackName, - [](const livekit::VideoFrame &frame, std::int64_t /*timestamp_us*/) { - g_video_frames.fetch_add(1, std::memory_order_relaxed); - if (g_selected_source.load(std::memory_order_relaxed) == - static_cast(SelectedSource::Webcam)) { - renderFrame(frame); - } - }); - - room->setOnVideoFrameCallback( - "robot", kRobotSimVideoTrackName, - [](const livekit::VideoFrame &frame, std::int64_t /*timestamp_us*/) { - g_video_frames.fetch_add(1, std::memory_order_relaxed); - if (g_selected_source.load(std::memory_order_relaxed) == - static_cast(SelectedSource::SimFrame)) { - renderFrame(frame); - } - }); - - // ----- Stdin input thread ----- - std::thread input_thread([&]() { - std::string line; - while (g_running.load() && std::getline(std::cin, line)) { - if (line == "w" || line == "W") { - g_selected_source.store(static_cast(SelectedSource::Webcam), - std::memory_order_relaxed); - std::cout << "[human] Switched to webcam + mic.\n"; - } else if (line == "s" || line == "S") { - g_selected_source.store(static_cast(SelectedSource::SimFrame), - std::memory_order_relaxed); - std::cout << "[human] Switched to sim frame + siren.\n"; - } - } - }); - - // ----- Main loop ----- - std::cout - << "[human] Rendering robot feed. Press 'w' for webcam + mic, " - "'s' for sim frame + siren (in this terminal or the SDL window). " - "Ctrl-C or close window to stop.\n"; - - auto last_report = std::chrono::steady_clock::now(); - - std::vector local_pixels; - - while (g_running.load()) { - SDL_Event ev; - while (SDL_PollEvent(&ev)) { - if (ev.type == SDL_EVENT_QUIT) { - g_running.store(false); - } else if (ev.type == SDL_EVENT_KEY_DOWN) { - if (ev.key.key == SDLK_W) { - g_selected_source.store(static_cast(SelectedSource::Webcam), - std::memory_order_relaxed); - std::cout << "[human] Switched to webcam + mic.\n"; - } else if (ev.key.key == SDLK_S) { - g_selected_source.store(static_cast(SelectedSource::SimFrame), - std::memory_order_relaxed); - std::cout << "[human] Switched to sim frame + siren.\n"; - } - } - } - - int fw = 0, fh = 0; - bool have_frame = false; - { - std::lock_guard lock(g_latest_video.mutex); - if (g_latest_video.dirty && g_latest_video.width > 0 && - g_latest_video.height > 0) { - fw = g_latest_video.width; - fh = g_latest_video.height; - local_pixels.swap(g_latest_video.data); - g_latest_video.dirty = false; - have_frame = true; - } - } - - if (have_frame) { - if (fw != tex_width || fh != tex_height) { - if (texture) - SDL_DestroyTexture(texture); - texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA32, - SDL_TEXTUREACCESS_STREAMING, fw, fh); - if (!texture) { - LK_LOG_ERROR("[human] SDL_CreateTexture failed: {}", SDL_GetError()); - } - tex_width = fw; - tex_height = fh; - } - - if (texture) { - void *pixels = nullptr; - int pitch = 0; - if (SDL_LockTexture(texture, nullptr, &pixels, &pitch)) { - const int srcPitch = fw * 4; - for (int y = 0; y < fh; ++y) { - std::memcpy(static_cast(pixels) + y * pitch, - local_pixels.data() + y * srcPitch, srcPitch); - } - SDL_UnlockTexture(texture); - } - } - } - - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); - SDL_RenderClear(renderer); - if (texture) { - SDL_RenderTexture(renderer, texture, nullptr, nullptr); - } - SDL_RenderPresent(renderer); - - auto now = std::chrono::steady_clock::now(); - if (now - last_report >= std::chrono::seconds(5)) { - last_report = now; - const char *src_name = - g_selected_source.load(std::memory_order_relaxed) == - static_cast(SelectedSource::Webcam) - ? "webcam" - : "sim_frame"; - std::cout << "[human] Status: " << g_audio_frames.load() - << " audio frames, " << g_video_frames.load() - << " video frames received (showing: " << src_name << ").\n"; - } - - SDL_Delay(16); - } - - // ----- Cleanup ----- - std::cout << "[human] Shutting down...\n"; - std::cout << "[human] Total received: " << g_audio_frames.load() - << " audio frames, " << g_video_frames.load() << " video frames.\n"; - - if (input_thread.joinable()) - input_thread.detach(); - - // Room destructor calls stopAllReaders() which closes streams and joins - // reader threads, then tears down FFI state. - room.reset(); - livekit::shutdown(); - - { - std::lock_guard lock(speaker_mutex); - speaker.reset(); - } - - if (texture) - SDL_DestroyTexture(texture); - SDL_DestroyRenderer(renderer); - SDL_DestroyWindow(window); - SDL_Quit(); - - std::cout << "[human] Done.\n"; - return 0; -} diff --git a/examples/bridge_human_robot/robot.cpp b/examples/bridge_human_robot/robot.cpp deleted file mode 100644 index 041580ef..00000000 --- a/examples/bridge_human_robot/robot.cpp +++ /dev/null @@ -1,661 +0,0 @@ -/* - * Copyright 2025 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. - */ - -/* - * Robot example -- streams real webcam video and microphone audio to a - * LiveKit room using SDL3 for hardware capture. - * - * Usage: - * robot [--no-mic] - * LIVEKIT_URL=... LIVEKIT_TOKEN=... robot [--no-mic] - * - * --no-mic Do not create robot-mic audio track (no SDL audio init, no mic - * capture). - * - * The token must grant identity "robot". Generate one with: - * lk token create --api-key --api-secret \ - * --join --room my-room --identity robot \ - * --valid-for 24h - * - * Run alongside the "human" example (which displays the robot's feed). - */ - -#include "livekit/audio_frame.h" -#include "livekit/track.h" -#include "livekit/video_frame.h" -#include "livekit_bridge/livekit_bridge.h" -#include "sdl_media.h" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// ---- Minimal 5x7 bitmap font for rendering text into RGBA buffers ---- -// Each glyph is 5 columns wide, 7 rows tall, stored as 7 bytes (one per row, -// MSB = leftmost pixel). Only printable ASCII 0x20..0x7E are defined. -namespace bitmap_font { - -constexpr int kGlyphW = 5; -constexpr int kGlyphH = 7; - -// clang-format off -static const std::uint8_t kGlyphs[][kGlyphH] = { - // 0x20 ' ' - {0x00,0x00,0x00,0x00,0x00,0x00,0x00}, - // 0x21 '!' - {0x20,0x20,0x20,0x20,0x00,0x20,0x00}, - // 0x22 '"' - {0x50,0x50,0x00,0x00,0x00,0x00,0x00}, - // 0x23 '#' - {0x50,0xF8,0x50,0x50,0xF8,0x50,0x00}, - // 0x24 '$' - {0x20,0x78,0xA0,0x70,0x28,0xF0,0x20}, - // 0x25 '%' - {0xC8,0xC8,0x10,0x20,0x48,0x98,0x00}, - // 0x26 '&' - {0x40,0xA0,0x40,0xA8,0x90,0x68,0x00}, - // 0x27 '\'' - {0x20,0x20,0x00,0x00,0x00,0x00,0x00}, - // 0x28 '(' - {0x10,0x20,0x40,0x40,0x20,0x10,0x00}, - // 0x29 ')' - {0x40,0x20,0x10,0x10,0x20,0x40,0x00}, - // 0x2A '*' - {0x00,0x50,0x20,0xF8,0x20,0x50,0x00}, - // 0x2B '+' - {0x00,0x20,0x20,0xF8,0x20,0x20,0x00}, - // 0x2C ',' - {0x00,0x00,0x00,0x00,0x20,0x20,0x40}, - // 0x2D '-' - {0x00,0x00,0x00,0xF8,0x00,0x00,0x00}, - // 0x2E '.' - {0x00,0x00,0x00,0x00,0x00,0x20,0x00}, - // 0x2F '/' - {0x08,0x08,0x10,0x20,0x40,0x80,0x00}, - // 0x30 '0' - {0x70,0x88,0x98,0xA8,0xC8,0x70,0x00}, - // 0x31 '1' - {0x20,0x60,0x20,0x20,0x20,0x70,0x00}, - // 0x32 '2' - {0x70,0x88,0x08,0x30,0x40,0xF8,0x00}, - // 0x33 '3' - {0x70,0x88,0x30,0x08,0x88,0x70,0x00}, - // 0x34 '4' - {0x10,0x30,0x50,0x90,0xF8,0x10,0x00}, - // 0x35 '5' - {0xF8,0x80,0xF0,0x08,0x08,0xF0,0x00}, - // 0x36 '6' - {0x30,0x40,0xF0,0x88,0x88,0x70,0x00}, - // 0x37 '7' - {0xF8,0x08,0x10,0x20,0x20,0x20,0x00}, - // 0x38 '8' - {0x70,0x88,0x70,0x88,0x88,0x70,0x00}, - // 0x39 '9' - {0x70,0x88,0x88,0x78,0x10,0x60,0x00}, - // 0x3A ':' - {0x00,0x00,0x20,0x00,0x20,0x00,0x00}, - // 0x3B ';' - {0x00,0x00,0x20,0x00,0x20,0x20,0x40}, - // 0x3C '<' - {0x08,0x10,0x20,0x40,0x20,0x10,0x08}, - // 0x3D '=' - {0x00,0x00,0xF8,0x00,0xF8,0x00,0x00}, - // 0x3E '>' - {0x80,0x40,0x20,0x10,0x20,0x40,0x80}, - // 0x3F '?' - {0x70,0x88,0x10,0x20,0x00,0x20,0x00}, - // 0x40 '@' - {0x70,0x88,0xB8,0xB8,0x80,0x70,0x00}, - // 0x41 'A' - {0x70,0x88,0x88,0xF8,0x88,0x88,0x00}, - // 0x42 'B' - {0xF0,0x88,0xF0,0x88,0x88,0xF0,0x00}, - // 0x43 'C' - {0x70,0x88,0x80,0x80,0x88,0x70,0x00}, - // 0x44 'D' - {0xF0,0x88,0x88,0x88,0x88,0xF0,0x00}, - // 0x45 'E' - {0xF8,0x80,0xF0,0x80,0x80,0xF8,0x00}, - // 0x46 'F' - {0xF8,0x80,0xF0,0x80,0x80,0x80,0x00}, - // 0x47 'G' - {0x70,0x88,0x80,0xB8,0x88,0x70,0x00}, - // 0x48 'H' - {0x88,0x88,0xF8,0x88,0x88,0x88,0x00}, - // 0x49 'I' - {0x70,0x20,0x20,0x20,0x20,0x70,0x00}, - // 0x4A 'J' - {0x08,0x08,0x08,0x08,0x88,0x70,0x00}, - // 0x4B 'K' - {0x88,0x90,0xA0,0xC0,0xA0,0x90,0x88}, - // 0x4C 'L' - {0x80,0x80,0x80,0x80,0x80,0xF8,0x00}, - // 0x4D 'M' - {0x88,0xD8,0xA8,0x88,0x88,0x88,0x00}, - // 0x4E 'N' - {0x88,0xC8,0xA8,0x98,0x88,0x88,0x00}, - // 0x4F 'O' - {0x70,0x88,0x88,0x88,0x88,0x70,0x00}, - // 0x50 'P' - {0xF0,0x88,0x88,0xF0,0x80,0x80,0x00}, - // 0x51 'Q' - {0x70,0x88,0x88,0xA8,0x90,0x68,0x00}, - // 0x52 'R' - {0xF0,0x88,0x88,0xF0,0xA0,0x90,0x00}, - // 0x53 'S' - {0x70,0x80,0x70,0x08,0x88,0x70,0x00}, - // 0x54 'T' - {0xF8,0x20,0x20,0x20,0x20,0x20,0x00}, - // 0x55 'U' - {0x88,0x88,0x88,0x88,0x88,0x70,0x00}, - // 0x56 'V' - {0x88,0x88,0x88,0x50,0x50,0x20,0x00}, - // 0x57 'W' - {0x88,0x88,0x88,0xA8,0xA8,0x50,0x00}, - // 0x58 'X' - {0x88,0x50,0x20,0x20,0x50,0x88,0x00}, - // 0x59 'Y' - {0x88,0x50,0x20,0x20,0x20,0x20,0x00}, - // 0x5A 'Z' - {0xF8,0x10,0x20,0x40,0x80,0xF8,0x00}, - // 0x5B '[' - {0x70,0x40,0x40,0x40,0x40,0x70,0x00}, - // 0x5C '\\' - {0x80,0x40,0x20,0x10,0x08,0x08,0x00}, - // 0x5D ']' - {0x70,0x10,0x10,0x10,0x10,0x70,0x00}, - // 0x5E '^' - {0x20,0x50,0x88,0x00,0x00,0x00,0x00}, - // 0x5F '_' - {0x00,0x00,0x00,0x00,0x00,0xF8,0x00}, - // 0x60 '`' - {0x40,0x20,0x00,0x00,0x00,0x00,0x00}, - // 0x61 'a' - {0x00,0x70,0x08,0x78,0x88,0x78,0x00}, - // 0x62 'b' - {0x80,0x80,0xF0,0x88,0x88,0xF0,0x00}, - // 0x63 'c' - {0x00,0x70,0x80,0x80,0x80,0x70,0x00}, - // 0x64 'd' - {0x08,0x08,0x78,0x88,0x88,0x78,0x00}, - // 0x65 'e' - {0x00,0x70,0x88,0xF8,0x80,0x70,0x00}, - // 0x66 'f' - {0x30,0x40,0xF0,0x40,0x40,0x40,0x00}, - // 0x67 'g' - {0x00,0x78,0x88,0x78,0x08,0x70,0x00}, - // 0x68 'h' - {0x80,0x80,0xF0,0x88,0x88,0x88,0x00}, - // 0x69 'i' - {0x20,0x00,0x60,0x20,0x20,0x70,0x00}, - // 0x6A 'j' - {0x10,0x00,0x30,0x10,0x10,0x10,0x60}, - // 0x6B 'k' - {0x80,0x90,0xA0,0xC0,0xA0,0x90,0x00}, - // 0x6C 'l' - {0x60,0x20,0x20,0x20,0x20,0x70,0x00}, - // 0x6D 'm' - {0x00,0xD0,0xA8,0xA8,0x88,0x88,0x00}, - // 0x6E 'n' - {0x00,0xF0,0x88,0x88,0x88,0x88,0x00}, - // 0x6F 'o' - {0x00,0x70,0x88,0x88,0x88,0x70,0x00}, - // 0x70 'p' - {0x00,0xF0,0x88,0xF0,0x80,0x80,0x00}, - // 0x71 'q' - {0x00,0x78,0x88,0x78,0x08,0x08,0x00}, - // 0x72 'r' - {0x00,0xB0,0xC8,0x80,0x80,0x80,0x00}, - // 0x73 's' - {0x00,0x78,0x80,0x70,0x08,0xF0,0x00}, - // 0x74 't' - {0x40,0xF0,0x40,0x40,0x48,0x30,0x00}, - // 0x75 'u' - {0x00,0x88,0x88,0x88,0x98,0x68,0x00}, - // 0x76 'v' - {0x00,0x88,0x88,0x50,0x50,0x20,0x00}, - // 0x77 'w' - {0x00,0x88,0x88,0xA8,0xA8,0x50,0x00}, - // 0x78 'x' - {0x00,0x88,0x50,0x20,0x50,0x88,0x00}, - // 0x79 'y' - {0x00,0x88,0x88,0x78,0x08,0x70,0x00}, - // 0x7A 'z' - {0x00,0xF8,0x10,0x20,0x40,0xF8,0x00}, - // 0x7B '{' - {0x18,0x20,0x60,0x20,0x20,0x18,0x00}, - // 0x7C '|' - {0x20,0x20,0x20,0x20,0x20,0x20,0x00}, - // 0x7D '}' - {0xC0,0x20,0x30,0x20,0x20,0xC0,0x00}, - // 0x7E '~' - {0x00,0x00,0x48,0xB0,0x00,0x00,0x00}, -}; -// clang-format on - -/// Draw a string into an RGBA buffer at the given pixel coordinate. -/// Each character is drawn at `scale` times the native 5x7 size. -static void drawString(std::uint8_t *buf, int buf_w, int buf_h, int x0, int y0, - const std::string &text, int scale, std::uint8_t r, - std::uint8_t g, std::uint8_t b) { - int cx = x0; - for (char ch : text) { - int idx = static_cast(ch) - 0x20; - if (idx < 0 || - idx >= static_cast(sizeof(kGlyphs) / sizeof(kGlyphs[0]))) - idx = 0; // fallback to space - for (int row = 0; row < kGlyphH; ++row) { - std::uint8_t bits = kGlyphs[idx][row]; - for (int col = 0; col < kGlyphW; ++col) { - if (bits & (0x80 >> col)) { - for (int sy = 0; sy < scale; ++sy) { - for (int sx = 0; sx < scale; ++sx) { - int px = cx + col * scale + sx; - int py = y0 + row * scale + sy; - if (px >= 0 && px < buf_w && py >= 0 && py < buf_h) { - int off = (py * buf_w + px) * 4; - buf[off + 0] = r; - buf[off + 1] = g; - buf[off + 2] = b; - buf[off + 3] = 255; - } - } - } - } - } - } - cx += (kGlyphW + 1) * scale; // 1px spacing between characters - } -} - -} // namespace bitmap_font - -static std::atomic g_running{true}; -static void handleSignal(int) { g_running.store(false); } - -int main(int argc, char *argv[]) { - // ----- Parse args / env ----- - bool use_mic = true; - std::vector positional; - for (int i = 1; i < argc; ++i) { - if (std::strcmp(argv[i], "--no-mic") == 0) { - use_mic = false; - } else { - positional.push_back(argv[i]); - } - } - - std::string url, token; - auto is_ws_url = [](const std::string &s) { - return (s.size() >= 5 && s.compare(0, 5, "ws://") == 0) || - (s.size() >= 6 && s.compare(0, 6, "wss://") == 0); - }; - if (positional.size() >= 2) { - for (const auto &arg : positional) { - if (is_ws_url(arg)) { - url = arg; - break; - } - } - for (const auto &arg : positional) { - if (arg != url) { - token = arg; - break; - } - } - if (url.empty()) - url = positional[0], token = positional[1]; // fallback by position - } else { - const char *e = std::getenv("LIVEKIT_URL"); - if (e) - url = e; - e = std::getenv("LIVEKIT_TOKEN"); - if (e) - token = e; - } - if (url.empty() || token.empty()) { - std::cerr << "Usage: robot [--no-mic] \n" - << " or: LIVEKIT_URL=... LIVEKIT_TOKEN=... robot [--no-mic]\n"; - return 1; - } - - std::signal(SIGINT, handleSignal); - - // ----- Initialize SDL3 (audio only when mic is used) ----- - Uint32 sdl_flags = SDL_INIT_CAMERA; - if (use_mic) - sdl_flags |= SDL_INIT_AUDIO; - if (!SDL_Init(sdl_flags)) { - LK_LOG_ERROR("[robot] SDL_Init failed: {}", SDL_GetError()); - return 1; - } - - // ----- Connect to LiveKit ----- - livekit_bridge::LiveKitBridge bridge; - LK_LOG_INFO("[robot] Connecting to {} ...", url); - livekit::RoomOptions options; - options.auto_subscribe = true; - if (!bridge.connect(url, token, options)) { - LK_LOG_ERROR("[robot] Failed to connect."); - SDL_Quit(); - return 1; - } - LK_LOG_INFO("[robot] Connected."); - - // ----- Create outgoing tracks ----- - constexpr int kSampleRate = 48000; - constexpr int kChannels = 1; - constexpr int kWidth = 1280; - constexpr int kHeight = 720; - - constexpr int kSimWidth = 480; - constexpr int kSimHeight = 320; - - std::shared_ptr mic; - if (use_mic) { - mic = bridge.createAudioTrack("robot-mic", kSampleRate, kChannels, - livekit::TrackSource::SOURCE_MICROPHONE); - } - auto sim_audio = - bridge.createAudioTrack("robot-sim-audio", kSampleRate, kChannels, - livekit::TrackSource::SOURCE_SCREENSHARE_AUDIO); - auto cam = bridge.createVideoTrack("robot-cam", kWidth, kHeight, - livekit::TrackSource::SOURCE_CAMERA); - auto sim_cam = - bridge.createVideoTrack("robot-sim-frame", kSimWidth, kSimHeight, - livekit::TrackSource::SOURCE_SCREENSHARE); - LK_LOG_INFO("[robot] Publishing {} sim audio ({} Hz, {} ch), cam + sim frame " - "({}x{} / {}x{}).", - use_mic ? "mic + " : "(no mic) ", kSampleRate, kChannels, kWidth, - kHeight, kSimWidth, kSimHeight); - - // ----- SDL Mic capture (only when use_mic) ----- - // SDLMicSource pulls 10ms frames from the default recording device and - // invokes our callback with interleaved int16 samples. - bool mic_using_sdl = false; - std::unique_ptr sdl_mic; - std::atomic mic_running{true}; - std::thread mic_thread; - - if (use_mic) { - int recCount = 0; - SDL_AudioDeviceID *recDevs = SDL_GetAudioRecordingDevices(&recCount); - bool has_mic = recDevs && recCount > 0; - if (recDevs) - SDL_free(recDevs); - - if (has_mic) { - sdl_mic = std::make_unique( - kSampleRate, kChannels, kSampleRate / 100, // 10ms frames - [&mic](const int16_t *samples, int num_samples_per_channel, - int /*sample_rate*/, int /*num_channels*/) { - if (mic && !mic->pushFrame(samples, num_samples_per_channel)) { - LK_LOG_WARN("[robot] Mic track released."); - } - }); - - if (sdl_mic->init()) { - mic_using_sdl = true; - LK_LOG_INFO("[robot] Using SDL microphone."); - mic_thread = std::thread([&]() { - while (mic_running.load()) { - sdl_mic->pump(); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - }); - } else { - LK_LOG_ERROR("[robot] SDL mic init failed."); - sdl_mic.reset(); - } - } - - if (!mic_using_sdl) { - LK_LOG_INFO("[robot] No microphone found; sending silence."); - mic_thread = std::thread([&]() { - const int kSamplesPerFrame = kSampleRate / 100; - std::vector silence(kSamplesPerFrame * kChannels, 0); - auto next = std::chrono::steady_clock::now(); - while (mic_running.load()) { - if (mic && !mic->pushFrame(silence, kSamplesPerFrame)) { - break; - } - next += std::chrono::milliseconds(10); - std::this_thread::sleep_until(next); - } - }); - } - } - - // ----- SDL Camera capture ----- - // SDLCamSource grabs webcam frames and invokes our callback with raw pixels. - bool cam_using_sdl = false; - std::unique_ptr sdl_cam; - std::atomic cam_running{true}; - std::thread cam_thread; - - { - int camCount = 0; - SDL_CameraID *cams = SDL_GetCameras(&camCount); - bool has_cam = cams && camCount > 0; - if (cams) - SDL_free(cams); - - if (has_cam) { - sdl_cam = std::make_unique( - kWidth, kHeight, 30, SDL_PIXELFORMAT_RGBA32, - [&cam](const uint8_t *pixels, int pitch, int width, int height, - SDL_PixelFormat /*fmt*/, Uint64 timestampNS) { - const int dstPitch = width * 4; - std::vector buf(dstPitch * height); - for (int y = 0; y < height; ++y) { - std::memcpy(buf.data() + y * dstPitch, pixels + y * pitch, - dstPitch); - } - if (!cam->pushFrame( - buf.data(), buf.size(), - static_cast(timestampNS / 1000))) { - LK_LOG_WARN("[robot] Cam track released."); - } - }); - - if (sdl_cam->init()) { - cam_using_sdl = true; - LK_LOG_INFO("[robot] Using SDL camera."); - cam_thread = std::thread([&]() { - while (cam_running.load()) { - sdl_cam->pump(); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - }); - } else { - LK_LOG_ERROR("[robot] SDL camera init failed."); - sdl_cam.reset(); - } - } - - if (!cam_using_sdl) { - LK_LOG_INFO("[robot] No camera found; sending solid green frames."); - cam_thread = std::thread([&]() { - std::vector green(kWidth * kHeight * 4); - for (int i = 0; i < kWidth * kHeight; ++i) { - green[i * 4 + 0] = 0; - green[i * 4 + 1] = 180; - green[i * 4 + 2] = 0; - green[i * 4 + 3] = 255; - } - std::int64_t ts = 0; - while (cam_running.load()) { - if (!cam->pushFrame(green, ts)) { - break; - } - ts += 33333; - std::this_thread::sleep_for(std::chrono::milliseconds(33)); - } - }); - } - } - - // ----- Sim frame video track (red bg, white text with frame # and time) - // ----- - std::atomic sim_running{true}; - std::thread sim_thread([&]() { - const std::size_t buf_size = kSimWidth * kSimHeight * 4; - std::vector frame(buf_size); - std::uint64_t frame_num = 0; - auto start = std::chrono::steady_clock::now(); - - while (sim_running.load()) { - // Fill with red background - for (int i = 0; i < kSimWidth * kSimHeight; ++i) { - frame[i * 4 + 0] = 200; // R - frame[i * 4 + 1] = 30; // G - frame[i * 4 + 2] = 30; // B - frame[i * 4 + 3] = 255; // A - } - - // Compute elapsed time - auto now = std::chrono::steady_clock::now(); - auto elapsed_ms = - std::chrono::duration_cast(now - start) - .count(); - int secs = static_cast(elapsed_ms / 1000); - int ms = static_cast(elapsed_ms % 1000); - - // Build text lines - std::string line1 = "FRAME " + std::to_string(frame_num); - char time_buf[32]; - std::snprintf(time_buf, sizeof(time_buf), "T=%d.%03ds", secs, ms); - std::string line2(time_buf); - - // Draw white text at scale=4 (each character is 20x28 pixels) - constexpr int kScale = 4; - constexpr int kCharW = (bitmap_font::kGlyphW + 1) * kScale; - int line1_w = static_cast(line1.size()) * kCharW; - int line2_w = static_cast(line2.size()) * kCharW; - int y1 = (kSimHeight / 2) - (bitmap_font::kGlyphH * kScale) - 4; - int y2 = (kSimHeight / 2) + 4; - int x1 = (kSimWidth - line1_w) / 2; - int x2 = (kSimWidth - line2_w) / 2; - - bitmap_font::drawString(frame.data(), kSimWidth, kSimHeight, x1, y1, - line1, kScale, 255, 255, 255); - bitmap_font::drawString(frame.data(), kSimWidth, kSimHeight, x2, y2, - line2, kScale, 255, 255, 255); - - std::int64_t ts = static_cast(elapsed_ms) * 1000; - if (!sim_cam->pushFrame(frame, ts)) { - break; - } - ++frame_num; - std::this_thread::sleep_for(std::chrono::milliseconds(33)); - } - }); - LK_LOG_INFO("[robot] Sim frame track started."); - - // ----- Sim audio track (siren: sine sweep 600-1200 Hz, 1s period) ----- - std::atomic sim_audio_running{true}; - std::thread sim_audio_thread([&]() { - const int kFrameSamples = kSampleRate / 100; // 10ms frames - constexpr double kLoFreq = 600.0; - constexpr double kHiFreq = 1200.0; - constexpr double kSweepPeriod = 1.0; // seconds per full up-down cycle - constexpr double kAmplitude = 16000.0; - constexpr double kTwoPi = 2.0 * 3.14159265358979323846; - - std::vector buf(kFrameSamples * kChannels); - double phase = 0.0; - std::uint64_t sample_idx = 0; - auto next = std::chrono::steady_clock::now(); - - while (sim_audio_running.load()) { - for (int i = 0; i < kFrameSamples; ++i) { - double t = static_cast(sample_idx) / kSampleRate; - // Triangle sweep between kLoFreq and kHiFreq - double sweep = std::fmod(t / kSweepPeriod, 1.0); - double freq = - (sweep < 0.5) - ? kLoFreq + (kHiFreq - kLoFreq) * (sweep * 2.0) - : kHiFreq - (kHiFreq - kLoFreq) * ((sweep - 0.5) * 2.0); - phase += kTwoPi * freq / kSampleRate; - if (phase > kTwoPi) - phase -= kTwoPi; - auto sample = static_cast(kAmplitude * std::sin(phase)); - for (int ch = 0; ch < kChannels; ++ch) - buf[i * kChannels + ch] = sample; - ++sample_idx; - } - if (!sim_audio->pushFrame(buf, kFrameSamples)) { - break; - } - next += std::chrono::milliseconds(10); - std::this_thread::sleep_until(next); - } - }); - LK_LOG_INFO("[robot] Sim audio (siren) track started."); - - // ----- Main loop: keep alive + pump SDL events ----- - LK_LOG_INFO("[robot] Streaming... press Ctrl-C to stop."); - - while (g_running.load()) { - SDL_Event e; - while (SDL_PollEvent(&e)) { - if (e.type == SDL_EVENT_QUIT) { - g_running.store(false); - } - } - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - - // ----- Cleanup ----- - LK_LOG_INFO("[robot] Shutting down..."); - - mic_running.store(false); - cam_running.store(false); - sim_running.store(false); - sim_audio_running.store(false); - if (mic_thread.joinable()) - mic_thread.join(); - if (cam_thread.joinable()) - cam_thread.join(); - if (sim_thread.joinable()) - sim_thread.join(); - if (sim_audio_thread.joinable()) - sim_audio_thread.join(); - sdl_mic.reset(); - sdl_cam.reset(); - - mic.reset(); - sim_audio.reset(); - cam.reset(); - sim_cam.reset(); - bridge.disconnect(); - - SDL_Quit(); - LK_LOG_INFO("[robot] Done."); - return 0; -} diff --git a/examples/bridge_mute_unmute/README.md b/examples/bridge_mute_unmute/README.md deleted file mode 100644 index 6dcd5443..00000000 --- a/examples/bridge_mute_unmute/README.md +++ /dev/null @@ -1,94 +0,0 @@ -# Bridge Mute/Unmute Example - -Demonstrates remote track control using the `LiveKitBridge` built-in -track-control RPC. A **receiver** publishes audio and video tracks, and a -**caller** subscribes to them and toggles mute/unmute every few seconds. - -## How it works - -| Executable | Role | -|-----------------------|------| -| **BridgeMuteReceiver** | Publishes an audio track (`"mic"`) and a video track (`"cam"`) using SDL3 hardware capture when available, falling back to silence and solid-color frames otherwise. The bridge automatically registers a built-in `lk.bridge.track-control` RPC handler on connect. | -| **BridgeMuteCaller** | Subscribes to the receiver's mic and cam tracks, renders them via SDL3 (speaker + window), and periodically calls `requestRemoteTrackMute` / `requestRemoteTrackUnmute` to toggle both tracks. | - -When the caller mutes a track, the receiver's `LocalAudioTrack::mute()` or -`LocalVideoTrack::mute()` is invoked via RPC, which signals the LiveKit -server to stop forwarding that track's media. The caller's audio goes -silent and the video freezes on the last received frame. On unmute, media -delivery resumes. - -## Running - -Generate two tokens for the same room with different identities: - -```bash -lk token create --join --room my-room --identity receiver --valid-for 24h -lk token create --join --room my-room --identity caller --valid-for 24h -``` - -Start the receiver first, then the caller: - -```bash -# Terminal 1 -LIVEKIT_URL=wss://... LIVEKIT_TOKEN= ./build-release/bin/BridgeMuteReceiver - -# Terminal 2 -LIVEKIT_URL=wss://... LIVEKIT_TOKEN= ./build-release/bin/BridgeMuteCaller -``` - -The caller also accepts an optional third argument for the receiver's -identity (defaults to `"receiver"`): - -```bash -./build-release/bin/BridgeMuteCaller wss://... my-receiver -``` - -## Sample output - -### Receiver - -``` -./build-release/bin/BridgeMuteReceiver -[receiver] Connecting to wss://sderosasandbox-15g80zq7.livekit.cloud ... -[receiver] Connected. -cs.state() is 1 connection_state_ is 1 -[receiver] Published audio track "mic" and video track "cam". -[receiver] Waiting for remote mute/unmute commands... -[receiver] Using SDL microphone. -[receiver] Using SDL camera. -[receiver] Press Ctrl-C to stop. -[RpcController] Handling track control RPC: mute:mic -[RpcController] Handling track control RPC: mute:cam -[RpcController] Handling track control RPC: unmute:mic -[RpcController] Handling track control RPC: unmute:cam -``` - -### Caller - -``` -./build-release/bin/BridgeMuteCaller -[caller] Connecting to wss://sderosasandbox-15g80zq7.livekit.cloud ... -cs.state() is 1 connection_state_ is 1 -[caller] Connected. -[caller] Target receiver identity: "receiver" -[caller] Subscribed to receiver's mic + cam. -[caller] Rendering receiver feed. Toggling mute every 5s. Close window or Ctrl-C to stop. -[caller] Speaker opened: 48000 Hz, 1 ch. - -[caller] --- Cycle 1: MUTE --- -[caller] mic: muted OK -[caller] cam: muted OK - -[caller] --- Cycle 2: UNMUTE --- -[caller] mic: unmuted OK -[caller] cam: unmuted OK -``` - -## Notes - -- The receiver uses SDL3 for microphone and camera capture. On macOS you - may need to grant camera/microphone permissions. -- If no hardware is detected, the receiver falls back to sending silence - (audio) and alternating solid-color frames (video). -- The caller opens an SDL3 window to render the received video and plays - audio through the default speaker. diff --git a/examples/bridge_mute_unmute/caller.cpp b/examples/bridge_mute_unmute/caller.cpp deleted file mode 100644 index a47b2e11..00000000 --- a/examples/bridge_mute_unmute/caller.cpp +++ /dev/null @@ -1,337 +0,0 @@ -/* - * Copyright 2026 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. - */ - -/* - * Caller (controller) for the bridge mute/unmute example. - * - * Connects to the same room as the receiver, subscribes to the receiver's - * "mic" and "cam" tracks, and renders them via SDL3 (speaker + window). - * Every 5 seconds the caller toggles mute/unmute on both tracks via RPC, - * so you can see and hear the tracks go silent and come back. - * - * Usage: - * BridgeMuteCaller [receiver-identity] - * LIVEKIT_URL=... LIVEKIT_TOKEN=... BridgeMuteCaller [receiver-identity] - * - * The token must grant a different identity (e.g. "caller"). Generate with: - * lk token create --api-key --api-secret \ - * --join --room my-room --identity caller --valid-for 24h - */ - -#include "livekit/audio_frame.h" -#include "livekit/rpc_error.h" -#include "livekit/track.h" -#include "livekit/video_frame.h" -#include "livekit_bridge/livekit_bridge.h" -#include "sdl_media.h" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -static std::atomic g_running{true}; -static void handleSignal(int) { g_running.store(false); } - -struct LatestVideoFrame { - std::mutex mutex; - std::vector data; - int width = 0; - int height = 0; - bool dirty = false; -}; - -static LatestVideoFrame g_latest_video; - -static void storeFrame(const livekit::VideoFrame &frame) { - const std::uint8_t *src = frame.data(); - const std::size_t size = frame.dataSize(); - if (!src || size == 0) - return; - - std::lock_guard lock(g_latest_video.mutex); - g_latest_video.data.assign(src, src + size); - g_latest_video.width = frame.width(); - g_latest_video.height = frame.height(); - g_latest_video.dirty = true; -} - -int main(int argc, char *argv[]) { - std::string url, token; - std::string receiver_identity = "receiver"; - - std::vector positional; - for (int i = 1; i < argc; ++i) { - positional.push_back(argv[i]); - } - - if (positional.size() >= 2) { - url = positional[0]; - token = positional[1]; - if (positional.size() >= 3) - receiver_identity = positional[2]; - } else { - const char *e = std::getenv("LIVEKIT_URL"); - if (e) - url = e; - e = std::getenv("LIVEKIT_TOKEN"); - if (e) - token = e; - if (!positional.empty()) - receiver_identity = positional[0]; - } - if (url.empty() || token.empty()) { - std::cerr - << "Usage: BridgeMuteCaller [receiver-identity]\n" - << " or: LIVEKIT_URL=... LIVEKIT_TOKEN=... BridgeMuteCaller " - "[receiver-identity]\n" - << "Default receiver-identity: \"receiver\"\n"; - return 1; - } - - std::signal(SIGINT, handleSignal); - - // ----- Initialize SDL3 ----- - if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) { - std::cerr << "[caller] SDL_Init failed: " << SDL_GetError() << "\n"; - return 1; - } - - constexpr int kWindowWidth = 640; - constexpr int kWindowHeight = 480; - - SDL_Window *window = SDL_CreateWindow("Caller - Receiver Feed", kWindowWidth, - kWindowHeight, 0); - if (!window) { - std::cerr << "[caller] SDL_CreateWindow failed: " << SDL_GetError() << "\n"; - SDL_Quit(); - return 1; - } - - SDL_Renderer *renderer = SDL_CreateRenderer(window, nullptr); - if (!renderer) { - std::cerr << "[caller] SDL_CreateRenderer failed: " << SDL_GetError() - << "\n"; - SDL_DestroyWindow(window); - SDL_Quit(); - return 1; - } - - SDL_Texture *texture = nullptr; - int tex_width = 0; - int tex_height = 0; - - std::unique_ptr speaker; - std::mutex speaker_mutex; - - // ----- Connect to LiveKit ----- - livekit_bridge::LiveKitBridge bridge; - std::cout << "[caller] Connecting to " << url << " ...\n"; - - livekit::RoomOptions options; - options.auto_subscribe = true; - - if (!bridge.connect(url, token, options)) { - std::cerr << "[caller] Failed to connect.\n"; - SDL_DestroyRenderer(renderer); - SDL_DestroyWindow(window); - SDL_Quit(); - return 1; - } - std::cout << "[caller] Connected.\n"; - std::cout << "[caller] Target receiver identity: \"" << receiver_identity - << "\"\n"; - - // ----- Subscribe to receiver's audio ----- - bridge.setOnAudioFrameCallback( - receiver_identity, livekit::TrackSource::SOURCE_MICROPHONE, - [&speaker, &speaker_mutex](const livekit::AudioFrame &frame) { - const auto &samples = frame.data(); - if (samples.empty()) - return; - - std::lock_guard lock(speaker_mutex); - if (!speaker) { - speaker = std::make_unique(frame.sample_rate(), - frame.num_channels()); - if (!speaker->init()) { - std::cerr << "[caller] Failed to init SDL speaker.\n"; - speaker.reset(); - return; - } - std::cout << "[caller] Speaker opened: " << frame.sample_rate() - << " Hz, " << frame.num_channels() << " ch.\n"; - } - speaker->enqueue(samples.data(), frame.samples_per_channel()); - }); - - // ----- Subscribe to receiver's video ----- - bridge.setOnVideoFrameCallback( - receiver_identity, livekit::TrackSource::SOURCE_CAMERA, - [](const livekit::VideoFrame &frame, std::int64_t /*timestamp_us*/) { - storeFrame(frame); - }); - - std::cout << "[caller] Subscribed to receiver's mic + cam.\n"; - - // ----- Mute/unmute toggle thread ----- - std::atomic muted{false}; - std::atomic cycle{0}; - - std::atomic toggle_running{true}; - std::thread toggle_thread([&]() { - // Let the receiver connect and publish before we start toggling - for (int i = 0; i < 30 && toggle_running.load(); ++i) - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - - while (toggle_running.load()) { - bool currently_muted = muted.load(); - const char *action = currently_muted ? "UNMUTE" : "MUTE"; - int c = cycle.fetch_add(1) + 1; - std::cout << "\n[caller] --- Cycle " << c << ": " << action << " ---\n"; - - // Toggle audio track "mic" - try { - if (currently_muted) { - bridge.requestRemoteTrackUnmute(receiver_identity, "mic"); - std::cout << "[caller] mic: unmuted OK\n"; - } else { - bridge.requestRemoteTrackMute(receiver_identity, "mic"); - std::cout << "[caller] mic: muted OK\n"; - } - } catch (const livekit::RpcError &e) { - std::cerr << "[caller] mic: RPC error (code=" << e.code() << " msg=\"" - << e.message() << "\")\n"; - } catch (const std::exception &e) { - std::cerr << "[caller] mic: error: " << e.what() << "\n"; - } - - // Toggle video track "cam" - try { - if (currently_muted) { - bridge.requestRemoteTrackUnmute(receiver_identity, "cam"); - std::cout << "[caller] cam: unmuted OK\n"; - } else { - bridge.requestRemoteTrackMute(receiver_identity, "cam"); - std::cout << "[caller] cam: muted OK\n"; - } - } catch (const livekit::RpcError &e) { - std::cerr << "[caller] cam: RPC error (code=" << e.code() << " msg=\"" - << e.message() << "\")\n"; - } catch (const std::exception &e) { - std::cerr << "[caller] cam: error: " << e.what() << "\n"; - } - - muted.store(!currently_muted); - - // Wait ~100 seconds, checking for shutdown every 100ms - for (int i = 0; i < 100 && toggle_running.load(); ++i) - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - }); - - // ----- Main loop: render video + pump SDL events ----- - std::cout << "[caller] Rendering receiver feed. Toggling mute every 5s. " - "Close window or Ctrl-C to stop.\n"; - - std::vector local_pixels; - - while (g_running.load()) { - SDL_Event ev; - while (SDL_PollEvent(&ev)) { - if (ev.type == SDL_EVENT_QUIT) { - g_running.store(false); - } - } - - int fw = 0, fh = 0; - bool have_frame = false; - { - std::lock_guard lock(g_latest_video.mutex); - if (g_latest_video.dirty && g_latest_video.width > 0 && - g_latest_video.height > 0) { - fw = g_latest_video.width; - fh = g_latest_video.height; - local_pixels.swap(g_latest_video.data); - g_latest_video.dirty = false; - have_frame = true; - } - } - - if (have_frame) { - if (fw != tex_width || fh != tex_height) { - if (texture) - SDL_DestroyTexture(texture); - texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA32, - SDL_TEXTUREACCESS_STREAMING, fw, fh); - tex_width = fw; - tex_height = fh; - } - - if (texture) { - void *pixels = nullptr; - int pitch = 0; - if (SDL_LockTexture(texture, nullptr, &pixels, &pitch)) { - const int srcPitch = fw * 4; - for (int y = 0; y < fh; ++y) { - std::memcpy(static_cast(pixels) + y * pitch, - local_pixels.data() + y * srcPitch, srcPitch); - } - SDL_UnlockTexture(texture); - } - } - } - - SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); - SDL_RenderClear(renderer); - if (texture) { - SDL_RenderTexture(renderer, texture, nullptr, nullptr); - } - SDL_RenderPresent(renderer); - - SDL_Delay(16); - } - - // ----- Cleanup ----- - std::cout << "\n[caller] Shutting down...\n"; - toggle_running.store(false); - if (toggle_thread.joinable()) - toggle_thread.join(); - - bridge.disconnect(); - - { - std::lock_guard lock(speaker_mutex); - speaker.reset(); - } - - if (texture) - SDL_DestroyTexture(texture); - SDL_DestroyRenderer(renderer); - SDL_DestroyWindow(window); - SDL_Quit(); - - std::cout << "[caller] Done.\n"; - return 0; -} diff --git a/examples/bridge_mute_unmute/receiver.cpp b/examples/bridge_mute_unmute/receiver.cpp deleted file mode 100644 index 1abafbc9..00000000 --- a/examples/bridge_mute_unmute/receiver.cpp +++ /dev/null @@ -1,266 +0,0 @@ -/* - * Copyright 2026 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. - */ - -/* - * Receiver (publisher) for the bridge mute/unmute example. - * - * Publishes an audio track ("mic") and a video track ("cam"), then enables - * remote track control so that a remote caller can mute/unmute them via RPC. - * - * By default, captures from the real microphone and webcam using SDL3. If - * no hardware is available, falls back to silence (audio) and solid-color - * frames (video). - * - * Usage: - * BridgeMuteReceiver - * LIVEKIT_URL=... LIVEKIT_TOKEN=... BridgeMuteReceiver - * - * The token must grant identity "receiver". Generate one with: - * lk token create --api-key --api-secret \ - * --join --room my-room --identity receiver --valid-for 24h - */ - -#include "livekit/track.h" -#include "livekit_bridge/livekit_bridge.h" -#include "sdl_media.h" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -static std::atomic g_running{true}; -static void handleSignal(int) { g_running.store(false); } - -int main(int argc, char *argv[]) { - std::string url, token; - if (argc >= 3) { - url = argv[1]; - token = argv[2]; - } else { - const char *e = std::getenv("LIVEKIT_URL"); - if (e) - url = e; - e = std::getenv("LIVEKIT_TOKEN"); - if (e) - token = e; - } - if (url.empty() || token.empty()) { - std::cerr - << "Usage: BridgeMuteReceiver \n" - << " or: LIVEKIT_URL=... LIVEKIT_TOKEN=... BridgeMuteReceiver\n"; - return 1; - } - - std::signal(SIGINT, handleSignal); - - // ----- Initialize SDL3 ----- - if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_CAMERA)) { - std::cerr << "[receiver] SDL_Init failed: " << SDL_GetError() << "\n"; - return 1; - } - - // ----- Connect to LiveKit ----- - livekit_bridge::LiveKitBridge bridge; - std::cout << "[receiver] Connecting to " << url << " ...\n"; - - livekit::RoomOptions options; - options.auto_subscribe = true; - - if (!bridge.connect(url, token, options)) { - std::cerr << "[receiver] Failed to connect.\n"; - SDL_Quit(); - return 1; - } - std::cout << "[receiver] Connected.\n"; - - constexpr int kSampleRate = 48000; - constexpr int kChannels = 1; - constexpr int kWidth = 1280; - constexpr int kHeight = 720; - - auto mic = bridge.createAudioTrack("mic", kSampleRate, kChannels, - livekit::TrackSource::SOURCE_MICROPHONE); - auto cam = bridge.createVideoTrack("cam", kWidth, kHeight, - livekit::TrackSource::SOURCE_CAMERA); - - std::cout << "[receiver] Published audio track \"mic\" and video track " - "\"cam\".\n"; - std::cout << "[receiver] Waiting for remote mute/unmute commands...\n"; - - // ----- SDL Mic capture ----- - bool mic_using_sdl = false; - std::unique_ptr sdl_mic; - std::atomic mic_running{true}; - std::thread mic_thread; - - { - int recCount = 0; - SDL_AudioDeviceID *recDevs = SDL_GetAudioRecordingDevices(&recCount); - bool has_mic = recDevs && recCount > 0; - if (recDevs) - SDL_free(recDevs); - - if (has_mic) { - sdl_mic = std::make_unique( - kSampleRate, kChannels, kSampleRate / 100, - [&mic](const int16_t *samples, int num_samples_per_channel, - int /*sample_rate*/, int /*num_channels*/) { - mic->pushFrame(samples, num_samples_per_channel); - }); - - if (sdl_mic->init()) { - mic_using_sdl = true; - std::cout << "[receiver] Using SDL microphone.\n"; - mic_thread = std::thread([&]() { - while (mic_running.load()) { - sdl_mic->pump(); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - }); - } else { - std::cerr << "[receiver] SDL mic init failed.\n"; - sdl_mic.reset(); - } - } - - if (!mic_using_sdl) { - std::cout << "[receiver] No microphone found; sending silence.\n"; - mic_thread = std::thread([&]() { - const int kSamplesPerFrame = kSampleRate / 100; - std::vector silence(kSamplesPerFrame * kChannels, 0); - auto next = std::chrono::steady_clock::now(); - while (mic_running.load()) { - mic->pushFrame(silence, kSamplesPerFrame); - next += std::chrono::milliseconds(10); - std::this_thread::sleep_until(next); - } - }); - } - } - - // ----- SDL Camera capture ----- - bool cam_using_sdl = false; - std::unique_ptr sdl_cam; - std::atomic cam_running{true}; - std::thread cam_thread; - - { - int camCount = 0; - SDL_CameraID *cams = SDL_GetCameras(&camCount); - bool has_cam = cams && camCount > 0; - if (cams) - SDL_free(cams); - - if (has_cam) { - sdl_cam = std::make_unique( - kWidth, kHeight, 30, SDL_PIXELFORMAT_RGBA32, - [&cam](const uint8_t *pixels, int pitch, int width, int height, - SDL_PixelFormat /*fmt*/, Uint64 timestampNS) { - const int dstPitch = width * 4; - std::vector buf(dstPitch * height); - for (int y = 0; y < height; ++y) { - std::memcpy(buf.data() + y * dstPitch, pixels + y * pitch, - dstPitch); - } - cam->pushFrame(buf.data(), buf.size(), - static_cast(timestampNS / 1000)); - }); - - if (sdl_cam->init()) { - cam_using_sdl = true; - std::cout << "[receiver] Using SDL camera.\n"; - cam_thread = std::thread([&]() { - while (cam_running.load()) { - sdl_cam->pump(); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - }); - } else { - std::cerr << "[receiver] SDL camera init failed.\n"; - sdl_cam.reset(); - } - } - - if (!cam_using_sdl) { - std::cout << "[receiver] No camera found; sending solid-color frames.\n"; - cam_thread = std::thread([&]() { - std::vector frame(kWidth * kHeight * 4); - std::int64_t ts = 0; - int frame_num = 0; - - while (cam_running.load()) { - bool blue = (frame_num / 30) % 2 == 0; - for (int i = 0; i < kWidth * kHeight; ++i) { - frame[i * 4 + 0] = 0; - frame[i * 4 + 1] = - blue ? static_cast(0) : static_cast(180); - frame[i * 4 + 2] = - blue ? static_cast(200) : static_cast(0); - frame[i * 4 + 3] = 255; - } - - cam->pushFrame(frame, ts); - - ++frame_num; - ts += 33333; - std::this_thread::sleep_for(std::chrono::milliseconds(33)); - } - }); - } - } - - // ----- Main loop: pump SDL events (needed for camera approval on macOS) - // ----- - std::cout << "[receiver] Press Ctrl-C to stop.\n"; - while (g_running.load()) { - SDL_Event ev; - while (SDL_PollEvent(&ev)) { - if (ev.type == SDL_EVENT_QUIT) { - g_running.store(false); - } - } - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - - // ----- Cleanup ----- - std::cout << "[receiver] Shutting down...\n"; - mic_running.store(false); - cam_running.store(false); - if (mic_thread.joinable()) - mic_thread.join(); - if (cam_thread.joinable()) - cam_thread.join(); - sdl_mic.reset(); - sdl_cam.reset(); - - mic.reset(); - cam.reset(); - bridge.disconnect(); - - SDL_Quit(); - std::cout << "[receiver] Done.\n"; - return 0; -} diff --git a/examples/bridge_rpc/README.md b/examples/bridge_rpc/README.md deleted file mode 100644 index 8969619e..00000000 --- a/examples/bridge_rpc/README.md +++ /dev/null @@ -1,105 +0,0 @@ -# Bridge RPC Example - -A minimal example of custom user-registered RPC methods using the -`LiveKitBridge` high-level API. - -Two headless executables β€” **BridgeRpcReceiver** and **BridgeRpcCaller** β€” -connect to the same LiveKit room. The receiver registers a `"print"` RPC -method that logs the caller's message and sleeps for a variable duration -before responding. The caller sends a numbered message every ~1 second and -prints the round-trip time. - -## Sleep schedule - -The receiver picks a sleep duration based on the call number: - -| Call number | Sleep | -|---------------|---------| -| `%10 == 0` | 20 s | -| `%5 == 0` | 10 s | -| otherwise | 1 s | - -Because the default LiveKit RPC timeout is 15 seconds, the caller sets a -30-second timeout so the 20-second sleeps can complete. The 10-second and -20-second cases demonstrate how long-running handlers affect the caller's -blocking `performRpc` call. - -## Running - -Generate two tokens for the same room with different identities: - -```bash -lk token create --join --room my-room --identity receiver --valid-for 24h -lk token create --join --room my-room --identity caller --valid-for 24h -``` - -Start the receiver first, then the caller: - -```bash -# Terminal 1 -LIVEKIT_URL=wss://... LIVEKIT_TOKEN= ./build-release/bin/BridgeRpcReceiver - -# Terminal 2 -LIVEKIT_URL=wss://... LIVEKIT_TOKEN= ./build-release/bin/BridgeRpcCaller -``` - -## Sample output - -### Receiver - -``` -[receiver] Connecting to wss://example.livekit.cloud ... -[receiver] Connected. -[receiver] Registered RPC method "print". -[receiver] call %10==0 -> 20s sleep -[receiver] call %5==0 -> 10s sleep -[receiver] otherwise -> 1s sleep -[receiver] Waiting for calls... -[receiver] Call #1 from caller: "Hello from caller #1" (sleeping 1s) -[receiver] Call #1 done. -[receiver] Call #2 from caller: "Hello from caller #2" (sleeping 1s) -[receiver] Call #2 done. -[receiver] Call #3 from caller: "Hello from caller #3" (sleeping 1s) -[receiver] Call #3 done. -[receiver] Call #4 from caller: "Hello from caller #4" (sleeping 1s) -[receiver] Call #4 done. -[receiver] Call #5 from caller: "Hello from caller #5" (sleeping 10s) -[receiver] Call #5 done. -[receiver] Call #6 from caller: "Hello from caller #6" (sleeping 1s) -[receiver] Call #6 done. -[receiver] Call #7 from caller: "Hello from caller #7" (sleeping 1s) -[receiver] Call #7 done. -[receiver] Call #8 from caller: "Hello from caller #8" (sleeping 1s) -[receiver] Call #8 done. -[receiver] Call #9 from caller: "Hello from caller #9" (sleeping 1s) -[receiver] Call #9 done. -[receiver] Call #10 from caller: "Hello from caller #10" (sleeping 20s) -[receiver] Call #10 done. -``` - -### Caller - -``` -[caller] Connecting to wss://example.livekit.cloud ... -[caller] Connected. -[caller] #1 Sending: "Hello from caller #1" ... -[caller] #1 Response: "ok (slept 1s)" (1159ms) -[caller] #2 Sending: "Hello from caller #2" ... -[caller] #2 Response: "ok (slept 1s)" (1174ms) -[caller] #3 Sending: "Hello from caller #3" ... -[caller] #3 Response: "ok (slept 1s)" (1152ms) -[caller] #4 Sending: "Hello from caller #4" ... -[caller] #4 Response: "ok (slept 1s)" (1135ms) -[caller] #5 Sending: "Hello from caller #5" ... -[caller] #5 Response: "ok (slept 10s)" (10139ms) -[caller] #6 Sending: "Hello from caller #6" ... -[caller] #6 Response: "ok (slept 1s)" (1138ms) -[caller] #7 Sending: "Hello from caller #7" ... -[caller] #7 Response: "ok (slept 1s)" (1143ms) -[caller] #8 Sending: "Hello from caller #8" ... -[caller] #8 Response: "ok (slept 1s)" (1115ms) -[caller] #9 Sending: "Hello from caller #9" ... -[caller] #9 Response: "ok (slept 1s)" (1123ms) -[caller] #10 Sending: "Hello from caller #10" ... -[caller] #10 Response: "ok (slept 20s)" (20119ms) -``` diff --git a/examples/bridge_rpc/custom_caller.cpp b/examples/bridge_rpc/custom_caller.cpp deleted file mode 100644 index 4ff5d355..00000000 --- a/examples/bridge_rpc/custom_caller.cpp +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright 2026 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. - */ - -/* - * Caller for the bridge_rpc example. - * - * Connects to a LiveKit room as "caller" and sends a string to the - * receiver's custom "print" RPC method every second. The receiver - * sleeps for 1s, 10s, or 20s depending on the call number, so some - * calls will take noticeably longer to return. - * - * Usage: - * BridgeRpcCaller - * LIVEKIT_URL=... LIVEKIT_TOKEN=... BridgeRpcCaller - * - * Generate a token with: - * lk token create --join --room --identity caller --valid-for 24h - */ - -#include "livekit/rpc_error.h" -#include "livekit_bridge/livekit_bridge.h" - -#include -#include -#include -#include -#include -#include -#include - -static std::atomic g_running{true}; -static void handleSignal(int) { g_running.store(false); } - -int main(int argc, char *argv[]) { - std::string url, token; - if (argc >= 3) { - url = argv[1]; - token = argv[2]; - } else { - const char *e = std::getenv("LIVEKIT_URL"); - if (e) - url = e; - e = std::getenv("LIVEKIT_TOKEN"); - if (e) - token = e; - } - if (url.empty() || token.empty()) { - std::cerr << "Usage: BridgeRpcCaller \n" - << " or: LIVEKIT_URL=... LIVEKIT_TOKEN=... BridgeRpcCaller\n"; - return 1; - } - - std::signal(SIGINT, handleSignal); - - livekit_bridge::LiveKitBridge bridge; - std::cout << "[caller] Connecting to " << url << " ...\n"; - - livekit::RoomOptions options; - if (!bridge.connect(url, token, options)) { - std::cerr << "[caller] Failed to connect.\n"; - return 1; - } - std::cout << "[caller] Connected.\n"; - - // Give the receiver a moment to join and register its handler. - for (int i = 0; i < 30 && g_running.load(); ++i) - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - - int count = 0; - while (g_running.load()) { - ++count; - std::string message = "Hello from caller #" + std::to_string(count); - - std::cout << "[caller] #" << count << " Sending: \"" << message - << "\" ...\n"; - - auto t0 = std::chrono::steady_clock::now(); - try { - auto response = - bridge.performRpc("receiver", "print", message, std::nullopt); - auto elapsed = std::chrono::duration_cast( - std::chrono::steady_clock::now() - t0) - .count(); - if (response.has_value()) { - std::cout << "[caller] #" << count << " Response: \"" - << response.value() << "\" (" << elapsed << "ms)\n"; - } else { - std::cout << "[caller] #" << count << " No response (" << elapsed - << "ms)\n"; - } - } catch (const livekit::RpcError &e) { - auto elapsed = std::chrono::duration_cast( - std::chrono::steady_clock::now() - t0) - .count(); - std::cerr << "[caller] #" << count << " RPC error (code=" << e.code() - << " msg=\"" << e.message() << "\") (" << elapsed << "ms)\n"; - } catch (const std::exception &e) { - std::cerr << "[caller] #" << count << " Error: " << e.what() << "\n"; - } - - for (int i = 0; i < 10 && g_running.load(); ++i) - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - - std::cout << "[caller] Shutting down...\n"; - bridge.disconnect(); - std::cout << "[caller] Done.\n"; - return 0; -} diff --git a/examples/bridge_rpc/custom_receiver.cpp b/examples/bridge_rpc/custom_receiver.cpp deleted file mode 100644 index a98cbd3b..00000000 --- a/examples/bridge_rpc/custom_receiver.cpp +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 2026 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. - */ - -/* - * Receiver for the bridge_rpc example. - * - * Connects to a LiveKit room as "receiver", registers a custom RPC method - * called "print", and prints whatever string the caller sends. - * - * Usage: - * BridgeRpcReceiver - * LIVEKIT_URL=... LIVEKIT_TOKEN=... BridgeRpcReceiver - * - * Generate a token with: - * lk token create --join --room --identity receiver --valid-for 24h - */ - -#include "livekit_bridge/livekit_bridge.h" - -#include -#include -#include -#include -#include -#include -#include - -static std::atomic g_running{true}; -static void handleSignal(int) { g_running.store(false); } - -int main(int argc, char *argv[]) { - std::string url, token; - if (argc >= 3) { - url = argv[1]; - token = argv[2]; - } else { - const char *e = std::getenv("LIVEKIT_URL"); - if (e) - url = e; - e = std::getenv("LIVEKIT_TOKEN"); - if (e) - token = e; - } - if (url.empty() || token.empty()) { - std::cerr << "Usage: BridgeRpcReceiver \n" - << " or: LIVEKIT_URL=... LIVEKIT_TOKEN=... BridgeRpcReceiver\n"; - return 1; - } - - std::signal(SIGINT, handleSignal); - - livekit_bridge::LiveKitBridge bridge; - std::cout << "[receiver] Connecting to " << url << " ...\n"; - - livekit::RoomOptions options; - if (!bridge.connect(url, token, options)) { - std::cerr << "[receiver] Failed to connect.\n"; - return 1; - } - std::cout << "[receiver] Connected.\n"; - - std::atomic call_count{0}; - - bridge.registerRpcMethod( - "print", - [&call_count](const livekit::RpcInvocationData &data) - -> std::optional { - int n = call_count.fetch_add(1) + 1; - - int sleep_sec = 1; - if (n % 10 == 0) - sleep_sec = 20; - else if (n % 5 == 0) - sleep_sec = 10; - - std::cout << "[receiver] Call #" << n << " from " - << data.caller_identity << ": \"" << data.payload - << "\" (sleeping " << sleep_sec << "s)\n"; - - std::this_thread::sleep_for(std::chrono::seconds(sleep_sec)); - - std::cout << "[receiver] Call #" << n << " done.\n"; - return "ok (slept " + std::to_string(sleep_sec) + "s)"; - }); - - std::cout << "[receiver] Registered RPC method \"print\".\n" - << "[receiver] call %10==0 -> 20s sleep\n" - << "[receiver] call %5==0 -> 10s sleep\n" - << "[receiver] otherwise -> 1s sleep\n" - << "[receiver] Waiting for calls...\n"; - - while (g_running.load()) { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - - std::cout << "[receiver] Shutting down...\n"; - bridge.disconnect(); - std::cout << "[receiver] Done.\n"; - return 0; -} diff --git a/examples/cmake/sdl3.cmake b/examples/cmake/sdl3.cmake deleted file mode 100644 index 1ba2ccb3..00000000 --- a/examples/cmake/sdl3.cmake +++ /dev/null @@ -1,52 +0,0 @@ -# cmake/sdl3.cmake -include(FetchContent) - -# Only fetch/build SDL3 once, even if this file is included multiple times -if (NOT TARGET SDL3::SDL3) - # Prevent SDL3 from polluting our lib directory - set(SDL_INSTALL OFF CACHE BOOL "Disable SDL3 install" FORCE) - set(SDL_SHARED ON CACHE BOOL "Build shared SDL3" FORCE) - - # Save current output directories - set(_SAVE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_ARCHIVE_OUTPUT_DIRECTORY}) - set(_SAVE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}) - set(_SAVE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) - - # Set SDL3 to build into its own subdirectory - set(SDL3_OUTPUT_DIR ${CMAKE_BINARY_DIR}/_deps/sdl3-build) - set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${SDL3_OUTPUT_DIR}) - set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${SDL3_OUTPUT_DIR}) - set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${SDL3_OUTPUT_DIR}) - - # For multi-config generators (Visual Studio), also set per-config directories - foreach(CONFIG_TYPE Debug Release RelWithDebInfo MinSizeRel) - string(TOUPPER ${CONFIG_TYPE} CONFIG_TYPE_UPPER) - set(_SAVE_ARCHIVE_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER} ${CMAKE_ARCHIVE_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER}}) - set(_SAVE_LIBRARY_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER} ${CMAKE_LIBRARY_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER}}) - set(_SAVE_RUNTIME_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER}}) - - set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER} ${SDL3_OUTPUT_DIR}) - set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER} ${SDL3_OUTPUT_DIR}) - set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER} ${SDL3_OUTPUT_DIR}) - endforeach() - - FetchContent_Declare( - SDL3 - GIT_REPOSITORY https://github.com/libsdl-org/SDL.git - GIT_TAG release-3.2.26 - ) - - FetchContent_MakeAvailable(SDL3) - - # Restore output directories - set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${_SAVE_ARCHIVE_OUTPUT_DIRECTORY}) - set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${_SAVE_LIBRARY_OUTPUT_DIRECTORY}) - set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${_SAVE_RUNTIME_OUTPUT_DIRECTORY}) - - foreach(CONFIG_TYPE Debug Release RelWithDebInfo MinSizeRel) - string(TOUPPER ${CONFIG_TYPE} CONFIG_TYPE_UPPER) - set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER} ${_SAVE_ARCHIVE_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER}}) - set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER} ${_SAVE_LIBRARY_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER}}) - set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER} ${_SAVE_RUNTIME_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER}}) - endforeach() -endif() diff --git a/examples/common/sdl_media.cpp b/examples/common/sdl_media.cpp deleted file mode 100644 index d4a44e63..00000000 --- a/examples/common/sdl_media.cpp +++ /dev/null @@ -1,226 +0,0 @@ -/* - * 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 "sdl_media.h" - -#include "livekit/lk_log.h" - -// ---------------------- SDLMicSource ----------------------------- - -SDLMicSource::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::~SDLMicSource() { - if (stream_) { - SDL_DestroyAudioStream(stream_); - stream_ = nullptr; - } -} - -bool SDLMicSource::init() { - // desired output (what SDL will give us when we call SDL_GetAudioStreamData) - SDL_zero(spec_); - spec_.format = SDL_AUDIO_S16; // 16-bit signed - spec_.channels = static_cast(channels_); - spec_.freq = sample_rate_; - - // Open default recording device as an audio stream - // This works for both playback and recording, depending on the device id. - stream_ = SDL_OpenAudioDeviceStream( - SDL_AUDIO_DEVICE_DEFAULT_RECORDING, // recording device - &spec_, - nullptr, // no callback, we'll poll - nullptr); - - if (!stream_) { - LK_LOG_ERROR("Failed to open recording stream: {}", SDL_GetError()); - return false; - } - - if (!SDL_ResumeAudioStreamDevice(stream_)) { // unpause device - LK_LOG_ERROR("Failed to resume recording device: {}", SDL_GetError()); - return false; - } - - return true; -} - -void SDLMicSource::pump() { - if (!stream_ || !callback_) - return; - - const int samples_per_frame_total = frame_samples_ * channels_; - const int bytes_per_frame = samples_per_frame_total * sizeof(int16_t); - - // Only pull if at least one "frame" worth of audio is available - const int available = SDL_GetAudioStreamAvailable(stream_); // bytes - if (available < bytes_per_frame) { - return; - } - - std::vector buffer(samples_per_frame_total); - - const int got_bytes = SDL_GetAudioStreamData(stream_, buffer.data(), - bytes_per_frame); // - - if (got_bytes <= 0) { - return; // nothing or error (log if you like) - } - - const int got_samples_total = got_bytes / sizeof(int16_t); - const int got_samples_per_channel = got_samples_total / channels_; - - callback_(buffer.data(), got_samples_per_channel, sample_rate_, channels_); -} - -void SDLMicSource::pause() { - if (stream_) { - SDL_PauseAudioStreamDevice(stream_); // - } -} - -void SDLMicSource::resume() { - if (stream_) { - SDL_ResumeAudioStreamDevice(stream_); // - } -} - -// ---------------------- DDLSpeakerSink ----------------------------- - -DDLSpeakerSink::DDLSpeakerSink(int sample_rate, int channels) - : sample_rate_(sample_rate), channels_(channels) {} - -DDLSpeakerSink::~DDLSpeakerSink() { - if (stream_) { - SDL_DestroyAudioStream(stream_); // also closes device - stream_ = nullptr; - } -} - -bool DDLSpeakerSink::init() { - SDL_zero(spec_); - spec_.format = SDL_AUDIO_S16; // expect S16 input for playback - spec_.channels = static_cast(channels_); - spec_.freq = sample_rate_; - - // Open default playback device as a stream. - stream_ = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec_, - nullptr, // no callback; we'll push data - nullptr); - - if (!stream_) { - LK_LOG_ERROR("Failed to open playback stream: {}", SDL_GetError()); - return false; - } - - if (!SDL_ResumeAudioStreamDevice(stream_)) { - LK_LOG_ERROR("Failed to resume playback device: {}", SDL_GetError()); - return false; - } - - return true; -} - -void DDLSpeakerSink::enqueue(const int16_t *samples, - int num_samples_per_channel) { - if (!stream_ || !samples) - return; - - const int totalSamples = num_samples_per_channel * channels_; - const int bytes = totalSamples * static_cast(sizeof(int16_t)); - - // SDL will resample / convert as needed on SDL_GetAudioStreamData() side. - if (!SDL_PutAudioStreamData(stream_, samples, bytes)) { - LK_LOG_ERROR("SDL_PutAudioStreamData failed: {}", SDL_GetError()); - } -} - -void DDLSpeakerSink::pause() { - if (stream_) { - SDL_PauseAudioStreamDevice(stream_); - } -} - -void DDLSpeakerSink::resume() { - if (stream_) { - SDL_ResumeAudioStreamDevice(stream_); - } -} - -// ---------------------- SDLCamSource ----------------------------- - -SDLCamSource::SDLCamSource(int desired_width, int desired_height, - int desired_fps, SDL_PixelFormat pixel_format, - VideoCallback cb) - : width_(desired_width), height_(desired_height), fps_(desired_fps), - format_(pixel_format), callback_(std::move(cb)) {} - -SDLCamSource::~SDLCamSource() { - if (camera_) { - SDL_CloseCamera(camera_); // - camera_ = nullptr; - } -} - -bool SDLCamSource::init() { - int count = 0; - SDL_CameraID *cams = SDL_GetCameras(&count); // - if (!cams || count == 0) { - LK_LOG_ERROR("No cameras available: {}", SDL_GetError()); - if (cams) - SDL_free(cams); - return false; - } - - SDL_CameraID camId = cams[0]; // first camera for now - SDL_free(cams); - - SDL_zero(spec_); - spec_.format = format_; - spec_.colorspace = SDL_COLORSPACE_SRGB; - spec_.width = width_; - spec_.height = height_; - spec_.framerate_numerator = fps_; - spec_.framerate_denominator = 1; - - camera_ = SDL_OpenCamera(camId, &spec_); - if (!camera_) { - LK_LOG_ERROR("Failed to open camera: {}", SDL_GetError()); - return false; - } - - // On many platforms you must wait for SDL_EVENT_CAMERA_DEVICE_APPROVED; - // here we assume the app’s main loop is already handling that. - return true; -} - -void SDLCamSource::pump() { - if (!camera_ || !callback_) - return; - - Uint64 tsNS = 0; - SDL_Surface *surf = SDL_AcquireCameraFrame(camera_, &tsNS); // non-blocking - if (!surf) { - return; - } - - callback_(static_cast(surf->pixels), surf->pitch, surf->w, surf->h, - surf->format, tsNS); - - SDL_ReleaseCameraFrame(camera_, surf); // -} diff --git a/examples/common/sdl_media.h b/examples/common/sdl_media.h deleted file mode 100644 index a60bca64..00000000 --- a/examples/common/sdl_media.h +++ /dev/null @@ -1,128 +0,0 @@ -/* - * 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 -#include -#include - -// ------------------------- -// SDLMicSource -// ------------------------- -// Periodically call pump() from your main loop or a capture thread. -// It will pull 10ms frames from the mic (by default) and pass them to the -// AudioCallback. -class SDLMicSource { -public: - using AudioCallback = std::function; - - SDLMicSource(int sample_rate = 48000, int channels = 1, - int frame_samples = 480, AudioCallback cb = nullptr); - - ~SDLMicSource(); - - // Initialize SDL audio stream for recording - bool init(); - - // Call regularly to pull mic data and send to callback. - void pump(); - - void pause(); - void resume(); - - bool isValid() const { return stream_ != nullptr; } - -private: - SDL_AudioStream *stream_ = nullptr; - SDL_AudioSpec spec_{}; - int sample_rate_; - int channels_; - int frame_samples_; - AudioCallback callback_; -}; - -// ------------------------- -// DDLSpeakerSink -// ------------------------- -// For remote audio: when you get a decoded PCM frame, -// call enqueue() with interleaved S16 samples. -class DDLSpeakerSink { -public: - DDLSpeakerSink(int sample_rate = 48000, int channels = 1); - - ~DDLSpeakerSink(); - - bool init(); - - // Enqueue interleaved S16 samples for playback. - void enqueue(const int16_t *samples, int num_samples_per_channel); - - void pause(); - void resume(); - - bool isValid() const { return stream_ != nullptr; } - -private: - SDL_AudioStream *stream_ = nullptr; - SDL_AudioSpec spec_{}; - int sample_rate_; - int channels_; -}; - -// ------------------------- -// SDLCamSource -// ------------------------- -// Periodically call pump(); each time a new frame is available -// it will invoke the VideoCallback with the raw pixels. -// -// NOTE: pixels are in the SDL_Surface format returned by the camera -// (often SDL_PIXELFORMAT_ARGB8888). You can either: -// - convert to whatever your LiveKit video source expects, or -// - tell LiveKit that this is ARGB with the given stride. -class SDLCamSource { -public: - using VideoCallback = std::function; - - SDLCamSource(int desired_width = 1280, int desired_height = 720, - int desired_fps = 30, - SDL_PixelFormat pixelFormat = SDL_PIXELFORMAT_RGBA8888, - VideoCallback cb = nullptr); - - ~SDLCamSource(); - - bool init(); // open first available camera with (approximately) given spec - - // Call regularly; will call VideoCallback when a frame is available. - void pump(); - - bool isValid() const { return camera_ != nullptr; } - -private: - SDL_Camera *camera_ = nullptr; - SDL_CameraSpec spec_{}; - int width_; - int height_; - int fps_; - SDL_PixelFormat format_; - VideoCallback callback_; -}; diff --git a/examples/common/sdl_media_manager.cpp b/examples/common/sdl_media_manager.cpp deleted file mode 100644 index f44c60ae..00000000 --- a/examples/common/sdl_media_manager.cpp +++ /dev/null @@ -1,402 +0,0 @@ -/* - * 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 "sdl_media_manager.h" - -#include "fallback_capture.h" -#include "livekit/livekit.h" -#include "livekit/lk_log.h" -#include "sdl_media.h" -#include "sdl_video_renderer.h" -#include -#include -using namespace livekit; - -SDLMediaManager::SDLMediaManager() = default; - -SDLMediaManager::~SDLMediaManager() { - stopMic(); - stopCamera(); - stopSpeaker(); - shutdownRenderer(); -} - -bool SDLMediaManager::ensureSDLInit(Uint32 flags) { - if ((SDL_WasInit(flags) & flags) == flags) { - return true; // already init - } - if (!SDL_InitSubSystem(flags)) { - LK_LOG_ERROR("SDL_InitSubSystem failed (flags={}): {}", flags, - SDL_GetError()); - return false; - } - return true; -} - -// ---------- Mic control ---------- - -bool SDLMediaManager::startMic( - const std::shared_ptr &audio_source) { - stopMic(); - - if (!audio_source) { - LK_LOG_ERROR("startMic: audioSource is null"); - return false; - } - - mic_source_ = audio_source; - mic_running_.store(true, std::memory_order_relaxed); - - // Try SDL path - if (!ensureSDLInit(SDL_INIT_AUDIO)) { - LK_LOG_WARN("No SDL audio, falling back to noise loop."); - mic_using_sdl_ = false; - mic_thread_ = - std::thread(runNoiseCaptureLoop, mic_source_, std::ref(mic_running_)); - return true; - } - - int recCount = 0; - SDL_AudioDeviceID *recDevs = SDL_GetAudioRecordingDevices(&recCount); - if (!recDevs || recCount == 0) { - LK_LOG_WARN("No microphone devices found, falling back to noise loop."); - if (recDevs) - SDL_free(recDevs); - mic_using_sdl_ = false; - mic_thread_ = - std::thread(runNoiseCaptureLoop, mic_source_, std::ref(mic_running_)); - return true; - } - SDL_free(recDevs); - - // We have at least one mic; use SDL - mic_using_sdl_ = true; - - mic_sdl_ = std::make_unique( - mic_source_->sample_rate(), mic_source_->num_channels(), - mic_source_->sample_rate() / 100, // ~10ms - [src = mic_source_](const int16_t *samples, int num_samples_per_channel, - int sample_rate, int num_channels) { - AudioFrame frame = AudioFrame::create(sample_rate, num_channels, - num_samples_per_channel); - std::memcpy(frame.data().data(), samples, - num_samples_per_channel * num_channels * sizeof(int16_t)); - try { - src->captureFrame(frame); - } catch (const std::exception &e) { - LK_LOG_ERROR("Error in captureFrame (SDL mic): {}", e.what()); - } - }); - - if (!mic_sdl_->init()) { - LK_LOG_WARN("Failed to init SDL mic, falling back to noise loop."); - mic_using_sdl_ = false; - mic_sdl_.reset(); - mic_thread_ = - std::thread(runNoiseCaptureLoop, mic_source_, std::ref(mic_running_)); - return true; - } - - mic_thread_ = std::thread(&SDLMediaManager::micLoopSDL, this); - return true; -} - -void SDLMediaManager::micLoopSDL() { - while (mic_running_.load(std::memory_order_relaxed)) { - mic_sdl_->pump(); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } -} - -void SDLMediaManager::stopMic() { - mic_running_.store(false, std::memory_order_relaxed); - if (mic_thread_.joinable()) { - mic_thread_.join(); - } - mic_sdl_.reset(); - mic_source_.reset(); -} - -// ---------- Camera control ---------- - -bool SDLMediaManager::startCamera( - const std::shared_ptr &video_source) { - stopCamera(); - - if (!video_source) { - LK_LOG_ERROR("startCamera: videoSource is null"); - return false; - } - - cam_source_ = video_source; - cam_running_.store(true, std::memory_order_relaxed); - - // Try SDL - if (!ensureSDLInit(SDL_INIT_CAMERA)) { - LK_LOG_WARN("No SDL camera subsystem, using fake video loop."); - cam_using_sdl_ = false; - cam_thread_ = std::thread(runFakeVideoCaptureLoop, cam_source_, - std::ref(cam_running_)); - return true; - } - - int camCount = 0; - SDL_CameraID *cams = SDL_GetCameras(&camCount); - if (!cams || camCount == 0) { - LK_LOG_WARN("No camera devices found, using fake video loop."); - if (cams) - SDL_free(cams); - cam_using_sdl_ = false; - cam_thread_ = std::thread(runFakeVideoCaptureLoop, cam_source_, - std::ref(cam_running_)); - return true; - } - SDL_free(cams); - - cam_using_sdl_ = true; - can_sdl_ = std::make_unique( - 1280, 720, 30, - SDL_PIXELFORMAT_RGBA32, // Note SDL_PIXELFORMAT_RGBA8888 is not compatable - // with Livekit RGBA format. - [src = cam_source_](const uint8_t *pixels, int pitch, int width, - int height, SDL_PixelFormat /*fmt*/, - Uint64 timestampNS) { - auto frame = VideoFrame::create(width, height, VideoBufferType::RGBA); - uint8_t *dst = frame.data(); - const int dstPitch = width * 4; - - for (int y = 0; y < height; ++y) { - std::memcpy(dst + y * dstPitch, pixels + y * pitch, dstPitch); - } - - try { - src->captureFrame(frame, timestampNS / 1000, - VideoRotation::VIDEO_ROTATION_0); - } catch (const std::exception &e) { - LK_LOG_ERROR("Error in captureFrame (SDL cam): {}", e.what()); - } - }); - - if (!can_sdl_->init()) { - LK_LOG_WARN("Failed to init SDL camera, using fake video loop."); - cam_using_sdl_ = false; - can_sdl_.reset(); - cam_thread_ = std::thread(runFakeVideoCaptureLoop, cam_source_, - std::ref(cam_running_)); - return true; - } - - cam_thread_ = std::thread(&SDLMediaManager::cameraLoopSDL, this); - return true; -} - -void SDLMediaManager::cameraLoopSDL() { - while (cam_running_.load(std::memory_order_relaxed)) { - can_sdl_->pump(); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } -} - -void SDLMediaManager::stopCamera() { - cam_running_.store(false, std::memory_order_relaxed); - if (cam_thread_.joinable()) { - cam_thread_.join(); - } - can_sdl_.reset(); - cam_source_.reset(); -} - -// ---------- Speaker control (placeholder) ---------- - -bool SDLMediaManager::startSpeaker( - const std::shared_ptr &audio_stream) { - stopSpeaker(); - - if (!audio_stream) { - LK_LOG_ERROR("startSpeaker: audioStream is null"); - return false; - } - - if (!ensureSDLInit(SDL_INIT_AUDIO)) { - LK_LOG_ERROR("startSpeaker: SDL_INIT_AUDIO failed"); - return false; - } - - speaker_stream_ = audio_stream; - speaker_running_.store(true, std::memory_order_relaxed); - - // Note, we don't open the speaker since the format is unknown yet. - // Instead, open the speaker in the speakerLoopSDL thread with the native - // format. - try { - speaker_thread_ = std::thread(&SDLMediaManager::speakerLoopSDL, this); - } catch (const std::exception &e) { - LK_LOG_ERROR("startSpeaker: failed to start speaker thread: {}", e.what()); - speaker_running_.store(false, std::memory_order_relaxed); - speaker_stream_.reset(); - return false; - } - - return true; -} - -void SDLMediaManager::speakerLoopSDL() { - SDL_AudioStream *localStream = nullptr; - SDL_AudioDeviceID dev = 0; - - while (speaker_running_.load(std::memory_order_relaxed)) { - if (!speaker_stream_) { - break; - } - - livekit::AudioFrameEvent ev; - if (!speaker_stream_->read(ev)) { - // EOS or closed - break; - } - - const livekit::AudioFrame &frame = ev.frame; - const auto &data = frame.data(); - if (data.empty()) { - continue; - } - - // Lazily open SDL audio stream based on the first frame's format, so no - // resampler is needed. - if (!localStream) { - SDL_AudioSpec want{}; - want.format = SDL_AUDIO_S16; - want.channels = static_cast(frame.num_channels()); - want.freq = frame.sample_rate(); - - localStream = - SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &want, - /*callback=*/nullptr, - /*userdata=*/nullptr); - - if (!localStream) { - LK_LOG_ERROR("speakerLoopSDL: SDL_OpenAudioDeviceStream failed: {}", - SDL_GetError()); - break; - } - - sdl_audio_stream_ = localStream; // store if you want to inspect later - - dev = SDL_GetAudioStreamDevice(localStream); - if (dev == 0) { - LK_LOG_ERROR("speakerLoopSDL: SDL_GetAudioStreamDevice failed: {}", - SDL_GetError()); - break; - } - - if (!SDL_ResumeAudioDevice(dev)) { - LK_LOG_ERROR("speakerLoopSDL: SDL_ResumeAudioDevice failed: {}", - SDL_GetError()); - break; - } - } - - // Push PCM to SDL. We assume frames are already S16, interleaved, matching - // sample_rate / channels we used above. - const int numBytes = static_cast(data.size() * sizeof(std::int16_t)); - - if (!SDL_PutAudioStreamData(localStream, data.data(), numBytes)) { - LK_LOG_ERROR("speakerLoopSDL: SDL_PutAudioStreamData failed: {}", - SDL_GetError()); - break; - } - - // Tiny sleep to avoid busy loop; SDL buffers internally. - SDL_Delay(2); - } - - if (localStream) { - SDL_DestroyAudioStream(localStream); - localStream = nullptr; - sdl_audio_stream_ = nullptr; - } - - speaker_running_.store(false, std::memory_order_relaxed); -} - -void SDLMediaManager::stopSpeaker() { - speaker_running_.store(false, std::memory_order_relaxed); - if (speaker_thread_.joinable()) { - speaker_thread_.join(); - } - if (sdl_audio_stream_) { - SDL_DestroyAudioStream(sdl_audio_stream_); - sdl_audio_stream_ = nullptr; - } - speaker_stream_.reset(); -} - -// ---------- Renderer control (placeholder) ---------- - -bool SDLMediaManager::initRenderer( - const std::shared_ptr &video_stream) { - if (!video_stream) { - LK_LOG_ERROR("startRenderer: videoStream is null"); - return false; - } - // Ensure SDL video subsystem is initialized - if (!ensureSDLInit(SDL_INIT_VIDEO)) { - LK_LOG_ERROR("startRenderer: SDL_INIT_VIDEO failed"); - return false; - } - renderer_stream_ = video_stream; - renderer_running_.store(true, std::memory_order_relaxed); - - // Lazily create the SDLVideoRenderer - if (!sdl_renderer_) { - sdl_renderer_ = std::make_unique(); - // You can tune these dimensions or even make them options - if (!sdl_renderer_->init("LiveKit Remote Video", 1280, 720)) { - LK_LOG_ERROR("startRenderer: SDLVideoRenderer::init failed"); - sdl_renderer_.reset(); - renderer_stream_.reset(); - renderer_running_.store(false, std::memory_order_relaxed); - return false; - } - } - - // Start the SDL renderer's own render thread - sdl_renderer_->setStream(renderer_stream_); - - return true; -} - -void SDLMediaManager::shutdownRenderer() { - renderer_running_.store(false, std::memory_order_relaxed); - - // Shut down SDL renderer thread if it exists - if (sdl_renderer_) { - sdl_renderer_->shutdown(); - } - - // Old renderer_thread_ is no longer used, but if you still have it: - if (renderer_thread_.joinable()) { - renderer_thread_.join(); - } - - renderer_stream_.reset(); -} - -void SDLMediaManager::render() { - if (renderer_running_.load(std::memory_order_relaxed) && sdl_renderer_) { - sdl_renderer_->render(); - } -} \ No newline at end of file diff --git a/examples/common/sdl_media_manager.h b/examples/common/sdl_media_manager.h deleted file mode 100644 index cd9ba46c..00000000 --- a/examples/common/sdl_media_manager.h +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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 -#include - -#include -#include -#include - -#include "wav_audio_source.h" - -namespace livekit { -class AudioSource; -class VideoSource; -class AudioStream; -class VideoStream; -} // namespace livekit - -// Forward-declared SDL helpers (you can also keep these separate if you like) -class SDLMicSource; -class SDLCamSource; -class SDLVideoRenderer; - -// SDLMediaManager gives you dedicated control over: -// - mic capture -> AudioSource -// - camera capture -> VideoSource -// - speaker playback -> AudioStream (TODO: integrate your API) -// - renderer -> VideoStream (TODO: integrate your API) -class SDLMediaManager { -public: - SDLMediaManager(); - ~SDLMediaManager(); - - // Mic (local capture -> AudioSource) - bool startMic(const std::shared_ptr &audio_source); - void stopMic(); - - // Camera (local capture -> VideoSource) - bool startCamera(const std::shared_ptr &video_source); - void stopCamera(); - - // Speaker (remote audio playback) - bool startSpeaker(const std::shared_ptr &audio_stream); - void stopSpeaker(); - - // Renderer (remote video rendering) - // Following APIs must be called on main thread - bool initRenderer(const std::shared_ptr &video_stream); - void shutdownRenderer(); - void render(); - -private: - // ---- SDL bootstrap helpers ---- - bool ensureSDLInit(Uint32 flags); - - // ---- Mic helpers ---- - void micLoopSDL(); - void micLoopNoise(); - - // ---- Camera helpers ---- - void cameraLoopSDL(); - void cameraLoopFake(); - - // ---- Speaker helpers (TODO: wire AudioStream -> SDL audio) ---- - void speakerLoopSDL(); - - // Mic - std::shared_ptr mic_source_; - std::unique_ptr mic_sdl_; - std::thread mic_thread_; - std::atomic mic_running_{false}; - bool mic_using_sdl_ = false; - - // Camera - std::shared_ptr cam_source_; - std::unique_ptr can_sdl_; - std::thread cam_thread_; - std::atomic cam_running_{false}; - bool cam_using_sdl_ = false; - - // Speaker (remote audio) – left mostly as a placeholder - std::shared_ptr speaker_stream_; - std::thread speaker_thread_; - std::atomic speaker_running_{false}; - SDL_AudioStream *sdl_audio_stream_ = nullptr; - - // Renderer (remote video) – left mostly as a placeholder - std::unique_ptr sdl_renderer_; - std::shared_ptr renderer_stream_; - std::thread renderer_thread_; - std::atomic renderer_running_{false}; -}; diff --git a/examples/common/sdl_video_renderer.cpp b/examples/common/sdl_video_renderer.cpp deleted file mode 100644 index 5ba2cd72..00000000 --- a/examples/common/sdl_video_renderer.cpp +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright 2025 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 "sdl_video_renderer.h" - -#include "livekit/livekit.h" -#include "livekit/lk_log.h" -#include - -using namespace livekit; - -constexpr int kMaxFPS = 60; - -SDLVideoRenderer::SDLVideoRenderer() = default; - -SDLVideoRenderer::~SDLVideoRenderer() { shutdown(); } - -bool SDLVideoRenderer::init(const char *title, int width, int height) { - width_ = width; - height_ = height; - - // Assume SDL_Init(SDL_INIT_VIDEO) already called in main() - window_ = SDL_CreateWindow(title, width_, height_, 0); - if (!window_) { - LK_LOG_ERROR("SDL_CreateWindow failed: {}", SDL_GetError()); - return false; - } - - renderer_ = SDL_CreateRenderer(window_, nullptr); - if (!renderer_) { - LK_LOG_ERROR("SDL_CreateRenderer failed: {}", SDL_GetError()); - return false; - } - - // Note, web will send out BGRA as default, and we can't use ARGB since ffi - // does not support converting from BGRA to ARGB. - texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, - SDL_TEXTUREACCESS_STREAMING, width_, height_); - if (!texture_) { - LK_LOG_ERROR("SDL_CreateTexture failed: {}", SDL_GetError()); - return false; - } - - return true; -} - -void SDLVideoRenderer::shutdown() { - if (texture_) { - SDL_DestroyTexture(texture_); - texture_ = nullptr; - } - if (renderer_) { - SDL_DestroyRenderer(renderer_); - renderer_ = nullptr; - } - if (window_) { - SDL_DestroyWindow(window_); - window_ = nullptr; - } - - stream_.reset(); -} - -void SDLVideoRenderer::setStream(std::shared_ptr stream) { - stream_ = std::move(stream); -} - -void SDLVideoRenderer::render() { - // 0) Basic sanity - if (!window_ || !renderer_) { - return; - } - - // 1) Pump SDL events on the main thread - SDL_Event e; - while (SDL_PollEvent(&e)) { - if (e.type == SDL_EVENT_QUIT) { - // TODO: set some global or member flag if you want to quit the app - } - } - - // 2) If no stream, nothing to render - if (!stream_) { - return; - } - - // Throttle rendering to kMaxFPS - const auto now = std::chrono::steady_clock::now(); - if (last_render_time_.time_since_epoch().count() != 0) { - const auto min_interval = std::chrono::microseconds(1'000'000 / kMaxFPS); - if (now - last_render_time_ < min_interval) { - return; - } - } - last_render_time_ = now; - - // 3) Read a frame from VideoStream (blocking until one is available) - livekit::VideoFrameEvent vfe; - bool gotFrame = stream_->read(vfe); - if (!gotFrame) { - // EOS / closed – nothing more to render - return; - } - - livekit::VideoFrame &frame = vfe.frame; - - // 4) Ensure the frame is RGBA. - // Ideally you requested RGBA from VideoStream::Options so this is a no-op. - if (frame.type() != livekit::VideoBufferType::RGBA) { - try { - frame = frame.convert(livekit::VideoBufferType::RGBA, false); - } catch (const std::exception &ex) { - LK_LOG_ERROR("SDLVideoRenderer: convert to RGBA failed: {}", ex.what()); - return; - } - } - - // Handle size change: recreate texture if needed - if (frame.width() != width_ || frame.height() != height_) { - width_ = frame.width(); - height_ = frame.height(); - - if (texture_) { - SDL_DestroyTexture(texture_); - texture_ = nullptr; - } - texture_ = SDL_CreateTexture( - renderer_, - SDL_PIXELFORMAT_RGBA32, // Note, SDL_PIXELFORMAT_RGBA8888 is not - // compatible with Livekit RGBA format. - SDL_TEXTUREACCESS_STREAMING, width_, height_); - if (!texture_) { - LK_LOG_ERROR("SDLVideoRenderer: SDL_CreateTexture failed: {}", - SDL_GetError()); - return; - } - } - - // 6) Upload RGBA data to SDL texture - void *pixels = nullptr; - int pitch = 0; - if (!SDL_LockTexture(texture_, nullptr, &pixels, &pitch)) { - LK_LOG_ERROR("SDLVideoRenderer: SDL_LockTexture failed: {}", - SDL_GetError()); - return; - } - - const std::uint8_t *src = frame.data(); - const int srcPitch = frame.width() * 4; // RGBA: 4 bytes per pixel - - for (int y = 0; y < frame.height(); ++y) { - std::memcpy(static_cast(pixels) + y * pitch, - src + y * srcPitch, srcPitch); - } - - SDL_UnlockTexture(texture_); - - // 7) Present - SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255); - SDL_RenderClear(renderer_); - SDL_RenderTexture(renderer_, texture_, nullptr, nullptr); - SDL_RenderPresent(renderer_); -} diff --git a/examples/common/sdl_video_renderer.h b/examples/common/sdl_video_renderer.h deleted file mode 100644 index fb0d41ea..00000000 --- a/examples/common/sdl_video_renderer.h +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2025 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. - */ - -#pragma once - -#include -#include -#include -#include - -namespace livekit { -class VideoStream; -} - -class SDLVideoRenderer { -public: - SDLVideoRenderer(); - ~SDLVideoRenderer(); - - // Must be called on main thread, after SDL_Init(SDL_INIT_VIDEO). - bool init(const char *title, int width, int height); - - // Set/replace the stream to render. Safe to call from main thread. - void setStream(std::shared_ptr stream); - - // Called on main thread each tick to pump events and draw latest frame. - void render(); - - void shutdown(); // destroy window/renderer/texture - -private: - SDL_Window *window_ = nullptr; - SDL_Renderer *renderer_ = nullptr; - SDL_Texture *texture_ = nullptr; - - std::shared_ptr stream_; - int width_ = 0; - int height_ = 0; - std::chrono::steady_clock::time_point last_render_time_{}; -}; diff --git a/examples/common/wav_audio_source.cpp b/examples/common/wav_audio_source.cpp deleted file mode 100644 index b519b818..00000000 --- a/examples/common/wav_audio_source.cpp +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright 2025 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 "wav_audio_source.h" - -#include -#include -#include - -// -------------------------------------------------- -// Minimal WAV loader (16-bit PCM only) -// -------------------------------------------------- -WavData load_wav16(const std::string &path) { - std::ifstream file(path, std::ios::binary); - if (!file) { - throw std::runtime_error("Failed to open WAV file: " + path + - " (If this file exists in the repo, ensure Git " - "LFS is installed and run `git lfs pull`)"); - } - - auto read_u32 = [&](uint32_t &out_value) { - file.read(reinterpret_cast(&out_value), 4); - }; - auto read_u16 = [&](uint16_t &out_value) { - file.read(reinterpret_cast(&out_value), 2); - }; - - char riff[4]; - file.read(riff, 4); - if (std::strncmp(riff, "RIFF", 4) != 0) { - throw std::runtime_error("Not a RIFF file"); - } - - uint32_t chunk_size = 0; - read_u32(chunk_size); - - char wave[4]; - file.read(wave, 4); - if (std::strncmp(wave, "WAVE", 4) != 0) { - throw std::runtime_error("Not a WAVE file"); - } - - uint16_t audio_format = 0; - uint16_t num_channels = 0; - uint32_t sample_rate = 0; - uint16_t bits_per_sample = 0; - - bool have_fmt = false; - bool have_data = false; - std::vector samples; - - while (!have_data && file) { - char sub_id[4]; - file.read(sub_id, 4); - - uint32_t sub_size = 0; - read_u32(sub_size); - - if (std::strncmp(sub_id, "fmt ", 4) == 0) { - have_fmt = true; - - read_u16(audio_format); - read_u16(num_channels); - read_u32(sample_rate); - - uint32_t byte_rate = 0; - uint16_t block_align = 0; - read_u32(byte_rate); - read_u16(block_align); - read_u16(bits_per_sample); - - if (sub_size > 16) { - file.seekg(sub_size - 16, std::ios::cur); - } - - if (audio_format != 1) { - throw std::runtime_error("Only PCM WAV supported"); - } - if (bits_per_sample != 16) { - throw std::runtime_error("Only 16-bit WAV supported"); - } - - } else if (std::strncmp(sub_id, "data", 4) == 0) { - if (!have_fmt) { - throw std::runtime_error("data chunk appeared before fmt chunk"); - } - - have_data = true; - const std::size_t count = sub_size / sizeof(int16_t); - samples.resize(count); - file.read(reinterpret_cast(samples.data()), sub_size); - - } else { - // Unknown chunk: skip it - file.seekg(sub_size, std::ios::cur); - } - } - - if (!have_data) { - throw std::runtime_error("No data chunk in WAV file"); - } - - WavData out; - out.sample_rate = static_cast(sample_rate); - out.num_channels = static_cast(num_channels); - out.samples = std::move(samples); - return out; -} - -WavAudioSource::WavAudioSource(const std::string &path, - int expected_sample_rate, int expected_channels, - bool loop_enabled) - : loop_enabled_(loop_enabled) { - wav_ = load_wav16(path); - - if (wav_.sample_rate != expected_sample_rate) { - throw std::runtime_error("WAV sample rate mismatch"); - } - if (wav_.num_channels != expected_channels) { - throw std::runtime_error("WAV channel count mismatch"); - } - - sample_rate_ = wav_.sample_rate; - num_channels_ = wav_.num_channels; - - playhead_ = 0; -} - -void WavAudioSource::fillFrame(AudioFrame &frame) { - const std::size_t frame_samples = - static_cast(frame.num_channels()) * - static_cast(frame.samples_per_channel()); - - int16_t *dst = frame.data().data(); - const std::size_t total_wav_samples = wav_.samples.size(); - - for (std::size_t i = 0; i < frame_samples; ++i) { - if (playhead_ < total_wav_samples) { - dst[i] = wav_.samples[playhead_]; - ++playhead_; - } else if (loop_enabled_ && total_wav_samples > 0) { - playhead_ = 0; - dst[i] = wav_.samples[playhead_]; - ++playhead_; - } else { - dst[i] = 0; - } - } -} diff --git a/examples/common/wav_audio_source.h b/examples/common/wav_audio_source.h deleted file mode 100644 index 51a101c3..00000000 --- a/examples/common/wav_audio_source.h +++ /dev/null @@ -1,56 +0,0 @@ - -/* - * Copyright 2025 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. - */ - -#pragma once - -#include "livekit/livekit.h" -#include -#include -#include -#include - -// Simple WAV container for 16-bit PCM files -struct WavData { - int sample_rate = 0; - int num_channels = 0; - std::vector samples; -}; - -// Helper that loads 16-bit PCM WAV (16-bit, PCM only) -WavData loadWav16(const std::string &path); - -using namespace livekit; - -class WavAudioSource { -public: - // loop_enabled: whether to loop when reaching the end - WavAudioSource(const std::string &path, int expected_sample_rate, - int expected_channels, bool loop_enabled = true); - - // Fill a frame with the next chunk of audio. - void fillFrame(AudioFrame &frame); - -private: - void initLoopDelayCounter(); - - WavData wav_; - std::size_t playhead_ = 0; - - const bool loop_enabled_; - int sample_rate_; - int num_channels_; -}; diff --git a/examples/hello_livekit/receiver.cpp b/examples/hello_livekit/receiver.cpp deleted file mode 100644 index bc05e5f2..00000000 --- a/examples/hello_livekit/receiver.cpp +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2026 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. - */ - -/// Subscribes to the sender's camera video and data track. Run -/// HelloLivekitSender first; use the identity it prints, or the sender's known -/// participant name. -/// -/// Usage: -/// HelloLivekitReceiver -/// -/// Or via environment variables: -/// LIVEKIT_URL, LIVEKIT_RECEIVER_TOKEN, LIVEKIT_SENDER_IDENTITY - -#include "livekit/livekit.h" - -#include -#include -#include -#include -#include - -using namespace livekit; - -constexpr const char *kDataTrackName = "app-data"; -constexpr const char *kVideoTrackName = "camera0"; - -std::atomic g_running{true}; - -void handleSignal(int) { g_running.store(false); } - -std::string getenvOrEmpty(const char *name) { - const char *v = std::getenv(name); - return v ? std::string(v) : std::string{}; -} - -int main(int argc, char *argv[]) { - std::string url = getenvOrEmpty("LIVEKIT_URL"); - std::string receiver_token = getenvOrEmpty("LIVEKIT_RECEIVER_TOKEN"); - std::string sender_identity = getenvOrEmpty("LIVEKIT_SENDER_IDENTITY"); - - if (argc >= 4) { - url = argv[1]; - receiver_token = argv[2]; - sender_identity = argv[3]; - } - - if (url.empty() || receiver_token.empty() || sender_identity.empty()) { - LK_LOG_ERROR("Usage: HelloLivekitReceiver " - "\n" - " or set LIVEKIT_URL, LIVEKIT_RECEIVER_TOKEN, " - "LIVEKIT_SENDER_IDENTITY"); - return 1; - } - - std::signal(SIGINT, handleSignal); -#ifdef SIGTERM - std::signal(SIGTERM, handleSignal); -#endif - - livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); - - auto room = std::make_unique(); - RoomOptions options; - options.auto_subscribe = true; - options.dynacast = false; - - if (!room->Connect(url, receiver_token, options)) { - LK_LOG_ERROR("[receiver] Failed to connect"); - livekit::shutdown(); - return 1; - } - - LocalParticipant *lp = room->localParticipant(); - assert(lp); - - LK_LOG_INFO("[receiver] Connected as identity='{}' room='{}'; subscribing " - "to sender identity='{}'", - lp->identity(), room->room_info().name, sender_identity); - - int video_frame_count = 0; - room->setOnVideoFrameCallback( - sender_identity, kVideoTrackName, - [&video_frame_count](const VideoFrame &frame, std::int64_t timestamp_us) { - const auto ts_ms = - std::chrono::duration(timestamp_us).count(); - const int n = video_frame_count++; - if (n % 10 == 0) { - LK_LOG_INFO("[receiver] Video frame #{} {}x{} ts_ms={}", n, - frame.width(), frame.height(), ts_ms); - } - }); - - int data_frame_count = 0; - room->addOnDataFrameCallback( - sender_identity, kDataTrackName, - [&data_frame_count](const std::vector &payload, - std::optional user_ts) { - const int n = data_frame_count++; - if (n % 10 == 0) { - LK_LOG_INFO("[receiver] Data frame #{}", n); - } - }); - - LK_LOG_INFO("[receiver] Listening for video track '{}' + data track '{}'; " - "Ctrl-C to exit", - kVideoTrackName, kDataTrackName); - - while (g_running.load()) { - std::this_thread::sleep_for(std::chrono::milliseconds(50)); - } - - LK_LOG_INFO("[receiver] Shutting down"); - room.reset(); - - livekit::shutdown(); - return 0; -} diff --git a/examples/hello_livekit/sender.cpp b/examples/hello_livekit/sender.cpp deleted file mode 100644 index 253091fd..00000000 --- a/examples/hello_livekit/sender.cpp +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright 2026 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. - */ - -/// Publishes synthetic RGBA video and a data track. Run the receiver in another -/// process and pass this participant's identity (printed after connect). -/// -/// Usage: -/// HelloLivekitSender -/// -/// Or via environment variables: -/// LIVEKIT_URL, LIVEKIT_SENDER_TOKEN - -#include "livekit/livekit.h" - -#include -#include -#include -#include -#include -#include -#include - -using namespace livekit; - -constexpr int kWidth = 640; -constexpr int kHeight = 480; -constexpr const char *kVideoTrackName = "camera0"; -constexpr const char *kDataTrackName = "app-data"; - -std::atomic g_running{true}; - -void handleSignal(int) { g_running.store(false); } - -std::string getenvOrEmpty(const char *name) { - const char *v = std::getenv(name); - return v ? std::string(v) : std::string{}; -} - -int main(int argc, char *argv[]) { - std::string url = getenvOrEmpty("LIVEKIT_URL"); - std::string sender_token = getenvOrEmpty("LIVEKIT_SENDER_TOKEN"); - - if (argc >= 3) { - url = argv[1]; - sender_token = argv[2]; - } - - if (url.empty() || sender_token.empty()) { - LK_LOG_ERROR("Usage: HelloLivekitSender \n" - " or set LIVEKIT_URL, LIVEKIT_SENDER_TOKEN"); - return 1; - } - - std::signal(SIGINT, handleSignal); -#ifdef SIGTERM - std::signal(SIGTERM, handleSignal); -#endif - - livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); - - auto room = std::make_unique(); - RoomOptions options; - options.auto_subscribe = true; - options.dynacast = false; - - if (!room->Connect(url, sender_token, options)) { - LK_LOG_ERROR("[sender] Failed to connect"); - livekit::shutdown(); - return 1; - } - - LocalParticipant *lp = room->localParticipant(); - assert(lp); - - LK_LOG_INFO("[sender] Connected as identity='{}' room='{}' β€” pass this " - "identity to HelloLivekitReceiver", - lp->identity(), room->room_info().name); - - auto video_source = std::make_shared(kWidth, kHeight); - - std::shared_ptr video_track = lp->publishVideoTrack( - kVideoTrackName, video_source, TrackSource::SOURCE_CAMERA); - - auto publish_result = lp->publishDataTrack(kDataTrackName); - if (!publish_result) { - const auto &error = publish_result.error(); - LK_LOG_ERROR("Failed to publish data track: code={} message={}", - static_cast(error.code), error.message); - room.reset(); - livekit::shutdown(); - return 1; - } - std::shared_ptr data_track = publish_result.value(); - - const auto t0 = std::chrono::steady_clock::now(); - std::uint64_t count = 0; - - LK_LOG_INFO( - "[sender] Publishing synthetic video + data on '{}'; Ctrl-C to exit", - kDataTrackName); - - while (g_running.load()) { - VideoFrame vf = VideoFrame::create(kWidth, kHeight, VideoBufferType::RGBA); - video_source->captureFrame(std::move(vf)); - - const auto now = std::chrono::steady_clock::now(); - const double ms = - std::chrono::duration(now - t0).count(); - std::ostringstream oss; - oss << std::fixed << std::setprecision(2) << ms << " ms, count: " << count; - const std::string msg = oss.str(); - auto push_result = - data_track->tryPush(std::vector(msg.begin(), msg.end())); - if (!push_result) { - const auto &error = push_result.error(); - LK_LOG_WARN("Failed to push data frame: code={} message={}", - static_cast(error.code), error.message); - } - - ++count; - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - - LK_LOG_INFO("[sender] Disconnecting"); - room.reset(); - - livekit::shutdown(); - return 0; -} diff --git a/examples/logging_levels/README.md b/examples/logging_levels/README.md deleted file mode 100644 index 546318b5..00000000 --- a/examples/logging_levels/README.md +++ /dev/null @@ -1,160 +0,0 @@ -# Logging Examples - -Demonstrates the LiveKit C++ SDK's two-tier logging system. No LiveKit server -is required -- the examples simply emit log messages at every severity level so -you can see how filtering works. - -There are two executables: - -| Target | Source | What it shows | -|------------------------------|---------------------|---------------| -| `LoggingLevelsBasicUsage` | `basic_usage.cpp` | Runtime level cycling and a basic custom callback | -| `LoggingLevelsCustomSinks` | `custom_sinks.cpp` | Three practical custom sink patterns: file, JSON, and ROS2 bridge | - -## Usage -- LoggingLevelsBasicUsage - -```bash -# Full demo: cycles through every runtime level, then shows the callback API -./build/examples/LoggingLevelsBasicUsage - -# Set a single runtime level and emit all messages -./build/examples/LoggingLevelsBasicUsage warn # only WARN, ERROR, CRITICAL printed -./build/examples/LoggingLevelsBasicUsage trace # everything printed -./build/examples/LoggingLevelsBasicUsage off # nothing printed -``` - -## Usage -- LoggingLevelsCustomSinks - -```bash -# Run all three sink demos in sequence -./build/examples/LoggingLevelsCustomSinks - -# Run a single sink demo -./build/examples/LoggingLevelsCustomSinks file # writes SDK logs to livekit.log -./build/examples/LoggingLevelsCustomSinks json # emits JSON-lines to stdout -./build/examples/LoggingLevelsCustomSinks ros2 # mimics RCLCPP_* output format -``` - -## How log-level filtering works - -The SDK filters log messages in two stages: - -### 1. Compile-time (`LIVEKIT_LOG_LEVEL`) - -Set at CMake configure time. Calls **below** this level are stripped from the -binary entirely -- the format string is never evaluated and no function is -called. This is zero-cost. - -```bash -# Default: nothing stripped (all levels available at runtime) -cmake -DLIVEKIT_LOG_LEVEL=TRACE .. - -# Strip TRACE, DEBUG, and INFO at compile time -cmake -DLIVEKIT_LOG_LEVEL=WARN .. - -# Only ERROR and CRITICAL survive -cmake -DLIVEKIT_LOG_LEVEL=ERROR .. -``` - -Valid values: `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, `CRITICAL`, `OFF`. - -Under the hood this sets `SPDLOG_ACTIVE_LEVEL`, which the `LK_LOG_*` macros -check with a preprocessor guard before emitting any code. - -### 2. Runtime (`setLogLevel`) - -Among the levels that survived compilation, `setLogLevel()` controls which -ones actually produce output. You can change it at any time after -`livekit::initialize()`: - -```cpp -livekit::initialize(); // default level: Info -livekit::setLogLevel(livekit::LogLevel::Debug); // show more detail -livekit::setLogLevel(livekit::LogLevel::Error); // only errors and above -``` - -### Interaction between the two tiers - -| Compile-time level | Runtime level | TRACE | DEBUG | INFO | WARN | ERROR | -|--------------------|---------------|:-----:|:-----:|:----:|:----:|:-----:| -| TRACE | Info | | | x | x | x | -| TRACE | Trace | x | x | x | x | x | -| WARN | Trace | | | | x | x | -| WARN | Error | | | | | x | - -Cells marked **x** produce output. Empty cells are filtered out -- either -stripped at compile time (left columns when compile-time > level) or suppressed -at runtime. - -## Custom log callbacks (`setLogCallback`) - -`setLogCallback()` lets you redirect **all** SDK log output to your own handler -instead of the default stderr sink. This is the integration point for frameworks -like ROS2, Android logcat, or any structured-logging pipeline. - -The basic API: - -```cpp -livekit::setLogCallback( - [](livekit::LogLevel level, - const std::string &logger_name, - const std::string &message) { - // Your code here -- e.g. write to file, emit JSON, call RCLCPP_INFO, ... - }); - -// Pass nullptr to restore the default stderr sink: -livekit::setLogCallback(nullptr); -``` - -`LoggingLevelsCustomSinks` (`custom_sinks.cpp`) provides three ready-to-copy patterns: - -### File sink - -Writes every SDK log line to a file with an ISO-8601 timestamp: - -```cpp -auto file = std::make_shared("livekit.log", std::ios::trunc); -livekit::setLogCallback( - [file](livekit::LogLevel level, const std::string &logger_name, - const std::string &message) { - *file << timestamp() << " [" << levelTag(level) << "] [" - << logger_name << "] " << message << "\n"; - }); -``` - -### JSON sink - -Emits one JSON object per line -- ready for piping into `jq` or a log -aggregation service: - -``` -{"ts":"2025-07-01T12:00:00.123Z","level":"INFO","logger":"livekit","msg":"track published"} -``` - -### ROS2 bridge sink - -Maps `livekit::LogLevel` to `RCLCPP_DEBUG` / `RCLCPP_INFO` / `RCLCPP_WARN` / -`RCLCPP_ERROR` so LiveKit logs appear in the standard ROS2 console output, -properly severity-tagged and namespaced under your node: - -```cpp -livekit::setLogCallback( - [node](livekit::LogLevel level, const std::string &logger_name, - const std::string &message) { - switch (level) { - case livekit::LogLevel::Trace: - case livekit::LogLevel::Debug: - RCLCPP_DEBUG(node->get_logger(), "[%s] %s", - logger_name.c_str(), message.c_str()); - break; - case livekit::LogLevel::Info: - RCLCPP_INFO(node->get_logger(), "[%s] %s", - logger_name.c_str(), message.c_str()); - break; - // ... Warn, Error, Critical ... - } - }); -``` - -The example compiles without rclcpp by stubbing the output to match ROS2 -formatting. diff --git a/examples/logging_levels/basic_usage.cpp b/examples/logging_levels/basic_usage.cpp deleted file mode 100644 index 8c0b0314..00000000 --- a/examples/logging_levels/basic_usage.cpp +++ /dev/null @@ -1,163 +0,0 @@ -/* - * 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. - */ - -/// @file main.cpp -/// @brief Demonstrates LiveKit SDK log-level control and custom log callbacks. -/// -/// Logging has two tiers of filtering: -/// -/// 1. **Compile-time** (LIVEKIT_LOG_LEVEL, set via CMake): -/// Calls below this level are stripped from the binary entirely. -/// Default is TRACE (nothing stripped). For a lean release build: -/// cmake -DLIVEKIT_LOG_LEVEL=WARN ... -/// -/// 2. **Runtime** (setLogLevel()): -/// Among the levels that survived compilation, setLogLevel() controls -/// which ones actually produce output. This is what this example demos. -/// -/// Usage: -/// LoggingLevels [trace|debug|info|warn|error|critical|off] -/// -/// If no argument is given, the example cycles through every level so you can -/// see which messages are filtered at each setting. - -#include "livekit/livekit.h" -#include "livekit/lk_log.h" - -#include -#include -#include -#include - -namespace { - -const char *levelName(livekit::LogLevel level) { - switch (level) { - case livekit::LogLevel::Trace: - return "TRACE"; - case livekit::LogLevel::Debug: - return "DEBUG"; - case livekit::LogLevel::Info: - return "INFO"; - case livekit::LogLevel::Warn: - return "WARN"; - case livekit::LogLevel::Error: - return "ERROR"; - case livekit::LogLevel::Critical: - return "CRITICAL"; - case livekit::LogLevel::Off: - return "OFF"; - } - return "UNKNOWN"; -} - -livekit::LogLevel parseLevel(const char *arg) { - if (std::strcmp(arg, "trace") == 0) - return livekit::LogLevel::Trace; - if (std::strcmp(arg, "debug") == 0) - return livekit::LogLevel::Debug; - if (std::strcmp(arg, "info") == 0) - return livekit::LogLevel::Info; - if (std::strcmp(arg, "warn") == 0) - return livekit::LogLevel::Warn; - if (std::strcmp(arg, "error") == 0) - return livekit::LogLevel::Error; - if (std::strcmp(arg, "critical") == 0) - return livekit::LogLevel::Critical; - if (std::strcmp(arg, "off") == 0) - return livekit::LogLevel::Off; - std::cerr << "Unknown level '" << arg << "', defaulting to Info.\n" - << "Valid: trace, debug, info, warn, error, critical, off\n"; - return livekit::LogLevel::Info; -} - -/// Emit one message at every severity level using the LK_LOG_* macros. -void emitAllLevels() { - LK_LOG_TRACE("This is a TRACE message (very verbose internals)"); - LK_LOG_DEBUG("This is a DEBUG message (diagnostic detail)"); - LK_LOG_INFO("This is an INFO message (normal operation)"); - LK_LOG_WARN("This is a WARN message (something unexpected)"); - LK_LOG_ERROR("This is an ERROR message (something failed)"); - LK_LOG_CRITICAL("This is a CRITICAL message (unrecoverable)"); -} - -/// Demonstrate cycling through every log level. -void runLevelCycleDemo() { - const livekit::LogLevel levels[] = { - livekit::LogLevel::Trace, livekit::LogLevel::Debug, - livekit::LogLevel::Info, livekit::LogLevel::Warn, - livekit::LogLevel::Error, livekit::LogLevel::Critical, - livekit::LogLevel::Off, - }; - - for (auto level : levels) { - std::cout << "\n========================================\n" - << " Setting log level to: " << levelName(level) << "\n" - << "========================================\n"; - livekit::setLogLevel(level); - emitAllLevels(); - } -} - -/// Demonstrate a custom log callback (e.g. for ROS2 integration). -void runCallbackDemo() { - std::cout << "\n========================================\n" - << " Custom LogCallback demo\n" - << "========================================\n"; - - livekit::setLogLevel(livekit::LogLevel::Trace); - - // Install a user-defined callback that captures all log output. - // In a real ROS2 node you would replace this with RCLCPP_* macros. - livekit::setLogCallback([](livekit::LogLevel level, - const std::string &logger_name, - const std::string &message) { - std::cout << "[CALLBACK] [" << levelName(level) << "] [" << logger_name - << "] " << message << "\n"; - }); - - LK_LOG_INFO("This message is routed through the custom callback"); - LK_LOG_WARN("Warnings also go through the callback"); - LK_LOG_ERROR("Errors too -- the callback sees everything >= the level"); - - // Restore default stderr sink by passing an empty callback. - livekit::setLogCallback(nullptr); - - std::cout << "\n(Restored default stderr sink)\n"; - LK_LOG_INFO("This message goes to stderr again (default sink)"); -} - -} // namespace - -int main(int argc, char *argv[]) { - // Initialize the LiveKit SDK (creates the spdlog logger). - livekit::initialize(); - - if (argc > 1) { - // Single-level mode: set the requested level and emit all messages. - livekit::LogLevel level = parseLevel(argv[1]); - std::cout << "Setting log level to: " << levelName(level) << "\n\n"; - livekit::setLogLevel(level); - emitAllLevels(); - } else { - // Full demo: cycle through levels, then show the callback API. - runLevelCycleDemo(); - runCallbackDemo(); - } - - livekit::shutdown(); - return 0; -} diff --git a/examples/logging_levels/custom_sinks.cpp b/examples/logging_levels/custom_sinks.cpp deleted file mode 100644 index 40ddcb4b..00000000 --- a/examples/logging_levels/custom_sinks.cpp +++ /dev/null @@ -1,292 +0,0 @@ -/* - * 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. - */ - -/// @file custom_sinks.cpp -/// @brief Shows how SDK consumers supply their own log backend via -/// livekit::setLogCallback(). -/// -/// This example uses ONLY the public SDK API (). -/// No internal headers or spdlog dependency required. -/// -/// Three patterns are demonstrated: -/// -/// 1. **File logger** -- write SDK logs to a file on disk. -/// 2. **JSON logger** -- emit structured JSON lines (for log aggregation). -/// 3. **ROS2 bridge** -- skeleton showing how to route SDK logs into -/// RCLCPP_* macros (the rclcpp headers are stubbed -/// so this compiles without a ROS2 install). -/// -/// Usage: -/// CustomSinks [file|json|ros2] -/// -/// If no argument is given, all three sinks are demonstrated in sequence. - -#include "livekit/livekit.h" - -#include -#include -#include -#include -#include -#include - -namespace { - -// --------------------------------------------------------------- -// Helpers -// --------------------------------------------------------------- - -const char *levelTag(livekit::LogLevel level) { - switch (level) { - case livekit::LogLevel::Trace: - return "TRACE"; - case livekit::LogLevel::Debug: - return "DEBUG"; - case livekit::LogLevel::Info: - return "INFO"; - case livekit::LogLevel::Warn: - return "WARN"; - case livekit::LogLevel::Error: - return "ERROR"; - case livekit::LogLevel::Critical: - return "CRITICAL"; - case livekit::LogLevel::Off: - return "OFF"; - } - return "?"; -} - -std::string nowISO8601() { - auto now = std::chrono::system_clock::now(); - auto tt = std::chrono::system_clock::to_time_t(now); - auto ms = std::chrono::duration_cast( - now.time_since_epoch()) % - 1000; - std::ostringstream ss; - ss << std::put_time(std::gmtime(&tt), "%FT%T") << '.' << std::setfill('0') - << std::setw(3) << ms.count() << 'Z'; - return ss.str(); -} - -struct SampleLog { - livekit::LogLevel level; - const char *message; -}; - -// Representative messages that the SDK would emit during normal operation. -// We drive the installed callback directly so this example has zero internal -// dependencies -- only the public API. -const SampleLog kSampleLogs[] = { - {livekit::LogLevel::Trace, "per-frame data: pts=12345 bytes=921600"}, - {livekit::LogLevel::Debug, "negotiating codec: VP8 -> H264 fallback"}, - {livekit::LogLevel::Info, "track published: sid=TR_abc123 kind=video"}, - {livekit::LogLevel::Warn, "ICE candidate pair changed unexpectedly"}, - {livekit::LogLevel::Error, "DTLS handshake failed: timeout after 10s"}, - {livekit::LogLevel::Critical, "out of memory allocating decode buffer"}, -}; - -void driveCallback(const livekit::LogCallback &cb) { - for (const auto &entry : kSampleLogs) { - cb(entry.level, "livekit", entry.message); - } -} - -// --------------------------------------------------------------- -// 1. File logger -// --------------------------------------------------------------- - -void runFileSinkDemo() { - const char *path = "livekit.log"; - std::cout << "\n=== File sink: writing SDK logs to '" << path << "' ===\n"; - - auto file = std::make_shared(path, std::ios::trunc); - if (!file->is_open()) { - std::cerr << "Could not open " << path << " for writing\n"; - return; - } - - // The shared_ptr keeps the stream alive inside the lambda even if - // the local variable goes out of scope before the callback fires. - livekit::LogCallback fileSink = [file](livekit::LogLevel level, - const std::string &logger_name, - const std::string &message) { - *file << nowISO8601() << " [" << levelTag(level) << "] [" << logger_name - << "] " << message << "\n"; - file->flush(); - }; - - // In a real app you would call: - // livekit::setLogCallback(fileSink); - // and then SDK operations (room.connect, publishTrack, ...) would route - // their internal log output through your callback automatically. - // - // Here we drive the callback directly with sample data so the example - // is self-contained and doesn't require a LiveKit server. - livekit::setLogCallback(fileSink); - driveCallback(fileSink); - livekit::setLogCallback(nullptr); - - std::cout << "Wrote " << path << " -- contents:\n\n"; - std::ifstream in(path); - std::cout << in.rdbuf() << "\n"; -} - -// --------------------------------------------------------------- -// 2. JSON structured logger -// --------------------------------------------------------------- - -std::string escapeJson(const std::string &s) { - std::string out; - out.reserve(s.size() + 8); - for (char c : s) { - switch (c) { - case '"': - out += "\\\""; - break; - case '\\': - out += "\\\\"; - break; - case '\n': - out += "\\n"; - break; - case '\r': - out += "\\r"; - break; - case '\t': - out += "\\t"; - break; - default: - out += c; - } - } - return out; -} - -void runJsonSinkDemo() { - std::cout << "\n=== JSON sink: structured log lines to stdout ===\n\n"; - - livekit::LogCallback jsonSink = [](livekit::LogLevel level, - const std::string &logger_name, - const std::string &message) { - std::cout << R"({"ts":")" << nowISO8601() << R"(","level":")" - << levelTag(level) << R"(","logger":")" << escapeJson(logger_name) - << R"(","msg":")" << escapeJson(message) << "\"}\n"; - }; - - livekit::setLogCallback(jsonSink); - driveCallback(jsonSink); - livekit::setLogCallback(nullptr); -} - -// --------------------------------------------------------------- -// 3. ROS2 bridge (stubbed -- compiles without rclcpp) -// --------------------------------------------------------------- -// -// In a real ROS2 node the lambda body would be: -// -// switch (level) { -// case livekit::LogLevel::Trace: -// case livekit::LogLevel::Debug: -// RCLCPP_DEBUG(node_->get_logger(), "[%s] %s", -// logger_name.c_str(), message.c_str()); -// break; -// case livekit::LogLevel::Info: -// RCLCPP_INFO(node_->get_logger(), "[%s] %s", -// logger_name.c_str(), message.c_str()); -// break; -// case livekit::LogLevel::Warn: -// RCLCPP_WARN(node_->get_logger(), "[%s] %s", -// logger_name.c_str(), message.c_str()); -// break; -// case livekit::LogLevel::Error: -// case livekit::LogLevel::Critical: -// RCLCPP_ERROR(node_->get_logger(), "[%s] %s", -// logger_name.c_str(), message.c_str()); -// break; -// default: -// break; -// } -// -// Here we stub it with console output that mimics ROS2 formatting. - -void runRos2SinkDemo() { - std::cout << "\n=== ROS2 bridge sink (stubbed) ===\n\n"; - - const std::string node_name = "livekit_bridge_node"; - - livekit::LogCallback ros2Sink = [&node_name](livekit::LogLevel level, - const std::string &logger_name, - const std::string &message) { - const char *ros_level; - switch (level) { - case livekit::LogLevel::Trace: - case livekit::LogLevel::Debug: - ros_level = "DEBUG"; - break; - case livekit::LogLevel::Info: - ros_level = "INFO"; - break; - case livekit::LogLevel::Warn: - ros_level = "WARN"; - break; - case livekit::LogLevel::Error: - case livekit::LogLevel::Critical: - ros_level = "ERROR"; - break; - default: - ros_level = "INFO"; - break; - } - - // Mimic: [INFO] [1719500000.123] [livekit_bridge_node]: [livekit] msg - auto epoch_s = std::chrono::duration( - std::chrono::system_clock::now().time_since_epoch()) - .count(); - std::cout << "[" << ros_level << "] [" << std::fixed << std::setprecision(3) - << epoch_s << "] [" << node_name << "]: [" << logger_name << "] " - << message << "\n"; - }; - - livekit::setLogCallback(ros2Sink); - driveCallback(ros2Sink); - livekit::setLogCallback(nullptr); -} - -} // namespace - -int main(int argc, char *argv[]) { - livekit::initialize(); - - if (argc > 1) { - if (std::strcmp(argv[1], "file") == 0) { - runFileSinkDemo(); - } else if (std::strcmp(argv[1], "json") == 0) { - runJsonSinkDemo(); - } else if (std::strcmp(argv[1], "ros2") == 0) { - runRos2SinkDemo(); - } else { - std::cerr << "Unknown sink '" << argv[1] << "'.\n" - << "Usage: CustomSinks [file|json|ros2]\n"; - } - } else { - runFileSinkDemo(); - runJsonSinkDemo(); - runRos2SinkDemo(); - } - - livekit::shutdown(); - return 0; -} diff --git a/examples/ping_pong/constants.h b/examples/ping_pong/constants.h deleted file mode 100644 index da3c9b53..00000000 --- a/examples/ping_pong/constants.h +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2026 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. - */ - -#pragma once - -#include - -namespace ping_pong { - -inline constexpr char kPingParticipantIdentity[] = "ping"; -inline constexpr char kPongParticipantIdentity[] = "pong"; - -inline constexpr char kPingTrackName[] = "ping"; -inline constexpr char kPongTrackName[] = "pong"; - -inline constexpr char kPingIdKey[] = "id"; -inline constexpr char kReceivedIdKey[] = "rec_id"; -inline constexpr char kTimestampKey[] = "ts"; - -inline constexpr auto kPingPeriod = std::chrono::seconds(1); -inline constexpr auto kPollPeriod = std::chrono::milliseconds(50); - -} // namespace ping_pong diff --git a/examples/ping_pong/json_converters.cpp b/examples/ping_pong/json_converters.cpp deleted file mode 100644 index 24f89b14..00000000 --- a/examples/ping_pong/json_converters.cpp +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2026 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 "json_converters.h" - -#include "constants.h" - -#include - -#include - -namespace ping_pong { - -std::string pingMessageToJson(const PingMessage &message) { - nlohmann::json json; - json[kPingIdKey] = message.id; - json[kTimestampKey] = message.ts_ns; - return json.dump(); -} - -PingMessage pingMessageFromJson(const std::string &json_text) { - try { - const auto json = nlohmann::json::parse(json_text); - - PingMessage message; - message.id = json.at(kPingIdKey).get(); - message.ts_ns = json.at(kTimestampKey).get(); - return message; - } catch (const nlohmann::json::exception &e) { - throw std::runtime_error(std::string("Failed to parse ping JSON: ") + - e.what()); - } -} - -std::string pongMessageToJson(const PongMessage &message) { - nlohmann::json json; - json[kReceivedIdKey] = message.rec_id; - json[kTimestampKey] = message.ts_ns; - return json.dump(); -} - -PongMessage pongMessageFromJson(const std::string &json_text) { - try { - const auto json = nlohmann::json::parse(json_text); - - PongMessage message; - message.rec_id = json.at(kReceivedIdKey).get(); - message.ts_ns = json.at(kTimestampKey).get(); - return message; - } catch (const nlohmann::json::exception &e) { - throw std::runtime_error(std::string("Failed to parse pong JSON: ") + - e.what()); - } -} - -} // namespace ping_pong diff --git a/examples/ping_pong/json_converters.h b/examples/ping_pong/json_converters.h deleted file mode 100644 index 3491ef6c..00000000 --- a/examples/ping_pong/json_converters.h +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2026 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. - */ - -#pragma once - -#include "messages.h" - -#include - -namespace ping_pong { - -std::string pingMessageToJson(const PingMessage &message); -PingMessage pingMessageFromJson(const std::string &json); - -std::string pongMessageToJson(const PongMessage &message); -PongMessage pongMessageFromJson(const std::string &json); - -} // namespace ping_pong diff --git a/examples/ping_pong/messages.h b/examples/ping_pong/messages.h deleted file mode 100644 index d4212ed6..00000000 --- a/examples/ping_pong/messages.h +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2026 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. - */ - -#pragma once - -#include - -namespace ping_pong { - -struct PingMessage { - std::uint64_t id = 0; - std::int64_t ts_ns = 0; -}; - -struct PongMessage { - std::uint64_t rec_id = 0; - std::int64_t ts_ns = 0; -}; - -struct LatencyMetrics { - std::uint64_t id = 0; - std::int64_t ping_sent_ts_ns = 0; - std::int64_t pong_sent_ts_ns = 0; - std::int64_t ping_received_ts_ns = 0; - std::int64_t round_trip_time_ns = 0; - std::int64_t pong_to_ping_time_ns = 0; - std::int64_t ping_to_pong_and_processing_ns = 0; - double estimated_one_way_latency_ns = 0.0; - double round_trip_time_ms = 0.0; - double pong_to_ping_time_ms = 0.0; - double ping_to_pong_and_processing_ms = 0.0; - double estimated_one_way_latency_ms = 0.0; -}; - -} // namespace ping_pong diff --git a/examples/ping_pong/ping.cpp b/examples/ping_pong/ping.cpp deleted file mode 100644 index c46f941c..00000000 --- a/examples/ping_pong/ping.cpp +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright 2026 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. - */ - -/// Ping participant: publishes on the "ping" data track, listens on "pong", -/// and logs latency metrics for each matched response. Use a token whose -/// identity is `ping`. - -#include "constants.h" -#include "json_converters.h" -#include "livekit/livekit.h" -#include "livekit/lk_log.h" -#include "messages.h" -#include "utils.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace livekit; - -namespace { - -std::atomic g_running{true}; - -void handleSignal(int) { g_running.store(false); } - -ping_pong::LatencyMetrics -calculateLatencyMetrics(const ping_pong::PingMessage &ping_message, - const ping_pong::PongMessage &pong_message, - std::int64_t received_ts_ns) { - ping_pong::LatencyMetrics metrics; - metrics.id = ping_message.id; - metrics.pong_sent_ts_ns = pong_message.ts_ns; - metrics.ping_received_ts_ns = received_ts_ns; - metrics.round_trip_time_ns = received_ts_ns - ping_message.ts_ns; - metrics.pong_to_ping_time_ns = received_ts_ns - pong_message.ts_ns; - metrics.ping_to_pong_and_processing_ns = - pong_message.ts_ns - ping_message.ts_ns; - metrics.estimated_one_way_latency_ns = - static_cast(metrics.round_trip_time_ns) / 2.0; - metrics.round_trip_time_ms = - static_cast(metrics.round_trip_time_ns) / 1'000'000.0; - metrics.pong_to_ping_time_ms = - static_cast(metrics.pong_to_ping_time_ns) / 1'000'000.0; - metrics.ping_to_pong_and_processing_ms = - static_cast(metrics.ping_to_pong_and_processing_ns) / 1'000'000.0; - metrics.estimated_one_way_latency_ms = - metrics.estimated_one_way_latency_ns / 1'000'000.0; - return metrics; -} - -} // namespace - -int main(int argc, char *argv[]) { - std::string url = ping_pong::getenvOrEmpty("LIVEKIT_URL"); - std::string token = ping_pong::getenvOrEmpty("LIVEKIT_TOKEN"); - - if (argc >= 3) { - url = argv[1]; - token = argv[2]; - } - - if (url.empty() || token.empty()) { - LK_LOG_ERROR("LIVEKIT_URL and LIVEKIT_TOKEN (or ) are " - "required"); - return 1; - } - - std::signal(SIGINT, handleSignal); -#ifdef SIGTERM - std::signal(SIGTERM, handleSignal); -#endif - - livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); - - auto room = std::make_unique(); - RoomOptions options; - options.auto_subscribe = true; - options.dynacast = false; - - if (!room->Connect(url, token, options)) { - LK_LOG_ERROR("Failed to connect to room"); - livekit::shutdown(); - return 1; - } - - LocalParticipant *local_participant = room->localParticipant(); - assert(local_participant); - - LK_LOG_INFO("ping connected as identity='{}' room='{}'", - local_participant->identity(), room->room_info().name); - - auto publish_result = - local_participant->publishDataTrack(ping_pong::kPingTrackName); - if (!publish_result) { - const auto &error = publish_result.error(); - LK_LOG_ERROR("Failed to publish ping data track: code={} message={}", - static_cast(error.code), error.message); - room->setDelegate(nullptr); - room.reset(); - livekit::shutdown(); - return 1; - } - - std::shared_ptr ping_track = publish_result.value(); - std::unordered_map sent_messages; - std::mutex sent_messages_mutex; - - const auto callback_id = room->addOnDataFrameCallback( - ping_pong::kPongParticipantIdentity, ping_pong::kPongTrackName, - [&sent_messages, - &sent_messages_mutex](const std::vector &payload, - std::optional /*user_timestamp*/) { - try { - if (payload.empty()) { - LK_LOG_DEBUG("Ignoring empty pong payload"); - return; - } - - const auto pong_message = - ping_pong::pongMessageFromJson(ping_pong::toString(payload)); - const auto received_ts_ns = ping_pong::timeSinceEpochNs(); - - ping_pong::PingMessage ping_message; - { - std::lock_guard lock(sent_messages_mutex); - const auto it = sent_messages.find(pong_message.rec_id); - if (it == sent_messages.end()) { - LK_LOG_WARN("Received pong for unknown id={}", - pong_message.rec_id); - return; - } - ping_message = it->second; - sent_messages.erase(it); - } - - const auto metrics = calculateLatencyMetrics( - ping_message, pong_message, received_ts_ns); - - LK_LOG_INFO("pong id={} rtt_ms={:.3f} " - "pong_to_ping_ms={:.3f} " - "ping_to_pong_and_processing_ms={:.3f} " - "estimated_one_way_latency_ms={:.3f}", - metrics.id, metrics.round_trip_time_ms, - metrics.pong_to_ping_time_ms, - metrics.ping_to_pong_and_processing_ms, - metrics.estimated_one_way_latency_ms); - } catch (const std::exception &e) { - LK_LOG_WARN("Failed to process pong payload: {}", e.what()); - } - }); - - LK_LOG_INFO("published data track '{}' and listening for '{}' from '{}'", - ping_pong::kPingTrackName, ping_pong::kPongTrackName, - ping_pong::kPongParticipantIdentity); - - std::uint64_t next_id = 1; - auto next_deadline = std::chrono::steady_clock::now(); - - while (g_running.load()) { - ping_pong::PingMessage ping_message; - ping_message.id = next_id++; - ping_message.ts_ns = ping_pong::timeSinceEpochNs(); - - const std::string json = ping_pong::pingMessageToJson(ping_message); - auto push_result = ping_track->tryPush(ping_pong::toPayload(json)); - if (!push_result) { - const auto &error = push_result.error(); - LK_LOG_WARN("Failed to push ping data frame: code={} message={}", - static_cast(error.code), error.message); - } else { - { - std::lock_guard lock(sent_messages_mutex); - sent_messages.emplace(ping_message.id, ping_message); - } - LK_LOG_INFO("sent ping id={} ts_ns={}", ping_message.id, - ping_message.ts_ns); - } - - next_deadline += ping_pong::kPingPeriod; - std::this_thread::sleep_until(next_deadline); - } - - LK_LOG_INFO("shutting down ping participant"); - room.reset(); - livekit::shutdown(); - return 0; -} diff --git a/examples/ping_pong/pong.cpp b/examples/ping_pong/pong.cpp deleted file mode 100644 index 34bdbd54..00000000 --- a/examples/ping_pong/pong.cpp +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright 2026 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. - */ - -/// Pong participant: listens on the "ping" data track and publishes responses -/// on the "pong" data track. Use a token whose identity is `pong`. - -#include "constants.h" -#include "json_converters.h" -#include "livekit/livekit.h" -#include "livekit/lk_log.h" -#include "messages.h" -#include "utils.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace livekit; - -namespace { - -std::atomic g_running{true}; - -void handleSignal(int) { g_running.store(false); } - -} // namespace - -int main(int argc, char *argv[]) { - std::string url = ping_pong::getenvOrEmpty("LIVEKIT_URL"); - std::string token = ping_pong::getenvOrEmpty("LIVEKIT_TOKEN"); - - if (argc >= 3) { - url = argv[1]; - token = argv[2]; - } - - if (url.empty() || token.empty()) { - LK_LOG_ERROR("LIVEKIT_URL and LIVEKIT_TOKEN (or ) are " - "required"); - return 1; - } - - std::signal(SIGINT, handleSignal); -#ifdef SIGTERM - std::signal(SIGTERM, handleSignal); -#endif - - livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); - - auto room = std::make_unique(); - RoomOptions options; - options.auto_subscribe = true; - options.dynacast = false; - - if (!room->Connect(url, token, options)) { - LK_LOG_ERROR("Failed to connect to room"); - livekit::shutdown(); - return 1; - } - - LocalParticipant *local_participant = room->localParticipant(); - assert(local_participant); - - LK_LOG_INFO("pong connected as identity='{}' room='{}'", - local_participant->identity(), room->room_info().name); - - auto publish_result = - local_participant->publishDataTrack(ping_pong::kPongTrackName); - if (!publish_result) { - const auto &error = publish_result.error(); - LK_LOG_ERROR("Failed to publish pong data track: code={} message={}", - static_cast(error.code), error.message); - room->setDelegate(nullptr); - room.reset(); - livekit::shutdown(); - return 1; - } - - std::shared_ptr pong_track = publish_result.value(); - - const auto callback_id = room->addOnDataFrameCallback( - ping_pong::kPingParticipantIdentity, ping_pong::kPingTrackName, - [pong_track](const std::vector &payload, - std::optional /*user_timestamp*/) { - try { - if (payload.empty()) { - LK_LOG_DEBUG("Ignoring empty ping payload"); - return; - } - - const auto ping_message = - ping_pong::pingMessageFromJson(ping_pong::toString(payload)); - - ping_pong::PongMessage pong_message; - pong_message.rec_id = ping_message.id; - pong_message.ts_ns = ping_pong::timeSinceEpochNs(); - - const std::string json = ping_pong::pongMessageToJson(pong_message); - auto push_result = pong_track->tryPush(ping_pong::toPayload(json)); - if (!push_result) { - const auto &error = push_result.error(); - LK_LOG_WARN("Failed to push pong data frame: code={} message={}", - static_cast(error.code), error.message); - return; - } - - LK_LOG_INFO("received ping id={} ts_ns={} and sent pong rec_id={} " - "ts_ns={}", - ping_message.id, ping_message.ts_ns, pong_message.rec_id, - pong_message.ts_ns); - } catch (const std::exception &e) { - LK_LOG_WARN("Failed to process ping payload: {}", e.what()); - } - }); - - LK_LOG_INFO("published data track '{}' and listening for '{}' from '{}'", - ping_pong::kPongTrackName, ping_pong::kPingTrackName, - ping_pong::kPingParticipantIdentity); - - while (g_running.load()) { - std::this_thread::sleep_for(ping_pong::kPollPeriod); - } - - LK_LOG_INFO("shutting down pong participant"); - room.reset(); - livekit::shutdown(); - return 0; -} diff --git a/examples/ping_pong/utils.h b/examples/ping_pong/utils.h deleted file mode 100644 index 56c915b9..00000000 --- a/examples/ping_pong/utils.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2026 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. - */ - -#pragma once - -#include -#include -#include -#include -#include - -namespace ping_pong { - -inline std::string getenvOrEmpty(const char *name) { - const char *value = std::getenv(name); - return value ? std::string(value) : std::string{}; -} - -inline std::int64_t timeSinceEpochNs() { - const auto now = std::chrono::system_clock::now().time_since_epoch(); - return std::chrono::duration_cast(now).count(); -} - -inline std::vector toPayload(const std::string &json) { - return std::vector(json.begin(), json.end()); -} - -inline std::string toString(const std::vector &payload) { - return std::string(payload.begin(), payload.end()); -} - -} // namespace ping_pong diff --git a/examples/simple_data_stream/main.cpp b/examples/simple_data_stream/main.cpp deleted file mode 100644 index f8144b71..00000000 --- a/examples/simple_data_stream/main.cpp +++ /dev/null @@ -1,284 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "livekit/livekit.h" - -using namespace livekit; - -namespace { - -std::atomic g_running{true}; - -void handleSignal(int) { g_running.store(false); } - -// Helper: get env var or empty string -std::string getenvOrEmpty(const char *name) { - const char *v = std::getenv(name); - return v ? std::string(v) : std::string{}; -} - -std::int64_t nowEpochMs() { - using namespace std::chrono; - return duration_cast(system_clock::now().time_since_epoch()) - .count(); -} - -std::string randomHexId(std::size_t nbytes = 16) { - static thread_local std::mt19937_64 rng{std::random_device{}()}; - std::ostringstream oss; - for (std::size_t i = 0; i < nbytes; ++i) { - std::uint8_t b = static_cast(rng() & 0xFF); - const char *hex = "0123456789abcdef"; - oss << hex[(b >> 4) & 0xF] << hex[b & 0xF]; - } - return oss.str(); -} - -// Greeting: send text + image -void greetParticipant(Room *room, const std::string &identity) { - std::cout << "[DataStream] Greeting participant: " << identity << "\n"; - - LocalParticipant *lp = room->localParticipant(); - if (!lp) { - std::cerr << "[DataStream] No local participant, cannot greet.\n"; - return; - } - - try { - const std::int64_t sent_ms = nowEpochMs(); - const std::string sender_id = - !lp->identity().empty() ? lp->identity() : std::string("cpp_sender"); - const std::vector dest{identity}; - - // Send text stream ("chat") - const std::string chat_stream_id = randomHexId(); - const std::string reply_to_id = ""; - std::map chat_attrs; - chat_attrs["sent_ms"] = std::to_string(sent_ms); - chat_attrs["kind"] = "chat"; - chat_attrs["test_flag"] = "1"; - chat_attrs["seq"] = "1"; - - // Put timestamp in payload too (so you can compute latency even if - // attributes aren’t plumbed through your reader info yet). - const std::string body = "Hi! Just a friendly message"; - const std::string payload = "sent_ms=" + std::to_string(sent_ms) + "\n" + - "stream_id=" + chat_stream_id + "\n" + body; - TextStreamWriter text_writer(*lp, "chat", chat_attrs, chat_stream_id, - payload.size(), reply_to_id, dest, sender_id); - - const std::string message = "Hi! Just a friendly message"; - text_writer.write(message); // will be chunked internally if needed - text_writer.close(); // optional reason/attributes omitted - - // Send image as byte stream - const std::string file_path = "data/green.avif"; - std::ifstream in(file_path, std::ios::binary); - if (!in) { - std::cerr << "[DataStream] Failed to open file: " << file_path << "\n"; - return; - } - - std::vector data((std::istreambuf_iterator(in)), - std::istreambuf_iterator()); - - const std::string file_stream_id = randomHexId(); - std::map file_attrs; - file_attrs["sent_ms"] = std::to_string(sent_ms); - file_attrs["kind"] = "file"; - file_attrs["test_flag"] = "1"; - file_attrs["orig_path"] = file_path; - const std::string name = - std::filesystem::path(file_path).filename().string(); - const std::string mime = "image/avif"; - ByteStreamWriter byte_writer(*lp, name, "files", file_attrs, file_stream_id, - data.size(), mime, dest, sender_id); - byte_writer.write(data); - byte_writer.close(); - - std::cout << "[DataStream] Greeting sent to " << identity - << " (sent_ms=" << sent_ms << ")\n"; - } catch (const std::exception &e) { - std::cerr << "[DataStream] Error greeting participant " << identity << ": " - << e.what() << "\n"; - } -} - -// Handlers for incoming streams -void handleChatMessage(std::shared_ptr reader, - const std::string &participant_identity) { - try { - const auto info = reader->info(); // copy (safe even if reader goes away) - const std::int64_t recv_ms = nowEpochMs(); - const std::int64_t sent_ms = info.timestamp; - const auto latency = (sent_ms > 0) ? (recv_ms - sent_ms) : -1; - std::string full_text = reader->readAll(); - std::cout << "[DataStream] Received chat from " << participant_identity - << " topic=" << info.topic << " stream_id=" << info.stream_id - << " latency_ms=" << latency << " text='" << full_text << "'\n"; - } catch (const std::exception &e) { - std::cerr << "[DataStream] Error reading chat stream from " - << participant_identity << ": " << e.what() << "\n"; - } -} - -void handleWelcomeImage(std::shared_ptr reader, - const std::string &participant_identity) { - try { - const auto info = reader->info(); - const std::string stream_id = - info.stream_id.empty() ? "unknown" : info.stream_id; - const std::string original_name = - info.name.empty() ? "received_image.bin" : info.name; - // Latency: prefer header timestamp - std::int64_t sent_ms = info.timestamp; - // Optional: override with explicit attribute if you set it - auto it = info.attributes.find("sent_ms"); - if (it != info.attributes.end()) { - try { - sent_ms = std::stoll(it->second); - } catch (...) { - } - } - const std::int64_t recv_ms = nowEpochMs(); - const std::int64_t latency_ms = (sent_ms > 0) ? (recv_ms - sent_ms) : -1; - const std::string out_file = "received_" + original_name; - std::cout << "[DataStream] Receiving image from " << participant_identity - << " stream_id=" << stream_id << " name='" << original_name << "'" - << " mime='" << info.mime_type << "'" - << " size=" - << (info.size ? std::to_string(*info.size) : "unknown") - << " latency_ms=" << latency_ms << " -> '" << out_file << "'\n"; - std::ofstream out(out_file, std::ios::binary); - if (!out) { - std::cerr << "[DataStream] Failed to open output file: " << out_file - << "\n"; - return; - } - std::vector chunk; - std::uint64_t total_written = 0; - while (reader->readNext(chunk)) { - if (!chunk.empty()) { - out.write(reinterpret_cast(chunk.data()), - static_cast(chunk.size())); - total_written += chunk.size(); - } - } - std::cout << "[DataStream] Saved image from " << participant_identity - << " stream_id=" << stream_id << " bytes=" << total_written - << " to '" << out_file << std::endl; - } catch (const std::exception &e) { - std::cerr << "[DataStream] Error reading image stream from " - << participant_identity << ": " << e.what() << "\n"; - } -} - -} // namespace - -int main(int argc, char *argv[]) { - // Get URL and token from env. - std::string url = getenvOrEmpty("LIVEKIT_URL"); - std::string token = getenvOrEmpty("LIVEKIT_TOKEN"); - - if (argc >= 3) { - // Allow overriding via CLI: ./SimpleDataStream - url = argv[1]; - token = argv[2]; - } - - if (url.empty() || token.empty()) { - std::cerr << "LIVEKIT_URL and LIVEKIT_TOKEN (or CLI args) are required\n"; - return 1; - } - - std::cout << "[DataStream] Connecting to: " << url << "\n"; - - std::signal(SIGINT, handleSignal); -#ifdef SIGTERM - std::signal(SIGTERM, handleSignal); -#endif - - // Initialize the livekit with logging to console. - livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); - auto room = std::make_unique(); - RoomOptions options; - options.auto_subscribe = true; - options.dynacast = false; - - bool ok = room->Connect(url, token, options); - std::cout << "[DataStream] Connect result: " << std::boolalpha << ok << "\n"; - if (!ok) { - std::cerr << "[DataStream] Failed to connect to room\n"; - livekit::shutdown(); - return 1; - } - - auto info = room->room_info(); - std::cout << "[DataStream] Connected to room '" << info.name - << "', participants: " << info.num_participants << "\n"; - - // Register stream handlers - room->registerTextStreamHandler( - "chat", [](std::shared_ptr reader, - const std::string &participant_identity) { - std::thread t(handleChatMessage, std::move(reader), - participant_identity); - t.detach(); - }); - - room->registerByteStreamHandler( - "files", [](std::shared_ptr reader, - const std::string &participant_identity) { - std::thread t(handleWelcomeImage, std::move(reader), - participant_identity); - t.detach(); - }); - - // Greet existing participants - { - auto remotes = room->remoteParticipants(); - for (const auto &rp : remotes) { - if (!rp) - continue; - std::cout << "Remote: " << rp->identity() << "\n"; - greetParticipant(room.get(), rp->identity()); - } - } - - // Optionally: greet on join - // - // If Room API exposes a participant-connected callback, you could do: - // - // room->onParticipantConnected( - // [&](RemoteParticipant& participant) { - // std::cout << "[DataStream] participant connected: " - // << participant.sid() << " " << participant.identity() - // << "\n"; - // greetParticipant(room.get(), participant.identity()); - // }); - // - // Adjust to your actual event API. - std::cout << "[DataStream] Ready. Waiting for streams (Ctrl-C to exit)...\n"; - // Keep process alive until signal - while (g_running.load()) { - std::this_thread::sleep_for(std::chrono::milliseconds(5)); - } - - std::cout << "[DataStream] Shutting down...\n"; - // It is important to clean up the delegate and room in order. - room->setDelegate(nullptr); - room.reset(); - livekit::shutdown(); - return 0; -} diff --git a/examples/simple_joystick/json_utils.cpp b/examples/simple_joystick/json_utils.cpp deleted file mode 100644 index d634aaa7..00000000 --- a/examples/simple_joystick/json_utils.cpp +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2025 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 "json_utils.h" - -#include -#include - -namespace simple_joystick { - -std::string joystick_to_json(const JoystickCommand &cmd) { - nlohmann::json j; - j["x"] = cmd.x; - j["y"] = cmd.y; - j["z"] = cmd.z; - return j.dump(); -} - -JoystickCommand json_to_joystick(const std::string &json) { - try { - auto j = nlohmann::json::parse(json); - JoystickCommand cmd; - cmd.x = j.at("x").get(); - cmd.y = j.at("y").get(); - cmd.z = j.at("z").get(); - return cmd; - } catch (const nlohmann::json::exception &e) { - throw std::runtime_error(std::string("Failed to parse joystick JSON: ") + - e.what()); - } -} - -} // namespace simple_joystick diff --git a/examples/simple_joystick/json_utils.h b/examples/simple_joystick/json_utils.h deleted file mode 100644 index 66ba16aa..00000000 --- a/examples/simple_joystick/json_utils.h +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2025 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. - */ - -#pragma once - -#include - -namespace simple_joystick { - -/// Represents a joystick command with three axes. -struct JoystickCommand { - double x = 0.0; - double y = 0.0; - double z = 0.0; -}; - -/// Serialize a JoystickCommand to a JSON string. -/// Example output: {"x":1.0,"y":2.0,"z":3.0} -std::string joystick_to_json(const JoystickCommand &cmd); - -/// Deserialize a JSON string into a JoystickCommand. -/// Throws std::runtime_error if the JSON is invalid or missing fields. -JoystickCommand json_to_joystick(const std::string &json); - -} // namespace simple_joystick diff --git a/examples/simple_joystick/receiver.cpp b/examples/simple_joystick/receiver.cpp deleted file mode 100644 index d62785a4..00000000 --- a/examples/simple_joystick/receiver.cpp +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2025 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 -#include -#include -#include -#include -#include - -#include "json_utils.h" -#include "livekit/livekit.h" -#include "utils.h" - -using namespace livekit; -using namespace std::chrono_literals; - -namespace { - -std::atomic g_running{true}; -std::atomic g_sender_connected{false}; - -void handleSignal(int) { g_running.store(false); } - -void printUsage(const char *prog) { - std::cerr << "Usage:\n" - << " " << prog << " \n" - << "or:\n" - << " " << prog << " --url= --token=\n\n" - << "Env fallbacks:\n" - << " LIVEKIT_URL, LIVEKIT_TOKEN\n\n" - << "This is the receiver. It waits for a sender peer to\n" - << "connect and send joystick commands via RPC.\n" - << "Exits after 2 minutes if no commands are received.\n"; -} - -} // namespace - -int main(int argc, char *argv[]) { - std::string url, token; - if (!simple_joystick::parseArgs(argc, argv, url, token)) { - printUsage(argv[0]); - return 1; - } - - std::cout << "[Receiver] Connecting to: " << url << "\n"; - std::signal(SIGINT, handleSignal); - - livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); - auto room = std::make_unique(); - RoomOptions options; - options.auto_subscribe = true; - options.dynacast = false; - - bool res = room->Connect(url, token, options); - std::cout << "[Receiver] Connect result: " << std::boolalpha << res << "\n"; - if (!res) { - std::cerr << "[Receiver] Failed to connect to room\n"; - livekit::shutdown(); - return 1; - } - - auto info = room->room_info(); - std::cout << "[Receiver] Connected to room: " << info.name << "\n"; - std::cout << "[Receiver] Waiting for sender peer (up to 2 minutes)...\n"; - - // Register RPC handler for joystick commands - LocalParticipant *lp = room->localParticipant(); - lp->registerRpcMethod( - "joystick_command", - [](const RpcInvocationData &data) -> std::optional { - try { - auto cmd = simple_joystick::json_to_joystick(data.payload); - g_sender_connected.store(true); - std::cout << "[Receiver] Joystick from '" << data.caller_identity - << "': x=" << cmd.x << " y=" << cmd.y << " z=" << cmd.z - << "\n"; - return std::optional{"ok"}; - } catch (const std::exception &e) { - std::cerr << "[Receiver] Bad joystick payload: " << e.what() << "\n"; - throw; - } - }); - - std::cout << "[Receiver] RPC handler 'joystick_command' registered. " - << "Listening for commands...\n"; - - // Wait up to 2 minutes for activity, then exit as failure - auto deadline = std::chrono::steady_clock::now() + 2min; - - while (g_running.load() && std::chrono::steady_clock::now() < deadline) { - std::this_thread::sleep_for(100ms); - } - - if (!g_running.load()) { - std::cout << "[Receiver] Interrupted by signal. Shutting down.\n"; - } else if (!g_sender_connected.load()) { - std::cerr - << "[Receiver] Timed out after 2 minutes with no sender connection. " - << "Exiting as failure.\n"; - room->setDelegate(nullptr); - room.reset(); - livekit::shutdown(); - return 1; - } else { - std::cout << "[Receiver] Session complete.\n"; - } - - room->setDelegate(nullptr); - room.reset(); - livekit::shutdown(); - return 0; -} diff --git a/examples/simple_joystick/sender.cpp b/examples/simple_joystick/sender.cpp deleted file mode 100644 index a235c3da..00000000 --- a/examples/simple_joystick/sender.cpp +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Copyright 2025 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 -#include -#include -#include -#include -#include - -#ifdef _WIN32 -#include -#else -#include -#include -#include -#endif - -#include "json_utils.h" -#include "livekit/livekit.h" -#include "utils.h" - -using namespace livekit; -using namespace std::chrono_literals; - -namespace { - -std::atomic g_running{true}; - -void handleSignal(int) { g_running.store(false); } - -// --- Raw terminal input helpers --- - -#ifndef _WIN32 -struct termios g_orig_termios; -bool g_raw_mode_enabled = false; - -void disableRawMode() { - if (g_raw_mode_enabled) { - tcsetattr(STDIN_FILENO, TCSAFLUSH, &g_orig_termios); - g_raw_mode_enabled = false; - } -} - -void enableRawMode() { - tcgetattr(STDIN_FILENO, &g_orig_termios); - g_raw_mode_enabled = true; - std::atexit(disableRawMode); - - struct termios raw = g_orig_termios; - raw.c_lflag &= ~(ECHO | ICANON); // disable echo and canonical mode - raw.c_cc[VMIN] = 0; // non-blocking read - raw.c_cc[VTIME] = 0; - tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw); -} - -// Returns -1 if no key is available, otherwise the character code. -int readKeyNonBlocking() { - fd_set fds; - FD_ZERO(&fds); - FD_SET(STDIN_FILENO, &fds); - struct timeval tv = {0, 0}; // immediate return - if (select(STDIN_FILENO + 1, &fds, nullptr, nullptr, &tv) > 0) { - unsigned char ch; - if (read(STDIN_FILENO, &ch, 1) == 1) - return ch; - } - return -1; -} -#else -void enableRawMode() { /* Windows _getch() is already unbuffered */ } -void disableRawMode() {} - -int readKeyNonBlocking() { - if (_kbhit()) - return _getch(); - return -1; -} -#endif - -void printUsage(const char *prog) { - std::cerr << "Usage:\n" - << " " << prog << " \n" - << "or:\n" - << " " << prog << " --url= --token=\n\n" - << "Env fallbacks:\n" - << " LIVEKIT_URL, LIVEKIT_TOKEN\n\n" - << "This is the sender. It connects to the room and\n" - << "continuously checks for a receiver peer every 2 seconds.\n" - << "Once connected, use keyboard to send joystick commands:\n" - << " w / s = +x / -x\n" - << " d / a = +y / -y\n" - << " z / c = +z / -z\n" - << " q = quit\n" - << "Automatically reconnects if receiver leaves.\n"; -} - -void printControls() { - std::cout << "\n" - << " Controls:\n" - << " w / s = +x / -x\n" - << " d / a = +y / -y\n" - << " z / c = +z / -z\n" - << " q = quit\n\n"; -} - -} // namespace - -int main(int argc, char *argv[]) { - std::string url, token; - if (!simple_joystick::parseArgs(argc, argv, url, token)) { - printUsage(argv[0]); - return 1; - } - - std::cout << "[Sender] Connecting to: " << url << "\n"; - std::signal(SIGINT, handleSignal); - - livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); - auto room = std::make_unique(); - RoomOptions options; - options.auto_subscribe = true; - options.dynacast = false; - - bool res = room->Connect(url, token, options); - std::cout << "[Sender] Connect result: " << std::boolalpha << res << "\n"; - if (!res) { - std::cerr << "[Sender] Failed to connect to room\n"; - livekit::shutdown(); - return 1; - } - - auto info = room->room_info(); - std::cout << "[Sender] Connected to room: " << info.name << "\n"; - - // Enable raw terminal mode for immediate keypress detection - enableRawMode(); - - std::cout << "[Sender] Waiting for 'robot' to join (checking every 2s)...\n"; - printControls(); - - LocalParticipant *lp = room->localParticipant(); - double x = 0.0, y = 0.0, z = 0.0; - bool receiver_connected = false; - auto last_receiver_check = std::chrono::steady_clock::now(); - - while (g_running.load()) { - // Periodically check receiver presence every 2 seconds - auto now = std::chrono::steady_clock::now(); - if (now - last_receiver_check >= 2s) { - last_receiver_check = now; - bool receiver_present = (room->remoteParticipant("robot") != nullptr); - - if (receiver_present && !receiver_connected) { - std::cout - << "[Sender] Receiver connected! Use keys to send commands.\n"; - receiver_connected = true; - } else if (!receiver_present && receiver_connected) { - std::cout - << "[Sender] Receiver disconnected. Waiting for reconnect...\n"; - receiver_connected = false; - } - } - - // Poll for keypress (non-blocking) - int key = readKeyNonBlocking(); - if (key == -1) { - std::this_thread::sleep_for(20ms); // avoid busy-wait - continue; - } - - // Handle quit - if (key == 'q' || key == 'Q') { - std::cout << "\n[Sender] Quit requested.\n"; - break; - } - - // Map key to axis change - bool changed = false; - switch (key) { - case 'w': - case 'W': - x += 1.0; - changed = true; - break; - case 's': - case 'S': - x -= 1.0; - changed = true; - break; - case 'd': - case 'D': - y += 1.0; - changed = true; - break; - case 'a': - case 'A': - y -= 1.0; - changed = true; - break; - case 'z': - case 'Z': - z += 1.0; - changed = true; - break; - case 'c': - case 'C': - z -= 1.0; - changed = true; - break; - default: - break; - } - - if (!changed) - continue; - - if (!receiver_connected) { - std::cout << "[Sender] (no receiver connected) x=" << x << " y=" << y - << " z=" << z << "\n"; - continue; - } - - // Send joystick command via RPC - simple_joystick::JoystickCommand cmd{x, y, z}; - std::string payload = simple_joystick::joystick_to_json(cmd); - - std::cout << "[Sender] Sending: x=" << x << " y=" << y << " z=" << z - << "\n"; - - try { - std::string response = - lp->performRpc("robot", "joystick_command", payload, 5.0); - std::cout << "[Sender] Receiver acknowledged: " << response << "\n"; - } catch (const RpcError &e) { - std::cerr << "[Sender] RPC error: " << e.message() << "\n"; - if (static_cast(e.code()) == - RpcError::ErrorCode::RECIPIENT_DISCONNECTED) { - std::cout - << "[Sender] Receiver disconnected. Waiting for reconnect...\n"; - receiver_connected = false; - } - } catch (const std::exception &e) { - std::cerr << "[Sender] Error sending command: " << e.what() << "\n"; - } - } - - disableRawMode(); - - std::cout << "[Sender] Done. Shutting down.\n"; - room->setDelegate(nullptr); - room.reset(); - livekit::shutdown(); - return 0; -} diff --git a/examples/simple_joystick/utils.cpp b/examples/simple_joystick/utils.cpp deleted file mode 100644 index cc0ef96c..00000000 --- a/examples/simple_joystick/utils.cpp +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2025 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 "utils.h" - -#include -#include -#include - -namespace simple_joystick { - -bool parseArgs(int argc, char *argv[], std::string &url, std::string &token) { - for (int i = 1; i < argc; ++i) { - std::string a = argv[i]; - if (a == "-h" || a == "--help") { - return false; - } - } - - auto get_flag_value = [&](const std::string &name, int &i) -> std::string { - std::string arg = argv[i]; - const std::string eq = name + "="; - if (arg.rfind(name, 0) == 0) { - if (arg.size() > name.size() && arg[name.size()] == '=') { - return arg.substr(eq.size()); - } else if (i + 1 < argc) { - return std::string(argv[++i]); - } - } - return {}; - }; - - for (int i = 1; i < argc; ++i) { - const std::string a = argv[i]; - if (a.rfind("--url", 0) == 0) { - auto v = get_flag_value("--url", i); - if (!v.empty()) - url = v; - } else if (a.rfind("--token", 0) == 0) { - auto v = get_flag_value("--token", i); - if (!v.empty()) - token = v; - } - } - - // Positional args: - std::vector pos; - for (int i = 1; i < argc; ++i) { - std::string a = argv[i]; - if (a.rfind("--", 0) == 0) - continue; - pos.push_back(std::move(a)); - } - if (url.empty() && pos.size() >= 1) - url = pos[0]; - if (token.empty() && pos.size() >= 2) - token = pos[1]; - - // Environment variable fallbacks - if (url.empty()) { - const char *e = std::getenv("LIVEKIT_URL"); - if (e) - url = e; - } - if (token.empty()) { - const char *e = std::getenv("LIVEKIT_TOKEN"); - if (e) - token = e; - } - - return !(url.empty() || token.empty()); -} - -} // namespace simple_joystick diff --git a/examples/simple_joystick/utils.h b/examples/simple_joystick/utils.h deleted file mode 100644 index 7fe94eec..00000000 --- a/examples/simple_joystick/utils.h +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2025 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. - */ - -#pragma once - -#include - -namespace simple_joystick { - -/// Parse command-line arguments for --url and --token. -/// Supports: -/// - Positional: -/// - Flags: --url= / --url , --token= / --token -/// - Env vars: LIVEKIT_URL, LIVEKIT_TOKEN -/// Returns true if both url and token were resolved, false otherwise. -bool parseArgs(int argc, char *argv[], std::string &url, std::string &token); - -} // namespace simple_joystick diff --git a/examples/simple_room/fallback_capture.cpp b/examples/simple_room/fallback_capture.cpp deleted file mode 100644 index 26269886..00000000 --- a/examples/simple_room/fallback_capture.cpp +++ /dev/null @@ -1,119 +0,0 @@ -/* - * 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 "fallback_capture.h" - -#include -#include -#include -#include -#include -#include - -#include "livekit/livekit.h" -#include "wav_audio_source.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; - - // FIX: variable name should not shadow the type - WavAudioSource wavSource("data/welcome.wav", 48000, 1, false); - - 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); - wavSource.fillFrame(frame); - try { - source->captureFrame(frame); - } catch (const std::exception &e) { - LK_LOG_ERROR("Error in captureFrame (noise): {}", e.what()); - break; - } - - // Pace the loop to roughly real-time - next_deadline += std::chrono::milliseconds(frame_ms); - std::this_thread::sleep_until(next_deadline); - } - - try { - source->clearQueue(); - } catch (...) { - LK_LOG_WARN("Error in clearQueue (noise)"); - } -} - -// Fake video source: solid color cycling -void runFakeVideoCaptureLoop(const std::shared_ptr &source, - std::atomic &running_flag) { - auto frame = VideoFrame::create(1280, 720, VideoBufferType::BGRA); - 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) { - LK_LOG_ERROR("Error in captureFrame (fake video): {}", e.what()); - break; - } - - std::this_thread::sleep_for(std::chrono::duration(framerate)); - } -} diff --git a/examples/simple_room/fallback_capture.h b/examples/simple_room/fallback_capture.h deleted file mode 100644 index a7d85361..00000000 --- a/examples/simple_room/fallback_capture.h +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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 - -// Assuming you already have this somewhere: -extern std::atomic g_running; - -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/examples/simple_room/main.cpp b/examples/simple_room/main.cpp deleted file mode 100644 index bd7c6abc..00000000 --- a/examples/simple_room/main.cpp +++ /dev/null @@ -1,416 +0,0 @@ -/* - * 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 -#include -#include - -#include "livekit/livekit.h" -#include "sdl_media_manager.h" -#include "wav_audio_source.h" - -using namespace livekit; - -namespace { - -std::atomic g_running{true}; - -void printUsage(const char *prog) { - std::cerr - << "Usage:\n" - << " " << prog - << " [--enable_e2ee] [--e2ee_key ]\n" - << "or:\n" - << " " << prog - << " --url= --token= [--enable_e2ee] [--e2ee_key=]\n" - << " " << prog - << " --url --token [--enable_e2ee] [--e2ee_key " - "]\n\n" - << "E2EE:\n" - << " --enable_e2ee Enable end-to-end encryption (E2EE)\n" - << " --e2ee_key Optional shared key (UTF-8). If omitted, " - "E2EE is enabled\n" - << " but no shared key is set (advanced " - "usage).\n\n" - << "Env fallbacks:\n" - << " LIVEKIT_URL, LIVEKIT_TOKEN, LIVEKIT_E2EE_KEY\n"; -} - -void handleSignal(int) { g_running.store(false); } - -bool parseArgs(int argc, char *argv[], std::string &url, std::string &token, - bool &enable_e2ee, std::string &e2ee_key) { - enable_e2ee = false; - // --help - for (int i = 1; i < argc; ++i) { - std::string a = argv[i]; - if (a == "-h" || a == "--help") { - return false; - } - } - - // flags: --url= / --token= or split form - auto get_flag_value = [&](const std::string &name, int &i) -> std::string { - std::string arg = argv[i]; - const std::string eq = name + "="; - if (arg.rfind(name, 0) == 0) { // starts with name - if (arg.size() > name.size() && arg[name.size()] == '=') { - return arg.substr(eq.size()); - } else if (i + 1 < argc) { - return std::string(argv[++i]); - } - } - return {}; - }; - - for (int i = 1; i < argc; ++i) { - const std::string a = argv[i]; - if (a == "--enable_e2ee") { - enable_e2ee = true; - } else if (a.rfind("--url", 0) == 0) { - auto v = get_flag_value("--url", i); - if (!v.empty()) - url = v; - } else if (a.rfind("--token", 0) == 0) { - auto v = get_flag_value("--token", i); - if (!v.empty()) - token = v; - } else if (a.rfind("--e2ee_key", 0) == 0) { - auto v = get_flag_value("--e2ee_key", i); - if (!v.empty()) - e2ee_key = v; - } - } - - // positional if still empty - if (url.empty() || token.empty()) { - std::vector pos; - for (int i = 1; i < argc; ++i) { - std::string a = argv[i]; - if (a.rfind("--", 0) == 0) - continue; // skip flags we already parsed - pos.push_back(std::move(a)); - } - if (pos.size() >= 2) { - if (url.empty()) - url = pos[0]; - if (token.empty()) - token = pos[1]; - } - } - - // 4) env fallbacks - if (url.empty()) { - const char *e = std::getenv("LIVEKIT_URL"); - if (e) - url = e; - } - if (token.empty()) { - const char *e = std::getenv("LIVEKIT_TOKEN"); - if (e) - token = e; - } - if (e2ee_key.empty()) { - const char *e = std::getenv("LIVEKIT_E2EE_KEY"); - if (e) - e2ee_key = e; - } - - return !(url.empty() || token.empty()); -} - -class MainThreadDispatcher { -public: - static void dispatch(std::function fn) { - std::lock_guard lock(mutex_); - queue_.push(std::move(fn)); - } - - static void update() { - std::queue> local; - - { - std::lock_guard lock(mutex_); - std::swap(local, queue_); - } - - // Run everything on main thread - while (!local.empty()) { - local.front()(); - local.pop(); - } - } - -private: - static inline std::mutex mutex_; - static inline std::queue> queue_; -}; - -class SimpleRoomDelegate : public livekit::RoomDelegate { -public: - explicit SimpleRoomDelegate(SDLMediaManager &media) : media_(media) {} - - void onParticipantConnected( - livekit::Room & /*room*/, - const livekit::ParticipantConnectedEvent &ev) override { - std::cout << "[Room] participant connected: identity=" - << ev.participant->identity() - << " name=" << ev.participant->name() << "\n"; - } - - void onTrackSubscribed(livekit::Room & /*room*/, - const livekit::TrackSubscribedEvent &ev) override { - const char *participant_identity = - ev.participant ? ev.participant->identity().c_str() : ""; - const std::string track_sid = - ev.publication ? ev.publication->sid() : ""; - const std::string track_name = - ev.publication ? ev.publication->name() : ""; - std::cout << "[Room] track subscribed: participant_identity=" - << participant_identity << " track_sid=" << track_sid - << " name=" << track_name; - if (ev.track) { - std::cout << " kind=" << static_cast(ev.track->kind()); - } - if (ev.publication) { - std::cout << " source=" << static_cast(ev.publication->source()); - } - std::cout << std::endl; - - // If this is a VIDEO track, create a VideoStream and attach to renderer - if (ev.track && ev.track->kind() == TrackKind::KIND_VIDEO) { - VideoStream::Options opts; - opts.format = livekit::VideoBufferType::RGBA; - auto video_stream = VideoStream::fromTrack(ev.track, opts); - if (!video_stream) { - LK_LOG_ERROR("Failed to create VideoStream for track {}", track_sid); - return; - } - - MainThreadDispatcher::dispatch([this, video_stream] { - if (!media_.initRenderer(video_stream)) { - LK_LOG_ERROR("SDLMediaManager::startRenderer failed for track"); - } - }); - } else if (ev.track && ev.track->kind() == TrackKind::KIND_AUDIO) { - AudioStream::Options opts; - auto audio_stream = AudioStream::fromTrack(ev.track, opts); - MainThreadDispatcher::dispatch([this, audio_stream] { - if (!media_.startSpeaker(audio_stream)) { - LK_LOG_ERROR("SDLMediaManager::startSpeaker failed for track"); - } - }); - } - } - -private: - SDLMediaManager &media_; -}; - -static std::vector toBytes(const std::string &s) { - return std::vector(s.begin(), s.end()); -} - -void print_livekit_version() { - std::cout << "LiveKit version: " << LIVEKIT_BUILD_VERSION_FULL << " (" - << LIVEKIT_BUILD_FLAVOR << ", commit " << LIVEKIT_BUILD_COMMIT - << ", built " << LIVEKIT_BUILD_DATE << ")" << std::endl; -} - -} // namespace - -int main(int argc, char *argv[]) { - print_livekit_version(); - std::string url, token; - bool enable_e2ee = false; - std::string e2ee_key; - if (!parseArgs(argc, argv, url, token, enable_e2ee, e2ee_key)) { - printUsage(argv[0]); - return 1; - } - - // Exit if token and url are not set - if (url.empty() || token.empty()) { - std::cerr << "LIVEKIT_URL and LIVEKIT_TOKEN (or CLI args) are required\n"; - return 1; - } - - if (!SDL_Init(SDL_INIT_VIDEO)) { - LK_LOG_ERROR("SDL_Init(SDL_INIT_VIDEO) failed: {}", SDL_GetError()); - // You can choose to exit, or run in "headless" mode without renderer. - // return 1; - } - - // Setup media; - SDLMediaManager media; - - std::cout << "Connecting to: " << url << std::endl; - - // Handle Ctrl-C to exit the idle loop - std::signal(SIGINT, handleSignal); - - livekit::initialize(); - auto room = std::make_unique(); - SimpleRoomDelegate delegate(media); - room->setDelegate(&delegate); - - RoomOptions options; - options.auto_subscribe = true; - options.dynacast = false; - - if (enable_e2ee) { - livekit::E2EEOptions encryption; - encryption.encryption_type = livekit::EncryptionType::GCM; - // Optional shared key: if empty, we enable E2EE without setting a shared - // key. (Advanced use: keys can be set/ratcheted later via - // E2EEManager/KeyProvider.) - if (!e2ee_key.empty()) { - encryption.key_provider_options.shared_key = toBytes(e2ee_key); - } - options.encryption = encryption; - if (!e2ee_key.empty()) { - std::cout << "[E2EE] enabled : (shared key length=" << e2ee_key.size() - << ")\n"; - } else { - std::cout << "[E2EE] enabled: (no shared key set)\n"; - } - } - - bool res = room->Connect(url, token, options); - std::cout << "Connect result is " << std::boolalpha << res << std::endl; - if (!res) { - LK_LOG_ERROR("Failed to connect to room"); - livekit::shutdown(); - return 1; - } - - auto info = room->room_info(); - std::cout << "Connected to room:\n" - << " SID: " << (info.sid ? *info.sid : "(none)") << "\n" - << " Name: " << info.name << "\n" - << " Metadata: " << info.metadata << "\n" - << " Max participants: " << info.max_participants << "\n" - << " Num participants: " << info.num_participants << "\n" - << " Num publishers: " << info.num_publishers << "\n" - << " Active recording: " << (info.active_recording ? "yes" : "no") - << "\n" - << " Empty timeout (s): " << info.empty_timeout << "\n" - << " Departure timeout (s): " << info.departure_timeout << "\n" - << " Lossy DC low threshold: " - << info.lossy_dc_buffered_amount_low_threshold << "\n" - << " Reliable DC low threshold: " - << info.reliable_dc_buffered_amount_low_threshold << "\n" - << " Creation time (ms): " << info.creation_time << "\n"; - - // Setup Audio Source / Track - auto audioSource = std::make_shared(44100, 1, 0); - auto audioTrack = - LocalAudioTrack::createLocalAudioTrack("micTrack", audioSource); - - TrackPublishOptions audioOpts; - audioOpts.source = TrackSource::SOURCE_MICROPHONE; - audioOpts.dtx = false; - audioOpts.simulcast = false; - try { - room->localParticipant()->publishTrack(audioTrack, audioOpts); - const auto audioPub = audioTrack->publication(); - - std::cout << "Published track:\n" - << " SID: " << audioPub->sid() << "\n" - << " Name: " << audioPub->name() << "\n" - << " Kind: " << static_cast(audioPub->kind()) << "\n" - << " Source: " << static_cast(audioPub->source()) << "\n" - << " Simulcasted: " << std::boolalpha << audioPub->simulcasted() - << "\n" - << " Muted: " << std::boolalpha << audioPub->muted() << "\n"; - } catch (const std::exception &e) { - LK_LOG_ERROR("Failed to publish track: {}", e.what()); - } - - media.startMic(audioSource); - - // Setup Video Source / Track - auto videoSource = std::make_shared(1280, 720); - auto videoTrack = LocalVideoTrack::createLocalVideoTrack("cam", videoSource); - - TrackPublishOptions videoOpts; - videoOpts.source = TrackSource::SOURCE_CAMERA; - videoOpts.dtx = false; - videoOpts.simulcast = true; - try { - // publishTrack takes std::shared_ptr, LocalAudioTrack derives from - // Track - room->localParticipant()->publishTrack(videoTrack, videoOpts); - - const auto videoPub = videoTrack->publication(); - - std::cout << "Published track:\n" - << " SID: " << videoPub->sid() << "\n" - << " Name: " << videoPub->name() << "\n" - << " Kind: " << static_cast(videoPub->kind()) << "\n" - << " Source: " << static_cast(videoPub->source()) << "\n" - << " Simulcasted: " << std::boolalpha << videoPub->simulcasted() - << "\n" - << " Muted: " << std::boolalpha << videoPub->muted() << "\n"; - } catch (const std::exception &e) { - LK_LOG_ERROR("Failed to publish track: {}", e.what()); - } - media.startCamera(videoSource); - - // Keep the app alive until Ctrl-C so we continue receiving events, - // similar to asyncio.run(main()) keeping the loop running. - while (g_running.load()) { - MainThreadDispatcher::update(); - media.render(); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - } - - // Shutdown the audio / video capture threads. - media.stopMic(); - media.stopCamera(); - media.stopSpeaker(); - media.shutdownRenderer(); - - // Drain any queued tasks that might still try to update the renderer / - // speaker - MainThreadDispatcher::update(); - - // Must be cleaned up before FfiClient::instance().shutdown(); - room->setDelegate(nullptr); - - if (audioTrack->publication()) { - room->localParticipant()->unpublishTrack(audioTrack->publication()->sid()); - } - if (videoTrack->publication()) { - room->localParticipant()->unpublishTrack(videoTrack->publication()->sid()); - } - audioTrack.reset(); - videoTrack.reset(); - - room.reset(); - - livekit::shutdown(); - std::cout << "Exiting.\n"; - return 0; -} diff --git a/examples/simple_rpc/README.md b/examples/simple_rpc/README.md deleted file mode 100644 index 2ded78c7..00000000 --- a/examples/simple_rpc/README.md +++ /dev/null @@ -1,157 +0,0 @@ -# πŸ“˜ SimpleRpc Example β€” Technical Overview - -This README provides deeper technical details about the RPC (Remote Procedure Call) support demonstrated in the SimpleRpc example. -It complements the example instructions found in the root README.md. - -If you're looking for how to run the example, see the root [README](https://github.com/livekit/client-sdk-cpp). - -This document explains: -- How LiveKit RPC works in the C++ SDK -- Where the APIs are defined -- How senders call RPC methods -- How receivers register handlers -- What happens if the receiver is absent -- How long-running operations behave -- Timeouts, disconnects, and unsupported methods -- RPC lifecycle events and error propagation - -## πŸ”§ Overview: How RPC Works -LiveKit RPC allows one participant (the caller) to invoke a method on another participant (the receiver) using the data channel transport. -It is: -- Peer-to-peer within the room (not server-executed RPC) -- Request/response (caller waits for a reply or an error) -- Asynchronous under the hood, synchronous or blocking from the caller’s perspective -- Delivery-guaranteed when using the reliable data channel - -Each RPC call includes: -| Field | Meaning | -|--------------------------|-----------------------------------------------------| -| **destination_identity** | Identity of the target participant | -| **method** | Method name string (e.g., "square-root") | -| **payload** | Arbitrary UTF-8 text | -| **response_timeout** | Optional timeout (seconds) | -| **invocation_id** | Server-generated ID used internally for correlation | - -## πŸ“ Location of APIs in C++ -All public-facing RPC APIs live in: -[include/livekit/local_participant.h](https://github.com/livekit/client-sdk-cpp/blob/main/include/livekit/local_participant.h#L160) - -### Key methods: - -#### Sender-side APIs -```bash -std::string performRpc( - const std::string& destination_identity, - const std::string& method, - const std::string& payload, - std::optional response_timeout_sec = std::nullopt -); - -Receiver-side APIs -void registerRpcMethod( - const std::string& method_name, - RpcHandler handler -); - -void unregisterRpcMethod(const std::string& method_name); - -Handler signature -using RpcHandler = - std::function(const RpcInvocationData&)>; -``` - -Handlers can: -- Return a string (the RPC response payload) -- Return std::nullopt (meaning β€œno return payload”) -- Throw exceptions (mapped to APPLICATION_ERROR) -- Throw a RpcError (mapped to specific error codes) - -#### πŸ›° Sender Behavior (performRpc) - -When the caller invokes: -```bash -auto reply = lp->performRpc("math-genius", "square-root", "{\"number\":16}"); -``` - -The following occurs: - -A PerformRpcRequest is sent through FFI to the SDK core. - -The SDK transmits the invocation to the target participant (if present). - -The caller begins waiting for a matching RpcMethodInvocationResponse. - -One of the following happens: -| Outcome | Meaning | -|--------------------------|------------------------------------------| -| **Success** | Receiver returned a payload | -| **UNSUPPORTED_METHOD** | Receiver did not register the method | -| **RECIPIENT_NOT_FOUND** | Target identity not present in room | -| **RECIPIENT_DISCONNECTED** | Target left before replying | -| **RESPONSE_TIMEOUT** | Receiver took too long | -| **APPLICATION_ERROR** | Handler threw an exception | - -#### πŸ”„ Round-trip time (RTT) - -The caller can measure RTT externally (as SimpleRpc does), but the SDK does not measure RTT internally. - -#### πŸ“‘ Receiver Behavior (registerRpcMethod) - -A receiver must explicitly register handlers: -```bash -local_participant->registerRpcMethod("square-root", - [](const RpcInvocationData& data) { - double number = parse(data.payload); - return make_json("result", std::sqrt(number)); - }); -``` - -When an invocation arrives: -- Room receives a RpcMethodInvocationEvent -- Room forwards it to the corresponding LocalParticipant -- LocalParticipant::handleRpcMethodInvocation(): -- Calls the handler -- Converts any exceptions into RpcError -- Sends back RpcMethodInvocationResponse - -⚠ If no handler exists: - -Receiver returns: UNSUPPORTED_METHOD - - -#### 🚨 What Happens if Receiver Is Absent? -| Case | Behavior | -|-----------------------------------------------------|---------------------------------------------------| -| Receiver identity is not in the room | Caller immediately receives `RECIPIENT_NOT_FOUND` | -| Receiver is present but disconnects before replying | Caller receives `RECIPIENT_DISCONNECTED` | -| Receiver joins later | Caller must retry manually (no automatic waiting) | - -**Important**: -LiveKit does not queue RPC calls for offline participants. - -#### ⏳ Timeout Behavior - -If the caller specifies: - -performRpc(..., /*response_timeout=*/10.0); - -Then: -- Receiver is given 10 seconds to respond. -- If the receiver handler takes longer (e.g., sleep 30s), caller receives: -RESPONSE_TIMEOUT - -**If no response_timeout is provided explicitly, the default timeout is 15 seconds.** - - -This is by design and demonstrated in the example. - -#### 🧨 Errors & Failure Modes -| Error Code | Cause | -|------------------------|---------------------------------------------| -| **APPLICATION_ERROR** | Handler threw a C++ exception | -| **UNSUPPORTED_METHOD** | No handler registered for the method | -| **RECIPIENT_NOT_FOUND** | Destination identity not in room | -| **RECIPIENT_DISCONNECTED** | Participant left mid-flight | -| **RESPONSE_TIMEOUT** | Handler exceeded allowed response time | -| **CONNECTION_TIMEOUT** | Transport-level issue | -| **SEND_FAILED** | SDK failed to send invocation | diff --git a/examples/simple_rpc/main.cpp b/examples/simple_rpc/main.cpp deleted file mode 100644 index b171f9b5..00000000 --- a/examples/simple_rpc/main.cpp +++ /dev/null @@ -1,547 +0,0 @@ -/* - * Copyright 2025 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 -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "livekit/livekit.h" - -using namespace livekit; -using namespace std::chrono_literals; - -namespace { - -std::atomic g_running{true}; - -void handleSignal(int) { g_running.store(false); } - -void printUsage(const char *prog) { - std::cerr << "Usage:\n" - << " " << prog << " [role]\n" - << "or:\n" - << " " << prog - << " --url= --token= [--role=]\n" - << " " << prog - << " --url --token [--role ]\n\n" - << "Env fallbacks:\n" - << " LIVEKIT_URL, LIVEKIT_TOKEN\n" - << "Role (participant behavior):\n" - << " SIMPLE_RPC_ROLE or --role=\n" - << " default: caller\n"; -} - -inline double nowMs() { - return std::chrono::duration( - std::chrono::steady_clock::now().time_since_epoch()) - .count(); -} - -// Poll the room until a remote participant with the given identity appears, -// or until 'timeout' elapses. Returns true if found, false on timeout. -bool waitForParticipant(Room *room, const std::string &identity, - std::chrono::milliseconds timeout) { - auto start = std::chrono::steady_clock::now(); - - while (std::chrono::steady_clock::now() - start < timeout) { - if (room->remoteParticipant(identity) != nullptr) { - return true; - } - std::this_thread::sleep_for(100ms); - } - return false; -} - -// For the caller: wait for a specific peer, and if they don't show up, -// explain why and how to start them in another terminal. -bool ensurePeerPresent(Room *room, const std::string &identity, - const std::string &friendly_role, const std::string &url, - std::chrono::seconds timeout) { - std::cout << "[Caller] Waiting up to " << timeout.count() << "s for " - << friendly_role << " (identity=\"" << identity - << "\") to join...\n"; - bool present = waitForParticipant( - room, identity, - std::chrono::duration_cast(timeout)); - if (present) { - std::cout << "[Caller] " << friendly_role << " is present.\n"; - return true; - } - // Timed out - auto info = room->room_info(); - const std::string room_name = info.name; - std::cout << "[Caller] Timed out after " << timeout.count() - << "s waiting for " << friendly_role << " (identity=\"" << identity - << "\").\n"; - std::cout << "[Caller] No participant with identity \"" << identity - << "\" appears to be connected to room \"" << room_name - << "\".\n\n"; - std::cout << "To start a " << friendly_role - << " in another terminal, run:\n\n" - << " lk token create -r test -i " << identity - << " --join --valid-for 99999h --dev --room=" << room_name << "\n" - << " ./build/examples/SimpleRpc " << url - << " $token --role=" << friendly_role << "\n\n"; - return false; -} - -bool parseArgs(int argc, char *argv[], std::string &url, std::string &token, - std::string &role) { - // --help - for (int i = 1; i < argc; ++i) { - std::string a = argv[i]; - if (a == "-h" || a == "--help") { - return false; - } - } - - // helper for flags - auto get_flag_value = [&](const std::string &name, int &i) -> std::string { - std::string arg = argv[i]; - const std::string eq = name + "="; - if (arg.rfind(name, 0) == 0) { // starts with name - if (arg.size() > name.size() && arg[name.size()] == '=') { - return arg.substr(eq.size()); - } else if (i + 1 < argc) { - return std::string(argv[++i]); - } - } - return {}; - }; - - // flags: --url / --token / --role (with = or split) - for (int i = 1; i < argc; ++i) { - const std::string a = argv[i]; - if (a.rfind("--url", 0) == 0) { - auto v = get_flag_value("--url", i); - if (!v.empty()) - url = v; - } else if (a.rfind("--token", 0) == 0) { - auto v = get_flag_value("--token", i); - if (!v.empty()) - token = v; - } else if (a.rfind("--role", 0) == 0) { - auto v = get_flag_value("--role", i); - if (!v.empty()) - role = v; - } - } - - std::vector pos; - for (int i = 1; i < argc; ++i) { - std::string a = argv[i]; - if (a.rfind("--", 0) == 0) - continue; - pos.push_back(std::move(a)); - } - if (!pos.empty()) { - if (url.empty() && pos.size() >= 1) { - url = pos[0]; - } - if (token.empty() && pos.size() >= 2) { - token = pos[1]; - } - if (role.empty() && pos.size() >= 3) { - role = pos[2]; - } - } - - if (url.empty()) { - const char *e = std::getenv("LIVEKIT_URL"); - if (e) - url = e; - } - if (token.empty()) { - const char *e = std::getenv("LIVEKIT_TOKEN"); - if (e) - token = e; - } - if (role.empty()) { - const char *e = std::getenv("SIMPLE_RPC_ROLE"); - if (e) - role = e; - } - if (role.empty()) { - role = "caller"; - } - - return !(url.empty() || token.empty()); -} - -std::string makeNumberJson(const std::string &key, double value) { - std::ostringstream oss; - oss << "{\"" << key << "\":" << value << "}"; - return oss.str(); -} - -std::string makeStringJson(const std::string &key, const std::string &value) { - std::ostringstream oss; - oss << "{\"" << key << "\":\"" << value << "\"}"; - return oss.str(); -} - -double parseNumberFromJson(const std::string &json) { - auto colon = json.find(':'); - if (colon == std::string::npos) - throw std::runtime_error("invalid json: " + json); - auto start = colon + 1; - auto end = json.find_first_of(",}", start); - std::string num_str = json.substr(start, end - start); - return std::stod(num_str); -} - -std::string parseStringFromJson(const std::string &json) { - auto colon = json.find(':'); - if (colon == std::string::npos) - throw std::runtime_error("invalid json: " + json); - auto first_quote = json.find('"', colon + 1); - if (first_quote == std::string::npos) - throw std::runtime_error("invalid json: " + json); - auto second_quote = json.find('"', first_quote + 1); - if (second_quote == std::string::npos) - throw std::runtime_error("invalid json: " + json); - return json.substr(first_quote + 1, second_quote - first_quote - 1); -} - -// RPC handler registration -void registerReceiverMethods(Room *greeters_room, Room *math_genius_room) { - LocalParticipant *greeter_lp = greeters_room->localParticipant(); - LocalParticipant *math_genius_lp = math_genius_room->localParticipant(); - - // arrival - greeter_lp->registerRpcMethod( - "arrival", - [](const RpcInvocationData &data) -> std::optional { - std::cout << "[Greeter] Oh " << data.caller_identity - << " arrived and said \"" << data.payload << "\"\n"; - std::this_thread::sleep_for(2s); - return std::optional{"Welcome and have a wonderful day!"}; - }); - - // square-root - math_genius_lp->registerRpcMethod( - "square-root", - [](const RpcInvocationData &data) -> std::optional { - double number = parseNumberFromJson(data.payload); - std::cout << "[Math Genius] I guess " << data.caller_identity - << " wants the square root of " << number - << ". I've only got " << data.response_timeout_sec - << " seconds to respond but I think I can pull it off.\n"; - std::cout << "[Math Genius] *doing math*…\n"; - std::this_thread::sleep_for(2s); - double result = std::sqrt(number); - std::cout << "[Math Genius] Aha! It's " << result << "\n"; - return makeNumberJson("result", result); - }); - - // divide - math_genius_lp->registerRpcMethod( - "divide", - [](const RpcInvocationData &data) -> std::optional { - // expect {"dividend":X,"divisor":Y} – we'll parse very lazily - auto div_pos = data.payload.find("dividend"); - auto dvr_pos = data.payload.find("divisor"); - if (div_pos == std::string::npos || dvr_pos == std::string::npos) { - throw std::runtime_error("invalid divide payload"); - } - - double dividend = parseNumberFromJson( - data.payload.substr(div_pos, dvr_pos - div_pos - 1)); // rough slice - double divisor = parseNumberFromJson(data.payload.substr(dvr_pos)); - - std::cout << "[Math Genius] " << data.caller_identity - << " wants to divide " << dividend << " by " << divisor - << ".\n"; - - if (divisor == 0.0) { - // will be translated to APPLICATION_ERROR by your RpcError logic - throw std::runtime_error("division by zero"); - } - - double result = dividend / divisor; - return makeNumberJson("result", result); - }); - - // long-calculation - math_genius_lp->registerRpcMethod( - "long-calculation", - [](const RpcInvocationData &data) -> std::optional { - std::cout << "[Math Genius] Starting a very long calculation for " - << data.caller_identity << "\n"; - std::cout << "[Math Genius] This will take 30 seconds even though " - "you're only giving me " - << data.response_timeout_sec << " seconds\n"; - // Sleep for 30 seconds to mimic a long running task. - std::this_thread::sleep_for(30s); - return makeStringJson("result", "Calculation complete!"); - }); - - // Note: we do NOT register "quantum-hypergeometric-series" here, - // so the caller sees UNSUPPORTED_METHOD -} - -void performGreeting(Room *room) { - std::cout << "[Caller] Letting the greeter know that I've arrived\n"; - double t0 = nowMs(); - try { - std::string response = room->localParticipant()->performRpc( - "greeter", "arrival", "Hello", std::nullopt); - double t1 = nowMs(); - std::cout << "[Caller] RTT: " << (t1 - t0) << " ms\n"; - std::cout << "[Caller] That's nice, the greeter said: \"" << response - << "\"\n"; - } catch (const std::exception &error) { - double t1 = nowMs(); - std::cout << "[Caller] (FAILED) RTT: " << (t1 - t0) << " ms\n"; - std::cout << "[Caller] RPC call failed: " << error.what() << "\n"; - throw; - } -} - -void performSquareRoot(Room *room) { - std::cout << "[Caller] What's the square root of 16?\n"; - double t0 = nowMs(); - try { - std::string payload = makeNumberJson("number", 16.0); - std::string response = room->localParticipant()->performRpc( - "math-genius", "square-root", payload, std::nullopt); - double t1 = nowMs(); - std::cout << "[Caller] RTT: " << (t1 - t0) << " ms\n"; - double result = parseNumberFromJson(response); - std::cout << "[Caller] Nice, the answer was " << result << "\n"; - } catch (const std::exception &error) { - double t1 = nowMs(); - std::cout << "[Caller] (FAILED) RTT: " << (t1 - t0) << " ms\n"; - std::cout << "[Caller] RPC call failed: " << error.what() << "\n"; - throw; - } -} - -void performQuantumHyperGeometricSeries(Room *room) { - std::cout << "\n=== Unsupported Method Example ===\n"; - std::cout - << "[Caller] Asking math-genius for 'quantum-hypergeometric-series'. " - "This should FAIL because the handler is NOT registered.\n"; - double t0 = nowMs(); - try { - std::string payload = makeNumberJson("number", 42.0); - std::string response = room->localParticipant()->performRpc( - "math-genius", "quantum-hypergeometric-series", payload, std::nullopt); - double t1 = nowMs(); - std::cout << "[Caller] (Unexpected success) RTT=" << (t1 - t0) << " ms\n"; - std::cout << "[Caller] Result: " << response << "\n"; - } catch (const RpcError &error) { - double t1 = nowMs(); - std::cout << "[Caller] RpcError RTT=" << (t1 - t0) << " ms\n"; - auto code = static_cast(error.code()); - if (code == RpcError::ErrorCode::UNSUPPORTED_METHOD) { - std::cout << "[Caller] βœ“ Expected: math-genius does NOT implement this " - "method.\n"; - std::cout << "[Caller] Server returned UNSUPPORTED_METHOD.\n"; - } else { - std::cout << "[Caller] βœ— Unexpected error type: " << error.message() - << "\n"; - } - } -} - -void performDivide(Room *room) { - std::cout << "\n=== Divide Example ===\n"; - std::cout << "[Caller] Asking math-genius to divide 10 by 0. " - "This is EXPECTED to FAIL with an APPLICATION_ERROR.\n"; - double t0 = nowMs(); - try { - std::string payload = "{\"dividend\":10,\"divisor\":0}"; - std::string response = room->localParticipant()->performRpc( - "math-genius", "divide", payload, std::nullopt); - double t1 = nowMs(); - std::cout << "[Caller] (Unexpected success) RTT=" << (t1 - t0) << " ms\n"; - std::cout << "[Caller] Result = " << response << "\n"; - } catch (const RpcError &error) { - double t1 = nowMs(); - std::cout << "[Caller] RpcError RTT=" << (t1 - t0) << " ms\n"; - auto code = static_cast(error.code()); - if (code == RpcError::ErrorCode::APPLICATION_ERROR) { - std::cout << "[Caller] βœ“ Expected: divide-by-zero triggers " - "APPLICATION_ERROR.\n"; - std::cout << "[Caller] Math-genius threw an exception: " - << error.message() << "\n"; - } else { - std::cout << "[Caller] βœ— Unexpected RpcError type: " << error.message() - << "\n"; - } - } -} - -void performLongCalculation(Room *room) { - std::cout << "\n=== Long Calculation Example ===\n"; - std::cout - << "[Caller] Asking math-genius for a calculation that takes 30s.\n"; - std::cout - << "[Caller] Giving only 10s to respond. EXPECTED RESULT: TIMEOUT.\n"; - double t0 = nowMs(); - try { - std::string response = room->localParticipant()->performRpc( - "math-genius", "long-calculation", "{}", 10.0); - double t1 = nowMs(); - std::cout << "[Caller] (Unexpected success) RTT=" << (t1 - t0) << " ms\n"; - std::cout << "[Caller] Result: " << response << "\n"; - } catch (const RpcError &error) { - double t1 = nowMs(); - std::cout << "[Caller] RpcError RTT=" << (t1 - t0) << " ms\n"; - auto code = static_cast(error.code()); - if (code == RpcError::ErrorCode::RESPONSE_TIMEOUT) { - std::cout - << "[Caller] βœ“ Expected: handler sleeps 30s but timeout is 10s.\n"; - std::cout << "[Caller] Server correctly returned RESPONSE_TIMEOUT.\n"; - } else if (code == RpcError::ErrorCode::RECIPIENT_DISCONNECTED) { - std::cout << "[Caller] βœ“ Expected if math-genius disconnects during the " - "test.\n"; - } else { - std::cout << "[Caller] βœ— Unexpected RPC error: " << error.message() - << "\n"; - } - } -} - -} // namespace - -int main(int argc, char *argv[]) { - std::string url, token, role; - if (!parseArgs(argc, argv, url, token, role)) { - printUsage(argv[0]); - return 1; - } - - if (url.empty() || token.empty()) { - std::cerr << "LIVEKIT_URL and LIVEKIT_TOKEN (or CLI args) are required\n"; - return 1; - } - - std::cout << "Connecting to: " << url << "\n"; - std::cout << "Role: " << role << "\n"; - - // Ctrl-C to quit the program - std::signal(SIGINT, handleSignal); - - // Initialize the livekit with logging to console. - livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); - auto room = std::make_unique(); - RoomOptions options; - options.auto_subscribe = true; - options.dynacast = false; - - bool res = room->Connect(url, token, options); - std::cout << "Connect result is " << std::boolalpha << res << "\n"; - if (!res) { - std::cerr << "Failed to connect to room\n"; - livekit::shutdown(); - return 1; - } - - auto info = room->room_info(); - std::cout << "Connected to room:\n" - << " Name: " << info.name << "\n" - << " Metadata: " << info.metadata << "\n" - << " Num participants: " << info.num_participants << "\n"; - - try { - if (role == "caller") { - // Check that both peers are present (or explain how to start them). - bool has_greeter = - ensurePeerPresent(room.get(), "greeter", "greeter", url, 8s); - bool has_math_genius = - ensurePeerPresent(room.get(), "math-genius", "math-genius", url, 8s); - if (!has_greeter || !has_math_genius) { - std::cout << "\n[Caller] One or more RPC peers are missing. " - << "Some examples may be skipped.\n"; - } - if (has_greeter) { - std::cout << "\n\nRunning greeting example...\n"; - performGreeting(room.get()); - } else { - std::cout << "[Caller] Skipping greeting example because greeter is " - "not present.\n"; - } - if (has_math_genius) { - std::cout << "\n\nRunning error handling example...\n"; - performDivide(room.get()); - - std::cout << "\n\nRunning math example...\n"; - performSquareRoot(room.get()); - std::this_thread::sleep_for(2s); - performQuantumHyperGeometricSeries(room.get()); - - std::cout << "\n\nRunning long calculation with timeout...\n"; - performLongCalculation(room.get()); - } else { - std::cout << "[Caller] Skipping math examples because math-genius is " - "not present.\n"; - } - - std::cout << "\n\nCaller done. Exiting.\n"; - } else if (role == "greeter" || role == "math-genius") { - // For these roles we expect multiple processes: - // - One process with role=caller - // - One with role=greeter - // - One with role=math-genius - // - // Each process gets its own token (with that identity) via LIVEKIT_TOKEN. - // Here we only register handlers for the appropriate role, and then - // stay alive until Ctrl-C so we can receive RPCs. - - if (role == "greeter") { - // Use the same room object for both arguments; only "arrival" is used. - registerReceiverMethods(room.get(), room.get()); - } else { // math-genius - // We only need math handlers; greeter handlers won't be used. - registerReceiverMethods(room.get(), room.get()); - } - - std::cout << "RPC handlers registered for role=" << role - << ". Waiting for RPC calls (Ctrl-C to exit)...\n"; - - while (g_running.load()) { - std::this_thread::sleep_for(50ms); - } - std::cout << "Exiting receiver role.\n"; - } else { - std::cerr << "Unknown role: " << role << "\n"; - } - } catch (const std::exception &e) { - std::cerr << "Unexpected error in main: " << e.what() << "\n"; - } - - // It is important to clean up the delegate and room in order. - room->setDelegate(nullptr); - room.reset(); - livekit::shutdown(); - return 0; -} diff --git a/examples/tokens/README.md b/examples/tokens/README.md deleted file mode 100644 index ebed99c1..00000000 --- a/examples/tokens/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Overview -Examples of generating tokens - -## gen_and_set.bash -Generate tokens and then set them as env vars for the current terminal session - -## set_data_track_test_tokens.bash -Generate tokens for data track integration tests and set them as env vars for the current terminal session. \ No newline at end of file diff --git a/examples/tokens/gen_and_set.bash b/examples/tokens/gen_and_set.bash deleted file mode 100755 index b933a24f..00000000 --- a/examples/tokens/gen_and_set.bash +++ /dev/null @@ -1,169 +0,0 @@ -#!/usr/bin/env bash -# Copyright 2026 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. - -# Generate a LiveKit access token via `lk` and set LIVEKIT_TOKEN (and LIVEKIT_URL) -# for your current shell session. -# -# source examples/tokens/gen_and_set.bash --id PARTICIPANT_ID --room ROOM_NAME [--view-token] -# eval "$(bash examples/tokens/gen_and_set.bash --id ID --room ROOM [--view-token])" -# -# Optional env: LIVEKIT_API_KEY, LIVEKIT_API_SECRET, LIVEKIT_VALID_FOR. - -# When sourced, we must NOT enable errexit/pipefail on the interactive shell β€” a -# failing pipeline (e.g. sed|head SIGPIPE) or any error would close your terminal. - -_sourced=0 -if [[ -n "${BASH_VERSION:-}" ]] && [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then - _sourced=1 -elif [[ -n "${ZSH_VERSION:-}" ]] && [[ "${ZSH_EVAL_CONTEXT:-}" == *:file* ]]; then - _sourced=1 -fi - -_fail() { - echo "gen_and_set.bash: $1" >&2 - if [[ "$_sourced" -eq 1 ]]; then - return "${2:-1}" - fi - exit "${2:-1}" -} - -_usage() { - echo "Usage: ${0##*/} --id PARTICIPANT_IDENTITY --room ROOM_NAME [--view-token]" >&2 - echo " --id LiveKit participant identity (required)" >&2 - echo " --room Room name (required; not read from env)" >&2 - echo " --view-token Print the JWT to stderr after generating" >&2 -} - -if [[ "$_sourced" -eq 0 ]]; then - set -euo pipefail -fi - -_view_token=0 -LIVEKIT_IDENTITY="" -LIVEKIT_ROOM="robo_room" -while [[ $# -gt 0 ]]; do - case "$1" in - --view-token) - _view_token=1 - shift - ;; - --id) - if [[ $# -lt 2 ]]; then - _usage - _fail "--id requires a value" 2 - fi - LIVEKIT_IDENTITY="$2" - shift 2 - ;; - --room) - if [[ $# -lt 2 ]]; then - _usage - _fail "--room requires a value" 2 - fi - LIVEKIT_ROOM="$2" - shift 2 - ;; - -h | --help) - _usage - if [[ "$_sourced" -eq 1 ]]; then - return 0 - fi - exit 0 - ;; - *) - _usage - _fail "unknown argument: $1" 2 - ;; - esac -done - -if [[ -z "$LIVEKIT_IDENTITY" ]]; then - _usage - _fail "--id is required" 2 -fi -if [[ -z "$LIVEKIT_ROOM" ]]; then - _usage - _fail "--room is required" 2 -fi - -LIVEKIT_API_KEY="${LIVEKIT_API_KEY:-devkey}" -LIVEKIT_API_SECRET="${LIVEKIT_API_SECRET:-secret}" -LIVEKIT_VALID_FOR="${LIVEKIT_VALID_FOR:-99999h}" -_grant_json='{"canPublish":true,"canSubscribe":true,"canPublishData":true}' - -if ! command -v lk >/dev/null 2>&1; then - _fail "'lk' CLI not found. Install: https://docs.livekit.io/home/cli/" 2 -fi - -# Run lk inside bash so --grant JSON (with embedded ") is safe when this file is -# sourced from zsh; zsh misparses --grant "$json" on the same line. -_out="$( - bash -c ' - lk token create \ - --api-key "$1" \ - --api-secret "$2" \ - -i "$3" \ - --join \ - --valid-for "$4" \ - --room "$5" \ - --grant "$6" 2>&1 - ' _ "$LIVEKIT_API_KEY" "$LIVEKIT_API_SECRET" "$LIVEKIT_IDENTITY" \ - "$LIVEKIT_VALID_FOR" "$LIVEKIT_ROOM" "$_grant_json" -)" -_lk_st=$? -if [[ "$_lk_st" -ne 0 ]]; then - echo "$_out" >&2 - _fail "lk token create failed" 1 -fi - -# Avoid sed|head pipelines (pipefail + SIGPIPE can kill a sourced shell). -LIVEKIT_TOKEN="" -LIVEKIT_URL="" -while IFS= read -r _line || [[ -n "${_line}" ]]; do - if [[ "$_line" == "Access token: "* ]]; then - LIVEKIT_TOKEN="${_line#Access token: }" - elif [[ "$_line" == "Project URL: "* ]]; then - LIVEKIT_URL="${_line#Project URL: }" - fi -done <<< "$_out" - -if [[ -z "$LIVEKIT_TOKEN" ]]; then - echo "gen_and_set.bash: could not parse Access token from lk output:" >&2 - echo "$_out" >&2 - _fail "missing Access token line" 1 -fi - -if [[ "$_view_token" -eq 1 ]]; then - echo "$LIVEKIT_TOKEN" >&2 -fi - -_apply() { - export LIVEKIT_TOKEN - export LIVEKIT_URL -} - -_emit_eval() { - printf 'export LIVEKIT_TOKEN=%q\n' "$LIVEKIT_TOKEN" - [[ -n "$LIVEKIT_URL" ]] && printf 'export LIVEKIT_URL=%q\n' "$LIVEKIT_URL" -} - -if [[ "$_sourced" -eq 1 ]]; then - _apply - echo "LIVEKIT_TOKEN and LIVEKIT_URL set for this shell." >&2 - [[ -n "$LIVEKIT_URL" ]] || echo "gen_and_set.bash: warning: no Project URL in output; set LIVEKIT_URL manually." >&2 -else - _emit_eval - echo "gen_and_set.bash: for this shell run: source $0 --id ... --room ... or: eval \"\$(bash $0 ...)\"" >&2 -fi diff --git a/examples/tokens/set_integration_test_tokens.bash b/examples/tokens/set_integration_test_tokens.bash deleted file mode 100755 index 8a46ca42..00000000 --- a/examples/tokens/set_integration_test_tokens.bash +++ /dev/null @@ -1,138 +0,0 @@ -#!/usr/bin/env bash -# Copyright 2026 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. - -# Generate two LiveKit access tokens via `lk` and set the environment variables -# required by src/tests/integration/test_data_track.cpp and src/tests/integration/test_rpc.cpp. -# -# source examples/tokens/set_integration_test_tokens.bash -# eval "$(bash examples/tokens/set_integration_test_tokens.bash)" -# -# Exports: -# LK_TOKEN_TEST_A -# LK_TOKEN_TEST_B -# LIVEKIT_URL=ws://localhost:7880 -# - -_sourced=0 -if [[ -n "${BASH_VERSION:-}" ]] && [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then - _sourced=1 -elif [[ -n "${ZSH_VERSION:-}" ]] && [[ "${ZSH_EVAL_CONTEXT:-}" == *:file* ]]; then - _sourced=1 -fi - -_fail() { - echo "set_integration_test_tokens.bash: $1" >&2 - if [[ "$_sourced" -eq 1 ]]; then - return "${2:-1}" - fi - exit "${2:-1}" -} - -if [[ "$_sourced" -eq 0 ]]; then - set -euo pipefail -fi - -# data track -LIVEKIT_ROOM="cpp_data_track_test" -LIVEKIT_IDENTITY_A="cpp-test-a" -LIVEKIT_IDENTITY_B="cpp-test-b" - -# rpc -LIVEKIT_RPC_ROOM="rpc-test-room" -LIVEKIT_CALLER_TOKEN="caller" -LIVEKIT_RECEIVER_TOKEN="receiver" - -if [[ $# -ne 0 ]]; then - _fail "this script is hard-coded and does not accept arguments" 2 -fi - -LIVEKIT_API_KEY="devkey" -LIVEKIT_API_SECRET="secret" -LIVEKIT_VALID_FOR="99999h" -LIVEKIT_URL="ws://localhost:7880" -_grant_json='{"canPublish":true,"canSubscribe":true,"canPublishData":true}' - -if ! command -v lk >/dev/null 2>&1; then - _fail "'lk' CLI not found. Install: https://docs.livekit.io/home/cli/" 2 -fi - -_create_token() { - local identity="$1" - local output="" - local command_status=0 - local token="" - - output="$( - bash -c ' - lk token create \ - --api-key "$1" \ - --api-secret "$2" \ - -i "$3" \ - --join \ - --valid-for "$4" \ - --room "$5" \ - --grant "$6" 2>&1 - ' _ "$LIVEKIT_API_KEY" "$LIVEKIT_API_SECRET" "$identity" \ - "$LIVEKIT_VALID_FOR" "$LIVEKIT_ROOM" "$_grant_json" - )" - command_status=$? - if [[ "$command_status" -ne 0 ]]; then - echo "$output" >&2 - _fail "lk token create failed for identity '$identity'" 1 - fi - - while IFS= read -r line || [[ -n "${line}" ]]; do - if [[ "$line" == "Access token: "* ]]; then - token="${line#Access token: }" - break - fi - done <<< "$output" - - if [[ -z "$token" ]]; then - echo "$output" >&2 - _fail "could not parse Access token for identity '$identity'" 1 - fi - - printf '%s' "$token" -} - -LK_TOKEN_TEST_A="$(_create_token "$LIVEKIT_IDENTITY_A")" -LK_TOKEN_TEST_B="$(_create_token "$LIVEKIT_IDENTITY_B")" -LK_TOKEN_RPC_CALLER="$(_create_token "$LIVEKIT_CALLER_TOKEN")" -LK_TOKEN_RPC_RECEIVER="$(_create_token "$LIVEKIT_RECEIVER_TOKEN")" - -_apply() { - export LK_TOKEN_TEST_A - export LK_TOKEN_TEST_B - export LK_TOKEN_RPC_CALLER - export LK_TOKEN_RPC_RECEIVER - export LIVEKIT_URL -} - -_emit_eval() { - printf 'export LK_TOKEN_TEST_A=%q\n' "$LK_TOKEN_TEST_A" - printf 'export LK_TOKEN_TEST_B=%q\n' "$LK_TOKEN_TEST_B" - printf 'export LK_TOKEN_RPC_CALLER=%q\n' "$LK_TOKEN_RPC_CALLER" - printf 'export LK_TOKEN_RPC_RECEIVER=%q\n' "$LK_TOKEN_RPC_RECEIVER" - printf 'export LIVEKIT_URL=%q\n' "$LIVEKIT_URL" -} - -if [[ "$_sourced" -eq 1 ]]; then - _apply - echo "LK_TOKEN_TEST_A, LK_TOKEN_TEST_B, LK_TOKEN_RPC_CALLER, LK_TOKEN_RPC_RECEIVER, and LIVEKIT_URL set for this shell." >&2 -else - _emit_eval - echo "set_integration_test_tokens.bash: for this shell run: source $0 or: eval \"\$(bash $0 ...)\"" >&2 -fi diff --git a/include/livekit/lk_log.h b/src/lk_log.h similarity index 100% rename from include/livekit/lk_log.h rename to src/lk_log.h From 43a77eed1562365c4f0e911097df03a17da9a02a Mon Sep 17 00:00:00 2001 From: Stephen DeRosa Date: Tue, 7 Apr 2026 21:15:55 -0600 Subject: [PATCH 5/6] make spdlog private --- CMakeLists.txt | 3 +-- cmake/LiveKitConfig.cmake.in | 3 --- cmake/spdlog.cmake | 2 +- include/livekit/livekit.h | 1 - src/audio_source.cpp | 2 +- src/data_stream.cpp | 2 +- src/data_track_stream.cpp | 2 +- src/ffi_client.cpp | 2 +- src/livekit.cpp | 2 +- src/local_data_track.cpp | 2 +- src/room.cpp | 2 +- src/subscription_thread_dispatcher.cpp | 2 +- src/tests/CMakeLists.txt | 1 + src/tests/integration/test_logging.cpp | 2 +- src/video_frame.cpp | 2 +- src/video_stream.cpp | 2 +- 16 files changed, 14 insertions(+), 18 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 86984eda..2ea1a353 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -378,9 +378,8 @@ target_include_directories(livekit ) target_link_libraries(livekit - PUBLIC - spdlog::spdlog PRIVATE + spdlog::spdlog livekit_ffi ${LIVEKIT_PROTOBUF_TARGET} ) diff --git a/cmake/LiveKitConfig.cmake.in b/cmake/LiveKitConfig.cmake.in index 90b1a7e1..b5805e4d 100644 --- a/cmake/LiveKitConfig.cmake.in +++ b/cmake/LiveKitConfig.cmake.in @@ -1,7 +1,4 @@ @PACKAGE_INIT@ -include(CMakeFindDependencyMacro) -find_dependency(spdlog) - include("${CMAKE_CURRENT_LIST_DIR}/LiveKitTargets.cmake") diff --git a/cmake/spdlog.cmake b/cmake/spdlog.cmake index 25117dd3..b0956b00 100644 --- a/cmake/spdlog.cmake +++ b/cmake/spdlog.cmake @@ -71,7 +71,7 @@ FetchContent_Declare( set(SPDLOG_BUILD_SHARED OFF CACHE BOOL "" FORCE) set(SPDLOG_BUILD_EXAMPLE OFF CACHE BOOL "" FORCE) set(SPDLOG_BUILD_TESTS OFF CACHE BOOL "" FORCE) -set(SPDLOG_INSTALL ON CACHE BOOL "" FORCE) +set(SPDLOG_INSTALL OFF CACHE BOOL "" FORCE) FetchContent_MakeAvailable(livekit_spdlog) diff --git a/include/livekit/livekit.h b/include/livekit/livekit.h index 7d055725..d6f3ec01 100644 --- a/include/livekit/livekit.h +++ b/include/livekit/livekit.h @@ -22,7 +22,6 @@ #include "audio_stream.h" #include "build.h" #include "e2ee.h" -#include "lk_log.h" #include "local_audio_track.h" #include "local_participant.h" #include "local_track_publication.h" diff --git a/src/audio_source.cpp b/src/audio_source.cpp index bc648e50..4e1a48a1 100644 --- a/src/audio_source.cpp +++ b/src/audio_source.cpp @@ -24,7 +24,7 @@ #include "ffi.pb.h" #include "ffi_client.h" #include "livekit/audio_frame.h" -#include "livekit/lk_log.h" +#include "lk_log.h" namespace livekit { diff --git a/src/data_stream.cpp b/src/data_stream.cpp index 9841a902..4db13120 100644 --- a/src/data_stream.cpp +++ b/src/data_stream.cpp @@ -6,7 +6,7 @@ #include #include "ffi_client.h" -#include "livekit/lk_log.h" +#include "lk_log.h" #include "livekit/local_participant.h" #include "room.pb.h" diff --git a/src/data_track_stream.cpp b/src/data_track_stream.cpp index 7a0a6d30..4a23d74d 100644 --- a/src/data_track_stream.cpp +++ b/src/data_track_stream.cpp @@ -19,7 +19,7 @@ #include "data_track.pb.h" #include "ffi.pb.h" #include "ffi_client.h" -#include "livekit/lk_log.h" +#include "lk_log.h" #include diff --git a/src/ffi_client.cpp b/src/ffi_client.cpp index e431fa5d..9c206911 100644 --- a/src/ffi_client.cpp +++ b/src/ffi_client.cpp @@ -24,7 +24,7 @@ #include "livekit/data_track_error.h" #include "livekit/e2ee.h" #include "livekit/ffi_handle.h" -#include "livekit/lk_log.h" +#include "lk_log.h" #include "livekit/room.h" #include "livekit/rpc_error.h" #include "livekit/track.h" diff --git a/src/livekit.cpp b/src/livekit.cpp index 86b9ebd6..b9132efc 100644 --- a/src/livekit.cpp +++ b/src/livekit.cpp @@ -16,7 +16,7 @@ #include "livekit/livekit.h" #include "ffi_client.h" -#include "livekit/lk_log.h" +#include "lk_log.h" namespace livekit { diff --git a/src/local_data_track.cpp b/src/local_data_track.cpp index 3d183699..d0cc8e42 100644 --- a/src/local_data_track.cpp +++ b/src/local_data_track.cpp @@ -17,7 +17,7 @@ #include "livekit/local_data_track.h" #include "livekit/data_track_error.h" -#include "livekit/lk_log.h" +#include "lk_log.h" #include "data_track.pb.h" #include "ffi.pb.h" diff --git a/src/room.cpp b/src/room.cpp index 0ee28ba7..919e126d 100644 --- a/src/room.cpp +++ b/src/room.cpp @@ -32,7 +32,7 @@ #include "data_track.pb.h" #include "ffi.pb.h" #include "ffi_client.h" -#include "livekit/lk_log.h" +#include "lk_log.h" #include "livekit_ffi.h" #include "room.pb.h" #include "room_proto_converter.h" diff --git a/src/subscription_thread_dispatcher.cpp b/src/subscription_thread_dispatcher.cpp index 1a32bc56..ee683650 100644 --- a/src/subscription_thread_dispatcher.cpp +++ b/src/subscription_thread_dispatcher.cpp @@ -18,7 +18,7 @@ #include "livekit/data_track_frame.h" #include "livekit/data_track_stream.h" -#include "livekit/lk_log.h" +#include "lk_log.h" #include "livekit/remote_data_track.h" #include "livekit/track.h" diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 1b9695eb..1ca804d3 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -41,6 +41,7 @@ if(INTEGRATION_TEST_SOURCES) target_link_libraries(livekit_integration_tests PRIVATE livekit + spdlog::spdlog GTest::gtest_main GTest::gmock ) diff --git a/src/tests/integration/test_logging.cpp b/src/tests/integration/test_logging.cpp index 5ccb0fa1..88e16166 100644 --- a/src/tests/integration/test_logging.cpp +++ b/src/tests/integration/test_logging.cpp @@ -17,7 +17,7 @@ #include #include -#include "livekit/lk_log.h" +#include "lk_log.h" #include #include diff --git a/src/video_frame.cpp b/src/video_frame.cpp index 271c5ce5..5fdc83e2 100644 --- a/src/video_frame.cpp +++ b/src/video_frame.cpp @@ -6,7 +6,7 @@ #include #include "livekit/ffi_handle.h" -#include "livekit/lk_log.h" +#include "lk_log.h" #include "video_utils.h" namespace livekit { diff --git a/src/video_stream.cpp b/src/video_stream.cpp index 657d4db1..ed65da9e 100644 --- a/src/video_stream.cpp +++ b/src/video_stream.cpp @@ -4,7 +4,7 @@ #include "ffi.pb.h" #include "ffi_client.h" -#include "livekit/lk_log.h" +#include "lk_log.h" #include "livekit/track.h" #include "video_frame.pb.h" #include "video_utils.h" From a690af1dc18b8d38b07c139172d55a36930efc53 Mon Sep 17 00:00:00 2001 From: Stephen DeRosa Date: Tue, 7 Apr 2026 21:45:06 -0600 Subject: [PATCH 6/6] examples: LK_LOG -> std::cout/cerr --- examples/CMakeLists.txt | 470 +++++++++++++ examples/bridge_human_robot/human.cpp | 416 +++++++++++ examples/bridge_human_robot/robot.cpp | 661 ++++++++++++++++++ examples/bridge_mute_unmute/README.md | 94 +++ examples/bridge_mute_unmute/caller.cpp | 337 +++++++++ examples/bridge_mute_unmute/receiver.cpp | 266 +++++++ examples/bridge_rpc/README.md | 105 +++ examples/bridge_rpc/custom_caller.cpp | 122 ++++ examples/bridge_rpc/custom_receiver.cpp | 113 +++ examples/cmake/sdl3.cmake | 52 ++ examples/common/sdl_media.cpp | 227 ++++++ examples/common/sdl_media.h | 128 ++++ examples/common/sdl_media_manager.cpp | 403 +++++++++++ examples/common/sdl_media_manager.h | 109 +++ examples/common/sdl_video_renderer.cpp | 177 +++++ examples/common/sdl_video_renderer.h | 53 ++ examples/common/wav_audio_source.cpp | 162 +++++ examples/common/wav_audio_source.h | 56 ++ examples/hello_livekit/receiver.cpp | 131 ++++ examples/hello_livekit/sender.cpp | 144 ++++ examples/logging_levels/README.md | 165 +++++ examples/logging_levels/basic_usage.cpp | 162 +++++ examples/logging_levels/custom_sinks.cpp | 292 ++++++++ examples/ping_pong/constants.h | 36 + examples/ping_pong/json_converters.cpp | 69 ++ examples/ping_pong/json_converters.h | 31 + examples/ping_pong/messages.h | 48 ++ examples/ping_pong/ping.cpp | 210 ++++++ examples/ping_pong/pong.cpp | 148 ++++ examples/ping_pong/utils.h | 45 ++ examples/simple_data_stream/main.cpp | 284 ++++++++ examples/simple_joystick/json_utils.cpp | 46 ++ examples/simple_joystick/json_utils.h | 38 + examples/simple_joystick/receiver.cpp | 126 ++++ examples/simple_joystick/sender.cpp | 268 +++++++ examples/simple_joystick/utils.cpp | 87 +++ examples/simple_joystick/utils.h | 31 + examples/simple_room/fallback_capture.cpp | 119 ++++ examples/simple_room/fallback_capture.h | 35 + examples/simple_room/main.cpp | 417 +++++++++++ examples/simple_rpc/README.md | 157 +++++ examples/simple_rpc/main.cpp | 547 +++++++++++++++ examples/tokens/README.md | 8 + examples/tokens/gen_and_set.bash | 169 +++++ .../tokens/set_integration_test_tokens.bash | 138 ++++ 45 files changed, 7902 insertions(+) create mode 100644 examples/CMakeLists.txt create mode 100644 examples/bridge_human_robot/human.cpp create mode 100644 examples/bridge_human_robot/robot.cpp create mode 100644 examples/bridge_mute_unmute/README.md create mode 100644 examples/bridge_mute_unmute/caller.cpp create mode 100644 examples/bridge_mute_unmute/receiver.cpp create mode 100644 examples/bridge_rpc/README.md create mode 100644 examples/bridge_rpc/custom_caller.cpp create mode 100644 examples/bridge_rpc/custom_receiver.cpp create mode 100644 examples/cmake/sdl3.cmake create mode 100644 examples/common/sdl_media.cpp create mode 100644 examples/common/sdl_media.h create mode 100644 examples/common/sdl_media_manager.cpp create mode 100644 examples/common/sdl_media_manager.h create mode 100644 examples/common/sdl_video_renderer.cpp create mode 100644 examples/common/sdl_video_renderer.h create mode 100644 examples/common/wav_audio_source.cpp create mode 100644 examples/common/wav_audio_source.h create mode 100644 examples/hello_livekit/receiver.cpp create mode 100644 examples/hello_livekit/sender.cpp create mode 100644 examples/logging_levels/README.md create mode 100644 examples/logging_levels/basic_usage.cpp create mode 100644 examples/logging_levels/custom_sinks.cpp create mode 100644 examples/ping_pong/constants.h create mode 100644 examples/ping_pong/json_converters.cpp create mode 100644 examples/ping_pong/json_converters.h create mode 100644 examples/ping_pong/messages.h create mode 100644 examples/ping_pong/ping.cpp create mode 100644 examples/ping_pong/pong.cpp create mode 100644 examples/ping_pong/utils.h create mode 100644 examples/simple_data_stream/main.cpp create mode 100644 examples/simple_joystick/json_utils.cpp create mode 100644 examples/simple_joystick/json_utils.h create mode 100644 examples/simple_joystick/receiver.cpp create mode 100644 examples/simple_joystick/sender.cpp create mode 100644 examples/simple_joystick/utils.cpp create mode 100644 examples/simple_joystick/utils.h create mode 100644 examples/simple_room/fallback_capture.cpp create mode 100644 examples/simple_room/fallback_capture.h create mode 100644 examples/simple_room/main.cpp create mode 100644 examples/simple_rpc/README.md create mode 100644 examples/simple_rpc/main.cpp create mode 100644 examples/tokens/README.md create mode 100755 examples/tokens/gen_and_set.bash create mode 100755 examples/tokens/set_integration_test_tokens.bash diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt new file mode 100644 index 00000000..5398e26d --- /dev/null +++ b/examples/CMakeLists.txt @@ -0,0 +1,470 @@ +cmake_minimum_required(VERSION 3.20) +project(livekit-examples) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Set RPATH for examples to find shared libraries in executable directory +# This ensures SDL3 and other shared libs are loaded from bin/ not _deps/ +if(UNIX) + if(APPLE) + set(CMAKE_BUILD_RPATH "@loader_path;@loader_path/../lib") + set(CMAKE_INSTALL_RPATH "@loader_path;@loader_path/../lib") + else() + set(CMAKE_BUILD_RPATH "$ORIGIN:$ORIGIN/../lib") + set(CMAKE_INSTALL_RPATH "$ORIGIN:$ORIGIN/../lib") + endif() + set(CMAKE_BUILD_RPATH_USE_ORIGIN TRUE) + set(CMAKE_SKIP_BUILD_RPATH FALSE) + set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE) +endif() + +# Always use FetchContent for SDL3 (vcpkg doesn't have it in stable baseline) +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") +include(sdl3) + +# Common include directories for examples that need private headers +# TODO: These should be refactored to use only public headers +set(EXAMPLES_PRIVATE_INCLUDE_DIRS + ${LIVEKIT_ROOT_DIR}/src + ${LIVEKIT_BINARY_DIR}/generated +) + +# Shared example code (SDL media, wav source, etc.) +set(EXAMPLES_COMMON_DIR ${CMAKE_CURRENT_SOURCE_DIR}/common) + +# All example executables (used for copying livekit_ffi DLL/shared lib) +set(EXAMPLES_ALL + SimpleRoom + SimpleRpc + SimpleJoystickSender + SimpleJoystickReceiver + SimpleDataStream + PingPongPing + PingPongPong + HelloLivekitSender + HelloLivekitReceiver + LoggingLevelsBasicUsage + LoggingLevelsCustomSinks + BridgeRobot + BridgeHuman + BridgeMuteCaller + BridgeMuteReceiver + BridgeRpcCaller + BridgeRpcReceiver +) + +# Bridge examples (need livekit_bridge DLL/shared lib in addition to livekit_ffi) +set(EXAMPLES_BRIDGE + BridgeRobot + BridgeMuteCaller + BridgeMuteReceiver + BridgeRpcCaller + BridgeRpcReceiver +) + +# Examples that use SDL3 (need SDL3 lib copied on Linux; SimpleRoom is handled separately above) +set(EXAMPLES_NEED_SDL3 + BridgeRobot + BridgeHuman + BridgeMuteCaller + BridgeMuteReceiver +) + +add_executable(SimpleRoom + simple_room/main.cpp + simple_room/fallback_capture.cpp + simple_room/fallback_capture.h + ${EXAMPLES_COMMON_DIR}/sdl_media.cpp + ${EXAMPLES_COMMON_DIR}/sdl_media.h + ${EXAMPLES_COMMON_DIR}/sdl_media_manager.cpp + ${EXAMPLES_COMMON_DIR}/sdl_media_manager.h + ${EXAMPLES_COMMON_DIR}/sdl_video_renderer.cpp + ${EXAMPLES_COMMON_DIR}/sdl_video_renderer.h + ${EXAMPLES_COMMON_DIR}/wav_audio_source.cpp + ${EXAMPLES_COMMON_DIR}/wav_audio_source.h +) + +target_include_directories(SimpleRoom PRIVATE + ${EXAMPLES_PRIVATE_INCLUDE_DIRS} + ${EXAMPLES_COMMON_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/simple_room +) + +target_link_libraries(SimpleRoom + PRIVATE + livekit + SDL3::SDL3 +) + +add_custom_command(TARGET SimpleRoom POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${LIVEKIT_ROOT_DIR}/data + ${CMAKE_CURRENT_BINARY_DIR}/data +) + +# Copy SDL3 shared library to SimpleRoom output directory +# On Linux, we also need to create the SONAME symlink (libSDL3.so.0 -> libSDL3.so.0.x.x) +# macOS doesn't need SONAME symlink (dylib versioning works differently) +if(UNIX AND NOT APPLE) + add_custom_command(TARGET SimpleRoom POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "$" + "$" + COMMAND ${CMAKE_COMMAND} -E create_symlink + "$" + "$/$" + COMMENT "Copying SDL3 shared library and SONAME symlink to SimpleRoom output directory" + VERBATIM + ) +else() + # Windows and macOS: just copy the library file + add_custom_command(TARGET SimpleRoom POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "$" + "$" + COMMENT "Copying SDL3 shared library to SimpleRoom output directory" + VERBATIM + ) +endif() + +find_package(nlohmann_json CONFIG QUIET) +if(NOT nlohmann_json_FOUND) + include(FetchContent) + FetchContent_Declare( + nlohmann_json + URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz + ) + FetchContent_MakeAvailable(nlohmann_json) +endif() + +add_executable(SimpleRpc + simple_rpc/main.cpp +) + +target_include_directories(SimpleRpc PRIVATE ${EXAMPLES_PRIVATE_INCLUDE_DIRS}) + +target_link_libraries(SimpleRpc + PRIVATE + nlohmann_json::nlohmann_json + livekit +) + +# --- SimpleJoystick example (sender + receiver executables with shared json_utils) --- + +add_library(simple_joystick_json_utils STATIC + simple_joystick/json_utils.cpp + simple_joystick/json_utils.h + simple_joystick/utils.cpp + simple_joystick/utils.h +) + +target_include_directories(simple_joystick_json_utils PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/simple_joystick +) + +target_link_libraries(simple_joystick_json_utils + PUBLIC + nlohmann_json::nlohmann_json +) + +add_executable(SimpleJoystickReceiver + simple_joystick/receiver.cpp +) + +target_include_directories(SimpleJoystickReceiver PRIVATE ${EXAMPLES_PRIVATE_INCLUDE_DIRS}) + +target_link_libraries(SimpleJoystickReceiver + PRIVATE + simple_joystick_json_utils + livekit +) + +add_executable(SimpleJoystickSender + simple_joystick/sender.cpp +) + +target_include_directories(SimpleJoystickSender PRIVATE ${EXAMPLES_PRIVATE_INCLUDE_DIRS}) + +target_link_libraries(SimpleJoystickSender + PRIVATE + simple_joystick_json_utils + livekit +) + +# --- LoggingLevelsBasicUsage example --- + +add_executable(LoggingLevelsBasicUsage + logging_levels/basic_usage.cpp +) + +target_include_directories(LoggingLevelsBasicUsage PRIVATE ${EXAMPLES_PRIVATE_INCLUDE_DIRS}) + +# Internal LK_LOG_* macros + lk_log.h require spdlog and the same compile-time +# log level as the main SDK (SPDLOG_ACTIVE_LEVEL). +target_link_libraries(LoggingLevelsBasicUsage + PRIVATE + livekit + spdlog::spdlog +) +target_compile_definitions(LoggingLevelsBasicUsage + PRIVATE + SPDLOG_ACTIVE_LEVEL=${_SPDLOG_ACTIVE_LEVEL} +) + +# --- LoggingLevelsCustomSinks example --- + +add_executable(LoggingLevelsCustomSinks + logging_levels/custom_sinks.cpp +) + +target_link_libraries(LoggingLevelsCustomSinks + PRIVATE + livekit +) + +# --- SimpleDataStream example --- + +add_executable(SimpleDataStream + simple_data_stream/main.cpp +) + +target_include_directories(SimpleDataStream PRIVATE ${EXAMPLES_PRIVATE_INCLUDE_DIRS}) + +target_link_libraries(SimpleDataStream + PRIVATE + livekit +) + +add_custom_command( + TARGET SimpleDataStream + POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + ${LIVEKIT_ROOT_DIR}/data + $/data +) + +# --- ping_pong (request/response latency measurement over data tracks) --- + +add_library(ping_pong_support STATIC + ping_pong/json_converters.cpp + ping_pong/json_converters.h + ping_pong/constants.h + ping_pong/messages.h + ping_pong/utils.h +) + +target_include_directories(ping_pong_support PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/ping_pong +) + +target_link_libraries(ping_pong_support + PRIVATE + nlohmann_json::nlohmann_json +) + +add_executable(PingPongPing + ping_pong/ping.cpp +) + +target_include_directories(PingPongPing PRIVATE ${EXAMPLES_PRIVATE_INCLUDE_DIRS}) + +target_link_libraries(PingPongPing + PRIVATE + ping_pong_support + livekit +) + +add_executable(PingPongPong + ping_pong/pong.cpp +) + +target_include_directories(PingPongPong PRIVATE ${EXAMPLES_PRIVATE_INCLUDE_DIRS}) + +target_link_libraries(PingPongPong + PRIVATE + ping_pong_support + livekit +) + +# --- hello_livekit (minimal synthetic video + data publish / subscribe) --- + +add_executable(HelloLivekitSender + hello_livekit/sender.cpp +) + +target_include_directories(HelloLivekitSender PRIVATE ${EXAMPLES_PRIVATE_INCLUDE_DIRS}) + +target_link_libraries(HelloLivekitSender + PRIVATE + livekit +) + +add_executable(HelloLivekitReceiver + hello_livekit/receiver.cpp +) + +target_include_directories(HelloLivekitReceiver PRIVATE ${EXAMPLES_PRIVATE_INCLUDE_DIRS}) + +target_link_libraries(HelloLivekitReceiver + PRIVATE + livekit +) + +# --- bridge_human_robot examples (robot + human; use livekit_bridge and SDL3) --- + +add_executable(BridgeRobot + bridge_human_robot/robot.cpp + ${EXAMPLES_COMMON_DIR}/sdl_media.cpp + ${EXAMPLES_COMMON_DIR}/sdl_media.h +) +target_include_directories(BridgeRobot PRIVATE + ${EXAMPLES_PRIVATE_INCLUDE_DIRS} + ${EXAMPLES_COMMON_DIR} +) +target_link_libraries(BridgeRobot PRIVATE livekit_bridge SDL3::SDL3) + +add_executable(BridgeHuman + bridge_human_robot/human.cpp + ${EXAMPLES_COMMON_DIR}/sdl_media.cpp + ${EXAMPLES_COMMON_DIR}/sdl_media.h +) +target_include_directories(BridgeHuman PRIVATE + ${EXAMPLES_PRIVATE_INCLUDE_DIRS} + ${EXAMPLES_COMMON_DIR} +) +target_link_libraries(BridgeHuman PRIVATE livekit SDL3::SDL3) + +# --- bridge_rpc examples (headless custom RPC caller + receiver) --- + +add_executable(BridgeRpcCaller + bridge_rpc/custom_caller.cpp +) +target_link_libraries(BridgeRpcCaller PRIVATE livekit_bridge) + +add_executable(BridgeRpcReceiver + bridge_rpc/custom_receiver.cpp +) +target_link_libraries(BridgeRpcReceiver PRIVATE livekit_bridge) + +# --- bridge_mute_unmute examples (caller uses SDL3 for A/V playback; receiver is headless) --- + +add_executable(BridgeMuteCaller + bridge_mute_unmute/caller.cpp + ${EXAMPLES_COMMON_DIR}/sdl_media.cpp + ${EXAMPLES_COMMON_DIR}/sdl_media.h +) +target_include_directories(BridgeMuteCaller PRIVATE + ${EXAMPLES_PRIVATE_INCLUDE_DIRS} + ${EXAMPLES_COMMON_DIR} +) +target_link_libraries(BridgeMuteCaller PRIVATE livekit_bridge SDL3::SDL3) + +add_executable(BridgeMuteReceiver + bridge_mute_unmute/receiver.cpp + ${EXAMPLES_COMMON_DIR}/sdl_media.cpp + ${EXAMPLES_COMMON_DIR}/sdl_media.h +) +target_include_directories(BridgeMuteReceiver PRIVATE + ${EXAMPLES_PRIVATE_INCLUDE_DIRS} + ${EXAMPLES_COMMON_DIR} +) +target_link_libraries(BridgeMuteReceiver PRIVATE livekit_bridge SDL3::SDL3) + +# Copy SDL3 shared library to bridge example output directories +if(UNIX AND NOT APPLE) + foreach(_target ${EXAMPLES_NEED_SDL3}) + add_custom_command(TARGET ${_target} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "$" + "$" + COMMAND ${CMAKE_COMMAND} -E create_symlink + "$" + "$/$" + COMMENT "Copying SDL3 shared library and SONAME symlink to ${_target} output directory" + VERBATIM + ) + endforeach() +else() + foreach(_target ${EXAMPLES_NEED_SDL3}) + add_custom_command(TARGET ${_target} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "$" + "$" + COMMENT "Copying SDL3 shared library to ${_target} output directory" + VERBATIM + ) + endforeach() +endif() + +# Windows: Copy required DLLs to examples output directory (single copy to avoid parallel POST_BUILD races) +if(WIN32) + set(REQUIRED_DLLS "livekit_ffi.dll") + + # One copy of each DLL to bin/; all examples depend on this target + add_custom_command(OUTPUT ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/livekit_ffi.dll + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $/livekit_ffi.dll + ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/livekit_ffi.dll + DEPENDS livekit + COMMENT "Copying livekit_ffi.dll to examples output directory" + ) + add_custom_target(copy_ffi_dll_to_bin ALL DEPENDS ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/livekit_ffi.dll) + foreach(EXAMPLE ${EXAMPLES_ALL}) + add_dependencies(${EXAMPLE} copy_ffi_dll_to_bin) + endforeach() + + # Bridge examples also need livekit_bridge.dll (single copy to bin/) + set(BRIDGE_DLL "livekit_bridge.dll") + add_custom_command(OUTPUT ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${BRIDGE_DLL} + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ + ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${BRIDGE_DLL} + DEPENDS livekit_bridge + COMMENT "Copying livekit_bridge DLL to examples output directory" + ) + add_custom_target(copy_bridge_dll_to_bin ALL DEPENDS ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${BRIDGE_DLL}) + foreach(EXAMPLE ${EXAMPLES_BRIDGE}) + add_dependencies(${EXAMPLE} copy_bridge_dll_to_bin) + endforeach() +endif() + +# Linux/macOS: Copy shared library to examples output directory (single copy to avoid parallel POST_BUILD races) +if(UNIX) + if(APPLE) + set(FFI_SHARED_LIB "liblivekit_ffi.dylib") + else() + set(FFI_SHARED_LIB "liblivekit_ffi.so") + endif() + + # One copy to bin/; all examples depend on this target so the copy runs once + add_custom_command(OUTPUT ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${FFI_SHARED_LIB} + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $/${FFI_SHARED_LIB} + ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${FFI_SHARED_LIB} + DEPENDS livekit + COMMENT "Copying ${FFI_SHARED_LIB} to examples output directory" + ) + add_custom_target(copy_ffi_to_bin ALL DEPENDS ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${FFI_SHARED_LIB}) + foreach(EXAMPLE ${EXAMPLES_ALL}) + add_dependencies(${EXAMPLE} copy_ffi_to_bin) + endforeach() + + # Bridge examples also need livekit_bridge shared library (single copy to bin/) + if(APPLE) + set(BRIDGE_SHARED_LIB "liblivekit_bridge.dylib") + else() + set(BRIDGE_SHARED_LIB "liblivekit_bridge.so") + endif() + add_custom_command(OUTPUT ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${BRIDGE_SHARED_LIB} + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ + ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${BRIDGE_SHARED_LIB} + DEPENDS livekit_bridge + COMMENT "Copying livekit_bridge to examples output directory" + ) + add_custom_target(copy_bridge_to_bin ALL DEPENDS ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${BRIDGE_SHARED_LIB}) + foreach(EXAMPLE ${EXAMPLES_BRIDGE}) + add_dependencies(${EXAMPLE} copy_bridge_to_bin) + endforeach() +endif() diff --git a/examples/bridge_human_robot/human.cpp b/examples/bridge_human_robot/human.cpp new file mode 100644 index 00000000..714a77ae --- /dev/null +++ b/examples/bridge_human_robot/human.cpp @@ -0,0 +1,416 @@ +/* + * Copyright 2025 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. + */ + +/* + * Human example -- receives audio and video frames from a robot in a + * LiveKit room and renders them using SDL3. + * + * This example demonstrates the base SDK's convenience frame callback API + * (Room::setOnAudioFrameCallback / Room::setOnVideoFrameCallback) which + * eliminates the need for a RoomDelegate subclass, manual AudioStream/ + * VideoStream creation, and reader threads. + * + * The robot publishes two video tracks and two audio tracks: + * - "robot-cam" (SOURCE_CAMERA) -- webcam or placeholder + * - "robot-sim-frame" (SOURCE_SCREENSHARE) -- simulated diagnostic + * frame + * - "robot-mic" (SOURCE_MICROPHONE) -- real microphone or + * silence + * - "robot-sim-audio" (SOURCE_SCREENSHARE_AUDIO) -- simulated siren tone + * + * Press 'w' to play the webcam feed + real mic, or 's' for sim frame + siren. + * The selection controls both video and audio simultaneously. + * + * Usage: + * human [--no-audio] + * LIVEKIT_URL=... LIVEKIT_TOKEN=... human [--no-audio] + * + * --no-audio Subscribe to audio tracks but suppress local playback. + * + * The token must grant identity "human". Generate one with: + * lk token create --api-key --api-secret \ + * --join --room my-room --identity human \ + * --valid-for 24h + * + * Run alongside the "robot" example (which publishes with identity "robot"). + */ + +#include "livekit/audio_frame.h" +#include "livekit/livekit.h" +#include "livekit/track.h" +#include "livekit/video_frame.h" +#include "sdl_media.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static std::atomic g_running{true}; +static void handleSignal(int) { g_running.store(false); } + +// ---- Video source selection ---- +enum class SelectedSource : int { Webcam = 0, SimFrame = 1 }; +static std::atomic g_selected_source{ + static_cast(SelectedSource::Webcam)}; + +// ---- Thread-safe video frame slot ---- +struct LatestVideoFrame { + std::mutex mutex; + std::vector data; + int width = 0; + int height = 0; + bool dirty = false; +}; + +static LatestVideoFrame g_latest_video; + +static void renderFrame(const livekit::VideoFrame &frame) { + const std::uint8_t *src = frame.data(); + const std::size_t size = frame.dataSize(); + if (!src || size == 0) + return; + + std::lock_guard lock(g_latest_video.mutex); + g_latest_video.data.assign(src, src + size); + g_latest_video.width = frame.width(); + g_latest_video.height = frame.height(); + g_latest_video.dirty = true; +} + +// ---- Counters for periodic status ---- +static std::atomic g_audio_frames{0}; +static std::atomic g_video_frames{0}; + +constexpr const char *kRobotMicTrackName = "robot-mic"; +constexpr const char *kRobotSimAudioTrackName = "robot-sim-audio"; +constexpr const char *kRobotCamTrackName = "robot-cam"; +constexpr const char *kRobotSimVideoTrackName = "robot-sim-frame"; + +int main(int argc, char *argv[]) { + // ----- Parse args / env ----- + bool no_audio = false; + std::vector positional; + for (int i = 1; i < argc; ++i) { + if (std::string(argv[i]) == "--no-audio") { + no_audio = true; + } else { + positional.push_back(argv[i]); + } + } + + std::string url, token; + auto is_ws_url = [](const std::string &s) { + return (s.size() >= 5 && s.compare(0, 5, "ws://") == 0) || + (s.size() >= 6 && s.compare(0, 6, "wss://") == 0); + }; + if (positional.size() >= 2) { + for (const auto &arg : positional) { + if (is_ws_url(arg)) { + url = arg; + break; + } + } + for (const auto &arg : positional) { + if (arg != url) { + token = arg; + break; + } + } + if (url.empty()) + url = positional[0], token = positional[1]; + } else { + const char *e = std::getenv("LIVEKIT_URL"); + if (e) + url = e; + e = std::getenv("LIVEKIT_TOKEN"); + if (e) + token = e; + } + if (url.empty() || token.empty()) { + std::cerr + << "Usage: human [--no-audio] \n" + << " or: LIVEKIT_URL=... LIVEKIT_TOKEN=... human [--no-audio]\n"; + return 1; + } + if (no_audio) { + std::cout << "[human] --no-audio: audio playback disabled.\n"; + } + + std::signal(SIGINT, handleSignal); + + // ----- Initialize SDL3 ----- + if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) { + std::cerr << "[human] SDL_Init failed: " << SDL_GetError() << "\n"; + return 1; + } + + // ----- Create SDL window + renderer ----- + constexpr int kWindowWidth = 1280; + constexpr int kWindowHeight = 720; + + SDL_Window *window = SDL_CreateWindow("Human - Robot Camera Feed", + kWindowWidth, kWindowHeight, 0); + if (!window) { + std::cerr << "[human] SDL_CreateWindow failed: " << SDL_GetError() << "\n"; + SDL_Quit(); + return 1; + } + + SDL_Renderer *renderer = SDL_CreateRenderer(window, nullptr); + if (!renderer) { + std::cerr << "[human] SDL_CreateRenderer failed: " << SDL_GetError() + << "\n"; + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + SDL_Texture *texture = nullptr; + int tex_width = 0; + int tex_height = 0; + + // ----- SDL speaker for audio playback ----- + std::unique_ptr speaker; + std::mutex speaker_mutex; + + // ----- Connect to LiveKit using the base SDK ----- + livekit::initialize(); + + auto room = std::make_unique(); + std::cout << "[human] Connecting to " << url << " ...\n"; + livekit::RoomOptions options; + options.auto_subscribe = true; + if (!room->Connect(url, token, options)) { + std::cerr << "[human] Failed to connect.\n"; + livekit::shutdown(); + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + std::cout << "[human] Connected. Waiting for robot...\n"; + + // Helper: enqueue audio to the speaker (lazily initializes on first call) + auto playAudio = [&speaker, + &speaker_mutex](const livekit::AudioFrame &frame) { + const auto &samples = frame.data(); + if (samples.empty()) + return; + + std::lock_guard lock(speaker_mutex); + + if (!speaker) { + speaker = std::make_unique(frame.sample_rate(), + frame.num_channels()); + if (!speaker->init()) { + std::cerr << "[human] Failed to init SDL speaker.\n"; + speaker.reset(); + return; + } + std::cout << "[human] Speaker opened: " << frame.sample_rate() << " Hz, " + << frame.num_channels() << " ch.\n"; + } + + speaker->enqueue(samples.data(), frame.samples_per_channel()); + }; + + // ----- Set audio callbacks using Room::setOnAudioFrameCallback ----- + room->setOnAudioFrameCallback( + "robot", kRobotMicTrackName, + [playAudio, no_audio](const livekit::AudioFrame &frame) { + g_audio_frames.fetch_add(1, std::memory_order_relaxed); + if (!no_audio && g_selected_source.load(std::memory_order_relaxed) == + static_cast(SelectedSource::Webcam)) { + playAudio(frame); + } + }); + + room->setOnAudioFrameCallback( + "robot", kRobotSimAudioTrackName, + [playAudio, no_audio](const livekit::AudioFrame &frame) { + g_audio_frames.fetch_add(1, std::memory_order_relaxed); + if (!no_audio && g_selected_source.load(std::memory_order_relaxed) == + static_cast(SelectedSource::SimFrame)) { + playAudio(frame); + } + }); + + // ----- Set video callbacks using Room::setOnVideoFrameCallback ----- + room->setOnVideoFrameCallback( + "robot", kRobotCamTrackName, + [](const livekit::VideoFrame &frame, std::int64_t /*timestamp_us*/) { + g_video_frames.fetch_add(1, std::memory_order_relaxed); + if (g_selected_source.load(std::memory_order_relaxed) == + static_cast(SelectedSource::Webcam)) { + renderFrame(frame); + } + }); + + room->setOnVideoFrameCallback( + "robot", kRobotSimVideoTrackName, + [](const livekit::VideoFrame &frame, std::int64_t /*timestamp_us*/) { + g_video_frames.fetch_add(1, std::memory_order_relaxed); + if (g_selected_source.load(std::memory_order_relaxed) == + static_cast(SelectedSource::SimFrame)) { + renderFrame(frame); + } + }); + + // ----- Stdin input thread ----- + std::thread input_thread([&]() { + std::string line; + while (g_running.load() && std::getline(std::cin, line)) { + if (line == "w" || line == "W") { + g_selected_source.store(static_cast(SelectedSource::Webcam), + std::memory_order_relaxed); + std::cout << "[human] Switched to webcam + mic.\n"; + } else if (line == "s" || line == "S") { + g_selected_source.store(static_cast(SelectedSource::SimFrame), + std::memory_order_relaxed); + std::cout << "[human] Switched to sim frame + siren.\n"; + } + } + }); + + // ----- Main loop ----- + std::cout + << "[human] Rendering robot feed. Press 'w' for webcam + mic, " + "'s' for sim frame + siren (in this terminal or the SDL window). " + "Ctrl-C or close window to stop.\n"; + + auto last_report = std::chrono::steady_clock::now(); + + std::vector local_pixels; + + while (g_running.load()) { + SDL_Event ev; + while (SDL_PollEvent(&ev)) { + if (ev.type == SDL_EVENT_QUIT) { + g_running.store(false); + } else if (ev.type == SDL_EVENT_KEY_DOWN) { + if (ev.key.key == SDLK_W) { + g_selected_source.store(static_cast(SelectedSource::Webcam), + std::memory_order_relaxed); + std::cout << "[human] Switched to webcam + mic.\n"; + } else if (ev.key.key == SDLK_S) { + g_selected_source.store(static_cast(SelectedSource::SimFrame), + std::memory_order_relaxed); + std::cout << "[human] Switched to sim frame + siren.\n"; + } + } + } + + int fw = 0, fh = 0; + bool have_frame = false; + { + std::lock_guard lock(g_latest_video.mutex); + if (g_latest_video.dirty && g_latest_video.width > 0 && + g_latest_video.height > 0) { + fw = g_latest_video.width; + fh = g_latest_video.height; + local_pixels.swap(g_latest_video.data); + g_latest_video.dirty = false; + have_frame = true; + } + } + + if (have_frame) { + if (fw != tex_width || fh != tex_height) { + if (texture) + SDL_DestroyTexture(texture); + texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA32, + SDL_TEXTUREACCESS_STREAMING, fw, fh); + if (!texture) { + std::cerr << "[human] SDL_CreateTexture failed: " << SDL_GetError() + << "\n"; + } + tex_width = fw; + tex_height = fh; + } + + if (texture) { + void *pixels = nullptr; + int pitch = 0; + if (SDL_LockTexture(texture, nullptr, &pixels, &pitch)) { + const int srcPitch = fw * 4; + for (int y = 0; y < fh; ++y) { + std::memcpy(static_cast(pixels) + y * pitch, + local_pixels.data() + y * srcPitch, srcPitch); + } + SDL_UnlockTexture(texture); + } + } + } + + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); + SDL_RenderClear(renderer); + if (texture) { + SDL_RenderTexture(renderer, texture, nullptr, nullptr); + } + SDL_RenderPresent(renderer); + + auto now = std::chrono::steady_clock::now(); + if (now - last_report >= std::chrono::seconds(5)) { + last_report = now; + const char *src_name = + g_selected_source.load(std::memory_order_relaxed) == + static_cast(SelectedSource::Webcam) + ? "webcam" + : "sim_frame"; + std::cout << "[human] Status: " << g_audio_frames.load() + << " audio frames, " << g_video_frames.load() + << " video frames received (showing: " << src_name << ").\n"; + } + + SDL_Delay(16); + } + + // ----- Cleanup ----- + std::cout << "[human] Shutting down...\n"; + std::cout << "[human] Total received: " << g_audio_frames.load() + << " audio frames, " << g_video_frames.load() << " video frames.\n"; + + if (input_thread.joinable()) + input_thread.detach(); + + // Room destructor calls stopAllReaders() which closes streams and joins + // reader threads, then tears down FFI state. + room.reset(); + livekit::shutdown(); + + { + std::lock_guard lock(speaker_mutex); + speaker.reset(); + } + + if (texture) + SDL_DestroyTexture(texture); + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); + SDL_Quit(); + + std::cout << "[human] Done.\n"; + return 0; +} diff --git a/examples/bridge_human_robot/robot.cpp b/examples/bridge_human_robot/robot.cpp new file mode 100644 index 00000000..f3e2f021 --- /dev/null +++ b/examples/bridge_human_robot/robot.cpp @@ -0,0 +1,661 @@ +/* + * Copyright 2025 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. + */ + +/* + * Robot example -- streams real webcam video and microphone audio to a + * LiveKit room using SDL3 for hardware capture. + * + * Usage: + * robot [--no-mic] + * LIVEKIT_URL=... LIVEKIT_TOKEN=... robot [--no-mic] + * + * --no-mic Do not create robot-mic audio track (no SDL audio init, no mic + * capture). + * + * The token must grant identity "robot". Generate one with: + * lk token create --api-key --api-secret \ + * --join --room my-room --identity robot \ + * --valid-for 24h + * + * Run alongside the "human" example (which displays the robot's feed). + */ + +#include "livekit/audio_frame.h" +#include "livekit/track.h" +#include "livekit/video_frame.h" +#include "livekit_bridge/livekit_bridge.h" +#include "sdl_media.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ---- Minimal 5x7 bitmap font for rendering text into RGBA buffers ---- +// Each glyph is 5 columns wide, 7 rows tall, stored as 7 bytes (one per row, +// MSB = leftmost pixel). Only printable ASCII 0x20..0x7E are defined. +namespace bitmap_font { + +constexpr int kGlyphW = 5; +constexpr int kGlyphH = 7; + +// clang-format off +static const std::uint8_t kGlyphs[][kGlyphH] = { + // 0x20 ' ' + {0x00,0x00,0x00,0x00,0x00,0x00,0x00}, + // 0x21 '!' + {0x20,0x20,0x20,0x20,0x00,0x20,0x00}, + // 0x22 '"' + {0x50,0x50,0x00,0x00,0x00,0x00,0x00}, + // 0x23 '#' + {0x50,0xF8,0x50,0x50,0xF8,0x50,0x00}, + // 0x24 '$' + {0x20,0x78,0xA0,0x70,0x28,0xF0,0x20}, + // 0x25 '%' + {0xC8,0xC8,0x10,0x20,0x48,0x98,0x00}, + // 0x26 '&' + {0x40,0xA0,0x40,0xA8,0x90,0x68,0x00}, + // 0x27 '\'' + {0x20,0x20,0x00,0x00,0x00,0x00,0x00}, + // 0x28 '(' + {0x10,0x20,0x40,0x40,0x20,0x10,0x00}, + // 0x29 ')' + {0x40,0x20,0x10,0x10,0x20,0x40,0x00}, + // 0x2A '*' + {0x00,0x50,0x20,0xF8,0x20,0x50,0x00}, + // 0x2B '+' + {0x00,0x20,0x20,0xF8,0x20,0x20,0x00}, + // 0x2C ',' + {0x00,0x00,0x00,0x00,0x20,0x20,0x40}, + // 0x2D '-' + {0x00,0x00,0x00,0xF8,0x00,0x00,0x00}, + // 0x2E '.' + {0x00,0x00,0x00,0x00,0x00,0x20,0x00}, + // 0x2F '/' + {0x08,0x08,0x10,0x20,0x40,0x80,0x00}, + // 0x30 '0' + {0x70,0x88,0x98,0xA8,0xC8,0x70,0x00}, + // 0x31 '1' + {0x20,0x60,0x20,0x20,0x20,0x70,0x00}, + // 0x32 '2' + {0x70,0x88,0x08,0x30,0x40,0xF8,0x00}, + // 0x33 '3' + {0x70,0x88,0x30,0x08,0x88,0x70,0x00}, + // 0x34 '4' + {0x10,0x30,0x50,0x90,0xF8,0x10,0x00}, + // 0x35 '5' + {0xF8,0x80,0xF0,0x08,0x08,0xF0,0x00}, + // 0x36 '6' + {0x30,0x40,0xF0,0x88,0x88,0x70,0x00}, + // 0x37 '7' + {0xF8,0x08,0x10,0x20,0x20,0x20,0x00}, + // 0x38 '8' + {0x70,0x88,0x70,0x88,0x88,0x70,0x00}, + // 0x39 '9' + {0x70,0x88,0x88,0x78,0x10,0x60,0x00}, + // 0x3A ':' + {0x00,0x00,0x20,0x00,0x20,0x00,0x00}, + // 0x3B ';' + {0x00,0x00,0x20,0x00,0x20,0x20,0x40}, + // 0x3C '<' + {0x08,0x10,0x20,0x40,0x20,0x10,0x08}, + // 0x3D '=' + {0x00,0x00,0xF8,0x00,0xF8,0x00,0x00}, + // 0x3E '>' + {0x80,0x40,0x20,0x10,0x20,0x40,0x80}, + // 0x3F '?' + {0x70,0x88,0x10,0x20,0x00,0x20,0x00}, + // 0x40 '@' + {0x70,0x88,0xB8,0xB8,0x80,0x70,0x00}, + // 0x41 'A' + {0x70,0x88,0x88,0xF8,0x88,0x88,0x00}, + // 0x42 'B' + {0xF0,0x88,0xF0,0x88,0x88,0xF0,0x00}, + // 0x43 'C' + {0x70,0x88,0x80,0x80,0x88,0x70,0x00}, + // 0x44 'D' + {0xF0,0x88,0x88,0x88,0x88,0xF0,0x00}, + // 0x45 'E' + {0xF8,0x80,0xF0,0x80,0x80,0xF8,0x00}, + // 0x46 'F' + {0xF8,0x80,0xF0,0x80,0x80,0x80,0x00}, + // 0x47 'G' + {0x70,0x88,0x80,0xB8,0x88,0x70,0x00}, + // 0x48 'H' + {0x88,0x88,0xF8,0x88,0x88,0x88,0x00}, + // 0x49 'I' + {0x70,0x20,0x20,0x20,0x20,0x70,0x00}, + // 0x4A 'J' + {0x08,0x08,0x08,0x08,0x88,0x70,0x00}, + // 0x4B 'K' + {0x88,0x90,0xA0,0xC0,0xA0,0x90,0x88}, + // 0x4C 'L' + {0x80,0x80,0x80,0x80,0x80,0xF8,0x00}, + // 0x4D 'M' + {0x88,0xD8,0xA8,0x88,0x88,0x88,0x00}, + // 0x4E 'N' + {0x88,0xC8,0xA8,0x98,0x88,0x88,0x00}, + // 0x4F 'O' + {0x70,0x88,0x88,0x88,0x88,0x70,0x00}, + // 0x50 'P' + {0xF0,0x88,0x88,0xF0,0x80,0x80,0x00}, + // 0x51 'Q' + {0x70,0x88,0x88,0xA8,0x90,0x68,0x00}, + // 0x52 'R' + {0xF0,0x88,0x88,0xF0,0xA0,0x90,0x00}, + // 0x53 'S' + {0x70,0x80,0x70,0x08,0x88,0x70,0x00}, + // 0x54 'T' + {0xF8,0x20,0x20,0x20,0x20,0x20,0x00}, + // 0x55 'U' + {0x88,0x88,0x88,0x88,0x88,0x70,0x00}, + // 0x56 'V' + {0x88,0x88,0x88,0x50,0x50,0x20,0x00}, + // 0x57 'W' + {0x88,0x88,0x88,0xA8,0xA8,0x50,0x00}, + // 0x58 'X' + {0x88,0x50,0x20,0x20,0x50,0x88,0x00}, + // 0x59 'Y' + {0x88,0x50,0x20,0x20,0x20,0x20,0x00}, + // 0x5A 'Z' + {0xF8,0x10,0x20,0x40,0x80,0xF8,0x00}, + // 0x5B '[' + {0x70,0x40,0x40,0x40,0x40,0x70,0x00}, + // 0x5C '\\' + {0x80,0x40,0x20,0x10,0x08,0x08,0x00}, + // 0x5D ']' + {0x70,0x10,0x10,0x10,0x10,0x70,0x00}, + // 0x5E '^' + {0x20,0x50,0x88,0x00,0x00,0x00,0x00}, + // 0x5F '_' + {0x00,0x00,0x00,0x00,0x00,0xF8,0x00}, + // 0x60 '`' + {0x40,0x20,0x00,0x00,0x00,0x00,0x00}, + // 0x61 'a' + {0x00,0x70,0x08,0x78,0x88,0x78,0x00}, + // 0x62 'b' + {0x80,0x80,0xF0,0x88,0x88,0xF0,0x00}, + // 0x63 'c' + {0x00,0x70,0x80,0x80,0x80,0x70,0x00}, + // 0x64 'd' + {0x08,0x08,0x78,0x88,0x88,0x78,0x00}, + // 0x65 'e' + {0x00,0x70,0x88,0xF8,0x80,0x70,0x00}, + // 0x66 'f' + {0x30,0x40,0xF0,0x40,0x40,0x40,0x00}, + // 0x67 'g' + {0x00,0x78,0x88,0x78,0x08,0x70,0x00}, + // 0x68 'h' + {0x80,0x80,0xF0,0x88,0x88,0x88,0x00}, + // 0x69 'i' + {0x20,0x00,0x60,0x20,0x20,0x70,0x00}, + // 0x6A 'j' + {0x10,0x00,0x30,0x10,0x10,0x10,0x60}, + // 0x6B 'k' + {0x80,0x90,0xA0,0xC0,0xA0,0x90,0x00}, + // 0x6C 'l' + {0x60,0x20,0x20,0x20,0x20,0x70,0x00}, + // 0x6D 'm' + {0x00,0xD0,0xA8,0xA8,0x88,0x88,0x00}, + // 0x6E 'n' + {0x00,0xF0,0x88,0x88,0x88,0x88,0x00}, + // 0x6F 'o' + {0x00,0x70,0x88,0x88,0x88,0x70,0x00}, + // 0x70 'p' + {0x00,0xF0,0x88,0xF0,0x80,0x80,0x00}, + // 0x71 'q' + {0x00,0x78,0x88,0x78,0x08,0x08,0x00}, + // 0x72 'r' + {0x00,0xB0,0xC8,0x80,0x80,0x80,0x00}, + // 0x73 's' + {0x00,0x78,0x80,0x70,0x08,0xF0,0x00}, + // 0x74 't' + {0x40,0xF0,0x40,0x40,0x48,0x30,0x00}, + // 0x75 'u' + {0x00,0x88,0x88,0x88,0x98,0x68,0x00}, + // 0x76 'v' + {0x00,0x88,0x88,0x50,0x50,0x20,0x00}, + // 0x77 'w' + {0x00,0x88,0x88,0xA8,0xA8,0x50,0x00}, + // 0x78 'x' + {0x00,0x88,0x50,0x20,0x50,0x88,0x00}, + // 0x79 'y' + {0x00,0x88,0x88,0x78,0x08,0x70,0x00}, + // 0x7A 'z' + {0x00,0xF8,0x10,0x20,0x40,0xF8,0x00}, + // 0x7B '{' + {0x18,0x20,0x60,0x20,0x20,0x18,0x00}, + // 0x7C '|' + {0x20,0x20,0x20,0x20,0x20,0x20,0x00}, + // 0x7D '}' + {0xC0,0x20,0x30,0x20,0x20,0xC0,0x00}, + // 0x7E '~' + {0x00,0x00,0x48,0xB0,0x00,0x00,0x00}, +}; +// clang-format on + +/// Draw a string into an RGBA buffer at the given pixel coordinate. +/// Each character is drawn at `scale` times the native 5x7 size. +static void drawString(std::uint8_t *buf, int buf_w, int buf_h, int x0, int y0, + const std::string &text, int scale, std::uint8_t r, + std::uint8_t g, std::uint8_t b) { + int cx = x0; + for (char ch : text) { + int idx = static_cast(ch) - 0x20; + if (idx < 0 || + idx >= static_cast(sizeof(kGlyphs) / sizeof(kGlyphs[0]))) + idx = 0; // fallback to space + for (int row = 0; row < kGlyphH; ++row) { + std::uint8_t bits = kGlyphs[idx][row]; + for (int col = 0; col < kGlyphW; ++col) { + if (bits & (0x80 >> col)) { + for (int sy = 0; sy < scale; ++sy) { + for (int sx = 0; sx < scale; ++sx) { + int px = cx + col * scale + sx; + int py = y0 + row * scale + sy; + if (px >= 0 && px < buf_w && py >= 0 && py < buf_h) { + int off = (py * buf_w + px) * 4; + buf[off + 0] = r; + buf[off + 1] = g; + buf[off + 2] = b; + buf[off + 3] = 255; + } + } + } + } + } + } + cx += (kGlyphW + 1) * scale; // 1px spacing between characters + } +} + +} // namespace bitmap_font + +static std::atomic g_running{true}; +static void handleSignal(int) { g_running.store(false); } + +int main(int argc, char *argv[]) { + // ----- Parse args / env ----- + bool use_mic = true; + std::vector positional; + for (int i = 1; i < argc; ++i) { + if (std::strcmp(argv[i], "--no-mic") == 0) { + use_mic = false; + } else { + positional.push_back(argv[i]); + } + } + + std::string url, token; + auto is_ws_url = [](const std::string &s) { + return (s.size() >= 5 && s.compare(0, 5, "ws://") == 0) || + (s.size() >= 6 && s.compare(0, 6, "wss://") == 0); + }; + if (positional.size() >= 2) { + for (const auto &arg : positional) { + if (is_ws_url(arg)) { + url = arg; + break; + } + } + for (const auto &arg : positional) { + if (arg != url) { + token = arg; + break; + } + } + if (url.empty()) + url = positional[0], token = positional[1]; // fallback by position + } else { + const char *e = std::getenv("LIVEKIT_URL"); + if (e) + url = e; + e = std::getenv("LIVEKIT_TOKEN"); + if (e) + token = e; + } + if (url.empty() || token.empty()) { + std::cerr << "Usage: robot [--no-mic] \n" + << " or: LIVEKIT_URL=... LIVEKIT_TOKEN=... robot [--no-mic]\n"; + return 1; + } + + std::signal(SIGINT, handleSignal); + + // ----- Initialize SDL3 (audio only when mic is used) ----- + Uint32 sdl_flags = SDL_INIT_CAMERA; + if (use_mic) + sdl_flags |= SDL_INIT_AUDIO; + if (!SDL_Init(sdl_flags)) { + std::cerr << "[robot] SDL_Init failed: " << SDL_GetError() << "\n"; + return 1; + } + + // ----- Connect to LiveKit ----- + livekit_bridge::LiveKitBridge bridge; + std::cout << "[robot] Connecting to " << url << " ...\n"; + livekit::RoomOptions options; + options.auto_subscribe = true; + if (!bridge.connect(url, token, options)) { + std::cerr << "[robot] Failed to connect.\n"; + SDL_Quit(); + return 1; + } + std::cout << "[robot] Connected.\n"; + + // ----- Create outgoing tracks ----- + constexpr int kSampleRate = 48000; + constexpr int kChannels = 1; + constexpr int kWidth = 1280; + constexpr int kHeight = 720; + + constexpr int kSimWidth = 480; + constexpr int kSimHeight = 320; + + std::shared_ptr mic; + if (use_mic) { + mic = bridge.createAudioTrack("robot-mic", kSampleRate, kChannels, + livekit::TrackSource::SOURCE_MICROPHONE); + } + auto sim_audio = + bridge.createAudioTrack("robot-sim-audio", kSampleRate, kChannels, + livekit::TrackSource::SOURCE_SCREENSHARE_AUDIO); + auto cam = bridge.createVideoTrack("robot-cam", kWidth, kHeight, + livekit::TrackSource::SOURCE_CAMERA); + auto sim_cam = + bridge.createVideoTrack("robot-sim-frame", kSimWidth, kSimHeight, + livekit::TrackSource::SOURCE_SCREENSHARE); + std::cout << "[robot] Publishing " << (use_mic ? "mic + " : "(no mic) ") + << "sim audio (" << kSampleRate << " Hz, " << kChannels + << " ch), cam + sim frame (" << kWidth << "x" << kHeight << " / " + << kSimWidth << "x" << kSimHeight << ").\n"; + + // ----- SDL Mic capture (only when use_mic) ----- + // SDLMicSource pulls 10ms frames from the default recording device and + // invokes our callback with interleaved int16 samples. + bool mic_using_sdl = false; + std::unique_ptr sdl_mic; + std::atomic mic_running{true}; + std::thread mic_thread; + + if (use_mic) { + int recCount = 0; + SDL_AudioDeviceID *recDevs = SDL_GetAudioRecordingDevices(&recCount); + bool has_mic = recDevs && recCount > 0; + if (recDevs) + SDL_free(recDevs); + + if (has_mic) { + sdl_mic = std::make_unique( + kSampleRate, kChannels, kSampleRate / 100, // 10ms frames + [&mic](const int16_t *samples, int num_samples_per_channel, + int /*sample_rate*/, int /*num_channels*/) { + if (mic && !mic->pushFrame(samples, num_samples_per_channel)) { + std::cerr << "[robot] Mic track released.\n"; + } + }); + + if (sdl_mic->init()) { + mic_using_sdl = true; + std::cout << "[robot] Using SDL microphone.\n"; + mic_thread = std::thread([&]() { + while (mic_running.load()) { + sdl_mic->pump(); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + }); + } else { + std::cerr << "[robot] SDL mic init failed.\n"; + sdl_mic.reset(); + } + } + + if (!mic_using_sdl) { + std::cout << "[robot] No microphone found; sending silence.\n"; + mic_thread = std::thread([&]() { + const int kSamplesPerFrame = kSampleRate / 100; + std::vector silence(kSamplesPerFrame * kChannels, 0); + auto next = std::chrono::steady_clock::now(); + while (mic_running.load()) { + if (mic && !mic->pushFrame(silence, kSamplesPerFrame)) { + break; + } + next += std::chrono::milliseconds(10); + std::this_thread::sleep_until(next); + } + }); + } + } + + // ----- SDL Camera capture ----- + // SDLCamSource grabs webcam frames and invokes our callback with raw pixels. + bool cam_using_sdl = false; + std::unique_ptr sdl_cam; + std::atomic cam_running{true}; + std::thread cam_thread; + + { + int camCount = 0; + SDL_CameraID *cams = SDL_GetCameras(&camCount); + bool has_cam = cams && camCount > 0; + if (cams) + SDL_free(cams); + + if (has_cam) { + sdl_cam = std::make_unique( + kWidth, kHeight, 30, SDL_PIXELFORMAT_RGBA32, + [&cam](const uint8_t *pixels, int pitch, int width, int height, + SDL_PixelFormat /*fmt*/, Uint64 timestampNS) { + const int dstPitch = width * 4; + std::vector buf(dstPitch * height); + for (int y = 0; y < height; ++y) { + std::memcpy(buf.data() + y * dstPitch, pixels + y * pitch, + dstPitch); + } + if (!cam->pushFrame( + buf.data(), buf.size(), + static_cast(timestampNS / 1000))) { + std::cerr << "[robot] Cam track released.\n"; + } + }); + + if (sdl_cam->init()) { + cam_using_sdl = true; + std::cout << "[robot] Using SDL camera.\n"; + cam_thread = std::thread([&]() { + while (cam_running.load()) { + sdl_cam->pump(); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + }); + } else { + std::cerr << "[robot] SDL camera init failed.\n"; + sdl_cam.reset(); + } + } + + if (!cam_using_sdl) { + std::cout << "[robot] No camera found; sending solid green frames.\n"; + cam_thread = std::thread([&]() { + std::vector green(kWidth * kHeight * 4); + for (int i = 0; i < kWidth * kHeight; ++i) { + green[i * 4 + 0] = 0; + green[i * 4 + 1] = 180; + green[i * 4 + 2] = 0; + green[i * 4 + 3] = 255; + } + std::int64_t ts = 0; + while (cam_running.load()) { + if (!cam->pushFrame(green, ts)) { + break; + } + ts += 33333; + std::this_thread::sleep_for(std::chrono::milliseconds(33)); + } + }); + } + } + + // ----- Sim frame video track (red bg, white text with frame # and time) + // ----- + std::atomic sim_running{true}; + std::thread sim_thread([&]() { + const std::size_t buf_size = kSimWidth * kSimHeight * 4; + std::vector frame(buf_size); + std::uint64_t frame_num = 0; + auto start = std::chrono::steady_clock::now(); + + while (sim_running.load()) { + // Fill with red background + for (int i = 0; i < kSimWidth * kSimHeight; ++i) { + frame[i * 4 + 0] = 200; // R + frame[i * 4 + 1] = 30; // G + frame[i * 4 + 2] = 30; // B + frame[i * 4 + 3] = 255; // A + } + + // Compute elapsed time + auto now = std::chrono::steady_clock::now(); + auto elapsed_ms = + std::chrono::duration_cast(now - start) + .count(); + int secs = static_cast(elapsed_ms / 1000); + int ms = static_cast(elapsed_ms % 1000); + + // Build text lines + std::string line1 = "FRAME " + std::to_string(frame_num); + char time_buf[32]; + std::snprintf(time_buf, sizeof(time_buf), "T=%d.%03ds", secs, ms); + std::string line2(time_buf); + + // Draw white text at scale=4 (each character is 20x28 pixels) + constexpr int kScale = 4; + constexpr int kCharW = (bitmap_font::kGlyphW + 1) * kScale; + int line1_w = static_cast(line1.size()) * kCharW; + int line2_w = static_cast(line2.size()) * kCharW; + int y1 = (kSimHeight / 2) - (bitmap_font::kGlyphH * kScale) - 4; + int y2 = (kSimHeight / 2) + 4; + int x1 = (kSimWidth - line1_w) / 2; + int x2 = (kSimWidth - line2_w) / 2; + + bitmap_font::drawString(frame.data(), kSimWidth, kSimHeight, x1, y1, + line1, kScale, 255, 255, 255); + bitmap_font::drawString(frame.data(), kSimWidth, kSimHeight, x2, y2, + line2, kScale, 255, 255, 255); + + std::int64_t ts = static_cast(elapsed_ms) * 1000; + if (!sim_cam->pushFrame(frame, ts)) { + break; + } + ++frame_num; + std::this_thread::sleep_for(std::chrono::milliseconds(33)); + } + }); + std::cout << "[robot] Sim frame track started.\n"; + + // ----- Sim audio track (siren: sine sweep 600-1200 Hz, 1s period) ----- + std::atomic sim_audio_running{true}; + std::thread sim_audio_thread([&]() { + const int kFrameSamples = kSampleRate / 100; // 10ms frames + constexpr double kLoFreq = 600.0; + constexpr double kHiFreq = 1200.0; + constexpr double kSweepPeriod = 1.0; // seconds per full up-down cycle + constexpr double kAmplitude = 16000.0; + constexpr double kTwoPi = 2.0 * 3.14159265358979323846; + + std::vector buf(kFrameSamples * kChannels); + double phase = 0.0; + std::uint64_t sample_idx = 0; + auto next = std::chrono::steady_clock::now(); + + while (sim_audio_running.load()) { + for (int i = 0; i < kFrameSamples; ++i) { + double t = static_cast(sample_idx) / kSampleRate; + // Triangle sweep between kLoFreq and kHiFreq + double sweep = std::fmod(t / kSweepPeriod, 1.0); + double freq = + (sweep < 0.5) + ? kLoFreq + (kHiFreq - kLoFreq) * (sweep * 2.0) + : kHiFreq - (kHiFreq - kLoFreq) * ((sweep - 0.5) * 2.0); + phase += kTwoPi * freq / kSampleRate; + if (phase > kTwoPi) + phase -= kTwoPi; + auto sample = static_cast(kAmplitude * std::sin(phase)); + for (int ch = 0; ch < kChannels; ++ch) + buf[i * kChannels + ch] = sample; + ++sample_idx; + } + if (!sim_audio->pushFrame(buf, kFrameSamples)) { + break; + } + next += std::chrono::milliseconds(10); + std::this_thread::sleep_until(next); + } + }); + std::cout << "[robot] Sim audio (siren) track started.\n"; + + // ----- Main loop: keep alive + pump SDL events ----- + std::cout << "[robot] Streaming... press Ctrl-C to stop.\n"; + + while (g_running.load()) { + SDL_Event e; + while (SDL_PollEvent(&e)) { + if (e.type == SDL_EVENT_QUIT) { + g_running.store(false); + } + } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + // ----- Cleanup ----- + std::cout << "[robot] Shutting down...\n"; + + mic_running.store(false); + cam_running.store(false); + sim_running.store(false); + sim_audio_running.store(false); + if (mic_thread.joinable()) + mic_thread.join(); + if (cam_thread.joinable()) + cam_thread.join(); + if (sim_thread.joinable()) + sim_thread.join(); + if (sim_audio_thread.joinable()) + sim_audio_thread.join(); + sdl_mic.reset(); + sdl_cam.reset(); + + mic.reset(); + sim_audio.reset(); + cam.reset(); + sim_cam.reset(); + bridge.disconnect(); + + SDL_Quit(); + std::cout << "[robot] Done.\n"; + return 0; +} diff --git a/examples/bridge_mute_unmute/README.md b/examples/bridge_mute_unmute/README.md new file mode 100644 index 00000000..6dcd5443 --- /dev/null +++ b/examples/bridge_mute_unmute/README.md @@ -0,0 +1,94 @@ +# Bridge Mute/Unmute Example + +Demonstrates remote track control using the `LiveKitBridge` built-in +track-control RPC. A **receiver** publishes audio and video tracks, and a +**caller** subscribes to them and toggles mute/unmute every few seconds. + +## How it works + +| Executable | Role | +|-----------------------|------| +| **BridgeMuteReceiver** | Publishes an audio track (`"mic"`) and a video track (`"cam"`) using SDL3 hardware capture when available, falling back to silence and solid-color frames otherwise. The bridge automatically registers a built-in `lk.bridge.track-control` RPC handler on connect. | +| **BridgeMuteCaller** | Subscribes to the receiver's mic and cam tracks, renders them via SDL3 (speaker + window), and periodically calls `requestRemoteTrackMute` / `requestRemoteTrackUnmute` to toggle both tracks. | + +When the caller mutes a track, the receiver's `LocalAudioTrack::mute()` or +`LocalVideoTrack::mute()` is invoked via RPC, which signals the LiveKit +server to stop forwarding that track's media. The caller's audio goes +silent and the video freezes on the last received frame. On unmute, media +delivery resumes. + +## Running + +Generate two tokens for the same room with different identities: + +```bash +lk token create --join --room my-room --identity receiver --valid-for 24h +lk token create --join --room my-room --identity caller --valid-for 24h +``` + +Start the receiver first, then the caller: + +```bash +# Terminal 1 +LIVEKIT_URL=wss://... LIVEKIT_TOKEN= ./build-release/bin/BridgeMuteReceiver + +# Terminal 2 +LIVEKIT_URL=wss://... LIVEKIT_TOKEN= ./build-release/bin/BridgeMuteCaller +``` + +The caller also accepts an optional third argument for the receiver's +identity (defaults to `"receiver"`): + +```bash +./build-release/bin/BridgeMuteCaller wss://... my-receiver +``` + +## Sample output + +### Receiver + +``` +./build-release/bin/BridgeMuteReceiver +[receiver] Connecting to wss://sderosasandbox-15g80zq7.livekit.cloud ... +[receiver] Connected. +cs.state() is 1 connection_state_ is 1 +[receiver] Published audio track "mic" and video track "cam". +[receiver] Waiting for remote mute/unmute commands... +[receiver] Using SDL microphone. +[receiver] Using SDL camera. +[receiver] Press Ctrl-C to stop. +[RpcController] Handling track control RPC: mute:mic +[RpcController] Handling track control RPC: mute:cam +[RpcController] Handling track control RPC: unmute:mic +[RpcController] Handling track control RPC: unmute:cam +``` + +### Caller + +``` +./build-release/bin/BridgeMuteCaller +[caller] Connecting to wss://sderosasandbox-15g80zq7.livekit.cloud ... +cs.state() is 1 connection_state_ is 1 +[caller] Connected. +[caller] Target receiver identity: "receiver" +[caller] Subscribed to receiver's mic + cam. +[caller] Rendering receiver feed. Toggling mute every 5s. Close window or Ctrl-C to stop. +[caller] Speaker opened: 48000 Hz, 1 ch. + +[caller] --- Cycle 1: MUTE --- +[caller] mic: muted OK +[caller] cam: muted OK + +[caller] --- Cycle 2: UNMUTE --- +[caller] mic: unmuted OK +[caller] cam: unmuted OK +``` + +## Notes + +- The receiver uses SDL3 for microphone and camera capture. On macOS you + may need to grant camera/microphone permissions. +- If no hardware is detected, the receiver falls back to sending silence + (audio) and alternating solid-color frames (video). +- The caller opens an SDL3 window to render the received video and plays + audio through the default speaker. diff --git a/examples/bridge_mute_unmute/caller.cpp b/examples/bridge_mute_unmute/caller.cpp new file mode 100644 index 00000000..a47b2e11 --- /dev/null +++ b/examples/bridge_mute_unmute/caller.cpp @@ -0,0 +1,337 @@ +/* + * Copyright 2026 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. + */ + +/* + * Caller (controller) for the bridge mute/unmute example. + * + * Connects to the same room as the receiver, subscribes to the receiver's + * "mic" and "cam" tracks, and renders them via SDL3 (speaker + window). + * Every 5 seconds the caller toggles mute/unmute on both tracks via RPC, + * so you can see and hear the tracks go silent and come back. + * + * Usage: + * BridgeMuteCaller [receiver-identity] + * LIVEKIT_URL=... LIVEKIT_TOKEN=... BridgeMuteCaller [receiver-identity] + * + * The token must grant a different identity (e.g. "caller"). Generate with: + * lk token create --api-key --api-secret \ + * --join --room my-room --identity caller --valid-for 24h + */ + +#include "livekit/audio_frame.h" +#include "livekit/rpc_error.h" +#include "livekit/track.h" +#include "livekit/video_frame.h" +#include "livekit_bridge/livekit_bridge.h" +#include "sdl_media.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static std::atomic g_running{true}; +static void handleSignal(int) { g_running.store(false); } + +struct LatestVideoFrame { + std::mutex mutex; + std::vector data; + int width = 0; + int height = 0; + bool dirty = false; +}; + +static LatestVideoFrame g_latest_video; + +static void storeFrame(const livekit::VideoFrame &frame) { + const std::uint8_t *src = frame.data(); + const std::size_t size = frame.dataSize(); + if (!src || size == 0) + return; + + std::lock_guard lock(g_latest_video.mutex); + g_latest_video.data.assign(src, src + size); + g_latest_video.width = frame.width(); + g_latest_video.height = frame.height(); + g_latest_video.dirty = true; +} + +int main(int argc, char *argv[]) { + std::string url, token; + std::string receiver_identity = "receiver"; + + std::vector positional; + for (int i = 1; i < argc; ++i) { + positional.push_back(argv[i]); + } + + if (positional.size() >= 2) { + url = positional[0]; + token = positional[1]; + if (positional.size() >= 3) + receiver_identity = positional[2]; + } else { + const char *e = std::getenv("LIVEKIT_URL"); + if (e) + url = e; + e = std::getenv("LIVEKIT_TOKEN"); + if (e) + token = e; + if (!positional.empty()) + receiver_identity = positional[0]; + } + if (url.empty() || token.empty()) { + std::cerr + << "Usage: BridgeMuteCaller [receiver-identity]\n" + << " or: LIVEKIT_URL=... LIVEKIT_TOKEN=... BridgeMuteCaller " + "[receiver-identity]\n" + << "Default receiver-identity: \"receiver\"\n"; + return 1; + } + + std::signal(SIGINT, handleSignal); + + // ----- Initialize SDL3 ----- + if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) { + std::cerr << "[caller] SDL_Init failed: " << SDL_GetError() << "\n"; + return 1; + } + + constexpr int kWindowWidth = 640; + constexpr int kWindowHeight = 480; + + SDL_Window *window = SDL_CreateWindow("Caller - Receiver Feed", kWindowWidth, + kWindowHeight, 0); + if (!window) { + std::cerr << "[caller] SDL_CreateWindow failed: " << SDL_GetError() << "\n"; + SDL_Quit(); + return 1; + } + + SDL_Renderer *renderer = SDL_CreateRenderer(window, nullptr); + if (!renderer) { + std::cerr << "[caller] SDL_CreateRenderer failed: " << SDL_GetError() + << "\n"; + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + + SDL_Texture *texture = nullptr; + int tex_width = 0; + int tex_height = 0; + + std::unique_ptr speaker; + std::mutex speaker_mutex; + + // ----- Connect to LiveKit ----- + livekit_bridge::LiveKitBridge bridge; + std::cout << "[caller] Connecting to " << url << " ...\n"; + + livekit::RoomOptions options; + options.auto_subscribe = true; + + if (!bridge.connect(url, token, options)) { + std::cerr << "[caller] Failed to connect.\n"; + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + std::cout << "[caller] Connected.\n"; + std::cout << "[caller] Target receiver identity: \"" << receiver_identity + << "\"\n"; + + // ----- Subscribe to receiver's audio ----- + bridge.setOnAudioFrameCallback( + receiver_identity, livekit::TrackSource::SOURCE_MICROPHONE, + [&speaker, &speaker_mutex](const livekit::AudioFrame &frame) { + const auto &samples = frame.data(); + if (samples.empty()) + return; + + std::lock_guard lock(speaker_mutex); + if (!speaker) { + speaker = std::make_unique(frame.sample_rate(), + frame.num_channels()); + if (!speaker->init()) { + std::cerr << "[caller] Failed to init SDL speaker.\n"; + speaker.reset(); + return; + } + std::cout << "[caller] Speaker opened: " << frame.sample_rate() + << " Hz, " << frame.num_channels() << " ch.\n"; + } + speaker->enqueue(samples.data(), frame.samples_per_channel()); + }); + + // ----- Subscribe to receiver's video ----- + bridge.setOnVideoFrameCallback( + receiver_identity, livekit::TrackSource::SOURCE_CAMERA, + [](const livekit::VideoFrame &frame, std::int64_t /*timestamp_us*/) { + storeFrame(frame); + }); + + std::cout << "[caller] Subscribed to receiver's mic + cam.\n"; + + // ----- Mute/unmute toggle thread ----- + std::atomic muted{false}; + std::atomic cycle{0}; + + std::atomic toggle_running{true}; + std::thread toggle_thread([&]() { + // Let the receiver connect and publish before we start toggling + for (int i = 0; i < 30 && toggle_running.load(); ++i) + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + while (toggle_running.load()) { + bool currently_muted = muted.load(); + const char *action = currently_muted ? "UNMUTE" : "MUTE"; + int c = cycle.fetch_add(1) + 1; + std::cout << "\n[caller] --- Cycle " << c << ": " << action << " ---\n"; + + // Toggle audio track "mic" + try { + if (currently_muted) { + bridge.requestRemoteTrackUnmute(receiver_identity, "mic"); + std::cout << "[caller] mic: unmuted OK\n"; + } else { + bridge.requestRemoteTrackMute(receiver_identity, "mic"); + std::cout << "[caller] mic: muted OK\n"; + } + } catch (const livekit::RpcError &e) { + std::cerr << "[caller] mic: RPC error (code=" << e.code() << " msg=\"" + << e.message() << "\")\n"; + } catch (const std::exception &e) { + std::cerr << "[caller] mic: error: " << e.what() << "\n"; + } + + // Toggle video track "cam" + try { + if (currently_muted) { + bridge.requestRemoteTrackUnmute(receiver_identity, "cam"); + std::cout << "[caller] cam: unmuted OK\n"; + } else { + bridge.requestRemoteTrackMute(receiver_identity, "cam"); + std::cout << "[caller] cam: muted OK\n"; + } + } catch (const livekit::RpcError &e) { + std::cerr << "[caller] cam: RPC error (code=" << e.code() << " msg=\"" + << e.message() << "\")\n"; + } catch (const std::exception &e) { + std::cerr << "[caller] cam: error: " << e.what() << "\n"; + } + + muted.store(!currently_muted); + + // Wait ~100 seconds, checking for shutdown every 100ms + for (int i = 0; i < 100 && toggle_running.load(); ++i) + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + }); + + // ----- Main loop: render video + pump SDL events ----- + std::cout << "[caller] Rendering receiver feed. Toggling mute every 5s. " + "Close window or Ctrl-C to stop.\n"; + + std::vector local_pixels; + + while (g_running.load()) { + SDL_Event ev; + while (SDL_PollEvent(&ev)) { + if (ev.type == SDL_EVENT_QUIT) { + g_running.store(false); + } + } + + int fw = 0, fh = 0; + bool have_frame = false; + { + std::lock_guard lock(g_latest_video.mutex); + if (g_latest_video.dirty && g_latest_video.width > 0 && + g_latest_video.height > 0) { + fw = g_latest_video.width; + fh = g_latest_video.height; + local_pixels.swap(g_latest_video.data); + g_latest_video.dirty = false; + have_frame = true; + } + } + + if (have_frame) { + if (fw != tex_width || fh != tex_height) { + if (texture) + SDL_DestroyTexture(texture); + texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA32, + SDL_TEXTUREACCESS_STREAMING, fw, fh); + tex_width = fw; + tex_height = fh; + } + + if (texture) { + void *pixels = nullptr; + int pitch = 0; + if (SDL_LockTexture(texture, nullptr, &pixels, &pitch)) { + const int srcPitch = fw * 4; + for (int y = 0; y < fh; ++y) { + std::memcpy(static_cast(pixels) + y * pitch, + local_pixels.data() + y * srcPitch, srcPitch); + } + SDL_UnlockTexture(texture); + } + } + } + + SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); + SDL_RenderClear(renderer); + if (texture) { + SDL_RenderTexture(renderer, texture, nullptr, nullptr); + } + SDL_RenderPresent(renderer); + + SDL_Delay(16); + } + + // ----- Cleanup ----- + std::cout << "\n[caller] Shutting down...\n"; + toggle_running.store(false); + if (toggle_thread.joinable()) + toggle_thread.join(); + + bridge.disconnect(); + + { + std::lock_guard lock(speaker_mutex); + speaker.reset(); + } + + if (texture) + SDL_DestroyTexture(texture); + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); + SDL_Quit(); + + std::cout << "[caller] Done.\n"; + return 0; +} diff --git a/examples/bridge_mute_unmute/receiver.cpp b/examples/bridge_mute_unmute/receiver.cpp new file mode 100644 index 00000000..1abafbc9 --- /dev/null +++ b/examples/bridge_mute_unmute/receiver.cpp @@ -0,0 +1,266 @@ +/* + * Copyright 2026 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. + */ + +/* + * Receiver (publisher) for the bridge mute/unmute example. + * + * Publishes an audio track ("mic") and a video track ("cam"), then enables + * remote track control so that a remote caller can mute/unmute them via RPC. + * + * By default, captures from the real microphone and webcam using SDL3. If + * no hardware is available, falls back to silence (audio) and solid-color + * frames (video). + * + * Usage: + * BridgeMuteReceiver + * LIVEKIT_URL=... LIVEKIT_TOKEN=... BridgeMuteReceiver + * + * The token must grant identity "receiver". Generate one with: + * lk token create --api-key --api-secret \ + * --join --room my-room --identity receiver --valid-for 24h + */ + +#include "livekit/track.h" +#include "livekit_bridge/livekit_bridge.h" +#include "sdl_media.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static std::atomic g_running{true}; +static void handleSignal(int) { g_running.store(false); } + +int main(int argc, char *argv[]) { + std::string url, token; + if (argc >= 3) { + url = argv[1]; + token = argv[2]; + } else { + const char *e = std::getenv("LIVEKIT_URL"); + if (e) + url = e; + e = std::getenv("LIVEKIT_TOKEN"); + if (e) + token = e; + } + if (url.empty() || token.empty()) { + std::cerr + << "Usage: BridgeMuteReceiver \n" + << " or: LIVEKIT_URL=... LIVEKIT_TOKEN=... BridgeMuteReceiver\n"; + return 1; + } + + std::signal(SIGINT, handleSignal); + + // ----- Initialize SDL3 ----- + if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_CAMERA)) { + std::cerr << "[receiver] SDL_Init failed: " << SDL_GetError() << "\n"; + return 1; + } + + // ----- Connect to LiveKit ----- + livekit_bridge::LiveKitBridge bridge; + std::cout << "[receiver] Connecting to " << url << " ...\n"; + + livekit::RoomOptions options; + options.auto_subscribe = true; + + if (!bridge.connect(url, token, options)) { + std::cerr << "[receiver] Failed to connect.\n"; + SDL_Quit(); + return 1; + } + std::cout << "[receiver] Connected.\n"; + + constexpr int kSampleRate = 48000; + constexpr int kChannels = 1; + constexpr int kWidth = 1280; + constexpr int kHeight = 720; + + auto mic = bridge.createAudioTrack("mic", kSampleRate, kChannels, + livekit::TrackSource::SOURCE_MICROPHONE); + auto cam = bridge.createVideoTrack("cam", kWidth, kHeight, + livekit::TrackSource::SOURCE_CAMERA); + + std::cout << "[receiver] Published audio track \"mic\" and video track " + "\"cam\".\n"; + std::cout << "[receiver] Waiting for remote mute/unmute commands...\n"; + + // ----- SDL Mic capture ----- + bool mic_using_sdl = false; + std::unique_ptr sdl_mic; + std::atomic mic_running{true}; + std::thread mic_thread; + + { + int recCount = 0; + SDL_AudioDeviceID *recDevs = SDL_GetAudioRecordingDevices(&recCount); + bool has_mic = recDevs && recCount > 0; + if (recDevs) + SDL_free(recDevs); + + if (has_mic) { + sdl_mic = std::make_unique( + kSampleRate, kChannels, kSampleRate / 100, + [&mic](const int16_t *samples, int num_samples_per_channel, + int /*sample_rate*/, int /*num_channels*/) { + mic->pushFrame(samples, num_samples_per_channel); + }); + + if (sdl_mic->init()) { + mic_using_sdl = true; + std::cout << "[receiver] Using SDL microphone.\n"; + mic_thread = std::thread([&]() { + while (mic_running.load()) { + sdl_mic->pump(); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + }); + } else { + std::cerr << "[receiver] SDL mic init failed.\n"; + sdl_mic.reset(); + } + } + + if (!mic_using_sdl) { + std::cout << "[receiver] No microphone found; sending silence.\n"; + mic_thread = std::thread([&]() { + const int kSamplesPerFrame = kSampleRate / 100; + std::vector silence(kSamplesPerFrame * kChannels, 0); + auto next = std::chrono::steady_clock::now(); + while (mic_running.load()) { + mic->pushFrame(silence, kSamplesPerFrame); + next += std::chrono::milliseconds(10); + std::this_thread::sleep_until(next); + } + }); + } + } + + // ----- SDL Camera capture ----- + bool cam_using_sdl = false; + std::unique_ptr sdl_cam; + std::atomic cam_running{true}; + std::thread cam_thread; + + { + int camCount = 0; + SDL_CameraID *cams = SDL_GetCameras(&camCount); + bool has_cam = cams && camCount > 0; + if (cams) + SDL_free(cams); + + if (has_cam) { + sdl_cam = std::make_unique( + kWidth, kHeight, 30, SDL_PIXELFORMAT_RGBA32, + [&cam](const uint8_t *pixels, int pitch, int width, int height, + SDL_PixelFormat /*fmt*/, Uint64 timestampNS) { + const int dstPitch = width * 4; + std::vector buf(dstPitch * height); + for (int y = 0; y < height; ++y) { + std::memcpy(buf.data() + y * dstPitch, pixels + y * pitch, + dstPitch); + } + cam->pushFrame(buf.data(), buf.size(), + static_cast(timestampNS / 1000)); + }); + + if (sdl_cam->init()) { + cam_using_sdl = true; + std::cout << "[receiver] Using SDL camera.\n"; + cam_thread = std::thread([&]() { + while (cam_running.load()) { + sdl_cam->pump(); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + }); + } else { + std::cerr << "[receiver] SDL camera init failed.\n"; + sdl_cam.reset(); + } + } + + if (!cam_using_sdl) { + std::cout << "[receiver] No camera found; sending solid-color frames.\n"; + cam_thread = std::thread([&]() { + std::vector frame(kWidth * kHeight * 4); + std::int64_t ts = 0; + int frame_num = 0; + + while (cam_running.load()) { + bool blue = (frame_num / 30) % 2 == 0; + for (int i = 0; i < kWidth * kHeight; ++i) { + frame[i * 4 + 0] = 0; + frame[i * 4 + 1] = + blue ? static_cast(0) : static_cast(180); + frame[i * 4 + 2] = + blue ? static_cast(200) : static_cast(0); + frame[i * 4 + 3] = 255; + } + + cam->pushFrame(frame, ts); + + ++frame_num; + ts += 33333; + std::this_thread::sleep_for(std::chrono::milliseconds(33)); + } + }); + } + } + + // ----- Main loop: pump SDL events (needed for camera approval on macOS) + // ----- + std::cout << "[receiver] Press Ctrl-C to stop.\n"; + while (g_running.load()) { + SDL_Event ev; + while (SDL_PollEvent(&ev)) { + if (ev.type == SDL_EVENT_QUIT) { + g_running.store(false); + } + } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + // ----- Cleanup ----- + std::cout << "[receiver] Shutting down...\n"; + mic_running.store(false); + cam_running.store(false); + if (mic_thread.joinable()) + mic_thread.join(); + if (cam_thread.joinable()) + cam_thread.join(); + sdl_mic.reset(); + sdl_cam.reset(); + + mic.reset(); + cam.reset(); + bridge.disconnect(); + + SDL_Quit(); + std::cout << "[receiver] Done.\n"; + return 0; +} diff --git a/examples/bridge_rpc/README.md b/examples/bridge_rpc/README.md new file mode 100644 index 00000000..8969619e --- /dev/null +++ b/examples/bridge_rpc/README.md @@ -0,0 +1,105 @@ +# Bridge RPC Example + +A minimal example of custom user-registered RPC methods using the +`LiveKitBridge` high-level API. + +Two headless executables β€” **BridgeRpcReceiver** and **BridgeRpcCaller** β€” +connect to the same LiveKit room. The receiver registers a `"print"` RPC +method that logs the caller's message and sleeps for a variable duration +before responding. The caller sends a numbered message every ~1 second and +prints the round-trip time. + +## Sleep schedule + +The receiver picks a sleep duration based on the call number: + +| Call number | Sleep | +|---------------|---------| +| `%10 == 0` | 20 s | +| `%5 == 0` | 10 s | +| otherwise | 1 s | + +Because the default LiveKit RPC timeout is 15 seconds, the caller sets a +30-second timeout so the 20-second sleeps can complete. The 10-second and +20-second cases demonstrate how long-running handlers affect the caller's +blocking `performRpc` call. + +## Running + +Generate two tokens for the same room with different identities: + +```bash +lk token create --join --room my-room --identity receiver --valid-for 24h +lk token create --join --room my-room --identity caller --valid-for 24h +``` + +Start the receiver first, then the caller: + +```bash +# Terminal 1 +LIVEKIT_URL=wss://... LIVEKIT_TOKEN= ./build-release/bin/BridgeRpcReceiver + +# Terminal 2 +LIVEKIT_URL=wss://... LIVEKIT_TOKEN= ./build-release/bin/BridgeRpcCaller +``` + +## Sample output + +### Receiver + +``` +[receiver] Connecting to wss://example.livekit.cloud ... +[receiver] Connected. +[receiver] Registered RPC method "print". +[receiver] call %10==0 -> 20s sleep +[receiver] call %5==0 -> 10s sleep +[receiver] otherwise -> 1s sleep +[receiver] Waiting for calls... +[receiver] Call #1 from caller: "Hello from caller #1" (sleeping 1s) +[receiver] Call #1 done. +[receiver] Call #2 from caller: "Hello from caller #2" (sleeping 1s) +[receiver] Call #2 done. +[receiver] Call #3 from caller: "Hello from caller #3" (sleeping 1s) +[receiver] Call #3 done. +[receiver] Call #4 from caller: "Hello from caller #4" (sleeping 1s) +[receiver] Call #4 done. +[receiver] Call #5 from caller: "Hello from caller #5" (sleeping 10s) +[receiver] Call #5 done. +[receiver] Call #6 from caller: "Hello from caller #6" (sleeping 1s) +[receiver] Call #6 done. +[receiver] Call #7 from caller: "Hello from caller #7" (sleeping 1s) +[receiver] Call #7 done. +[receiver] Call #8 from caller: "Hello from caller #8" (sleeping 1s) +[receiver] Call #8 done. +[receiver] Call #9 from caller: "Hello from caller #9" (sleeping 1s) +[receiver] Call #9 done. +[receiver] Call #10 from caller: "Hello from caller #10" (sleeping 20s) +[receiver] Call #10 done. +``` + +### Caller + +``` +[caller] Connecting to wss://example.livekit.cloud ... +[caller] Connected. +[caller] #1 Sending: "Hello from caller #1" ... +[caller] #1 Response: "ok (slept 1s)" (1159ms) +[caller] #2 Sending: "Hello from caller #2" ... +[caller] #2 Response: "ok (slept 1s)" (1174ms) +[caller] #3 Sending: "Hello from caller #3" ... +[caller] #3 Response: "ok (slept 1s)" (1152ms) +[caller] #4 Sending: "Hello from caller #4" ... +[caller] #4 Response: "ok (slept 1s)" (1135ms) +[caller] #5 Sending: "Hello from caller #5" ... +[caller] #5 Response: "ok (slept 10s)" (10139ms) +[caller] #6 Sending: "Hello from caller #6" ... +[caller] #6 Response: "ok (slept 1s)" (1138ms) +[caller] #7 Sending: "Hello from caller #7" ... +[caller] #7 Response: "ok (slept 1s)" (1143ms) +[caller] #8 Sending: "Hello from caller #8" ... +[caller] #8 Response: "ok (slept 1s)" (1115ms) +[caller] #9 Sending: "Hello from caller #9" ... +[caller] #9 Response: "ok (slept 1s)" (1123ms) +[caller] #10 Sending: "Hello from caller #10" ... +[caller] #10 Response: "ok (slept 20s)" (20119ms) +``` diff --git a/examples/bridge_rpc/custom_caller.cpp b/examples/bridge_rpc/custom_caller.cpp new file mode 100644 index 00000000..4ff5d355 --- /dev/null +++ b/examples/bridge_rpc/custom_caller.cpp @@ -0,0 +1,122 @@ +/* + * Copyright 2026 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. + */ + +/* + * Caller for the bridge_rpc example. + * + * Connects to a LiveKit room as "caller" and sends a string to the + * receiver's custom "print" RPC method every second. The receiver + * sleeps for 1s, 10s, or 20s depending on the call number, so some + * calls will take noticeably longer to return. + * + * Usage: + * BridgeRpcCaller + * LIVEKIT_URL=... LIVEKIT_TOKEN=... BridgeRpcCaller + * + * Generate a token with: + * lk token create --join --room --identity caller --valid-for 24h + */ + +#include "livekit/rpc_error.h" +#include "livekit_bridge/livekit_bridge.h" + +#include +#include +#include +#include +#include +#include +#include + +static std::atomic g_running{true}; +static void handleSignal(int) { g_running.store(false); } + +int main(int argc, char *argv[]) { + std::string url, token; + if (argc >= 3) { + url = argv[1]; + token = argv[2]; + } else { + const char *e = std::getenv("LIVEKIT_URL"); + if (e) + url = e; + e = std::getenv("LIVEKIT_TOKEN"); + if (e) + token = e; + } + if (url.empty() || token.empty()) { + std::cerr << "Usage: BridgeRpcCaller \n" + << " or: LIVEKIT_URL=... LIVEKIT_TOKEN=... BridgeRpcCaller\n"; + return 1; + } + + std::signal(SIGINT, handleSignal); + + livekit_bridge::LiveKitBridge bridge; + std::cout << "[caller] Connecting to " << url << " ...\n"; + + livekit::RoomOptions options; + if (!bridge.connect(url, token, options)) { + std::cerr << "[caller] Failed to connect.\n"; + return 1; + } + std::cout << "[caller] Connected.\n"; + + // Give the receiver a moment to join and register its handler. + for (int i = 0; i < 30 && g_running.load(); ++i) + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + int count = 0; + while (g_running.load()) { + ++count; + std::string message = "Hello from caller #" + std::to_string(count); + + std::cout << "[caller] #" << count << " Sending: \"" << message + << "\" ...\n"; + + auto t0 = std::chrono::steady_clock::now(); + try { + auto response = + bridge.performRpc("receiver", "print", message, std::nullopt); + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - t0) + .count(); + if (response.has_value()) { + std::cout << "[caller] #" << count << " Response: \"" + << response.value() << "\" (" << elapsed << "ms)\n"; + } else { + std::cout << "[caller] #" << count << " No response (" << elapsed + << "ms)\n"; + } + } catch (const livekit::RpcError &e) { + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - t0) + .count(); + std::cerr << "[caller] #" << count << " RPC error (code=" << e.code() + << " msg=\"" << e.message() << "\") (" << elapsed << "ms)\n"; + } catch (const std::exception &e) { + std::cerr << "[caller] #" << count << " Error: " << e.what() << "\n"; + } + + for (int i = 0; i < 10 && g_running.load(); ++i) + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + std::cout << "[caller] Shutting down...\n"; + bridge.disconnect(); + std::cout << "[caller] Done.\n"; + return 0; +} diff --git a/examples/bridge_rpc/custom_receiver.cpp b/examples/bridge_rpc/custom_receiver.cpp new file mode 100644 index 00000000..a98cbd3b --- /dev/null +++ b/examples/bridge_rpc/custom_receiver.cpp @@ -0,0 +1,113 @@ +/* + * Copyright 2026 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. + */ + +/* + * Receiver for the bridge_rpc example. + * + * Connects to a LiveKit room as "receiver", registers a custom RPC method + * called "print", and prints whatever string the caller sends. + * + * Usage: + * BridgeRpcReceiver + * LIVEKIT_URL=... LIVEKIT_TOKEN=... BridgeRpcReceiver + * + * Generate a token with: + * lk token create --join --room --identity receiver --valid-for 24h + */ + +#include "livekit_bridge/livekit_bridge.h" + +#include +#include +#include +#include +#include +#include +#include + +static std::atomic g_running{true}; +static void handleSignal(int) { g_running.store(false); } + +int main(int argc, char *argv[]) { + std::string url, token; + if (argc >= 3) { + url = argv[1]; + token = argv[2]; + } else { + const char *e = std::getenv("LIVEKIT_URL"); + if (e) + url = e; + e = std::getenv("LIVEKIT_TOKEN"); + if (e) + token = e; + } + if (url.empty() || token.empty()) { + std::cerr << "Usage: BridgeRpcReceiver \n" + << " or: LIVEKIT_URL=... LIVEKIT_TOKEN=... BridgeRpcReceiver\n"; + return 1; + } + + std::signal(SIGINT, handleSignal); + + livekit_bridge::LiveKitBridge bridge; + std::cout << "[receiver] Connecting to " << url << " ...\n"; + + livekit::RoomOptions options; + if (!bridge.connect(url, token, options)) { + std::cerr << "[receiver] Failed to connect.\n"; + return 1; + } + std::cout << "[receiver] Connected.\n"; + + std::atomic call_count{0}; + + bridge.registerRpcMethod( + "print", + [&call_count](const livekit::RpcInvocationData &data) + -> std::optional { + int n = call_count.fetch_add(1) + 1; + + int sleep_sec = 1; + if (n % 10 == 0) + sleep_sec = 20; + else if (n % 5 == 0) + sleep_sec = 10; + + std::cout << "[receiver] Call #" << n << " from " + << data.caller_identity << ": \"" << data.payload + << "\" (sleeping " << sleep_sec << "s)\n"; + + std::this_thread::sleep_for(std::chrono::seconds(sleep_sec)); + + std::cout << "[receiver] Call #" << n << " done.\n"; + return "ok (slept " + std::to_string(sleep_sec) + "s)"; + }); + + std::cout << "[receiver] Registered RPC method \"print\".\n" + << "[receiver] call %10==0 -> 20s sleep\n" + << "[receiver] call %5==0 -> 10s sleep\n" + << "[receiver] otherwise -> 1s sleep\n" + << "[receiver] Waiting for calls...\n"; + + while (g_running.load()) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + std::cout << "[receiver] Shutting down...\n"; + bridge.disconnect(); + std::cout << "[receiver] Done.\n"; + return 0; +} diff --git a/examples/cmake/sdl3.cmake b/examples/cmake/sdl3.cmake new file mode 100644 index 00000000..1ba2ccb3 --- /dev/null +++ b/examples/cmake/sdl3.cmake @@ -0,0 +1,52 @@ +# cmake/sdl3.cmake +include(FetchContent) + +# Only fetch/build SDL3 once, even if this file is included multiple times +if (NOT TARGET SDL3::SDL3) + # Prevent SDL3 from polluting our lib directory + set(SDL_INSTALL OFF CACHE BOOL "Disable SDL3 install" FORCE) + set(SDL_SHARED ON CACHE BOOL "Build shared SDL3" FORCE) + + # Save current output directories + set(_SAVE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_ARCHIVE_OUTPUT_DIRECTORY}) + set(_SAVE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}) + set(_SAVE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}) + + # Set SDL3 to build into its own subdirectory + set(SDL3_OUTPUT_DIR ${CMAKE_BINARY_DIR}/_deps/sdl3-build) + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${SDL3_OUTPUT_DIR}) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${SDL3_OUTPUT_DIR}) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${SDL3_OUTPUT_DIR}) + + # For multi-config generators (Visual Studio), also set per-config directories + foreach(CONFIG_TYPE Debug Release RelWithDebInfo MinSizeRel) + string(TOUPPER ${CONFIG_TYPE} CONFIG_TYPE_UPPER) + set(_SAVE_ARCHIVE_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER} ${CMAKE_ARCHIVE_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER}}) + set(_SAVE_LIBRARY_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER} ${CMAKE_LIBRARY_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER}}) + set(_SAVE_RUNTIME_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER} ${CMAKE_RUNTIME_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER}}) + + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER} ${SDL3_OUTPUT_DIR}) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER} ${SDL3_OUTPUT_DIR}) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER} ${SDL3_OUTPUT_DIR}) + endforeach() + + FetchContent_Declare( + SDL3 + GIT_REPOSITORY https://github.com/libsdl-org/SDL.git + GIT_TAG release-3.2.26 + ) + + FetchContent_MakeAvailable(SDL3) + + # Restore output directories + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${_SAVE_ARCHIVE_OUTPUT_DIRECTORY}) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${_SAVE_LIBRARY_OUTPUT_DIRECTORY}) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${_SAVE_RUNTIME_OUTPUT_DIRECTORY}) + + foreach(CONFIG_TYPE Debug Release RelWithDebInfo MinSizeRel) + string(TOUPPER ${CONFIG_TYPE} CONFIG_TYPE_UPPER) + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER} ${_SAVE_ARCHIVE_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER}}) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER} ${_SAVE_LIBRARY_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER}}) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER} ${_SAVE_RUNTIME_OUTPUT_DIRECTORY_${CONFIG_TYPE_UPPER}}) + endforeach() +endif() diff --git a/examples/common/sdl_media.cpp b/examples/common/sdl_media.cpp new file mode 100644 index 00000000..4961f513 --- /dev/null +++ b/examples/common/sdl_media.cpp @@ -0,0 +1,227 @@ +/* + * 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 "sdl_media.h" + +#include + +// ---------------------- SDLMicSource ----------------------------- + +SDLMicSource::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::~SDLMicSource() { + if (stream_) { + SDL_DestroyAudioStream(stream_); + stream_ = nullptr; + } +} + +bool SDLMicSource::init() { + // desired output (what SDL will give us when we call SDL_GetAudioStreamData) + SDL_zero(spec_); + spec_.format = SDL_AUDIO_S16; // 16-bit signed + spec_.channels = static_cast(channels_); + spec_.freq = sample_rate_; + + // Open default recording device as an audio stream + // This works for both playback and recording, depending on the device id. + stream_ = SDL_OpenAudioDeviceStream( + SDL_AUDIO_DEVICE_DEFAULT_RECORDING, // recording device + &spec_, + nullptr, // no callback, we'll poll + nullptr); + + if (!stream_) { + std::cerr << "Failed to open recording stream: " << SDL_GetError() << "\n"; + return false; + } + + if (!SDL_ResumeAudioStreamDevice(stream_)) { // unpause device + std::cerr << "Failed to resume recording device: " << SDL_GetError() + << "\n"; + return false; + } + + return true; +} + +void SDLMicSource::pump() { + if (!stream_ || !callback_) + return; + + const int samples_per_frame_total = frame_samples_ * channels_; + const int bytes_per_frame = samples_per_frame_total * sizeof(int16_t); + + // Only pull if at least one "frame" worth of audio is available + const int available = SDL_GetAudioStreamAvailable(stream_); // bytes + if (available < bytes_per_frame) { + return; + } + + std::vector buffer(samples_per_frame_total); + + const int got_bytes = SDL_GetAudioStreamData(stream_, buffer.data(), + bytes_per_frame); // + + if (got_bytes <= 0) { + return; // nothing or error (log if you like) + } + + const int got_samples_total = got_bytes / sizeof(int16_t); + const int got_samples_per_channel = got_samples_total / channels_; + + callback_(buffer.data(), got_samples_per_channel, sample_rate_, channels_); +} + +void SDLMicSource::pause() { + if (stream_) { + SDL_PauseAudioStreamDevice(stream_); // + } +} + +void SDLMicSource::resume() { + if (stream_) { + SDL_ResumeAudioStreamDevice(stream_); // + } +} + +// ---------------------- DDLSpeakerSink ----------------------------- + +DDLSpeakerSink::DDLSpeakerSink(int sample_rate, int channels) + : sample_rate_(sample_rate), channels_(channels) {} + +DDLSpeakerSink::~DDLSpeakerSink() { + if (stream_) { + SDL_DestroyAudioStream(stream_); // also closes device + stream_ = nullptr; + } +} + +bool DDLSpeakerSink::init() { + SDL_zero(spec_); + spec_.format = SDL_AUDIO_S16; // expect S16 input for playback + spec_.channels = static_cast(channels_); + spec_.freq = sample_rate_; + + // Open default playback device as a stream. + stream_ = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec_, + nullptr, // no callback; we'll push data + nullptr); + + if (!stream_) { + std::cerr << "Failed to open playback stream: " << SDL_GetError() << "\n"; + return false; + } + + if (!SDL_ResumeAudioStreamDevice(stream_)) { + std::cerr << "Failed to resume playback device: " << SDL_GetError() << "\n"; + return false; + } + + return true; +} + +void DDLSpeakerSink::enqueue(const int16_t *samples, + int num_samples_per_channel) { + if (!stream_ || !samples) + return; + + const int totalSamples = num_samples_per_channel * channels_; + const int bytes = totalSamples * static_cast(sizeof(int16_t)); + + // SDL will resample / convert as needed on SDL_GetAudioStreamData() side. + if (!SDL_PutAudioStreamData(stream_, samples, bytes)) { + std::cerr << "SDL_PutAudioStreamData failed: " << SDL_GetError() << "\n"; + } +} + +void DDLSpeakerSink::pause() { + if (stream_) { + SDL_PauseAudioStreamDevice(stream_); + } +} + +void DDLSpeakerSink::resume() { + if (stream_) { + SDL_ResumeAudioStreamDevice(stream_); + } +} + +// ---------------------- SDLCamSource ----------------------------- + +SDLCamSource::SDLCamSource(int desired_width, int desired_height, + int desired_fps, SDL_PixelFormat pixel_format, + VideoCallback cb) + : width_(desired_width), height_(desired_height), fps_(desired_fps), + format_(pixel_format), callback_(std::move(cb)) {} + +SDLCamSource::~SDLCamSource() { + if (camera_) { + SDL_CloseCamera(camera_); // + camera_ = nullptr; + } +} + +bool SDLCamSource::init() { + int count = 0; + SDL_CameraID *cams = SDL_GetCameras(&count); // + if (!cams || count == 0) { + std::cerr << "No cameras available: " << SDL_GetError() << "\n"; + if (cams) + SDL_free(cams); + return false; + } + + SDL_CameraID camId = cams[0]; // first camera for now + SDL_free(cams); + + SDL_zero(spec_); + spec_.format = format_; + spec_.colorspace = SDL_COLORSPACE_SRGB; + 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() << "\n"; + return false; + } + + // On many platforms you must wait for SDL_EVENT_CAMERA_DEVICE_APPROVED; + // here we assume the app’s main loop is already handling that. + return true; +} + +void SDLCamSource::pump() { + if (!camera_ || !callback_) + return; + + Uint64 tsNS = 0; + SDL_Surface *surf = SDL_AcquireCameraFrame(camera_, &tsNS); // non-blocking + if (!surf) { + return; + } + + callback_(static_cast(surf->pixels), surf->pitch, surf->w, surf->h, + surf->format, tsNS); + + SDL_ReleaseCameraFrame(camera_, surf); // +} diff --git a/examples/common/sdl_media.h b/examples/common/sdl_media.h new file mode 100644 index 00000000..a60bca64 --- /dev/null +++ b/examples/common/sdl_media.h @@ -0,0 +1,128 @@ +/* + * 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 +#include +#include + +// ------------------------- +// SDLMicSource +// ------------------------- +// Periodically call pump() from your main loop or a capture thread. +// It will pull 10ms frames from the mic (by default) and pass them to the +// AudioCallback. +class SDLMicSource { +public: + using AudioCallback = std::function; + + SDLMicSource(int sample_rate = 48000, int channels = 1, + int frame_samples = 480, AudioCallback cb = nullptr); + + ~SDLMicSource(); + + // Initialize SDL audio stream for recording + bool init(); + + // Call regularly to pull mic data and send to callback. + void pump(); + + void pause(); + void resume(); + + bool isValid() const { return stream_ != nullptr; } + +private: + SDL_AudioStream *stream_ = nullptr; + SDL_AudioSpec spec_{}; + int sample_rate_; + int channels_; + int frame_samples_; + AudioCallback callback_; +}; + +// ------------------------- +// DDLSpeakerSink +// ------------------------- +// For remote audio: when you get a decoded PCM frame, +// call enqueue() with interleaved S16 samples. +class DDLSpeakerSink { +public: + DDLSpeakerSink(int sample_rate = 48000, int channels = 1); + + ~DDLSpeakerSink(); + + bool init(); + + // Enqueue interleaved S16 samples for playback. + void enqueue(const int16_t *samples, int num_samples_per_channel); + + void pause(); + void resume(); + + bool isValid() const { return stream_ != nullptr; } + +private: + SDL_AudioStream *stream_ = nullptr; + SDL_AudioSpec spec_{}; + int sample_rate_; + int channels_; +}; + +// ------------------------- +// SDLCamSource +// ------------------------- +// Periodically call pump(); each time a new frame is available +// it will invoke the VideoCallback with the raw pixels. +// +// NOTE: pixels are in the SDL_Surface format returned by the camera +// (often SDL_PIXELFORMAT_ARGB8888). You can either: +// - convert to whatever your LiveKit video source expects, or +// - tell LiveKit that this is ARGB with the given stride. +class SDLCamSource { +public: + using VideoCallback = std::function; + + SDLCamSource(int desired_width = 1280, int desired_height = 720, + int desired_fps = 30, + SDL_PixelFormat pixelFormat = SDL_PIXELFORMAT_RGBA8888, + VideoCallback cb = nullptr); + + ~SDLCamSource(); + + bool init(); // open first available camera with (approximately) given spec + + // Call regularly; will call VideoCallback when a frame is available. + void pump(); + + bool isValid() const { return camera_ != nullptr; } + +private: + SDL_Camera *camera_ = nullptr; + SDL_CameraSpec spec_{}; + int width_; + int height_; + int fps_; + SDL_PixelFormat format_; + VideoCallback callback_; +}; diff --git a/examples/common/sdl_media_manager.cpp b/examples/common/sdl_media_manager.cpp new file mode 100644 index 00000000..380334f2 --- /dev/null +++ b/examples/common/sdl_media_manager.cpp @@ -0,0 +1,403 @@ +/* + * 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 "sdl_media_manager.h" + +#include "fallback_capture.h" +#include "livekit/livekit.h" +#include "sdl_media.h" +#include "sdl_video_renderer.h" +#include +#include +#include +using namespace livekit; + +SDLMediaManager::SDLMediaManager() = default; + +SDLMediaManager::~SDLMediaManager() { + stopMic(); + stopCamera(); + stopSpeaker(); + shutdownRenderer(); +} + +bool SDLMediaManager::ensureSDLInit(Uint32 flags) { + if ((SDL_WasInit(flags) & flags) == flags) { + return true; // already init + } + if (!SDL_InitSubSystem(flags)) { + std::cerr << "SDL_InitSubSystem failed (flags=" << flags + << "): " << SDL_GetError() << "\n"; + return false; + } + return true; +} + +// ---------- Mic control ---------- + +bool SDLMediaManager::startMic( + const std::shared_ptr &audio_source) { + stopMic(); + + if (!audio_source) { + std::cerr << "startMic: audioSource is null\n"; + return false; + } + + mic_source_ = audio_source; + mic_running_.store(true, std::memory_order_relaxed); + + // Try SDL path + if (!ensureSDLInit(SDL_INIT_AUDIO)) { + std::cerr << "No SDL audio, falling back to noise loop.\n"; + mic_using_sdl_ = false; + mic_thread_ = + std::thread(runNoiseCaptureLoop, mic_source_, std::ref(mic_running_)); + return true; + } + + int recCount = 0; + SDL_AudioDeviceID *recDevs = SDL_GetAudioRecordingDevices(&recCount); + if (!recDevs || recCount == 0) { + std::cerr << "No microphone devices found, falling back to noise loop.\n"; + if (recDevs) + SDL_free(recDevs); + mic_using_sdl_ = false; + mic_thread_ = + std::thread(runNoiseCaptureLoop, mic_source_, std::ref(mic_running_)); + return true; + } + SDL_free(recDevs); + + // We have at least one mic; use SDL + mic_using_sdl_ = true; + + mic_sdl_ = std::make_unique( + mic_source_->sample_rate(), mic_source_->num_channels(), + mic_source_->sample_rate() / 100, // ~10ms + [src = mic_source_](const int16_t *samples, int num_samples_per_channel, + int sample_rate, int num_channels) { + AudioFrame frame = AudioFrame::create(sample_rate, num_channels, + num_samples_per_channel); + std::memcpy(frame.data().data(), samples, + num_samples_per_channel * num_channels * sizeof(int16_t)); + try { + src->captureFrame(frame); + } catch (const std::exception &e) { + std::cerr << "Error in captureFrame (SDL mic): " << e.what() << "\n"; + } + }); + + if (!mic_sdl_->init()) { + std::cerr << "Failed to init SDL mic, falling back to noise loop.\n"; + mic_using_sdl_ = false; + mic_sdl_.reset(); + mic_thread_ = + std::thread(runNoiseCaptureLoop, mic_source_, std::ref(mic_running_)); + return true; + } + + mic_thread_ = std::thread(&SDLMediaManager::micLoopSDL, this); + return true; +} + +void SDLMediaManager::micLoopSDL() { + while (mic_running_.load(std::memory_order_relaxed)) { + mic_sdl_->pump(); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +void SDLMediaManager::stopMic() { + mic_running_.store(false, std::memory_order_relaxed); + if (mic_thread_.joinable()) { + mic_thread_.join(); + } + mic_sdl_.reset(); + mic_source_.reset(); +} + +// ---------- Camera control ---------- + +bool SDLMediaManager::startCamera( + const std::shared_ptr &video_source) { + stopCamera(); + + if (!video_source) { + std::cerr << "startCamera: videoSource is null\n"; + return false; + } + + cam_source_ = video_source; + cam_running_.store(true, std::memory_order_relaxed); + + // Try SDL + if (!ensureSDLInit(SDL_INIT_CAMERA)) { + std::cerr << "No SDL camera subsystem, using fake video loop.\n"; + cam_using_sdl_ = false; + cam_thread_ = std::thread(runFakeVideoCaptureLoop, cam_source_, + std::ref(cam_running_)); + return true; + } + + int camCount = 0; + SDL_CameraID *cams = SDL_GetCameras(&camCount); + if (!cams || camCount == 0) { + std::cerr << "No camera devices found, using fake video loop.\n"; + if (cams) + SDL_free(cams); + cam_using_sdl_ = false; + cam_thread_ = std::thread(runFakeVideoCaptureLoop, cam_source_, + std::ref(cam_running_)); + return true; + } + SDL_free(cams); + + cam_using_sdl_ = true; + can_sdl_ = std::make_unique( + 1280, 720, 30, + SDL_PIXELFORMAT_RGBA32, // Note SDL_PIXELFORMAT_RGBA8888 is not compatable + // with Livekit RGBA format. + [src = cam_source_](const uint8_t *pixels, int pitch, int width, + int height, SDL_PixelFormat /*fmt*/, + Uint64 timestampNS) { + auto frame = VideoFrame::create(width, height, VideoBufferType::RGBA); + uint8_t *dst = frame.data(); + const int dstPitch = width * 4; + + for (int y = 0; y < height; ++y) { + std::memcpy(dst + y * dstPitch, pixels + y * pitch, dstPitch); + } + + try { + src->captureFrame(frame, timestampNS / 1000, + VideoRotation::VIDEO_ROTATION_0); + } catch (const std::exception &e) { + std::cerr << "Error in captureFrame (SDL cam): " << e.what() << "\n"; + } + }); + + if (!can_sdl_->init()) { + std::cerr << "Failed to init SDL camera, using fake video loop.\n"; + cam_using_sdl_ = false; + can_sdl_.reset(); + cam_thread_ = std::thread(runFakeVideoCaptureLoop, cam_source_, + std::ref(cam_running_)); + return true; + } + + cam_thread_ = std::thread(&SDLMediaManager::cameraLoopSDL, this); + return true; +} + +void SDLMediaManager::cameraLoopSDL() { + while (cam_running_.load(std::memory_order_relaxed)) { + can_sdl_->pump(); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } +} + +void SDLMediaManager::stopCamera() { + cam_running_.store(false, std::memory_order_relaxed); + if (cam_thread_.joinable()) { + cam_thread_.join(); + } + can_sdl_.reset(); + cam_source_.reset(); +} + +// ---------- Speaker control (placeholder) ---------- + +bool SDLMediaManager::startSpeaker( + const std::shared_ptr &audio_stream) { + stopSpeaker(); + + if (!audio_stream) { + std::cerr << "startSpeaker: audioStream is null\n"; + return false; + } + + if (!ensureSDLInit(SDL_INIT_AUDIO)) { + std::cerr << "startSpeaker: SDL_INIT_AUDIO failed\n"; + return false; + } + + speaker_stream_ = audio_stream; + speaker_running_.store(true, std::memory_order_relaxed); + + // Note, we don't open the speaker since the format is unknown yet. + // Instead, open the speaker in the speakerLoopSDL thread with the native + // format. + try { + speaker_thread_ = std::thread(&SDLMediaManager::speakerLoopSDL, this); + } catch (const std::exception &e) { + std::cerr << "startSpeaker: failed to start speaker thread: " << e.what() + << "\n"; + speaker_running_.store(false, std::memory_order_relaxed); + speaker_stream_.reset(); + return false; + } + + return true; +} + +void SDLMediaManager::speakerLoopSDL() { + SDL_AudioStream *localStream = nullptr; + SDL_AudioDeviceID dev = 0; + + while (speaker_running_.load(std::memory_order_relaxed)) { + if (!speaker_stream_) { + break; + } + + livekit::AudioFrameEvent ev; + if (!speaker_stream_->read(ev)) { + // EOS or closed + break; + } + + const livekit::AudioFrame &frame = ev.frame; + const auto &data = frame.data(); + if (data.empty()) { + continue; + } + + // Lazily open SDL audio stream based on the first frame's format, so no + // resampler is needed. + if (!localStream) { + SDL_AudioSpec want{}; + want.format = SDL_AUDIO_S16; + want.channels = static_cast(frame.num_channels()); + want.freq = frame.sample_rate(); + + localStream = + SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &want, + /*callback=*/nullptr, + /*userdata=*/nullptr); + + if (!localStream) { + std::cerr << "speakerLoopSDL: SDL_OpenAudioDeviceStream failed: " + << SDL_GetError() << "\n"; + break; + } + + sdl_audio_stream_ = localStream; // store if you want to inspect later + + dev = SDL_GetAudioStreamDevice(localStream); + if (dev == 0) { + std::cerr << "speakerLoopSDL: SDL_GetAudioStreamDevice failed: " + << SDL_GetError() << "\n"; + break; + } + + if (!SDL_ResumeAudioDevice(dev)) { + std::cerr << "speakerLoopSDL: SDL_ResumeAudioDevice failed: " + << SDL_GetError() << "\n"; + break; + } + } + + // Push PCM to SDL. We assume frames are already S16, interleaved, matching + // sample_rate / channels we used above. + const int numBytes = static_cast(data.size() * sizeof(std::int16_t)); + + if (!SDL_PutAudioStreamData(localStream, data.data(), numBytes)) { + std::cerr << "speakerLoopSDL: SDL_PutAudioStreamData failed: " + << SDL_GetError() << "\n"; + break; + } + + // Tiny sleep to avoid busy loop; SDL buffers internally. + SDL_Delay(2); + } + + if (localStream) { + SDL_DestroyAudioStream(localStream); + localStream = nullptr; + sdl_audio_stream_ = nullptr; + } + + speaker_running_.store(false, std::memory_order_relaxed); +} + +void SDLMediaManager::stopSpeaker() { + speaker_running_.store(false, std::memory_order_relaxed); + if (speaker_thread_.joinable()) { + speaker_thread_.join(); + } + if (sdl_audio_stream_) { + SDL_DestroyAudioStream(sdl_audio_stream_); + sdl_audio_stream_ = nullptr; + } + speaker_stream_.reset(); +} + +// ---------- Renderer control (placeholder) ---------- + +bool SDLMediaManager::initRenderer( + const std::shared_ptr &video_stream) { + if (!video_stream) { + std::cerr << "startRenderer: videoStream is null\n"; + return false; + } + // Ensure SDL video subsystem is initialized + if (!ensureSDLInit(SDL_INIT_VIDEO)) { + std::cerr << "startRenderer: SDL_INIT_VIDEO failed\n"; + return false; + } + renderer_stream_ = video_stream; + renderer_running_.store(true, std::memory_order_relaxed); + + // Lazily create the SDLVideoRenderer + if (!sdl_renderer_) { + sdl_renderer_ = std::make_unique(); + // You can tune these dimensions or even make them options + if (!sdl_renderer_->init("LiveKit Remote Video", 1280, 720)) { + std::cerr << "startRenderer: SDLVideoRenderer::init failed\n"; + sdl_renderer_.reset(); + renderer_stream_.reset(); + renderer_running_.store(false, std::memory_order_relaxed); + return false; + } + } + + // Start the SDL renderer's own render thread + sdl_renderer_->setStream(renderer_stream_); + + return true; +} + +void SDLMediaManager::shutdownRenderer() { + renderer_running_.store(false, std::memory_order_relaxed); + + // Shut down SDL renderer thread if it exists + if (sdl_renderer_) { + sdl_renderer_->shutdown(); + } + + // Old renderer_thread_ is no longer used, but if you still have it: + if (renderer_thread_.joinable()) { + renderer_thread_.join(); + } + + renderer_stream_.reset(); +} + +void SDLMediaManager::render() { + if (renderer_running_.load(std::memory_order_relaxed) && sdl_renderer_) { + sdl_renderer_->render(); + } +} \ No newline at end of file diff --git a/examples/common/sdl_media_manager.h b/examples/common/sdl_media_manager.h new file mode 100644 index 00000000..cd9ba46c --- /dev/null +++ b/examples/common/sdl_media_manager.h @@ -0,0 +1,109 @@ +/* + * 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 +#include + +#include +#include +#include + +#include "wav_audio_source.h" + +namespace livekit { +class AudioSource; +class VideoSource; +class AudioStream; +class VideoStream; +} // namespace livekit + +// Forward-declared SDL helpers (you can also keep these separate if you like) +class SDLMicSource; +class SDLCamSource; +class SDLVideoRenderer; + +// SDLMediaManager gives you dedicated control over: +// - mic capture -> AudioSource +// - camera capture -> VideoSource +// - speaker playback -> AudioStream (TODO: integrate your API) +// - renderer -> VideoStream (TODO: integrate your API) +class SDLMediaManager { +public: + SDLMediaManager(); + ~SDLMediaManager(); + + // Mic (local capture -> AudioSource) + bool startMic(const std::shared_ptr &audio_source); + void stopMic(); + + // Camera (local capture -> VideoSource) + bool startCamera(const std::shared_ptr &video_source); + void stopCamera(); + + // Speaker (remote audio playback) + bool startSpeaker(const std::shared_ptr &audio_stream); + void stopSpeaker(); + + // Renderer (remote video rendering) + // Following APIs must be called on main thread + bool initRenderer(const std::shared_ptr &video_stream); + void shutdownRenderer(); + void render(); + +private: + // ---- SDL bootstrap helpers ---- + bool ensureSDLInit(Uint32 flags); + + // ---- Mic helpers ---- + void micLoopSDL(); + void micLoopNoise(); + + // ---- Camera helpers ---- + void cameraLoopSDL(); + void cameraLoopFake(); + + // ---- Speaker helpers (TODO: wire AudioStream -> SDL audio) ---- + void speakerLoopSDL(); + + // Mic + std::shared_ptr mic_source_; + std::unique_ptr mic_sdl_; + std::thread mic_thread_; + std::atomic mic_running_{false}; + bool mic_using_sdl_ = false; + + // Camera + std::shared_ptr cam_source_; + std::unique_ptr can_sdl_; + std::thread cam_thread_; + std::atomic cam_running_{false}; + bool cam_using_sdl_ = false; + + // Speaker (remote audio) – left mostly as a placeholder + std::shared_ptr speaker_stream_; + std::thread speaker_thread_; + std::atomic speaker_running_{false}; + SDL_AudioStream *sdl_audio_stream_ = nullptr; + + // Renderer (remote video) – left mostly as a placeholder + std::unique_ptr sdl_renderer_; + std::shared_ptr renderer_stream_; + std::thread renderer_thread_; + std::atomic renderer_running_{false}; +}; diff --git a/examples/common/sdl_video_renderer.cpp b/examples/common/sdl_video_renderer.cpp new file mode 100644 index 00000000..7ba3a783 --- /dev/null +++ b/examples/common/sdl_video_renderer.cpp @@ -0,0 +1,177 @@ +/* + * Copyright 2025 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 "sdl_video_renderer.h" + +#include "livekit/livekit.h" +#include +#include + +using namespace livekit; + +constexpr int kMaxFPS = 60; + +SDLVideoRenderer::SDLVideoRenderer() = default; + +SDLVideoRenderer::~SDLVideoRenderer() { shutdown(); } + +bool SDLVideoRenderer::init(const char *title, int width, int height) { + width_ = width; + height_ = height; + + // Assume SDL_Init(SDL_INIT_VIDEO) already called in main() + window_ = SDL_CreateWindow(title, width_, height_, 0); + if (!window_) { + std::cerr << "SDL_CreateWindow failed: " << SDL_GetError() << "\n"; + return false; + } + + renderer_ = SDL_CreateRenderer(window_, nullptr); + if (!renderer_) { + std::cerr << "SDL_CreateRenderer failed: " << SDL_GetError() << "\n"; + return false; + } + + // Note, web will send out BGRA as default, and we can't use ARGB since ffi + // does not support converting from BGRA to ARGB. + texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, + SDL_TEXTUREACCESS_STREAMING, width_, height_); + if (!texture_) { + std::cerr << "SDL_CreateTexture failed: " << SDL_GetError() << "\n"; + return false; + } + + return true; +} + +void SDLVideoRenderer::shutdown() { + if (texture_) { + SDL_DestroyTexture(texture_); + texture_ = nullptr; + } + if (renderer_) { + SDL_DestroyRenderer(renderer_); + renderer_ = nullptr; + } + if (window_) { + SDL_DestroyWindow(window_); + window_ = nullptr; + } + + stream_.reset(); +} + +void SDLVideoRenderer::setStream(std::shared_ptr stream) { + stream_ = std::move(stream); +} + +void SDLVideoRenderer::render() { + // 0) Basic sanity + if (!window_ || !renderer_) { + return; + } + + // 1) Pump SDL events on the main thread + SDL_Event e; + while (SDL_PollEvent(&e)) { + if (e.type == SDL_EVENT_QUIT) { + // TODO: set some global or member flag if you want to quit the app + } + } + + // 2) If no stream, nothing to render + if (!stream_) { + return; + } + + // Throttle rendering to kMaxFPS + const auto now = std::chrono::steady_clock::now(); + if (last_render_time_.time_since_epoch().count() != 0) { + const auto min_interval = std::chrono::microseconds(1'000'000 / kMaxFPS); + if (now - last_render_time_ < min_interval) { + return; + } + } + last_render_time_ = now; + + // 3) Read a frame from VideoStream (blocking until one is available) + livekit::VideoFrameEvent vfe; + bool gotFrame = stream_->read(vfe); + if (!gotFrame) { + // EOS / closed – nothing more to render + return; + } + + livekit::VideoFrame &frame = vfe.frame; + + // 4) Ensure the frame is RGBA. + // Ideally you requested RGBA from VideoStream::Options so this is a no-op. + if (frame.type() != livekit::VideoBufferType::RGBA) { + try { + frame = frame.convert(livekit::VideoBufferType::RGBA, false); + } catch (const std::exception &ex) { + std::cerr << "SDLVideoRenderer: convert to RGBA failed: " << ex.what() + << "\n"; + return; + } + } + + // Handle size change: recreate texture if needed + if (frame.width() != width_ || frame.height() != height_) { + width_ = frame.width(); + height_ = frame.height(); + + if (texture_) { + SDL_DestroyTexture(texture_); + texture_ = nullptr; + } + texture_ = SDL_CreateTexture( + renderer_, + SDL_PIXELFORMAT_RGBA32, // Note, SDL_PIXELFORMAT_RGBA8888 is not + // compatible with Livekit RGBA format. + SDL_TEXTUREACCESS_STREAMING, width_, height_); + if (!texture_) { + std::cerr << "SDLVideoRenderer: SDL_CreateTexture failed: " + << SDL_GetError() << "\n"; + return; + } + } + + // 6) Upload RGBA data to SDL texture + void *pixels = nullptr; + int pitch = 0; + if (!SDL_LockTexture(texture_, nullptr, &pixels, &pitch)) { + std::cerr << "SDLVideoRenderer: SDL_LockTexture failed: " << SDL_GetError() + << "\n"; + return; + } + + const std::uint8_t *src = frame.data(); + const int srcPitch = frame.width() * 4; // RGBA: 4 bytes per pixel + + for (int y = 0; y < frame.height(); ++y) { + std::memcpy(static_cast(pixels) + y * pitch, + src + y * srcPitch, srcPitch); + } + + SDL_UnlockTexture(texture_); + + // 7) Present + SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 255); + SDL_RenderClear(renderer_); + SDL_RenderTexture(renderer_, texture_, nullptr, nullptr); + SDL_RenderPresent(renderer_); +} diff --git a/examples/common/sdl_video_renderer.h b/examples/common/sdl_video_renderer.h new file mode 100644 index 00000000..fb0d41ea --- /dev/null +++ b/examples/common/sdl_video_renderer.h @@ -0,0 +1,53 @@ +/* + * Copyright 2025 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. + */ + +#pragma once + +#include +#include +#include +#include + +namespace livekit { +class VideoStream; +} + +class SDLVideoRenderer { +public: + SDLVideoRenderer(); + ~SDLVideoRenderer(); + + // Must be called on main thread, after SDL_Init(SDL_INIT_VIDEO). + bool init(const char *title, int width, int height); + + // Set/replace the stream to render. Safe to call from main thread. + void setStream(std::shared_ptr stream); + + // Called on main thread each tick to pump events and draw latest frame. + void render(); + + void shutdown(); // destroy window/renderer/texture + +private: + SDL_Window *window_ = nullptr; + SDL_Renderer *renderer_ = nullptr; + SDL_Texture *texture_ = nullptr; + + std::shared_ptr stream_; + int width_ = 0; + int height_ = 0; + std::chrono::steady_clock::time_point last_render_time_{}; +}; diff --git a/examples/common/wav_audio_source.cpp b/examples/common/wav_audio_source.cpp new file mode 100644 index 00000000..b519b818 --- /dev/null +++ b/examples/common/wav_audio_source.cpp @@ -0,0 +1,162 @@ +/* + * Copyright 2025 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 "wav_audio_source.h" + +#include +#include +#include + +// -------------------------------------------------- +// Minimal WAV loader (16-bit PCM only) +// -------------------------------------------------- +WavData load_wav16(const std::string &path) { + std::ifstream file(path, std::ios::binary); + if (!file) { + throw std::runtime_error("Failed to open WAV file: " + path + + " (If this file exists in the repo, ensure Git " + "LFS is installed and run `git lfs pull`)"); + } + + auto read_u32 = [&](uint32_t &out_value) { + file.read(reinterpret_cast(&out_value), 4); + }; + auto read_u16 = [&](uint16_t &out_value) { + file.read(reinterpret_cast(&out_value), 2); + }; + + char riff[4]; + file.read(riff, 4); + if (std::strncmp(riff, "RIFF", 4) != 0) { + throw std::runtime_error("Not a RIFF file"); + } + + uint32_t chunk_size = 0; + read_u32(chunk_size); + + char wave[4]; + file.read(wave, 4); + if (std::strncmp(wave, "WAVE", 4) != 0) { + throw std::runtime_error("Not a WAVE file"); + } + + uint16_t audio_format = 0; + uint16_t num_channels = 0; + uint32_t sample_rate = 0; + uint16_t bits_per_sample = 0; + + bool have_fmt = false; + bool have_data = false; + std::vector samples; + + while (!have_data && file) { + char sub_id[4]; + file.read(sub_id, 4); + + uint32_t sub_size = 0; + read_u32(sub_size); + + if (std::strncmp(sub_id, "fmt ", 4) == 0) { + have_fmt = true; + + read_u16(audio_format); + read_u16(num_channels); + read_u32(sample_rate); + + uint32_t byte_rate = 0; + uint16_t block_align = 0; + read_u32(byte_rate); + read_u16(block_align); + read_u16(bits_per_sample); + + if (sub_size > 16) { + file.seekg(sub_size - 16, std::ios::cur); + } + + if (audio_format != 1) { + throw std::runtime_error("Only PCM WAV supported"); + } + if (bits_per_sample != 16) { + throw std::runtime_error("Only 16-bit WAV supported"); + } + + } else if (std::strncmp(sub_id, "data", 4) == 0) { + if (!have_fmt) { + throw std::runtime_error("data chunk appeared before fmt chunk"); + } + + have_data = true; + const std::size_t count = sub_size / sizeof(int16_t); + samples.resize(count); + file.read(reinterpret_cast(samples.data()), sub_size); + + } else { + // Unknown chunk: skip it + file.seekg(sub_size, std::ios::cur); + } + } + + if (!have_data) { + throw std::runtime_error("No data chunk in WAV file"); + } + + WavData out; + out.sample_rate = static_cast(sample_rate); + out.num_channels = static_cast(num_channels); + out.samples = std::move(samples); + return out; +} + +WavAudioSource::WavAudioSource(const std::string &path, + int expected_sample_rate, int expected_channels, + bool loop_enabled) + : loop_enabled_(loop_enabled) { + wav_ = load_wav16(path); + + if (wav_.sample_rate != expected_sample_rate) { + throw std::runtime_error("WAV sample rate mismatch"); + } + if (wav_.num_channels != expected_channels) { + throw std::runtime_error("WAV channel count mismatch"); + } + + sample_rate_ = wav_.sample_rate; + num_channels_ = wav_.num_channels; + + playhead_ = 0; +} + +void WavAudioSource::fillFrame(AudioFrame &frame) { + const std::size_t frame_samples = + static_cast(frame.num_channels()) * + static_cast(frame.samples_per_channel()); + + int16_t *dst = frame.data().data(); + const std::size_t total_wav_samples = wav_.samples.size(); + + for (std::size_t i = 0; i < frame_samples; ++i) { + if (playhead_ < total_wav_samples) { + dst[i] = wav_.samples[playhead_]; + ++playhead_; + } else if (loop_enabled_ && total_wav_samples > 0) { + playhead_ = 0; + dst[i] = wav_.samples[playhead_]; + ++playhead_; + } else { + dst[i] = 0; + } + } +} diff --git a/examples/common/wav_audio_source.h b/examples/common/wav_audio_source.h new file mode 100644 index 00000000..51a101c3 --- /dev/null +++ b/examples/common/wav_audio_source.h @@ -0,0 +1,56 @@ + +/* + * Copyright 2025 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. + */ + +#pragma once + +#include "livekit/livekit.h" +#include +#include +#include +#include + +// Simple WAV container for 16-bit PCM files +struct WavData { + int sample_rate = 0; + int num_channels = 0; + std::vector samples; +}; + +// Helper that loads 16-bit PCM WAV (16-bit, PCM only) +WavData loadWav16(const std::string &path); + +using namespace livekit; + +class WavAudioSource { +public: + // loop_enabled: whether to loop when reaching the end + WavAudioSource(const std::string &path, int expected_sample_rate, + int expected_channels, bool loop_enabled = true); + + // Fill a frame with the next chunk of audio. + void fillFrame(AudioFrame &frame); + +private: + void initLoopDelayCounter(); + + WavData wav_; + std::size_t playhead_ = 0; + + const bool loop_enabled_; + int sample_rate_; + int num_channels_; +}; diff --git a/examples/hello_livekit/receiver.cpp b/examples/hello_livekit/receiver.cpp new file mode 100644 index 00000000..cc913879 --- /dev/null +++ b/examples/hello_livekit/receiver.cpp @@ -0,0 +1,131 @@ +/* + * Copyright 2026 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. + */ + +/// Subscribes to the sender's camera video and data track. Run +/// HelloLivekitSender first; use the identity it prints, or the sender's known +/// participant name. +/// +/// Usage: +/// HelloLivekitReceiver +/// +/// Or via environment variables: +/// LIVEKIT_URL, LIVEKIT_RECEIVER_TOKEN, LIVEKIT_SENDER_IDENTITY + +#include "livekit/livekit.h" + +#include +#include +#include +#include +#include +#include + +using namespace livekit; + +constexpr const char *kDataTrackName = "app-data"; +constexpr const char *kVideoTrackName = "camera0"; + +std::atomic g_running{true}; + +void handleSignal(int) { g_running.store(false); } + +std::string getenvOrEmpty(const char *name) { + const char *v = std::getenv(name); + return v ? std::string(v) : std::string{}; +} + +int main(int argc, char *argv[]) { + std::string url = getenvOrEmpty("LIVEKIT_URL"); + std::string receiver_token = getenvOrEmpty("LIVEKIT_RECEIVER_TOKEN"); + std::string sender_identity = getenvOrEmpty("LIVEKIT_SENDER_IDENTITY"); + + if (argc >= 4) { + url = argv[1]; + receiver_token = argv[2]; + sender_identity = argv[3]; + } + + if (url.empty() || receiver_token.empty() || sender_identity.empty()) { + std::cerr << "Usage: HelloLivekitReceiver " + "\n" + " or set LIVEKIT_URL, LIVEKIT_RECEIVER_TOKEN, " + "LIVEKIT_SENDER_IDENTITY\n"; + return 1; + } + + std::signal(SIGINT, handleSignal); +#ifdef SIGTERM + std::signal(SIGTERM, handleSignal); +#endif + + livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); + + auto room = std::make_unique(); + RoomOptions options; + options.auto_subscribe = true; + options.dynacast = false; + + if (!room->Connect(url, receiver_token, options)) { + std::cerr << "[receiver] Failed to connect\n"; + livekit::shutdown(); + return 1; + } + + LocalParticipant *lp = room->localParticipant(); + assert(lp); + + std::cout << "[receiver] Connected as identity='" << lp->identity() + << "' room='" << room->room_info().name + << "'; subscribing to sender identity='" << sender_identity + << "'\n"; + + int video_frame_count = 0; + room->setOnVideoFrameCallback( + sender_identity, kVideoTrackName, + [&video_frame_count](const VideoFrame &frame, std::int64_t timestamp_us) { + const auto ts_ms = + std::chrono::duration(timestamp_us).count(); + const int n = video_frame_count++; + if (n % 10 == 0) { + std::cout << "[receiver] Video frame #" << n << " " << frame.width() + << "x" << frame.height() << " ts_ms=" << ts_ms << "\n"; + } + }); + + int data_frame_count = 0; + room->addOnDataFrameCallback( + sender_identity, kDataTrackName, + [&data_frame_count](const std::vector &payload, + std::optional user_ts) { + const int n = data_frame_count++; + if (n % 10 == 0) { + std::cout << "[receiver] Data frame #" << n << "\n"; + } + }); + + std::cout << "[receiver] Listening for video track '" << kVideoTrackName + << "' + data track '" << kDataTrackName << "'; Ctrl-C to exit\n"; + + while (g_running.load()) { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + } + + std::cout << "[receiver] Shutting down\n"; + room.reset(); + + livekit::shutdown(); + return 0; +} diff --git a/examples/hello_livekit/sender.cpp b/examples/hello_livekit/sender.cpp new file mode 100644 index 00000000..c58dcd34 --- /dev/null +++ b/examples/hello_livekit/sender.cpp @@ -0,0 +1,144 @@ +/* + * Copyright 2026 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. + */ + +/// Publishes synthetic RGBA video and a data track. Run the receiver in another +/// process and pass this participant's identity (printed after connect). +/// +/// Usage: +/// HelloLivekitSender +/// +/// Or via environment variables: +/// LIVEKIT_URL, LIVEKIT_SENDER_TOKEN + +#include "livekit/livekit.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace livekit; + +constexpr int kWidth = 640; +constexpr int kHeight = 480; +constexpr const char *kVideoTrackName = "camera0"; +constexpr const char *kDataTrackName = "app-data"; + +std::atomic g_running{true}; + +void handleSignal(int) { g_running.store(false); } + +std::string getenvOrEmpty(const char *name) { + const char *v = std::getenv(name); + return v ? std::string(v) : std::string{}; +} + +int main(int argc, char *argv[]) { + std::string url = getenvOrEmpty("LIVEKIT_URL"); + std::string sender_token = getenvOrEmpty("LIVEKIT_SENDER_TOKEN"); + + if (argc >= 3) { + url = argv[1]; + sender_token = argv[2]; + } + + if (url.empty() || sender_token.empty()) { + std::cerr << "Usage: HelloLivekitSender \n" + " or set LIVEKIT_URL, LIVEKIT_SENDER_TOKEN\n"; + return 1; + } + + std::signal(SIGINT, handleSignal); +#ifdef SIGTERM + std::signal(SIGTERM, handleSignal); +#endif + + livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); + + auto room = std::make_unique(); + RoomOptions options; + options.auto_subscribe = true; + options.dynacast = false; + + if (!room->Connect(url, sender_token, options)) { + std::cerr << "[sender] Failed to connect\n"; + livekit::shutdown(); + return 1; + } + + LocalParticipant *lp = room->localParticipant(); + assert(lp); + + std::cout << "[sender] Connected as identity='" << lp->identity() + << "' room='" << room->room_info().name + << "' β€” pass this identity to HelloLivekitReceiver\n"; + + auto video_source = std::make_shared(kWidth, kHeight); + + std::shared_ptr video_track = lp->publishVideoTrack( + kVideoTrackName, video_source, TrackSource::SOURCE_CAMERA); + + auto publish_result = lp->publishDataTrack(kDataTrackName); + if (!publish_result) { + const auto &error = publish_result.error(); + std::cerr << "Failed to publish data track: code=" + << static_cast(error.code) + << " message=" << error.message << "\n"; + room.reset(); + livekit::shutdown(); + return 1; + } + std::shared_ptr data_track = publish_result.value(); + + const auto t0 = std::chrono::steady_clock::now(); + std::uint64_t count = 0; + + std::cout << "[sender] Publishing synthetic video + data on '" << kDataTrackName + << "'; Ctrl-C to exit\n"; + + while (g_running.load()) { + VideoFrame vf = VideoFrame::create(kWidth, kHeight, VideoBufferType::RGBA); + video_source->captureFrame(std::move(vf)); + + const auto now = std::chrono::steady_clock::now(); + const double ms = + std::chrono::duration(now - t0).count(); + std::ostringstream oss; + oss << std::fixed << std::setprecision(2) << ms << " ms, count: " << count; + const std::string msg = oss.str(); + auto push_result = + data_track->tryPush(std::vector(msg.begin(), msg.end())); + if (!push_result) { + const auto &error = push_result.error(); + std::cerr << "Failed to push data frame: code=" + << static_cast(error.code) + << " message=" << error.message << "\n"; + } + + ++count; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + std::cout << "[sender] Disconnecting\n"; + room.reset(); + + livekit::shutdown(); + return 0; +} diff --git a/examples/logging_levels/README.md b/examples/logging_levels/README.md new file mode 100644 index 00000000..080ec9bc --- /dev/null +++ b/examples/logging_levels/README.md @@ -0,0 +1,165 @@ +# Logging Examples + +Demonstrates the LiveKit C++ SDK's two-tier logging system. No LiveKit server +is required -- the examples simply emit log messages at every severity level so +you can see how filtering works. + +There are two executables: + +| Target | Source | What it shows | +|------------------------------|---------------------|---------------| +| `LoggingLevelsBasicUsage` | `basic_usage.cpp` | Runtime level cycling and a basic custom callback | +| `LoggingLevelsCustomSinks` | `custom_sinks.cpp` | Three practical custom sink patterns: file, JSON, and ROS2 bridge | + +## Usage -- LoggingLevelsBasicUsage + +```bash +# Full demo: cycles through every runtime level, then shows the callback API +./build/examples/LoggingLevelsBasicUsage + +# Set a single runtime level and emit all messages +./build/examples/LoggingLevelsBasicUsage warn # only WARN, ERROR, CRITICAL printed +./build/examples/LoggingLevelsBasicUsage trace # everything printed +./build/examples/LoggingLevelsBasicUsage off # nothing printed +``` + +## Usage -- LoggingLevelsCustomSinks + +```bash +# Run all three sink demos in sequence +./build/examples/LoggingLevelsCustomSinks + +# Run a single sink demo +./build/examples/LoggingLevelsCustomSinks file # writes SDK logs to livekit.log +./build/examples/LoggingLevelsCustomSinks json # emits JSON-lines to stdout +./build/examples/LoggingLevelsCustomSinks ros2 # mimics RCLCPP_* output format +``` + +## How log-level filtering works + +The SDK filters log messages in two stages: + +### 1. Compile-time (`LIVEKIT_LOG_LEVEL`) + +Set at CMake configure time. Calls **below** this level are stripped from the +binary entirely -- the format string is never evaluated and no function is +called. This is zero-cost. + +```bash +# Default: nothing stripped (all levels available at runtime) +cmake -DLIVEKIT_LOG_LEVEL=TRACE .. + +# Strip TRACE, DEBUG, and INFO at compile time +cmake -DLIVEKIT_LOG_LEVEL=WARN .. + +# Only ERROR and CRITICAL survive +cmake -DLIVEKIT_LOG_LEVEL=ERROR .. +``` + +Valid values: `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, `CRITICAL`, `OFF`. + +Under the hood this sets `SPDLOG_ACTIVE_LEVEL`, which the `LK_LOG_*` macros +check with a preprocessor guard before emitting any code. + +`LoggingLevelsBasicUsage` links **spdlog** and includes the internal `lk_log.h` +header so it can call `LK_LOG_*` (same path as the SDK implementation). Other +examples use only the public `livekit::logging.h` API and normal application +logging (`std::cerr`, etc.). + +### 2. Runtime (`setLogLevel`) + +Among the levels that survived compilation, `setLogLevel()` controls which +ones actually produce output. You can change it at any time after +`livekit::initialize()`: + +```cpp +livekit::initialize(); // default level: Info +livekit::setLogLevel(livekit::LogLevel::Debug); // show more detail +livekit::setLogLevel(livekit::LogLevel::Error); // only errors and above +``` + +### Interaction between the two tiers + +| Compile-time level | Runtime level | TRACE | DEBUG | INFO | WARN | ERROR | +|--------------------|---------------|:-----:|:-----:|:----:|:----:|:-----:| +| TRACE | Info | | | x | x | x | +| TRACE | Trace | x | x | x | x | x | +| WARN | Trace | | | | x | x | +| WARN | Error | | | | | x | + +Cells marked **x** produce output. Empty cells are filtered out -- either +stripped at compile time (left columns when compile-time > level) or suppressed +at runtime. + +## Custom log callbacks (`setLogCallback`) + +`setLogCallback()` lets you redirect **all** SDK log output to your own handler +instead of the default stderr sink. This is the integration point for frameworks +like ROS2, Android logcat, or any structured-logging pipeline. + +The basic API: + +```cpp +livekit::setLogCallback( + [](livekit::LogLevel level, + const std::string &logger_name, + const std::string &message) { + // Your code here -- e.g. write to file, emit JSON, call RCLCPP_INFO, ... + }); + +// Pass nullptr to restore the default stderr sink: +livekit::setLogCallback(nullptr); +``` + +`LoggingLevelsCustomSinks` (`custom_sinks.cpp`) provides three ready-to-copy patterns: + +### File sink + +Writes every SDK log line to a file with an ISO-8601 timestamp: + +```cpp +auto file = std::make_shared("livekit.log", std::ios::trunc); +livekit::setLogCallback( + [file](livekit::LogLevel level, const std::string &logger_name, + const std::string &message) { + *file << timestamp() << " [" << levelTag(level) << "] [" + << logger_name << "] " << message << "\n"; + }); +``` + +### JSON sink + +Emits one JSON object per line -- ready for piping into `jq` or a log +aggregation service: + +``` +{"ts":"2025-07-01T12:00:00.123Z","level":"INFO","logger":"livekit","msg":"track published"} +``` + +### ROS2 bridge sink + +Maps `livekit::LogLevel` to `RCLCPP_DEBUG` / `RCLCPP_INFO` / `RCLCPP_WARN` / +`RCLCPP_ERROR` so LiveKit logs appear in the standard ROS2 console output, +properly severity-tagged and namespaced under your node: + +```cpp +livekit::setLogCallback( + [node](livekit::LogLevel level, const std::string &logger_name, + const std::string &message) { + switch (level) { + case livekit::LogLevel::Trace: + case livekit::LogLevel::Debug: + RCLCPP_DEBUG(node->get_logger(), "[%s] %s", + logger_name.c_str(), message.c_str()); + break; + case livekit::LogLevel::Info: + RCLCPP_INFO(node->get_logger(), "[%s] %s", + logger_name.c_str(), message.c_str()); + break; + // ... Warn, Error, Critical ... + } + }); +``` + +The example compiles without rclcpp by stubbing the output to match ROS2 +formatting. diff --git a/examples/logging_levels/basic_usage.cpp b/examples/logging_levels/basic_usage.cpp new file mode 100644 index 00000000..02cdb474 --- /dev/null +++ b/examples/logging_levels/basic_usage.cpp @@ -0,0 +1,162 @@ +/* + * 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. + */ + +/// @file main.cpp +/// @brief Demonstrates LiveKit SDK log-level control and custom log callbacks. +/// +/// Logging has two tiers of filtering: +/// +/// 1. **Compile-time** (LIVEKIT_LOG_LEVEL, set via CMake): +/// Calls below this level are stripped from the binary entirely. +/// Default is TRACE (nothing stripped). For a lean release build: +/// cmake -DLIVEKIT_LOG_LEVEL=WARN ... +/// +/// 2. **Runtime** (setLogLevel()): +/// Among the levels that survived compilation, setLogLevel() controls +/// which ones actually produce output. This is what this example demos. +/// +/// Usage: +/// LoggingLevels [trace|debug|info|warn|error|critical|off] +/// +/// If no argument is given, the example cycles through every level so you can +/// see which messages are filtered at each setting. + +#include "livekit/livekit.h" +#include "lk_log.h" + +#include +#include +#include + +namespace { + +const char *levelName(livekit::LogLevel level) { + switch (level) { + case livekit::LogLevel::Trace: + return "TRACE"; + case livekit::LogLevel::Debug: + return "DEBUG"; + case livekit::LogLevel::Info: + return "INFO"; + case livekit::LogLevel::Warn: + return "WARN"; + case livekit::LogLevel::Error: + return "ERROR"; + case livekit::LogLevel::Critical: + return "CRITICAL"; + case livekit::LogLevel::Off: + return "OFF"; + } + return "UNKNOWN"; +} + +livekit::LogLevel parseLevel(const char *arg) { + if (std::strcmp(arg, "trace") == 0) + return livekit::LogLevel::Trace; + if (std::strcmp(arg, "debug") == 0) + return livekit::LogLevel::Debug; + if (std::strcmp(arg, "info") == 0) + return livekit::LogLevel::Info; + if (std::strcmp(arg, "warn") == 0) + return livekit::LogLevel::Warn; + if (std::strcmp(arg, "error") == 0) + return livekit::LogLevel::Error; + if (std::strcmp(arg, "critical") == 0) + return livekit::LogLevel::Critical; + if (std::strcmp(arg, "off") == 0) + return livekit::LogLevel::Off; + std::cerr << "Unknown level '" << arg << "', defaulting to Info.\n" + << "Valid: trace, debug, info, warn, error, critical, off\n"; + return livekit::LogLevel::Info; +} + +/// Emit one message at every severity level using the LK_LOG_* macros. +void emitAllLevels() { + LK_LOG_TRACE("This is a TRACE message (very verbose internals)"); + LK_LOG_DEBUG("This is a DEBUG message (diagnostic detail)"); + LK_LOG_INFO("This is an INFO message (normal operation)"); + LK_LOG_WARN("This is a WARN message (something unexpected)"); + LK_LOG_ERROR("This is an ERROR message (something failed)"); + LK_LOG_CRITICAL("This is a CRITICAL message (unrecoverable)"); +} + +/// Demonstrate cycling through every log level. +void runLevelCycleDemo() { + const livekit::LogLevel levels[] = { + livekit::LogLevel::Trace, livekit::LogLevel::Debug, + livekit::LogLevel::Info, livekit::LogLevel::Warn, + livekit::LogLevel::Error, livekit::LogLevel::Critical, + livekit::LogLevel::Off, + }; + + for (auto level : levels) { + std::cout << "\n========================================\n" + << " Setting log level to: " << levelName(level) << "\n" + << "========================================\n"; + livekit::setLogLevel(level); + emitAllLevels(); + } +} + +/// Demonstrate a custom log callback (e.g. for ROS2 integration). +void runCallbackDemo() { + std::cout << "\n========================================\n" + << " Custom LogCallback demo\n" + << "========================================\n"; + + livekit::setLogLevel(livekit::LogLevel::Trace); + + // Install a user-defined callback that captures all log output. + // In a real ROS2 node you would replace this with RCLCPP_* macros. + livekit::setLogCallback([](livekit::LogLevel level, + const std::string &logger_name, + const std::string &message) { + std::cout << "[CALLBACK] [" << levelName(level) << "] [" << logger_name + << "] " << message << "\n"; + }); + + LK_LOG_INFO("This message is routed through the custom callback"); + LK_LOG_WARN("Warnings also go through the callback"); + LK_LOG_ERROR("Errors too -- the callback sees everything >= the level"); + + // Restore default stderr sink by passing an empty callback. + livekit::setLogCallback(nullptr); + + std::cout << "\n(Restored default stderr sink)\n"; + LK_LOG_INFO("This message goes to stderr again (default sink)"); +} + +} // namespace + +int main(int argc, char *argv[]) { + // Initialize the LiveKit SDK (creates the spdlog logger). + livekit::initialize(); + + if (argc > 1) { + // Single-level mode: set the requested level and emit all messages. + livekit::LogLevel level = parseLevel(argv[1]); + std::cout << "Setting log level to: " << levelName(level) << "\n\n"; + livekit::setLogLevel(level); + emitAllLevels(); + } else { + // Full demo: cycle through levels, then show the callback API. + runLevelCycleDemo(); + runCallbackDemo(); + } + + livekit::shutdown(); + return 0; +} diff --git a/examples/logging_levels/custom_sinks.cpp b/examples/logging_levels/custom_sinks.cpp new file mode 100644 index 00000000..40ddcb4b --- /dev/null +++ b/examples/logging_levels/custom_sinks.cpp @@ -0,0 +1,292 @@ +/* + * 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. + */ + +/// @file custom_sinks.cpp +/// @brief Shows how SDK consumers supply their own log backend via +/// livekit::setLogCallback(). +/// +/// This example uses ONLY the public SDK API (). +/// No internal headers or spdlog dependency required. +/// +/// Three patterns are demonstrated: +/// +/// 1. **File logger** -- write SDK logs to a file on disk. +/// 2. **JSON logger** -- emit structured JSON lines (for log aggregation). +/// 3. **ROS2 bridge** -- skeleton showing how to route SDK logs into +/// RCLCPP_* macros (the rclcpp headers are stubbed +/// so this compiles without a ROS2 install). +/// +/// Usage: +/// CustomSinks [file|json|ros2] +/// +/// If no argument is given, all three sinks are demonstrated in sequence. + +#include "livekit/livekit.h" + +#include +#include +#include +#include +#include +#include + +namespace { + +// --------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------- + +const char *levelTag(livekit::LogLevel level) { + switch (level) { + case livekit::LogLevel::Trace: + return "TRACE"; + case livekit::LogLevel::Debug: + return "DEBUG"; + case livekit::LogLevel::Info: + return "INFO"; + case livekit::LogLevel::Warn: + return "WARN"; + case livekit::LogLevel::Error: + return "ERROR"; + case livekit::LogLevel::Critical: + return "CRITICAL"; + case livekit::LogLevel::Off: + return "OFF"; + } + return "?"; +} + +std::string nowISO8601() { + auto now = std::chrono::system_clock::now(); + auto tt = std::chrono::system_clock::to_time_t(now); + auto ms = std::chrono::duration_cast( + now.time_since_epoch()) % + 1000; + std::ostringstream ss; + ss << std::put_time(std::gmtime(&tt), "%FT%T") << '.' << std::setfill('0') + << std::setw(3) << ms.count() << 'Z'; + return ss.str(); +} + +struct SampleLog { + livekit::LogLevel level; + const char *message; +}; + +// Representative messages that the SDK would emit during normal operation. +// We drive the installed callback directly so this example has zero internal +// dependencies -- only the public API. +const SampleLog kSampleLogs[] = { + {livekit::LogLevel::Trace, "per-frame data: pts=12345 bytes=921600"}, + {livekit::LogLevel::Debug, "negotiating codec: VP8 -> H264 fallback"}, + {livekit::LogLevel::Info, "track published: sid=TR_abc123 kind=video"}, + {livekit::LogLevel::Warn, "ICE candidate pair changed unexpectedly"}, + {livekit::LogLevel::Error, "DTLS handshake failed: timeout after 10s"}, + {livekit::LogLevel::Critical, "out of memory allocating decode buffer"}, +}; + +void driveCallback(const livekit::LogCallback &cb) { + for (const auto &entry : kSampleLogs) { + cb(entry.level, "livekit", entry.message); + } +} + +// --------------------------------------------------------------- +// 1. File logger +// --------------------------------------------------------------- + +void runFileSinkDemo() { + const char *path = "livekit.log"; + std::cout << "\n=== File sink: writing SDK logs to '" << path << "' ===\n"; + + auto file = std::make_shared(path, std::ios::trunc); + if (!file->is_open()) { + std::cerr << "Could not open " << path << " for writing\n"; + return; + } + + // The shared_ptr keeps the stream alive inside the lambda even if + // the local variable goes out of scope before the callback fires. + livekit::LogCallback fileSink = [file](livekit::LogLevel level, + const std::string &logger_name, + const std::string &message) { + *file << nowISO8601() << " [" << levelTag(level) << "] [" << logger_name + << "] " << message << "\n"; + file->flush(); + }; + + // In a real app you would call: + // livekit::setLogCallback(fileSink); + // and then SDK operations (room.connect, publishTrack, ...) would route + // their internal log output through your callback automatically. + // + // Here we drive the callback directly with sample data so the example + // is self-contained and doesn't require a LiveKit server. + livekit::setLogCallback(fileSink); + driveCallback(fileSink); + livekit::setLogCallback(nullptr); + + std::cout << "Wrote " << path << " -- contents:\n\n"; + std::ifstream in(path); + std::cout << in.rdbuf() << "\n"; +} + +// --------------------------------------------------------------- +// 2. JSON structured logger +// --------------------------------------------------------------- + +std::string escapeJson(const std::string &s) { + std::string out; + out.reserve(s.size() + 8); + for (char c : s) { + switch (c) { + case '"': + out += "\\\""; + break; + case '\\': + out += "\\\\"; + break; + case '\n': + out += "\\n"; + break; + case '\r': + out += "\\r"; + break; + case '\t': + out += "\\t"; + break; + default: + out += c; + } + } + return out; +} + +void runJsonSinkDemo() { + std::cout << "\n=== JSON sink: structured log lines to stdout ===\n\n"; + + livekit::LogCallback jsonSink = [](livekit::LogLevel level, + const std::string &logger_name, + const std::string &message) { + std::cout << R"({"ts":")" << nowISO8601() << R"(","level":")" + << levelTag(level) << R"(","logger":")" << escapeJson(logger_name) + << R"(","msg":")" << escapeJson(message) << "\"}\n"; + }; + + livekit::setLogCallback(jsonSink); + driveCallback(jsonSink); + livekit::setLogCallback(nullptr); +} + +// --------------------------------------------------------------- +// 3. ROS2 bridge (stubbed -- compiles without rclcpp) +// --------------------------------------------------------------- +// +// In a real ROS2 node the lambda body would be: +// +// switch (level) { +// case livekit::LogLevel::Trace: +// case livekit::LogLevel::Debug: +// RCLCPP_DEBUG(node_->get_logger(), "[%s] %s", +// logger_name.c_str(), message.c_str()); +// break; +// case livekit::LogLevel::Info: +// RCLCPP_INFO(node_->get_logger(), "[%s] %s", +// logger_name.c_str(), message.c_str()); +// break; +// case livekit::LogLevel::Warn: +// RCLCPP_WARN(node_->get_logger(), "[%s] %s", +// logger_name.c_str(), message.c_str()); +// break; +// case livekit::LogLevel::Error: +// case livekit::LogLevel::Critical: +// RCLCPP_ERROR(node_->get_logger(), "[%s] %s", +// logger_name.c_str(), message.c_str()); +// break; +// default: +// break; +// } +// +// Here we stub it with console output that mimics ROS2 formatting. + +void runRos2SinkDemo() { + std::cout << "\n=== ROS2 bridge sink (stubbed) ===\n\n"; + + const std::string node_name = "livekit_bridge_node"; + + livekit::LogCallback ros2Sink = [&node_name](livekit::LogLevel level, + const std::string &logger_name, + const std::string &message) { + const char *ros_level; + switch (level) { + case livekit::LogLevel::Trace: + case livekit::LogLevel::Debug: + ros_level = "DEBUG"; + break; + case livekit::LogLevel::Info: + ros_level = "INFO"; + break; + case livekit::LogLevel::Warn: + ros_level = "WARN"; + break; + case livekit::LogLevel::Error: + case livekit::LogLevel::Critical: + ros_level = "ERROR"; + break; + default: + ros_level = "INFO"; + break; + } + + // Mimic: [INFO] [1719500000.123] [livekit_bridge_node]: [livekit] msg + auto epoch_s = std::chrono::duration( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + std::cout << "[" << ros_level << "] [" << std::fixed << std::setprecision(3) + << epoch_s << "] [" << node_name << "]: [" << logger_name << "] " + << message << "\n"; + }; + + livekit::setLogCallback(ros2Sink); + driveCallback(ros2Sink); + livekit::setLogCallback(nullptr); +} + +} // namespace + +int main(int argc, char *argv[]) { + livekit::initialize(); + + if (argc > 1) { + if (std::strcmp(argv[1], "file") == 0) { + runFileSinkDemo(); + } else if (std::strcmp(argv[1], "json") == 0) { + runJsonSinkDemo(); + } else if (std::strcmp(argv[1], "ros2") == 0) { + runRos2SinkDemo(); + } else { + std::cerr << "Unknown sink '" << argv[1] << "'.\n" + << "Usage: CustomSinks [file|json|ros2]\n"; + } + } else { + runFileSinkDemo(); + runJsonSinkDemo(); + runRos2SinkDemo(); + } + + livekit::shutdown(); + return 0; +} diff --git a/examples/ping_pong/constants.h b/examples/ping_pong/constants.h new file mode 100644 index 00000000..da3c9b53 --- /dev/null +++ b/examples/ping_pong/constants.h @@ -0,0 +1,36 @@ +/* + * Copyright 2026 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. + */ + +#pragma once + +#include + +namespace ping_pong { + +inline constexpr char kPingParticipantIdentity[] = "ping"; +inline constexpr char kPongParticipantIdentity[] = "pong"; + +inline constexpr char kPingTrackName[] = "ping"; +inline constexpr char kPongTrackName[] = "pong"; + +inline constexpr char kPingIdKey[] = "id"; +inline constexpr char kReceivedIdKey[] = "rec_id"; +inline constexpr char kTimestampKey[] = "ts"; + +inline constexpr auto kPingPeriod = std::chrono::seconds(1); +inline constexpr auto kPollPeriod = std::chrono::milliseconds(50); + +} // namespace ping_pong diff --git a/examples/ping_pong/json_converters.cpp b/examples/ping_pong/json_converters.cpp new file mode 100644 index 00000000..24f89b14 --- /dev/null +++ b/examples/ping_pong/json_converters.cpp @@ -0,0 +1,69 @@ +/* + * Copyright 2026 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 "json_converters.h" + +#include "constants.h" + +#include + +#include + +namespace ping_pong { + +std::string pingMessageToJson(const PingMessage &message) { + nlohmann::json json; + json[kPingIdKey] = message.id; + json[kTimestampKey] = message.ts_ns; + return json.dump(); +} + +PingMessage pingMessageFromJson(const std::string &json_text) { + try { + const auto json = nlohmann::json::parse(json_text); + + PingMessage message; + message.id = json.at(kPingIdKey).get(); + message.ts_ns = json.at(kTimestampKey).get(); + return message; + } catch (const nlohmann::json::exception &e) { + throw std::runtime_error(std::string("Failed to parse ping JSON: ") + + e.what()); + } +} + +std::string pongMessageToJson(const PongMessage &message) { + nlohmann::json json; + json[kReceivedIdKey] = message.rec_id; + json[kTimestampKey] = message.ts_ns; + return json.dump(); +} + +PongMessage pongMessageFromJson(const std::string &json_text) { + try { + const auto json = nlohmann::json::parse(json_text); + + PongMessage message; + message.rec_id = json.at(kReceivedIdKey).get(); + message.ts_ns = json.at(kTimestampKey).get(); + return message; + } catch (const nlohmann::json::exception &e) { + throw std::runtime_error(std::string("Failed to parse pong JSON: ") + + e.what()); + } +} + +} // namespace ping_pong diff --git a/examples/ping_pong/json_converters.h b/examples/ping_pong/json_converters.h new file mode 100644 index 00000000..3491ef6c --- /dev/null +++ b/examples/ping_pong/json_converters.h @@ -0,0 +1,31 @@ +/* + * Copyright 2026 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. + */ + +#pragma once + +#include "messages.h" + +#include + +namespace ping_pong { + +std::string pingMessageToJson(const PingMessage &message); +PingMessage pingMessageFromJson(const std::string &json); + +std::string pongMessageToJson(const PongMessage &message); +PongMessage pongMessageFromJson(const std::string &json); + +} // namespace ping_pong diff --git a/examples/ping_pong/messages.h b/examples/ping_pong/messages.h new file mode 100644 index 00000000..d4212ed6 --- /dev/null +++ b/examples/ping_pong/messages.h @@ -0,0 +1,48 @@ +/* + * Copyright 2026 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. + */ + +#pragma once + +#include + +namespace ping_pong { + +struct PingMessage { + std::uint64_t id = 0; + std::int64_t ts_ns = 0; +}; + +struct PongMessage { + std::uint64_t rec_id = 0; + std::int64_t ts_ns = 0; +}; + +struct LatencyMetrics { + std::uint64_t id = 0; + std::int64_t ping_sent_ts_ns = 0; + std::int64_t pong_sent_ts_ns = 0; + std::int64_t ping_received_ts_ns = 0; + std::int64_t round_trip_time_ns = 0; + std::int64_t pong_to_ping_time_ns = 0; + std::int64_t ping_to_pong_and_processing_ns = 0; + double estimated_one_way_latency_ns = 0.0; + double round_trip_time_ms = 0.0; + double pong_to_ping_time_ms = 0.0; + double ping_to_pong_and_processing_ms = 0.0; + double estimated_one_way_latency_ms = 0.0; +}; + +} // namespace ping_pong diff --git a/examples/ping_pong/ping.cpp b/examples/ping_pong/ping.cpp new file mode 100644 index 00000000..a0a26b3b --- /dev/null +++ b/examples/ping_pong/ping.cpp @@ -0,0 +1,210 @@ +/* + * Copyright 2026 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. + */ + +/// Ping participant: publishes on the "ping" data track, listens on "pong", +/// and logs latency metrics for each matched response. Use a token whose +/// identity is `ping`. + +#include "constants.h" +#include "json_converters.h" +#include "livekit/livekit.h" +#include "messages.h" +#include "utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace livekit; + +namespace { + +std::atomic g_running{true}; + +void handleSignal(int) { g_running.store(false); } + +ping_pong::LatencyMetrics +calculateLatencyMetrics(const ping_pong::PingMessage &ping_message, + const ping_pong::PongMessage &pong_message, + std::int64_t received_ts_ns) { + ping_pong::LatencyMetrics metrics; + metrics.id = ping_message.id; + metrics.pong_sent_ts_ns = pong_message.ts_ns; + metrics.ping_received_ts_ns = received_ts_ns; + metrics.round_trip_time_ns = received_ts_ns - ping_message.ts_ns; + metrics.pong_to_ping_time_ns = received_ts_ns - pong_message.ts_ns; + metrics.ping_to_pong_and_processing_ns = + pong_message.ts_ns - ping_message.ts_ns; + metrics.estimated_one_way_latency_ns = + static_cast(metrics.round_trip_time_ns) / 2.0; + metrics.round_trip_time_ms = + static_cast(metrics.round_trip_time_ns) / 1'000'000.0; + metrics.pong_to_ping_time_ms = + static_cast(metrics.pong_to_ping_time_ns) / 1'000'000.0; + metrics.ping_to_pong_and_processing_ms = + static_cast(metrics.ping_to_pong_and_processing_ns) / 1'000'000.0; + metrics.estimated_one_way_latency_ms = + metrics.estimated_one_way_latency_ns / 1'000'000.0; + return metrics; +} + +} // namespace + +int main(int argc, char *argv[]) { + std::string url = ping_pong::getenvOrEmpty("LIVEKIT_URL"); + std::string token = ping_pong::getenvOrEmpty("LIVEKIT_TOKEN"); + + if (argc >= 3) { + url = argv[1]; + token = argv[2]; + } + + if (url.empty() || token.empty()) { + std::cerr << "LIVEKIT_URL and LIVEKIT_TOKEN (or ) are " + "required\n"; + return 1; + } + + std::signal(SIGINT, handleSignal); +#ifdef SIGTERM + std::signal(SIGTERM, handleSignal); +#endif + + livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); + + auto room = std::make_unique(); + RoomOptions options; + options.auto_subscribe = true; + options.dynacast = false; + + if (!room->Connect(url, token, options)) { + std::cerr << "Failed to connect to room\n"; + livekit::shutdown(); + return 1; + } + + LocalParticipant *local_participant = room->localParticipant(); + assert(local_participant); + + std::cout << "ping connected as identity='" << local_participant->identity() + << "' room='" << room->room_info().name << "'\n"; + + auto publish_result = + local_participant->publishDataTrack(ping_pong::kPingTrackName); + if (!publish_result) { + const auto &error = publish_result.error(); + std::cerr << "Failed to publish ping data track: code=" + << static_cast(error.code) + << " message=" << error.message << "\n"; + room->setDelegate(nullptr); + room.reset(); + livekit::shutdown(); + return 1; + } + + std::shared_ptr ping_track = publish_result.value(); + std::unordered_map sent_messages; + std::mutex sent_messages_mutex; + + const auto callback_id = room->addOnDataFrameCallback( + ping_pong::kPongParticipantIdentity, ping_pong::kPongTrackName, + [&sent_messages, + &sent_messages_mutex](const std::vector &payload, + std::optional /*user_timestamp*/) { + try { + if (payload.empty()) { + return; + } + + const auto pong_message = + ping_pong::pongMessageFromJson(ping_pong::toString(payload)); + const auto received_ts_ns = ping_pong::timeSinceEpochNs(); + + ping_pong::PingMessage ping_message; + { + std::lock_guard lock(sent_messages_mutex); + const auto it = sent_messages.find(pong_message.rec_id); + if (it == sent_messages.end()) { + std::cerr << "Received pong for unknown id=" << pong_message.rec_id + << "\n"; + return; + } + ping_message = it->second; + sent_messages.erase(it); + } + + const auto metrics = calculateLatencyMetrics( + ping_message, pong_message, received_ts_ns); + + std::cout << "pong id=" << metrics.id << " rtt_ms=" << std::fixed + << std::setprecision(3) << metrics.round_trip_time_ms + << " pong_to_ping_ms=" << metrics.pong_to_ping_time_ms + << " ping_to_pong_and_processing_ms=" + << metrics.ping_to_pong_and_processing_ms + << " estimated_one_way_latency_ms=" + << metrics.estimated_one_way_latency_ms << "\n"; + } catch (const std::exception &e) { + std::cerr << "Failed to process pong payload: " << e.what() << "\n"; + } + }); + + std::cout << "published data track '" << ping_pong::kPingTrackName + << "' and listening for '" << ping_pong::kPongTrackName + << "' from '" << ping_pong::kPongParticipantIdentity << "'\n"; + + std::uint64_t next_id = 1; + auto next_deadline = std::chrono::steady_clock::now(); + + while (g_running.load()) { + ping_pong::PingMessage ping_message; + ping_message.id = next_id++; + ping_message.ts_ns = ping_pong::timeSinceEpochNs(); + + const std::string json = ping_pong::pingMessageToJson(ping_message); + auto push_result = ping_track->tryPush(ping_pong::toPayload(json)); + if (!push_result) { + const auto &error = push_result.error(); + std::cerr << "Failed to push ping data frame: code=" + << static_cast(error.code) + << " message=" << error.message << "\n"; + } else { + { + std::lock_guard lock(sent_messages_mutex); + sent_messages.emplace(ping_message.id, ping_message); + } + std::cout << "sent ping id=" << ping_message.id + << " ts_ns=" << ping_message.ts_ns << "\n"; + } + + next_deadline += ping_pong::kPingPeriod; + std::this_thread::sleep_until(next_deadline); + } + + std::cout << "shutting down ping participant\n"; + room.reset(); + livekit::shutdown(); + return 0; +} diff --git a/examples/ping_pong/pong.cpp b/examples/ping_pong/pong.cpp new file mode 100644 index 00000000..c7307978 --- /dev/null +++ b/examples/ping_pong/pong.cpp @@ -0,0 +1,148 @@ +/* + * Copyright 2026 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. + */ + +/// Pong participant: listens on the "ping" data track and publishes responses +/// on the "pong" data track. Use a token whose identity is `pong`. + +#include "constants.h" +#include "json_converters.h" +#include "livekit/livekit.h" +#include "messages.h" +#include "utils.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace livekit; + +namespace { + +std::atomic g_running{true}; + +void handleSignal(int) { g_running.store(false); } + +} // namespace + +int main(int argc, char *argv[]) { + std::string url = ping_pong::getenvOrEmpty("LIVEKIT_URL"); + std::string token = ping_pong::getenvOrEmpty("LIVEKIT_TOKEN"); + + if (argc >= 3) { + url = argv[1]; + token = argv[2]; + } + + if (url.empty() || token.empty()) { + std::cerr << "LIVEKIT_URL and LIVEKIT_TOKEN (or ) are " + "required\n"; + return 1; + } + + std::signal(SIGINT, handleSignal); +#ifdef SIGTERM + std::signal(SIGTERM, handleSignal); +#endif + + livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); + + auto room = std::make_unique(); + RoomOptions options; + options.auto_subscribe = true; + options.dynacast = false; + + if (!room->Connect(url, token, options)) { + std::cerr << "Failed to connect to room\n"; + livekit::shutdown(); + return 1; + } + + LocalParticipant *local_participant = room->localParticipant(); + assert(local_participant); + + std::cout << "pong connected as identity='" << local_participant->identity() + << "' room='" << room->room_info().name << "'\n"; + + auto publish_result = + local_participant->publishDataTrack(ping_pong::kPongTrackName); + if (!publish_result) { + const auto &error = publish_result.error(); + std::cerr << "Failed to publish pong data track: code=" + << static_cast(error.code) + << " message=" << error.message << "\n"; + room->setDelegate(nullptr); + room.reset(); + livekit::shutdown(); + return 1; + } + + std::shared_ptr pong_track = publish_result.value(); + + const auto callback_id = room->addOnDataFrameCallback( + ping_pong::kPingParticipantIdentity, ping_pong::kPingTrackName, + [pong_track](const std::vector &payload, + std::optional /*user_timestamp*/) { + try { + if (payload.empty()) { + return; + } + + const auto ping_message = + ping_pong::pingMessageFromJson(ping_pong::toString(payload)); + + ping_pong::PongMessage pong_message; + pong_message.rec_id = ping_message.id; + pong_message.ts_ns = ping_pong::timeSinceEpochNs(); + + const std::string json = ping_pong::pongMessageToJson(pong_message); + auto push_result = pong_track->tryPush(ping_pong::toPayload(json)); + if (!push_result) { + const auto &error = push_result.error(); + std::cerr << "Failed to push pong data frame: code=" + << static_cast(error.code) + << " message=" << error.message << "\n"; + return; + } + + std::cout << "received ping id=" << ping_message.id + << " ts_ns=" << ping_message.ts_ns << " and sent pong rec_id=" + << pong_message.rec_id << " ts_ns=" << pong_message.ts_ns + << "\n"; + } catch (const std::exception &e) { + std::cerr << "Failed to process ping payload: " << e.what() << "\n"; + } + }); + + std::cout << "published data track '" << ping_pong::kPongTrackName + << "' and listening for '" << ping_pong::kPingTrackName + << "' from '" << ping_pong::kPingParticipantIdentity << "'\n"; + + while (g_running.load()) { + std::this_thread::sleep_for(ping_pong::kPollPeriod); + } + + std::cout << "shutting down pong participant\n"; + room.reset(); + livekit::shutdown(); + return 0; +} diff --git a/examples/ping_pong/utils.h b/examples/ping_pong/utils.h new file mode 100644 index 00000000..56c915b9 --- /dev/null +++ b/examples/ping_pong/utils.h @@ -0,0 +1,45 @@ +/* + * Copyright 2026 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. + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace ping_pong { + +inline std::string getenvOrEmpty(const char *name) { + const char *value = std::getenv(name); + return value ? std::string(value) : std::string{}; +} + +inline std::int64_t timeSinceEpochNs() { + const auto now = std::chrono::system_clock::now().time_since_epoch(); + return std::chrono::duration_cast(now).count(); +} + +inline std::vector toPayload(const std::string &json) { + return std::vector(json.begin(), json.end()); +} + +inline std::string toString(const std::vector &payload) { + return std::string(payload.begin(), payload.end()); +} + +} // namespace ping_pong diff --git a/examples/simple_data_stream/main.cpp b/examples/simple_data_stream/main.cpp new file mode 100644 index 00000000..f8144b71 --- /dev/null +++ b/examples/simple_data_stream/main.cpp @@ -0,0 +1,284 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "livekit/livekit.h" + +using namespace livekit; + +namespace { + +std::atomic g_running{true}; + +void handleSignal(int) { g_running.store(false); } + +// Helper: get env var or empty string +std::string getenvOrEmpty(const char *name) { + const char *v = std::getenv(name); + return v ? std::string(v) : std::string{}; +} + +std::int64_t nowEpochMs() { + using namespace std::chrono; + return duration_cast(system_clock::now().time_since_epoch()) + .count(); +} + +std::string randomHexId(std::size_t nbytes = 16) { + static thread_local std::mt19937_64 rng{std::random_device{}()}; + std::ostringstream oss; + for (std::size_t i = 0; i < nbytes; ++i) { + std::uint8_t b = static_cast(rng() & 0xFF); + const char *hex = "0123456789abcdef"; + oss << hex[(b >> 4) & 0xF] << hex[b & 0xF]; + } + return oss.str(); +} + +// Greeting: send text + image +void greetParticipant(Room *room, const std::string &identity) { + std::cout << "[DataStream] Greeting participant: " << identity << "\n"; + + LocalParticipant *lp = room->localParticipant(); + if (!lp) { + std::cerr << "[DataStream] No local participant, cannot greet.\n"; + return; + } + + try { + const std::int64_t sent_ms = nowEpochMs(); + const std::string sender_id = + !lp->identity().empty() ? lp->identity() : std::string("cpp_sender"); + const std::vector dest{identity}; + + // Send text stream ("chat") + const std::string chat_stream_id = randomHexId(); + const std::string reply_to_id = ""; + std::map chat_attrs; + chat_attrs["sent_ms"] = std::to_string(sent_ms); + chat_attrs["kind"] = "chat"; + chat_attrs["test_flag"] = "1"; + chat_attrs["seq"] = "1"; + + // Put timestamp in payload too (so you can compute latency even if + // attributes aren’t plumbed through your reader info yet). + const std::string body = "Hi! Just a friendly message"; + const std::string payload = "sent_ms=" + std::to_string(sent_ms) + "\n" + + "stream_id=" + chat_stream_id + "\n" + body; + TextStreamWriter text_writer(*lp, "chat", chat_attrs, chat_stream_id, + payload.size(), reply_to_id, dest, sender_id); + + const std::string message = "Hi! Just a friendly message"; + text_writer.write(message); // will be chunked internally if needed + text_writer.close(); // optional reason/attributes omitted + + // Send image as byte stream + const std::string file_path = "data/green.avif"; + std::ifstream in(file_path, std::ios::binary); + if (!in) { + std::cerr << "[DataStream] Failed to open file: " << file_path << "\n"; + return; + } + + std::vector data((std::istreambuf_iterator(in)), + std::istreambuf_iterator()); + + const std::string file_stream_id = randomHexId(); + std::map file_attrs; + file_attrs["sent_ms"] = std::to_string(sent_ms); + file_attrs["kind"] = "file"; + file_attrs["test_flag"] = "1"; + file_attrs["orig_path"] = file_path; + const std::string name = + std::filesystem::path(file_path).filename().string(); + const std::string mime = "image/avif"; + ByteStreamWriter byte_writer(*lp, name, "files", file_attrs, file_stream_id, + data.size(), mime, dest, sender_id); + byte_writer.write(data); + byte_writer.close(); + + std::cout << "[DataStream] Greeting sent to " << identity + << " (sent_ms=" << sent_ms << ")\n"; + } catch (const std::exception &e) { + std::cerr << "[DataStream] Error greeting participant " << identity << ": " + << e.what() << "\n"; + } +} + +// Handlers for incoming streams +void handleChatMessage(std::shared_ptr reader, + const std::string &participant_identity) { + try { + const auto info = reader->info(); // copy (safe even if reader goes away) + const std::int64_t recv_ms = nowEpochMs(); + const std::int64_t sent_ms = info.timestamp; + const auto latency = (sent_ms > 0) ? (recv_ms - sent_ms) : -1; + std::string full_text = reader->readAll(); + std::cout << "[DataStream] Received chat from " << participant_identity + << " topic=" << info.topic << " stream_id=" << info.stream_id + << " latency_ms=" << latency << " text='" << full_text << "'\n"; + } catch (const std::exception &e) { + std::cerr << "[DataStream] Error reading chat stream from " + << participant_identity << ": " << e.what() << "\n"; + } +} + +void handleWelcomeImage(std::shared_ptr reader, + const std::string &participant_identity) { + try { + const auto info = reader->info(); + const std::string stream_id = + info.stream_id.empty() ? "unknown" : info.stream_id; + const std::string original_name = + info.name.empty() ? "received_image.bin" : info.name; + // Latency: prefer header timestamp + std::int64_t sent_ms = info.timestamp; + // Optional: override with explicit attribute if you set it + auto it = info.attributes.find("sent_ms"); + if (it != info.attributes.end()) { + try { + sent_ms = std::stoll(it->second); + } catch (...) { + } + } + const std::int64_t recv_ms = nowEpochMs(); + const std::int64_t latency_ms = (sent_ms > 0) ? (recv_ms - sent_ms) : -1; + const std::string out_file = "received_" + original_name; + std::cout << "[DataStream] Receiving image from " << participant_identity + << " stream_id=" << stream_id << " name='" << original_name << "'" + << " mime='" << info.mime_type << "'" + << " size=" + << (info.size ? std::to_string(*info.size) : "unknown") + << " latency_ms=" << latency_ms << " -> '" << out_file << "'\n"; + std::ofstream out(out_file, std::ios::binary); + if (!out) { + std::cerr << "[DataStream] Failed to open output file: " << out_file + << "\n"; + return; + } + std::vector chunk; + std::uint64_t total_written = 0; + while (reader->readNext(chunk)) { + if (!chunk.empty()) { + out.write(reinterpret_cast(chunk.data()), + static_cast(chunk.size())); + total_written += chunk.size(); + } + } + std::cout << "[DataStream] Saved image from " << participant_identity + << " stream_id=" << stream_id << " bytes=" << total_written + << " to '" << out_file << std::endl; + } catch (const std::exception &e) { + std::cerr << "[DataStream] Error reading image stream from " + << participant_identity << ": " << e.what() << "\n"; + } +} + +} // namespace + +int main(int argc, char *argv[]) { + // Get URL and token from env. + std::string url = getenvOrEmpty("LIVEKIT_URL"); + std::string token = getenvOrEmpty("LIVEKIT_TOKEN"); + + if (argc >= 3) { + // Allow overriding via CLI: ./SimpleDataStream + url = argv[1]; + token = argv[2]; + } + + if (url.empty() || token.empty()) { + std::cerr << "LIVEKIT_URL and LIVEKIT_TOKEN (or CLI args) are required\n"; + return 1; + } + + std::cout << "[DataStream] Connecting to: " << url << "\n"; + + std::signal(SIGINT, handleSignal); +#ifdef SIGTERM + std::signal(SIGTERM, handleSignal); +#endif + + // Initialize the livekit with logging to console. + livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); + auto room = std::make_unique(); + RoomOptions options; + options.auto_subscribe = true; + options.dynacast = false; + + bool ok = room->Connect(url, token, options); + std::cout << "[DataStream] Connect result: " << std::boolalpha << ok << "\n"; + if (!ok) { + std::cerr << "[DataStream] Failed to connect to room\n"; + livekit::shutdown(); + return 1; + } + + auto info = room->room_info(); + std::cout << "[DataStream] Connected to room '" << info.name + << "', participants: " << info.num_participants << "\n"; + + // Register stream handlers + room->registerTextStreamHandler( + "chat", [](std::shared_ptr reader, + const std::string &participant_identity) { + std::thread t(handleChatMessage, std::move(reader), + participant_identity); + t.detach(); + }); + + room->registerByteStreamHandler( + "files", [](std::shared_ptr reader, + const std::string &participant_identity) { + std::thread t(handleWelcomeImage, std::move(reader), + participant_identity); + t.detach(); + }); + + // Greet existing participants + { + auto remotes = room->remoteParticipants(); + for (const auto &rp : remotes) { + if (!rp) + continue; + std::cout << "Remote: " << rp->identity() << "\n"; + greetParticipant(room.get(), rp->identity()); + } + } + + // Optionally: greet on join + // + // If Room API exposes a participant-connected callback, you could do: + // + // room->onParticipantConnected( + // [&](RemoteParticipant& participant) { + // std::cout << "[DataStream] participant connected: " + // << participant.sid() << " " << participant.identity() + // << "\n"; + // greetParticipant(room.get(), participant.identity()); + // }); + // + // Adjust to your actual event API. + std::cout << "[DataStream] Ready. Waiting for streams (Ctrl-C to exit)...\n"; + // Keep process alive until signal + while (g_running.load()) { + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } + + std::cout << "[DataStream] Shutting down...\n"; + // It is important to clean up the delegate and room in order. + room->setDelegate(nullptr); + room.reset(); + livekit::shutdown(); + return 0; +} diff --git a/examples/simple_joystick/json_utils.cpp b/examples/simple_joystick/json_utils.cpp new file mode 100644 index 00000000..d634aaa7 --- /dev/null +++ b/examples/simple_joystick/json_utils.cpp @@ -0,0 +1,46 @@ +/* + * Copyright 2025 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 "json_utils.h" + +#include +#include + +namespace simple_joystick { + +std::string joystick_to_json(const JoystickCommand &cmd) { + nlohmann::json j; + j["x"] = cmd.x; + j["y"] = cmd.y; + j["z"] = cmd.z; + return j.dump(); +} + +JoystickCommand json_to_joystick(const std::string &json) { + try { + auto j = nlohmann::json::parse(json); + JoystickCommand cmd; + cmd.x = j.at("x").get(); + cmd.y = j.at("y").get(); + cmd.z = j.at("z").get(); + return cmd; + } catch (const nlohmann::json::exception &e) { + throw std::runtime_error(std::string("Failed to parse joystick JSON: ") + + e.what()); + } +} + +} // namespace simple_joystick diff --git a/examples/simple_joystick/json_utils.h b/examples/simple_joystick/json_utils.h new file mode 100644 index 00000000..66ba16aa --- /dev/null +++ b/examples/simple_joystick/json_utils.h @@ -0,0 +1,38 @@ +/* + * Copyright 2025 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. + */ + +#pragma once + +#include + +namespace simple_joystick { + +/// Represents a joystick command with three axes. +struct JoystickCommand { + double x = 0.0; + double y = 0.0; + double z = 0.0; +}; + +/// Serialize a JoystickCommand to a JSON string. +/// Example output: {"x":1.0,"y":2.0,"z":3.0} +std::string joystick_to_json(const JoystickCommand &cmd); + +/// Deserialize a JSON string into a JoystickCommand. +/// Throws std::runtime_error if the JSON is invalid or missing fields. +JoystickCommand json_to_joystick(const std::string &json); + +} // namespace simple_joystick diff --git a/examples/simple_joystick/receiver.cpp b/examples/simple_joystick/receiver.cpp new file mode 100644 index 00000000..d62785a4 --- /dev/null +++ b/examples/simple_joystick/receiver.cpp @@ -0,0 +1,126 @@ +/* + * Copyright 2025 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 +#include +#include +#include +#include +#include + +#include "json_utils.h" +#include "livekit/livekit.h" +#include "utils.h" + +using namespace livekit; +using namespace std::chrono_literals; + +namespace { + +std::atomic g_running{true}; +std::atomic g_sender_connected{false}; + +void handleSignal(int) { g_running.store(false); } + +void printUsage(const char *prog) { + std::cerr << "Usage:\n" + << " " << prog << " \n" + << "or:\n" + << " " << prog << " --url= --token=\n\n" + << "Env fallbacks:\n" + << " LIVEKIT_URL, LIVEKIT_TOKEN\n\n" + << "This is the receiver. It waits for a sender peer to\n" + << "connect and send joystick commands via RPC.\n" + << "Exits after 2 minutes if no commands are received.\n"; +} + +} // namespace + +int main(int argc, char *argv[]) { + std::string url, token; + if (!simple_joystick::parseArgs(argc, argv, url, token)) { + printUsage(argv[0]); + return 1; + } + + std::cout << "[Receiver] Connecting to: " << url << "\n"; + std::signal(SIGINT, handleSignal); + + livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); + auto room = std::make_unique(); + RoomOptions options; + options.auto_subscribe = true; + options.dynacast = false; + + bool res = room->Connect(url, token, options); + std::cout << "[Receiver] Connect result: " << std::boolalpha << res << "\n"; + if (!res) { + std::cerr << "[Receiver] Failed to connect to room\n"; + livekit::shutdown(); + return 1; + } + + auto info = room->room_info(); + std::cout << "[Receiver] Connected to room: " << info.name << "\n"; + std::cout << "[Receiver] Waiting for sender peer (up to 2 minutes)...\n"; + + // Register RPC handler for joystick commands + LocalParticipant *lp = room->localParticipant(); + lp->registerRpcMethod( + "joystick_command", + [](const RpcInvocationData &data) -> std::optional { + try { + auto cmd = simple_joystick::json_to_joystick(data.payload); + g_sender_connected.store(true); + std::cout << "[Receiver] Joystick from '" << data.caller_identity + << "': x=" << cmd.x << " y=" << cmd.y << " z=" << cmd.z + << "\n"; + return std::optional{"ok"}; + } catch (const std::exception &e) { + std::cerr << "[Receiver] Bad joystick payload: " << e.what() << "\n"; + throw; + } + }); + + std::cout << "[Receiver] RPC handler 'joystick_command' registered. " + << "Listening for commands...\n"; + + // Wait up to 2 minutes for activity, then exit as failure + auto deadline = std::chrono::steady_clock::now() + 2min; + + while (g_running.load() && std::chrono::steady_clock::now() < deadline) { + std::this_thread::sleep_for(100ms); + } + + if (!g_running.load()) { + std::cout << "[Receiver] Interrupted by signal. Shutting down.\n"; + } else if (!g_sender_connected.load()) { + std::cerr + << "[Receiver] Timed out after 2 minutes with no sender connection. " + << "Exiting as failure.\n"; + room->setDelegate(nullptr); + room.reset(); + livekit::shutdown(); + return 1; + } else { + std::cout << "[Receiver] Session complete.\n"; + } + + room->setDelegate(nullptr); + room.reset(); + livekit::shutdown(); + return 0; +} diff --git a/examples/simple_joystick/sender.cpp b/examples/simple_joystick/sender.cpp new file mode 100644 index 00000000..a235c3da --- /dev/null +++ b/examples/simple_joystick/sender.cpp @@ -0,0 +1,268 @@ +/* + * Copyright 2025 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 +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif + +#include "json_utils.h" +#include "livekit/livekit.h" +#include "utils.h" + +using namespace livekit; +using namespace std::chrono_literals; + +namespace { + +std::atomic g_running{true}; + +void handleSignal(int) { g_running.store(false); } + +// --- Raw terminal input helpers --- + +#ifndef _WIN32 +struct termios g_orig_termios; +bool g_raw_mode_enabled = false; + +void disableRawMode() { + if (g_raw_mode_enabled) { + tcsetattr(STDIN_FILENO, TCSAFLUSH, &g_orig_termios); + g_raw_mode_enabled = false; + } +} + +void enableRawMode() { + tcgetattr(STDIN_FILENO, &g_orig_termios); + g_raw_mode_enabled = true; + std::atexit(disableRawMode); + + struct termios raw = g_orig_termios; + raw.c_lflag &= ~(ECHO | ICANON); // disable echo and canonical mode + raw.c_cc[VMIN] = 0; // non-blocking read + raw.c_cc[VTIME] = 0; + tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw); +} + +// Returns -1 if no key is available, otherwise the character code. +int readKeyNonBlocking() { + fd_set fds; + FD_ZERO(&fds); + FD_SET(STDIN_FILENO, &fds); + struct timeval tv = {0, 0}; // immediate return + if (select(STDIN_FILENO + 1, &fds, nullptr, nullptr, &tv) > 0) { + unsigned char ch; + if (read(STDIN_FILENO, &ch, 1) == 1) + return ch; + } + return -1; +} +#else +void enableRawMode() { /* Windows _getch() is already unbuffered */ } +void disableRawMode() {} + +int readKeyNonBlocking() { + if (_kbhit()) + return _getch(); + return -1; +} +#endif + +void printUsage(const char *prog) { + std::cerr << "Usage:\n" + << " " << prog << " \n" + << "or:\n" + << " " << prog << " --url= --token=\n\n" + << "Env fallbacks:\n" + << " LIVEKIT_URL, LIVEKIT_TOKEN\n\n" + << "This is the sender. It connects to the room and\n" + << "continuously checks for a receiver peer every 2 seconds.\n" + << "Once connected, use keyboard to send joystick commands:\n" + << " w / s = +x / -x\n" + << " d / a = +y / -y\n" + << " z / c = +z / -z\n" + << " q = quit\n" + << "Automatically reconnects if receiver leaves.\n"; +} + +void printControls() { + std::cout << "\n" + << " Controls:\n" + << " w / s = +x / -x\n" + << " d / a = +y / -y\n" + << " z / c = +z / -z\n" + << " q = quit\n\n"; +} + +} // namespace + +int main(int argc, char *argv[]) { + std::string url, token; + if (!simple_joystick::parseArgs(argc, argv, url, token)) { + printUsage(argv[0]); + return 1; + } + + std::cout << "[Sender] Connecting to: " << url << "\n"; + std::signal(SIGINT, handleSignal); + + livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); + auto room = std::make_unique(); + RoomOptions options; + options.auto_subscribe = true; + options.dynacast = false; + + bool res = room->Connect(url, token, options); + std::cout << "[Sender] Connect result: " << std::boolalpha << res << "\n"; + if (!res) { + std::cerr << "[Sender] Failed to connect to room\n"; + livekit::shutdown(); + return 1; + } + + auto info = room->room_info(); + std::cout << "[Sender] Connected to room: " << info.name << "\n"; + + // Enable raw terminal mode for immediate keypress detection + enableRawMode(); + + std::cout << "[Sender] Waiting for 'robot' to join (checking every 2s)...\n"; + printControls(); + + LocalParticipant *lp = room->localParticipant(); + double x = 0.0, y = 0.0, z = 0.0; + bool receiver_connected = false; + auto last_receiver_check = std::chrono::steady_clock::now(); + + while (g_running.load()) { + // Periodically check receiver presence every 2 seconds + auto now = std::chrono::steady_clock::now(); + if (now - last_receiver_check >= 2s) { + last_receiver_check = now; + bool receiver_present = (room->remoteParticipant("robot") != nullptr); + + if (receiver_present && !receiver_connected) { + std::cout + << "[Sender] Receiver connected! Use keys to send commands.\n"; + receiver_connected = true; + } else if (!receiver_present && receiver_connected) { + std::cout + << "[Sender] Receiver disconnected. Waiting for reconnect...\n"; + receiver_connected = false; + } + } + + // Poll for keypress (non-blocking) + int key = readKeyNonBlocking(); + if (key == -1) { + std::this_thread::sleep_for(20ms); // avoid busy-wait + continue; + } + + // Handle quit + if (key == 'q' || key == 'Q') { + std::cout << "\n[Sender] Quit requested.\n"; + break; + } + + // Map key to axis change + bool changed = false; + switch (key) { + case 'w': + case 'W': + x += 1.0; + changed = true; + break; + case 's': + case 'S': + x -= 1.0; + changed = true; + break; + case 'd': + case 'D': + y += 1.0; + changed = true; + break; + case 'a': + case 'A': + y -= 1.0; + changed = true; + break; + case 'z': + case 'Z': + z += 1.0; + changed = true; + break; + case 'c': + case 'C': + z -= 1.0; + changed = true; + break; + default: + break; + } + + if (!changed) + continue; + + if (!receiver_connected) { + std::cout << "[Sender] (no receiver connected) x=" << x << " y=" << y + << " z=" << z << "\n"; + continue; + } + + // Send joystick command via RPC + simple_joystick::JoystickCommand cmd{x, y, z}; + std::string payload = simple_joystick::joystick_to_json(cmd); + + std::cout << "[Sender] Sending: x=" << x << " y=" << y << " z=" << z + << "\n"; + + try { + std::string response = + lp->performRpc("robot", "joystick_command", payload, 5.0); + std::cout << "[Sender] Receiver acknowledged: " << response << "\n"; + } catch (const RpcError &e) { + std::cerr << "[Sender] RPC error: " << e.message() << "\n"; + if (static_cast(e.code()) == + RpcError::ErrorCode::RECIPIENT_DISCONNECTED) { + std::cout + << "[Sender] Receiver disconnected. Waiting for reconnect...\n"; + receiver_connected = false; + } + } catch (const std::exception &e) { + std::cerr << "[Sender] Error sending command: " << e.what() << "\n"; + } + } + + disableRawMode(); + + std::cout << "[Sender] Done. Shutting down.\n"; + room->setDelegate(nullptr); + room.reset(); + livekit::shutdown(); + return 0; +} diff --git a/examples/simple_joystick/utils.cpp b/examples/simple_joystick/utils.cpp new file mode 100644 index 00000000..cc0ef96c --- /dev/null +++ b/examples/simple_joystick/utils.cpp @@ -0,0 +1,87 @@ +/* + * Copyright 2025 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 "utils.h" + +#include +#include +#include + +namespace simple_joystick { + +bool parseArgs(int argc, char *argv[], std::string &url, std::string &token) { + for (int i = 1; i < argc; ++i) { + std::string a = argv[i]; + if (a == "-h" || a == "--help") { + return false; + } + } + + auto get_flag_value = [&](const std::string &name, int &i) -> std::string { + std::string arg = argv[i]; + const std::string eq = name + "="; + if (arg.rfind(name, 0) == 0) { + if (arg.size() > name.size() && arg[name.size()] == '=') { + return arg.substr(eq.size()); + } else if (i + 1 < argc) { + return std::string(argv[++i]); + } + } + return {}; + }; + + for (int i = 1; i < argc; ++i) { + const std::string a = argv[i]; + if (a.rfind("--url", 0) == 0) { + auto v = get_flag_value("--url", i); + if (!v.empty()) + url = v; + } else if (a.rfind("--token", 0) == 0) { + auto v = get_flag_value("--token", i); + if (!v.empty()) + token = v; + } + } + + // Positional args: + std::vector pos; + for (int i = 1; i < argc; ++i) { + std::string a = argv[i]; + if (a.rfind("--", 0) == 0) + continue; + pos.push_back(std::move(a)); + } + if (url.empty() && pos.size() >= 1) + url = pos[0]; + if (token.empty() && pos.size() >= 2) + token = pos[1]; + + // Environment variable fallbacks + if (url.empty()) { + const char *e = std::getenv("LIVEKIT_URL"); + if (e) + url = e; + } + if (token.empty()) { + const char *e = std::getenv("LIVEKIT_TOKEN"); + if (e) + token = e; + } + + return !(url.empty() || token.empty()); +} + +} // namespace simple_joystick diff --git a/examples/simple_joystick/utils.h b/examples/simple_joystick/utils.h new file mode 100644 index 00000000..7fe94eec --- /dev/null +++ b/examples/simple_joystick/utils.h @@ -0,0 +1,31 @@ +/* + * Copyright 2025 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. + */ + +#pragma once + +#include + +namespace simple_joystick { + +/// Parse command-line arguments for --url and --token. +/// Supports: +/// - Positional: +/// - Flags: --url= / --url , --token= / --token +/// - Env vars: LIVEKIT_URL, LIVEKIT_TOKEN +/// Returns true if both url and token were resolved, false otherwise. +bool parseArgs(int argc, char *argv[], std::string &url, std::string &token); + +} // namespace simple_joystick diff --git a/examples/simple_room/fallback_capture.cpp b/examples/simple_room/fallback_capture.cpp new file mode 100644 index 00000000..40f6194c --- /dev/null +++ b/examples/simple_room/fallback_capture.cpp @@ -0,0 +1,119 @@ +/* + * 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 "fallback_capture.h" + +#include +#include +#include +#include +#include +#include + +#include "livekit/livekit.h" +#include "wav_audio_source.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; + + // FIX: variable name should not shadow the type + WavAudioSource wavSource("data/welcome.wav", 48000, 1, false); + + 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); + wavSource.fillFrame(frame); + try { + source->captureFrame(frame); + } catch (const std::exception &e) { + std::cerr << "Error in captureFrame (noise): " << e.what() << "\n"; + break; + } + + // Pace the loop to roughly real-time + next_deadline += std::chrono::milliseconds(frame_ms); + std::this_thread::sleep_until(next_deadline); + } + + try { + source->clearQueue(); + } catch (...) { + std::cerr << "Error in clearQueue (noise)\n"; + } +} + +// Fake video source: solid color cycling +void runFakeVideoCaptureLoop(const std::shared_ptr &source, + std::atomic &running_flag) { + auto frame = VideoFrame::create(1280, 720, VideoBufferType::BGRA); + 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() << "\n"; + break; + } + + std::this_thread::sleep_for(std::chrono::duration(framerate)); + } +} diff --git a/examples/simple_room/fallback_capture.h b/examples/simple_room/fallback_capture.h new file mode 100644 index 00000000..a7d85361 --- /dev/null +++ b/examples/simple_room/fallback_capture.h @@ -0,0 +1,35 @@ +/* + * 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 + +// Assuming you already have this somewhere: +extern std::atomic g_running; + +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/examples/simple_room/main.cpp b/examples/simple_room/main.cpp new file mode 100644 index 00000000..717519a9 --- /dev/null +++ b/examples/simple_room/main.cpp @@ -0,0 +1,417 @@ +/* + * 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 +#include +#include + +#include "livekit/livekit.h" +#include "sdl_media_manager.h" +#include "wav_audio_source.h" + +using namespace livekit; + +namespace { + +std::atomic g_running{true}; + +void printUsage(const char *prog) { + std::cerr + << "Usage:\n" + << " " << prog + << " [--enable_e2ee] [--e2ee_key ]\n" + << "or:\n" + << " " << prog + << " --url= --token= [--enable_e2ee] [--e2ee_key=]\n" + << " " << prog + << " --url --token [--enable_e2ee] [--e2ee_key " + "]\n\n" + << "E2EE:\n" + << " --enable_e2ee Enable end-to-end encryption (E2EE)\n" + << " --e2ee_key Optional shared key (UTF-8). If omitted, " + "E2EE is enabled\n" + << " but no shared key is set (advanced " + "usage).\n\n" + << "Env fallbacks:\n" + << " LIVEKIT_URL, LIVEKIT_TOKEN, LIVEKIT_E2EE_KEY\n"; +} + +void handleSignal(int) { g_running.store(false); } + +bool parseArgs(int argc, char *argv[], std::string &url, std::string &token, + bool &enable_e2ee, std::string &e2ee_key) { + enable_e2ee = false; + // --help + for (int i = 1; i < argc; ++i) { + std::string a = argv[i]; + if (a == "-h" || a == "--help") { + return false; + } + } + + // flags: --url= / --token= or split form + auto get_flag_value = [&](const std::string &name, int &i) -> std::string { + std::string arg = argv[i]; + const std::string eq = name + "="; + if (arg.rfind(name, 0) == 0) { // starts with name + if (arg.size() > name.size() && arg[name.size()] == '=') { + return arg.substr(eq.size()); + } else if (i + 1 < argc) { + return std::string(argv[++i]); + } + } + return {}; + }; + + for (int i = 1; i < argc; ++i) { + const std::string a = argv[i]; + if (a == "--enable_e2ee") { + enable_e2ee = true; + } else if (a.rfind("--url", 0) == 0) { + auto v = get_flag_value("--url", i); + if (!v.empty()) + url = v; + } else if (a.rfind("--token", 0) == 0) { + auto v = get_flag_value("--token", i); + if (!v.empty()) + token = v; + } else if (a.rfind("--e2ee_key", 0) == 0) { + auto v = get_flag_value("--e2ee_key", i); + if (!v.empty()) + e2ee_key = v; + } + } + + // positional if still empty + if (url.empty() || token.empty()) { + std::vector pos; + for (int i = 1; i < argc; ++i) { + std::string a = argv[i]; + if (a.rfind("--", 0) == 0) + continue; // skip flags we already parsed + pos.push_back(std::move(a)); + } + if (pos.size() >= 2) { + if (url.empty()) + url = pos[0]; + if (token.empty()) + token = pos[1]; + } + } + + // 4) env fallbacks + if (url.empty()) { + const char *e = std::getenv("LIVEKIT_URL"); + if (e) + url = e; + } + if (token.empty()) { + const char *e = std::getenv("LIVEKIT_TOKEN"); + if (e) + token = e; + } + if (e2ee_key.empty()) { + const char *e = std::getenv("LIVEKIT_E2EE_KEY"); + if (e) + e2ee_key = e; + } + + return !(url.empty() || token.empty()); +} + +class MainThreadDispatcher { +public: + static void dispatch(std::function fn) { + std::lock_guard lock(mutex_); + queue_.push(std::move(fn)); + } + + static void update() { + std::queue> local; + + { + std::lock_guard lock(mutex_); + std::swap(local, queue_); + } + + // Run everything on main thread + while (!local.empty()) { + local.front()(); + local.pop(); + } + } + +private: + static inline std::mutex mutex_; + static inline std::queue> queue_; +}; + +class SimpleRoomDelegate : public livekit::RoomDelegate { +public: + explicit SimpleRoomDelegate(SDLMediaManager &media) : media_(media) {} + + void onParticipantConnected( + livekit::Room & /*room*/, + const livekit::ParticipantConnectedEvent &ev) override { + std::cout << "[Room] participant connected: identity=" + << ev.participant->identity() + << " name=" << ev.participant->name() << "\n"; + } + + void onTrackSubscribed(livekit::Room & /*room*/, + const livekit::TrackSubscribedEvent &ev) override { + const char *participant_identity = + ev.participant ? ev.participant->identity().c_str() : ""; + const std::string track_sid = + ev.publication ? ev.publication->sid() : ""; + const std::string track_name = + ev.publication ? ev.publication->name() : ""; + std::cout << "[Room] track subscribed: participant_identity=" + << participant_identity << " track_sid=" << track_sid + << " name=" << track_name; + if (ev.track) { + std::cout << " kind=" << static_cast(ev.track->kind()); + } + if (ev.publication) { + std::cout << " source=" << static_cast(ev.publication->source()); + } + std::cout << std::endl; + + // If this is a VIDEO track, create a VideoStream and attach to renderer + if (ev.track && ev.track->kind() == TrackKind::KIND_VIDEO) { + VideoStream::Options opts; + opts.format = livekit::VideoBufferType::RGBA; + auto video_stream = VideoStream::fromTrack(ev.track, opts); + if (!video_stream) { + std::cerr << "Failed to create VideoStream for track " << track_sid + << "\n"; + return; + } + + MainThreadDispatcher::dispatch([this, video_stream] { + if (!media_.initRenderer(video_stream)) { + std::cerr << "SDLMediaManager::startRenderer failed for track\n"; + } + }); + } else if (ev.track && ev.track->kind() == TrackKind::KIND_AUDIO) { + AudioStream::Options opts; + auto audio_stream = AudioStream::fromTrack(ev.track, opts); + MainThreadDispatcher::dispatch([this, audio_stream] { + if (!media_.startSpeaker(audio_stream)) { + std::cerr << "SDLMediaManager::startSpeaker failed for track\n"; + } + }); + } + } + +private: + SDLMediaManager &media_; +}; + +static std::vector toBytes(const std::string &s) { + return std::vector(s.begin(), s.end()); +} + +void print_livekit_version() { + std::cout << "LiveKit version: " << LIVEKIT_BUILD_VERSION_FULL << " (" + << LIVEKIT_BUILD_FLAVOR << ", commit " << LIVEKIT_BUILD_COMMIT + << ", built " << LIVEKIT_BUILD_DATE << ")" << std::endl; +} + +} // namespace + +int main(int argc, char *argv[]) { + print_livekit_version(); + std::string url, token; + bool enable_e2ee = false; + std::string e2ee_key; + if (!parseArgs(argc, argv, url, token, enable_e2ee, e2ee_key)) { + printUsage(argv[0]); + return 1; + } + + // Exit if token and url are not set + if (url.empty() || token.empty()) { + std::cerr << "LIVEKIT_URL and LIVEKIT_TOKEN (or CLI args) are required\n"; + return 1; + } + + if (!SDL_Init(SDL_INIT_VIDEO)) { + std::cerr << "SDL_Init(SDL_INIT_VIDEO) failed: " << SDL_GetError() << "\n"; + // You can choose to exit, or run in "headless" mode without renderer. + // return 1; + } + + // Setup media; + SDLMediaManager media; + + std::cout << "Connecting to: " << url << std::endl; + + // Handle Ctrl-C to exit the idle loop + std::signal(SIGINT, handleSignal); + + livekit::initialize(); + auto room = std::make_unique(); + SimpleRoomDelegate delegate(media); + room->setDelegate(&delegate); + + RoomOptions options; + options.auto_subscribe = true; + options.dynacast = false; + + if (enable_e2ee) { + livekit::E2EEOptions encryption; + encryption.encryption_type = livekit::EncryptionType::GCM; + // Optional shared key: if empty, we enable E2EE without setting a shared + // key. (Advanced use: keys can be set/ratcheted later via + // E2EEManager/KeyProvider.) + if (!e2ee_key.empty()) { + encryption.key_provider_options.shared_key = toBytes(e2ee_key); + } + options.encryption = encryption; + if (!e2ee_key.empty()) { + std::cout << "[E2EE] enabled : (shared key length=" << e2ee_key.size() + << ")\n"; + } else { + std::cout << "[E2EE] enabled: (no shared key set)\n"; + } + } + + bool res = room->Connect(url, token, options); + std::cout << "Connect result is " << std::boolalpha << res << std::endl; + if (!res) { + std::cerr << "Failed to connect to room\n"; + livekit::shutdown(); + return 1; + } + + auto info = room->room_info(); + std::cout << "Connected to room:\n" + << " SID: " << (info.sid ? *info.sid : "(none)") << "\n" + << " Name: " << info.name << "\n" + << " Metadata: " << info.metadata << "\n" + << " Max participants: " << info.max_participants << "\n" + << " Num participants: " << info.num_participants << "\n" + << " Num publishers: " << info.num_publishers << "\n" + << " Active recording: " << (info.active_recording ? "yes" : "no") + << "\n" + << " Empty timeout (s): " << info.empty_timeout << "\n" + << " Departure timeout (s): " << info.departure_timeout << "\n" + << " Lossy DC low threshold: " + << info.lossy_dc_buffered_amount_low_threshold << "\n" + << " Reliable DC low threshold: " + << info.reliable_dc_buffered_amount_low_threshold << "\n" + << " Creation time (ms): " << info.creation_time << "\n"; + + // Setup Audio Source / Track + auto audioSource = std::make_shared(44100, 1, 0); + auto audioTrack = + LocalAudioTrack::createLocalAudioTrack("micTrack", audioSource); + + TrackPublishOptions audioOpts; + audioOpts.source = TrackSource::SOURCE_MICROPHONE; + audioOpts.dtx = false; + audioOpts.simulcast = false; + try { + room->localParticipant()->publishTrack(audioTrack, audioOpts); + const auto audioPub = audioTrack->publication(); + + std::cout << "Published track:\n" + << " SID: " << audioPub->sid() << "\n" + << " Name: " << audioPub->name() << "\n" + << " Kind: " << static_cast(audioPub->kind()) << "\n" + << " Source: " << static_cast(audioPub->source()) << "\n" + << " Simulcasted: " << std::boolalpha << audioPub->simulcasted() + << "\n" + << " Muted: " << std::boolalpha << audioPub->muted() << "\n"; + } catch (const std::exception &e) { + std::cerr << "Failed to publish track: " << e.what() << "\n"; + } + + media.startMic(audioSource); + + // Setup Video Source / Track + auto videoSource = std::make_shared(1280, 720); + auto videoTrack = LocalVideoTrack::createLocalVideoTrack("cam", videoSource); + + TrackPublishOptions videoOpts; + videoOpts.source = TrackSource::SOURCE_CAMERA; + videoOpts.dtx = false; + videoOpts.simulcast = true; + try { + // publishTrack takes std::shared_ptr, LocalAudioTrack derives from + // Track + room->localParticipant()->publishTrack(videoTrack, videoOpts); + + const auto videoPub = videoTrack->publication(); + + std::cout << "Published track:\n" + << " SID: " << videoPub->sid() << "\n" + << " Name: " << videoPub->name() << "\n" + << " Kind: " << static_cast(videoPub->kind()) << "\n" + << " Source: " << static_cast(videoPub->source()) << "\n" + << " Simulcasted: " << std::boolalpha << videoPub->simulcasted() + << "\n" + << " Muted: " << std::boolalpha << videoPub->muted() << "\n"; + } catch (const std::exception &e) { + std::cerr << "Failed to publish track: " << e.what() << "\n"; + } + media.startCamera(videoSource); + + // Keep the app alive until Ctrl-C so we continue receiving events, + // similar to asyncio.run(main()) keeping the loop running. + while (g_running.load()) { + MainThreadDispatcher::update(); + media.render(); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + // Shutdown the audio / video capture threads. + media.stopMic(); + media.stopCamera(); + media.stopSpeaker(); + media.shutdownRenderer(); + + // Drain any queued tasks that might still try to update the renderer / + // speaker + MainThreadDispatcher::update(); + + // Must be cleaned up before FfiClient::instance().shutdown(); + room->setDelegate(nullptr); + + if (audioTrack->publication()) { + room->localParticipant()->unpublishTrack(audioTrack->publication()->sid()); + } + if (videoTrack->publication()) { + room->localParticipant()->unpublishTrack(videoTrack->publication()->sid()); + } + audioTrack.reset(); + videoTrack.reset(); + + room.reset(); + + livekit::shutdown(); + std::cout << "Exiting.\n"; + return 0; +} diff --git a/examples/simple_rpc/README.md b/examples/simple_rpc/README.md new file mode 100644 index 00000000..2ded78c7 --- /dev/null +++ b/examples/simple_rpc/README.md @@ -0,0 +1,157 @@ +# πŸ“˜ SimpleRpc Example β€” Technical Overview + +This README provides deeper technical details about the RPC (Remote Procedure Call) support demonstrated in the SimpleRpc example. +It complements the example instructions found in the root README.md. + +If you're looking for how to run the example, see the root [README](https://github.com/livekit/client-sdk-cpp). + +This document explains: +- How LiveKit RPC works in the C++ SDK +- Where the APIs are defined +- How senders call RPC methods +- How receivers register handlers +- What happens if the receiver is absent +- How long-running operations behave +- Timeouts, disconnects, and unsupported methods +- RPC lifecycle events and error propagation + +## πŸ”§ Overview: How RPC Works +LiveKit RPC allows one participant (the caller) to invoke a method on another participant (the receiver) using the data channel transport. +It is: +- Peer-to-peer within the room (not server-executed RPC) +- Request/response (caller waits for a reply or an error) +- Asynchronous under the hood, synchronous or blocking from the caller’s perspective +- Delivery-guaranteed when using the reliable data channel + +Each RPC call includes: +| Field | Meaning | +|--------------------------|-----------------------------------------------------| +| **destination_identity** | Identity of the target participant | +| **method** | Method name string (e.g., "square-root") | +| **payload** | Arbitrary UTF-8 text | +| **response_timeout** | Optional timeout (seconds) | +| **invocation_id** | Server-generated ID used internally for correlation | + +## πŸ“ Location of APIs in C++ +All public-facing RPC APIs live in: +[include/livekit/local_participant.h](https://github.com/livekit/client-sdk-cpp/blob/main/include/livekit/local_participant.h#L160) + +### Key methods: + +#### Sender-side APIs +```bash +std::string performRpc( + const std::string& destination_identity, + const std::string& method, + const std::string& payload, + std::optional response_timeout_sec = std::nullopt +); + +Receiver-side APIs +void registerRpcMethod( + const std::string& method_name, + RpcHandler handler +); + +void unregisterRpcMethod(const std::string& method_name); + +Handler signature +using RpcHandler = + std::function(const RpcInvocationData&)>; +``` + +Handlers can: +- Return a string (the RPC response payload) +- Return std::nullopt (meaning β€œno return payload”) +- Throw exceptions (mapped to APPLICATION_ERROR) +- Throw a RpcError (mapped to specific error codes) + +#### πŸ›° Sender Behavior (performRpc) + +When the caller invokes: +```bash +auto reply = lp->performRpc("math-genius", "square-root", "{\"number\":16}"); +``` + +The following occurs: + +A PerformRpcRequest is sent through FFI to the SDK core. + +The SDK transmits the invocation to the target participant (if present). + +The caller begins waiting for a matching RpcMethodInvocationResponse. + +One of the following happens: +| Outcome | Meaning | +|--------------------------|------------------------------------------| +| **Success** | Receiver returned a payload | +| **UNSUPPORTED_METHOD** | Receiver did not register the method | +| **RECIPIENT_NOT_FOUND** | Target identity not present in room | +| **RECIPIENT_DISCONNECTED** | Target left before replying | +| **RESPONSE_TIMEOUT** | Receiver took too long | +| **APPLICATION_ERROR** | Handler threw an exception | + +#### πŸ”„ Round-trip time (RTT) + +The caller can measure RTT externally (as SimpleRpc does), but the SDK does not measure RTT internally. + +#### πŸ“‘ Receiver Behavior (registerRpcMethod) + +A receiver must explicitly register handlers: +```bash +local_participant->registerRpcMethod("square-root", + [](const RpcInvocationData& data) { + double number = parse(data.payload); + return make_json("result", std::sqrt(number)); + }); +``` + +When an invocation arrives: +- Room receives a RpcMethodInvocationEvent +- Room forwards it to the corresponding LocalParticipant +- LocalParticipant::handleRpcMethodInvocation(): +- Calls the handler +- Converts any exceptions into RpcError +- Sends back RpcMethodInvocationResponse + +⚠ If no handler exists: + +Receiver returns: UNSUPPORTED_METHOD + + +#### 🚨 What Happens if Receiver Is Absent? +| Case | Behavior | +|-----------------------------------------------------|---------------------------------------------------| +| Receiver identity is not in the room | Caller immediately receives `RECIPIENT_NOT_FOUND` | +| Receiver is present but disconnects before replying | Caller receives `RECIPIENT_DISCONNECTED` | +| Receiver joins later | Caller must retry manually (no automatic waiting) | + +**Important**: +LiveKit does not queue RPC calls for offline participants. + +#### ⏳ Timeout Behavior + +If the caller specifies: + +performRpc(..., /*response_timeout=*/10.0); + +Then: +- Receiver is given 10 seconds to respond. +- If the receiver handler takes longer (e.g., sleep 30s), caller receives: +RESPONSE_TIMEOUT + +**If no response_timeout is provided explicitly, the default timeout is 15 seconds.** + + +This is by design and demonstrated in the example. + +#### 🧨 Errors & Failure Modes +| Error Code | Cause | +|------------------------|---------------------------------------------| +| **APPLICATION_ERROR** | Handler threw a C++ exception | +| **UNSUPPORTED_METHOD** | No handler registered for the method | +| **RECIPIENT_NOT_FOUND** | Destination identity not in room | +| **RECIPIENT_DISCONNECTED** | Participant left mid-flight | +| **RESPONSE_TIMEOUT** | Handler exceeded allowed response time | +| **CONNECTION_TIMEOUT** | Transport-level issue | +| **SEND_FAILED** | SDK failed to send invocation | diff --git a/examples/simple_rpc/main.cpp b/examples/simple_rpc/main.cpp new file mode 100644 index 00000000..b171f9b5 --- /dev/null +++ b/examples/simple_rpc/main.cpp @@ -0,0 +1,547 @@ +/* + * Copyright 2025 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "livekit/livekit.h" + +using namespace livekit; +using namespace std::chrono_literals; + +namespace { + +std::atomic g_running{true}; + +void handleSignal(int) { g_running.store(false); } + +void printUsage(const char *prog) { + std::cerr << "Usage:\n" + << " " << prog << " [role]\n" + << "or:\n" + << " " << prog + << " --url= --token= [--role=]\n" + << " " << prog + << " --url --token [--role ]\n\n" + << "Env fallbacks:\n" + << " LIVEKIT_URL, LIVEKIT_TOKEN\n" + << "Role (participant behavior):\n" + << " SIMPLE_RPC_ROLE or --role=\n" + << " default: caller\n"; +} + +inline double nowMs() { + return std::chrono::duration( + std::chrono::steady_clock::now().time_since_epoch()) + .count(); +} + +// Poll the room until a remote participant with the given identity appears, +// or until 'timeout' elapses. Returns true if found, false on timeout. +bool waitForParticipant(Room *room, const std::string &identity, + std::chrono::milliseconds timeout) { + auto start = std::chrono::steady_clock::now(); + + while (std::chrono::steady_clock::now() - start < timeout) { + if (room->remoteParticipant(identity) != nullptr) { + return true; + } + std::this_thread::sleep_for(100ms); + } + return false; +} + +// For the caller: wait for a specific peer, and if they don't show up, +// explain why and how to start them in another terminal. +bool ensurePeerPresent(Room *room, const std::string &identity, + const std::string &friendly_role, const std::string &url, + std::chrono::seconds timeout) { + std::cout << "[Caller] Waiting up to " << timeout.count() << "s for " + << friendly_role << " (identity=\"" << identity + << "\") to join...\n"; + bool present = waitForParticipant( + room, identity, + std::chrono::duration_cast(timeout)); + if (present) { + std::cout << "[Caller] " << friendly_role << " is present.\n"; + return true; + } + // Timed out + auto info = room->room_info(); + const std::string room_name = info.name; + std::cout << "[Caller] Timed out after " << timeout.count() + << "s waiting for " << friendly_role << " (identity=\"" << identity + << "\").\n"; + std::cout << "[Caller] No participant with identity \"" << identity + << "\" appears to be connected to room \"" << room_name + << "\".\n\n"; + std::cout << "To start a " << friendly_role + << " in another terminal, run:\n\n" + << " lk token create -r test -i " << identity + << " --join --valid-for 99999h --dev --room=" << room_name << "\n" + << " ./build/examples/SimpleRpc " << url + << " $token --role=" << friendly_role << "\n\n"; + return false; +} + +bool parseArgs(int argc, char *argv[], std::string &url, std::string &token, + std::string &role) { + // --help + for (int i = 1; i < argc; ++i) { + std::string a = argv[i]; + if (a == "-h" || a == "--help") { + return false; + } + } + + // helper for flags + auto get_flag_value = [&](const std::string &name, int &i) -> std::string { + std::string arg = argv[i]; + const std::string eq = name + "="; + if (arg.rfind(name, 0) == 0) { // starts with name + if (arg.size() > name.size() && arg[name.size()] == '=') { + return arg.substr(eq.size()); + } else if (i + 1 < argc) { + return std::string(argv[++i]); + } + } + return {}; + }; + + // flags: --url / --token / --role (with = or split) + for (int i = 1; i < argc; ++i) { + const std::string a = argv[i]; + if (a.rfind("--url", 0) == 0) { + auto v = get_flag_value("--url", i); + if (!v.empty()) + url = v; + } else if (a.rfind("--token", 0) == 0) { + auto v = get_flag_value("--token", i); + if (!v.empty()) + token = v; + } else if (a.rfind("--role", 0) == 0) { + auto v = get_flag_value("--role", i); + if (!v.empty()) + role = v; + } + } + + std::vector pos; + for (int i = 1; i < argc; ++i) { + std::string a = argv[i]; + if (a.rfind("--", 0) == 0) + continue; + pos.push_back(std::move(a)); + } + if (!pos.empty()) { + if (url.empty() && pos.size() >= 1) { + url = pos[0]; + } + if (token.empty() && pos.size() >= 2) { + token = pos[1]; + } + if (role.empty() && pos.size() >= 3) { + role = pos[2]; + } + } + + if (url.empty()) { + const char *e = std::getenv("LIVEKIT_URL"); + if (e) + url = e; + } + if (token.empty()) { + const char *e = std::getenv("LIVEKIT_TOKEN"); + if (e) + token = e; + } + if (role.empty()) { + const char *e = std::getenv("SIMPLE_RPC_ROLE"); + if (e) + role = e; + } + if (role.empty()) { + role = "caller"; + } + + return !(url.empty() || token.empty()); +} + +std::string makeNumberJson(const std::string &key, double value) { + std::ostringstream oss; + oss << "{\"" << key << "\":" << value << "}"; + return oss.str(); +} + +std::string makeStringJson(const std::string &key, const std::string &value) { + std::ostringstream oss; + oss << "{\"" << key << "\":\"" << value << "\"}"; + return oss.str(); +} + +double parseNumberFromJson(const std::string &json) { + auto colon = json.find(':'); + if (colon == std::string::npos) + throw std::runtime_error("invalid json: " + json); + auto start = colon + 1; + auto end = json.find_first_of(",}", start); + std::string num_str = json.substr(start, end - start); + return std::stod(num_str); +} + +std::string parseStringFromJson(const std::string &json) { + auto colon = json.find(':'); + if (colon == std::string::npos) + throw std::runtime_error("invalid json: " + json); + auto first_quote = json.find('"', colon + 1); + if (first_quote == std::string::npos) + throw std::runtime_error("invalid json: " + json); + auto second_quote = json.find('"', first_quote + 1); + if (second_quote == std::string::npos) + throw std::runtime_error("invalid json: " + json); + return json.substr(first_quote + 1, second_quote - first_quote - 1); +} + +// RPC handler registration +void registerReceiverMethods(Room *greeters_room, Room *math_genius_room) { + LocalParticipant *greeter_lp = greeters_room->localParticipant(); + LocalParticipant *math_genius_lp = math_genius_room->localParticipant(); + + // arrival + greeter_lp->registerRpcMethod( + "arrival", + [](const RpcInvocationData &data) -> std::optional { + std::cout << "[Greeter] Oh " << data.caller_identity + << " arrived and said \"" << data.payload << "\"\n"; + std::this_thread::sleep_for(2s); + return std::optional{"Welcome and have a wonderful day!"}; + }); + + // square-root + math_genius_lp->registerRpcMethod( + "square-root", + [](const RpcInvocationData &data) -> std::optional { + double number = parseNumberFromJson(data.payload); + std::cout << "[Math Genius] I guess " << data.caller_identity + << " wants the square root of " << number + << ". I've only got " << data.response_timeout_sec + << " seconds to respond but I think I can pull it off.\n"; + std::cout << "[Math Genius] *doing math*…\n"; + std::this_thread::sleep_for(2s); + double result = std::sqrt(number); + std::cout << "[Math Genius] Aha! It's " << result << "\n"; + return makeNumberJson("result", result); + }); + + // divide + math_genius_lp->registerRpcMethod( + "divide", + [](const RpcInvocationData &data) -> std::optional { + // expect {"dividend":X,"divisor":Y} – we'll parse very lazily + auto div_pos = data.payload.find("dividend"); + auto dvr_pos = data.payload.find("divisor"); + if (div_pos == std::string::npos || dvr_pos == std::string::npos) { + throw std::runtime_error("invalid divide payload"); + } + + double dividend = parseNumberFromJson( + data.payload.substr(div_pos, dvr_pos - div_pos - 1)); // rough slice + double divisor = parseNumberFromJson(data.payload.substr(dvr_pos)); + + std::cout << "[Math Genius] " << data.caller_identity + << " wants to divide " << dividend << " by " << divisor + << ".\n"; + + if (divisor == 0.0) { + // will be translated to APPLICATION_ERROR by your RpcError logic + throw std::runtime_error("division by zero"); + } + + double result = dividend / divisor; + return makeNumberJson("result", result); + }); + + // long-calculation + math_genius_lp->registerRpcMethod( + "long-calculation", + [](const RpcInvocationData &data) -> std::optional { + std::cout << "[Math Genius] Starting a very long calculation for " + << data.caller_identity << "\n"; + std::cout << "[Math Genius] This will take 30 seconds even though " + "you're only giving me " + << data.response_timeout_sec << " seconds\n"; + // Sleep for 30 seconds to mimic a long running task. + std::this_thread::sleep_for(30s); + return makeStringJson("result", "Calculation complete!"); + }); + + // Note: we do NOT register "quantum-hypergeometric-series" here, + // so the caller sees UNSUPPORTED_METHOD +} + +void performGreeting(Room *room) { + std::cout << "[Caller] Letting the greeter know that I've arrived\n"; + double t0 = nowMs(); + try { + std::string response = room->localParticipant()->performRpc( + "greeter", "arrival", "Hello", std::nullopt); + double t1 = nowMs(); + std::cout << "[Caller] RTT: " << (t1 - t0) << " ms\n"; + std::cout << "[Caller] That's nice, the greeter said: \"" << response + << "\"\n"; + } catch (const std::exception &error) { + double t1 = nowMs(); + std::cout << "[Caller] (FAILED) RTT: " << (t1 - t0) << " ms\n"; + std::cout << "[Caller] RPC call failed: " << error.what() << "\n"; + throw; + } +} + +void performSquareRoot(Room *room) { + std::cout << "[Caller] What's the square root of 16?\n"; + double t0 = nowMs(); + try { + std::string payload = makeNumberJson("number", 16.0); + std::string response = room->localParticipant()->performRpc( + "math-genius", "square-root", payload, std::nullopt); + double t1 = nowMs(); + std::cout << "[Caller] RTT: " << (t1 - t0) << " ms\n"; + double result = parseNumberFromJson(response); + std::cout << "[Caller] Nice, the answer was " << result << "\n"; + } catch (const std::exception &error) { + double t1 = nowMs(); + std::cout << "[Caller] (FAILED) RTT: " << (t1 - t0) << " ms\n"; + std::cout << "[Caller] RPC call failed: " << error.what() << "\n"; + throw; + } +} + +void performQuantumHyperGeometricSeries(Room *room) { + std::cout << "\n=== Unsupported Method Example ===\n"; + std::cout + << "[Caller] Asking math-genius for 'quantum-hypergeometric-series'. " + "This should FAIL because the handler is NOT registered.\n"; + double t0 = nowMs(); + try { + std::string payload = makeNumberJson("number", 42.0); + std::string response = room->localParticipant()->performRpc( + "math-genius", "quantum-hypergeometric-series", payload, std::nullopt); + double t1 = nowMs(); + std::cout << "[Caller] (Unexpected success) RTT=" << (t1 - t0) << " ms\n"; + std::cout << "[Caller] Result: " << response << "\n"; + } catch (const RpcError &error) { + double t1 = nowMs(); + std::cout << "[Caller] RpcError RTT=" << (t1 - t0) << " ms\n"; + auto code = static_cast(error.code()); + if (code == RpcError::ErrorCode::UNSUPPORTED_METHOD) { + std::cout << "[Caller] βœ“ Expected: math-genius does NOT implement this " + "method.\n"; + std::cout << "[Caller] Server returned UNSUPPORTED_METHOD.\n"; + } else { + std::cout << "[Caller] βœ— Unexpected error type: " << error.message() + << "\n"; + } + } +} + +void performDivide(Room *room) { + std::cout << "\n=== Divide Example ===\n"; + std::cout << "[Caller] Asking math-genius to divide 10 by 0. " + "This is EXPECTED to FAIL with an APPLICATION_ERROR.\n"; + double t0 = nowMs(); + try { + std::string payload = "{\"dividend\":10,\"divisor\":0}"; + std::string response = room->localParticipant()->performRpc( + "math-genius", "divide", payload, std::nullopt); + double t1 = nowMs(); + std::cout << "[Caller] (Unexpected success) RTT=" << (t1 - t0) << " ms\n"; + std::cout << "[Caller] Result = " << response << "\n"; + } catch (const RpcError &error) { + double t1 = nowMs(); + std::cout << "[Caller] RpcError RTT=" << (t1 - t0) << " ms\n"; + auto code = static_cast(error.code()); + if (code == RpcError::ErrorCode::APPLICATION_ERROR) { + std::cout << "[Caller] βœ“ Expected: divide-by-zero triggers " + "APPLICATION_ERROR.\n"; + std::cout << "[Caller] Math-genius threw an exception: " + << error.message() << "\n"; + } else { + std::cout << "[Caller] βœ— Unexpected RpcError type: " << error.message() + << "\n"; + } + } +} + +void performLongCalculation(Room *room) { + std::cout << "\n=== Long Calculation Example ===\n"; + std::cout + << "[Caller] Asking math-genius for a calculation that takes 30s.\n"; + std::cout + << "[Caller] Giving only 10s to respond. EXPECTED RESULT: TIMEOUT.\n"; + double t0 = nowMs(); + try { + std::string response = room->localParticipant()->performRpc( + "math-genius", "long-calculation", "{}", 10.0); + double t1 = nowMs(); + std::cout << "[Caller] (Unexpected success) RTT=" << (t1 - t0) << " ms\n"; + std::cout << "[Caller] Result: " << response << "\n"; + } catch (const RpcError &error) { + double t1 = nowMs(); + std::cout << "[Caller] RpcError RTT=" << (t1 - t0) << " ms\n"; + auto code = static_cast(error.code()); + if (code == RpcError::ErrorCode::RESPONSE_TIMEOUT) { + std::cout + << "[Caller] βœ“ Expected: handler sleeps 30s but timeout is 10s.\n"; + std::cout << "[Caller] Server correctly returned RESPONSE_TIMEOUT.\n"; + } else if (code == RpcError::ErrorCode::RECIPIENT_DISCONNECTED) { + std::cout << "[Caller] βœ“ Expected if math-genius disconnects during the " + "test.\n"; + } else { + std::cout << "[Caller] βœ— Unexpected RPC error: " << error.message() + << "\n"; + } + } +} + +} // namespace + +int main(int argc, char *argv[]) { + std::string url, token, role; + if (!parseArgs(argc, argv, url, token, role)) { + printUsage(argv[0]); + return 1; + } + + if (url.empty() || token.empty()) { + std::cerr << "LIVEKIT_URL and LIVEKIT_TOKEN (or CLI args) are required\n"; + return 1; + } + + std::cout << "Connecting to: " << url << "\n"; + std::cout << "Role: " << role << "\n"; + + // Ctrl-C to quit the program + std::signal(SIGINT, handleSignal); + + // Initialize the livekit with logging to console. + livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); + auto room = std::make_unique(); + RoomOptions options; + options.auto_subscribe = true; + options.dynacast = false; + + bool res = room->Connect(url, token, options); + std::cout << "Connect result is " << std::boolalpha << res << "\n"; + if (!res) { + std::cerr << "Failed to connect to room\n"; + livekit::shutdown(); + return 1; + } + + auto info = room->room_info(); + std::cout << "Connected to room:\n" + << " Name: " << info.name << "\n" + << " Metadata: " << info.metadata << "\n" + << " Num participants: " << info.num_participants << "\n"; + + try { + if (role == "caller") { + // Check that both peers are present (or explain how to start them). + bool has_greeter = + ensurePeerPresent(room.get(), "greeter", "greeter", url, 8s); + bool has_math_genius = + ensurePeerPresent(room.get(), "math-genius", "math-genius", url, 8s); + if (!has_greeter || !has_math_genius) { + std::cout << "\n[Caller] One or more RPC peers are missing. " + << "Some examples may be skipped.\n"; + } + if (has_greeter) { + std::cout << "\n\nRunning greeting example...\n"; + performGreeting(room.get()); + } else { + std::cout << "[Caller] Skipping greeting example because greeter is " + "not present.\n"; + } + if (has_math_genius) { + std::cout << "\n\nRunning error handling example...\n"; + performDivide(room.get()); + + std::cout << "\n\nRunning math example...\n"; + performSquareRoot(room.get()); + std::this_thread::sleep_for(2s); + performQuantumHyperGeometricSeries(room.get()); + + std::cout << "\n\nRunning long calculation with timeout...\n"; + performLongCalculation(room.get()); + } else { + std::cout << "[Caller] Skipping math examples because math-genius is " + "not present.\n"; + } + + std::cout << "\n\nCaller done. Exiting.\n"; + } else if (role == "greeter" || role == "math-genius") { + // For these roles we expect multiple processes: + // - One process with role=caller + // - One with role=greeter + // - One with role=math-genius + // + // Each process gets its own token (with that identity) via LIVEKIT_TOKEN. + // Here we only register handlers for the appropriate role, and then + // stay alive until Ctrl-C so we can receive RPCs. + + if (role == "greeter") { + // Use the same room object for both arguments; only "arrival" is used. + registerReceiverMethods(room.get(), room.get()); + } else { // math-genius + // We only need math handlers; greeter handlers won't be used. + registerReceiverMethods(room.get(), room.get()); + } + + std::cout << "RPC handlers registered for role=" << role + << ". Waiting for RPC calls (Ctrl-C to exit)...\n"; + + while (g_running.load()) { + std::this_thread::sleep_for(50ms); + } + std::cout << "Exiting receiver role.\n"; + } else { + std::cerr << "Unknown role: " << role << "\n"; + } + } catch (const std::exception &e) { + std::cerr << "Unexpected error in main: " << e.what() << "\n"; + } + + // It is important to clean up the delegate and room in order. + room->setDelegate(nullptr); + room.reset(); + livekit::shutdown(); + return 0; +} diff --git a/examples/tokens/README.md b/examples/tokens/README.md new file mode 100644 index 00000000..ebed99c1 --- /dev/null +++ b/examples/tokens/README.md @@ -0,0 +1,8 @@ +# Overview +Examples of generating tokens + +## gen_and_set.bash +Generate tokens and then set them as env vars for the current terminal session + +## set_data_track_test_tokens.bash +Generate tokens for data track integration tests and set them as env vars for the current terminal session. \ No newline at end of file diff --git a/examples/tokens/gen_and_set.bash b/examples/tokens/gen_and_set.bash new file mode 100755 index 00000000..b933a24f --- /dev/null +++ b/examples/tokens/gen_and_set.bash @@ -0,0 +1,169 @@ +#!/usr/bin/env bash +# Copyright 2026 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. + +# Generate a LiveKit access token via `lk` and set LIVEKIT_TOKEN (and LIVEKIT_URL) +# for your current shell session. +# +# source examples/tokens/gen_and_set.bash --id PARTICIPANT_ID --room ROOM_NAME [--view-token] +# eval "$(bash examples/tokens/gen_and_set.bash --id ID --room ROOM [--view-token])" +# +# Optional env: LIVEKIT_API_KEY, LIVEKIT_API_SECRET, LIVEKIT_VALID_FOR. + +# When sourced, we must NOT enable errexit/pipefail on the interactive shell β€” a +# failing pipeline (e.g. sed|head SIGPIPE) or any error would close your terminal. + +_sourced=0 +if [[ -n "${BASH_VERSION:-}" ]] && [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then + _sourced=1 +elif [[ -n "${ZSH_VERSION:-}" ]] && [[ "${ZSH_EVAL_CONTEXT:-}" == *:file* ]]; then + _sourced=1 +fi + +_fail() { + echo "gen_and_set.bash: $1" >&2 + if [[ "$_sourced" -eq 1 ]]; then + return "${2:-1}" + fi + exit "${2:-1}" +} + +_usage() { + echo "Usage: ${0##*/} --id PARTICIPANT_IDENTITY --room ROOM_NAME [--view-token]" >&2 + echo " --id LiveKit participant identity (required)" >&2 + echo " --room Room name (required; not read from env)" >&2 + echo " --view-token Print the JWT to stderr after generating" >&2 +} + +if [[ "$_sourced" -eq 0 ]]; then + set -euo pipefail +fi + +_view_token=0 +LIVEKIT_IDENTITY="" +LIVEKIT_ROOM="robo_room" +while [[ $# -gt 0 ]]; do + case "$1" in + --view-token) + _view_token=1 + shift + ;; + --id) + if [[ $# -lt 2 ]]; then + _usage + _fail "--id requires a value" 2 + fi + LIVEKIT_IDENTITY="$2" + shift 2 + ;; + --room) + if [[ $# -lt 2 ]]; then + _usage + _fail "--room requires a value" 2 + fi + LIVEKIT_ROOM="$2" + shift 2 + ;; + -h | --help) + _usage + if [[ "$_sourced" -eq 1 ]]; then + return 0 + fi + exit 0 + ;; + *) + _usage + _fail "unknown argument: $1" 2 + ;; + esac +done + +if [[ -z "$LIVEKIT_IDENTITY" ]]; then + _usage + _fail "--id is required" 2 +fi +if [[ -z "$LIVEKIT_ROOM" ]]; then + _usage + _fail "--room is required" 2 +fi + +LIVEKIT_API_KEY="${LIVEKIT_API_KEY:-devkey}" +LIVEKIT_API_SECRET="${LIVEKIT_API_SECRET:-secret}" +LIVEKIT_VALID_FOR="${LIVEKIT_VALID_FOR:-99999h}" +_grant_json='{"canPublish":true,"canSubscribe":true,"canPublishData":true}' + +if ! command -v lk >/dev/null 2>&1; then + _fail "'lk' CLI not found. Install: https://docs.livekit.io/home/cli/" 2 +fi + +# Run lk inside bash so --grant JSON (with embedded ") is safe when this file is +# sourced from zsh; zsh misparses --grant "$json" on the same line. +_out="$( + bash -c ' + lk token create \ + --api-key "$1" \ + --api-secret "$2" \ + -i "$3" \ + --join \ + --valid-for "$4" \ + --room "$5" \ + --grant "$6" 2>&1 + ' _ "$LIVEKIT_API_KEY" "$LIVEKIT_API_SECRET" "$LIVEKIT_IDENTITY" \ + "$LIVEKIT_VALID_FOR" "$LIVEKIT_ROOM" "$_grant_json" +)" +_lk_st=$? +if [[ "$_lk_st" -ne 0 ]]; then + echo "$_out" >&2 + _fail "lk token create failed" 1 +fi + +# Avoid sed|head pipelines (pipefail + SIGPIPE can kill a sourced shell). +LIVEKIT_TOKEN="" +LIVEKIT_URL="" +while IFS= read -r _line || [[ -n "${_line}" ]]; do + if [[ "$_line" == "Access token: "* ]]; then + LIVEKIT_TOKEN="${_line#Access token: }" + elif [[ "$_line" == "Project URL: "* ]]; then + LIVEKIT_URL="${_line#Project URL: }" + fi +done <<< "$_out" + +if [[ -z "$LIVEKIT_TOKEN" ]]; then + echo "gen_and_set.bash: could not parse Access token from lk output:" >&2 + echo "$_out" >&2 + _fail "missing Access token line" 1 +fi + +if [[ "$_view_token" -eq 1 ]]; then + echo "$LIVEKIT_TOKEN" >&2 +fi + +_apply() { + export LIVEKIT_TOKEN + export LIVEKIT_URL +} + +_emit_eval() { + printf 'export LIVEKIT_TOKEN=%q\n' "$LIVEKIT_TOKEN" + [[ -n "$LIVEKIT_URL" ]] && printf 'export LIVEKIT_URL=%q\n' "$LIVEKIT_URL" +} + +if [[ "$_sourced" -eq 1 ]]; then + _apply + echo "LIVEKIT_TOKEN and LIVEKIT_URL set for this shell." >&2 + [[ -n "$LIVEKIT_URL" ]] || echo "gen_and_set.bash: warning: no Project URL in output; set LIVEKIT_URL manually." >&2 +else + _emit_eval + echo "gen_and_set.bash: for this shell run: source $0 --id ... --room ... or: eval \"\$(bash $0 ...)\"" >&2 +fi diff --git a/examples/tokens/set_integration_test_tokens.bash b/examples/tokens/set_integration_test_tokens.bash new file mode 100755 index 00000000..8a46ca42 --- /dev/null +++ b/examples/tokens/set_integration_test_tokens.bash @@ -0,0 +1,138 @@ +#!/usr/bin/env bash +# Copyright 2026 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. + +# Generate two LiveKit access tokens via `lk` and set the environment variables +# required by src/tests/integration/test_data_track.cpp and src/tests/integration/test_rpc.cpp. +# +# source examples/tokens/set_integration_test_tokens.bash +# eval "$(bash examples/tokens/set_integration_test_tokens.bash)" +# +# Exports: +# LK_TOKEN_TEST_A +# LK_TOKEN_TEST_B +# LIVEKIT_URL=ws://localhost:7880 +# + +_sourced=0 +if [[ -n "${BASH_VERSION:-}" ]] && [[ "${BASH_SOURCE[0]}" != "${0}" ]]; then + _sourced=1 +elif [[ -n "${ZSH_VERSION:-}" ]] && [[ "${ZSH_EVAL_CONTEXT:-}" == *:file* ]]; then + _sourced=1 +fi + +_fail() { + echo "set_integration_test_tokens.bash: $1" >&2 + if [[ "$_sourced" -eq 1 ]]; then + return "${2:-1}" + fi + exit "${2:-1}" +} + +if [[ "$_sourced" -eq 0 ]]; then + set -euo pipefail +fi + +# data track +LIVEKIT_ROOM="cpp_data_track_test" +LIVEKIT_IDENTITY_A="cpp-test-a" +LIVEKIT_IDENTITY_B="cpp-test-b" + +# rpc +LIVEKIT_RPC_ROOM="rpc-test-room" +LIVEKIT_CALLER_TOKEN="caller" +LIVEKIT_RECEIVER_TOKEN="receiver" + +if [[ $# -ne 0 ]]; then + _fail "this script is hard-coded and does not accept arguments" 2 +fi + +LIVEKIT_API_KEY="devkey" +LIVEKIT_API_SECRET="secret" +LIVEKIT_VALID_FOR="99999h" +LIVEKIT_URL="ws://localhost:7880" +_grant_json='{"canPublish":true,"canSubscribe":true,"canPublishData":true}' + +if ! command -v lk >/dev/null 2>&1; then + _fail "'lk' CLI not found. Install: https://docs.livekit.io/home/cli/" 2 +fi + +_create_token() { + local identity="$1" + local output="" + local command_status=0 + local token="" + + output="$( + bash -c ' + lk token create \ + --api-key "$1" \ + --api-secret "$2" \ + -i "$3" \ + --join \ + --valid-for "$4" \ + --room "$5" \ + --grant "$6" 2>&1 + ' _ "$LIVEKIT_API_KEY" "$LIVEKIT_API_SECRET" "$identity" \ + "$LIVEKIT_VALID_FOR" "$LIVEKIT_ROOM" "$_grant_json" + )" + command_status=$? + if [[ "$command_status" -ne 0 ]]; then + echo "$output" >&2 + _fail "lk token create failed for identity '$identity'" 1 + fi + + while IFS= read -r line || [[ -n "${line}" ]]; do + if [[ "$line" == "Access token: "* ]]; then + token="${line#Access token: }" + break + fi + done <<< "$output" + + if [[ -z "$token" ]]; then + echo "$output" >&2 + _fail "could not parse Access token for identity '$identity'" 1 + fi + + printf '%s' "$token" +} + +LK_TOKEN_TEST_A="$(_create_token "$LIVEKIT_IDENTITY_A")" +LK_TOKEN_TEST_B="$(_create_token "$LIVEKIT_IDENTITY_B")" +LK_TOKEN_RPC_CALLER="$(_create_token "$LIVEKIT_CALLER_TOKEN")" +LK_TOKEN_RPC_RECEIVER="$(_create_token "$LIVEKIT_RECEIVER_TOKEN")" + +_apply() { + export LK_TOKEN_TEST_A + export LK_TOKEN_TEST_B + export LK_TOKEN_RPC_CALLER + export LK_TOKEN_RPC_RECEIVER + export LIVEKIT_URL +} + +_emit_eval() { + printf 'export LK_TOKEN_TEST_A=%q\n' "$LK_TOKEN_TEST_A" + printf 'export LK_TOKEN_TEST_B=%q\n' "$LK_TOKEN_TEST_B" + printf 'export LK_TOKEN_RPC_CALLER=%q\n' "$LK_TOKEN_RPC_CALLER" + printf 'export LK_TOKEN_RPC_RECEIVER=%q\n' "$LK_TOKEN_RPC_RECEIVER" + printf 'export LIVEKIT_URL=%q\n' "$LIVEKIT_URL" +} + +if [[ "$_sourced" -eq 1 ]]; then + _apply + echo "LK_TOKEN_TEST_A, LK_TOKEN_TEST_B, LK_TOKEN_RPC_CALLER, LK_TOKEN_RPC_RECEIVER, and LIVEKIT_URL set for this shell." >&2 +else + _emit_eval + echo "set_integration_test_tokens.bash: for this shell run: source $0 or: eval \"\$(bash $0 ...)\"" >&2 +fi