Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2ea5d84
livekit_bridge: Ergonomic library on top of the Client C++ SDK
stephen-derosa Feb 12, 2026
10f0e20
copyright and clang
stephen-derosa Feb 17, 2026
4ec86e3
human.cpp: avoid heavy work with lock. assert for non null unique ptr…
stephen-derosa Feb 17, 2026
bb1782e
livekit_bridge: dont keep mutex for whole connect() call, use connect…
stephen-derosa Feb 17, 2026
97f5f7c
dont blindly detatch threads
stephen-derosa Feb 18, 2026
c527d93
tracks: release() is private. video track: data -> rgba
stephen-derosa Feb 18, 2026
26fad41
make bridge_audio_track.h and bridge_video_track.h thread safe
stephen-derosa Feb 18, 2026
21ab1e1
support for different types of audio and video sources
stephen-derosa Feb 18, 2026
9b1873a
enable --no-audio in human.cpp to avoid echo playback
stephen-derosa Feb 19, 2026
22ca476
register/unset callbacks -> set/clear callbacks. Only one callback pe…
stephen-derosa Feb 20, 2026
f49b84b
log errors if we have more than 25 threads to measure expected streams
stephen-derosa Feb 20, 2026
91985bd
expose RoomOptions to the livekit_bridge
stephen-derosa Feb 20, 2026
b256b1b
bridge tracks: pushFrame returns bool
stephen-derosa Feb 20, 2026
92ce86b
safe sendFrame()
stephen-derosa Feb 20, 2026
0afc628
store shared_ptr of tracks in the bridge to enable unpublishing on di…
stephen-derosa Feb 20, 2026
9d1634b
windows compatible examples
stephen-derosa Feb 23, 2026
297c955
windows compatible examples
stephen-derosa Feb 23, 2026
a231643
rm data_track.proto
stephen-derosa Feb 23, 2026
9005742
SDL and WAV examples/common dir. move bridge/examples to examples/
stephen-derosa Feb 24, 2026
07afb8b
single_peer_connection = false by default
stephen-derosa Feb 24, 2026
7b7d422
Doxygen for all of bridge/
stephen-derosa Feb 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ out
build/
build-debug/
build-release/
release/
vcpkg_installed/
# Generated header
include/livekit/build.h
Expand All @@ -17,6 +18,7 @@ docs/html/
docs/latex/
.vs/
.vscode/
.cursor/
# Compiled output
bin/
lib/
Expand Down
4 changes: 4 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")

option(LIVEKIT_BUILD_EXAMPLES "Build LiveKit examples" OFF)
option(LIVEKIT_BUILD_TESTS "Build LiveKit tests" OFF)
option(LIVEKIT_BUILD_BRIDGE "Build LiveKit Bridge (simplified high-level API)" OFF)

# vcpkg is only used on Windows; Linux/macOS use system package managers
if(WIN32)
Expand Down Expand Up @@ -627,6 +628,9 @@ install(FILES

# ------------------------------------------------------------------------

# Build the LiveKit C++ bridge before examples (human_robot depends on it)
add_subdirectory(bridge)

# ---- Examples ----
# add_subdirectory(examples)

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ This page covers how to build and install the LiveKit C++ Client SDK for real-ti
- **Git LFS** (required for examples)
Some example data files (e.g., audio assets) are stored using Git LFS.
You must install Git LFS before cloning or pulling the repo if you want to run the examples.
- **livekit-cli** install livekit-cli by following the (official livekit docs)[https://docs.livekit.io/intro/basics/cli/start/]
- **livekit-server** install livekit-server by following the (official livekit docs)[https://docs.livekit.io/transport/self-hosting/local/]

**Platform-Specific Requirements:**

Expand Down Expand Up @@ -340,4 +342,4 @@ brew install clang-format
```

<!--BEGIN_REPO_NAV-->
<!--END_REPO_NAV-->
<!--END_REPO_NAV-->
51 changes: 51 additions & 0 deletions bridge/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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/bridge_room_delegate.cpp
src/bridge_room_delegate.h
)

if(WIN32)
set_target_properties(livekit_bridge PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON)
endif()

target_include_directories(livekit_bridge
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
PRIVATE
${CMAKE_CURRENT_SOURCE_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/.
249 changes: 249 additions & 0 deletions bridge/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
# LiveKit Bridge

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. 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.

**`BridgeRoomDelegate`** -- Internal (not part of the public API; lives in `src/`). Listens for `onTrackSubscribed` / `onTrackUnsubscribed` events from the LiveKit SDK and wires up reader threads automatically.

### 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 bridge subscribes to it (auto-subscribe is enabled by default), the bridge 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 entirely by the bridge. 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 `disconnect()` is called.

### Callback Registration Timing

Callbacks are keyed by `(participant_identity, track_source)`. You can register them **before** the remote participant has joined the room. The bridge 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
// Register first, connect second -- or register after connect but before
// the remote participant joins.
bridge.setOnAudioFrameCallback("robot-1", livekit::TrackSource::SOURCE_MICROPHONE, my_callback);
livekit::RoomOptions options;
options.auto_subscribe = true;
bridge.connect(url, token, options);
// 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()` 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<BridgeAudioTrack>`. |
| `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<BridgeVideoTrack>`. |
| `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. |

### `BridgeAudioTrack`

| Method | Description |
|---|---|
| `pushFrame(data, samples_per_channel, timeout_ms)` | Push interleaved int16 PCM samples. Accepts `std::vector<int16_t>` 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<uint8_t>` 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 <key> --api-secret <secret> \
--join --room robo_room --identity robot --valid-for 24h

lk token create --api-key <key> --api-secret <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=<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=<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. `CallbackKey` hashing/equality,
2. `BridgeAudioTrack`/`BridgeVideoTrack` state management, and
3. `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
- RPC / data channels / 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.
Loading
Loading