diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml index 6fbae309..2e5dbad7 100644 --- a/.github/workflows/builds.yml +++ b/.github/workflows/builds.yml @@ -7,7 +7,6 @@ on: - src/** - include/** - cpp-example-collection/** - - bridge/** - client-sdk-rust/** - cmake/** - scripts/** @@ -28,7 +27,6 @@ on: - src/** - include/** - cpp-example-collection/** - - bridge/** - client-sdk-rust/** - cmake/** - scripts/** @@ -58,7 +56,7 @@ env: # failing the build. SCCACHE_GHA_ENABLED: "true" # Pinned commit for cpp-example-collection smoke build (https://github.com/livekit-examples/cpp-example-collection) - CPP_EXAMPLE_COLLECTION_REF: 56815733a71c14692569e8adf2916a56a14d4882 + CPP_EXAMPLE_COLLECTION_REF: 402e6fbcc3cb8b2b2aaf80e21b289f27a9060dc6 # vcpkg binary caching for Windows VCPKG_DEFAULT_TRIPLET: x64-windows-static-md VCPKG_DEFAULT_HOST_TRIPLET: x64-windows-static-md diff --git a/.github/workflows/docker-images.yml b/.github/workflows/docker-images.yml index 5803e340..1e9aa5a6 100644 --- a/.github/workflows/docker-images.yml +++ b/.github/workflows/docker-images.yml @@ -6,7 +6,6 @@ on: paths: - src/** - include/** - - bridge/** - client-sdk-rust/** - CMakeLists.txt - build.sh @@ -90,7 +89,7 @@ jobs: fi case "${path}" in - docker/Dockerfile.sdk|src/*|include/*|bridge/*|client-sdk-rust/*|cmake/*|data/*|CMakeLists.txt|build.sh|build.cmd|build.h.in|.build-info.json.in|CMakePresets.json) + docker/Dockerfile.sdk|src/*|include/*|client-sdk-rust/*|cmake/*|data/*|CMakeLists.txt|build.sh|build.cmd|build.h.in|.build-info.json.in|CMakePresets.json) sdk_changed=true ;; esac diff --git a/.github/workflows/docker-validate.yml b/.github/workflows/docker-validate.yml index ffd0d121..63c94817 100644 --- a/.github/workflows/docker-validate.yml +++ b/.github/workflows/docker-validate.yml @@ -11,7 +11,7 @@ permissions: env: # Pinned commit for cpp-example-collection smoke build (https://github.com/livekit-examples/cpp-example-collection) - CPP_EXAMPLE_COLLECTION_REF: 56815733a71c14692569e8adf2916a56a14d4882 + CPP_EXAMPLE_COLLECTION_REF: 402e6fbcc3cb8b2b2aaf80e21b289f27a9060dc6 jobs: validate-x64: diff --git a/.github/workflows/license_check.yml b/.github/workflows/license_check.yml index f28aac16..fbfd9ea1 100644 --- a/.github/workflows/license_check.yml +++ b/.github/workflows/license_check.yml @@ -18,7 +18,7 @@ jobs: set -euo pipefail search_dirs=() - for dir in src include bridge cpp-example-collection; do + for dir in src include cpp-example-collection; do if [[ -d "$dir" ]]; then search_dirs+=("$dir") fi diff --git a/AGENTS.md b/AGENTS.md index 6663fdef..e234ec3a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -74,7 +74,6 @@ Be sure to update the directory layout in this file if the directory layout chan | `src/` | Implementation files and internal-only headers (`ffi_client.h`, `lk_log.h`, etc.) | | `src/tests/` | Google Test integration and stress tests | | `examples/` | In-tree example applications | -| `bridge/` | **Deprecated** C-style bridge layer — do not add new functionality | | `client-sdk-rust/` | Git submodule holding the Rust core of the SDK| | `client-sdk-rust/livekit-ffi/protocol/*.proto` | FFI contract (protobuf definitions, read-only reference) | | `cmake/` | Build helpers (`protobuf.cmake`, `spdlog.cmake`, `LiveKitConfig.cmake.in`) | @@ -368,11 +367,6 @@ When adding new client facing functionality, add benchmarking to understand the - Declare all data objects at the smallest possible level of scope - Each calling function must check the return value of nonvoid functions, and each called function must check the validity of all parameters provided by the caller - -## Deprecated / Out of Scope - -- **`bridge/`** (`livekit_bridge`) is deprecated. Do not add new functionality to it. - ## Common Pitfalls - A `Room` with `auto_subscribe = false` will never receive remote audio/video frames — this is almost never what you want. diff --git a/CMakeLists.txt b/CMakeLists.txt index 895284b1..ebfeeeb9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -14,6 +14,7 @@ project(livekit VERSION ${LIVEKIT_PROJECT_VERSION} LANGUAGES C CXX) set(LIVEKIT_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR}) set(LIVEKIT_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") +include(warnings) option(LIVEKIT_BUILD_EXAMPLES "Build LiveKit examples" OFF) option(LIVEKIT_BUILD_TESTS "Build LiveKit tests" OFF) @@ -113,17 +114,28 @@ if(TARGET protobuf::libprotobuf) else() message(FATAL_ERROR "No protobuf library target found (expected protobuf::libprotobuf)") endif() -target_include_directories(livekit_proto PRIVATE - "${PROTO_BINARY_DIR}" - ${Protobuf_INCLUDE_DIRS} -) -target_link_libraries(livekit_proto PRIVATE ${LIVEKIT_PROTOBUF_TARGET}) +set(LIVEKIT_PROTOBUF_DEP_INCLUDE_DIRS ${Protobuf_INCLUDE_DIRS}) if(TARGET absl::base) - get_target_property(_absl_inc absl::base INTERFACE_INCLUDE_DIRECTORIES) + livekit_get_interface_includes(absl::base _absl_inc) if(_absl_inc) - target_include_directories(livekit_proto PRIVATE ${_absl_inc}) + list(APPEND LIVEKIT_PROTOBUF_DEP_INCLUDE_DIRS ${_absl_inc}) endif() endif() +set(LIVEKIT_SYSTEM_INCLUDE_DIRS) +if(TARGET spdlog::spdlog) + livekit_get_interface_includes(spdlog::spdlog _spdlog_inc) + if(_spdlog_inc) + list(APPEND LIVEKIT_SYSTEM_INCLUDE_DIRS ${_spdlog_inc}) + endif() +endif() +target_include_directories(livekit_proto SYSTEM PRIVATE + "${PROTO_BINARY_DIR}" +) +target_include_directories(livekit_proto PRIVATE + ${LIVEKIT_PROTOBUF_DEP_INCLUDE_DIRS} +) +target_link_libraries(livekit_proto PRIVATE ${LIVEKIT_PROTOBUF_TARGET}) +livekit_disable_warnings(livekit_proto) # Manually generate protobuf files to avoid path prefix issues set(PROTO_SRCS) @@ -432,8 +444,13 @@ target_include_directories(livekit PRIVATE ${LIVEKIT_ROOT_DIR}/src ${LIVEKIT_ROOT_DIR}/src/trace - ${RUST_ROOT}/livekit-ffi/include - ${PROTO_BINARY_DIR} + ${LIVEKIT_PROTOBUF_DEP_INCLUDE_DIRS} +) + +target_include_directories(livekit SYSTEM PRIVATE + ${RUST_ROOT}/livekit-ffi/include + ${PROTO_BINARY_DIR} + ${LIVEKIT_SYSTEM_INCLUDE_DIRS} ) target_link_libraries(livekit @@ -748,9 +765,6 @@ install(FILES # ------------------------------------------------------------------------ -# Build the LiveKit C++ bridge before examples (human_robot depends on it) -add_subdirectory(bridge) - if(LIVEKIT_BUILD_EXAMPLES) include(cpp-example-collection) livekit_configure_cpp_example_collection() diff --git a/README.md b/README.md index 665f0042..f8f5c285 100644 --- a/README.md +++ b/README.md @@ -621,11 +621,9 @@ lk token create \ --grant '{"canPublish":true,"canSubscribe":true,"canPublishData":true}' ``` -## Deprecation - -- livekit_bridge (bridge/ folder) is deprecated. Avoid using it. Migrate to the base SDK. This will be removed on 06/01/2026. -- setOn*FrameCallback with TrackSource is deprecated. Use track name instead. This will be removed on 06/01/2026. -- All public headers that do not follow `camelBack()` case. This will be removed on 06/01/2026. +# Deprecation +> NOTE: With the official 1.0.0 release we have introduced breaking changes to previous unofficial versions in order +to align with other LiveKit client SDKs. See [PR #143](https://github.com/livekit/client-sdk-cpp/pull/143) for the source code changes.
diff --git a/benchmarks/data_track_throughput/consumer.cpp b/benchmarks/data_track_throughput/consumer.cpp index d3fde8d2..2d031541 100644 --- a/benchmarks/data_track_throughput/consumer.cpp +++ b/benchmarks/data_track_throughput/consumer.cpp @@ -481,7 +481,7 @@ int main(int argc, char* argv[]) { throw std::runtime_error("Failed to connect to LiveKit room"); } - auto* local_participant = room.localParticipant(); + auto local_participant = room.localParticipant().lock(); if (local_participant == nullptr) { throw std::runtime_error("Local participant unavailable after connect"); } diff --git a/benchmarks/data_track_throughput/producer.cpp b/benchmarks/data_track_throughput/producer.cpp index d1b36a5b..b0732cb6 100644 --- a/benchmarks/data_track_throughput/producer.cpp +++ b/benchmarks/data_track_throughput/producer.cpp @@ -200,13 +200,15 @@ std::string waitForConsumerIdentity(Room& room, const std::string& requested_ide const auto deadline = std::chrono::steady_clock::now() + timeout; while (g_running.load() && std::chrono::steady_clock::now() < deadline) { if (!requested_identity.empty()) { - if (room.remoteParticipant(requested_identity) != nullptr) { + if (!room.remoteParticipant(requested_identity).expired()) { return requested_identity; } } else { const auto participants = room.remoteParticipants(); - if (participants.size() == 1 && participants.front() != nullptr) { - return participants.front()->identity(); + if (participants.size() == 1) { + if (auto participant = participants.front().lock()) { + return participant->identity(); + } } } @@ -316,7 +318,7 @@ int main(int argc, char* argv[]) { throw std::runtime_error("Failed to connect to LiveKit room"); } - auto* local_participant = room.localParticipant(); + auto local_participant = room.localParticipant().lock(); if (local_participant == nullptr) { throw std::runtime_error("Local participant unavailable after connect"); } diff --git a/bridge/CMakeLists.txt b/bridge/CMakeLists.txt deleted file mode 100644 index 3b5e9f90..00000000 --- a/bridge/CMakeLists.txt +++ /dev/null @@ -1,65 +0,0 @@ -cmake_minimum_required(VERSION 3.20) - -project(livekit_bridge LANGUAGES CXX) - -set(CMAKE_CXX_STANDARD 17) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -add_library(livekit_bridge SHARED - src/livekit_bridge.cpp - src/bridge_audio_track.cpp - src/bridge_video_track.cpp - src/rpc_constants.cpp - src/rpc_controller.cpp - src/rpc_controller.h -) - -# Workaround for bridge deprecation: handle visibility differently to avoid -# major LIVEKIT_API changes across its code, and not affecting the SDK -if(WIN32) - set_target_properties(livekit_bridge PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON) -else() - set_target_properties(livekit_bridge PROPERTIES - CXX_VISIBILITY_PRESET default - C_VISIBILITY_PRESET default - VISIBILITY_INLINES_HIDDEN OFF - ) - target_compile_options(livekit_bridge INTERFACE - -fvisibility=default - -fno-visibility-inlines-hidden - ) -endif() - -target_include_directories(livekit_bridge - PUBLIC - $ - $ - PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/src - ${LIVEKIT_ROOT_DIR}/src -) - -# Link against the main livekit SDK library (which transitively provides -# include paths for livekit/*.h and links livekit_ffi). -target_link_libraries(livekit_bridge - PUBLIC - livekit -) - -if(MSVC) - target_compile_options(livekit_bridge PRIVATE /permissive- /Zc:__cplusplus /W4) -else() - target_compile_options(livekit_bridge PRIVATE -Wall -Wextra -Wpedantic) -endif() - -# --- Tests --- -# Bridge tests default to OFF. They are automatically enabled when the parent -# SDK tests are enabled (LIVEKIT_BUILD_TESTS=ON), e.g. via ./build.sh debug-tests. -option(LIVEKIT_BRIDGE_BUILD_TESTS "Build bridge unit tests" OFF) - -if(LIVEKIT_BRIDGE_BUILD_TESTS OR LIVEKIT_BUILD_TESTS) - add_subdirectory(tests) -endif() - -# Bridge examples (robot + human) are built from examples/CMakeLists.txt -# when LIVEKIT_BUILD_EXAMPLES is ON; see examples/bridge_human_robot/. diff --git a/bridge/README.md b/bridge/README.md deleted file mode 100644 index a2c2acf8..00000000 --- a/bridge/README.md +++ /dev/null @@ -1,268 +0,0 @@ -# LiveKit Bridge - -# **WARNING: This library is deprecated, use the base sdk found in src/** - -# **WARNING: This folder and functionality will be removed on 06/01/2026** - -A simplified, high-level C++ wrapper around the [LiveKit C++ SDK](../README.md). The bridge abstracts away room lifecycle management, track creation, publishing, and subscription boilerplate so that external codebases can interface with LiveKit in just a few lines. It is intended that this library will be used to bridge the LiveKit C++ SDK into other SDKs such as, but not limited to, Foxglove, ROS, and Rerun. - -It is intended that this library closely matches the style of the core LiveKit C++ SDK. - -# Prerequisites -Since this is an extention of the LiveKit C++ SDK, go through the LiveKit C++ SDK installation instructions first: -*__[LiveKit C++ SDK](../README.md)__* - -## Usage Overview - -```cpp -#include "livekit_bridge/livekit_bridge.h" -#include "livekit/audio_frame.h" -#include "livekit/video_frame.h" -#include "livekit/track.h" - -// 1. Connect -livekit_bridge::LiveKitBridge bridge; -livekit::RoomOptions options; -options.auto_subscribe = true; // automatically subscribe to all remote tracks -options.dynacast = false; -bridge.connect("wss://my-server.livekit.cloud", token, options); - -// 2. Create outgoing tracks (RAII-managed) -auto mic = bridge.createAudioTrack("mic", 48000, 2, - livekit::TrackSource::SOURCE_MICROPHONE); // name, sample_rate, channels, source -auto cam = bridge.createVideoTrack("cam", 1280, 720, - livekit::TrackSource::SOURCE_CAMERA); // name, width, height, source - -// 3. Push frames to remote participants -mic->pushFrame(pcm_data, samples_per_channel); -cam->pushFrame(rgba_data, timestamp_us); - -// 4. Receive frames from a remote participant -bridge.setOnAudioFrameCallback("remote-peer", livekit::TrackSource::SOURCE_MICROPHONE, - [](const livekit::AudioFrame& frame) { - // Called on a background reader thread - }); - -bridge.setOnVideoFrameCallback("remote-peer", livekit::TrackSource::SOURCE_CAMERA, - [](const livekit::VideoFrame& frame, int64_t timestamp_us) { - // Called on a background reader thread - }); - -// 5. RPC (Remote Procedure Call) -bridge.registerRpcMethod("greet", - [](const livekit::RpcInvocationData& data) -> std::optional { - return "Hello, " + data.caller_identity + "!"; - }); - -std::string response = bridge.performRpc("remote-peer", "greet", ""); - -bridge.unregisterRpcMethod("greet"); - -// Controller side: send commands to the publisher -controller_bridge.requestRemoteTrackMute("robot-1", "mic"); // mute audio track "mic" -controller_bridge.requestRemoteTrackUnmute("robot-1", "mic"); // unmute it - -// 7. Cleanup is automatic (RAII), or explicit: -mic.reset(); // unpublishes the audio track -cam.reset(); // unpublishes the video track -bridge.disconnect(); -``` - -## Building - -The bridge is a component of the `client-sdk-cpp` build. See the "⚙️ BUILD" section of the [LiveKit C++ SDK README](../README.md) for instructions on how to build the bridge. - -This produces `liblivekit_bridge` (shared library) and optional `robot_stub`, `human_stub`, `robot`, and `human` executables. - -### Using the bridge in your own CMake project -TODO(sderosa): add instructions on how to use the bridge in your own CMake project. - -## Architecture - -### Data Flow Overview - -``` -Your Application - | | - | pushFrame() -----> BridgeAudioTrack | (sending to remote participants) - | pushFrame() -----> BridgeVideoTrack | - | | - | callback() <------ Reader Thread | (receiving from remote participants) - | | - +------- LiveKitBridge -----------------+ - | - LiveKit Room - | - LiveKit Server -``` - -### Core Components - -**`LiveKitBridge`** -- The main entry point. Owns the full room lifecycle: SDK initialization, room connection, track publishing, and frame callback management. - -**`BridgeAudioTrack` / `BridgeVideoTrack`** -- RAII handles for published local tracks. Created via `createAudioTrack()` / `createVideoTrack()`. When the `shared_ptr` is dropped, the track is automatically unpublished and all underlying SDK resources are freed. Call `pushFrame()` to send audio/video data to remote participants. - -### What is a Reader? - -A **reader** is a background thread that receives decoded media frames from a remote participant. - -When a remote participant publishes an audio or video track and the room subscribes to it (auto-subscribe is enabled by default), `Room` creates an `AudioStream` or `VideoStream` from that track and spins up a dedicated thread. This thread loops on `stream->read()`, which blocks until a new frame arrives. Each received frame is forwarded to the user's registered callback. - -In short: - -- **Sending** (you -> remote): `BridgeAudioTrack::pushFrame()` / `BridgeVideoTrack::pushFrame()` -- **Receiving** (remote -> you): reader threads invoke your registered callbacks - -Reader threads are managed by `Room` internally. They are created when a matching remote track is subscribed, and torn down (stream closed, thread joined) when the track is unsubscribed, the callback is unregistered, or the `Room` is destroyed. - -### Callback Registration Timing - -Callbacks are keyed by `(participant_identity, track_source)`. You can register them **after connecting** but before the remote participant has joined the room. `Room` stores the callback and automatically wires it up when the matching track is subscribed. - -> **Note:** Only one callback may be set per `(participant_identity, track_source)` pair. Calling `setOnAudioFrameCallback` or `setOnVideoFrameCallback` again with the same identity and source will silently replace the previous callback. If you need to fan-out a single stream to multiple consumers, do so inside your callback. - -This means the typical pattern is: - -```cpp -// Connect first, then register callbacks before the remote participant joins. -livekit::RoomOptions options; -options.auto_subscribe = true; -bridge.connect(url, token, options); -bridge.setOnAudioFrameCallback("robot-1", livekit::TrackSource::SOURCE_MICROPHONE, my_callback); -// When robot-1 joins and publishes a mic track, my_callback starts firing. -``` - -### Thread Safety - -- `LiveKitBridge` uses a mutex to protect the callback map and active reader state. -- Frame callbacks fire on background reader threads. If your callback accesses shared application state, you are responsible for synchronization. -- `disconnect()` destroys the `Room`, which closes all streams and joins all reader threads before returning -- it is safe to destroy the bridge immediately after. - -## API Reference - -### `LiveKitBridge` - -| Method | Description | -|---|---| -| `connect(url, token, options)` | Connect to a LiveKit room. Initializes the SDK, creates a Room, and connects with auto-subscribe enabled. | -| `disconnect()` | Disconnect and release all resources. Joins all reader threads. Safe to call multiple times. | -| `isConnected()` | Returns whether the bridge is currently connected. | -| `createAudioTrack(name, sample_rate, num_channels, source)` | Create and publish a local audio track with the given `TrackSource` (e.g. `SOURCE_MICROPHONE`, `SOURCE_SCREENSHARE_AUDIO`). Returns an RAII `shared_ptr`. | -| `createVideoTrack(name, width, height, source)` | Create and publish a local video track with the given `TrackSource` (e.g. `SOURCE_CAMERA`, `SOURCE_SCREENSHARE`). Returns an RAII `shared_ptr`. | -| `setOnAudioFrameCallback(identity, source, callback)` | Register a callback for audio frames from a specific remote participant + track source. | -| `setOnVideoFrameCallback(identity, source, callback)` | Register a callback for video frames from a specific remote participant + track source. | -| `clearOnAudioFrameCallback(identity, source)` | Clear the audio callback for a specific remote participant + track source. Stops and joins the reader thread if active. | -| `clearOnVideoFrameCallback(identity, source)` | Clear the video callback for a specific remote participant + track source. Stops and joins the reader thread if active. | -| `performRpc(destination_identity, method, payload, response_timeout?)` | Blocking RPC call to a remote participant. Returns the response payload. Throws `livekit::RpcError` on failure. | -| `registerRpcMethod(method_name, handler)` | Register a handler for incoming RPC invocations. The handler returns an optional response payload or throws `livekit::RpcError`. | -| `unregisterRpcMethod(method_name)` | Unregister a previously registered RPC handler. | -| `requestRemoteTrackMute(identity, track_name)` | Ask a remote participant to mute a track by name. Throws `livekit::RpcError` on failure. | -| `requestRemoteTrackUnmute(identity, track_name)` | Ask a remote participant to unmute a track by name. Throws `livekit::RpcError` on failure. | - -### `BridgeAudioTrack` - -| Method | Description | -|---|---| -| `pushFrame(data, samples_per_channel, timeout_ms)` | Push interleaved int16 PCM samples. Accepts `std::vector` or raw pointer. | -| `mute()` / `unmute()` | Mute/unmute the track (stops/resumes sending audio). | -| `release()` | Explicitly unpublish and free resources. Called automatically by the destructor. | -| `name()` / `sampleRate()` / `numChannels()` | Accessors for track configuration. | - -### `BridgeVideoTrack` - -| Method | Description | -|---|---| -| `pushFrame(data, timestamp_us)` | Push RGBA pixel data. Accepts `std::vector` or raw pointer + size. | -| `mute()` / `unmute()` | Mute/unmute the track (stops/resumes sending video). | -| `release()` | Explicitly unpublish and free resources. Called automatically by the destructor. | -| `name()` / `width()` / `height()` | Accessors for track configuration. | - -## Examples -- examples/robot.cpp: publishes video and audio from a webcam and microphone. This requires a webcam and microphone to be available. -- examples/human.cpp: receives and renders video to the screen, receives and plays audio through the speaker. - -### Running the examples: -Note: the following workflow works for both `human` and `robot`. - -1. create a `robo_room` -``` -lk token create \ - --join --room robo_room --identity test_user \ - --valid-for 24h -``` - -2. generate tokens for the robot and human -``` -lk token create --api-key --api-secret \ - --join --room robo_room --identity robot --valid-for 24h - -lk token create --api-key --api-secret \ - --join --room robo_room --identity human --valid-for 24h -``` - -save these tokens as you will need them to run the examples. - -3. kick off the robot: -``` -export LIVEKIT_URL="wss://your-server.livekit.cloud" -export LIVEKIT_TOKEN= -./build-release/bin/robot_stub -``` - -4. kick off the human (in a new terminal): -``` -export LIVEKIT_URL="wss://your-server.livekit.cloud" -export LIVEKIT_TOKEN= -./build-release/bin/human -``` - -The human will print periodic summaries like: - -``` -[human] Audio frame #1: 480 samples/ch, 48000 Hz, 1 ch, duration=0.010s -[human] Video frame #1: 640x480, 1228800 bytes, ts=0 us -[human] Status: 500 audio frames, 150 video frames received so far. -``` - -## Testing - -The bridge includes a unit test suite built with [Google Test](https://github.com/google/googletest). Tests cover -1. `BridgeAudioTrack`/`BridgeVideoTrack` state management, and -2. `LiveKitBridge` pre-connection behaviour (callback registration, error handling). - -### Building and running tests - -Bridge tests are automatically included when you build with the `debug-tests` or `release-tests` command: - -```bash -./build.sh debug-tests -``` - -Then run them directly: - -```bash -./build-debug/bin/livekit_bridge_tests -``` - -### Standalone bridge tests only - -If you want to build bridge tests independently (without the parent SDK tests), set `LIVEKIT_BRIDGE_BUILD_TESTS=ON`: - -```bash -cmake --preset macos-debug -DLIVEKIT_BRIDGE_BUILD_TESTS=ON -cmake --build build-debug --target livekit_bridge_tests -``` - -## Limitations - -The bridge is designed for simplicity and currently only supports limited audio and video features. It does not expose: - -- We dont support all events defined in the RoomDelegate interface. -- E2EE configuration -- data tracks -- Simulcast tuning -- Video format selection (RGBA is the default; no format option yet) -- Custom `RoomOptions` or `TrackPublishOptions` -- **One callback per (participant, source):** Only a single callback can be registered for each `(participant_identity, track_source)` pair. Re-registering with the same key silently replaces the previous callback. To fan-out a stream to multiple consumers, dispatch from within your single callback. - -For advanced use cases, use the full `client-sdk-cpp` API directly, or expand the bridge to support your use case. diff --git a/bridge/include/livekit_bridge/bridge_audio_track.h b/bridge/include/livekit_bridge/bridge_audio_track.h deleted file mode 100644 index cdd22342..00000000 --- a/bridge/include/livekit_bridge/bridge_audio_track.h +++ /dev/null @@ -1,148 +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. - */ - -/// @file bridge_audio_track.h -/// @brief Handle for a published local audio track. - -#pragma once - -#include -#include -#include -#include -#include - -namespace livekit { -class AudioSource; -class LocalAudioTrack; -class LocalTrackPublication; -class LocalParticipant; -} // namespace livekit - -namespace livekit_bridge { - -namespace test { -class BridgeAudioTrackTest; -} // namespace test - -/** - * Handle to a published local audio track. - * - * Created via LiveKitBridge::createAudioTrack(). The bridge retains a - * reference to every track it creates and will automatically release all - * tracks when disconnect() is called. To unpublish a track mid-session, - * call release() explicitly; dropping the shared_ptr alone is not - * sufficient because the bridge still holds a reference. - * - * After release() (whether called explicitly or by the bridge on - * disconnect), pushFrame() returns false and mute()/unmute() become - * no-ops. The track object remains valid but inert. - * - * All public methods are thread-safe: it is safe to call pushFrame() from - * one thread while another calls mute()/unmute()/release(), or to call - * pushFrame() concurrently from multiple threads. - * - * Usage: - * auto mic = bridge.createAudioTrack("mic", 48000, 2, - * livekit::TrackSource::SOURCE_MICROPHONE); - * mic->pushFrame(pcm_data, samples_per_channel); - * mic->mute(); - * mic->release(); // unpublishes the track mid-session - */ -class BridgeAudioTrack { -public: - ~BridgeAudioTrack(); - - // Non-copyable - BridgeAudioTrack(const BridgeAudioTrack &) = delete; - BridgeAudioTrack &operator=(const BridgeAudioTrack &) = delete; - - /** - * Push a PCM audio frame to the track. - * - * @param data Interleaved int16 PCM samples. - * Must contain exactly - * (samples_per_channel * num_channels) elements. - * @param samples_per_channel Number of samples per channel in this frame. - * @param timeout_ms Max time to wait for FFI confirmation. - * 0 = wait indefinitely (default). - * @return true if the frame was pushed, false if the track has been released. - */ - bool pushFrame(const std::vector &data, int samples_per_channel, - int timeout_ms = 0); - - /** - * Push a PCM audio frame from a raw pointer. - * - * @param data Pointer to interleaved int16 PCM samples. - * @param samples_per_channel Number of samples per channel. - * @param timeout_ms Max time to wait for FFI confirmation. - * @return true if the frame was pushed, false if the track has been released. - */ - bool pushFrame(const std::int16_t *data, int samples_per_channel, - int timeout_ms = 0); - - /// Mute the audio track (stops sending audio to the room). - void mute(); - - /// Unmute the audio track (resumes sending audio to the room). - void unmute(); - - /// Track name as provided at creation. - const std::string &name() const noexcept { return name_; } - - /// Sample rate in Hz. - int sampleRate() const noexcept { return sample_rate_; } - - /// Number of audio channels. - int numChannels() const noexcept { return num_channels_; } - - /// Whether this track has been released / unpublished. - bool isReleased() const noexcept; - - /** - * Explicitly unpublish the track and release all underlying SDK resources. - * - * After this call, pushFrame() returns false and mute()/unmute() are - * no-ops. Called automatically by the destructor and by - * LiveKitBridge::disconnect(). Safe to call multiple times (idempotent). - */ - void release(); - -private: - friend class LiveKitBridge; - friend class test::BridgeAudioTrackTest; - - BridgeAudioTrack(std::string name, int sample_rate, int num_channels, - std::shared_ptr source, - std::shared_ptr track, - std::shared_ptr publication, - livekit::LocalParticipant *participant); - - mutable std::mutex mutex_; - std::string name_; - int sample_rate_; - int num_channels_; - bool released_ = false; - - std::shared_ptr source_; - std::shared_ptr track_; - /* DEPRECATED. use track_->publication() instead */ - std::shared_ptr publication_; - livekit::LocalParticipant *participant_ = nullptr; // not owned -}; - -} // namespace livekit_bridge diff --git a/bridge/include/livekit_bridge/bridge_video_track.h b/bridge/include/livekit_bridge/bridge_video_track.h deleted file mode 100644 index 27169151..00000000 --- a/bridge/include/livekit_bridge/bridge_video_track.h +++ /dev/null @@ -1,146 +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. - */ - -/// @file bridge_video_track.h -/// @brief Handle for a published local video track. - -#pragma once - -#include -#include -#include -#include -#include - -namespace livekit { -class VideoSource; -class LocalVideoTrack; -class LocalTrackPublication; -class LocalParticipant; -} // namespace livekit - -namespace livekit_bridge { - -namespace test { -class BridgeVideoTrackTest; -} // namespace test - -/** - * Handle to a published local video track. - * - * Created via LiveKitBridge::createVideoTrack(). The bridge retains a - * reference to every track it creates and will automatically release all - * tracks when disconnect() is called. To unpublish a track mid-session, - * call release() explicitly; dropping the shared_ptr alone is not - * sufficient because the bridge still holds a reference. - * - * After release() (whether called explicitly or by the bridge on - * disconnect), pushFrame() returns false and mute()/unmute() become - * no-ops. The track object remains valid but inert. - * - * All public methods are thread-safe: it is safe to call pushFrame() from - * one thread while another calls mute()/unmute()/release(), or to call - * pushFrame() concurrently from multiple threads. - * - * Usage: - * auto cam = bridge.createVideoTrack("cam", 1280, 720, - * livekit::TrackSource::SOURCE_CAMERA); - * cam->pushFrame(rgba_data, timestamp_us); - * cam->mute(); - * cam->release(); // unpublishes the track mid-session - */ -class BridgeVideoTrack { -public: - ~BridgeVideoTrack(); - - // Non-copyable - BridgeVideoTrack(const BridgeVideoTrack &) = delete; - BridgeVideoTrack &operator=(const BridgeVideoTrack &) = delete; - - /** - * Push an RGBA video frame to the track. - * - * @param rgba Raw RGBA pixel data. Must contain exactly - * (width * height * 4) bytes. - * @param timestamp_us Presentation timestamp in microseconds. - * Pass 0 to let the SDK assign one. - * @return true if the frame was pushed, false if the track has been released. - */ - bool pushFrame(const std::vector &rgba, - std::int64_t timestamp_us = 0); - - /** - * Push an RGBA video frame from a raw pointer. - * - * @param rgba Pointer to RGBA pixel data. - * @param rgba_size Size of the data buffer in bytes. - * @param timestamp_us Presentation timestamp in microseconds. - * @return true if the frame was pushed, false if the track has been released. - */ - bool pushFrame(const std::uint8_t *rgba, std::size_t rgba_size, - std::int64_t timestamp_us = 0); - - /// Mute the video track (stops sending video to the room). - void mute(); - - /// Unmute the video track (resumes sending video to the room). - void unmute(); - - /// Track name as provided at creation. - const std::string &name() const noexcept { return name_; } - - /// Video width in pixels. - int width() const noexcept { return width_; } - - /// Video height in pixels. - int height() const noexcept { return height_; } - - /// Whether this track has been released / unpublished. - bool isReleased() const noexcept; - - /** - * Explicitly unpublish the track and release all underlying SDK resources. - * - * After this call, pushFrame() returns false and mute()/unmute() are - * no-ops. Called automatically by the destructor and by - * LiveKitBridge::disconnect(). Safe to call multiple times (idempotent). - */ - void release(); - -private: - friend class LiveKitBridge; - friend class test::BridgeVideoTrackTest; - - BridgeVideoTrack(std::string name, int width, int height, - std::shared_ptr source, - std::shared_ptr track, - std::shared_ptr publication, - livekit::LocalParticipant *participant); - - mutable std::mutex mutex_; - std::string name_; - int width_; - int height_; - bool released_ = false; - - std::shared_ptr source_; - std::shared_ptr track_; - /* DEPRECATED. use track_->publication() instead */ - std::shared_ptr publication_; - livekit::LocalParticipant *participant_ = nullptr; // not owned -}; - -} // namespace livekit_bridge diff --git a/bridge/include/livekit_bridge/livekit_bridge.h b/bridge/include/livekit_bridge/livekit_bridge.h deleted file mode 100644 index 1f2d44a6..00000000 --- a/bridge/include/livekit_bridge/livekit_bridge.h +++ /dev/null @@ -1,374 +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. - */ - -/// @file livekit_bridge.h -/// @brief High-level bridge API for the LiveKit C++ SDK. - -#pragma once - -#include "livekit_bridge/bridge_audio_track.h" -#include "livekit_bridge/bridge_video_track.h" -#include "livekit_bridge/rpc_constants.h" - -#include "livekit/local_participant.h" -#include "livekit/room.h" -#include "livekit/rpc_error.h" - -#include -#include -#include -#include -#include - -namespace livekit { -class Room; -class AudioFrame; -class VideoFrame; -enum class TrackSource; -} // namespace livekit - -namespace livekit_bridge { - -class RpcController; - -namespace test { -class LiveKitBridgeTest; -} // namespace test - -/// Callback type for incoming audio frames. -/// Called on a background reader thread owned by Room. -using AudioFrameCallback = livekit::AudioFrameCallback; - -/// Callback type for incoming video frames. -/// Called on a background reader thread owned by Room. -/// @param frame The decoded video frame (RGBA by default). -/// @param timestamp_us Presentation timestamp in microseconds. -using VideoFrameCallback = livekit::VideoFrameCallback; - -/** - * High-level bridge to the LiveKit C++ SDK. - * - * Owns the full room lifecycle: initialize SDK, create Room, connect, - * publish tracks, and manage incoming frame callbacks. - * - * Frame callback reader threads are managed by Room internally via - * Room::setOnAudioFrameCallback / Room::setOnVideoFrameCallback. - * - * The bridge retains a shared_ptr to every track it creates. On - * disconnect(), all tracks are released (unpublished) before the room - * is torn down, guaranteeing safe teardown order. To unpublish a track - * mid-session, call release() on the track explicitly; dropping the - * application's shared_ptr alone is not sufficient. - * - * Example: - * - * LiveKitBridge bridge; - * livekit::RoomOptions options; - * options.auto_subscribe = true; - * bridge.connect("wss://my-server.livekit.cloud", my_token, options); - * - * auto mic = bridge.createAudioTrack("mic", 48000, 2, - * livekit::TrackSource::SOURCE_MICROPHONE); - * auto cam = bridge.createVideoTrack("cam", 1280, 720, - * livekit::TrackSource::SOURCE_CAMERA); - * - * mic->pushFrame(pcm_data, samples_per_channel); - * cam->pushFrame(rgba_data, timestamp_us); - * - * bridge.setOnAudioFrameCallback("remote-participant", - * livekit::TrackSource::SOURCE_MICROPHONE, - * [](const livekit::AudioFrame& f) { process(f); }); - * - * bridge.setOnVideoFrameCallback("remote-participant", - * livekit::TrackSource::SOURCE_CAMERA, - * [](const livekit::VideoFrame& f, int64_t ts) { render(f); }); - * - * // Unpublish a single track mid-session: - * mic->release(); - * - * // Disconnect releases all remaining tracks and tears down the room: - * bridge.disconnect(); - */ -class LiveKitBridge { -public: - LiveKitBridge(); - ~LiveKitBridge(); - - // Non-copyable, non-movable (owns room, callbacks) - LiveKitBridge(const LiveKitBridge &) = delete; - LiveKitBridge &operator=(const LiveKitBridge &) = delete; - LiveKitBridge(LiveKitBridge &&) = delete; - LiveKitBridge &operator=(LiveKitBridge &&) = delete; - - // --------------------------------------------------------------- - // Connection - // --------------------------------------------------------------- - - /** - * Connect to a LiveKit room. - * - * Initializes the SDK (if not already), creates a Room, and performs - * the WebSocket handshake. This call **blocks** until the connection - * succeeds or fails. auto_subscribe is enabled so that remote tracks - * are subscribed automatically. - * - * If the bridge is already connected, returns true immediately. - * If another thread is already in the process of connecting, returns - * false without blocking. - * - * @param url WebSocket URL of the LiveKit server. - * @param token Access token for authentication. - * @param options Room options. - * @return true if connection succeeded (or was already connected). - */ - bool connect(const std::string &url, const std::string &token, - const livekit::RoomOptions &options); - - /** - * Disconnect from the room and release all resources. - * - * All published tracks are unpublished, reader threads are stopped - * by Room's destructor, and the SDK is shut down. Safe to call - * multiple times. - */ - void disconnect(); - - /// Whether the bridge is currently connected to a room. - bool isConnected() const; - - // --------------------------------------------------------------- - // Track creation (publishing) - // --------------------------------------------------------------- - - /** - * Create and publish a local audio track. - * - * The bridge retains a reference to the track internally. To unpublish - * mid-session, call release() on the returned track. All surviving - * tracks are automatically released on disconnect(). - * - * @pre The bridge must be connected (via connect()). Calling this on a - * disconnected bridge is a programming error. - * - * @param name Human-readable track name. - * @param sample_rate Sample rate in Hz (e.g. 48000). - * @param num_channels Number of audio channels (1 = mono, 2 = stereo). - * @param source Track source type (e.g. SOURCE_MICROPHONE). Use a - * different source (e.g. SOURCE_SCREENSHARE_AUDIO) to - * publish multiple audio tracks from the same - * participant that can be independently subscribed to. - * @return Shared pointer to the published audio track handle (never null). - * @throws std::runtime_error if the bridge is not connected. - */ - std::shared_ptr - createAudioTrack(const std::string &name, int sample_rate, int num_channels, - livekit::TrackSource source); - - /** - * Create and publish a local video track. - * - * The bridge retains a reference to the track internally. To unpublish - * mid-session, call release() on the returned track. All surviving - * tracks are automatically released on disconnect(). - * - * @pre The bridge must be connected (via connect()). Calling this on a - * disconnected bridge is a programming error. - * - * @param name Human-readable track name. - * @param width Video width in pixels. - * @param height Video height in pixels. - * @param source Track source type (default: SOURCE_CAMERA). Use a - * different source (e.g. SOURCE_SCREENSHARE) to publish - * multiple video tracks from the same participant that - * can be independently subscribed to. - * @return Shared pointer to the published video track handle (never null). - * @throws std::runtime_error if the bridge is not connected. - */ - std::shared_ptr - createVideoTrack(const std::string &name, int width, int height, - livekit::TrackSource source); - - // --------------------------------------------------------------- - // Incoming frame callbacks (delegates to Room) - // --------------------------------------------------------------- - - /** - * Set the callback for audio frames from a specific remote participant - * and track source. - * - * Delegates to Room::setOnAudioFrameCallback. The callback fires on a - * dedicated reader thread owned by Room whenever a new audio frame is - * received. - * - * @note Only **one** callback may be registered per (participant, source) - * pair. Calling this again with the same identity and source will - * silently replace the previous callback. - * - * @param participant_identity Identity of the remote participant. - * @param source Track source (e.g. SOURCE_MICROPHONE). - * @param callback Function to invoke per audio frame. - */ - void setOnAudioFrameCallback(const std::string &participant_identity, - livekit::TrackSource source, - AudioFrameCallback callback); - - /** - * Register a callback for video frames from a specific remote participant - * and track source. - * - * Delegates to Room::setOnVideoFrameCallback. - * - * @note Only **one** callback may be registered per (participant, source) - * pair. Calling this again with the same identity and source will - * silently replace the previous callback. - * - * @param participant_identity Identity of the remote participant. - * @param source Track source (e.g. SOURCE_CAMERA). - * @param callback Function to invoke per video frame. - */ - void setOnVideoFrameCallback(const std::string &participant_identity, - livekit::TrackSource source, - VideoFrameCallback callback); - - /** - * Clear the audio frame callback for a specific remote participant + track - * source. - * - * Delegates to Room::clearOnAudioFrameCallback. - */ - void clearOnAudioFrameCallback(const std::string &participant_identity, - livekit::TrackSource source); - - /** - * Clear the video frame callback for a specific remote participant + track - * source. - * - * Delegates to Room::clearOnVideoFrameCallback. - */ - void clearOnVideoFrameCallback(const std::string &participant_identity, - livekit::TrackSource source); - - // --------------------------------------------------------------- - // RPC (Remote Procedure Call) - // --------------------------------------------------------------- - - /** - * Initiate a blocking RPC call to a remote participant. - * - * Sends a request to the participant identified by - * @p destination_identity and blocks until a response is received - * or the call times out. - * - * @param destination_identity Identity of the remote participant. - * @param method Name of the RPC method to invoke. - * @param payload Request payload string. - * @param response_timeout Optional timeout in seconds. If not set, - * the server default (15 s) is used. - * @return The response payload returned by the remote handler. nullptr if the - * RPC call fails, or the bridge is not connected. - */ - std::optional - performRpc(const std::string &destination_identity, const std::string &method, - const std::string &payload, - const std::optional &response_timeout = std::nullopt); - - /** - * Register a handler for incoming RPC method invocations. - * - * When a remote participant calls the given @p method_name on this - * participant, the bridge invokes @p handler. The handler may return - * an optional response payload or throw a @c livekit::RpcError to - * signal failure to the caller. - * - * If a handler is already registered for @p method_name, it is - * silently replaced. - * - * @param method_name Name of the RPC method to handle. - * @param handler Callback invoked on each incoming invocation. - * @return true if the RPC method was registered successfully. - */ - bool registerRpcMethod(const std::string &method_name, - livekit::LocalParticipant::RpcHandler handler); - - /** - * Unregister a previously registered RPC method handler. - * - * After this call, invocations for @p method_name result in an - * "unsupported method" error being returned to the remote caller. - * If no handler is registered for this name, the call is a no-op. - * - * @param method_name Name of the RPC method to unregister. - * @return true if the RPC method was unregistered successfully. - */ - bool unregisterRpcMethod(const std::string &method_name); - - // --------------------------------------------------------------- - // Remote Track Control (via RPC) - // --------------------------------------------------------------- - - /** - * Request a remote participant to mute a published track. - * - * The remote participant must be a LiveKitBridge instance (which - * automatically registers the built-in track-control RPC handler). - * - * @param destination_identity Identity of the remote participant. - * @param track_name Name of the track to mute. - * @return true if the track was muted successfully. - */ - bool requestRemoteTrackMute(const std::string &destination_identity, - const std::string &track_name); - - /** - * Request a remote participant to unmute a published track. - * - * The remote participant must be a LiveKitBridge instance (which - * automatically registers the built-in track-control RPC handler). - * - * @param destination_identity Identity of the remote participant. - * @param track_name Name of the track to unmute. - * @return true if the track was unmuted successfully. - */ - bool requestRemoteTrackUnmute(const std::string &destination_identity, - const std::string &track_name); - -private: - friend class test::LiveKitBridgeTest; - - /// Execute a track action (mute/unmute) by track name. - /// Used as the TrackActionFn callback for RpcController. - /// Throws livekit::RpcError if the track is not found. - /// @pre Caller does NOT hold mutex_ (acquires it internally). - void executeTrackAction(const rpc::track_control::Action &action, - const std::string &track_name); - - mutable std::mutex mutex_; - bool connected_; - bool connecting_; // guards against concurrent connect() calls - bool sdk_initialized_; - - std::unique_ptr room_; - std::unique_ptr rpc_controller_; - - /// All tracks created by this bridge. The bridge retains a shared_ptr so - /// it can force-release every track on disconnect() before the room is - /// destroyed, preventing dangling @c participant_ pointers. - std::vector> published_audio_tracks_; - /// @copydoc published_audio_tracks_ - std::vector> published_video_tracks_; -}; - -} // namespace livekit_bridge diff --git a/bridge/include/livekit_bridge/rpc_constants.h b/bridge/include/livekit_bridge/rpc_constants.h deleted file mode 100644 index 2c08df96..00000000 --- a/bridge/include/livekit_bridge/rpc_constants.h +++ /dev/null @@ -1,65 +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. - */ - -/// @file rpc_constants.h -/// @brief Constants for built-in bridge RPC methods. - -#pragma once - -#include - -#ifdef _WIN32 -#ifdef livekit_bridge_EXPORTS -#define LIVEKIT_BRIDGE_API __declspec(dllexport) -#else -#define LIVEKIT_BRIDGE_API __declspec(dllimport) -#endif -#else -#define LIVEKIT_BRIDGE_API -#endif - -namespace livekit_bridge { -namespace rpc { - -/// Built-in RPC method name used by remote track control. -/// Allows remote participants to mute or unmute tracks -/// published by this bridge. Must be called after connect(). -/// Audio/video tracks support mute and unmute. Data tracks -/// only support mute and unmute. -namespace track_control { - -enum class Action { kActionMute, kActionUnmute }; - -/// RPC method name registered by the bridge for remote track control. -LIVEKIT_BRIDGE_API extern const char *const kMethod; - -/// Payload action strings. -LIVEKIT_BRIDGE_API extern const char *const kActionMute; -LIVEKIT_BRIDGE_API extern const char *const kActionUnmute; - -/// Delimiter between action and track name in the payload (e.g. "mute:cam"). -LIVEKIT_BRIDGE_API extern const char kDelimiter; - -/// Response payload returned on success. -LIVEKIT_BRIDGE_API extern const char *const kResponseOk; - -/// Build a track-control RPC payload: ":". -LIVEKIT_BRIDGE_API std::string formatPayload(const char *action, - const std::string &track_name); - -} // namespace track_control -} // namespace rpc -} // namespace livekit_bridge diff --git a/bridge/src/bridge_audio_track.cpp b/bridge/src/bridge_audio_track.cpp deleted file mode 100644 index 5816cfce..00000000 --- a/bridge/src/bridge_audio_track.cpp +++ /dev/null @@ -1,128 +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. - */ - -/// @file bridge_audio_track.cpp -/// @brief Implementation of BridgeAudioTrack. - -#include "livekit_bridge/bridge_audio_track.h" - -#include "livekit/audio_frame.h" -#include "livekit/audio_source.h" -#include "livekit/local_audio_track.h" -#include "livekit/local_participant.h" - -#include -#include - -namespace livekit_bridge { - -BridgeAudioTrack::BridgeAudioTrack( - std::string name, int sample_rate, int num_channels, - std::shared_ptr source, - std::shared_ptr track, - std::shared_ptr publication, - livekit::LocalParticipant *participant) - : name_(std::move(name)), sample_rate_(sample_rate), - num_channels_(num_channels), source_(std::move(source)), - track_(std::move(track)), publication_(std::move(publication)), - participant_(participant) {} - -BridgeAudioTrack::~BridgeAudioTrack() { release(); } - -bool BridgeAudioTrack::pushFrame(const std::vector &data, - int samples_per_channel, int timeout_ms) { - livekit::AudioFrame frame(std::vector(data.begin(), data.end()), - sample_rate_, num_channels_, samples_per_channel); - - std::lock_guard lock(mutex_); - if (released_) { - return false; - } - - try { - source_->captureFrame(frame, timeout_ms); - } catch (const std::exception &e) { - std::cerr << "[error] BridgeAudioTrack captureFrame error: " << e.what() << "\n"; - return false; - } - return true; -} - -bool BridgeAudioTrack::pushFrame(const std::int16_t *data, - int samples_per_channel, int timeout_ms) { - const int total_samples = samples_per_channel * num_channels_; - livekit::AudioFrame frame( - std::vector(data, data + total_samples), sample_rate_, - num_channels_, samples_per_channel); - - std::lock_guard lock(mutex_); - if (released_) { - return false; - } - - try { - source_->captureFrame(frame, timeout_ms); - } catch (const std::exception &e) { - std::cerr << "[error] BridgeAudioTrack captureFrame error: " << e.what() << "\n"; - return false; - } - return true; -} - -void BridgeAudioTrack::mute() { - std::lock_guard lock(mutex_); - if (!released_ && track_) { - track_->mute(); - } -} - -void BridgeAudioTrack::unmute() { - std::lock_guard lock(mutex_); - if (!released_ && track_) { - track_->unmute(); - } -} - -bool BridgeAudioTrack::isReleased() const noexcept { - std::lock_guard lock(mutex_); - return released_; -} - -void BridgeAudioTrack::release() { - std::lock_guard lock(mutex_); - if (released_) { - return; - } - released_ = true; - - // Unpublish the track from the room - if (participant_ && track_ && track_->publication()) { - try { - participant_->unpublishTrack(track_->publication()->sid()); - } catch (...) { - // Best-effort cleanup; ignore errors during teardown - std::cerr << "[warn] BridgeAudioTrack unpublishTrack error, continuing " - "with cleanup\n"; - } - } - - // Release SDK objects in reverse order - track_.reset(); - source_.reset(); - participant_ = nullptr; -} - -} // namespace livekit_bridge diff --git a/bridge/src/bridge_video_track.cpp b/bridge/src/bridge_video_track.cpp deleted file mode 100644 index 7a66155f..00000000 --- a/bridge/src/bridge_video_track.cpp +++ /dev/null @@ -1,127 +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. - */ - -/// @file bridge_video_track.cpp -/// @brief Implementation of BridgeVideoTrack. - -#include "livekit_bridge/bridge_video_track.h" - -#include "livekit/local_participant.h" -#include "livekit/local_video_track.h" -#include "livekit/video_frame.h" -#include "livekit/video_source.h" - -#include -#include - -namespace livekit_bridge { - -BridgeVideoTrack::BridgeVideoTrack( - std::string name, int width, int height, - std::shared_ptr source, - std::shared_ptr track, - std::shared_ptr publication, - livekit::LocalParticipant *participant) - : name_(std::move(name)), width_(width), height_(height), - source_(std::move(source)), track_(std::move(track)), - publication_(std::move(publication)), participant_(participant) {} - -BridgeVideoTrack::~BridgeVideoTrack() { release(); } - -bool BridgeVideoTrack::pushFrame(const std::vector &rgba, - std::int64_t timestamp_us) { - livekit::VideoFrame frame( - width_, height_, livekit::VideoBufferType::RGBA, - std::vector(rgba.begin(), rgba.end())); - - std::lock_guard lock(mutex_); - if (released_) { - return false; - } - - try { - source_->captureFrame(frame, timestamp_us); - } catch (const std::exception &e) { - std::cerr << "[error] BridgeVideoTrack captureFrame error: " << e.what() << "\n"; - return false; - } - return true; -} - -bool BridgeVideoTrack::pushFrame(const std::uint8_t *rgba, - std::size_t rgba_size, - std::int64_t timestamp_us) { - livekit::VideoFrame frame(width_, height_, livekit::VideoBufferType::RGBA, - std::vector(rgba, rgba + rgba_size)); - - std::lock_guard lock(mutex_); - if (released_) { - return false; - } - - try { - source_->captureFrame(frame, timestamp_us); - } catch (const std::exception &e) { - std::cerr << "[error] BridgeVideoTrack captureFrame error: " << e.what() << "\n"; - return false; - } - return true; -} - -void BridgeVideoTrack::mute() { - std::lock_guard lock(mutex_); - if (!released_ && track_) { - track_->mute(); - } -} - -void BridgeVideoTrack::unmute() { - std::lock_guard lock(mutex_); - if (!released_ && track_) { - track_->unmute(); - } -} - -bool BridgeVideoTrack::isReleased() const noexcept { - std::lock_guard lock(mutex_); - return released_; -} - -void BridgeVideoTrack::release() { - std::lock_guard lock(mutex_); - if (released_) { - return; - } - released_ = true; - - // Unpublish the track from the room - if (participant_ && track_ && track_->publication()) { - try { - participant_->unpublishTrack(track_->publication()->sid()); - } catch (...) { - // Best-effort cleanup; ignore errors during teardown - std::cerr << "[warn] BridgeVideoTrack unpublishTrack error, continuing " - "with cleanup\n"; - } - } - - // Release SDK objects in reverse order - track_.reset(); - source_.reset(); - participant_ = nullptr; -} - -} // namespace livekit_bridge diff --git a/bridge/src/livekit_bridge.cpp b/bridge/src/livekit_bridge.cpp deleted file mode 100644 index b15587ec..00000000 --- a/bridge/src/livekit_bridge.cpp +++ /dev/null @@ -1,427 +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. - */ - -/// @file livekit_bridge.cpp -/// @brief Implementation of the LiveKitBridge high-level API. - -#include "livekit_bridge/livekit_bridge.h" -#include "livekit_bridge/rpc_constants.h" -#include "rpc_controller.h" - -#include "livekit/audio_frame.h" -#include "livekit/audio_source.h" -#include "livekit/livekit.h" -#include "livekit/local_audio_track.h" -#include "livekit/local_participant.h" -#include "livekit/local_track_publication.h" -#include "livekit/local_video_track.h" -#include "livekit/room.h" -#include "livekit/track.h" -#include "livekit/video_frame.h" -#include "livekit/video_source.h" - -#include -#include -#include - -namespace livekit_bridge { - -// --------------------------------------------------------------- -// Construction / Destruction -// --------------------------------------------------------------- - -LiveKitBridge::LiveKitBridge() - : connected_(false), connecting_(false), sdk_initialized_(false), - rpc_controller_(std::make_unique( - [this](const rpc::track_control::Action &action, - const std::string &track_name) { - executeTrackAction(action, track_name); - })) {} - -LiveKitBridge::~LiveKitBridge() { disconnect(); } - -// --------------------------------------------------------------- -// Connection -// --------------------------------------------------------------- - -bool LiveKitBridge::connect(const std::string &url, const std::string &token, - const livekit::RoomOptions &options) { - // ---- Phase 1: quick check under lock ---- - { - std::lock_guard lock(mutex_); - - if (connected_) { - return true; // already connected - } - - if (connecting_) { - return false; // another thread is already connecting - } - - connecting_ = true; - - // Initialize the LiveKit SDK (idempotent) - if (!sdk_initialized_) { - livekit::initialize(); - sdk_initialized_ = true; - } - } - - // ---- Phase 2: create room and connect without holding the lock ---- - // This avoids blocking other threads during the network handshake and - // eliminates the risk of deadlock if the SDK delivers callbacks synchronously - // during Connect(). - auto room = std::make_unique(); - assert(room != nullptr); - - bool result = room->Connect(url, token, options); - if (!result) { - std::lock_guard lock(mutex_); - connecting_ = false; - return false; - } - - // ---- Phase 3: commit under lock ---- - livekit::LocalParticipant *lp = nullptr; - { - std::lock_guard lock(mutex_); - room_ = std::move(room); - connected_ = true; - connecting_ = false; - - lp = room_->localParticipant(); - assert(lp != nullptr); - } - - rpc_controller_->enable(lp); - return true; -} - -void LiveKitBridge::disconnect() { - // Disable the RPC controller before tearing down the room. This unregisters - // built-in handlers while the LocalParticipant is still alive. - if (rpc_controller_ && rpc_controller_->isEnabled()) { - rpc_controller_->disable(); - } - - bool should_shutdown_sdk = false; - - { - std::lock_guard lock(mutex_); - - if (!connected_) { - std::cerr << "[warn] Attempting to disconnect an already disconnected " - "bridge. Things may not disconnect properly.\n"; - } - - connected_ = false; - connecting_ = false; - - for (auto &track : published_audio_tracks_) { - track->release(); - } - for (auto &track : published_video_tracks_) { - track->release(); - } - published_audio_tracks_.clear(); - published_video_tracks_.clear(); - - // Room destructor handles stopping all reader threads - room_.reset(); - - if (sdk_initialized_) { - sdk_initialized_ = false; - should_shutdown_sdk = true; - } - } - - if (should_shutdown_sdk) { - livekit::shutdown(); - } -} - -bool LiveKitBridge::isConnected() const { - std::lock_guard lock(mutex_); - return connected_; -} - -// --------------------------------------------------------------- -// Track creation (publishing) -// --------------------------------------------------------------- - -std::shared_ptr -LiveKitBridge::createAudioTrack(const std::string &name, int sample_rate, - int num_channels, livekit::TrackSource source) { - std::lock_guard lock(mutex_); - - if (!connected_ || !room_) { - throw std::runtime_error( - "createAudioTrack requires an active connection; call connect() first"); - } - - // 1. Create audio source (real-time mode, queue_size_ms=0) - auto audio_source = - std::make_shared(sample_rate, num_channels, 0); - - // 2. Create local audio track - auto track = - livekit::LocalAudioTrack::createLocalAudioTrack(name, audio_source); - - // 3. Publish with the caller-specified source - livekit::TrackPublishOptions opts; - opts.source = source; - - auto lp = room_->localParticipant(); - assert(lp != nullptr); - - lp->publishTrack(track, opts); - auto publication = track->publication(); - - // 4. Wrap in handle and retain a reference - auto bridge_track = std::shared_ptr(new BridgeAudioTrack( - name, sample_rate, num_channels, std::move(audio_source), - std::move(track), publication, lp)); - published_audio_tracks_.emplace_back(bridge_track); - return bridge_track; -} - -std::shared_ptr -LiveKitBridge::createVideoTrack(const std::string &name, int width, int height, - livekit::TrackSource source) { - std::lock_guard lock(mutex_); - - if (!connected_ || !room_) { - throw std::runtime_error( - "createVideoTrack requires an active connection; call connect() first"); - } - - // 1. Create video source - auto video_source = std::make_shared(width, height); - - // 2. Create local video track - auto track = - livekit::LocalVideoTrack::createLocalVideoTrack(name, video_source); - - // 3. Publish with the caller-specified source - livekit::TrackPublishOptions opts; - opts.source = source; - - auto lp = room_->localParticipant(); - assert(lp != nullptr); - - lp->publishTrack(track, opts); - auto publication = track->publication(); - - // 4. Wrap in handle and retain a reference - auto bridge_track = std::shared_ptr( - new BridgeVideoTrack(name, width, height, std::move(video_source), - std::move(track), publication, lp)); - published_video_tracks_.emplace_back(bridge_track); - return bridge_track; -} - -// --------------------------------------------------------------- -// Incoming frame callbacks -// --------------------------------------------------------------- - -void LiveKitBridge::setOnAudioFrameCallback( - const std::string &participant_identity, livekit::TrackSource source, - AudioFrameCallback callback) { - std::lock_guard lock(mutex_); - if (!room_) { - std::cerr << "[warn] setOnAudioFrameCallback called before connect(); " - "ignored\n"; - return; - } - room_->setOnAudioFrameCallback(participant_identity, source, - std::move(callback)); -} - -void LiveKitBridge::setOnVideoFrameCallback( - const std::string &participant_identity, livekit::TrackSource source, - VideoFrameCallback callback) { - std::lock_guard lock(mutex_); - if (!room_) { - std::cerr << "[warn] setOnVideoFrameCallback called before connect(); " - "ignored\n"; - return; - } - room_->setOnVideoFrameCallback(participant_identity, source, - std::move(callback)); -} - -void LiveKitBridge::clearOnAudioFrameCallback( - const std::string &participant_identity, livekit::TrackSource source) { - std::lock_guard lock(mutex_); - if (!room_) { - return; - } - room_->clearOnAudioFrameCallback(participant_identity, source); -} - -void LiveKitBridge::clearOnVideoFrameCallback( - const std::string &participant_identity, livekit::TrackSource source) { - std::lock_guard lock(mutex_); - if (!room_) { - return; - } - room_->clearOnVideoFrameCallback(participant_identity, source); -} - -// --------------------------------------------------------------- -// RPC (delegates to RpcController) -// --------------------------------------------------------------- - -std::optional -LiveKitBridge::performRpc(const std::string &destination_identity, - const std::string &method, const std::string &payload, - const std::optional &response_timeout) { - - if (!isConnected()) { - return std::nullopt; - } - - try { - return rpc_controller_->performRpc(destination_identity, method, payload, - response_timeout); - } catch (const std::exception &e) { - std::cerr << "[LiveKitBridge] Exception: " << e.what() << "\n"; - return std::nullopt; - } catch (const std::runtime_error &e) { - std::cerr << "[LiveKitBridge] Runtime error: " << e.what() << "\n"; - return std::nullopt; - } catch (const livekit::RpcError &e) { - std::cerr << "[LiveKitBridge] RPC error: " << e.what() << "\n"; - return std::nullopt; - } -} - -bool LiveKitBridge::registerRpcMethod( - const std::string &method_name, - livekit::LocalParticipant::RpcHandler handler) { - - if (!isConnected()) { - return false; - } - try { - rpc_controller_->registerRpcMethod(method_name, std::move(handler)); - return true; - } catch (const std::exception &e) { - std::cerr << "[LiveKitBridge] Exception: " << e.what() << "\n"; - return false; - } catch (const std::runtime_error &e) { - std::cerr << "[LiveKitBridge] Runtime error: " << e.what() << "\n"; - return false; - } catch (const livekit::RpcError &e) { - std::cerr << "[LiveKitBridge] RPC error: " << e.what() << "\n"; - return false; - } -} - -bool LiveKitBridge::unregisterRpcMethod(const std::string &method_name) { - if (!isConnected()) { - return false; - } - try { - rpc_controller_->unregisterRpcMethod(method_name); - return true; - } catch (const std::exception &e) { - std::cerr << "[LiveKitBridge] Exception: " << e.what() << "\n"; - return false; - } catch (const std::runtime_error &e) { - std::cerr << "[LiveKitBridge] Runtime error: " << e.what() << "\n"; - return false; - } catch (const livekit::RpcError &e) { - std::cerr << "[LiveKitBridge] RPC error: " << e.what() << "\n"; - return false; - } -} - -bool LiveKitBridge::requestRemoteTrackMute( - const std::string &destination_identity, const std::string &track_name) { - if (!isConnected()) { - return false; - } - try { - rpc_controller_->requestRemoteTrackMute(destination_identity, track_name); - return true; - } catch (const std::exception &e) { - std::cerr << "[LiveKitBridge] Exception: " << e.what() << "\n"; - return false; - } catch (const std::runtime_error &e) { - std::cerr << "[LiveKitBridge] Runtime error: " << e.what() << "\n"; - return false; - } catch (const livekit::RpcError &e) { - std::cerr << "[LiveKitBridge] RPC error: " << e.what() << "\n"; - return false; - } -} - -bool LiveKitBridge::requestRemoteTrackUnmute( - const std::string &destination_identity, const std::string &track_name) { - if (!isConnected()) { - return false; - } - try { - rpc_controller_->requestRemoteTrackUnmute(destination_identity, track_name); - return true; - } catch (const std::exception &e) { - std::cerr << "[LiveKitBridge] Exception: " << e.what() << "\n"; - return false; - } catch (const std::runtime_error &e) { - std::cerr << "[LiveKitBridge] Runtime error: " << e.what() << "\n"; - return false; - } catch (const livekit::RpcError &e) { - std::cerr << "[LiveKitBridge] RPC error: " << e.what() << "\n"; - return false; - } -} - -// --------------------------------------------------------------- -// Track action callback for RpcController -// --------------------------------------------------------------- - -void LiveKitBridge::executeTrackAction(const rpc::track_control::Action &action, - const std::string &track_name) { - std::lock_guard lock(mutex_); - - for (auto &track : published_audio_tracks_) { - if (track->name() == track_name && !track->isReleased()) { - if (action == rpc::track_control::Action::kActionMute) { - track->mute(); - } else { - track->unmute(); - } - return; - } - } - - for (auto &track : published_video_tracks_) { - if (track->name() == track_name && !track->isReleased()) { - if (action == rpc::track_control::Action::kActionMute) { - track->mute(); - } else { - track->unmute(); - } - return; - } - } - - throw livekit::RpcError(livekit::RpcError::ErrorCode::APPLICATION_ERROR, - "track not found: " + track_name); -} - -} // namespace livekit_bridge diff --git a/bridge/src/rpc_constants.cpp b/bridge/src/rpc_constants.cpp deleted file mode 100644 index 03386fe9..00000000 --- a/bridge/src/rpc_constants.cpp +++ /dev/null @@ -1,41 +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 "livekit_bridge/rpc_constants.h" - -namespace livekit_bridge { -namespace rpc { -namespace track_control { - -const char *const kMethod = "lk.bridge.track-control"; -const char *const kActionMute = "mute"; -const char *const kActionUnmute = "unmute"; -const char kDelimiter = ':'; -const char *const kResponseOk = "ok"; - -std::string formatPayload(const char *action, const std::string &track_name) { - std::string payload; - payload.reserve(std::char_traits::length(action) + 1 + - track_name.size()); - payload += action; - payload += kDelimiter; - payload += track_name; - return payload; -} - -} // namespace track_control -} // namespace rpc -} // namespace livekit_bridge diff --git a/bridge/src/rpc_controller.cpp b/bridge/src/rpc_controller.cpp deleted file mode 100644 index 31514666..00000000 --- a/bridge/src/rpc_controller.cpp +++ /dev/null @@ -1,144 +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. - */ - -/// @file rpc_controller.cpp -/// @brief Implementation of RpcController. - -#include "rpc_controller.h" -#include "livekit_bridge/rpc_constants.h" - -#include "livekit/local_participant.h" -#include "livekit/rpc_error.h" - -#include -#include - -namespace livekit_bridge { - -RpcController::RpcController(TrackActionFn track_action_fn) - : track_action_fn_(std::move(track_action_fn)), lp_(nullptr) {} - -void RpcController::enable(livekit::LocalParticipant *lp) { - assert(lp != nullptr); - lp_ = lp; - enableBuiltInHandlers(); -} - -void RpcController::disable() { - if (lp_) { - disableBuiltInHandlers(); - } - lp_ = nullptr; -} - -// --------------------------------------------------------------- -// Generic RPC -// --------------------------------------------------------------- - -std::string -RpcController::performRpc(const std::string &destination_identity, - const std::string &method, const std::string &payload, - const std::optional &response_timeout) { - assert(lp_ != nullptr); - return lp_->performRpc(destination_identity, method, payload, - response_timeout); -} - -// --------------------------------------------------------------- -// User-registered handlers -// --------------------------------------------------------------- - -void RpcController::registerRpcMethod( - const std::string &method_name, - livekit::LocalParticipant::RpcHandler handler) { - assert(lp_ != nullptr); - lp_->registerRpcMethod(method_name, std::move(handler)); -} - -void RpcController::unregisterRpcMethod(const std::string &method_name) { - assert(lp_ != nullptr); - lp_->unregisterRpcMethod(method_name); -} - -// --------------------------------------------------------------- -// Built-in outgoing convenience (track control) -// --------------------------------------------------------------- - -void RpcController::requestRemoteTrackMute(const std::string &destination_identity, - const std::string &track_name) { - namespace tc = rpc::track_control; - performRpc(destination_identity, tc::kMethod, - tc::formatPayload(tc::kActionMute, track_name), std::nullopt); -} - -void RpcController::requestRemoteTrackUnmute( - const std::string &destination_identity, const std::string &track_name) { - namespace tc = rpc::track_control; - performRpc(destination_identity, tc::kMethod, - tc::formatPayload(tc::kActionUnmute, track_name), std::nullopt); -} - -// --------------------------------------------------------------- -// Built-in handler registration -// --------------------------------------------------------------- - -void RpcController::enableBuiltInHandlers() { - assert(lp_ != nullptr); - lp_->registerRpcMethod(rpc::track_control::kMethod, - [this](const livekit::RpcInvocationData &data) - -> std::optional { - return handleTrackControlRpc(data); - }); -} - -void RpcController::disableBuiltInHandlers() { - assert(lp_ != nullptr); - lp_->unregisterRpcMethod(rpc::track_control::kMethod); -} - -// --------------------------------------------------------------- -// Built-in handler: track control -// --------------------------------------------------------------- - -std::optional -RpcController::handleTrackControlRpc(const livekit::RpcInvocationData &data) { - namespace tc = rpc::track_control; - - std::cout << "[RpcController] Handling track control RPC: " << data.payload - << "\n"; - auto delim = data.payload.find(tc::kDelimiter); - if (delim == std::string::npos || delim == 0) { - throw livekit::RpcError( - livekit::RpcError::ErrorCode::APPLICATION_ERROR, - "invalid payload format, expected \":\""); - } - const std::string action = data.payload.substr(0, delim); - const std::string track_name = data.payload.substr(delim + 1); - - if (action != tc::kActionMute && action != tc::kActionUnmute) { - throw livekit::RpcError(livekit::RpcError::ErrorCode::APPLICATION_ERROR, - "unknown action: " + action); - } - - const auto action_enum = action == tc::kActionMute - ? rpc::track_control::Action::kActionMute - : rpc::track_control::Action::kActionUnmute; - - track_action_fn_(action_enum, track_name); - return tc::kResponseOk; -} - -} // namespace livekit_bridge diff --git a/bridge/src/rpc_controller.h b/bridge/src/rpc_controller.h deleted file mode 100644 index 97d096dd..00000000 --- a/bridge/src/rpc_controller.h +++ /dev/null @@ -1,145 +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. - */ - -/// @file rpc_controller.h -/// @brief Internal RPC controller that owns all RPC concerns for the bridge. - -#pragma once - -#include "livekit/local_participant.h" -#include "livekit_bridge/rpc_constants.h" - -#include -#include -#include -#include - -namespace livekit { -struct RpcInvocationData; -} // namespace livekit - -namespace livekit_bridge { - -namespace test { -class RpcControllerTest; -} // namespace test - -/** - * Owns all RPC concerns for the LiveKitBridge: built-in handler registration - * and dispatch, user-registered custom handlers, and outgoing RPC calls. - * - * The controller is bound to a LocalParticipant via enable() and unbound via - * disable(). All public methods require the controller to be enabled (i.e., - * enable() has been called and disable() has not). - * - * Built-in handlers (e.g., track-control) are automatically registered on - * enable() and unregistered on disable(). User-registered handlers are - * forwarded directly to the underlying LocalParticipant. - * - * Not part of the public API; lives in bridge/src/. - */ -class RpcController { -public: - /// Callback the bridge provides to execute a track action - /// (mute/unmute). Throws livekit::RpcError if the track is not found - /// or the action is invalid. - using TrackActionFn = std::function; - - explicit RpcController(TrackActionFn track_action_fn); - - /// Bind to a LocalParticipant and register all built-in RPC handlers. - /// @pre @p lp must be non-null and remain valid until disable() is called. - void enable(livekit::LocalParticipant *lp); - - /// Unregister built-in handlers and unbind from the LocalParticipant. - void disable(); - - /// Whether the controller is currently bound to a LocalParticipant. - bool isEnabled() const { return lp_ != nullptr; } - - // -- Generic RPC -- - - /// @brief Perform an RPC call to a remote participant. - /// @param destination_identity Identity of the destination participant. - /// @param method Name of the RPC method to invoke. - /// @param payload Request payload to send to the remote - /// handler. - /// @param response_timeout Optional timeout in seconds for receiving - /// a response. If not set, the server default - /// timeout (15 seconds) is used. - /// @return The response payload returned by the remote handler. - /// @throws if the LocalParticipant performRpc fails. - std::string performRpc(const std::string &destination_identity, - const std::string &method, const std::string &payload, - const std::optional &response_timeout); - - // -- User-registered handlers -- - /// @brief Register a handler for an incoming RPC method. - /// @param method_name Name of the RPC method to handle. - /// @param handler Callback to execute when an invocation is received. - /// The handler may return an optional response payload - /// or throw an RpcError to signal failure. - /// @throws if the LocalParticipant registerRpcMethod fails. - void registerRpcMethod(const std::string &method_name, - livekit::LocalParticipant::RpcHandler handler); - - /// @brief Unregister a handler for an incoming RPC method. - /// @param method_name Name of the RPC method to unregister. - /// @throws if the LocalParticipant unregisterRpcMethod fails. - void unregisterRpcMethod(const std::string &method_name); - - // -- Built-in outgoing convenience (track control) -- - - /// @brief Request a remote participant to mute a published track. - /// @param destination_identity Identity of the remote participant. - /// @param track_name Name of the track to mute. - /// @throws if the LocalParticipant requestRemoteTrackMute fails. - void requestRemoteTrackMute(const std::string &destination_identity, - const std::string &track_name); - /// @brief Request a remote participant to unmute a published track. - /// @param destination_identity Identity of the remote participant. - /// @param track_name Name of the track to unmute. - /// @throws if the LocalParticipant requestRemoteTrackUnmute fails. - void requestRemoteTrackUnmute(const std::string &destination_identity, - const std::string &track_name); - -private: - friend class test::RpcControllerTest; - - /// @brief Enable built-in handlers. - /// @throws if the LocalParticipant registerRpcMethod fails. - void enableBuiltInHandlers(); - - /// @brief Disable built-in handlers. - /// @throws if the LocalParticipant unregisterRpcMethod fails. - void disableBuiltInHandlers(); - - /// @brief Handle a track control RPC. - /// @param data The RPC invocation data. - /// @return The response payload returned by the remote handler. - /// @throws if the RPC is invalid or the track is not found. - std::optional - handleTrackControlRpc(const livekit::RpcInvocationData &data); - - /// Callback to execute a track action RPC - TrackActionFn track_action_fn_; - - /// The LocalParticipant bound to the controller. - livekit::LocalParticipant *lp_; -}; - -} // namespace livekit_bridge diff --git a/bridge/tests/CMakeLists.txt b/bridge/tests/CMakeLists.txt deleted file mode 100644 index 227f0a0c..00000000 --- a/bridge/tests/CMakeLists.txt +++ /dev/null @@ -1,100 +0,0 @@ -cmake_minimum_required(VERSION 3.20) - -# ============================================================================ -# Google Test Setup via FetchContent -# ============================================================================ - -include(FetchContent) - -FetchContent_Declare( - googletest - GIT_REPOSITORY https://github.com/google/googletest.git - GIT_TAG v1.14.0 -) - -# Prevent overriding the parent project's compiler/linker settings on Windows -set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) - -# Don't install gtest when installing this project -set(INSTALL_GTEST OFF CACHE BOOL "" FORCE) - -FetchContent_MakeAvailable(googletest) - -# Enable CTest -enable_testing() -include(GoogleTest) - -# ============================================================================ -# Bridge Unit Tests -# ============================================================================ - -file(GLOB BRIDGE_TEST_SOURCES - "${CMAKE_CURRENT_SOURCE_DIR}/*.cpp" -) - -if(BRIDGE_TEST_SOURCES) - add_executable(livekit_bridge_tests - ${BRIDGE_TEST_SOURCES} - ) - - target_include_directories(livekit_bridge_tests - PRIVATE - ${CMAKE_CURRENT_SOURCE_DIR}/../src - ) - - target_link_libraries(livekit_bridge_tests - PRIVATE - livekit_bridge - GTest::gtest_main - ) - - # Copy shared libraries to test executable directory - if(WIN32) - add_custom_command(TARGET livekit_bridge_tests POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - $ - $ - COMMAND ${CMAKE_COMMAND} -E copy_if_different - $ - $ - COMMAND ${CMAKE_COMMAND} -E copy_if_different - "$/livekit_ffi.dll" - $ - COMMENT "Copying DLLs to bridge test directory" - ) - elseif(APPLE) - add_custom_command(TARGET livekit_bridge_tests POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - $ - $ - COMMAND ${CMAKE_COMMAND} -E copy_if_different - $ - $ - COMMAND ${CMAKE_COMMAND} -E copy_if_different - "$/liblivekit_ffi.dylib" - $ - COMMENT "Copying dylibs to bridge test directory" - ) - else() - add_custom_command(TARGET livekit_bridge_tests POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_if_different - $ - $ - COMMAND ${CMAKE_COMMAND} -E copy_if_different - $ - $ - COMMAND ${CMAKE_COMMAND} -E copy_if_different - "$/liblivekit_ffi.so" - $ - COMMENT "Copying shared libraries to bridge test directory" - ) - endif() - - # Register tests with CTest - gtest_discover_tests(livekit_bridge_tests - WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} - DISCOVERY_MODE PRE_TEST - PROPERTIES - LABELS "bridge_unit" - ) -endif() diff --git a/bridge/tests/integration/test_bridge_rpc_roundtrip.cpp b/bridge/tests/integration/test_bridge_rpc_roundtrip.cpp deleted file mode 100644 index e649450a..00000000 --- a/bridge/tests/integration/test_bridge_rpc_roundtrip.cpp +++ /dev/null @@ -1,271 +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 "../common/bridge_test_common.h" -#include - -namespace livekit_bridge { -namespace test { - -class BridgeRpcRoundtripTest : public BridgeTestBase {}; - -// --------------------------------------------------------------------------- -// Test 1: Basic RPC round-trip through the bridge. -// -// Receiver registers an "echo" handler, caller performs an RPC call, and the -// response is verified. -// --------------------------------------------------------------------------- -TEST_F(BridgeRpcRoundtripTest, BasicRpcRoundTrip) { - skipIfNotConfigured(); - - std::cout << "\n=== Bridge RPC Round-Trip Test ===" << std::endl; - - LiveKitBridge caller; - LiveKitBridge receiver; - - ASSERT_TRUE(connectPair(caller, receiver)); - - const std::string receiver_identity = "rpc-receiver"; - - std::atomic rpc_calls_received{0}; - receiver.registerRpcMethod( - "echo", - [&rpc_calls_received](const livekit::RpcInvocationData &data) - -> std::optional { - rpc_calls_received++; - size_t checksum = 0; - for (char c : data.payload) { - checksum += static_cast(c); - } - return "echo:" + std::to_string(data.payload.size()) + ":" + - std::to_string(checksum); - }); - - std::cout << "RPC handler registered, performing call..." << std::endl; - - std::string test_payload = "hello from bridge"; - std::string response = - caller.performRpc(receiver_identity, "echo", test_payload, 10.0); - - size_t expected_checksum = 0; - for (char c : test_payload) { - expected_checksum += static_cast(c); - } - std::string expected_response = - "echo:" + std::to_string(test_payload.size()) + ":" + - std::to_string(expected_checksum); - - std::cout << "Response: " << response << std::endl; - std::cout << "Expected: " << expected_response << std::endl; - - EXPECT_EQ(response, expected_response); - EXPECT_EQ(rpc_calls_received.load(), 1); - - receiver.unregisterRpcMethod("echo"); -} - -// --------------------------------------------------------------------------- -// Test 2: RPC error propagation. -// -// The handler throws an RpcError with a custom code and message. The caller -// should catch the same error code, message, and data. -// --------------------------------------------------------------------------- -TEST_F(BridgeRpcRoundtripTest, RpcErrorPropagation) { - skipIfNotConfigured(); - - std::cout << "\n=== Bridge RPC Error Propagation Test ===" << std::endl; - - LiveKitBridge caller; - LiveKitBridge receiver; - - ASSERT_TRUE(connectPair(caller, receiver)); - - const std::string receiver_identity = "rpc-receiver"; - - receiver.registerRpcMethod( - "fail-method", - [](const livekit::RpcInvocationData &) -> std::optional { - throw livekit::RpcError(livekit::RpcError::ErrorCode::APPLICATION_ERROR, - "intentional failure", "extra-data"); - }); - - std::cout << "Calling method that throws RpcError..." << std::endl; - - try { - caller.performRpc(receiver_identity, "fail-method", "", 10.0); - FAIL() << "Expected RpcError to be thrown"; - } catch (const livekit::RpcError &e) { - std::cout << "Caught RpcError: code=" << e.code() << " message=\"" - << e.message() << "\"" - << " data=\"" << e.data() << "\"" << std::endl; - - EXPECT_EQ(static_cast(e.code()), - livekit::RpcError::ErrorCode::APPLICATION_ERROR); - EXPECT_EQ(e.message(), "intentional failure"); - EXPECT_EQ(e.data(), "extra-data"); - } - - receiver.unregisterRpcMethod("fail-method"); -} - -// --------------------------------------------------------------------------- -// Test 3: Calling an unregistered method returns UNSUPPORTED_METHOD. -// --------------------------------------------------------------------------- -TEST_F(BridgeRpcRoundtripTest, UnregisteredMethod) { - skipIfNotConfigured(); - - std::cout << "\n=== Bridge RPC Unsupported Method Test ===" << std::endl; - - LiveKitBridge caller; - LiveKitBridge receiver; - - ASSERT_TRUE(connectPair(caller, receiver)); - - const std::string receiver_identity = "rpc-receiver"; - - std::cout << "Calling nonexistent method..." << std::endl; - - try { - caller.performRpc(receiver_identity, "nonexistent-method", "", 5.0); - FAIL() << "Expected RpcError for unsupported method"; - } catch (const livekit::RpcError &e) { - std::cout << "Caught RpcError: code=" << e.code() << " message=\"" - << e.message() << "\"" << std::endl; - - EXPECT_EQ(static_cast(e.code()), - livekit::RpcError::ErrorCode::UNSUPPORTED_METHOD); - } -} - -// =========================================================================== -// Remote Track Control Tests -// =========================================================================== - -class BridgeRemoteTrackControlTest : public BridgeTestBase {}; - -// --------------------------------------------------------------------------- -// Test 4: Remote mute of an audio track. -// -// Publisher creates an audio track, enables remote track control. Controller -// requests mute, then unmute. -// --------------------------------------------------------------------------- -TEST_F(BridgeRemoteTrackControlTest, RemoteMuteAudioTrack) { - skipIfNotConfigured(); - - std::cout << "\n=== Bridge Remote Mute Audio Track Test ===" << std::endl; - - LiveKitBridge publisher; - LiveKitBridge controller; - - ASSERT_TRUE(connectPair(controller, publisher)); - - const std::string publisher_identity = "rpc-receiver"; - - auto audio_track = publisher.createAudioTrack( - "mic", 48000, 1, livekit::TrackSource::SOURCE_MICROPHONE); - ASSERT_NE(audio_track, nullptr); - - std::this_thread::sleep_for(2s); - - std::cout << "Requesting mute..." << std::endl; - EXPECT_NO_THROW(controller.requestRemoteTrackMute(publisher_identity, "mic")); - - std::vector silence(480, 0); - bool pushed_while_muted = audio_track->pushFrame(silence, 480); - std::cout << "pushFrame while muted: " << pushed_while_muted << std::endl; - - std::cout << "Requesting unmute..." << std::endl; - EXPECT_NO_THROW( - controller.requestRemoteTrackUnmute(publisher_identity, "mic")); - - bool pushed_after_unmute = audio_track->pushFrame(silence, 480); - EXPECT_TRUE(pushed_after_unmute); - std::cout << "pushFrame after unmute: " << pushed_after_unmute << std::endl; - - audio_track->release(); -} - -// --------------------------------------------------------------------------- -// Test 5: Remote mute of a video track. -// --------------------------------------------------------------------------- -TEST_F(BridgeRemoteTrackControlTest, RemoteMuteVideoTrack) { - skipIfNotConfigured(); - - std::cout << "\n=== Bridge Remote Mute Video Track Test ===" << std::endl; - - LiveKitBridge publisher; - LiveKitBridge controller; - - ASSERT_TRUE(connectPair(controller, publisher)); - - const std::string publisher_identity = "rpc-receiver"; - - auto video_track = publisher.createVideoTrack( - "cam", 320, 240, livekit::TrackSource::SOURCE_CAMERA); - ASSERT_NE(video_track, nullptr); - - std::this_thread::sleep_for(2s); - - std::cout << "Requesting mute on video track..." << std::endl; - EXPECT_NO_THROW(controller.requestRemoteTrackMute(publisher_identity, "cam")); - - std::cout << "Requesting unmute on video track..." << std::endl; - EXPECT_NO_THROW( - controller.requestRemoteTrackUnmute(publisher_identity, "cam")); - - std::vector frame(320 * 240 * 4, 128); - bool pushed_after_unmute = video_track->pushFrame(frame); - EXPECT_TRUE(pushed_after_unmute); - std::cout << "pushFrame after unmute: " << pushed_after_unmute << std::endl; - - video_track->release(); -} - -// --------------------------------------------------------------------------- -// Test 7: Remote mute on a nonexistent track returns an error. -// --------------------------------------------------------------------------- -TEST_F(BridgeRemoteTrackControlTest, RemoteMuteNonexistentTrack) { - skipIfNotConfigured(); - - std::cout << "\n=== Bridge Remote Mute Nonexistent Track Test ===" - << std::endl; - - LiveKitBridge publisher; - LiveKitBridge controller; - - ASSERT_TRUE(connectPair(controller, publisher)); - - const std::string publisher_identity = "rpc-receiver"; - - std::this_thread::sleep_for(2s); - - std::cout << "Requesting mute on nonexistent track..." << std::endl; - try { - controller.requestRemoteTrackMute(publisher_identity, "no-such-track"); - FAIL() << "Expected RpcError for nonexistent track"; - } catch (const livekit::RpcError &e) { - std::cout << "Caught RpcError: code=" << e.code() << " message=\"" - << e.message() << "\"" << std::endl; - - EXPECT_EQ(static_cast(e.code()), - livekit::RpcError::ErrorCode::APPLICATION_ERROR); - EXPECT_NE(e.message().find("track not found"), std::string::npos) - << "Error message should mention 'track not found'"; - } -} - -} // namespace test -} // namespace livekit_bridge diff --git a/bridge/tests/test_bridge_audio_track.cpp b/bridge/tests/test_bridge_audio_track.cpp deleted file mode 100644 index 8e7274e9..00000000 --- a/bridge/tests/test_bridge_audio_track.cpp +++ /dev/null @@ -1,118 +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. - */ - -/// @file test_bridge_audio_track.cpp -/// @brief Unit tests for BridgeAudioTrack. - -#include -#include - -#include -#include -#include - -namespace livekit_bridge { -namespace test { - -class BridgeAudioTrackTest : public ::testing::Test { -protected: - /// Create a BridgeAudioTrack with null SDK objects for pure-logic testing. - /// The track is usable for accessor and state management tests but will - /// crash if pushFrame / mute / unmute try to dereference SDK pointers - /// on a non-released track. - static BridgeAudioTrack createNullTrack(const std::string &name = "mic", - int sample_rate = 48000, - int num_channels = 2) { - return BridgeAudioTrack(name, sample_rate, num_channels, - nullptr, // source - nullptr, // track - nullptr, // publication - nullptr // participant - ); - } -}; - -TEST_F(BridgeAudioTrackTest, AccessorsReturnConstructionValues) { - auto track = createNullTrack("test-mic", 16000, 1); - - EXPECT_EQ(track.name(), "test-mic") << "Name should match construction value"; - EXPECT_EQ(track.sampleRate(), 16000) << "Sample rate should match"; - EXPECT_EQ(track.numChannels(), 1) << "Channel count should match"; -} - -TEST_F(BridgeAudioTrackTest, InitiallyNotReleased) { - auto track = createNullTrack(); - - EXPECT_FALSE(track.isReleased()) - << "Track should not be released immediately after construction"; -} - -TEST_F(BridgeAudioTrackTest, ReleaseMarksTrackAsReleased) { - auto track = createNullTrack(); - - track.release(); - - EXPECT_TRUE(track.isReleased()) - << "Track should be released after calling release()"; -} - -TEST_F(BridgeAudioTrackTest, DoubleReleaseIsIdempotent) { - auto track = createNullTrack(); - - track.release(); - EXPECT_NO_THROW(track.release()) - << "Calling release() a second time should be a no-op"; - EXPECT_TRUE(track.isReleased()); -} - -TEST_F(BridgeAudioTrackTest, PushFrameAfterReleaseReturnsFalse) { - auto track = createNullTrack(); - track.release(); - - std::vector data(960, 0); - - EXPECT_FALSE(track.pushFrame(data, 480)) - << "pushFrame (vector) on a released track should return false"; -} - -TEST_F(BridgeAudioTrackTest, PushFrameRawPointerAfterReleaseReturnsFalse) { - auto track = createNullTrack(); - track.release(); - - std::vector data(960, 0); - - EXPECT_FALSE(track.pushFrame(data.data(), 480)) - << "pushFrame (raw pointer) on a released track should return false"; -} - -TEST_F(BridgeAudioTrackTest, MuteOnReleasedTrackDoesNotCrash) { - auto track = createNullTrack(); - track.release(); - - EXPECT_NO_THROW(track.mute()) - << "mute() on a released track should be a no-op"; -} - -TEST_F(BridgeAudioTrackTest, UnmuteOnReleasedTrackDoesNotCrash) { - auto track = createNullTrack(); - track.release(); - - EXPECT_NO_THROW(track.unmute()) - << "unmute() on a released track should be a no-op"; -} - -} // namespace test -} // namespace livekit_bridge diff --git a/bridge/tests/test_bridge_video_track.cpp b/bridge/tests/test_bridge_video_track.cpp deleted file mode 100644 index 08517b02..00000000 --- a/bridge/tests/test_bridge_video_track.cpp +++ /dev/null @@ -1,114 +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. - */ - -/// @file test_bridge_video_track.cpp -/// @brief Unit tests for BridgeVideoTrack. - -#include -#include - -#include -#include -#include - -namespace livekit_bridge { -namespace test { - -class BridgeVideoTrackTest : public ::testing::Test { -protected: - /// Create a BridgeVideoTrack with null SDK objects for pure-logic testing. - static BridgeVideoTrack createNullTrack(const std::string &name = "cam", - int width = 1280, int height = 720) { - return BridgeVideoTrack(name, width, height, - nullptr, // source - nullptr, // track - nullptr, // publication - nullptr // participant - ); - } -}; - -TEST_F(BridgeVideoTrackTest, AccessorsReturnConstructionValues) { - auto track = createNullTrack("test-cam", 640, 480); - - EXPECT_EQ(track.name(), "test-cam") << "Name should match construction value"; - EXPECT_EQ(track.width(), 640) << "Width should match"; - EXPECT_EQ(track.height(), 480) << "Height should match"; -} - -TEST_F(BridgeVideoTrackTest, InitiallyNotReleased) { - auto track = createNullTrack(); - - EXPECT_FALSE(track.isReleased()) - << "Track should not be released immediately after construction"; -} - -TEST_F(BridgeVideoTrackTest, ReleaseMarksTrackAsReleased) { - auto track = createNullTrack(); - - track.release(); - - EXPECT_TRUE(track.isReleased()) - << "Track should be released after calling release()"; -} - -TEST_F(BridgeVideoTrackTest, DoubleReleaseIsIdempotent) { - auto track = createNullTrack(); - - track.release(); - EXPECT_NO_THROW(track.release()) - << "Calling release() a second time should be a no-op"; - EXPECT_TRUE(track.isReleased()); -} - -TEST_F(BridgeVideoTrackTest, PushFrameAfterReleaseReturnsFalse) { - auto track = createNullTrack(); - track.release(); - - std::vector data(1280 * 720 * 4, 0); - - EXPECT_FALSE(track.pushFrame(data)) - << "pushFrame (vector) on a released track should return false"; -} - -TEST_F(BridgeVideoTrackTest, PushFrameRawPointerAfterReleaseReturnsFalse) { - auto track = createNullTrack(); - track.release(); - - std::vector data(1280 * 720 * 4, 0); - - EXPECT_FALSE(track.pushFrame(data.data(), data.size())) - << "pushFrame (raw pointer) on a released track should return false"; -} - -TEST_F(BridgeVideoTrackTest, MuteOnReleasedTrackDoesNotCrash) { - auto track = createNullTrack(); - track.release(); - - EXPECT_NO_THROW(track.mute()) - << "mute() on a released track should be a no-op"; -} - -TEST_F(BridgeVideoTrackTest, UnmuteOnReleasedTrackDoesNotCrash) { - auto track = createNullTrack(); - track.release(); - - EXPECT_NO_THROW(track.unmute()) - << "unmute() on a released track should be a no-op"; -} - -} // namespace test -} // namespace livekit_bridge diff --git a/bridge/tests/test_livekit_bridge.cpp b/bridge/tests/test_livekit_bridge.cpp deleted file mode 100644 index 43c8f6fb..00000000 --- a/bridge/tests/test_livekit_bridge.cpp +++ /dev/null @@ -1,138 +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. - */ - -/// @file test_livekit_bridge.cpp -/// @brief Unit tests for LiveKitBridge. - -#include -#include - -#include - -#include - -namespace livekit_bridge { -namespace test { - -class LiveKitBridgeTest : public ::testing::Test { -protected: - // No SetUp/TearDown needed -- we test the bridge without initializing - // the LiveKit SDK, since we only exercise pre-connection behaviour. -}; - -// ============================================================================ -// Initial state -// ============================================================================ - -TEST_F(LiveKitBridgeTest, InitiallyNotConnected) { - LiveKitBridge bridge; - - EXPECT_FALSE(bridge.isConnected()) - << "Bridge should not be connected immediately after construction"; -} - -TEST_F(LiveKitBridgeTest, DisconnectBeforeConnectIsNoOp) { - LiveKitBridge bridge; - - EXPECT_NO_THROW(bridge.disconnect()) - << "disconnect() on an unconnected bridge should be a safe no-op"; - - EXPECT_FALSE(bridge.isConnected()); -} - -TEST_F(LiveKitBridgeTest, MultipleDisconnectsAreIdempotent) { - LiveKitBridge bridge; - - EXPECT_NO_THROW({ - bridge.disconnect(); - bridge.disconnect(); - bridge.disconnect(); - }) << "Multiple disconnect() calls should be safe"; -} - -TEST_F(LiveKitBridgeTest, DestructorOnUnconnectedBridgeIsSafe) { - // Just verify no crash when the bridge is destroyed without connecting. - EXPECT_NO_THROW({ - LiveKitBridge bridge; - // bridge goes out of scope here - }); -} - -// ============================================================================ -// Track creation before connection -// ============================================================================ - -TEST_F(LiveKitBridgeTest, CreateAudioTrackBeforeConnectThrows) { - LiveKitBridge bridge; - - EXPECT_THROW(bridge.createAudioTrack("mic", 48000, 2, - livekit::TrackSource::SOURCE_MICROPHONE), - std::runtime_error) - << "createAudioTrack should throw when not connected"; -} - -TEST_F(LiveKitBridgeTest, CreateVideoTrackBeforeConnectThrows) { - LiveKitBridge bridge; - - EXPECT_THROW(bridge.createVideoTrack("cam", 1280, 720, - livekit::TrackSource::SOURCE_CAMERA), - std::runtime_error) - << "createVideoTrack should throw when not connected"; -} - -// ============================================================================ -// Callback registration (pre-connection — warns but does not crash) -// ============================================================================ - -TEST_F(LiveKitBridgeTest, SetAndClearAudioCallbackBeforeConnectDoesNotCrash) { - LiveKitBridge bridge; - - EXPECT_NO_THROW({ - bridge.setOnAudioFrameCallback("remote-participant", - livekit::TrackSource::SOURCE_MICROPHONE, - [](const livekit::AudioFrame &) {}); - - bridge.clearOnAudioFrameCallback("remote-participant", - livekit::TrackSource::SOURCE_MICROPHONE); - }) << "set/clear audio callback before connect should be safe (warns)"; -} - -TEST_F(LiveKitBridgeTest, SetAndClearVideoCallbackBeforeConnectDoesNotCrash) { - LiveKitBridge bridge; - - EXPECT_NO_THROW({ - bridge.setOnVideoFrameCallback( - "remote-participant", livekit::TrackSource::SOURCE_CAMERA, - [](const livekit::VideoFrame &, std::int64_t) {}); - - bridge.clearOnVideoFrameCallback("remote-participant", - livekit::TrackSource::SOURCE_CAMERA); - }) << "set/clear video callback before connect should be safe (warns)"; -} - -TEST_F(LiveKitBridgeTest, ClearNonExistentCallbackIsNoOp) { - LiveKitBridge bridge; - - EXPECT_NO_THROW({ - bridge.clearOnAudioFrameCallback("nonexistent", - livekit::TrackSource::SOURCE_MICROPHONE); - bridge.clearOnVideoFrameCallback("nonexistent", - livekit::TrackSource::SOURCE_CAMERA); - }) << "Clearing a callback that was never registered should be a no-op"; -} - -} // namespace test -} // namespace livekit_bridge diff --git a/bridge/tests/test_rpc_controller.cpp b/bridge/tests/test_rpc_controller.cpp deleted file mode 100644 index be2d0355..00000000 --- a/bridge/tests/test_rpc_controller.cpp +++ /dev/null @@ -1,273 +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. - */ - -/// @file test_rpc_controller.cpp -/// @brief Unit tests for RpcController. - -#include - -#include "livekit_bridge/rpc_constants.h" -#include "rpc_controller.h" - -#include "livekit/local_participant.h" -#include "livekit/rpc_error.h" - -#include -#include - -namespace livekit_bridge { -namespace test { - -// Records (action, track_name) pairs passed to the TrackActionFn callback. -struct TrackActionRecord { - std::string action; - std::string track_name; -}; - -class RpcControllerTest : public ::testing::Test { -protected: - std::vector recorded_actions_; - - std::unique_ptr makeController() { - namespace tc = rpc::track_control; - return std::make_unique( - [this](const tc::Action &action, const std::string &track_name) { - const char *action_str = - (action == tc::Action::kActionMute) ? tc::kActionMute - : tc::kActionUnmute; - recorded_actions_.push_back({action_str, track_name}); - }); - } - - std::unique_ptr makeThrowingController() { - return std::make_unique( - [](const rpc::track_control::Action &, const std::string &track_name) { - throw livekit::RpcError( - livekit::RpcError::ErrorCode::APPLICATION_ERROR, - "track not found: " + track_name); - }); - } - - // Helper: call the private handleTrackControlRpc with a given payload. - std::optional - callHandler(RpcController &controller, const std::string &payload, - const std::string &caller = "test-caller") { - livekit::RpcInvocationData data; - data.request_id = "test-request-id"; - data.caller_identity = caller; - data.payload = payload; - data.response_timeout_sec = 10.0; - return controller.handleTrackControlRpc(data); - } -}; - -// ============================================================================ -// Construction & lifecycle -// ============================================================================ - -TEST_F(RpcControllerTest, InitiallyDisabled) { - auto controller = makeController(); - EXPECT_FALSE(controller->isEnabled()); -} - -TEST_F(RpcControllerTest, DisableOnAlreadyDisabledIsNoOp) { - auto controller = makeController(); - EXPECT_NO_THROW(controller->disable()); - EXPECT_FALSE(controller->isEnabled()); -} - -TEST_F(RpcControllerTest, DisableMultipleTimesIsIdempotent) { - auto controller = makeController(); - EXPECT_NO_THROW({ - controller->disable(); - controller->disable(); - controller->disable(); - }); -} - -TEST_F(RpcControllerTest, DestructorWithoutEnableIsSafe) { - EXPECT_NO_THROW({ auto controller = makeController(); }); -} - -// ============================================================================ -// handleTrackControlRpc — payload parsing -// ============================================================================ - -TEST_F(RpcControllerTest, ValidMutePayload) { - auto controller = makeController(); - auto result = callHandler(*controller, "mute:my-track"); - - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(result.value(), rpc::track_control::kResponseOk); - - ASSERT_EQ(recorded_actions_.size(), 1u); - EXPECT_EQ(recorded_actions_[0].action, "mute"); - EXPECT_EQ(recorded_actions_[0].track_name, "my-track"); -} - -TEST_F(RpcControllerTest, ValidUnmutePayload) { - auto controller = makeController(); - auto result = callHandler(*controller, "unmute:cam"); - - ASSERT_TRUE(result.has_value()); - EXPECT_EQ(result.value(), rpc::track_control::kResponseOk); - - ASSERT_EQ(recorded_actions_.size(), 1u); - EXPECT_EQ(recorded_actions_[0].action, "unmute"); - EXPECT_EQ(recorded_actions_[0].track_name, "cam"); -} - -TEST_F(RpcControllerTest, TrackNameWithColons) { - auto controller = makeController(); - auto result = callHandler(*controller, "mute:track:with:colons"); - - ASSERT_TRUE(result.has_value()); - ASSERT_EQ(recorded_actions_.size(), 1u); - EXPECT_EQ(recorded_actions_[0].action, "mute"); - EXPECT_EQ(recorded_actions_[0].track_name, "track:with:colons"); -} - -TEST_F(RpcControllerTest, TrackNameWithSpaces) { - auto controller = makeController(); - auto result = callHandler(*controller, "unmute:my track name"); - - ASSERT_TRUE(result.has_value()); - ASSERT_EQ(recorded_actions_.size(), 1u); - EXPECT_EQ(recorded_actions_[0].action, "unmute"); - EXPECT_EQ(recorded_actions_[0].track_name, "my track name"); -} - -// ============================================================================ -// handleTrackControlRpc — invalid payloads -// ============================================================================ - -TEST_F(RpcControllerTest, EmptyPayloadThrows) { - auto controller = makeController(); - EXPECT_THROW(callHandler(*controller, ""), livekit::RpcError); - EXPECT_TRUE(recorded_actions_.empty()); -} - -TEST_F(RpcControllerTest, NoDelimiterThrows) { - auto controller = makeController(); - EXPECT_THROW(callHandler(*controller, "mutetrack"), livekit::RpcError); - EXPECT_TRUE(recorded_actions_.empty()); -} - -TEST_F(RpcControllerTest, LeadingDelimiterThrows) { - auto controller = makeController(); - EXPECT_THROW(callHandler(*controller, ":track"), livekit::RpcError); - EXPECT_TRUE(recorded_actions_.empty()); -} - -TEST_F(RpcControllerTest, UnknownActionThrows) { - auto controller = makeController(); - EXPECT_THROW(callHandler(*controller, "pause:cam"), livekit::RpcError); - EXPECT_TRUE(recorded_actions_.empty()); -} - -TEST_F(RpcControllerTest, CaseSensitiveAction) { - auto controller = makeController(); - EXPECT_THROW(callHandler(*controller, "MUTE:cam"), livekit::RpcError); - EXPECT_THROW(callHandler(*controller, "Mute:cam"), livekit::RpcError); - EXPECT_TRUE(recorded_actions_.empty()); -} - -// ============================================================================ -// handleTrackControlRpc — TrackActionFn propagation -// ============================================================================ - -TEST_F(RpcControllerTest, TrackActionFnExceptionPropagates) { - auto controller = makeThrowingController(); - - try { - callHandler(*controller, "mute:nonexistent"); - FAIL() << "Expected RpcError to propagate from TrackActionFn"; - } catch (const livekit::RpcError &e) { - EXPECT_EQ(e.code(), static_cast( - livekit::RpcError::ErrorCode::APPLICATION_ERROR)); - EXPECT_NE(std::string(e.message()).find("nonexistent"), std::string::npos) - << "Error message should contain the track name"; - } -} - -TEST_F(RpcControllerTest, MultipleCallsAccumulate) { - auto controller = makeController(); - - callHandler(*controller, "mute:audio"); - callHandler(*controller, "unmute:audio"); - callHandler(*controller, "mute:video"); - - ASSERT_EQ(recorded_actions_.size(), 3u); - EXPECT_EQ(recorded_actions_[0].action, "mute"); - EXPECT_EQ(recorded_actions_[0].track_name, "audio"); - EXPECT_EQ(recorded_actions_[1].action, "unmute"); - EXPECT_EQ(recorded_actions_[1].track_name, "audio"); - EXPECT_EQ(recorded_actions_[2].action, "mute"); - EXPECT_EQ(recorded_actions_[2].track_name, "video"); -} - -// ============================================================================ -// handleTrackControlRpc — caller identity forwarded -// ============================================================================ - -TEST_F(RpcControllerTest, CallerIdentityPassedThrough) { - auto controller = makeController(); - auto result = callHandler(*controller, "mute:mic", "remote-robot"); - - ASSERT_TRUE(result.has_value()); - ASSERT_EQ(recorded_actions_.size(), 1u); - EXPECT_EQ(recorded_actions_[0].action, "mute"); - EXPECT_EQ(recorded_actions_[0].track_name, "mic"); -} - -// ============================================================================ -// rpc_constants — formatPayload -// ============================================================================ - -TEST_F(RpcControllerTest, FormatPayloadMute) { - namespace tc = rpc::track_control; - std::string payload = tc::formatPayload(tc::kActionMute, "cam"); - EXPECT_EQ(payload, "mute:cam"); -} - -TEST_F(RpcControllerTest, FormatPayloadUnmute) { - namespace tc = rpc::track_control; - std::string payload = tc::formatPayload(tc::kActionUnmute, "mic"); - EXPECT_EQ(payload, "unmute:mic"); -} - -TEST_F(RpcControllerTest, FormatPayloadEmptyTrackName) { - namespace tc = rpc::track_control; - std::string payload = tc::formatPayload(tc::kActionMute, ""); - EXPECT_EQ(payload, "mute:"); -} - -TEST_F(RpcControllerTest, FormatPayloadRoundTrip) { - namespace tc = rpc::track_control; - std::string track_name = "some-track-123"; - std::string payload = tc::formatPayload(tc::kActionMute, track_name); - - auto controller = makeController(); - auto result = callHandler(*controller, payload); - - ASSERT_TRUE(result.has_value()); - ASSERT_EQ(recorded_actions_.size(), 1u); - EXPECT_EQ(recorded_actions_[0].action, tc::kActionMute); - EXPECT_EQ(recorded_actions_[0].track_name, track_name); -} - -} // namespace test -} // namespace livekit_bridge diff --git a/cmake/protobuf.cmake b/cmake/protobuf.cmake index 8132a084..3e0fa33c 100644 --- a/cmake/protobuf.cmake +++ b/cmake/protobuf.cmake @@ -10,6 +10,7 @@ # - Target protobuf::protoc (on vendored path; on Windows we may only have an executable) include(FetchContent) +include(warnings) option(LIVEKIT_USE_SYSTEM_PROTOBUF "Use system-installed Protobuf instead of vendoring" OFF) @@ -63,8 +64,13 @@ if(WIN32 AND NOT LIVEKIT_USE_SYSTEM_PROTOBUF) # Include dirs: prefer the imported target usage requirements. if(TARGET protobuf::libprotobuf) get_target_property(_pb_includes protobuf::libprotobuf INTERFACE_INCLUDE_DIRECTORIES) + livekit_treat_as_external(protobuf::libprotobuf) elseif(TARGET protobuf::protobuf) # some protobuf builds use protobuf::protobuf get_target_property(_pb_includes protobuf::protobuf INTERFACE_INCLUDE_DIRECTORIES) + livekit_treat_as_external(protobuf::protobuf) + endif() + if(TARGET protobuf::protoc) + livekit_treat_as_external(protobuf::protoc) endif() if(NOT _pb_includes) # Best-effort fallback: Protobuf_INCLUDE_DIRS is commonly set by ProtobufConfig @@ -89,6 +95,14 @@ if(LIVEKIT_USE_SYSTEM_PROTOBUF) if(NOT Protobuf_PROTOC_EXECUTABLE) find_program(Protobuf_PROTOC_EXECUTABLE NAMES protoc REQUIRED) endif() + if(TARGET protobuf::libprotobuf) + livekit_treat_as_external(protobuf::libprotobuf) + elseif(TARGET protobuf::protobuf) + livekit_treat_as_external(protobuf::protobuf) + endif() + if(TARGET protobuf::protoc) + livekit_treat_as_external(protobuf::protoc) + endif() message(STATUS "Using system protoc: ${Protobuf_PROTOC_EXECUTABLE}") return() endif() @@ -117,6 +131,7 @@ set(BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE) # Disable installs/exports in subprojects (avoids export-set errors) set(protobuf_INSTALL OFF CACHE BOOL "" FORCE) set(ABSL_ENABLE_INSTALL OFF CACHE BOOL "" FORCE) +set(ABSL_PROPAGATE_CXX_STD ON CACHE BOOL "" FORCE) set(utf8_range_ENABLE_INSTALL OFF CACHE BOOL "" FORCE) # Force hidden visibility on every target created by the FetchContent @@ -145,7 +160,11 @@ if(MSVC) endif() # Make abseil available first so protobuf can find absl:: targets. -FetchContent_MakeAvailable(livekit_abseil) +livekit_fetchcontent_makeavailable(livekit_abseil) +livekit_collect_targets_in_directory(_livekit_abseil_targets "${livekit_abseil_BINARY_DIR}") +foreach(_livekit_abseil_target IN LISTS _livekit_abseil_targets) + livekit_disable_warnings(${_livekit_abseil_target}) +endforeach() # Workaround for some abseil flags on Apple Silicon. if(APPLE AND (CMAKE_SYSTEM_PROCESSOR MATCHES "arm64|aarch64")) @@ -172,7 +191,11 @@ if(NOT TARGET absl::base) endif() # Now make protobuf available. -FetchContent_MakeAvailable(livekit_protobuf) +livekit_fetchcontent_makeavailable(livekit_protobuf) +livekit_collect_targets_in_directory(_livekit_protobuf_targets "${livekit_protobuf_BINARY_DIR}") +foreach(_livekit_protobuf_target IN LISTS _livekit_protobuf_targets) + livekit_disable_warnings(${_livekit_protobuf_target}) +endforeach() # Protobuf targets: modern protobuf exports protobuf::protoc etc. if(TARGET protobuf::protoc) @@ -194,7 +217,7 @@ endif() # Include dirs: prefer target usage; keep this var for your existing CMakeLists. if(TARGET protobuf::libprotobuf) - get_target_property(_pb_includes protobuf::libprotobuf INTERFACE_INCLUDE_DIRECTORIES) + livekit_get_interface_includes(protobuf::libprotobuf _pb_includes) endif() if(NOT _pb_includes) set(_pb_includes "${livekit_protobuf_SOURCE_DIR}/src") diff --git a/cmake/spdlog.cmake b/cmake/spdlog.cmake index 0ef0e191..6a22b771 100644 --- a/cmake/spdlog.cmake +++ b/cmake/spdlog.cmake @@ -47,6 +47,7 @@ endif() message(STATUS "LiveKit compile-time log level: ${_LK_LOG_LEVEL_UPPER} (SPDLOG_ACTIVE_LEVEL=${_SPDLOG_ACTIVE_LEVEL})") include(FetchContent) +include(warnings) set(LIVEKIT_SPDLOG_VERSION "1.15.1" CACHE STRING "Vendored spdlog version") @@ -61,6 +62,9 @@ endif() # --------------------------------------------------------------------------- if(WIN32 AND LIVEKIT_USE_VCPKG) find_package(spdlog CONFIG REQUIRED) + if(TARGET spdlog::spdlog) + livekit_treat_as_external(spdlog::spdlog) + endif() message(STATUS "Windows: using vcpkg spdlog") return() endif() @@ -79,7 +83,11 @@ set(SPDLOG_BUILD_EXAMPLE OFF CACHE BOOL "" FORCE) set(SPDLOG_BUILD_TESTS OFF CACHE BOOL "" FORCE) set(SPDLOG_INSTALL OFF CACHE BOOL "" FORCE) -FetchContent_MakeAvailable(livekit_spdlog) +livekit_fetchcontent_makeavailable(livekit_spdlog) +livekit_collect_targets_in_directory(_livekit_spdlog_targets "${livekit_spdlog_BINARY_DIR}") +foreach(_livekit_spdlog_target IN LISTS _livekit_spdlog_targets) + livekit_disable_warnings(${_livekit_spdlog_target}) +endforeach() # spdlog is linked PRIVATE into liblivekit and must not leak its symbols into # the SDK's exported ABI. Force hidden visibility on the spdlog target so its diff --git a/cmake/warnings.cmake b/cmake/warnings.cmake new file mode 100644 index 00000000..aa450333 --- /dev/null +++ b/cmake/warnings.cmake @@ -0,0 +1,109 @@ +# 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. + +# Resolves target aliases, marks their exported include directories as system includes, +# and disables compiler warnings on compilable third-party targets. +function(_livekit_resolve_target target out_var) + if(NOT TARGET ${target}) + set(${out_var} "" PARENT_SCOPE) + return() + endif() + + get_target_property(_aliased_target ${target} ALIASED_TARGET) + if(_aliased_target) + set(${out_var} "${_aliased_target}" PARENT_SCOPE) + else() + set(${out_var} "${target}" PARENT_SCOPE) + endif() +endfunction() + +function(livekit_disable_warnings target) + _livekit_resolve_target(${target} _resolved_target) + if(NOT _resolved_target) + return() + endif() + + get_target_property(_target_type ${_resolved_target} TYPE) + get_target_property(_is_imported ${_resolved_target} IMPORTED) + if(_is_imported OR _target_type STREQUAL "INTERFACE_LIBRARY") + return() + endif() + if(NOT _target_type MATCHES "^(STATIC_LIBRARY|SHARED_LIBRARY|MODULE_LIBRARY|OBJECT_LIBRARY|EXECUTABLE)$") + return() + endif() + + target_compile_options(${_resolved_target} PRIVATE + $<$:/W0> + $<$:/W0> + $<$:-w> + $<$:-w> + ) +endfunction() + +function(livekit_mark_system_includes target) + _livekit_resolve_target(${target} _resolved_target) + if(NOT _resolved_target) + return() + endif() + + get_target_property(_interface_includes ${_resolved_target} INTERFACE_INCLUDE_DIRECTORIES) + if(_interface_includes) + set_property(TARGET ${_resolved_target} APPEND PROPERTY + INTERFACE_SYSTEM_INCLUDE_DIRECTORIES ${_interface_includes} + ) + endif() +endfunction() + +function(livekit_get_interface_includes target out_var) + _livekit_resolve_target(${target} _resolved_target) + if(NOT _resolved_target) + set(${out_var} "" PARENT_SCOPE) + return() + endif() + + get_target_property(_interface_includes ${_resolved_target} INTERFACE_INCLUDE_DIRECTORIES) + if(_interface_includes) + set(${out_var} ${_interface_includes} PARENT_SCOPE) + else() + set(${out_var} "" PARENT_SCOPE) + endif() +endfunction() + +function(livekit_fetchcontent_makeavailable) + set(CMAKE_WARN_DEPRECATED OFF) + set(CMAKE_POLICY_VERSION_MINIMUM 3.10) + FetchContent_MakeAvailable(${ARGV}) +endfunction() + +function(livekit_collect_targets_in_directory out_var directory) + get_property(_targets DIRECTORY "${directory}" PROPERTY BUILDSYSTEM_TARGETS) + get_property(_subdirectories DIRECTORY "${directory}" PROPERTY SUBDIRECTORIES) + + set(_all_targets ${_targets}) + foreach(_subdirectory IN LISTS _subdirectories) + livekit_collect_targets_in_directory(_subdirectory_targets "${_subdirectory}") + list(APPEND _all_targets ${_subdirectory_targets}) + endforeach() + + set(${out_var} ${_all_targets} PARENT_SCOPE) +endfunction() + +function(livekit_treat_as_external target) + if(NOT TARGET ${target}) + return() + endif() + + livekit_mark_system_includes(${target}) + livekit_disable_warnings(${target}) +endfunction() diff --git a/cpp-example-collection b/cpp-example-collection index 56815733..008808dc 160000 --- a/cpp-example-collection +++ b/cpp-example-collection @@ -1 +1 @@ -Subproject commit 56815733a71c14692569e8adf2916a56a14d4882 +Subproject commit 008808dcda21bcb188295425908219e64eae395d diff --git a/docker/Dockerfile.sdk b/docker/Dockerfile.sdk index 8d2ad155..6d31a878 100644 --- a/docker/Dockerfile.sdk +++ b/docker/Dockerfile.sdk @@ -28,7 +28,6 @@ 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 diff --git a/docs/doxygen/index.md b/docs/doxygen/index.md deleted file mode 100644 index 56df9d37..00000000 --- a/docs/doxygen/index.md +++ /dev/null @@ -1,135 +0,0 @@ -# Overview - -Build real-time audio/video applications in C++ with LiveKit. - -## Quick Start - -```cpp -#include "livekit/livekit.h" - -bool initializeLivekit(const std::string& url, const std::string& token) { - // Init LiveKit - livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); - - room_ = std::make_unique(); - livekit::RoomOptions options; - options.auto_subscribe = true; - options.dynacast = false; - if (!room_->connect(url, token, options)) { - std::cerr << "Failed to connect\n"; - livekit::shutdown(); - return false; - } - - std::cout << "Connected.\n"; - - // ---- Create & publish AUDIO ---- - // Note: Hook up your own audio capture flow to |audioSource_| - audioSource_ = std::make_shared(48000, 1, 10); - auto audioTrack = livekit::LocalAudioTrack::createLocalAudioTrack("noise", audioSource_); - - livekit::TrackPublishOptions audioOpts; - audioOpts.source = livekit::TrackSource::SOURCE_MICROPHONE; - - try { - audioPub_ = room_->localParticipant()->publishTrack(audioTrack, audioOpts); - std::cout << "Published audio: sid=" << audioPub_->sid() << "\n"; - } catch (const std::exception& e) { - std::cerr << "Failed to publish audio: " << e.what() << "\n"; - return false; - } - - // ---- Create & publish VIDEO ---- - // Note: Hook up your own video capture flow to |videoSource_| - videoSource_ = std::make_shared(1280, 720); - auto videoTrack = livekit::LocalVideoTrack::createLocalVideoTrack("rgb", videoSource_); - - livekit::TrackPublishOptions videoOpts; - videoOpts.source = livekit::TrackSource::SOURCE_CAMERA; - - try { - videoPub_ = room_->localParticipant()->publishTrack(videoTrack, videoOpts); - std::cout << "Published video: sid=" << videoPub_->sid() << "\n"; - } catch (const std::exception& e) { - std::cerr << "Failed to publish video: " << e.what() << "\n"; - return false; - } - return true; -} - -void shutdownLivekit() { - // Best-effort unpublish - try { - if (room_ && audioPub_) - room_->localParticipant()->unpublishTrack(audioPub_->sid()); - if (room_ && videoPub_) - room_->localParticipant()->unpublishTrack(videoPub_->sid()); - } catch (...) { - } - - audioPub_.reset(); - videoPub_.reset(); - audioSource_.reset(); - videoSource_.reset(); - room_.reset(); - - livekit::shutdown(); -} -``` - -## Key Classes - -| Class | Description | -|-------|-------------| -| @ref livekit::Room | Main entry point - connect to a LiveKit room | -| @ref livekit::RoomOptions | Configuration for room connection (auto_subscribe, dynacast, etc.) | -| @ref livekit::LocalParticipant | The local user - publish tracks and send data | -| @ref livekit::RemoteParticipant | Other participants in the room | -| @ref livekit::AudioSource | Audio input source for publishing (sample rate, channels) | -| @ref livekit::VideoSource | Video input source for publishing (width, height) | -| @ref livekit::LocalAudioTrack | Local audio track created from AudioSource | -| @ref livekit::LocalVideoTrack | Local video track created from VideoSource | -| @ref livekit::LocalTrackPublication | Handle to a published local track | -| @ref livekit::TrackPublishOptions | Options for publishing (source type, codec, etc.) | -| @ref livekit::AudioStream | Receive audio from remote participants | -| @ref livekit::VideoStream | Receive video from remote participants | -| @ref livekit::RoomDelegate | Callbacks for room events | - -## Installation - -See the [GitHub README](https://github.com/livekit/client-sdk-cpp#readme) for build instructions. - -**Requirements:** - -- CMake ≥ 3.20 -- Rust/Cargo (latest stable) -- Platform: Windows, macOS, or Linux - -## Examples - -**Getting started** - -- [SimpleRoom](https://github.com/livekit-examples/cpp-example-collection/tree/main/simple_room) - Minimal room connection that publishes audio and video. -- [BasicRoom](https://github.com/livekit-examples/cpp-example-collection/tree/main/basic_room) - Publishes synthetic noise audio plus an RGB test pattern with capture loops; runs until Ctrl-C. -- [HelloLiveKit](https://github.com/livekit-examples/cpp-example-collection/tree/main/hello_livekit) - Two-process sender/receiver demo of publishing video and a data track from one app and subscribing in another. - -**Logging** - -- [LoggingLevels](https://github.com/livekit-examples/cpp-example-collection/tree/main/logging_levels) - Demonstrates @ref livekit::setLogLevel() and @ref livekit::setLogCallback(), including custom sinks for redirecting SDK logs. - -**RPC and data** - -- [SimpleRpc](https://github.com/livekit-examples/cpp-example-collection/tree/main/simple_rpc) - Remote procedure calls between participants. -- [SimpleDataStream](https://github.com/livekit-examples/cpp-example-collection/tree/main/simple_data_stream) - Send and receive text and binary data streams. -- [PingPong](https://github.com/livekit-examples/cpp-example-collection/tree/main/ping_pong) - Two-process round-trip over data tracks that prints RTT and one-way latency metrics. -- [SimpleJoystick](https://github.com/livekit-examples/cpp-example-collection/tree/main/simple_joystick) - Interactive sender/receiver: keyboard-driven joystick commands delivered via RPC, with auto-reconnect. - -**Advanced video** - -- [UserTimestampedVideo](https://github.com/livekit-examples/cpp-example-collection/tree/main/user_timestamped_video) - Producer/consumer pair showing per-frame `VideoFrameMetadata::user_timestamp_us` and the rich `setOnVideoFrameEventCallback` vs. legacy `setOnVideoFrameCallback` paths. - -## Resources - -- [GitHub Repository](https://github.com/livekit/client-sdk-cpp) -- [LiveKit Documentation](https://docs.livekit.io/) -- [Community Slack](https://livekit.io/join-slack) diff --git a/include/livekit/audio_frame.h b/include/livekit/audio_frame.h index 80678903..cac5765f 100644 --- a/include/livekit/audio_frame.h +++ b/include/livekit/audio_frame.h @@ -78,34 +78,6 @@ class LIVEKIT_API AudioFrame { /// A human-readable description. std::string toString() const; - /// @deprecated Use totalSamples() instead. - [[deprecated("AudioFrame::total_samples is deprecated; use AudioFrame::totalSamples instead")]] - std::size_t total_samples() const noexcept { // NOLINT(readability-identifier-naming) - return totalSamples(); - } - - /// @deprecated Use sampleRate() instead. - [[deprecated("AudioFrame::sample_rate is deprecated; use AudioFrame::sampleRate instead")]] - int sample_rate() const noexcept { // NOLINT(readability-identifier-naming) - return sampleRate(); - } - - /// @deprecated Use numChannels() instead. - [[deprecated("AudioFrame::num_channels is deprecated; use AudioFrame::numChannels instead")]] - int num_channels() const noexcept { // NOLINT(readability-identifier-naming) - return numChannels(); - } - - /// @deprecated Use samplesPerChannel() instead. - [[deprecated("AudioFrame::samples_per_channel is deprecated; use AudioFrame::samplesPerChannel instead")]] - int samples_per_channel() const noexcept { // NOLINT(readability-identifier-naming) - return samplesPerChannel(); - } - - /// @deprecated Use toString() instead. - [[deprecated("AudioFrame::to_string is deprecated; use AudioFrame::toString instead")]] - std::string to_string() const; // NOLINT(readability-identifier-naming) - protected: // Build a proto AudioFrameBufferInfo pointing at this frame’s data. // Used internally by AudioSource. diff --git a/include/livekit/audio_source.h b/include/livekit/audio_source.h index 5c149dd0..249907c5 100644 --- a/include/livekit/audio_source.h +++ b/include/livekit/audio_source.h @@ -82,24 +82,6 @@ class LIVEKIT_API AudioSource { /// Underlying FFI handle ID used in FFI requests. std::uint64_t ffiHandleId() const noexcept { return static_cast(handle_.get()); } - /// @deprecated Use sampleRate() instead. - [[deprecated("AudioSource::sample_rate is deprecated; use AudioSource::sampleRate instead")]] - int sample_rate() const noexcept { // NOLINT(readability-identifier-naming) - return sampleRate(); - } - - /// @deprecated Use numChannels() instead. - [[deprecated("AudioSource::num_channels is deprecated; use AudioSource::numChannels instead")]] - int num_channels() const noexcept { // NOLINT(readability-identifier-naming) - return numChannels(); - } - - /// @deprecated Use ffiHandleId() instead. - [[deprecated("AudioSource::ffi_handle_id is deprecated; use AudioSource::ffiHandleId instead")]] - std::uint64_t ffi_handle_id() const noexcept { // NOLINT(readability-identifier-naming) - return ffiHandleId(); - } - /// Current duration of queued audio (in seconds). double queuedDuration() const noexcept; diff --git a/include/livekit/e2ee.h b/include/livekit/e2ee.h index 4bc81a26..1ba74534 100644 --- a/include/livekit/e2ee.h +++ b/include/livekit/e2ee.h @@ -203,10 +203,17 @@ class LIVEKIT_API E2EEManager { /// will result in undecodable media (black video / silent audio). void setEnabled(bool enabled); - /// Returns the key provider if E2EE was configured for the room; otherwise - /// nullptr. - KeyProvider* keyProvider(); - const KeyProvider* keyProvider() const; + /// Returns a weak reference to the key provider if E2EE was configured for + /// the room; otherwise an expired weak_ptr. + /// + /// The KeyProvider is owned by this E2EEManager (which is in turn owned by + /// Room). Callers must lock() the returned weak_ptr before use and must not + /// retain the resulting shared_ptr beyond the lifetime of the Room. + /// + /// @return A weak_ptr to the KeyProvider, or an expired weak_ptr if E2EE was + /// not configured. + std::weak_ptr keyProvider(); + std::weak_ptr keyProvider() const; /// Retrieves the current list of frame cryptors from the underlying runtime. std::vector frameCryptors() const; @@ -220,7 +227,9 @@ class LIVEKIT_API E2EEManager { std::uint64_t room_handle_{0}; bool enabled_{false}; E2EEOptions options_; - KeyProvider key_provider_; + /// The key provider is owned by the E2EEManager and is not shared with other objects. + /// It is a shared_ptr just to utilize the weak_ptr interface for the keyProvider() accessor. + std::shared_ptr key_provider_; }; } // namespace livekit diff --git a/include/livekit/livekit.h b/include/livekit/livekit.h index d5ea02f5..e02ce67e 100644 --- a/include/livekit/livekit.h +++ b/include/livekit/livekit.h @@ -44,14 +44,6 @@ /// @brief Public API for the LiveKit C++ Client SDK. namespace livekit { -/// The log sink to use for SDK messages. -enum class LogSink { - /// Log messages to the console. - kConsole = 0, - /// Log messages to a callback function. - kCallback = 1, -}; - /// Initialize the LiveKit SDK. /// /// This **must be the first LiveKit API called** in the process. @@ -59,10 +51,9 @@ enum class LogSink { /// /// @param level Minimum log level for SDK messages (default: Info). /// Use setLogLevel() to change at runtime. -/// @param log_sink The log sink to use for SDK messages (default: Console). /// @returns true if initialization happened on this call, false if it was /// already initialized. -LIVEKIT_API bool initialize(const LogLevel& level = LogLevel::Info, const LogSink& log_sink = LogSink::kConsole); +LIVEKIT_API bool initialize(const LogLevel& level = LogLevel::Info); /// Shut down the LiveKit SDK. /// diff --git a/include/livekit/local_audio_track.h b/include/livekit/local_audio_track.h index 827ba4e7..2797fcde 100644 --- a/include/livekit/local_audio_track.h +++ b/include/livekit/local_audio_track.h @@ -42,7 +42,9 @@ class AudioSource; /// /// auto source = AudioSource::create(...); /// auto track = LocalAudioTrack::createLocalAudioTrack("mic", source); -/// room->localParticipant()->publishTrack(track); +/// if (auto lp = room->localParticipant().lock()) { +/// lp->publishTrack(track); +/// } /// /// Muting a local audio track stops transmitting audio to the room, but /// the underlying source may continue capturing depending on platform @@ -77,12 +79,6 @@ class LIVEKIT_API LocalAudioTrack : public Track { /// including its SID and name. Useful for debugging and logging. std::string toString() const; - /// @deprecated Use toString() instead. - // NOLINTBEGIN(readability-identifier-naming) - [[deprecated("LocalAudioTrack::to_string is deprecated; use LocalAudioTrack::toString instead")]] - std::string to_string() const; - // NOLINTEND(readability-identifier-naming) - /// Returns the publication that owns this track, or nullptr if the track is /// not published. std::shared_ptr publication() const noexcept { return local_publication_; } diff --git a/include/livekit/local_data_track.h b/include/livekit/local_data_track.h index 80d57dac..18c2eb77 100644 --- a/include/livekit/local_data_track.h +++ b/include/livekit/local_data_track.h @@ -45,7 +45,8 @@ class OwnedLocalDataTrack; /// /// Typical usage: /// -/// auto lp = room->localParticipant(); +/// auto lp = room->localParticipant().lock(); +/// if (!lp) return; // room not connected or already torn down /// auto result = lp->publishDataTrack("sensor-data"); /// if (result) { /// auto dt = result.value(); diff --git a/include/livekit/local_video_track.h b/include/livekit/local_video_track.h index 7b8da0ef..c89893de 100644 --- a/include/livekit/local_video_track.h +++ b/include/livekit/local_video_track.h @@ -41,7 +41,9 @@ class VideoSource; /// /// auto source = std::make_shared(1280, 720); /// auto track = LocalVideoTrack::createLocalVideoTrack("cam", source); -/// room->localParticipant()->publishTrack(track); +/// if (auto lp = room->localParticipant().lock()) { +/// lp->publishTrack(track); +/// } /// // Capture frames on the video thread via `source`, not via the track. /// /// Muting a local video track stops transmitting video to the room, but @@ -77,12 +79,6 @@ class LIVEKIT_API LocalVideoTrack : public Track { /// including its SID and name. Useful for debugging and logging. std::string toString() const; - /// @deprecated Use toString() instead. - // NOLINTBEGIN(readability-identifier-naming) - [[deprecated("LocalVideoTrack::to_string is deprecated; use LocalVideoTrack::toString instead")]] - std::string to_string() const; - // NOLINTEND(readability-identifier-naming) - /// Returns the publication that owns this track, or nullptr if the track is /// not published. std::shared_ptr publication() const noexcept { return local_publication_; } diff --git a/include/livekit/participant.h b/include/livekit/participant.h index 2b5857db..eeecba52 100644 --- a/include/livekit/participant.h +++ b/include/livekit/participant.h @@ -56,58 +56,6 @@ class LIVEKIT_API Participant { uintptr_t ffiHandleId() const noexcept { return handle_.get(); } - // --------------------------------------------------------------------------- - // Deprecated public mutators - // --------------------------------------------------------------------------- - - // NOLINTBEGIN(readability-identifier-naming) - - /// @deprecated Use setName() instead. - [[deprecated("Participant::set_name is deprecated; use LocalParticipant::setName instead")]] - void set_name(std::string name) noexcept { - name_ = std::move(name); - } - - /// @deprecated Use setMetadata() instead. - [[deprecated("Participant::set_metadata is deprecated; use LocalParticipant::setMetadata instead")]] - void set_metadata(std::string metadata) noexcept { - metadata_ = std::move(metadata); - } - - /// @deprecated Use setAttributes() instead. - [[deprecated("Participant::set_attributes is deprecated; use LocalParticipant::setAttributes instead")]] - void set_attributes(std::unordered_map attrs) noexcept { - attributes_ = std::move(attrs); - } - - /// @deprecated Use setAttribute() instead. - [[deprecated("Participant::set_attribute is deprecated; use LocalParticipant::setAttributes instead")]] - void set_attribute(const std::string& key, const std::string& value) { - attributes_[key] = value; - } - - /// @deprecated Use removeAttribute() instead. - [[deprecated("Participant::remove_attribute is deprecated; use LocalParticipant::setAttributes instead")]] - void remove_attribute(const std::string& key) { - attributes_.erase(key); - } - - /// @deprecated Kind is server-determined and not user-settable; this mutator will be removed. - [[deprecated("Participant::set_kind is deprecated; Kind is server-determined and not user-settable")]] - void set_kind(ParticipantKind kind) noexcept { - kind_ = kind; - } - - /// @deprecated DisconnectReason is server-determined and not user-settable; this mutator will be removed. - [[deprecated( - "Participant::set_disconnect_reason is deprecated; DisconnectReason is server-determined and not " - "user-settable")]] - void set_disconnect_reason(DisconnectReason reason) noexcept { - reason_ = reason; - } - - // NOLINTEND(readability-identifier-naming) - protected: virtual std::shared_ptr findTrackPublication(const std::string& sid) const = 0; diff --git a/include/livekit/remote_audio_track.h b/include/livekit/remote_audio_track.h index 837e0262..cfbee358 100644 --- a/include/livekit/remote_audio_track.h +++ b/include/livekit/remote_audio_track.h @@ -50,12 +50,6 @@ class LIVEKIT_API RemoteAudioTrack : public Track { /// Returns a concise, human-readable string summarizing the track, /// including its SID and name. Useful for debugging and logging. std::string toString() const; - - /// @deprecated Use toString() instead. - // NOLINTBEGIN(readability-identifier-naming) - [[deprecated("RemoteAudioTrack::to_string is deprecated; use RemoteAudioTrack::toString instead")]] - std::string to_string() const; - // NOLINTEND(readability-identifier-naming) }; } // namespace livekit \ No newline at end of file diff --git a/include/livekit/remote_participant.h b/include/livekit/remote_participant.h index 1addd141..c050c0e3 100644 --- a/include/livekit/remote_participant.h +++ b/include/livekit/remote_participant.h @@ -44,12 +44,6 @@ class LIVEKIT_API RemoteParticipant : public Participant { std::string toString() const; - /// @deprecated Use toString() instead. - // NOLINTBEGIN(readability-identifier-naming) - [[deprecated("RemoteParticipant::to_string is deprecated; use RemoteParticipant::toString instead")]] - std::string to_string() const; - // NOLINTEND(readability-identifier-naming) - protected: /// Called by Room events like kTrackMuted. This is internal plumbing and not /// intended to be called directly by SDK users. diff --git a/include/livekit/remote_video_track.h b/include/livekit/remote_video_track.h index 0f2054ac..f3cf5dfc 100644 --- a/include/livekit/remote_video_track.h +++ b/include/livekit/remote_video_track.h @@ -50,12 +50,6 @@ class LIVEKIT_API RemoteVideoTrack : public Track { /// Returns a concise, human-readable string summarizing the track, /// including its SID and name. Useful for debugging and logging. std::string toString() const; - - /// @deprecated Use toString() instead. - // NOLINTBEGIN(readability-identifier-naming) - [[deprecated("RemoteVideoTrack::to_string is deprecated; use RemoteVideoTrack::toString instead")]] - std::string to_string() const; - // NOLINTEND(readability-identifier-naming) }; } // namespace livekit \ No newline at end of file diff --git a/include/livekit/result.h b/include/livekit/result.h index 6155fe92..0c3f5a69 100644 --- a/include/livekit/result.h +++ b/include/livekit/result.h @@ -58,14 +58,6 @@ class [[nodiscard]] Result { /// Allows `if (result)` style success checks. explicit operator bool() const noexcept { return ok(); } - /// @deprecated Use hasError() instead. - // NOLINTBEGIN(readability-identifier-naming) - [[deprecated("Result::has_error is deprecated; use Result::hasError instead")]] - bool has_error() const noexcept { - return hasError(); - } - // NOLINTEND(readability-identifier-naming) - /// Access the success value. /// /// @throws std::logic_error if `ok() == false`. diff --git a/include/livekit/room.h b/include/livekit/room.h index ac80fb4d..81664de7 100644 --- a/include/livekit/room.h +++ b/include/livekit/room.h @@ -16,7 +16,6 @@ #pragma once -#include #include #include #include @@ -110,13 +109,15 @@ class LIVEKIT_API Room { /// MyDelegate del; /// Room room; /// room.setDelegate(&del); - /// - /// @param delegate The RoomDelegate implementation to receive room lifecycle callbacks. void setDelegate(RoomDelegate* delegate); /// Connect to a LiveKit room using the given URL and token, applying the /// supplied connection options. /// + /// @param url WebSocket URL of the LiveKit server. + /// @param token Access token for authentication. + /// @param options Connection options controlling auto-subscribe, + /// dynacast, E2EE, and WebRTC configuration. /// Behavior: /// - Registers an FFI event listener *before* sending the connect request. /// - Sends a proto::FfiRequest::Connect with the URL, token, @@ -128,19 +129,8 @@ class LIVEKIT_API Room { /// RoomOptions defaults auto_subscribe = true. /// Without auto_subscribe enabled, remote tracks will NOT be subscribed /// automatically, and no remote audio/video will ever arrive. - /// - /// @param url WebSocket URL of the LiveKit server. - /// @param token Access token for authentication. - /// @param options Connection options controlling auto-subscribe, - /// dynacast, E2EE, and WebRTC configuration. bool connect(const std::string& url, const std::string& token, const RoomOptions& options); - /// @deprecated Use connect() instead. - // NOLINTBEGIN(readability-identifier-naming) - [[deprecated("Room::Connect is deprecated; use Room::connect instead")]] - bool Connect(const std::string& url, const std::string& token, const RoomOptions& options); - // NOLINTEND(readability-identifier-naming) - // Accessors /// Retrieve static metadata about the room. @@ -152,33 +142,40 @@ class LIVEKIT_API Room { /// - creation timestamp RoomInfoData roomInfo() const; - /// @deprecated Use roomInfo() instead. - // NOLINTBEGIN(readability-identifier-naming) - [[deprecated("Room::room_info is deprecated; use Room::roomInfo instead")]] - RoomInfoData room_info() const; - // NOLINTEND(readability-identifier-naming) - /// Get the local participant. /// /// This object represents the current user, including: /// - published tracks (audio/video/screen) /// - identity, SID, metadata /// - publishing/unpublishing operations - /// @return Non-null pointer after successful connect(). - LocalParticipant* localParticipant() const; + /// + /// The returned handle is non-owning. Call @c lock() to obtain a usable + /// @c weak_ptr; the result is empty (`lock() == nullptr`) before connect, + /// after room end-of-stream teardown, or once the room is destroyed. This + /// lets callers that cache the handle detect object lifetime instead of holding a + /// dangling pointer. + /// + /// @return Weak handle to the local participant. + std::weak_ptr localParticipant() const; /// Look up a remote participant by identity. /// - /// @param identity The participant’s identity string (not SID) - /// @return Pointer to RemoteParticipant if present, otherwise nullptr. - /// RemoteParticipant contains: + /// @param identity The participant’s identity string (not SID). + /// @return Weak handle to the RemoteParticipant if present, otherwise an + /// empty handle (`lock() == nullptr`). The handle also becomes empty once + /// the participant disconnects, the room is torn down, or the room is + /// destroyed. RemoteParticipant contains: /// - identity/name/metadata /// - track publications - /// - callbacks for track subscribed/unsubscribed, muted/unmuted - RemoteParticipant* remoteParticipant(const std::string& identity) const; + /// - callbacks for track subscribed/unsubscribed, muted/unmuted + std::weak_ptr remoteParticipant(const std::string& identity) const; /// Returns a snapshot of all current remote participants. - std::vector> remoteParticipants() const; + /// + /// @return Vector of weak handles to the current remote participants. Each + /// handle can be promoted with @c lock(); a handle becomes empty once the + /// corresponding participant disconnects or the room is torn down. + std::vector> remoteParticipants() const; /// Returns the current connection state of the room. ConnectionState connectionState() const; @@ -233,41 +230,33 @@ class LIVEKIT_API Room { /// - The ByteStreamReader remains valid as long as the shared_ptr is held, /// preventing lifetime-related crashes when reading asynchronously. /// - /// @param topic The topic to register the byte stream handler for. - /// @param handler The ByteStreamHandler to invoke when a byte stream is received. /// @throws std::runtime_error if a handler is already registered for the topic. void registerByteStreamHandler(const std::string& topic, ByteStreamHandler handler); /// Unregister the byte stream handler for the given topic. /// /// If no handler exists for the topic, this function is a no-op. - /// @param topic The topic to unregister the byte stream handler for. void unregisterByteStreamHandler(const std::string& topic); - /// Returns the room's E2EE manager, or nullptr if E2EE was not enabled at - /// connect time. + /// Returns the room's E2EE manager as a weak handle, or an empty handle if + /// E2EE was not enabled at connect time. /// /// Notes: /// - The manager is created after a successful connect(). - /// - If E2EE was not configured in RoomOptions, this will return nullptr. - E2EEManager* e2eeManager() const; + /// - If E2EE was not configured in RoomOptions, @c lock() returns nullptr. + /// - The handle also becomes empty once the room is torn down or destroyed. + /// + /// @return Weak handle to the E2EE manager. + std::weak_ptr e2eeManager() const; // --------------------------------------------------------------- // Frame callbacks // --------------------------------------------------------------- - /// @brief Sets the audio frame callback via SubscriptionThreadDispatcher. - void setOnAudioFrameCallback(const std::string& participant_identity, TrackSource source, AudioFrameCallback callback, - const AudioStream::Options& opts = {}); - /// @brief Sets the audio frame callback via SubscriptionThreadDispatcher. void setOnAudioFrameCallback(const std::string& participant_identity, const std::string& track_name, AudioFrameCallback callback, const AudioStream::Options& opts = {}); - /// @brief Sets the video frame callback via SubscriptionThreadDispatcher. - void setOnVideoFrameCallback(const std::string& participant_identity, TrackSource source, VideoFrameCallback callback, - const VideoStream::Options& opts = {}); - /// @brief Sets the video frame callback via SubscriptionThreadDispatcher. void setOnVideoFrameCallback(const std::string& participant_identity, const std::string& track_name, VideoFrameCallback callback, const VideoStream::Options& opts = {}); @@ -277,14 +266,9 @@ class LIVEKIT_API Room { void setOnVideoFrameEventCallback(const std::string& participant_identity, const std::string& track_name, VideoFrameEventCallback callback, const VideoStream::Options& opts = {}); - /// @brief Clears the audio frame callback via SubscriptionThreadDispatcher. - void clearOnAudioFrameCallback(const std::string& participant_identity, TrackSource source); /// @brief Clears the audio frame callback via SubscriptionThreadDispatcher. void clearOnAudioFrameCallback(const std::string& participant_identity, const std::string& track_name); - /// @brief Clears the video frame callback via SubscriptionThreadDispatcher. - void clearOnVideoFrameCallback(const std::string& participant_identity, TrackSource source); - /// @brief Clears the video frame callback via SubscriptionThreadDispatcher. void clearOnVideoFrameCallback(const std::string& participant_identity, const std::string& track_name); @@ -303,15 +287,18 @@ class LIVEKIT_API Room { RoomDelegate* delegate_ = nullptr; // Not owned RoomInfoData room_info_; std::shared_ptr room_handle_; - std::unique_ptr local_participant_; + /// The local participant is owned by the room and is not shared with other objects. + /// It is a shared_ptr just to utilize the weak_ptr interface for the localParticipant() accessor. + std::shared_ptr local_participant_; std::unordered_map> remote_participants_; // Data stream std::unordered_map text_stream_handlers_; std::unordered_map byte_stream_handlers_; std::unordered_map> text_stream_readers_; std::unordered_map> byte_stream_readers_; - // E2EE - std::unique_ptr e2ee_manager_; + // The E2EE manager is owned by the room and is not shared with other objects. + // It is a shared_ptr just to utilize the weak_ptr interface for the e2eeManager() accessor. + std::shared_ptr e2ee_manager_; std::shared_ptr subscription_thread_dispatcher_; // FfiClient listener ID (0 means no listener registered) diff --git a/include/livekit/subscription_thread_dispatcher.h b/include/livekit/subscription_thread_dispatcher.h index f62b78ce..73c86684 100644 --- a/include/livekit/subscription_thread_dispatcher.h +++ b/include/livekit/subscription_thread_dispatcher.h @@ -39,11 +39,11 @@ class Track; class VideoFrame; /// Callback type for incoming audio frames. -/// Invoked on a dedicated reader thread per (participant, source) pair. +/// Invoked on a dedicated reader thread per (participant, track_name) pair. using AudioFrameCallback = std::function; /// Callback type for incoming video frames. -/// Invoked on a dedicated reader thread per (participant, source) pair. +/// Invoked on a dedicated reader thread per (participant, track_name) pair. using VideoFrameCallback = std::function; /// Callback type for incoming video frame events. @@ -68,7 +68,7 @@ using DataFrameCallbackId = std::uint64_t; /// registration requests here, and then calls @ref handleTrackSubscribed and /// @ref handleTrackUnsubscribed as room events arrive. /// -/// For each registered `(participant identity, TrackSource)` pair, this class +/// For each registered `(participant identity, track name)` pair, this class /// may create a dedicated @ref AudioStream or @ref VideoStream and a matching /// reader thread. That thread blocks on stream reads and invokes the /// registered callback with decoded frames. @@ -89,20 +89,6 @@ class LIVEKIT_API SubscriptionThreadDispatcher { /// Stops all active readers and clears all registered callbacks. ~SubscriptionThreadDispatcher(); - /// Register or replace an audio frame callback for a remote subscription. - /// - /// The callback is keyed by remote participant identity plus @p source. - /// If the matching remote audio track is already subscribed, @ref Room may - /// immediately call @ref handleTrackSubscribed to start a reader. - /// - /// @param participant_identity Identity of the remote participant. - /// @param source Track source to match. - /// @param callback Function invoked for each decoded audio frame. - /// @param opts Options used when creating the backing - /// @ref AudioStream. - void setOnAudioFrameCallback(const std::string& participant_identity, TrackSource source, AudioFrameCallback callback, - const AudioStream::Options& opts = {}); - /// Register or replace an audio frame callback for a remote subscription. /// /// The callback is keyed by remote participant identity plus @p track_name. @@ -117,20 +103,6 @@ class LIVEKIT_API SubscriptionThreadDispatcher { void setOnAudioFrameCallback(const std::string& participant_identity, const std::string& track_name, AudioFrameCallback callback, const AudioStream::Options& opts = {}); - /// Register or replace a video frame callback for a remote subscription. - /// - /// The callback is keyed by remote participant identity plus @p source. - /// If the matching remote video track is already subscribed, @ref Room may - /// immediately call @ref handleTrackSubscribed to start a reader. - /// - /// @param participant_identity Identity of the remote participant. - /// @param source Track source to match. - /// @param callback Function invoked for each decoded video frame. - /// @param opts Options used when creating the backing - /// @ref VideoStream. - void setOnVideoFrameCallback(const std::string& participant_identity, TrackSource source, VideoFrameCallback callback, - const VideoStream::Options& opts = {}); - /// Register or replace a video frame callback for a remote subscription. /// /// The callback is keyed by remote participant identity plus @p track_name. @@ -161,15 +133,6 @@ class LIVEKIT_API SubscriptionThreadDispatcher { void setOnVideoFrameEventCallback(const std::string& participant_identity, const std::string& track_name, VideoFrameEventCallback callback, const VideoStream::Options& opts = {}); - /// Remove an audio callback registration and stop any active reader. - /// - /// If an audio reader thread is active for the given key, its stream is - /// closed and the thread is joined before this call returns. - /// - /// @param participant_identity Identity of the remote participant. - /// @param source Track source to clear. - void clearOnAudioFrameCallback(const std::string& participant_identity, TrackSource source); - /// Remove an audio callback registration and stop any active reader. /// /// If an audio reader thread is active for the given key, its stream is @@ -179,15 +142,6 @@ class LIVEKIT_API SubscriptionThreadDispatcher { /// @param track_name Track name to clear. void clearOnAudioFrameCallback(const std::string& participant_identity, const std::string& track_name); - /// Remove a video callback registration and stop any active reader. - /// - /// If a video reader thread is active for the given key, its stream is - /// closed and the thread is joined before this call returns. - /// - /// @param participant_identity Identity of the remote participant. - /// @param source Track source to clear. - void clearOnVideoFrameCallback(const std::string& participant_identity, TrackSource source); - /// Remove a video callback registration and stop any active reader. /// /// If a video reader thread is active for the given key, its stream is @@ -202,21 +156,20 @@ class LIVEKIT_API SubscriptionThreadDispatcher { /// @ref Room calls this after it has processed a track-subscription event and /// updated its publication state. If a matching callback registration exists, /// the dispatcher creates the appropriate stream type and launches a reader - /// thread for the `(participant, source)` key. + /// thread for the `(participant, track_name)` key. /// /// If no matching callback is registered, this is a no-op. /// /// @param participant_identity Identity of the remote participant. - /// @param source Track source associated with the subscription. /// @param track_name Track name associated with the subscription. /// @param track Subscribed remote track to read from. - void handleTrackSubscribed(const std::string& participant_identity, TrackSource source, const std::string& track_name, + void handleTrackSubscribed(const std::string& participant_identity, const std::string& track_name, const std::shared_ptr& track); /// Stop reader dispatch for an unsubscribed remote track. /// /// @ref Room calls this when a remote track is unsubscribed. Any active - /// reader stream for the given `(participant, source)` key is closed and its + /// reader stream for the given `(participant, track_name)` key is closed and its /// thread is joined. Callback registration is preserved so future /// re-subscription can start dispatch again automatically. /// @@ -282,16 +235,13 @@ class LIVEKIT_API SubscriptionThreadDispatcher { private: friend class SubscriptionThreadDispatcherTest; - /// Compound lookup key for callback dispatch: - /// either `(participant, source, "")` or `(participant, SOURCE_UNKNOWN, - /// track_name)`. + /// Compound lookup key for audio/video callback dispatch. struct CallbackKey { std::string participant_identity; - TrackSource source; std::string track_name; bool operator==(const CallbackKey& o) const { - return participant_identity == o.participant_identity && source == o.source && track_name == o.track_name; + return participant_identity == o.participant_identity && track_name == o.track_name; } }; @@ -299,9 +249,8 @@ class LIVEKIT_API SubscriptionThreadDispatcher { struct CallbackKeyHash { std::size_t operator()(const CallbackKey& k) const { auto h1 = std::hash{}(k.participant_identity); - auto h2 = std::hash{}(static_cast(k.source)); - auto h3 = std::hash{}(k.track_name); - return h1 ^ (h2 << 1) ^ (h3 << 2); + auto h2 = std::hash{}(k.track_name); + return h1 ^ (h2 << 1); } }; diff --git a/include/livekit/track.h b/include/livekit/track.h index 544e82d4..5fed6fe6 100644 --- a/include/livekit/track.h +++ b/include/livekit/track.h @@ -100,35 +100,6 @@ class LIVEKIT_API Track { bool hasHandle() const noexcept { return handle_.valid(); } uintptr_t ffiHandleId() const noexcept { return handle_.get(); } - // NOLINTBEGIN(readability-identifier-naming) - - /// @deprecated Use streamState() instead. - [[deprecated("Track::stream_state is deprecated; use Track::streamState instead")]] - StreamState stream_state() const noexcept { - return streamState(); - } - - /// @deprecated Use mimeType() instead. - // NOLINTNEXTLINE(bugprone-exception-escape) - [[deprecated("Track::mime_type is deprecated; use Track::mimeType instead")]] - std::optional mime_type() const noexcept { - return mimeType(); - } - - /// @deprecated Use hasHandle() instead. - [[deprecated("Track::has_handle is deprecated; use Track::hasHandle instead")]] - bool has_handle() const noexcept { - return hasHandle(); - } - - /// @deprecated Use ffiHandleId() instead. - [[deprecated("Track::ffi_handle_id is deprecated; use Track::ffiHandleId instead")]] - uintptr_t ffi_handle_id() const noexcept { - return ffiHandleId(); - } - - // NOLINTEND(readability-identifier-naming) - // Async get stats std::future> getStats() const; diff --git a/include/livekit/video_source.h b/include/livekit/video_source.h index 03af3fed..1763144c 100644 --- a/include/livekit/video_source.h +++ b/include/livekit/video_source.h @@ -79,12 +79,6 @@ class LIVEKIT_API VideoSource { /// Underlying FFI handle ID (0 if invalid). std::uint64_t ffiHandleId() const noexcept { return handle_.get(); } - /// @deprecated Use ffiHandleId() instead. - [[deprecated("VideoSource::ffi_handle_id is deprecated; use VideoSource::ffiHandleId instead")]] - std::uint64_t ffi_handle_id() const noexcept { // NOLINT(readability-identifier-naming) - return ffiHandleId(); - } - /// Push a VideoFrame into the FFI video source. /// /// @param frame Video frame to send. diff --git a/scripts/clang-format.sh b/scripts/clang-format.sh index 1b61b7c9..f2b58dae 100755 --- a/scripts/clang-format.sh +++ b/scripts/clang-format.sh @@ -179,10 +179,7 @@ clang-format --version # - benchmarks/ (in-tree micro-benchmarks) # Using `git ls-files` automatically skips the client-sdk-rust/ submodule, # build-*/, _deps/, local-install/, vcpkg_installed/, etc., without having -# to maintain a lookahead exclusion regex. The deprecated bridge/ tree and -# the empty examples/ tree are intentionally left out -- bridge/ has its -# own conventions (it's frozen pending removal), and examples/ has no -# tracked C/C++ files today. +# to maintain a lookahead exclusion regex. files=() if (( ${#explicit_files[@]} > 0 )); then files=("${explicit_files[@]}") diff --git a/scripts/clang-tidy.sh b/scripts/clang-tidy.sh index fdbc43b2..2d520419 100755 --- a/scripts/clang-tidy.sh +++ b/scripts/clang-tidy.sh @@ -113,7 +113,7 @@ BUILD_DIR="build-release" # Positive match for top-level src/*.{c,cpp,cc,cxx}; negative lookahead excludes # dep paths (_deps/, build-*/, -src/src/) and every other top-level dir. Python # regex (PCRE-ish) supports lookahead; this regex is evaluated by run-clang-tidy. -FILE_REGEX='^(?!.*/(_deps|build-[^/]*|bridge|examples|client-sdk-rust|cpp-example-collection|vcpkg_installed|docker|docs|data)/).*/src/(?!tests/).*\.(c|cpp|cc|cxx)$' +FILE_REGEX='^(?!.*/(_deps|build-[^/]*|client-sdk-rust|cpp-example-collection|vcpkg_installed|docker|docs|data)/).*/src/(?!tests/).*\.(c|cpp|cc|cxx)$' CI_MODE=0 # Automatically detect CI mode if in GitHub actions environment @@ -206,7 +206,9 @@ fi # SDK and we forward it to every clang-tidy invocation via --extra-arg. Linux # CI doesn't need this -- the system clang-tidy already finds libstdc++/libc++ # through its built-in resource dir. -extra_args=() +# Match the Clang build's variadic macro diagnostic suppression when clang-tidy +# is driven from GCC compile commands in Linux CI. +extra_args=(-extra-arg=-Wno-gnu-zero-variadic-macro-arguments) if [[ "$(uname)" == "Darwin" ]]; then sdk_path="$(xcrun --show-sdk-path 2>/dev/null || true)" if [[ -n "${sdk_path}" ]]; then diff --git a/src/audio_frame.cpp b/src/audio_frame.cpp index a197ea00..e9770c03 100644 --- a/src/audio_frame.cpp +++ b/src/audio_frame.cpp @@ -110,6 +110,4 @@ std::string AudioFrame::toString() const { return oss.str(); } -std::string AudioFrame::to_string() const { return toString(); } - } // namespace livekit diff --git a/src/e2ee.cpp b/src/e2ee.cpp index c1f8db2b..8ce73742 100644 --- a/src/e2ee.cpp +++ b/src/e2ee.cpp @@ -137,7 +137,7 @@ E2EEManager::E2EEManager(std::uint64_t room_handle, const E2EEOptions& options) : room_handle_(room_handle), enabled_(true), // or false, depending on your desired default behavior options_(options), - key_provider_(room_handle, options.key_provider_options) {} + key_provider_(std::shared_ptr(new KeyProvider(room_handle, options.key_provider_options))) {} bool E2EEManager::enabled() const { return enabled_; } @@ -149,8 +149,8 @@ void E2EEManager::setEnabled(bool enabled) { enabled_ = enabled; } -E2EEManager::KeyProvider* E2EEManager::keyProvider() { return &key_provider_; } -const E2EEManager::KeyProvider* E2EEManager::keyProvider() const { return &key_provider_; } +std::weak_ptr E2EEManager::keyProvider() { return key_provider_; } +std::weak_ptr E2EEManager::keyProvider() const { return key_provider_; } std::vector E2EEManager::frameCryptors() const { proto::FfiRequest req; diff --git a/src/livekit.cpp b/src/livekit.cpp index 1417213e..6b76759f 100644 --- a/src/livekit.cpp +++ b/src/livekit.cpp @@ -21,11 +21,12 @@ namespace livekit { -bool initialize(const LogLevel& level, const LogSink& log_sink) { +bool initialize(const LogLevel& level) { // Initializes logger if singleton instance is not already initialized setLogLevel(level); auto& ffi_client = FfiClient::instance(); - return ffi_client.initialize(log_sink == LogSink::kCallback); + // Note: capture_logs currently disabled, requires event support in FfiClient + return ffi_client.initialize(false); } bool isInitialized() { return FfiClient::instance().isInitialized(); } diff --git a/src/local_audio_track.cpp b/src/local_audio_track.cpp index 489cf6a3..3a2dd765 100644 --- a/src/local_audio_track.cpp +++ b/src/local_audio_track.cpp @@ -73,6 +73,4 @@ void LocalAudioTrack::unmute() { std::string LocalAudioTrack::toString() const { return "rtc.LocalAudioTrack(sid=" + sid() + ", name=" + name() + ")"; } -std::string LocalAudioTrack::to_string() const { return toString(); } - } // namespace livekit \ No newline at end of file diff --git a/src/local_video_track.cpp b/src/local_video_track.cpp index a02f6927..1cc6ee5a 100644 --- a/src/local_video_track.cpp +++ b/src/local_video_track.cpp @@ -73,6 +73,4 @@ void LocalVideoTrack::unmute() { std::string LocalVideoTrack::toString() const { return "rtc.LocalVideoTrack(sid=" + sid() + ", name=" + name() + ")"; } -std::string LocalVideoTrack::to_string() const { return toString(); } - } // namespace livekit \ No newline at end of file diff --git a/src/remote_audio_track.cpp b/src/remote_audio_track.cpp index 9a5ad621..cddf669d 100644 --- a/src/remote_audio_track.cpp +++ b/src/remote_audio_track.cpp @@ -16,8 +16,6 @@ #include "livekit/remote_audio_track.h" -#include "ffi.pb.h" -#include "ffi_client.h" #include "track.pb.h" #include "track_proto_converter.h" @@ -31,6 +29,4 @@ std::string RemoteAudioTrack::toString() const { return "rtc.RemoteAudioTrack(sid=" + sid() + ", name=" + name() + ")"; } -std::string RemoteAudioTrack::to_string() const { return toString(); } - } // namespace livekit \ No newline at end of file diff --git a/src/remote_participant.cpp b/src/remote_participant.cpp index f47ea57c..798b79fe 100644 --- a/src/remote_participant.cpp +++ b/src/remote_participant.cpp @@ -37,8 +37,6 @@ std::string RemoteParticipant::toString() const { return oss.str(); } -std::string RemoteParticipant::to_string() const { return toString(); } - std::ostream& operator<<(std::ostream& os, const RemoteParticipant& participant) { os << participant.toString(); return os; diff --git a/src/remote_video_track.cpp b/src/remote_video_track.cpp index e27ee562..fb3171d5 100644 --- a/src/remote_video_track.cpp +++ b/src/remote_video_track.cpp @@ -31,6 +31,4 @@ std::string RemoteVideoTrack::toString() const { return "rtc.RemoteVideoTrack(sid=" + sid() + ", name=" + name() + ")"; } -std::string RemoteVideoTrack::to_string() const { return toString(); } - } // namespace livekit \ No newline at end of file diff --git a/src/room.cpp b/src/room.cpp index 99b050a6..599fa897 100644 --- a/src/room.cpp +++ b/src/room.cpp @@ -81,13 +81,20 @@ Room::~Room() { } int listener_to_remove = 0; - std::unique_ptr local_participant_to_cleanup; + std::shared_ptr local_participant_to_cleanup; + std::unordered_map> remote_participants_to_cleanup; { const std::scoped_lock g(lock_); listener_to_remove = listener_id_; listener_id_ = 0; - // Move local participant out for cleanup outside the lock + connection_state_ = ConnectionState::Disconnected; + // Move participant state out so weak handles expire during teardown. local_participant_to_cleanup = std::move(local_participant_); + remote_participants_to_cleanup = std::move(remote_participants_); + room_handle_.reset(); + e2ee_manager_.reset(); + text_stream_readers_.clear(); + byte_stream_readers_.clear(); } // Shutdown local participant (unregisters RPC handlers, etc.) before @@ -142,7 +149,7 @@ bool Room::connect(const std::string& url, const std::string& token, const RoomO auto new_room_info = fromProto(owned_room.info()); // Setup local particpant - std::unique_ptr new_local_participant; + std::shared_ptr new_local_participant; { const auto& owned_local = connectCb.result().local_participant(); const auto& pinfo = owned_local.info(); @@ -159,7 +166,7 @@ bool Room::connect(const std::string& url, const std::string& token, const RoomO // Participant base stores a weak_ptr, so share the room handle FfiHandle participant_handle(static_cast(owned_local.handle().id())); new_local_participant = - std::make_unique(std::move(participant_handle), pinfo.sid(), pinfo.name(), pinfo.identity(), + std::make_shared(std::move(participant_handle), pinfo.sid(), pinfo.name(), pinfo.identity(), pinfo.metadata(), std::move(attrs), kind, reason); } @@ -182,11 +189,11 @@ bool Room::connect(const std::string& url, const std::string& token, const RoomO } // Setup e2eeManager - std::unique_ptr new_e2ee_manager; + std::shared_ptr new_e2ee_manager; if (options.encryption) { LK_LOG_INFO("creating E2eeManager"); new_e2ee_manager = - std::unique_ptr(new E2EEManager(new_room_handle->get(), options.encryption.value())); + std::shared_ptr(new E2EEManager(new_room_handle->get(), options.encryption.value())); } // Publish all state atomically under lock @@ -204,7 +211,7 @@ bool Room::connect(const std::string& url, const std::string& token, const RoomO return true; } catch (const std::exception& e) { int listener_to_remove = 0; - std::unique_ptr local_participant_to_cleanup; + std::shared_ptr local_participant_to_cleanup; { const std::scoped_lock g(lock_); connection_state_ = ConnectionState::Disconnected; @@ -225,36 +232,30 @@ bool Room::connect(const std::string& url, const std::string& token, const RoomO if (listener_to_remove != 0) { FfiClient::instance().removeListener(listener_to_remove); } - LK_LOG_ERROR("Room::Connect failed: {}", e.what()); + LK_LOG_ERROR("Room::connect failed: {}", e.what()); return false; } } -bool Room::Connect(const std::string& url, const std::string& token, const RoomOptions& options) { - return connect(url, token, options); -} - RoomInfoData Room::roomInfo() const { const std::scoped_lock g(lock_); return room_info_; } -RoomInfoData Room::room_info() const { return roomInfo(); } - -LocalParticipant* Room::localParticipant() const { +std::weak_ptr Room::localParticipant() const { const std::scoped_lock g(lock_); - return local_participant_.get(); + return local_participant_; } -RemoteParticipant* Room::remoteParticipant(const std::string& identity) const { +std::weak_ptr Room::remoteParticipant(const std::string& identity) const { const std::scoped_lock g(lock_); auto it = remote_participants_.find(identity); - return it == remote_participants_.end() ? nullptr : it->second.get(); + return it == remote_participants_.end() ? std::weak_ptr{} : it->second; } -std::vector> Room::remoteParticipants() const { +std::vector> Room::remoteParticipants() const { const std::scoped_lock guard(lock_); - std::vector> out; + std::vector> out; out.reserve(remote_participants_.size()); for (const auto& kv : remote_participants_) { out.push_back(kv.second); @@ -279,9 +280,9 @@ std::future Room::getStats() const { return FfiClient::instance().getSessionStatsAsync(handle->get()); } -E2EEManager* Room::e2eeManager() const { +std::weak_ptr Room::e2eeManager() const { const std::scoped_lock g(lock_); - return e2ee_manager_.get(); + return e2ee_manager_; } void Room::registerTextStreamHandler(const std::string& topic, TextStreamHandler handler) { @@ -314,13 +315,6 @@ void Room::unregisterByteStreamHandler(const std::string& topic) { // Frame callback registration // ------------------------------------------------------------------- -void Room::setOnAudioFrameCallback(const std::string& participant_identity, TrackSource source, - AudioFrameCallback callback, const AudioStream::Options& opts) { - if (subscription_thread_dispatcher_) { - subscription_thread_dispatcher_->setOnAudioFrameCallback(participant_identity, source, std::move(callback), opts); - } -} - void Room::setOnAudioFrameCallback(const std::string& participant_identity, const std::string& track_name, AudioFrameCallback callback, const AudioStream::Options& opts) { if (subscription_thread_dispatcher_) { @@ -329,13 +323,6 @@ void Room::setOnAudioFrameCallback(const std::string& participant_identity, cons } } -void Room::setOnVideoFrameCallback(const std::string& participant_identity, TrackSource source, - VideoFrameCallback callback, const VideoStream::Options& opts) { - if (subscription_thread_dispatcher_) { - subscription_thread_dispatcher_->setOnVideoFrameCallback(participant_identity, source, std::move(callback), opts); - } -} - void Room::setOnVideoFrameCallback(const std::string& participant_identity, const std::string& track_name, VideoFrameCallback callback, const VideoStream::Options& opts) { if (subscription_thread_dispatcher_) { @@ -352,24 +339,12 @@ void Room::setOnVideoFrameEventCallback(const std::string& participant_identity, } } -void Room::clearOnAudioFrameCallback(const std::string& participant_identity, TrackSource source) { - if (subscription_thread_dispatcher_) { - subscription_thread_dispatcher_->clearOnAudioFrameCallback(participant_identity, source); - } -} - void Room::clearOnAudioFrameCallback(const std::string& participant_identity, const std::string& track_name) { if (subscription_thread_dispatcher_) { subscription_thread_dispatcher_->clearOnAudioFrameCallback(participant_identity, track_name); } } -void Room::clearOnVideoFrameCallback(const std::string& participant_identity, TrackSource source) { - if (subscription_thread_dispatcher_) { - subscription_thread_dispatcher_->clearOnVideoFrameCallback(participant_identity, source); - } -} - void Room::clearOnVideoFrameCallback(const std::string& participant_identity, const std::string& track_name) { if (subscription_thread_dispatcher_) { subscription_thread_dispatcher_->clearOnVideoFrameCallback(participant_identity, track_name); @@ -668,8 +643,7 @@ void Room::onEvent(const FfiEvent& event) { } if (subscription_thread_dispatcher_ && remote_track && rpublication) { - subscription_thread_dispatcher_->handleTrackSubscribed(identity, rpublication->source(), - rpublication->name(), remote_track); + subscription_thread_dispatcher_->handleTrackSubscribed(identity, rpublication->name(), remote_track); } break; } @@ -1169,10 +1143,10 @@ void Room::onEvent(const FfiEvent& event) { // Move state out of lock scope before destroying to avoid holding lock // during potentially long destructors - std::unique_ptr old_local_participant; + std::shared_ptr old_local_participant; std::unordered_map> old_remote_participants; std::shared_ptr old_room_handle; - std::unique_ptr old_e2ee_manager; + std::shared_ptr old_e2ee_manager; std::unordered_map> old_text_readers; std::unordered_map> old_byte_readers; @@ -1198,7 +1172,11 @@ void Room::onEvent(const FfiEvent& event) { FfiClient::instance().removeListener(listener_to_remove); } - // Old state will be destroyed here when going out of scope + if (old_local_participant) { + old_local_participant->shutdown(); + } + + // old_* state is destroyed here when going out of scope const RoomEosEvent ev; if (delegate_snapshot) { diff --git a/src/subscription_thread_dispatcher.cpp b/src/subscription_thread_dispatcher.cpp index 758707ba..ed77d0be 100644 --- a/src/subscription_thread_dispatcher.cpp +++ b/src/subscription_thread_dispatcher.cpp @@ -57,23 +57,10 @@ SubscriptionThreadDispatcher::~SubscriptionThreadDispatcher() { } // NOLINTEND(bugprone-exception-escape) -void SubscriptionThreadDispatcher::setOnAudioFrameCallback(const std::string& participant_identity, TrackSource source, - AudioFrameCallback callback, - const AudioStream::Options& opts) { - const CallbackKey key{participant_identity, source, ""}; - const std::scoped_lock lock(lock_); - const bool replacing = audio_callbacks_.find(key) != audio_callbacks_.end(); - audio_callbacks_[key] = RegisteredAudioCallback{std::move(callback), opts}; - LK_LOG_DEBUG( - "Registered audio frame callback for participant={} source={} " - "replacing_existing={} total_audio_callbacks={}", - participant_identity, static_cast(source), replacing, audio_callbacks_.size()); -} - void SubscriptionThreadDispatcher::setOnAudioFrameCallback(const std::string& participant_identity, const std::string& track_name, AudioFrameCallback callback, const AudioStream::Options& opts) { - const CallbackKey key{participant_identity, TrackSource::SOURCE_UNKNOWN, track_name}; + const CallbackKey key{participant_identity, track_name}; const std::scoped_lock lock(lock_); const bool replacing = audio_callbacks_.find(key) != audio_callbacks_.end(); audio_callbacks_[key] = RegisteredAudioCallback{std::move(callback), opts}; @@ -83,28 +70,11 @@ void SubscriptionThreadDispatcher::setOnAudioFrameCallback(const std::string& pa participant_identity, track_name, replacing, audio_callbacks_.size()); } -void SubscriptionThreadDispatcher::setOnVideoFrameCallback(const std::string& participant_identity, TrackSource source, - VideoFrameCallback callback, - const VideoStream::Options& opts) { - const CallbackKey key{participant_identity, source, ""}; - const std::scoped_lock lock(lock_); - const bool replacing = video_callbacks_.find(key) != video_callbacks_.end(); - video_callbacks_[key] = RegisteredVideoCallback{ - std::move(callback), - VideoFrameEventCallback{}, - opts, - }; - LK_LOG_DEBUG( - "Registered legacy video frame callback for participant={} " - "source={} replacing_existing={} total_video_callbacks={}", - participant_identity, static_cast(source), replacing, video_callbacks_.size()); -} - void SubscriptionThreadDispatcher::setOnVideoFrameEventCallback(const std::string& participant_identity, const std::string& track_name, VideoFrameEventCallback callback, const VideoStream::Options& opts) { - const CallbackKey key{participant_identity, TrackSource::SOURCE_UNKNOWN, track_name}; + const CallbackKey key{participant_identity, track_name}; const std::scoped_lock lock(lock_); const bool replacing = video_callbacks_.find(key) != video_callbacks_.end(); video_callbacks_[key] = RegisteredVideoCallback{ @@ -121,7 +91,7 @@ void SubscriptionThreadDispatcher::setOnVideoFrameEventCallback(const std::strin void SubscriptionThreadDispatcher::setOnVideoFrameCallback(const std::string& participant_identity, const std::string& track_name, VideoFrameCallback callback, const VideoStream::Options& opts) { - const CallbackKey key{participant_identity, TrackSource::SOURCE_UNKNOWN, track_name}; + const CallbackKey key{participant_identity, track_name}; const std::scoped_lock lock(lock_); const bool replacing = video_callbacks_.find(key) != video_callbacks_.end(); video_callbacks_[key] = RegisteredVideoCallback{ @@ -135,29 +105,9 @@ void SubscriptionThreadDispatcher::setOnVideoFrameCallback(const std::string& pa participant_identity, track_name, replacing, video_callbacks_.size()); } -void SubscriptionThreadDispatcher::clearOnAudioFrameCallback(const std::string& participant_identity, - TrackSource source) { - const CallbackKey key{participant_identity, source, ""}; - std::thread old_thread; - bool removed_callback = false; - { - const std::scoped_lock lock(lock_); - removed_callback = audio_callbacks_.erase(key) > 0; - old_thread = extractReaderThreadLocked(key); - LK_LOG_DEBUG( - "Clearing audio frame callback for participant={} source={} " - "removed_callback={} stopped_reader={} remaining_audio_callbacks={}", - participant_identity, static_cast(source), removed_callback, old_thread.joinable(), - audio_callbacks_.size()); - } - if (old_thread.joinable()) { - old_thread.join(); - } -} - void SubscriptionThreadDispatcher::clearOnAudioFrameCallback(const std::string& participant_identity, const std::string& track_name) { - const CallbackKey key{participant_identity, TrackSource::SOURCE_UNKNOWN, track_name}; + const CallbackKey key{participant_identity, track_name}; std::thread old_thread; bool removed_callback = false; { @@ -174,29 +124,9 @@ void SubscriptionThreadDispatcher::clearOnAudioFrameCallback(const std::string& } } -void SubscriptionThreadDispatcher::clearOnVideoFrameCallback(const std::string& participant_identity, - TrackSource source) { - const CallbackKey key{participant_identity, source, ""}; - std::thread old_thread; - bool removed_callback = false; - { - const std::scoped_lock lock(lock_); - removed_callback = video_callbacks_.erase(key) > 0; - old_thread = extractReaderThreadLocked(key); - LK_LOG_DEBUG( - "Clearing video frame callback for participant={} source={} " - "removed_callback={} stopped_reader={} remaining_video_callbacks={}", - participant_identity, static_cast(source), removed_callback, old_thread.joinable(), - video_callbacks_.size()); - } - if (old_thread.joinable()) { - old_thread.join(); - } -} - void SubscriptionThreadDispatcher::clearOnVideoFrameCallback(const std::string& participant_identity, const std::string& track_name) { - const CallbackKey key{participant_identity, TrackSource::SOURCE_UNKNOWN, track_name}; + const CallbackKey key{participant_identity, track_name}; std::thread old_thread; bool removed_callback = false; { @@ -213,29 +143,22 @@ void SubscriptionThreadDispatcher::clearOnVideoFrameCallback(const std::string& } } -void SubscriptionThreadDispatcher::handleTrackSubscribed(const std::string& participant_identity, TrackSource source, +void SubscriptionThreadDispatcher::handleTrackSubscribed(const std::string& participant_identity, const std::string& track_name, const std::shared_ptr& track) { if (!track) { - LK_LOG_WARN( - "Ignoring subscribed track dispatch for participant={} source={} " - "because track is null", - participant_identity, static_cast(source)); + LK_LOG_WARN("Ignoring subscribed track dispatch for participant={} track_name={} because track is null", + participant_identity, track_name); return; } - LK_LOG_DEBUG("Handling subscribed track for participant={} source={} kind={}", participant_identity, - static_cast(source), trackKindName(track->kind())); + LK_LOG_DEBUG("Handling subscribed track for participant={} track_name={} kind={}", participant_identity, track_name, + trackKindName(track->kind())); - CallbackKey key{participant_identity, TrackSource::SOURCE_UNKNOWN, track_name}; - const CallbackKey fallback_key{participant_identity, source, ""}; + const CallbackKey key{participant_identity, track_name}; std::thread old_thread; { const std::scoped_lock lock(lock_); - if ((track->kind() == TrackKind::KIND_AUDIO && audio_callbacks_.find(key) == audio_callbacks_.end()) || - (track->kind() == TrackKind::KIND_VIDEO && video_callbacks_.find(key) == video_callbacks_.end())) { - key = fallback_key; - } old_thread = startReaderLocked(key, track); } if (old_thread.joinable()) { @@ -245,26 +168,19 @@ void SubscriptionThreadDispatcher::handleTrackSubscribed(const std::string& part void SubscriptionThreadDispatcher::handleTrackUnsubscribed(const std::string& participant_identity, TrackSource source, const std::string& track_name) { - const CallbackKey key{participant_identity, TrackSource::SOURCE_UNKNOWN, track_name}; - const CallbackKey fallback_key{participant_identity, source, ""}; + const CallbackKey key{participant_identity, track_name}; std::thread old_thread; - std::thread fallback_old_thread; { const std::scoped_lock lock(lock_); old_thread = extractReaderThreadLocked(key); - fallback_old_thread = extractReaderThreadLocked(fallback_key); LK_LOG_DEBUG( "Handling unsubscribed track for participant={} source={} " - "track_name={} stopped_reader={} fallback_stopped_reader={}", - participant_identity, static_cast(source), track_name, old_thread.joinable(), - fallback_old_thread.joinable()); + "track_name={} stopped_reader={}", + participant_identity, static_cast(source), track_name, old_thread.joinable()); } if (old_thread.joinable()) { old_thread.join(); } - if (fallback_old_thread.joinable()) { - fallback_old_thread.join(); - } } // ------------------------------------------------------------------- @@ -419,17 +335,12 @@ void SubscriptionThreadDispatcher::stopAll() { std::thread SubscriptionThreadDispatcher::extractReaderThreadLocked(const CallbackKey& key) { auto it = active_readers_.find(key); if (it == active_readers_.end()) { - LK_LOG_TRACE( - "No active reader to extract for participant={} source={} " - "track_name={}", - key.participant_identity, static_cast(key.source), key.track_name); + LK_LOG_TRACE("No active reader to extract for participant={} track_name={}", key.participant_identity, + key.track_name); return {}; } - LK_LOG_DEBUG( - "Extracting active reader for participant={} source={} " - "track_name={}", - key.participant_identity, static_cast(key.source), key.track_name); + LK_LOG_DEBUG("Extracting active reader for participant={} track_name={}", key.participant_identity, key.track_name); ActiveReader reader = std::move(it->second); active_readers_.erase(it); @@ -448,9 +359,9 @@ std::thread SubscriptionThreadDispatcher::startReaderLocked(const CallbackKey& k auto it = audio_callbacks_.find(key); if (it == audio_callbacks_.end()) { LK_LOG_TRACE( - "Skipping audio reader start for participant={} source={} " + "Skipping audio reader start for participant={} track_name={} " "because no audio callback is registered", - key.participant_identity, static_cast(key.source)); + key.participant_identity, key.track_name); return {}; } return startAudioReaderLocked(key, track, it->second.callback, it->second.options); @@ -459,25 +370,25 @@ std::thread SubscriptionThreadDispatcher::startReaderLocked(const CallbackKey& k auto it = video_callbacks_.find(key); if (it == video_callbacks_.end()) { LK_LOG_TRACE( - "Skipping video reader start for participant={} source={} " + "Skipping video reader start for participant={} track_name={} " "because no video callback is registered", - key.participant_identity, static_cast(key.source)); + key.participant_identity, key.track_name); return {}; } return startVideoReaderLocked(key, track, it->second); } if (track->kind() == TrackKind::KIND_UNKNOWN) { LK_LOG_WARN( - "Skipping reader start for participant={} source={} because track " + "Skipping reader start for participant={} track_name={} because track " "kind is unknown", - key.participant_identity, static_cast(key.source)); + key.participant_identity, key.track_name); return {}; } LK_LOG_WARN( - "Skipping reader start for participant={} source={} because track kind " + "Skipping reader start for participant={} track_name={} because track kind " "is unsupported", - key.participant_identity, static_cast(key.source)); + key.participant_identity, key.track_name); return {}; } @@ -485,39 +396,36 @@ std::thread SubscriptionThreadDispatcher::startAudioReaderLocked(const CallbackK const std::shared_ptr& track, const AudioFrameCallback& cb, const AudioStream::Options& opts) { - LK_LOG_DEBUG("Starting audio reader for participant={} source={}", key.participant_identity, - static_cast(key.source)); + LK_LOG_DEBUG("Starting audio reader for participant={} track_name={}", key.participant_identity, key.track_name); auto old_thread = extractReaderThreadLocked(key); if (static_cast(active_readers_.size()) >= kMaxActiveReaders) { LK_LOG_ERROR( - "Cannot start audio reader for {} source={}: active reader limit ({}) " + "Cannot start audio reader for {} track_name={}: active reader limit ({}) " "reached", - key.participant_identity, static_cast(key.source), kMaxActiveReaders); + key.participant_identity, key.track_name, kMaxActiveReaders); return old_thread; } const auto stream = AudioStream::fromTrack(track, opts); if (!stream) { - LK_LOG_ERROR("Failed to create AudioStream for {} source={}", key.participant_identity, - static_cast(key.source)); + LK_LOG_ERROR("Failed to create AudioStream for {} track_name={}", key.participant_identity, key.track_name); return old_thread; } ActiveReader reader; reader.audio_stream = stream; const std::string participant_identity = key.participant_identity; - const TrackSource source = key.source; + const std::string track_name = key.track_name; // NOLINTBEGIN(bugprone-lambda-function-name,bugprone-exception-escape) // Outer try/catch contains anything escaping the per-frame try/catch // (stream->read, LK_LOG formatting, etc.) so an exception in this reader // thread cannot std::terminate the process. clang-tidy still flags a // residual escape path through spdlog's own formatter; that's a logger // fault, not application logic -- suppressed at the lambda level. - reader.thread = std::thread([stream, cb, participant_identity, source]() { + reader.thread = std::thread([stream, cb, participant_identity, track_name]() { try { - LK_LOG_DEBUG("Audio reader thread started for participant={} source={}", participant_identity, - static_cast(source)); + LK_LOG_DEBUG("Audio reader thread started for participant={} track_name={}", participant_identity, track_name); AudioFrameEvent ev; while (stream->read(ev)) { try { @@ -526,8 +434,7 @@ std::thread SubscriptionThreadDispatcher::startAudioReaderLocked(const CallbackK LK_LOG_ERROR("Audio frame callback exception: {}", e.what()); } } - LK_LOG_DEBUG("Audio reader thread exiting for participant={} source={}", participant_identity, - static_cast(source)); + LK_LOG_DEBUG("Audio reader thread exiting for participant={} track_name={}", participant_identity, track_name); } catch (const std::exception& e) { LK_LOG_ERROR("Audio reader thread terminating due to exception: {}", e.what()); } catch (...) { @@ -537,31 +444,29 @@ std::thread SubscriptionThreadDispatcher::startAudioReaderLocked(const CallbackK // NOLINTEND(bugprone-lambda-function-name,bugprone-exception-escape) active_readers_[key] = std::move(reader); LK_LOG_DEBUG( - "Started audio reader for participant={} source={} " + "Started audio reader for participant={} track_name={} " "active_readers={}", - key.participant_identity, static_cast(key.source), active_readers_.size()); + key.participant_identity, key.track_name, active_readers_.size()); return old_thread; } std::thread SubscriptionThreadDispatcher::startVideoReaderLocked(const CallbackKey& key, const std::shared_ptr& track, const RegisteredVideoCallback& callback) { - LK_LOG_DEBUG("Starting video reader for participant={} source={}", key.participant_identity, - static_cast(key.source)); + LK_LOG_DEBUG("Starting video reader for participant={} track_name={}", key.participant_identity, key.track_name); auto old_thread = extractReaderThreadLocked(key); if (static_cast(active_readers_.size()) >= kMaxActiveReaders) { LK_LOG_ERROR( - "Cannot start video reader for {} source={}: active reader limit ({}) " + "Cannot start video reader for {} track_name={}: active reader limit ({}) " "reached", - key.participant_identity, static_cast(key.source), kMaxActiveReaders); + key.participant_identity, key.track_name, kMaxActiveReaders); return old_thread; } auto stream = VideoStream::fromTrack(track, callback.options); if (!stream) { - LK_LOG_ERROR("Failed to create VideoStream for {} source={}", key.participant_identity, - static_cast(key.source)); + LK_LOG_ERROR("Failed to create VideoStream for {} track_name={}", key.participant_identity, key.track_name); return old_thread; } @@ -570,15 +475,14 @@ std::thread SubscriptionThreadDispatcher::startVideoReaderLocked(const CallbackK auto legacy_cb = callback.legacy_callback; auto event_cb = callback.event_callback; const std::string participant_identity = key.participant_identity; - const TrackSource source = key.source; + const std::string track_name = key.track_name; // NOLINTBEGIN(bugprone-lambda-function-name,bugprone-exception-escape) // Mirrors the audio reader: outer try/catch contains escapes from // stream->read, LK_LOG, etc. Residual diagnostic from spdlog's own // formatter is an unrelated logger-fault path and is suppressed. - reader.thread = std::thread([stream = std::move(stream), legacy_cb, event_cb, participant_identity, source]() { + reader.thread = std::thread([stream = std::move(stream), legacy_cb, event_cb, participant_identity, track_name]() { try { - LK_LOG_DEBUG("Video reader thread started for participant={} source={}", participant_identity, - static_cast(source)); + LK_LOG_DEBUG("Video reader thread started for participant={} track_name={}", participant_identity, track_name); VideoFrameEvent ev; while (stream->read(ev)) { try { @@ -591,8 +495,7 @@ std::thread SubscriptionThreadDispatcher::startVideoReaderLocked(const CallbackK LK_LOG_ERROR("Video frame callback exception: {}", e.what()); } } - LK_LOG_DEBUG("Video reader thread exiting for participant={} source={}", participant_identity, - static_cast(source)); + LK_LOG_DEBUG("Video reader thread exiting for participant={} track_name={}", participant_identity, track_name); } catch (const std::exception& e) { LK_LOG_ERROR("Video reader thread terminating due to exception: {}", e.what()); } catch (...) { @@ -602,9 +505,9 @@ std::thread SubscriptionThreadDispatcher::startVideoReaderLocked(const CallbackK // NOLINTEND(bugprone-lambda-function-name,bugprone-exception-escape) active_readers_[key] = std::move(reader); LK_LOG_DEBUG( - "Started video reader for participant={} source={} " + "Started video reader for participant={} track_name={} " "active_readers={}", - key.participant_identity, static_cast(key.source), active_readers_.size()); + key.participant_identity, key.track_name, active_readers_.size()); return old_thread; } diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 2bd0c09e..9583af73 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -5,6 +5,7 @@ cmake_minimum_required(VERSION 3.20) # ============================================================================ include(FetchContent) +include(warnings) FetchContent_Declare( googletest @@ -21,7 +22,33 @@ set(INSTALL_GTEST OFF CACHE BOOL "" FORCE) # Skip GMock, not used currently set(BUILD_GMOCK OFF CACHE BOOL "" FORCE) -FetchContent_MakeAvailable(googletest) +livekit_fetchcontent_makeavailable(googletest) +livekit_collect_targets_in_directory(_livekit_googletest_targets "${googletest_BINARY_DIR}") +foreach(_livekit_googletest_target IN LISTS _livekit_googletest_targets) + livekit_disable_warnings(${_livekit_googletest_target}) +endforeach() + +set(LIVEKIT_TEST_PROTOBUF_DEP_INCLUDE_DIRS ${Protobuf_INCLUDE_DIRS}) +if(TARGET absl::base) + livekit_get_interface_includes(absl::base _livekit_test_absl_inc) + if(_livekit_test_absl_inc) + list(APPEND LIVEKIT_TEST_PROTOBUF_DEP_INCLUDE_DIRS ${_livekit_test_absl_inc}) + endif() +endif() + +set(LIVEKIT_TEST_SYSTEM_INCLUDE_DIRS ${LIVEKIT_BINARY_DIR}/generated) +if(TARGET spdlog::spdlog) + livekit_get_interface_includes(spdlog::spdlog _livekit_test_spdlog_inc) + if(_livekit_test_spdlog_inc) + list(APPEND LIVEKIT_TEST_SYSTEM_INCLUDE_DIRS ${_livekit_test_spdlog_inc}) + endif() +endif() +if(TARGET GTest::gtest_main) + livekit_get_interface_includes(GTest::gtest_main _livekit_test_gtest_inc) + if(_livekit_test_gtest_inc) + list(APPEND LIVEKIT_TEST_SYSTEM_INCLUDE_DIRS ${_livekit_test_gtest_inc}) + endif() +endif() # Enable CTest enable_testing() @@ -72,18 +99,12 @@ if(UNIT_TEST_SOURCES) ${LIVEKIT_ROOT_DIR}/include ${LIVEKIT_ROOT_DIR}/src ${LIVEKIT_ROOT_DIR}/src/trace - ${LIVEKIT_BINARY_DIR}/generated ${CMAKE_CURRENT_SOURCE_DIR}/benchmark - ${Protobuf_INCLUDE_DIRS} + ${LIVEKIT_TEST_PROTOBUF_DEP_INCLUDE_DIRS} + ) + target_include_directories(livekit_unit_tests SYSTEM PRIVATE + ${LIVEKIT_TEST_SYSTEM_INCLUDE_DIRS} ) - if(TARGET absl::base) - get_target_property(_livekit_unit_test_absl_inc absl::base INTERFACE_INCLUDE_DIRECTORIES) - if(_livekit_unit_test_absl_inc) - target_include_directories(livekit_unit_tests PRIVATE - ${_livekit_unit_test_absl_inc} - ) - endif() - endif() target_compile_definitions(livekit_unit_tests PRIVATE @@ -170,18 +191,12 @@ if(INTEGRATION_TEST_SOURCES) PRIVATE ${LIVEKIT_ROOT_DIR}/include ${LIVEKIT_ROOT_DIR}/src - ${LIVEKIT_BINARY_DIR}/generated ${CMAKE_CURRENT_SOURCE_DIR}/benchmark - ${Protobuf_INCLUDE_DIRS} + ${LIVEKIT_TEST_PROTOBUF_DEP_INCLUDE_DIRS} + ) + target_include_directories(livekit_integration_tests SYSTEM PRIVATE + ${LIVEKIT_TEST_SYSTEM_INCLUDE_DIRS} ) - if(TARGET absl::base) - get_target_property(_livekit_test_absl_inc absl::base INTERFACE_INCLUDE_DIRECTORIES) - if(_livekit_test_absl_inc) - target_include_directories(livekit_integration_tests PRIVATE - ${_livekit_test_absl_inc} - ) - endif() - endif() target_compile_definitions(livekit_integration_tests PRIVATE @@ -258,8 +273,10 @@ if(STRESS_TEST_SOURCES) PRIVATE ${LIVEKIT_ROOT_DIR}/include ${LIVEKIT_ROOT_DIR}/src - ${LIVEKIT_BINARY_DIR}/generated - ${Protobuf_INCLUDE_DIRS} + ${LIVEKIT_TEST_PROTOBUF_DEP_INCLUDE_DIRS} + ) + target_include_directories(livekit_stress_tests SYSTEM PRIVATE + ${LIVEKIT_TEST_SYSTEM_INCLUDE_DIRS} ) target_compile_definitions(livekit_stress_tests diff --git a/src/tests/common/test_common.h b/src/tests/common/test_common.h index 6fd68a67..427cd4ca 100644 --- a/src/tests/common/test_common.h +++ b/src/tests/common/test_common.h @@ -130,7 +130,7 @@ inline uint64_t getTimestampUs() { inline 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) { + if (!room->remoteParticipant(identity).expired()) { return true; } std::this_thread::sleep_for(100ms); @@ -138,6 +138,47 @@ inline bool waitForParticipant(Room* room, const std::string& identity, std::chr return false; } +/// Safely promote the local participant weak handle to a shared_ptr. +/// +/// Room::localParticipant() returns a std::weak_ptr whose lock() yields nullptr +/// once the room is torn down (or before connect). Dereferencing the result of +/// lock() blindly is undefined behavior, so tests must go through this helper, +/// which throws instead of crashing when the handle is expired. +inline std::shared_ptr lockLocalParticipant(const Room& room) { + if (auto participant = room.localParticipant().lock()) { + return participant; + } + throw std::runtime_error("Local participant handle is expired"); +} + +/// Pointer overload of lockLocalParticipant(); throws if @p room is null. +inline std::shared_ptr lockLocalParticipant(const Room* room) { + if (room == nullptr) { + throw std::runtime_error("Room is null"); + } + return lockLocalParticipant(*room); +} + +/// Safely promote a remote participant weak handle to a shared_ptr. +/// +/// Mirrors lockLocalParticipant(): Room::remoteParticipant() returns a +/// std::weak_ptr that lock()s to nullptr once the participant disconnects, so +/// this helper throws rather than letting callers dereference a null pointer. +inline std::shared_ptr lockRemoteParticipant(const Room& room, const std::string& identity) { + if (auto participant = room.remoteParticipant(identity).lock()) { + return participant; + } + throw std::runtime_error("Remote participant '" + identity + "' handle is expired"); +} + +/// Pointer overload of lockRemoteParticipant(); throws if @p room is null. +inline std::shared_ptr lockRemoteParticipant(const Room* room, const std::string& identity) { + if (room == nullptr) { + throw std::runtime_error("Room is null"); + } + return lockRemoteParticipant(*room, identity); +} + inline std::array getDataTrackTestTokens() { const char* token_a = std::getenv("LIVEKIT_TOKEN_A"); if (token_a == nullptr || std::string(token_a).empty()) { @@ -161,10 +202,10 @@ inline void waitForParticipantVisibility(const std::vector std::vector participant_identities; participant_identities.reserve(rooms.size()); for (const auto& room : rooms) { - if (!room || room->localParticipant() == nullptr) { + if (!room || room->localParticipant().expired()) { throw std::runtime_error("Test room is missing a local participant after connect"); } - participant_identities.push_back(room->localParticipant()->identity()); + participant_identities.push_back(lockLocalParticipant(room.get())->identity()); } auto start = std::chrono::steady_clock::now(); @@ -172,7 +213,7 @@ inline void waitForParticipantVisibility(const std::vector bool all_visible = true; for (size_t i = 0; i < rooms.size(); ++i) { const auto& room = rooms[i]; - if (!room || room->localParticipant() == nullptr) { + if (!room || room->localParticipant().expired()) { throw std::runtime_error("Test room is missing a local participant after connect"); } @@ -181,7 +222,7 @@ inline void waitForParticipantVisibility(const std::vector continue; } - if (room->remoteParticipant(participant_identities[j]) == nullptr) { + if (room->remoteParticipant(participant_identities[j]).expired()) { all_visible = false; break; } @@ -456,7 +497,7 @@ class StressTestStats { class LiveKitTestBase : public ::testing::Test { protected: void SetUp() override { - livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); + livekit::initialize(livekit::LogLevel::Info); config_ = TestConfig::fromEnv(); // Tracing is controlled by compile-time macro LIVEKIT_TEST_ENABLE_TRACING diff --git a/src/tests/integration/test_data_track.cpp b/src/tests/integration/test_data_track.cpp index 54fcbc3c..b575c410 100644 --- a/src/tests/integration/test_data_track.cpp +++ b/src/tests/integration/test_data_track.cpp @@ -113,8 +113,13 @@ std::string describeDataTrackError(const Error& error) { return "code=" + std::to_string(static_cast(error.code)) + " message=" + error.message; } -std::shared_ptr requirePublishedTrack(LocalParticipant* participant, const std::string& name) { - auto result = participant->publishDataTrack(name); +std::shared_ptr requirePublishedTrack(const std::weak_ptr& participant, + const std::string& name) { + auto locked = participant.lock(); + if (!locked) { + throw std::runtime_error("Local participant handle is expired"); + } + auto result = locked->publishDataTrack(name); if (!result) { throw std::runtime_error("Failed to publish data track: " + describeDataTrackError(result.error())); } @@ -203,18 +208,20 @@ void runEncryptedDataTrackRoundTrip(KeyDerivationFunction key_derivation_functio auto& publisher_room = rooms[0]; auto& subscriber_room = rooms[1]; - ASSERT_NE(publisher_room->e2eeManager(), nullptr); - ASSERT_NE(subscriber_room->e2eeManager(), nullptr); - ASSERT_NE(publisher_room->e2eeManager()->keyProvider(), nullptr); - ASSERT_NE(subscriber_room->e2eeManager()->keyProvider(), nullptr); - EXPECT_EQ(publisher_room->e2eeManager()->keyProvider()->options().key_derivation_function, key_derivation_function); - EXPECT_EQ(subscriber_room->e2eeManager()->keyProvider()->options().key_derivation_function, key_derivation_function); - publisher_room->e2eeManager()->setEnabled(true); - subscriber_room->e2eeManager()->setEnabled(true); - EXPECT_EQ(publisher_room->e2eeManager()->keyProvider()->exportSharedKey(), e2eeSharedKey()); - EXPECT_EQ(subscriber_room->e2eeManager()->keyProvider()->exportSharedKey(), e2eeSharedKey()); - - auto publish_result = publisher_room->localParticipant()->publishDataTrack(track_name); + ASSERT_FALSE(publisher_room->e2eeManager().expired()); + ASSERT_FALSE(subscriber_room->e2eeManager().expired()); + ASSERT_FALSE(publisher_room->e2eeManager().lock()->keyProvider().expired()); + ASSERT_FALSE(subscriber_room->e2eeManager().lock()->keyProvider().expired()); + EXPECT_EQ(publisher_room->e2eeManager().lock()->keyProvider().lock()->options().key_derivation_function, + key_derivation_function); + EXPECT_EQ(subscriber_room->e2eeManager().lock()->keyProvider().lock()->options().key_derivation_function, + key_derivation_function); + publisher_room->e2eeManager().lock()->setEnabled(true); + subscriber_room->e2eeManager().lock()->setEnabled(true); + EXPECT_EQ(publisher_room->e2eeManager().lock()->keyProvider().lock()->exportSharedKey(), e2eeSharedKey()); + EXPECT_EQ(subscriber_room->e2eeManager().lock()->keyProvider().lock()->exportSharedKey(), e2eeSharedKey()); + + auto publish_result = lockLocalParticipant(*publisher_room)->publishDataTrack(track_name); if (!publish_result) { FAIL() << describeDataTrackError(publish_result.error()); } @@ -305,7 +312,7 @@ TEST_P(DataTrackTransportTest, PublishesAndReceivesFramesEndToEnd) { auto rooms = testRooms(room_configs); auto& publisher_room = rooms[0]; - const auto publisher_identity = publisher_room->localParticipant()->identity(); + const auto publisher_identity = lockLocalParticipant(*publisher_room)->identity(); auto local_track = requirePublishedTrack(publisher_room->localParticipant(), track_name); ASSERT_TRUE(local_track->isPublished()); @@ -370,7 +377,7 @@ TEST_F(DataTrackE2ETest, UnpublishUpdatesPublishedStateEndToEnd) { auto rooms = testRooms(room_configs); auto& publisher_room = rooms[0]; - auto publish_result = publisher_room->localParticipant()->publishDataTrack(track_name); + auto publish_result = lockLocalParticipant(*publisher_room)->publishDataTrack(track_name); if (!publish_result) { FAIL() << describeDataTrackError(publish_result.error()); } @@ -453,7 +460,7 @@ TEST_F(DataTrackE2ETest, PublishManyTracks) { const auto start = std::chrono::steady_clock::now(); for (int index = 0; index < kPublishManyTrackCount; ++index) { const auto track_name = "track_" + std::to_string(index); - auto publish_result = room->localParticipant()->publishDataTrack(track_name); + auto publish_result = lockLocalParticipant(*room)->publishDataTrack(track_name); if (!publish_result) { FAIL() << "Failed to publish track " << track_name << ": " << describeDataTrackError(publish_result.error()); } @@ -494,14 +501,14 @@ TEST_F(DataTrackE2ETest, PublishDuplicateName) { auto rooms = testRooms(1); auto& room = rooms[0]; - auto first_track_result = room->localParticipant()->publishDataTrack("first"); + auto first_track_result = lockLocalParticipant(*room)->publishDataTrack("first"); if (!first_track_result) { FAIL() << describeDataTrackError(first_track_result.error()); } auto first_track = first_track_result.value(); ASSERT_TRUE(first_track->isPublished()); - auto duplicate_result = room->localParticipant()->publishDataTrack("first"); + auto duplicate_result = lockLocalParticipant(*room)->publishDataTrack("first"); ASSERT_FALSE(duplicate_result) << "Expected duplicate data-track name to be rejected"; EXPECT_EQ(duplicate_result.error().code, PublishDataTrackErrorCode::DUPLICATE_NAME); EXPECT_FALSE(duplicate_result.error().message.empty()); @@ -580,7 +587,7 @@ TEST_F(DataTrackE2ETest, FfiClientSubscribeDataTrackReturnsSyncResult) { for (std::size_t idx = 0; idx < kTopicCount; ++idx) { const auto track_name = "test_" + std::to_string(idx); - auto publish_result = publisher_room->localParticipant()->publishDataTrack(track_name); + auto publish_result = lockLocalParticipant(*publisher_room)->publishDataTrack(track_name); if (!publish_result) { FAIL() << "Failed to publish " << track_name << ": " << describeDataTrackError(publish_result.error()); } @@ -642,7 +649,7 @@ TEST_F(DataTrackE2ETest, PreservesUserTimestampEndToEnd) { auto rooms = testRooms(room_configs); auto& publisher_room = rooms[0]; - auto publish_result = publisher_room->localParticipant()->publishDataTrack(track_name); + auto publish_result = lockLocalParticipant(*publisher_room)->publishDataTrack(track_name); if (!publish_result) { FAIL() << describeDataTrackError(publish_result.error()); } @@ -728,12 +735,12 @@ TEST_F(DataTrackE2ETest, PreservesUserTimestampOnEncryptedDataTrack) { auto& publisher_room = rooms[0]; auto& subscriber_room = rooms[1]; - ASSERT_NE(publisher_room->e2eeManager(), nullptr); - ASSERT_NE(subscriber_room->e2eeManager(), nullptr); - publisher_room->e2eeManager()->setEnabled(true); - subscriber_room->e2eeManager()->setEnabled(true); + ASSERT_FALSE(publisher_room->e2eeManager().expired()); + ASSERT_FALSE(subscriber_room->e2eeManager().expired()); + publisher_room->e2eeManager().lock()->setEnabled(true); + subscriber_room->e2eeManager().lock()->setEnabled(true); - auto publish_result = publisher_room->localParticipant()->publishDataTrack(track_name); + auto publish_result = lockLocalParticipant(*publisher_room)->publishDataTrack(track_name); if (!publish_result) { FAIL() << describeDataTrackError(publish_result.error()); } diff --git a/src/tests/integration/test_late_join_track_publication.cpp b/src/tests/integration/test_late_join_track_publication.cpp index 89e84392..5e7a3ec4 100644 --- a/src/tests/integration/test_late_join_track_publication.cpp +++ b/src/tests/integration/test_late_join_track_publication.cpp @@ -131,7 +131,7 @@ class LateJoinPublicationDelegate : public RoomDelegate { } void validateCommonCallbackState(Room& room, const std::string& callback_name) { - if (room.localParticipant() == nullptr) { + if (room.localParticipant().expired()) { recordInvariantFailure(callback_name + " fired before room.localParticipant() was initialized"); } @@ -141,7 +141,7 @@ class LateJoinPublicationDelegate : public RoomDelegate { expected_publisher_identity = state_.expected_publisher_identity; } - if (!expected_publisher_identity.empty() && room.remoteParticipant(expected_publisher_identity) == nullptr) { + if (!expected_publisher_identity.empty() && room.remoteParticipant(expected_publisher_identity).expired()) { recordInvariantFailure(callback_name + " fired before expected remote participant was visible: " + expected_publisher_identity); } @@ -164,8 +164,8 @@ class LateJoinPublicationDelegate : public RoomDelegate { recordInvariantFailure(callback_name + " fired for unexpected media publication: " + track_name); } - auto* participant = room.remoteParticipant(expected_publisher_identity); - if (!hasPublication(participant, track_name, kind)) { + auto participant = room.remoteParticipant(expected_publisher_identity).lock(); + if (!hasPublication(participant.get(), track_name, kind)) { recordInvariantFailure(callback_name + " fired before expected remote publication was visible: " + track_name); } } @@ -364,12 +364,12 @@ TEST_P(LateJoinTrackPublicationIntegrationTest, ConsumerReceivesAlreadyPublished Room publisher_room; ASSERT_TRUE(publisher_room.connect(config_.url, config_.token_a, options)) << "Publisher failed to connect"; - ASSERT_NE(publisher_room.localParticipant(), nullptr); + ASSERT_FALSE(publisher_room.localParticipant().expired()); - const std::string publisher_identity = publisher_room.localParticipant()->identity(); + const std::string publisher_identity = lockLocalParticipant(publisher_room)->identity(); ASSERT_FALSE(publisher_identity.empty()); - PublishedTrackGuard published_tracks(publisher_room.localParticipant()); + PublishedTrackGuard published_tracks(lockLocalParticipant(publisher_room).get()); MediaLoopGuard media_loops; std::vector expected_media; @@ -380,7 +380,7 @@ TEST_P(LateJoinTrackPublicationIntegrationTest, ConsumerReceivesAlreadyPublished TrackPublishOptions publish_options; publish_options.source = TrackSource::SOURCE_MICROPHONE; - ASSERT_NO_THROW(publisher_room.localParticipant()->publishTrack(track, publish_options)); + ASSERT_NO_THROW(lockLocalParticipant(publisher_room)->publishTrack(track, publish_options)); ASSERT_NE(track->publication(), nullptr) << "Audio track was not locally published"; published_tracks.addMediaTrack(track, track->publication()->sid()); @@ -398,7 +398,7 @@ TEST_P(LateJoinTrackPublicationIntegrationTest, ConsumerReceivesAlreadyPublished consumer_room.setDelegate(&delegate); ASSERT_TRUE(consumer_room.connect(config_.url, config_.token_b, options)) << "Consumer failed to connect"; - ASSERT_NE(consumer_room.localParticipant(), nullptr); + ASSERT_FALSE(consumer_room.localParticipant().expired()); ASSERT_TRUE(waitForParticipant(&consumer_room, publisher_identity, 10s)) << "Publisher not visible to late-joining consumer"; @@ -436,7 +436,7 @@ TEST_P(LateJoinTrackPublicationIntegrationTest, ConsumerReceivesAlreadyPublished EXPECT_EQ(subscribed_media_counts[expected.name], 1) << "Unexpected onTrackSubscribed count for " << expected.name; } - auto* publisher_on_consumer = consumer_room.remoteParticipant(publisher_identity); + auto publisher_on_consumer = consumer_room.remoteParticipant(publisher_identity).lock(); ASSERT_NE(publisher_on_consumer, nullptr); std::map remote_publications; @@ -468,12 +468,12 @@ TEST_P(LateJoinTrackPublicationIntegrationTest, ConsumerReceivesAlreadyPublished Room publisher_room; ASSERT_TRUE(publisher_room.connect(config_.url, config_.token_a, options)) << "Publisher failed to connect"; - ASSERT_NE(publisher_room.localParticipant(), nullptr); + ASSERT_FALSE(publisher_room.localParticipant().expired()); - const std::string publisher_identity = publisher_room.localParticipant()->identity(); + const std::string publisher_identity = lockLocalParticipant(publisher_room)->identity(); ASSERT_FALSE(publisher_identity.empty()); - PublishedTrackGuard published_tracks(publisher_room.localParticipant()); + PublishedTrackGuard published_tracks(lockLocalParticipant(publisher_room).get()); MediaLoopGuard media_loops; std::vector expected_media; @@ -485,7 +485,7 @@ TEST_P(LateJoinTrackPublicationIntegrationTest, ConsumerReceivesAlreadyPublished publish_options.source = TrackSource::SOURCE_CAMERA; publish_options.simulcast = false; - ASSERT_NO_THROW(publisher_room.localParticipant()->publishTrack(track, publish_options)); + ASSERT_NO_THROW(lockLocalParticipant(publisher_room)->publishTrack(track, publish_options)); ASSERT_NE(track->publication(), nullptr) << "Video track was not locally published"; published_tracks.addMediaTrack(track, track->publication()->sid()); @@ -503,7 +503,7 @@ TEST_P(LateJoinTrackPublicationIntegrationTest, ConsumerReceivesAlreadyPublished consumer_room.setDelegate(&delegate); ASSERT_TRUE(consumer_room.connect(config_.url, config_.token_b, options)) << "Consumer failed to connect"; - ASSERT_NE(consumer_room.localParticipant(), nullptr); + ASSERT_FALSE(consumer_room.localParticipant().expired()); ASSERT_TRUE(waitForParticipant(&consumer_room, publisher_identity, 10s)) << "Publisher not visible to late-joining consumer"; @@ -541,7 +541,7 @@ TEST_P(LateJoinTrackPublicationIntegrationTest, ConsumerReceivesAlreadyPublished EXPECT_EQ(subscribed_media_counts[expected.name], 1) << "Unexpected onTrackSubscribed count for " << expected.name; } - auto* publisher_on_consumer = consumer_room.remoteParticipant(publisher_identity); + auto publisher_on_consumer = consumer_room.remoteParticipant(publisher_identity).lock(); ASSERT_NE(publisher_on_consumer, nullptr); std::map remote_publications; @@ -573,17 +573,17 @@ TEST_P(LateJoinTrackPublicationIntegrationTest, ConsumerReceivesAlreadyPublished Room publisher_room; ASSERT_TRUE(publisher_room.connect(config_.url, config_.token_a, options)) << "Publisher failed to connect"; - ASSERT_NE(publisher_room.localParticipant(), nullptr); + ASSERT_FALSE(publisher_room.localParticipant().expired()); - const std::string publisher_identity = publisher_room.localParticipant()->identity(); + const std::string publisher_identity = lockLocalParticipant(publisher_room)->identity(); ASSERT_FALSE(publisher_identity.empty()); - PublishedTrackGuard published_tracks(publisher_room.localParticipant()); + PublishedTrackGuard published_tracks(lockLocalParticipant(publisher_room).get()); std::set expected_data; for (int i = 0; i < kDataTrackCount; ++i) { const std::string track_name = makeTrackName("late-join-data", i); - auto publish_result = publisher_room.localParticipant()->publishDataTrack(track_name); + auto publish_result = lockLocalParticipant(publisher_room)->publishDataTrack(track_name); ASSERT_TRUE(publish_result) << "Failed to publish data track " << track_name << ": " << publish_result.error().message; @@ -602,7 +602,7 @@ TEST_P(LateJoinTrackPublicationIntegrationTest, ConsumerReceivesAlreadyPublished consumer_room.setDelegate(&delegate); ASSERT_TRUE(consumer_room.connect(config_.url, config_.token_b, options)) << "Consumer failed to connect"; - ASSERT_NE(consumer_room.localParticipant(), nullptr); + ASSERT_FALSE(consumer_room.localParticipant().expired()); ASSERT_TRUE(waitForParticipant(&consumer_room, publisher_identity, 10s)) << "Publisher not visible to late-joining consumer"; diff --git a/src/tests/integration/test_media_multistream.cpp b/src/tests/integration/test_media_multistream.cpp index 0bd4ee5f..d23b75f6 100644 --- a/src/tests/integration/test_media_multistream.cpp +++ b/src/tests/integration/test_media_multistream.cpp @@ -101,8 +101,7 @@ void MediaMultiStreamIntegrationTest::runPublishTwoVideoAndTwoAudioTracks(bool s auto sender_room = std::make_unique(); ASSERT_TRUE(sender_room->connect(config_.url, config_.token_a, options)) << "Sender failed to connect"; - const std::string receiver_identity = receiver_room->localParticipant()->identity(); - const std::string sender_identity = sender_room->localParticipant()->identity(); + const std::string sender_identity = lockLocalParticipant(*sender_room)->identity(); constexpr int kVideoTrackCount = 10; constexpr int kAudioTrackCount = 10; @@ -126,7 +125,7 @@ void MediaMultiStreamIntegrationTest::runPublishTwoVideoAndTwoAudioTracks(bool s auto track = LocalVideoTrack::createLocalVideoTrack(name, source); TrackPublishOptions opts; opts.source = (i % 2 == 0) ? TrackSource::SOURCE_CAMERA : TrackSource::SOURCE_SCREENSHARE; - sender_room->localParticipant()->publishTrack(track, opts); + lockLocalParticipant(*sender_room)->publishTrack(track, opts); std::cerr << "[MediaMultiStream] published video " << name << " sid=" << track->sid() << std::endl; video_sources.push_back(source); video_tracks.push_back(track); @@ -139,7 +138,7 @@ void MediaMultiStreamIntegrationTest::runPublishTwoVideoAndTwoAudioTracks(bool s auto track = LocalAudioTrack::createLocalAudioTrack(name, source); TrackPublishOptions opts; opts.source = (i % 2 == 0) ? TrackSource::SOURCE_MICROPHONE : TrackSource::SOURCE_SCREENSHARE_AUDIO; - sender_room->localParticipant()->publishTrack(track, opts); + lockLocalParticipant(*sender_room)->publishTrack(track, opts); std::cerr << "[MediaMultiStream] published audio " << name << " sid=" << track->sid() << std::endl; audio_sources.push_back(source); audio_tracks.push_back(track); @@ -188,7 +187,7 @@ void MediaMultiStreamIntegrationTest::runPublishTwoVideoAndTwoAudioTracks(bool s EXPECT_GE(receiver_state.audio_tracks, kAudioTrackCount); } - auto* sender_on_receiver = receiver_room->remoteParticipant(sender_identity); + auto sender_on_receiver = receiver_room->remoteParticipant(sender_identity).lock(); ASSERT_NE(sender_on_receiver, nullptr); std::cerr << "[MediaMultiStream] receiver sees sender publications=" << sender_on_receiver->trackPublications().size() << std::endl; @@ -210,12 +209,12 @@ void MediaMultiStreamIntegrationTest::runPublishTwoVideoAndTwoAudioTracks(bool s for (const auto& track : video_tracks) { if (track->publication()) { - sender_room->localParticipant()->unpublishTrack(track->publication()->sid()); + lockLocalParticipant(*sender_room)->unpublishTrack(track->publication()->sid()); } } for (const auto& track : audio_tracks) { if (track->publication()) { - sender_room->localParticipant()->unpublishTrack(track->publication()->sid()); + lockLocalParticipant(*sender_room)->unpublishTrack(track->publication()->sid()); } } } diff --git a/src/tests/integration/test_room.cpp b/src/tests/integration/test_room.cpp index 9fb2490d..184aa9f9 100644 --- a/src/tests/integration/test_room.cpp +++ b/src/tests/integration/test_room.cpp @@ -17,13 +17,19 @@ #include #include +#include +#include +#include + +#include "../common/test_common.h" + namespace livekit::test { // Server-dependent tests - require LIVEKIT_URL and LIVEKIT_TOKEN_A env vars class RoomTest : public ::testing::Test { protected: void SetUp() override { - livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); + livekit::initialize(livekit::LogLevel::Info); const char* url_env = std::getenv("LIVEKIT_URL"); const char* token_env = std::getenv("LIVEKIT_TOKEN_A"); @@ -50,7 +56,7 @@ TEST_F(RoomTest, ConnectToServer) { EXPECT_TRUE(connected) << "Should connect to server successfully"; if (connected) { - EXPECT_NE(room.localParticipant(), nullptr) << "Local participant should exist after connect"; + EXPECT_FALSE(room.localParticipant().expired()) << "Local participant should exist after connect"; } } @@ -70,4 +76,61 @@ TEST_F(RoomTest, ConnectWithInvalidUrl) { EXPECT_FALSE(connected) << "Should fail to connect to invalid URL"; } +// Verifies that participant handles handed out by Room expire once the Room is +// destroyed. Because the accessors return std::weak_ptr and the Room is the +// sole owner of the participant shared_ptrs, a consumer that caches the handles +// can detect teardown via expired()/lock() == nullptr. Requires a second peer +// (TOKEN_B) so the room under test has a remote participant to observe. +class RoomLifecycleTest : public LiveKitTestBase {}; + +TEST_F(RoomLifecycleTest, ParticipantHandlesExpireOnRoomDestruction) { + if (!config_.available) { + throw std::runtime_error("RoomLifecycleTest: test configuration not set up"); + } + + RoomOptions options; + options.auto_subscribe = true; + + // 1. Connect the room under test plus a peer so a remote participant exists. + auto room = std::make_unique(); + ASSERT_TRUE(room->connect(config_.url, config_.token_a, options)) << "Room failed to connect"; + + auto peer = std::make_unique(); + ASSERT_TRUE(peer->connect(config_.url, config_.token_b, options)) << "Peer failed to connect"; + + ASSERT_FALSE(peer->localParticipant().expired()); + const std::string peer_identity = lockLocalParticipant(*peer)->identity(); + ASSERT_TRUE(waitForParticipant(room.get(), peer_identity, 10s)) << "Peer not visible to room"; + + // 2. Store the local participant handle. Keep the weak_ptr itself - locking + // it here would co-own the participant and keep it alive past teardown, + // defeating the check. + std::weak_ptr local_handle = room->localParticipant(); + ASSERT_FALSE(local_handle.expired()) << "Local participant should be live while connected"; + + // 3. Store the remote participant handles (again, as weak_ptr). + std::vector> remote_handles = room->remoteParticipants(); + ASSERT_FALSE(remote_handles.empty()) << "Expected at least one remote participant"; + for (const auto& handle : remote_handles) { + EXPECT_FALSE(handle.expired()) << "Remote participant should be live while connected"; + } + std::weak_ptr remote_by_identity = room->remoteParticipant(peer_identity); + ASSERT_FALSE(remote_by_identity.expired()); + + // 4. Destroy the room. + room.reset(); + + // 5. Validate every cached handle now reports as expired / null. + EXPECT_TRUE(local_handle.expired()); + EXPECT_EQ(local_handle.lock(), nullptr); + EXPECT_TRUE(remote_by_identity.expired()); + EXPECT_EQ(remote_by_identity.lock(), nullptr); + for (const auto& handle : remote_handles) { + EXPECT_TRUE(handle.expired()); + EXPECT_EQ(handle.lock(), nullptr); + } + + peer.reset(); +} + } // namespace livekit::test diff --git a/src/tests/integration/test_room_listener_cleanup.cpp b/src/tests/integration/test_room_listener_cleanup.cpp index 912df3b0..316ae0a0 100644 --- a/src/tests/integration/test_room_listener_cleanup.cpp +++ b/src/tests/integration/test_room_listener_cleanup.cpp @@ -86,12 +86,12 @@ void expectFailedConnectDoesNotDuplicateParticipantCallbacks(const TestConfig& c EXPECT_FALSE(observed_room.connect(failed_url, failed_token, options)) << "Initial failing connect() should fail"; ASSERT_TRUE(observed_room.connect(config.url, config.token_a, options)) << "Reconnect after failed connect() failed"; - ASSERT_NE(observed_room.localParticipant(), nullptr); + ASSERT_FALSE(observed_room.localParticipant().expired()); Room peer_room; ASSERT_TRUE(peer_room.connect(config.url, config.token_b, options)) << "Peer failed to connect"; - ASSERT_NE(peer_room.localParticipant(), nullptr); - const std::string peer_identity = peer_room.localParticipant()->identity(); + ASSERT_FALSE(peer_room.localParticipant().expired()); + const std::string peer_identity = lockLocalParticipant(peer_room)->identity(); ASSERT_FALSE(peer_identity.empty()); expectSingleParticipantConnectedCallback(counter, peer_identity); @@ -130,14 +130,14 @@ TEST_F(RoomListenerCleanupIntegrationTest, AlreadyConnectedConnectDoesNotReplace observed_room.setDelegate(&counter); ASSERT_TRUE(observed_room.connect(config_.url, config_.token_a, options)) << "Initial connect() failed"; - ASSERT_NE(observed_room.localParticipant(), nullptr); + ASSERT_FALSE(observed_room.localParticipant().expired()); EXPECT_THROW((void)observed_room.connect(config_.url, config_.token_a, options), std::runtime_error); Room peer_room; ASSERT_TRUE(peer_room.connect(config_.url, config_.token_b, options)) << "Peer failed to connect"; - ASSERT_NE(peer_room.localParticipant(), nullptr); - const std::string peer_identity = peer_room.localParticipant()->identity(); + ASSERT_FALSE(peer_room.localParticipant().expired()); + const std::string peer_identity = lockLocalParticipant(peer_room)->identity(); ASSERT_FALSE(peer_identity.empty()); expectSingleParticipantConnectedCallback(counter, peer_identity); diff --git a/src/tests/integration/test_rpc.cpp b/src/tests/integration/test_rpc.cpp index 18353244..cde9ee00 100644 --- a/src/tests/integration/test_rpc.cpp +++ b/src/tests/integration/test_rpc.cpp @@ -93,7 +93,7 @@ std::string generateRandomPayload(size_t size) { class RpcIntegrationTest : public ::testing::Test { protected: void SetUp() override { - livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); + livekit::initialize(livekit::LogLevel::Info); config_ = RpcTestConfig::fromEnv(); } @@ -116,13 +116,13 @@ TEST_F(RpcIntegrationTest, BasicRpcRoundTrip) { bool receiver_connected = receiver_room->connect(config_.url, config_.token_b, receiver_options); ASSERT_TRUE(receiver_connected) << "Receiver failed to connect"; - std::string receiver_identity = receiver_room->localParticipant()->identity(); + std::string receiver_identity = lockLocalParticipant(*receiver_room)->identity(); // Register RPC handler on receiver - returns size and checksum instead of // full payload std::atomic rpc_calls_received{0}; - receiver_room->localParticipant()->registerRpcMethod( - "echo", [&rpc_calls_received](const RpcInvocationData& data) -> std::optional { + lockLocalParticipant(*receiver_room) + ->registerRpcMethod("echo", [&rpc_calls_received](const RpcInvocationData& data) -> std::optional { rpc_calls_received++; size_t checksum = 0; for (char c : data.payload) { @@ -145,7 +145,7 @@ TEST_F(RpcIntegrationTest, BasicRpcRoundTrip) { // Perform RPC call std::string test_payload = "hello world"; - std::string response = caller_room->localParticipant()->performRpc(receiver_identity, "echo", test_payload, 10.0); + std::string response = lockLocalParticipant(*caller_room)->performRpc(receiver_identity, "echo", test_payload, 10.0); // Verify response contains correct size and checksum size_t expected_checksum = 0; @@ -158,7 +158,7 @@ TEST_F(RpcIntegrationTest, BasicRpcRoundTrip) { EXPECT_EQ(rpc_calls_received.load(), 1); // Cleanup - receiver_room->localParticipant()->unregisterRpcMethod("echo"); + lockLocalParticipant(*receiver_room)->unregisterRpcMethod("echo"); caller_room.reset(); receiver_room.reset(); } @@ -176,12 +176,13 @@ TEST_F(RpcIntegrationTest, MaxPayloadSize) { bool receiver_connected = receiver_room->connect(config_.url, config_.token_b, options); ASSERT_TRUE(receiver_connected) << "Receiver failed to connect"; - std::string receiver_identity = receiver_room->localParticipant()->identity(); + std::string receiver_identity = lockLocalParticipant(*receiver_room)->identity(); // Register handler that echoes payload size - receiver_room->localParticipant()->registerRpcMethod( - "payload-size", - [](const RpcInvocationData& data) -> std::optional { return std::to_string(data.payload.size()); }); + lockLocalParticipant(*receiver_room) + ->registerRpcMethod("payload-size", [](const RpcInvocationData& data) -> std::optional { + return std::to_string(data.payload.size()); + }); auto caller_room = std::make_unique(); bool caller_connected = caller_room->connect(config_.url, config_.token_a, options); @@ -193,11 +194,11 @@ TEST_F(RpcIntegrationTest, MaxPayloadSize) { // Test with max payload size (15KB) std::string max_payload = generateRandomPayload(kMaxRpcPayloadSize); std::string response = - caller_room->localParticipant()->performRpc(receiver_identity, "payload-size", max_payload, 30.0); + lockLocalParticipant(*caller_room)->performRpc(receiver_identity, "payload-size", max_payload, 30.0); EXPECT_EQ(response, std::to_string(kMaxRpcPayloadSize)); - receiver_room->localParticipant()->unregisterRpcMethod("payload-size"); + lockLocalParticipant(*receiver_room)->unregisterRpcMethod("payload-size"); caller_room.reset(); receiver_room.reset(); } @@ -215,14 +216,14 @@ TEST_F(RpcIntegrationTest, RpcTimeout) { bool receiver_connected = receiver_room->connect(config_.url, config_.token_b, options); ASSERT_TRUE(receiver_connected) << "Receiver failed to connect"; - std::string receiver_identity = receiver_room->localParticipant()->identity(); + std::string receiver_identity = lockLocalParticipant(*receiver_room)->identity(); // Register handler that takes too long - receiver_room->localParticipant()->registerRpcMethod("slow-method", - [](const RpcInvocationData&) -> std::optional { - std::this_thread::sleep_for(10s); - return "done"; - }); + lockLocalParticipant(*receiver_room) + ->registerRpcMethod("slow-method", [](const RpcInvocationData&) -> std::optional { + std::this_thread::sleep_for(10s); + return "done"; + }); auto caller_room = std::make_unique(); bool caller_connected = caller_room->connect(config_.url, config_.token_a, options); @@ -232,9 +233,10 @@ TEST_F(RpcIntegrationTest, RpcTimeout) { ASSERT_TRUE(receiver_visible) << "Receiver not visible to caller"; // Call with short timeout - should fail - EXPECT_THROW({ caller_room->localParticipant()->performRpc(receiver_identity, "slow-method", "", 2.0); }, RpcError); + EXPECT_THROW( + { lockLocalParticipant(*caller_room)->performRpc(receiver_identity, "slow-method", "", 2.0); }, RpcError); - receiver_room->localParticipant()->unregisterRpcMethod("slow-method"); + lockLocalParticipant(*receiver_room)->unregisterRpcMethod("slow-method"); caller_room.reset(); receiver_room.reset(); } @@ -252,7 +254,7 @@ TEST_F(RpcIntegrationTest, UnsupportedMethod) { bool receiver_connected = receiver_room->connect(config_.url, config_.token_b, options); ASSERT_TRUE(receiver_connected) << "Receiver failed to connect"; - std::string receiver_identity = receiver_room->localParticipant()->identity(); + std::string receiver_identity = lockLocalParticipant(*receiver_room)->identity(); auto caller_room = std::make_unique(); bool caller_connected = caller_room->connect(config_.url, config_.token_a, options); @@ -263,7 +265,7 @@ TEST_F(RpcIntegrationTest, UnsupportedMethod) { // Call unregistered method try { - caller_room->localParticipant()->performRpc(receiver_identity, "nonexistent-method", "", 5.0); + lockLocalParticipant(*caller_room)->performRpc(receiver_identity, "nonexistent-method", "", 5.0); FAIL() << "Expected RpcError for unsupported method"; } catch (const RpcError& e) { EXPECT_EQ(static_cast(e.code()), RpcError::ErrorCode::UNSUPPORTED_METHOD); @@ -286,12 +288,13 @@ TEST_F(RpcIntegrationTest, ApplicationError) { bool receiver_connected = receiver_room->connect(config_.url, config_.token_b, options); ASSERT_TRUE(receiver_connected) << "Receiver failed to connect"; - std::string receiver_identity = receiver_room->localParticipant()->identity(); + std::string receiver_identity = lockLocalParticipant(*receiver_room)->identity(); // Register handler that throws an error - receiver_room->localParticipant()->registerRpcMethod( - "error-method", - [](const RpcInvocationData&) -> std::optional { throw std::runtime_error("intentional error"); }); + lockLocalParticipant(*receiver_room) + ->registerRpcMethod("error-method", [](const RpcInvocationData&) -> std::optional { + throw std::runtime_error("intentional error"); + }); auto caller_room = std::make_unique(); bool caller_connected = caller_room->connect(config_.url, config_.token_a, options); @@ -301,13 +304,13 @@ TEST_F(RpcIntegrationTest, ApplicationError) { ASSERT_TRUE(receiver_visible) << "Receiver not visible to caller"; try { - caller_room->localParticipant()->performRpc(receiver_identity, "error-method", "", 5.0); + lockLocalParticipant(*caller_room)->performRpc(receiver_identity, "error-method", "", 5.0); FAIL() << "Expected RpcError for application error"; } catch (const RpcError& e) { EXPECT_EQ(static_cast(e.code()), RpcError::ErrorCode::APPLICATION_ERROR); } - receiver_room->localParticipant()->unregisterRpcMethod("error-method"); + lockLocalParticipant(*receiver_room)->unregisterRpcMethod("error-method"); caller_room.reset(); receiver_room.reset(); } @@ -325,11 +328,11 @@ TEST_F(RpcIntegrationTest, ConcurrentRpcCalls) { bool receiver_connected = receiver_room->connect(config_.url, config_.token_b, options); ASSERT_TRUE(receiver_connected) << "Receiver failed to connect"; - std::string receiver_identity = receiver_room->localParticipant()->identity(); + std::string receiver_identity = lockLocalParticipant(*receiver_room)->identity(); std::atomic calls_processed{0}; - receiver_room->localParticipant()->registerRpcMethod( - "counter", [&calls_processed](const RpcInvocationData& data) -> std::optional { + lockLocalParticipant(*receiver_room) + ->registerRpcMethod("counter", [&calls_processed](const RpcInvocationData& data) -> std::optional { int id = std::stoi(data.payload); calls_processed++; std::this_thread::sleep_for(100ms); // Simulate some work @@ -343,6 +346,10 @@ TEST_F(RpcIntegrationTest, ConcurrentRpcCalls) { bool receiver_visible = waitForParticipant(caller_room.get(), receiver_identity, 10s); ASSERT_TRUE(receiver_visible) << "Receiver not visible to caller"; + // Hold the local participant alive for the duration of the worker threads so + // it cannot expire mid-call while RPCs are in flight. + ASSERT_NO_THROW(lockLocalParticipant(*caller_room)); + const int num_concurrent_calls = 10; std::vector threads; std::atomic successful_calls{0}; @@ -350,8 +357,9 @@ TEST_F(RpcIntegrationTest, ConcurrentRpcCalls) { for (int i = 0; i < num_concurrent_calls; ++i) { threads.emplace_back([&, i]() { try { - std::string response = - caller_room->localParticipant()->performRpc(receiver_identity, "counter", std::to_string(i), 30.0); + auto caller_lp = lockLocalParticipant(*caller_room); + ASSERT_NE(caller_lp, nullptr); + std::string response = caller_lp->performRpc(receiver_identity, "counter", std::to_string(i), 30.0); int expected = i * 2; if (std::stoi(response) == expected) { successful_calls++; @@ -369,7 +377,7 @@ TEST_F(RpcIntegrationTest, ConcurrentRpcCalls) { EXPECT_EQ(successful_calls.load(), num_concurrent_calls); EXPECT_EQ(calls_processed.load(), num_concurrent_calls); - receiver_room->localParticipant()->unregisterRpcMethod("counter"); + lockLocalParticipant(*receiver_room)->unregisterRpcMethod("counter"); caller_room.reset(); receiver_room.reset(); } @@ -387,13 +395,13 @@ TEST_F(RpcIntegrationTest, OneMinuteIntegration) { bool receiver_connected = receiver_room->connect(config_.url, config_.token_b, options); ASSERT_TRUE(receiver_connected) << "Receiver failed to connect"; - std::string receiver_identity = receiver_room->localParticipant()->identity(); + std::string receiver_identity = lockLocalParticipant(*receiver_room)->identity(); std::atomic total_received{0}; std::atomic total_bytes_received{0}; - receiver_room->localParticipant()->registerRpcMethod( - "integration-test", [&](const RpcInvocationData& data) -> std::optional { + lockLocalParticipant(*receiver_room) + ->registerRpcMethod("integration-test", [&](const RpcInvocationData& data) -> std::optional { total_received++; total_bytes_received += data.payload.size(); return "ack:" + std::to_string(data.payload.size()); @@ -406,6 +414,10 @@ TEST_F(RpcIntegrationTest, OneMinuteIntegration) { bool receiver_visible = waitForParticipant(caller_room.get(), receiver_identity, 10s); ASSERT_TRUE(receiver_visible) << "Receiver not visible to caller"; + // Hold the local participant alive for the duration of the sender thread so + // it cannot expire mid-call while RPCs are in flight. + ASSERT_NO_THROW(lockLocalParticipant(*caller_room)); + // Run for 1 minute const auto test_duration = 60s; const auto start_time = std::chrono::steady_clock::now(); @@ -425,8 +437,9 @@ TEST_F(RpcIntegrationTest, OneMinuteIntegration) { std::string payload = generateRandomPayload(payload_size); try { - std::string response = - caller_room->localParticipant()->performRpc(receiver_identity, "integration-test", payload, 30.0); + auto caller_lp = lockLocalParticipant(*caller_room); + ASSERT_NE(caller_lp, nullptr); + std::string response = caller_lp->performRpc(receiver_identity, "integration-test", payload, 30.0); if (response == "ack:" + std::to_string(payload_size)) { successful_calls++; } @@ -460,7 +473,7 @@ TEST_F(RpcIntegrationTest, OneMinuteIntegration) { EXPECT_GT(successful_calls.load(), 0); EXPECT_EQ(total_sent.load(), total_received.load()); - receiver_room->localParticipant()->unregisterRpcMethod("integration-test"); + lockLocalParticipant(*receiver_room)->unregisterRpcMethod("integration-test"); caller_room.reset(); receiver_room.reset(); } diff --git a/src/tests/integration/test_session_stats.cpp b/src/tests/integration/test_session_stats.cpp index 4bd88c56..1035ae4a 100644 --- a/src/tests/integration/test_session_stats.cpp +++ b/src/tests/integration/test_session_stats.cpp @@ -163,7 +163,7 @@ TEST_F(SessionStatsIntegrationTest, PublishAudioThenFetchSessionStats) { auto track = LocalAudioTrack::createLocalAudioTrack("session-stats-audio", source); TrackPublishOptions opts; opts.source = TrackSource::SOURCE_MICROPHONE; - sender_room->localParticipant()->publishTrack(track, opts); + lockLocalParticipant(*sender_room)->publishTrack(track, opts); std::cerr << "[SessionStats] published audio track sid=" << track->sid() << std::endl; std::atomic running{true}; @@ -184,7 +184,7 @@ TEST_F(SessionStatsIntegrationTest, PublishAudioThenFetchSessionStats) { audio_thread.join(); } if (track->publication()) { - sender_room->localParticipant()->unpublishTrack(track->publication()->sid()); + lockLocalParticipant(*sender_room)->unpublishTrack(track->publication()->sid()); } printSessionStats("sender", sender_stats); diff --git a/src/tests/integration/test_video_frame_metadata.cpp b/src/tests/integration/test_video_frame_metadata.cpp index 58625afa..3c205454 100644 --- a/src/tests/integration/test_video_frame_metadata.cpp +++ b/src/tests/integration/test_video_frame_metadata.cpp @@ -38,10 +38,10 @@ TEST_F(VideoFrameMetadataServerTest, UserTimestampRoundTripsToReceiverEventCallb ASSERT_TRUE(receiver_room.connect(config_.url, config_.token_b, options)); ASSERT_TRUE(sender_room.connect(config_.url, config_.token_a, options)); - ASSERT_NE(sender_room.localParticipant(), nullptr); - ASSERT_NE(receiver_room.localParticipant(), nullptr); + ASSERT_FALSE(sender_room.localParticipant().expired()); + ASSERT_FALSE(receiver_room.localParticipant().expired()); - const std::string sender_identity = sender_room.localParticipant()->identity(); + const std::string sender_identity = lockLocalParticipant(sender_room)->identity(); ASSERT_FALSE(sender_identity.empty()); ASSERT_TRUE(waitForParticipant(&receiver_room, sender_identity, 10s)); @@ -71,12 +71,12 @@ TEST_F(VideoFrameMetadataServerTest, UserTimestampRoundTripsToReceiverEventCallb publish_options.simulcast = false; publish_options.packet_trailer_features.user_timestamp = true; - ASSERT_NO_THROW(sender_room.localParticipant()->publishTrack(track, publish_options)); + ASSERT_NO_THROW(lockLocalParticipant(sender_room)->publishTrack(track, publish_options)); const auto track_ready_deadline = std::chrono::steady_clock::now() + 10s; bool receiver_track_ready = false; while (std::chrono::steady_clock::now() < track_ready_deadline) { - auto* sender_on_receiver = receiver_room.remoteParticipant(sender_identity); + auto sender_on_receiver = receiver_room.remoteParticipant(sender_identity).lock(); if (sender_on_receiver != nullptr) { for (const auto& [sid, publication] : sender_on_receiver->trackPublications()) { (void)sid; @@ -144,7 +144,7 @@ TEST_F(VideoFrameMetadataServerTest, UserTimestampRoundTripsToReceiverEventCallb receiver_room.clearOnVideoFrameCallback(sender_identity, track_name); if (track->publication()) { - sender_room.localParticipant()->unpublishTrack(track->publication()->sid()); + lockLocalParticipant(sender_room)->unpublishTrack(track->publication()->sid()); } ASSERT_TRUE(got_metadata) << "Timed out waiting for user timestamp metadata"; diff --git a/src/tests/stress/test_audio_frame_stress.cpp b/src/tests/stress/test_audio_frame_stress.cpp index 88f811d0..3d4e8550 100644 --- a/src/tests/stress/test_audio_frame_stress.cpp +++ b/src/tests/stress/test_audio_frame_stress.cpp @@ -27,7 +27,7 @@ namespace livekit::test { class AudioFrameStressTest : public ::testing::Test { protected: - void SetUp() override { livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); } + void SetUp() override { livekit::initialize(livekit::LogLevel::Info); } void TearDown() override { livekit::shutdown(); } }; diff --git a/src/tests/stress/test_latency_measurement.cpp b/src/tests/stress/test_latency_measurement.cpp index 5091254d..2c9e1fb6 100644 --- a/src/tests/stress/test_latency_measurement.cpp +++ b/src/tests/stress/test_latency_measurement.cpp @@ -259,7 +259,8 @@ TEST_F(LatencyMeasurementTest, ConnectionTime) { // Get room and participant session IDs for debugging auto room_info = room->roomInfo(); std::string room_sid = room_info.sid.has_value() ? room_info.sid.value() : "unknown"; - std::string participant_sid = room->localParticipant() ? room->localParticipant()->sid() : "unknown"; + auto latency_local_participant = room->localParticipant().lock(); + std::string participant_sid = latency_local_participant ? latency_local_participant->sid() : "unknown"; std::cout << " Iteration " << (i + 1) << ": " << std::fixed << std::setprecision(2) << latency_ms << " ms" << " | participant_sid=" << participant_sid << " | room_sid=" << room_sid << std::endl; @@ -337,7 +338,7 @@ TEST_F(LatencyMeasurementTest, AudioLatency) { bool receiver_connected = receiver_room->connect(config_.url, config_.token_b, options); ASSERT_TRUE(receiver_connected) << "Receiver failed to connect"; - std::string receiver_identity = receiver_room->localParticipant()->identity(); + std::string receiver_identity = lockLocalParticipant(*receiver_room)->identity(); std::cout << "Receiver connected as: " << receiver_identity << std::endl; // Create sender room (using token_a) @@ -345,7 +346,7 @@ TEST_F(LatencyMeasurementTest, AudioLatency) { bool sender_connected = sender_room->connect(config_.url, config_.token_a, options); ASSERT_TRUE(sender_connected) << "Sender failed to connect"; - std::string sender_identity = sender_room->localParticipant()->identity(); + std::string sender_identity = lockLocalParticipant(*sender_room)->identity(); std::cout << "Sender connected as: " << sender_identity << std::endl; // Wait for sender to be visible to receiver @@ -356,7 +357,7 @@ TEST_F(LatencyMeasurementTest, AudioLatency) { auto audio_track = LocalAudioTrack::createLocalAudioTrack("latency-test", audio_source); TrackPublishOptions publish_options; - sender_room->localParticipant()->publishTrack(audio_track, publish_options); + lockLocalParticipant(*sender_room)->publishTrack(audio_track, publish_options); ASSERT_NE(audio_track->publication(), nullptr) << "Failed to publish audio track"; std::cout << "Audio track published, waiting for subscription..." << std::endl; @@ -492,7 +493,7 @@ TEST_F(LatencyMeasurementTest, AudioLatency) { } // Clean up - sender_room->localParticipant()->unpublishTrack(audio_track->publication()->sid()); + lockLocalParticipant(*sender_room)->unpublishTrack(audio_track->publication()->sid()); // Tracing is automatically handled by LiveKitTestBase // Stats for audio_latency will be printed in TearDown() @@ -524,8 +525,8 @@ TEST_F(LatencyMeasurementTest, FullDeplexAudioLatency) { ASSERT_TRUE(room_a->connect(config_.url, config_.token_a, options)) << "Participant A failed to connect"; ASSERT_TRUE(room_b->connect(config_.url, config_.token_b, options)) << "Participant B failed to connect"; - std::string id_a = room_a->localParticipant()->identity(); - std::string id_b = room_b->localParticipant()->identity(); + std::string id_a = lockLocalParticipant(*room_a)->identity(); + std::string id_b = lockLocalParticipant(*room_b)->identity(); std::cout << "Participant A: " << id_a << std::endl; std::cout << "Participant B: " << id_b << std::endl; @@ -540,8 +541,8 @@ TEST_F(LatencyMeasurementTest, FullDeplexAudioLatency) { ASSERT_NE(track_b, nullptr); TrackPublishOptions publish_options; - room_a->localParticipant()->publishTrack(track_a, publish_options); - room_b->localParticipant()->publishTrack(track_b, publish_options); + lockLocalParticipant(*room_a)->publishTrack(track_a, publish_options); + lockLocalParticipant(*room_b)->publishTrack(track_b, publish_options); auto track_from_a_on_b = delegate_b.waitForAudioTrackFromParticipant(id_a, 10s); auto track_from_b_on_a = delegate_a.waitForAudioTrackFromParticipant(id_b, 10s); @@ -764,10 +765,10 @@ TEST_F(LatencyMeasurementTest, FullDeplexAudioLatency) { // Tracing is automatically handled by LiveKitTestBase // Stats for A_to_B, B_to_A, round_trip will be printed in TearDown() if (track_a->publication()) { - room_a->localParticipant()->unpublishTrack(track_a->publication()->sid()); + lockLocalParticipant(*room_a)->unpublishTrack(track_a->publication()->sid()); } if (track_b->publication()) { - room_b->localParticipant()->unpublishTrack(track_b->publication()->sid()); + lockLocalParticipant(*room_b)->unpublishTrack(track_b->publication()->sid()); } EXPECT_GT(round_trip_count.load(), 0) << "At least one round-trip latency measurement should be recorded"; diff --git a/src/tests/stress/test_room_listener_race_stress.cpp b/src/tests/stress/test_room_listener_race_stress.cpp index 51dee819..4c54c9f0 100644 --- a/src/tests/stress/test_room_listener_race_stress.cpp +++ b/src/tests/stress/test_room_listener_race_stress.cpp @@ -93,7 +93,7 @@ TEST_F(RoomListenerRaceStressTest, ConnectFailDestroyReconnectAndPublishDataTrac return; } - auto* local_participant = room.localParticipant(); + auto local_participant = room.localParticipant().lock(); if (local_participant == nullptr) { addStressError(errors, errors_mutex, "local participant missing after valid connect"); return; diff --git a/src/tests/stress/test_room_stress.cpp b/src/tests/stress/test_room_stress.cpp index 0174f315..7563b101 100644 --- a/src/tests/stress/test_room_stress.cpp +++ b/src/tests/stress/test_room_stress.cpp @@ -27,7 +27,7 @@ namespace livekit::test { class RoomStressTest : public ::testing::Test { protected: - void SetUp() override { livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); } + void SetUp() override { livekit::initialize(livekit::LogLevel::Info); } void TearDown() override { livekit::shutdown(); } }; @@ -40,7 +40,7 @@ TEST_F(RoomStressTest, RapidRoomCreation) { for (int i = 0; i < num_iterations; ++i) { Room room; - ASSERT_EQ(room.localParticipant(), nullptr); + ASSERT_TRUE(room.localParticipant().expired()); } auto end = std::chrono::high_resolution_clock::now(); @@ -65,7 +65,7 @@ TEST_F(RoomStressTest, MultipleSimultaneousRooms) { // Verify all rooms are valid for (const auto& room : rooms) { ASSERT_NE(room, nullptr); - ASSERT_EQ(room->localParticipant(), nullptr); + ASSERT_TRUE(room->localParticipant().expired()); } auto end = std::chrono::high_resolution_clock::now(); @@ -89,7 +89,7 @@ TEST_F(RoomStressTest, ConcurrentRoomCreation) { threads.emplace_back([&total_rooms, rooms_per_thread]() { for (int i = 0; i < rooms_per_thread; ++i) { Room room; - if (room.localParticipant() == nullptr) { + if (room.localParticipant().expired()) { total_rooms.fetch_add(1, std::memory_order_relaxed); } } @@ -176,7 +176,7 @@ TEST_F(RoomStressTest, StreamHandlerRegistrationStress) { class RoomServerStressTest : public ::testing::Test { protected: void SetUp() override { - livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); + livekit::initialize(livekit::LogLevel::Info); const char* url_env = std::getenv("LIVEKIT_URL"); const char* token_env = std::getenv("LIVEKIT_TOKEN_A"); @@ -211,7 +211,7 @@ TEST_F(RoomServerStressTest, RepeatedConnectDisconnect) { bool connected = room.connect(server_url_, token_, options); if (connected) { - ASSERT_NE(room.localParticipant(), nullptr); + ASSERT_FALSE(room.localParticipant().expired()); } // Room disconnects when it goes out of scope } diff --git a/src/tests/stress/test_rpc_stress.cpp b/src/tests/stress/test_rpc_stress.cpp index 54d46b98..6b081618 100644 --- a/src/tests/stress/test_rpc_stress.cpp +++ b/src/tests/stress/test_rpc_stress.cpp @@ -166,17 +166,18 @@ TEST_F(RpcStressTest, MaxPayloadStress) { std::cout << "Receiver connected - Room: " << receiver_info.name << " (SID: " << receiver_info.sid.value_or("unknown") << ")" << std::endl; - std::string receiver_identity = receiver_room->localParticipant()->identity(); + std::string receiver_identity = lockLocalParticipant(*receiver_room)->identity(); std::atomic total_received{0}; // Register RPC handler that processes max payloads - receiver_room->localParticipant()->registerRpcMethod( - "max-payload-stress", [&total_received](const RpcInvocationData& data) -> std::optional { - total_received++; - // Echo the payload back for round-trip verification - return data.payload; - }); + lockLocalParticipant(*receiver_room) + ->registerRpcMethod("max-payload-stress", + [&total_received](const RpcInvocationData& data) -> std::optional { + total_received++; + // Echo the payload back for round-trip verification + return data.payload; + }); // Create caller room auto caller_room = std::make_unique(); @@ -202,6 +203,8 @@ TEST_F(RpcStressTest, MaxPayloadStress) { auto start_time = std::chrono::steady_clock::now(); auto duration = std::chrono::seconds(config_.stress_duration_seconds); + ASSERT_NO_THROW(lockLocalParticipant(*caller_room)); + // Create caller threads std::vector caller_threads; for (int t = 0; t < config_.num_caller_threads; ++t) { @@ -219,8 +222,9 @@ TEST_F(RpcStressTest, MaxPayloadStress) { auto call_start = std::chrono::high_resolution_clock::now(); try { - std::string response = - caller_room->localParticipant()->performRpc(receiver_identity, "max-payload-stress", payload, 60.0); + auto caller_lp = lockLocalParticipant(*caller_room); + ASSERT_NE(caller_lp, nullptr); + std::string response = caller_lp->performRpc(receiver_identity, "max-payload-stress", payload, 60.0); auto call_end = std::chrono::high_resolution_clock::now(); double latency_ms = std::chrono::duration(call_end - call_start).count(); @@ -314,7 +318,7 @@ TEST_F(RpcStressTest, MaxPayloadStress) { double success_rate = (stats.totalCalls() > 0) ? (100.0 * stats.successfulCalls() / stats.totalCalls()) : 0.0; EXPECT_GT(success_rate, 95.0) << "Success rate below 95%"; - receiver_room->localParticipant()->unregisterRpcMethod("max-payload-stress"); + lockLocalParticipant(*receiver_room)->unregisterRpcMethod("max-payload-stress"); caller_room.reset(); receiver_room.reset(); } @@ -344,16 +348,17 @@ TEST_F(RpcStressTest, SmallPayloadStress) { std::cout << "Receiver connected - Room: " << receiver_info.name << " (SID: " << receiver_info.sid.value_or("unknown") << ")" << std::endl; - std::string receiver_identity = receiver_room->localParticipant()->identity(); + std::string receiver_identity = lockLocalParticipant(*receiver_room)->identity(); std::atomic total_received{0}; // Register RPC handler - receiver_room->localParticipant()->registerRpcMethod( - "small-payload-stress", [&total_received](const RpcInvocationData& data) -> std::optional { - total_received++; - return data.payload; - }); + lockLocalParticipant(*receiver_room) + ->registerRpcMethod("small-payload-stress", + [&total_received](const RpcInvocationData& data) -> std::optional { + total_received++; + return data.payload; + }); // Create caller room auto caller_room = std::make_unique(); @@ -379,6 +384,9 @@ TEST_F(RpcStressTest, SmallPayloadStress) { auto start_time = std::chrono::steady_clock::now(); auto duration = std::chrono::seconds(config_.stress_duration_seconds); + // Hold the local participant alive for the lifetime of the caller threads. + ASSERT_NO_THROW(lockLocalParticipant(*caller_room)); + // Create caller threads std::vector caller_threads; for (int t = 0; t < config_.num_caller_threads; ++t) { @@ -395,8 +403,9 @@ TEST_F(RpcStressTest, SmallPayloadStress) { auto call_start = std::chrono::high_resolution_clock::now(); try { - std::string response = - caller_room->localParticipant()->performRpc(receiver_identity, "small-payload-stress", payload, 60.0); + auto caller_lp = lockLocalParticipant(*caller_room); + ASSERT_NE(caller_lp, nullptr); + std::string response = caller_lp->performRpc(receiver_identity, "small-payload-stress", payload, 60.0); auto call_end = std::chrono::high_resolution_clock::now(); double latency_ms = std::chrono::duration(call_end - call_start).count(); @@ -485,7 +494,7 @@ TEST_F(RpcStressTest, SmallPayloadStress) { double success_rate = (stats.totalCalls() > 0) ? (100.0 * stats.successfulCalls() / stats.totalCalls()) : 0.0; EXPECT_GT(success_rate, 95.0) << "Success rate below 95%"; - receiver_room->localParticipant()->unregisterRpcMethod("small-payload-stress"); + lockLocalParticipant(*receiver_room)->unregisterRpcMethod("small-payload-stress"); caller_room.reset(); receiver_room.reset(); } @@ -508,8 +517,8 @@ TEST_F(RpcStressTest, BidirectionalRpcStress) { bool b_connected = room_b->connect(config_.url, config_.token_b, options); ASSERT_TRUE(b_connected) << "Room B failed to connect"; - std::string identity_a = room_a->localParticipant()->identity(); - std::string identity_b = room_b->localParticipant()->identity(); + std::string identity_a = lockLocalParticipant(*room_a)->identity(); + std::string identity_b = lockLocalParticipant(*room_b)->identity(); ASSERT_TRUE(waitForParticipant(room_a.get(), identity_b, 10s)) << "Room B not visible to Room A"; ASSERT_TRUE(waitForParticipant(room_b.get(), identity_a, 10s)) << "Room A not visible to Room B"; @@ -518,14 +527,14 @@ TEST_F(RpcStressTest, BidirectionalRpcStress) { std::atomic b_received{0}; // Register handlers on both sides - echo payload back for verification - room_a->localParticipant()->registerRpcMethod( + lockLocalParticipant(*room_a)->registerRpcMethod( "ping", [&a_received](const RpcInvocationData& data) -> std::optional { a_received++; // Echo the payload back for round-trip verification return data.payload; }); - room_b->localParticipant()->registerRpcMethod( + lockLocalParticipant(*room_b)->registerRpcMethod( "ping", [&b_received](const RpcInvocationData& data) -> std::optional { b_received++; // Echo the payload back for round-trip verification @@ -543,6 +552,9 @@ TEST_F(RpcStressTest, BidirectionalRpcStress) { auto start_time = std::chrono::steady_clock::now(); auto duration = std::chrono::seconds(config_.stress_duration_seconds); + // Hold both local participants alive for the lifetime of the worker threads. + ASSERT_NO_THROW(lockLocalParticipant(*room_a)); + // A calling B std::thread thread_a_to_b([&]() { while (running.load()) { @@ -557,7 +569,9 @@ TEST_F(RpcStressTest, BidirectionalRpcStress) { auto call_start = std::chrono::high_resolution_clock::now(); try { - std::string response = room_a->localParticipant()->performRpc(identity_b, "ping", payload, 60.0); + auto room_a_lp = lockLocalParticipant(*room_a); + ASSERT_NE(room_a_lp, nullptr); + std::string response = room_a_lp->performRpc(identity_b, "ping", payload, 60.0); auto call_end = std::chrono::high_resolution_clock::now(); double latency_ms = std::chrono::duration(call_end - call_start).count(); @@ -588,6 +602,8 @@ TEST_F(RpcStressTest, BidirectionalRpcStress) { } }); + ASSERT_NO_THROW(lockLocalParticipant(*room_b)); + // B calling A std::thread thread_b_to_a([&]() { while (running.load()) { @@ -602,7 +618,9 @@ TEST_F(RpcStressTest, BidirectionalRpcStress) { auto call_start = std::chrono::high_resolution_clock::now(); try { - std::string response = room_b->localParticipant()->performRpc(identity_a, "ping", payload, 60.0); + auto room_b_lp = lockLocalParticipant(*room_b); + ASSERT_NE(room_b_lp, nullptr); + std::string response = room_b_lp->performRpc(identity_a, "ping", payload, 60.0); auto call_end = std::chrono::high_resolution_clock::now(); double latency_ms = std::chrono::duration(call_end - call_start).count(); @@ -676,8 +694,8 @@ TEST_F(RpcStressTest, BidirectionalRpcStress) { EXPECT_GT(stats_a_to_b.successfulCalls(), 0); EXPECT_GT(stats_b_to_a.successfulCalls(), 0); - room_a->localParticipant()->unregisterRpcMethod("ping"); - room_b->localParticipant()->unregisterRpcMethod("ping"); + lockLocalParticipant(*room_a)->unregisterRpcMethod("ping"); + lockLocalParticipant(*room_b)->unregisterRpcMethod("ping"); room_a.reset(); room_b.reset(); } @@ -697,12 +715,12 @@ TEST_F(RpcStressTest, HighThroughputBurst) { bool receiver_connected = receiver_room->connect(config_.url, config_.token_b, options); ASSERT_TRUE(receiver_connected) << "Receiver failed to connect"; - std::string receiver_identity = receiver_room->localParticipant()->identity(); + std::string receiver_identity = lockLocalParticipant(*receiver_room)->identity(); std::atomic total_received{0}; - receiver_room->localParticipant()->registerRpcMethod( - "burst-test", [&total_received](const RpcInvocationData& data) -> std::optional { + lockLocalParticipant(*receiver_room) + ->registerRpcMethod("burst-test", [&total_received](const RpcInvocationData& data) -> std::optional { total_received++; // Echo the payload back for round-trip verification return data.payload; @@ -725,6 +743,9 @@ TEST_F(RpcStressTest, HighThroughputBurst) { auto start_time = std::chrono::steady_clock::now(); auto duration = std::chrono::seconds(config_.stress_duration_seconds); + // Hold the local participant alive for the lifetime of the burst threads. + ASSERT_NO_THROW(lockLocalParticipant(*caller_room)); + // Multiple threads sending as fast as possible std::vector burst_threads; for (int t = 0; t < config_.num_caller_threads * 2; ++t) { @@ -741,8 +762,9 @@ TEST_F(RpcStressTest, HighThroughputBurst) { auto call_start = std::chrono::high_resolution_clock::now(); try { - std::string response = - caller_room->localParticipant()->performRpc(receiver_identity, "burst-test", payload, 60.0); + auto caller_lp = lockLocalParticipant(*caller_room); + ASSERT_NE(caller_lp, nullptr); + std::string response = caller_lp->performRpc(receiver_identity, "burst-test", payload, 60.0); auto call_end = std::chrono::high_resolution_clock::now(); double latency_ms = std::chrono::duration(call_end - call_start).count(); @@ -821,7 +843,7 @@ TEST_F(RpcStressTest, HighThroughputBurst) { EXPECT_GT(stats.successfulCalls(), 0); - receiver_room->localParticipant()->unregisterRpcMethod("burst-test"); + lockLocalParticipant(*receiver_room)->unregisterRpcMethod("burst-test"); caller_room.reset(); receiver_room.reset(); } diff --git a/src/tests/unit/test_audio_frame.cpp b/src/tests/unit/test_audio_frame.cpp index 935a620e..312f0e61 100644 --- a/src/tests/unit/test_audio_frame.cpp +++ b/src/tests/unit/test_audio_frame.cpp @@ -22,7 +22,7 @@ namespace livekit::test { class AudioFrameTest : public ::testing::Test { protected: - void SetUp() override { livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); } + void SetUp() override { livekit::initialize(livekit::LogLevel::Info); } void TearDown() override { livekit::shutdown(); } }; diff --git a/src/tests/unit/test_audio_processing_module.cpp b/src/tests/unit/test_audio_processing_module.cpp index 304dbb9f..62d86d65 100644 --- a/src/tests/unit/test_audio_processing_module.cpp +++ b/src/tests/unit/test_audio_processing_module.cpp @@ -32,7 +32,7 @@ namespace livekit::test { class AudioProcessingModuleTest : public ::testing::Test { protected: - void SetUp() override { livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); } + void SetUp() override { livekit::initialize(livekit::LogLevel::Info); } void TearDown() override { livekit::shutdown(); } diff --git a/src/tests/unit/test_audio_source.cpp b/src/tests/unit/test_audio_source.cpp index a201ca9c..9a6e9f1e 100644 --- a/src/tests/unit/test_audio_source.cpp +++ b/src/tests/unit/test_audio_source.cpp @@ -22,7 +22,7 @@ namespace livekit::test { class AudioSourceTest : public ::testing::Test { protected: - void SetUp() override { livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); } + void SetUp() override { livekit::initialize(livekit::LogLevel::Info); } void TearDown() override { livekit::shutdown(); } }; diff --git a/src/tests/unit/test_ffi_client.cpp b/src/tests/unit/test_ffi_client.cpp index 79f74263..d240654b 100644 --- a/src/tests/unit/test_ffi_client.cpp +++ b/src/tests/unit/test_ffi_client.cpp @@ -56,7 +56,7 @@ TEST_F(FfiClientTest, Initialize) { } TEST_F(FfiClientTest, InitializeFromSDK) { - EXPECT_TRUE(livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole)); + EXPECT_TRUE(livekit::initialize(livekit::LogLevel::Info)); EXPECT_TRUE(FfiClient::instance().isInitialized()); } diff --git a/src/tests/unit/test_room.cpp b/src/tests/unit/test_room.cpp index 18c6f222..c33df213 100644 --- a/src/tests/unit/test_room.cpp +++ b/src/tests/unit/test_room.cpp @@ -21,7 +21,7 @@ namespace livekit::test { class RoomTest : public ::testing::Test { protected: - void SetUp() override { livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); } + void SetUp() override { livekit::initialize(livekit::LogLevel::Info); } void TearDown() override { livekit::shutdown(); } }; @@ -33,12 +33,14 @@ TEST_F(RoomTest, ConnectWithoutInitialize) { Room room; bool result = room.connect("wss://localhost:7880", "test", livekit::RoomOptions()); EXPECT_FALSE(result) << "Connecting without initializing should return false"; + EXPECT_TRUE(room.localParticipant().expired()) << "Local participant should be empty after failed connect"; + EXPECT_TRUE(room.remoteParticipants().empty()) << "Remote participants should be empty after failed connect"; } TEST_F(RoomTest, CreateRoom) { Room room; // Room should be created without issues - EXPECT_EQ(room.localParticipant(), nullptr) << "Local participant should be null before connect"; + EXPECT_TRUE(room.localParticipant().expired()) << "Local participant should be empty before connect"; } TEST_F(RoomTest, RoomOptionsDefaults) { @@ -94,8 +96,8 @@ TEST_F(RoomTest, RemoteParticipantsEmptyBeforeConnect) { TEST_F(RoomTest, RemoteParticipantLookupBeforeConnect) { Room room; - auto participant = room.remoteParticipant("nonexistent"); - EXPECT_EQ(participant, nullptr) << "Looking up participant before connect should return nullptr"; + EXPECT_TRUE(room.remoteParticipant("nonexistent").expired()) + << "Looking up participant before connect should return an empty handle"; } } // namespace livekit::test diff --git a/src/tests/unit/test_room_callbacks.cpp b/src/tests/unit/test_room_callbacks.cpp index feee6a3c..71349d5b 100644 --- a/src/tests/unit/test_room_callbacks.cpp +++ b/src/tests/unit/test_room_callbacks.cpp @@ -29,95 +29,18 @@ namespace livekit { class RoomCallbackTest : public ::testing::Test { protected: - void SetUp() override { livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); } + void SetUp() override { livekit::initialize(livekit::LogLevel::Info); } void TearDown() override { livekit::shutdown(); } }; -TEST_F(RoomCallbackTest, AudioCallbackRegistrationIsAccepted) { - Room room; - - EXPECT_NO_THROW(room.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, [](const AudioFrame&) {})); -} - -TEST_F(RoomCallbackTest, VideoCallbackRegistrationIsAccepted) { - Room room; - - EXPECT_NO_THROW( - room.setOnVideoFrameCallback("alice", TrackSource::SOURCE_CAMERA, [](const VideoFrame&, std::int64_t) {})); -} - -TEST_F(RoomCallbackTest, AudioCallbackRegistrationByTrackNameIsAccepted) { +TEST_F(RoomCallbackTest, FrameCallbackRegistrationByTrackNameIsAccepted) { Room room; EXPECT_NO_THROW(room.setOnAudioFrameCallback("alice", "mic-main", [](const AudioFrame&) {})); -} - -TEST_F(RoomCallbackTest, VideoCallbackRegistrationByTrackNameIsAccepted) { - Room room; - EXPECT_NO_THROW(room.setOnVideoFrameCallback("alice", "cam-main", [](const VideoFrame&, std::int64_t) {})); -} - -TEST_F(RoomCallbackTest, ClearingMissingCallbacksIsNoOp) { - Room room; - - EXPECT_NO_THROW(room.clearOnAudioFrameCallback("nobody", TrackSource::SOURCE_MICROPHONE)); - EXPECT_NO_THROW(room.clearOnVideoFrameCallback("nobody", TrackSource::SOURCE_CAMERA)); - EXPECT_NO_THROW(room.clearOnAudioFrameCallback("nobody", "missing-audio")); - EXPECT_NO_THROW(room.clearOnVideoFrameCallback("nobody", "missing-video")); -} - -TEST_F(RoomCallbackTest, ReRegisteringSameAudioKeyDoesNotThrow) { - Room room; - std::atomic counter1{0}; - std::atomic counter2{0}; - - EXPECT_NO_THROW(room.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, - [&counter1](const AudioFrame&) { counter1++; })); - EXPECT_NO_THROW(room.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, - [&counter2](const AudioFrame&) { counter2++; })); -} - -TEST_F(RoomCallbackTest, ReRegisteringSameVideoKeyDoesNotThrow) { - Room room; - - EXPECT_NO_THROW( - room.setOnVideoFrameCallback("alice", TrackSource::SOURCE_CAMERA, [](const VideoFrame&, std::int64_t) {})); - EXPECT_NO_THROW( - room.setOnVideoFrameCallback("alice", TrackSource::SOURCE_CAMERA, [](const VideoFrame&, std::int64_t) {})); -} - -TEST_F(RoomCallbackTest, DistinctAudioAndVideoCallbacksCanCoexist) { - Room room; - - EXPECT_NO_THROW(room.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, [](const AudioFrame&) {})); - EXPECT_NO_THROW( - room.setOnVideoFrameCallback("alice", TrackSource::SOURCE_CAMERA, [](const VideoFrame&, std::int64_t) {})); - EXPECT_NO_THROW(room.setOnAudioFrameCallback("bob", TrackSource::SOURCE_MICROPHONE, [](const AudioFrame&) {})); - EXPECT_NO_THROW( - room.setOnVideoFrameCallback("bob", TrackSource::SOURCE_CAMERA, [](const VideoFrame&, std::int64_t) {})); -} - -TEST_F(RoomCallbackTest, SameSourceDifferentTrackNamesAreAccepted) { - Room room; - - EXPECT_NO_THROW(room.setOnVideoFrameCallback("alice", "cam-main", [](const VideoFrame&, std::int64_t) {})); - EXPECT_NO_THROW(room.setOnVideoFrameCallback("alice", "cam-backup", [](const VideoFrame&, std::int64_t) {})); -} - -TEST_F(RoomCallbackTest, ClearingTrackNameCallbackIsAccepted) { - Room room; - - EXPECT_NO_THROW(room.setOnAudioFrameCallback("alice", "mic-main", [](const AudioFrame&) {})); EXPECT_NO_THROW(room.clearOnAudioFrameCallback("alice", "mic-main")); -} - -TEST_F(RoomCallbackTest, SourceAndTrackNameCallbacksCanCoexist) { - Room room; - - EXPECT_NO_THROW(room.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, [](const AudioFrame&) {})); - EXPECT_NO_THROW(room.setOnAudioFrameCallback("alice", "mic-main", [](const AudioFrame&) {})); + EXPECT_NO_THROW(room.clearOnVideoFrameCallback("alice", "cam-main")); } TEST_F(RoomCallbackTest, DataCallbackRegistrationReturnsUsableIds) { @@ -145,8 +68,8 @@ TEST_F(RoomCallbackTest, RemovingUnknownDataCallbackIsNoOp) { TEST_F(RoomCallbackTest, DestroyRoomWithRegisteredCallbacksIsSafe) { EXPECT_NO_THROW({ Room room; - room.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, [](const AudioFrame&) {}); - room.setOnVideoFrameCallback("bob", TrackSource::SOURCE_CAMERA, [](const VideoFrame&, std::int64_t) {}); + room.setOnAudioFrameCallback("alice", "mic-main", [](const AudioFrame&) {}); + room.setOnVideoFrameCallback("bob", "cam-main", [](const VideoFrame&, std::int64_t) {}); room.addOnDataFrameCallback("carol", "track", [](const std::vector&, std::optional) {}); }); @@ -155,8 +78,8 @@ TEST_F(RoomCallbackTest, DestroyRoomWithRegisteredCallbacksIsSafe) { TEST_F(RoomCallbackTest, DestroyRoomAfterClearingCallbacksIsSafe) { EXPECT_NO_THROW({ Room room; - room.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, [](const AudioFrame&) {}); - room.clearOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE); + room.setOnAudioFrameCallback("alice", "mic-main", [](const AudioFrame&) {}); + room.clearOnAudioFrameCallback("alice", "mic-main"); const auto id = room.addOnDataFrameCallback("alice", "track", [](const std::vector&, std::optional) {}); @@ -164,73 +87,6 @@ TEST_F(RoomCallbackTest, DestroyRoomAfterClearingCallbacksIsSafe) { }); } -TEST_F(RoomCallbackTest, ConcurrentRegistrationDoesNotCrash) { - Room room; - constexpr int kThreads = 8; - constexpr int kIterations = 100; - - std::vector threads; - threads.reserve(kThreads); - - for (int t = 0; t < kThreads; ++t) { - threads.emplace_back([&room, t, kIterations]() { - for (int i = 0; i < kIterations; ++i) { - const std::string id = "participant-" + std::to_string(t); - room.setOnAudioFrameCallback(id, TrackSource::SOURCE_MICROPHONE, [](const AudioFrame&) {}); - room.clearOnAudioFrameCallback(id, TrackSource::SOURCE_MICROPHONE); - } - }); - } - - for (auto& thread : threads) { - thread.join(); - } - - SUCCEED(); -} - -TEST_F(RoomCallbackTest, ConcurrentMixedRegistrationDoesNotCrash) { - Room room; - constexpr int kThreads = 4; - constexpr int kIterations = 50; - - std::vector threads; - threads.reserve(kThreads); - - for (int t = 0; t < kThreads; ++t) { - threads.emplace_back([&room, t, kIterations]() { - const std::string id = "p-" + std::to_string(t); - for (int i = 0; i < kIterations; ++i) { - room.setOnAudioFrameCallback(id, TrackSource::SOURCE_MICROPHONE, [](const AudioFrame&) {}); - room.setOnVideoFrameCallback(id, TrackSource::SOURCE_CAMERA, [](const VideoFrame&, std::int64_t) {}); - const auto data_id = room.addOnDataFrameCallback( - id, "track", [](const std::vector&, std::optional) {}); - room.removeOnDataFrameCallback(data_id); - } - }); - } - - for (auto& thread : threads) { - thread.join(); - } - - SUCCEED(); -} - -TEST_F(RoomCallbackTest, ManyDistinctAudioCallbacksCanBeRegisteredAndCleared) { - Room room; - constexpr int kCount = 50; - - for (int i = 0; i < kCount; ++i) { - EXPECT_NO_THROW(room.setOnAudioFrameCallback("participant-" + std::to_string(i), TrackSource::SOURCE_MICROPHONE, - [](const AudioFrame&) {})); - } - - for (int i = 0; i < kCount; ++i) { - EXPECT_NO_THROW(room.clearOnAudioFrameCallback("participant-" + std::to_string(i), TrackSource::SOURCE_MICROPHONE)); - } -} - TEST_F(RoomCallbackTest, DefaultConnectionStateIsDisconnected) { Room room; EXPECT_EQ(room.connectionState(), ConnectionState::Disconnected); @@ -239,10 +95,10 @@ TEST_F(RoomCallbackTest, DefaultConnectionStateIsDisconnected) { TEST_F(RoomCallbackTest, ConnectionStateRemainsDisconnectedWithoutConnect) { // Register callbacks, do other operations — state must stay Disconnected. Room room; - room.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, [](const AudioFrame&) {}); - room.setOnVideoFrameCallback("alice", TrackSource::SOURCE_CAMERA, [](const VideoFrame&, std::int64_t) {}); + room.setOnAudioFrameCallback("alice", "mic-main", [](const AudioFrame&) {}); + room.setOnVideoFrameCallback("alice", "cam-main", [](const VideoFrame&, std::int64_t) {}); room.addOnDataFrameCallback("alice", "track", [](const std::vector&, std::optional) {}); - room.registerTextStreamHandler("topic", [](std::shared_ptr, const std::string&) {}); + room.registerTextStreamHandler("topic", [](const std::shared_ptr&, const std::string&) {}); EXPECT_EQ(room.connectionState(), ConnectionState::Disconnected); } diff --git a/src/tests/unit/test_subscription_thread_dispatcher.cpp b/src/tests/unit/test_subscription_thread_dispatcher.cpp index 7840c666..80b52120 100644 --- a/src/tests/unit/test_subscription_thread_dispatcher.cpp +++ b/src/tests/unit/test_subscription_thread_dispatcher.cpp @@ -28,7 +28,7 @@ namespace livekit { class SubscriptionThreadDispatcherTest : public ::testing::Test { protected: - void SetUp() override { livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); } + void SetUp() override { livekit::initialize(livekit::LogLevel::Info); } void TearDown() override { livekit::shutdown(); } @@ -51,26 +51,20 @@ class SubscriptionThreadDispatcherTest : public ::testing::Test { // ============================================================================ TEST_F(SubscriptionThreadDispatcherTest, CallbackKeyEqualKeysCompareEqual) { - CallbackKey a{"alice", TrackSource::SOURCE_MICROPHONE, ""}; - CallbackKey b{"alice", TrackSource::SOURCE_MICROPHONE, ""}; + CallbackKey a{"alice", "mic-main"}; + CallbackKey b{"alice", "mic-main"}; EXPECT_TRUE(a == b); } TEST_F(SubscriptionThreadDispatcherTest, CallbackKeyDifferentIdentityNotEqual) { - CallbackKey a{"alice", TrackSource::SOURCE_MICROPHONE, ""}; - CallbackKey b{"bob", TrackSource::SOURCE_MICROPHONE, ""}; - EXPECT_FALSE(a == b); -} - -TEST_F(SubscriptionThreadDispatcherTest, CallbackKeyDifferentSourceNotEqual) { - CallbackKey a{"alice", TrackSource::SOURCE_MICROPHONE, ""}; - CallbackKey b{"alice", TrackSource::SOURCE_CAMERA, ""}; + CallbackKey a{"alice", "mic-main"}; + CallbackKey b{"bob", "mic-main"}; EXPECT_FALSE(a == b); } TEST_F(SubscriptionThreadDispatcherTest, CallbackKeyDifferentTrackNameNotEqual) { - CallbackKey a{"alice", TrackSource::SOURCE_UNKNOWN, "cam-main"}; - CallbackKey b{"alice", TrackSource::SOURCE_UNKNOWN, "cam-backup"}; + CallbackKey a{"alice", "cam-main"}; + CallbackKey b{"alice", "cam-backup"}; EXPECT_FALSE(a == b); } @@ -79,30 +73,28 @@ TEST_F(SubscriptionThreadDispatcherTest, CallbackKeyDifferentTrackNameNotEqual) // ============================================================================ TEST_F(SubscriptionThreadDispatcherTest, CallbackKeyHashEqualKeysProduceSameHash) { - CallbackKey a{"alice", TrackSource::SOURCE_MICROPHONE, ""}; - CallbackKey b{"alice", TrackSource::SOURCE_MICROPHONE, ""}; + CallbackKey a{"alice", "mic-main"}; + CallbackKey b{"alice", "mic-main"}; CallbackKeyHash hasher; EXPECT_EQ(hasher(a), hasher(b)); } TEST_F(SubscriptionThreadDispatcherTest, CallbackKeyHashDifferentKeysLikelyDifferentHash) { CallbackKeyHash hasher; - CallbackKey mic{"alice", TrackSource::SOURCE_MICROPHONE, ""}; - CallbackKey cam{"alice", TrackSource::SOURCE_CAMERA, ""}; - CallbackKey bob{"bob", TrackSource::SOURCE_MICROPHONE, ""}; - CallbackKey named{"alice", TrackSource::SOURCE_UNKNOWN, "mic-main"}; + CallbackKey mic{"alice", "mic-main"}; + CallbackKey cam{"alice", "cam-main"}; + CallbackKey bob{"bob", "mic-main"}; EXPECT_NE(hasher(mic), hasher(cam)); EXPECT_NE(hasher(mic), hasher(bob)); - EXPECT_NE(hasher(mic), hasher(named)); } TEST_F(SubscriptionThreadDispatcherTest, CallbackKeyWorksAsUnorderedMapKey) { std::unordered_map map; - CallbackKey k1{"alice", TrackSource::SOURCE_MICROPHONE, ""}; - CallbackKey k2{"bob", TrackSource::SOURCE_CAMERA, ""}; - CallbackKey k3{"alice", TrackSource::SOURCE_CAMERA, ""}; + CallbackKey k1{"alice", "mic-main"}; + CallbackKey k2{"bob", "cam-main"}; + CallbackKey k3{"alice", "cam-backup"}; map[k1] = 1; map[k2] = 2; @@ -123,8 +115,8 @@ TEST_F(SubscriptionThreadDispatcherTest, CallbackKeyWorksAsUnorderedMapKey) { } TEST_F(SubscriptionThreadDispatcherTest, CallbackKeyEmptyIdentityWorks) { - CallbackKey a{"", TrackSource::SOURCE_UNKNOWN, ""}; - CallbackKey b{"", TrackSource::SOURCE_UNKNOWN, ""}; + CallbackKey a{"", "mic-main"}; + CallbackKey b{"", "mic-main"}; CallbackKeyHash hasher; EXPECT_TRUE(a == b); EXPECT_EQ(hasher(a), hasher(b)); @@ -141,45 +133,20 @@ TEST_F(SubscriptionThreadDispatcherTest, MaxActiveReadersIs20) { EXPECT_EQ(maxAc // ============================================================================ TEST_F(SubscriptionThreadDispatcherTest, SetAudioCallbackStoresRegistration) { - SubscriptionThreadDispatcher dispatcher; - dispatcher.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, [](const AudioFrame&) {}); - - EXPECT_EQ(audioCallbacks(dispatcher).size(), 1u); -} - -TEST_F(SubscriptionThreadDispatcherTest, SetAudioCallbackByTrackNameStoresRegistration) { SubscriptionThreadDispatcher dispatcher; dispatcher.setOnAudioFrameCallback("alice", "mic-main", [](const AudioFrame&) {}); EXPECT_EQ(audioCallbacks(dispatcher).size(), 1u); - EXPECT_EQ(audioCallbacks(dispatcher).count(CallbackKey{"alice", TrackSource::SOURCE_UNKNOWN, "mic-main"}), 1u); } TEST_F(SubscriptionThreadDispatcherTest, SetVideoCallbackStoresRegistration) { - SubscriptionThreadDispatcher dispatcher; - dispatcher.setOnVideoFrameCallback("alice", TrackSource::SOURCE_CAMERA, [](const VideoFrame&, std::int64_t) {}); - - EXPECT_EQ(videoCallbacks(dispatcher).size(), 1u); -} - -TEST_F(SubscriptionThreadDispatcherTest, SetVideoCallbackByTrackNameStoresRegistration) { SubscriptionThreadDispatcher dispatcher; dispatcher.setOnVideoFrameCallback("alice", "cam-main", [](const VideoFrame&, std::int64_t) {}); EXPECT_EQ(videoCallbacks(dispatcher).size(), 1u); - EXPECT_EQ(videoCallbacks(dispatcher).count(CallbackKey{"alice", TrackSource::SOURCE_UNKNOWN, "cam-main"}), 1u); } TEST_F(SubscriptionThreadDispatcherTest, ClearAudioCallbackRemovesRegistration) { - SubscriptionThreadDispatcher dispatcher; - dispatcher.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, [](const AudioFrame&) {}); - ASSERT_EQ(audioCallbacks(dispatcher).size(), 1u); - - dispatcher.clearOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE); - EXPECT_EQ(audioCallbacks(dispatcher).size(), 0u); -} - -TEST_F(SubscriptionThreadDispatcherTest, ClearAudioCallbackByTrackNameRemovesRegistration) { SubscriptionThreadDispatcher dispatcher; dispatcher.setOnAudioFrameCallback("alice", "mic-main", [](const AudioFrame&) {}); ASSERT_EQ(audioCallbacks(dispatcher).size(), 1u); @@ -189,15 +156,6 @@ TEST_F(SubscriptionThreadDispatcherTest, ClearAudioCallbackByTrackNameRemovesReg } TEST_F(SubscriptionThreadDispatcherTest, ClearVideoCallbackRemovesRegistration) { - SubscriptionThreadDispatcher dispatcher; - dispatcher.setOnVideoFrameCallback("alice", TrackSource::SOURCE_CAMERA, [](const VideoFrame&, std::int64_t) {}); - ASSERT_EQ(videoCallbacks(dispatcher).size(), 1u); - - dispatcher.clearOnVideoFrameCallback("alice", TrackSource::SOURCE_CAMERA); - EXPECT_EQ(videoCallbacks(dispatcher).size(), 0u); -} - -TEST_F(SubscriptionThreadDispatcherTest, ClearVideoCallbackByTrackNameRemovesRegistration) { SubscriptionThreadDispatcher dispatcher; dispatcher.setOnVideoFrameCallback("alice", "cam-main", [](const VideoFrame&, std::int64_t) {}); ASSERT_EQ(videoCallbacks(dispatcher).size(), 1u); @@ -208,8 +166,6 @@ TEST_F(SubscriptionThreadDispatcherTest, ClearVideoCallbackByTrackNameRemovesReg TEST_F(SubscriptionThreadDispatcherTest, ClearNonExistentCallbackIsNoOp) { SubscriptionThreadDispatcher dispatcher; - EXPECT_NO_THROW(dispatcher.clearOnAudioFrameCallback("nobody", TrackSource::SOURCE_MICROPHONE)); - EXPECT_NO_THROW(dispatcher.clearOnVideoFrameCallback("nobody", TrackSource::SOURCE_CAMERA)); EXPECT_NO_THROW(dispatcher.clearOnAudioFrameCallback("nobody", "missing")); EXPECT_NO_THROW(dispatcher.clearOnVideoFrameCallback("nobody", "missing")); } @@ -219,67 +175,46 @@ TEST_F(SubscriptionThreadDispatcherTest, OverwriteAudioCallbackKeepsSingleEntry) std::atomic counter1{0}; std::atomic counter2{0}; - dispatcher.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, - [&counter1](const AudioFrame&) { counter1++; }); - dispatcher.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, - [&counter2](const AudioFrame&) { counter2++; }); + dispatcher.setOnAudioFrameCallback("alice", "mic-main", [&counter1](const AudioFrame&) { counter1++; }); + dispatcher.setOnAudioFrameCallback("alice", "mic-main", [&counter2](const AudioFrame&) { counter2++; }); EXPECT_EQ(audioCallbacks(dispatcher).size(), 1u) << "Re-registering with the same key should overwrite, not add"; } TEST_F(SubscriptionThreadDispatcherTest, OverwriteVideoCallbackKeepsSingleEntry) { SubscriptionThreadDispatcher dispatcher; - dispatcher.setOnVideoFrameCallback("alice", TrackSource::SOURCE_CAMERA, [](const VideoFrame&, std::int64_t) {}); - dispatcher.setOnVideoFrameCallback("alice", TrackSource::SOURCE_CAMERA, [](const VideoFrame&, std::int64_t) {}); + dispatcher.setOnVideoFrameCallback("alice", "cam-main", [](const VideoFrame&, std::int64_t) {}); + dispatcher.setOnVideoFrameCallback("alice", "cam-main", [](const VideoFrame&, std::int64_t) {}); EXPECT_EQ(videoCallbacks(dispatcher).size(), 1u); } -TEST_F(SubscriptionThreadDispatcherTest, OverwriteTrackNameAudioCallbackKeepsSingleEntry) { - SubscriptionThreadDispatcher dispatcher; - dispatcher.setOnAudioFrameCallback("alice", "mic-main", [](const AudioFrame&) {}); - dispatcher.setOnAudioFrameCallback("alice", "mic-main", [](const AudioFrame&) {}); - - EXPECT_EQ(audioCallbacks(dispatcher).size(), 1u); -} - TEST_F(SubscriptionThreadDispatcherTest, MultipleDistinctCallbacksAreIndependent) { SubscriptionThreadDispatcher dispatcher; - dispatcher.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, [](const AudioFrame&) {}); - dispatcher.setOnVideoFrameCallback("alice", TrackSource::SOURCE_CAMERA, [](const VideoFrame&, std::int64_t) {}); - dispatcher.setOnAudioFrameCallback("bob", TrackSource::SOURCE_MICROPHONE, [](const AudioFrame&) {}); - dispatcher.setOnVideoFrameCallback("bob", TrackSource::SOURCE_CAMERA, [](const VideoFrame&, std::int64_t) {}); + dispatcher.setOnAudioFrameCallback("alice", "mic-main", [](const AudioFrame&) {}); + dispatcher.setOnVideoFrameCallback("alice", "cam-main", [](const VideoFrame&, std::int64_t) {}); + dispatcher.setOnAudioFrameCallback("bob", "mic-main", [](const AudioFrame&) {}); + dispatcher.setOnVideoFrameCallback("bob", "cam-main", [](const VideoFrame&, std::int64_t) {}); EXPECT_EQ(audioCallbacks(dispatcher).size(), 2u); EXPECT_EQ(videoCallbacks(dispatcher).size(), 2u); - dispatcher.clearOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE); + dispatcher.clearOnAudioFrameCallback("alice", "mic-main"); EXPECT_EQ(audioCallbacks(dispatcher).size(), 1u); EXPECT_EQ(videoCallbacks(dispatcher).size(), 2u); } -TEST_F(SubscriptionThreadDispatcherTest, ClearingOneSourceDoesNotAffectOther) { +TEST_F(SubscriptionThreadDispatcherTest, ClearingOneTrackNameDoesNotAffectOther) { SubscriptionThreadDispatcher dispatcher; - dispatcher.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, [](const AudioFrame&) {}); - dispatcher.setOnAudioFrameCallback("alice", TrackSource::SOURCE_SCREENSHARE_AUDIO, [](const AudioFrame&) {}); - ASSERT_EQ(audioCallbacks(dispatcher).size(), 2u); - - dispatcher.clearOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE); - EXPECT_EQ(audioCallbacks(dispatcher).size(), 1u); - - CallbackKey remaining{"alice", TrackSource::SOURCE_SCREENSHARE_AUDIO, ""}; - EXPECT_EQ(audioCallbacks(dispatcher).count(remaining), 1u); -} - -TEST_F(SubscriptionThreadDispatcherTest, SourceAndTrackNameAudioCallbacksAreIndependent) { - SubscriptionThreadDispatcher dispatcher; - dispatcher.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, [](const AudioFrame&) {}); dispatcher.setOnAudioFrameCallback("alice", "mic-main", [](const AudioFrame&) {}); + dispatcher.setOnAudioFrameCallback("alice", "screenshare-main", [](const AudioFrame&) {}); ASSERT_EQ(audioCallbacks(dispatcher).size(), 2u); dispatcher.clearOnAudioFrameCallback("alice", "mic-main"); EXPECT_EQ(audioCallbacks(dispatcher).size(), 1u); - EXPECT_EQ(audioCallbacks(dispatcher).count(CallbackKey{"alice", TrackSource::SOURCE_MICROPHONE, ""}), 1u); + + CallbackKey remaining{"alice", "screenshare-main"}; + EXPECT_EQ(audioCallbacks(dispatcher).count(remaining), 1u); } // ============================================================================ @@ -293,7 +228,7 @@ TEST_F(SubscriptionThreadDispatcherTest, NoActiveReadersInitially) { TEST_F(SubscriptionThreadDispatcherTest, ActiveReadersEmptyAfterCallbackRegistration) { SubscriptionThreadDispatcher dispatcher; - dispatcher.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, [](const AudioFrame&) {}); + dispatcher.setOnAudioFrameCallback("alice", "mic-main", [](const AudioFrame&) {}); EXPECT_TRUE(activeReaders(dispatcher).empty()) << "Registering a callback without a subscribed track should not spawn " "readers"; @@ -306,16 +241,16 @@ TEST_F(SubscriptionThreadDispatcherTest, ActiveReadersEmptyAfterCallbackRegistra TEST_F(SubscriptionThreadDispatcherTest, DestroyDispatcherWithRegisteredCallbacksIsSafe) { EXPECT_NO_THROW({ SubscriptionThreadDispatcher dispatcher; - dispatcher.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, [](const AudioFrame&) {}); - dispatcher.setOnVideoFrameCallback("bob", TrackSource::SOURCE_CAMERA, [](const VideoFrame&, std::int64_t) {}); + dispatcher.setOnAudioFrameCallback("alice", "mic-main", [](const AudioFrame&) {}); + dispatcher.setOnVideoFrameCallback("bob", "cam-main", [](const VideoFrame&, std::int64_t) {}); }); } TEST_F(SubscriptionThreadDispatcherTest, DestroyDispatcherAfterClearingCallbacksIsSafe) { EXPECT_NO_THROW({ SubscriptionThreadDispatcher dispatcher; - dispatcher.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, [](const AudioFrame&) {}); - dispatcher.clearOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE); + dispatcher.setOnAudioFrameCallback("alice", "mic-main", [](const AudioFrame&) {}); + dispatcher.clearOnAudioFrameCallback("alice", "mic-main"); }); } @@ -334,9 +269,9 @@ TEST_F(SubscriptionThreadDispatcherTest, ConcurrentRegistrationDoesNotCrash) { for (int t = 0; t < kThreads; ++t) { threads.emplace_back([&dispatcher, t, kIterations]() { for (int i = 0; i < kIterations; ++i) { - std::string id = "participant-" + std::to_string(t); - dispatcher.setOnAudioFrameCallback(id, TrackSource::SOURCE_MICROPHONE, [](const AudioFrame&) {}); - dispatcher.clearOnAudioFrameCallback(id, TrackSource::SOURCE_MICROPHONE); + const std::string id = "participant-" + std::to_string(t); + dispatcher.setOnAudioFrameCallback(id, "mic-main", [](const AudioFrame&) {}); + dispatcher.clearOnAudioFrameCallback(id, "mic-main"); } }); } @@ -354,13 +289,14 @@ TEST_F(SubscriptionThreadDispatcherTest, ConcurrentMixedAudioVideoRegistration) constexpr int kIterations = 50; std::vector threads; + threads.reserve(kThreads); for (int t = 0; t < kThreads; ++t) { threads.emplace_back([&dispatcher, t, kIterations]() { - std::string id = "p-" + std::to_string(t); + const std::string id = "p-" + std::to_string(t); for (int i = 0; i < kIterations; ++i) { - dispatcher.setOnAudioFrameCallback(id, TrackSource::SOURCE_MICROPHONE, [](const AudioFrame&) {}); - dispatcher.setOnVideoFrameCallback(id, TrackSource::SOURCE_CAMERA, [](const VideoFrame&, std::int64_t) {}); + dispatcher.setOnAudioFrameCallback(id, "mic-main", [](const AudioFrame&) {}); + dispatcher.setOnVideoFrameCallback(id, "cam-main", [](const VideoFrame&, std::int64_t) {}); } }); } @@ -382,14 +318,13 @@ TEST_F(SubscriptionThreadDispatcherTest, ManyDistinctCallbacksCanBeRegistered) { constexpr int kCount = 50; for (int i = 0; i < kCount; ++i) { - dispatcher.setOnAudioFrameCallback("participant-" + std::to_string(i), TrackSource::SOURCE_MICROPHONE, - [](const AudioFrame&) {}); + dispatcher.setOnAudioFrameCallback("participant-" + std::to_string(i), "mic-main", [](const AudioFrame&) {}); } EXPECT_EQ(audioCallbacks(dispatcher).size(), static_cast(kCount)); for (int i = 0; i < kCount; ++i) { - dispatcher.clearOnAudioFrameCallback("participant-" + std::to_string(i), TrackSource::SOURCE_MICROPHONE); + dispatcher.clearOnAudioFrameCallback("participant-" + std::to_string(i), "mic-main"); } EXPECT_EQ(audioCallbacks(dispatcher).size(), 0u); @@ -572,8 +507,8 @@ TEST_F(SubscriptionThreadDispatcherTest, DestroyDispatcherAfterRemovingDataCallb TEST_F(SubscriptionThreadDispatcherTest, MixedAudioVideoDataCallbacksAreIndependent) { SubscriptionThreadDispatcher dispatcher; - dispatcher.setOnAudioFrameCallback("alice", TrackSource::SOURCE_MICROPHONE, [](const AudioFrame&) {}); - dispatcher.setOnVideoFrameCallback("alice", TrackSource::SOURCE_CAMERA, [](const VideoFrame&, std::int64_t) {}); + dispatcher.setOnAudioFrameCallback("alice", "mic-main", [](const AudioFrame&) {}); + dispatcher.setOnVideoFrameCallback("alice", "cam-main", [](const VideoFrame&, std::int64_t) {}); dispatcher.addOnDataFrameCallback("alice", "data-track", [](const std::vector&, std::optional) {}); diff --git a/src/tests/unit/test_video_source.cpp b/src/tests/unit/test_video_source.cpp index f1ae4f79..4d481d4c 100644 --- a/src/tests/unit/test_video_source.cpp +++ b/src/tests/unit/test_video_source.cpp @@ -22,7 +22,7 @@ namespace livekit::test { class VideoSourceTest : public ::testing::Test { protected: - void SetUp() override { livekit::initialize(livekit::LogLevel::Info, livekit::LogSink::kConsole); } + void SetUp() override { livekit::initialize(livekit::LogLevel::Info); } void TearDown() override { livekit::shutdown(); } }; diff --git a/src/video_source.cpp b/src/video_source.cpp index 468b900d..e173fe7b 100644 --- a/src/video_source.cpp +++ b/src/video_source.cpp @@ -63,7 +63,7 @@ void VideoSource::captureFrame(const VideoFrame& frame, const VideoCaptureOption } void VideoSource::captureFrame(const VideoFrame& frame, std::int64_t timestamp_us, VideoRotation rotation) { - captureFrame(frame, VideoCaptureOptions{timestamp_us, rotation}); + captureFrame(frame, VideoCaptureOptions{timestamp_us, rotation, {}}); } } // namespace livekit