From c5c399a31530df27f37ba6568b555fc279ca2162 Mon Sep 17 00:00:00 2001 From: Stephen DeRosa Date: Fri, 20 Mar 2026 15:53:44 -0600 Subject: [PATCH 1/2] Build cpp examples from CMakeLists --- .gitignore | 1 + .gitmodules | 3 + CMakeLists.txt | 3 +- cmake/cpp-example-collection.cmake | 48 ++ cpp-example-collection | 1 + docker/Dockerfile | 4 - 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 ---- 51 files changed, 55 insertions(+), 7898 deletions(-) create mode 100644 cmake/cpp-example-collection.cmake create mode 160000 cpp-example-collection 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 diff --git a/.gitignore b/.gitignore index 6c8853a7..10affee8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ build.log build/ build-debug/ build-release/ +local-install/ release/ vcpkg_installed/ # Generated header diff --git a/.gitmodules b/.gitmodules index cbbd96d2..0a43c64f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "client-sdk-rust"] path = client-sdk-rust url = https://github.com/livekit/client-sdk-rust +[submodule "cpp-example-collection"] + path = cpp-example-collection + url = https://github.com/livekit-examples/cpp-example-collection.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 3af32d2c..bfc3aa97 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -690,7 +690,8 @@ install(FILES add_subdirectory(bridge) if(LIVEKIT_BUILD_EXAMPLES) - add_subdirectory(examples) + include(cpp-example-collection) + livekit_configure_cpp_example_collection() endif() if(LIVEKIT_BUILD_TESTS) diff --git a/cmake/cpp-example-collection.cmake b/cmake/cpp-example-collection.cmake new file mode 100644 index 00000000..70d5e8f1 --- /dev/null +++ b/cmake/cpp-example-collection.cmake @@ -0,0 +1,48 @@ +# Copyright 2026 LiveKit, Inc. +# +# LiveKit examples integration helpers. + +include_guard(GLOBAL) + +function(livekit_configure_cpp_example_collection) + set(LIVEKIT_CPP_EXAMPLES_SOURCE_DIR + "${CMAKE_SOURCE_DIR}/cpp-example-collection") + set(LIVEKIT_CPP_EXAMPLES_INSTALL_PREFIX + "${CMAKE_BINARY_DIR}/../local-install" + CACHE PATH "Install prefix used by cpp-example-collection") + set(LIVEKIT_CPP_EXAMPLES_BINARY_DIR + "${CMAKE_BINARY_DIR}/cpp-example-collection-build" + CACHE PATH "Build directory for cpp-example-collection") + set(LIVEKIT_CPP_EXAMPLES_LIVEKIT_DIR + "${LIVEKIT_CPP_EXAMPLES_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/cmake/LiveKit") + + if(NOT EXISTS "${LIVEKIT_CPP_EXAMPLES_SOURCE_DIR}/CMakeLists.txt") + message(FATAL_ERROR + "cpp-example-collection submodule is missing. Run: " + "git submodule sync --recursive && " + "git submodule update --init --recursive --checkout") + endif() + + add_custom_target(install_livekit_sdk_for_examples + COMMAND ${CMAKE_COMMAND} -E make_directory "${LIVEKIT_CPP_EXAMPLES_INSTALL_PREFIX}" + COMMAND ${CMAKE_COMMAND} --install "${CMAKE_BINARY_DIR}" + --prefix "${LIVEKIT_CPP_EXAMPLES_INSTALL_PREFIX}" + --config "$" + DEPENDS livekit + COMMENT "Installing LiveKit SDK for cpp-example-collection" + VERBATIM + ) + + add_custom_target(cpp_example_collection ALL + COMMAND ${CMAKE_COMMAND} -S "${LIVEKIT_CPP_EXAMPLES_SOURCE_DIR}" + -B "${LIVEKIT_CPP_EXAMPLES_BINARY_DIR}" + -DCMAKE_PREFIX_PATH="${LIVEKIT_CPP_EXAMPLES_INSTALL_PREFIX}" + -DCMAKE_BUILD_TYPE="${CMAKE_BUILD_TYPE}" + -DLiveKit_DIR="${LIVEKIT_CPP_EXAMPLES_LIVEKIT_DIR}" + COMMAND ${CMAKE_COMMAND} --build "${LIVEKIT_CPP_EXAMPLES_BINARY_DIR}" + --config "$" + DEPENDS install_livekit_sdk_for_examples + COMMENT "Configuring and building cpp-example-collection" + VERBATIM + ) +endfunction() diff --git a/cpp-example-collection b/cpp-example-collection new file mode 160000 index 00000000..a74ca36f --- /dev/null +++ b/cpp-example-collection @@ -0,0 +1 @@ +Subproject commit a74ca36fbb9ac711ac9d56133dc34dc5e3634db4 diff --git a/docker/Dockerfile b/docker/Dockerfile index 0f42ff0b..e6035f1f 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -104,18 +104,14 @@ WORKDIR /client-sdk-cpp RUN mkdir -p /client-sdk-cpp COPY src /client-sdk-cpp/src COPY include /client-sdk-cpp/include -COPY bridge /client-sdk-cpp/bridge COPY build.sh /client-sdk-cpp/build.sh COPY CMakePresets.json /client-sdk-cpp/CMakePresets.json COPY build.cmd /client-sdk-cpp/build.cmd COPY client-sdk-rust /client-sdk-cpp/client-sdk-rust COPY data /client-sdk-cpp/data -COPY examples /client-sdk-cpp/examples COPY cmake /client-sdk-cpp/cmake COPY CMakeLists.txt /client-sdk-cpp/CMakeLists.txt COPY build.h.in /client-sdk-cpp/build.h.in -COPY build.sh /client-sdk-cpp/build.sh -COPY build.cmd /client-sdk-cpp/build.cmd # Configure Rust linker: use full GCC path so liblto_plugin.so is found (not /home/installs/ which has no plugin) RUN mkdir -p /client-sdk-cpp/client-sdk-rust/.cargo \ 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 From cc4992f6758baeeb14b6a98dfd18b815adc9b138 Mon Sep 17 00:00:00 2001 From: Stephen DeRosa Date: Thu, 2 Apr 2026 11:57:42 -0600 Subject: [PATCH 2/2] compiling --- README.md | 16 ++++++------ cmake/cpp-example-collection.cmake | 39 ++++++++++++++++++++---------- cmake/spdlog.cmake | 2 +- 3 files changed, 35 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index b318e0d5..0f1cb389 100644 --- a/README.md +++ b/README.md @@ -150,22 +150,22 @@ lk token create -r test -i your_own_identity --join --valid-for 99999h --dev -- ### SimpleRoom -```bash -./build/examples/SimpleRoom --url $URL --token +```bash` +./build-release/cpp-example-collection-build/simple_room/SimpleRoom --url $URL --token ``` You can also provide the URL and token via environment variables: ```bash export LIVEKIT_URL=ws://localhost:7880 export LIVEKIT_TOKEN= -./build/examples/SimpleRoom +./build-release/cpp-example-collection-build/simple_room/SimpleRoom ``` **End-to-End Encryption (E2EE)** You can enable E2E encryption for the streams via --enable_e2ee and --e2ee_key flags, by running the following cmds in two terminals or computers. **Note, jwt_token needs to be different identity** ```bash -./build/examples/SimpleRoom --url $URL --token --enable_e2ee --e2ee_key="your_key" +./build-release/cpp-example-collection-build/simple_room/SimpleRoom --url $URL --token --enable_e2ee --e2ee_key="your_key" ``` **Note**, **all participants must use the exact same E2EE configuration and shared key.** If the E2EE keys do not match between participants: @@ -195,7 +195,7 @@ lk token create -r test -i math-genius --join --valid-for 99999h --dev --room=yo #### β–Ά Start Participants Every participant is run as a separate terminal process, note --role needs to match the token identity. ```bash -./build/examples/SimpleRpc --url $URL --token --role=math-genius +./build-release/cpp-example-collection-build/simple_rpc/SimpleRpc --url $URL --token --role=math-genius ``` The caller will automatically: - Wait for the greeter and math-genius to join @@ -223,11 +223,11 @@ lk token create -r test -i greeter --join --valid-for 99999h --dev --room=your_o #### β–Ά Start Participants Start the receiver first (so it registers stream handlers before messages arrive): ```bash -./build/examples/SimpleDataStream --url $URL --token +./build-release/cpp-example-collection-build/simple_data_stream/SimpleDataStream --url $URL --token ``` On another terminal or computer, start the sender ```bash -./build/examples/SimpleDataStream --url $URL --token +./build-release/cpp-example-collection-build/simple_data_stream/SimpleDataStream --url $URL --token ``` **Sender** (e.g. greeter) @@ -306,7 +306,7 @@ livekit::setLogCallback( livekit::setLogCallback(nullptr); ``` -See [`examples/logging_levels/custom_sinks.cpp`](examples/logging_levels/custom_sinks.cpp) +See [`cpp-example-collection/logging_levels/custom_sinks.cpp`](cpp-example-collection/logging_levels/custom_sinks.cpp) for three copy-paste-ready patterns: **file logger**, **JSON structured lines**, and a **ROS2 bridge** that maps `LogLevel` to `RCLCPP_*` macros. diff --git a/cmake/cpp-example-collection.cmake b/cmake/cpp-example-collection.cmake index 70d5e8f1..187be593 100644 --- a/cmake/cpp-example-collection.cmake +++ b/cmake/cpp-example-collection.cmake @@ -5,16 +5,23 @@ include_guard(GLOBAL) function(livekit_configure_cpp_example_collection) - set(LIVEKIT_CPP_EXAMPLES_SOURCE_DIR - "${CMAKE_SOURCE_DIR}/cpp-example-collection") - set(LIVEKIT_CPP_EXAMPLES_INSTALL_PREFIX - "${CMAKE_BINARY_DIR}/../local-install" - CACHE PATH "Install prefix used by cpp-example-collection") - set(LIVEKIT_CPP_EXAMPLES_BINARY_DIR - "${CMAKE_BINARY_DIR}/cpp-example-collection-build" - CACHE PATH "Build directory for cpp-example-collection") - set(LIVEKIT_CPP_EXAMPLES_LIVEKIT_DIR - "${LIVEKIT_CPP_EXAMPLES_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/cmake/LiveKit") + # Absolute paths so out-of-tree builds and symlinks behave consistently. + get_filename_component(LIVEKIT_CPP_EXAMPLES_SOURCE_DIR + "${CMAKE_SOURCE_DIR}/cpp-example-collection" ABSOLUTE) + + get_filename_component(_lk_examples_install_default + "${CMAKE_SOURCE_DIR}/local-install" ABSOLUTE) + set(LIVEKIT_CPP_EXAMPLES_INSTALL_PREFIX "${_lk_examples_install_default}" + CACHE PATH "Install prefix used by cpp-example-collection") + + get_filename_component(_lk_examples_binary_default + "${CMAKE_BINARY_DIR}/cpp-example-collection-build" ABSOLUTE) + set(LIVEKIT_CPP_EXAMPLES_BINARY_DIR "${_lk_examples_binary_default}" + CACHE PATH "Build directory for cpp-example-collection") + + get_filename_component(LIVEKIT_CPP_EXAMPLES_LIVEKIT_DIR + "${LIVEKIT_CPP_EXAMPLES_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}/cmake/LiveKit" + ABSOLUTE) if(NOT EXISTS "${LIVEKIT_CPP_EXAMPLES_SOURCE_DIR}/CMakeLists.txt") message(FATAL_ERROR @@ -33,12 +40,18 @@ function(livekit_configure_cpp_example_collection) VERBATIM ) + # cmake --install /Users/sderosa/workspaces/client-sdk-cpp/build-debug --prefix ~/livekit-sdk-local + + add_custom_target(cpp_example_collection ALL + COMMAND ${CMAKE_COMMAND} --install "${CMAKE_BINARY_DIR}" + --prefix "${LIVEKIT_CPP_EXAMPLES_INSTALL_PREFIX}" COMMAND ${CMAKE_COMMAND} -S "${LIVEKIT_CPP_EXAMPLES_SOURCE_DIR}" -B "${LIVEKIT_CPP_EXAMPLES_BINARY_DIR}" - -DCMAKE_PREFIX_PATH="${LIVEKIT_CPP_EXAMPLES_INSTALL_PREFIX}" - -DCMAKE_BUILD_TYPE="${CMAKE_BUILD_TYPE}" - -DLiveKit_DIR="${LIVEKIT_CPP_EXAMPLES_LIVEKIT_DIR}" + "-DCMAKE_PREFIX_PATH=${LIVEKIT_CPP_EXAMPLES_INSTALL_PREFIX}" + "-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}" + "-DLIVEKIT_LOCAL_SDK_DIR=${LIVEKIT_CPP_EXAMPLES_INSTALL_PREFIX}" + "-DLiveKit_DIR=${LIVEKIT_CPP_EXAMPLES_LIVEKIT_DIR}" COMMAND ${CMAKE_COMMAND} --build "${LIVEKIT_CPP_EXAMPLES_BINARY_DIR}" --config "$" DEPENDS install_livekit_sdk_for_examples 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)