Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
data/welcome.wav filter=lfs diff=lfs merge=lfs -text
build.cmd whitespace=cr-at-eol
CMakePresets.json whitespace=cr-at-eol
4 changes: 3 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ The SDK has three categories of threads:
1. Completes any pending async `std::promise` matched by `async_id`.
2. Invokes all registered `FfiClient` listeners (including `Room::onEvent`).

All `RoomDelegate` callbacks and stream handler callbacks (e.g., `registerTextStreamHandler`) are invoked on this FFI callback thread. **Handlers must not block**; spawn a background thread if synchronous work is needed.
All `RoomDelegate` callbacks, stream handler callbacks (e.g., `registerTextStreamHandler`), and `CaptureSource::FinishedCallback` notifications are invoked on this FFI callback thread. **Handlers must not block**; spawn a background thread if synchronous work is needed.

**Per-subscription reader threads** — `SubscriptionThreadDispatcher` creates a dedicated `std::thread` for each active audio, video, or data track subscription. These threads block on `AudioStream::read()`, `VideoStream::read()`, or `DataTrackStream::read()` and invoke the registered `AudioFrameCallback`, `VideoFrameCallback`, or `DataFrameCallback` on that reader thread — not on the FFI callback thread. A hard limit of 20 concurrent reader threads is enforced.

Expand All @@ -77,6 +77,7 @@ All `RoomDelegate` callbacks and stream handler callbacks (e.g., `registerTextSt
| `LocalAudioTrack` / `LocalVideoTrack` | No | Thin `sendRequest` wrappers with no internal synchronization. |
| `LocalDataTrack::tryPush` | No | Thin `sendRequest` wrapper with no internal synchronization. |
| `TextStreamWriter` / `ByteStreamWriter` | Serialized | `write()` is serialized by an internal `write_mutex_`. |
| `CaptureSource` | Partially | Callback registration is mutex-protected; `start()` and `stop()` send FFI requests. Destruction must be synchronized with application calls. The finished callback runs on the FFI callback thread. |

### Directory Layout
Be sure to update the directory layout in this file if the directory layout changes.
Expand Down Expand Up @@ -104,6 +105,7 @@ Be sure to update the directory layout in this file if the directory layout chan
- **`RoomDelegate`** — Virtual callback interface for room lifecycle events.
- **`SubscriptionThreadDispatcher`** — Owns callback registrations and per-subscription reader threads for audio, video, and data tracks. `Room` delegates callback management here.
- **`LocalParticipant` / `RemoteParticipant`** — Participant objects for publishing and receiving tracks.
- **`CaptureSource`** — Wrapper for Rust-owned video capture producers and their frame pumps. Pattern and clock sources are supported initially.

## Build
for building, use the build.sh script for Linux and macOS, and the build.cmd script for Windows. Do not invoke CMake directly to build the SDK.
Expand Down
16 changes: 16 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ include(warnings)

option(LIVEKIT_BUILD_EXAMPLES "Build LiveKit examples" OFF)
option(LIVEKIT_BUILD_TESTS "Build LiveKit tests" OFF)
option(LIVEKIT_ENABLE_CAPTURE "Build the Rust FFI with pattern and clock capture sources" OFF)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Release bundles cannot create capture sources

Official release builds leave LIVEKIT_ENABLE_CAPTURE off, so CaptureSource::create cannot create either built-in source. The release workflow uses release-examples, not the capture-enabled test presets.

Learn more

The public capture factories are always compiled, but the Rust implementation is gated by this CMake option. The official release workflow builds using the release-examples preset, which does not enable it. Consumers of a clean release bundle therefore cannot use either advertised source.

Example: A release built through ./build.sh release-examples --bundle includes CaptureSource::create, but awaiting a pattern-source creation future fails because the Rust FFI was built without capture-pattern.

Recommended fix: Enable the capture features in release and example presets, including the presets used by .github/workflows/make-release.yml and .github/workflows/builds.yml, or make capture support the default while explicitly allowing opt-out.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LIVEKIT_ENABLE_CAPTURE -> LIVEKIT_BUILD_CAPTURE?

Also should we have additional CI release artifacts with capture? like <SDK>_capture.tar.gz

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in #276


# vcpkg is only used on Windows; Linux/macOS use system package managers
if(WIN32)
Expand Down Expand Up @@ -167,6 +168,14 @@ set(GENERATED_BUILD_H "${LIVEKIT_ROOT_DIR}/include/livekit/build.h")
set(GENERATED_BUILD_INFO_JSON "${LIVEKIT_BINARY_DIR}/build-info.json")
set(RUST_ROOT ${LIVEKIT_ROOT_DIR}/client-sdk-rust)

# Cargo features for the Rust FFI build.
set(LIVEKIT_CARGO_FEATURES "")
set(LIVEKIT_CAPTURE_ENABLED 0)
if(LIVEKIT_ENABLE_CAPTURE)
set(LIVEKIT_CARGO_FEATURES "capture-pattern,capture-clock")
set(LIVEKIT_CAPTURE_ENABLED 1)
endif()

set(GIT_COMMIT "unknown")
set(RUST_GIT_COMMIT "unknown")
find_package(Git QUIET)
Expand Down Expand Up @@ -262,6 +271,11 @@ if(NOT CFG STREQUAL \"Debug\")
list(APPEND ARGS --release)
endif()

if(DEFINED CARGO_FEATURES AND NOT CARGO_FEATURES STREQUAL \"\")
list(APPEND ARGS --features \"\${CARGO_FEATURES}\")
message(STATUS \"[run_cargo.cmake] Cargo features: \${CARGO_FEATURES}\")
endif()

if(DEFINED RUST_TARGET AND NOT RUST_TARGET STREQUAL \"\")
list(APPEND ARGS --target \"\${RUST_TARGET}\")
message(STATUS \"[run_cargo.cmake] Cross-compiling for target: \${RUST_TARGET}\")
Expand Down Expand Up @@ -378,6 +392,7 @@ add_custom_command(
-DPROTOC_PATH=${Protobuf_PROTOC_EXECUTABLE}
-DRUST_TARGET=${RUST_TARGET_TRIPLE}
-DGCC_LIB_DIR=${GCC_LIB_DIR}
-DCARGO_FEATURES=${LIVEKIT_CARGO_FEATURES}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Capture-enabled builds can reuse disabled FFI

When separate build directories share a Rust target, LIVEKIT_ENABLE_CAPTURE can change without rebuilding its existing FFI artifact. The new build advertises capture support while CaptureSource::create still uses the disabled library.

Learn more

The Rust library output path is shared across configurations with the same Rust profile, but the custom command only tracks Rust source files. A second build directory can see the first one's existing artifact as up to date despite requesting different Cargo features. The generated kCaptureEnabled constant then describes CMake configuration, not the library actually loaded.

Example: Build a capture-disabled Debug configuration, then configure a different Debug build directory with LIVEKIT_ENABLE_CAPTURE=ON and build without changing Rust sources. Its FFI output already exists, so Cargo is skipped and pattern creation fails despite kCaptureEnabled == true.

Recommended fix: Give each feature configuration its own Rust target/output directory, or add a feature-specific stamp that forces Cargo to run whenever the feature selection differs from the library's actual build. Ensure header generation and the selected FFI artifact use the same feature set.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

-P "${RUN_CARGO_SCRIPT}"
WORKING_DIRECTORY "${RUST_ROOT}"
DEPENDS ${RUST_SOURCES}
Expand All @@ -396,6 +411,7 @@ include(uniffi_cpp)
add_library(livekit SHARED
src/audio_frame.cpp
src/audio_processing_module.cpp
src/capture_source.cpp
src/audio_source.cpp
src/audio_stream.cpp
src/data_track_frame.cpp
Expand Down
36 changes: 24 additions & 12 deletions CMakePresets.json
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,8 @@
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"LIVEKIT_BUILD_EXAMPLES": "ON",
"LIVEKIT_BUILD_TESTS": "ON"
"LIVEKIT_BUILD_TESTS": "ON",
"LIVEKIT_ENABLE_CAPTURE": "ON"
}
},
{
Expand All @@ -226,7 +227,8 @@
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"LIVEKIT_BUILD_EXAMPLES": "ON",
"LIVEKIT_BUILD_TESTS": "ON"
"LIVEKIT_BUILD_TESTS": "ON",
"LIVEKIT_ENABLE_CAPTURE": "ON"
}
},
{
Expand All @@ -238,7 +240,8 @@
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"LIVEKIT_BUILD_EXAMPLES": "OFF",
"LIVEKIT_BUILD_TESTS": "ON"
"LIVEKIT_BUILD_TESTS": "ON",
"LIVEKIT_ENABLE_CAPTURE": "ON"
}
},
{
Expand All @@ -250,7 +253,8 @@
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"LIVEKIT_BUILD_EXAMPLES": "OFF",
"LIVEKIT_BUILD_TESTS": "ON"
"LIVEKIT_BUILD_TESTS": "ON",
"LIVEKIT_ENABLE_CAPTURE": "ON"
}
},
{
Expand All @@ -262,7 +266,8 @@
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"LIVEKIT_BUILD_EXAMPLES": "ON",
"LIVEKIT_BUILD_TESTS": "ON"
"LIVEKIT_BUILD_TESTS": "ON",
"LIVEKIT_ENABLE_CAPTURE": "ON"
}
},
{
Expand All @@ -274,7 +279,8 @@
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"LIVEKIT_BUILD_EXAMPLES": "ON",
"LIVEKIT_BUILD_TESTS": "ON"
"LIVEKIT_BUILD_TESTS": "ON",
"LIVEKIT_ENABLE_CAPTURE": "ON"
}
},
{
Expand All @@ -286,7 +292,8 @@
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"LIVEKIT_BUILD_EXAMPLES": "OFF",
"LIVEKIT_BUILD_TESTS": "ON"
"LIVEKIT_BUILD_TESTS": "ON",
"LIVEKIT_ENABLE_CAPTURE": "ON"
}
},
{
Expand All @@ -298,7 +305,8 @@
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"LIVEKIT_BUILD_EXAMPLES": "OFF",
"LIVEKIT_BUILD_TESTS": "ON"
"LIVEKIT_BUILD_TESTS": "ON",
"LIVEKIT_ENABLE_CAPTURE": "ON"
}
},
{
Expand All @@ -310,7 +318,8 @@
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"LIVEKIT_BUILD_EXAMPLES": "ON",
"LIVEKIT_BUILD_TESTS": "ON"
"LIVEKIT_BUILD_TESTS": "ON",
"LIVEKIT_ENABLE_CAPTURE": "ON"
}
},
{
Expand All @@ -322,7 +331,8 @@
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"LIVEKIT_BUILD_EXAMPLES": "ON",
"LIVEKIT_BUILD_TESTS": "ON"
"LIVEKIT_BUILD_TESTS": "ON",
"LIVEKIT_ENABLE_CAPTURE": "ON"
}
},
{
Expand All @@ -334,7 +344,8 @@
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"LIVEKIT_BUILD_EXAMPLES": "OFF",
"LIVEKIT_BUILD_TESTS": "ON"
"LIVEKIT_BUILD_TESTS": "ON",
"LIVEKIT_ENABLE_CAPTURE": "ON"
}
},
{
Expand All @@ -346,7 +357,8 @@
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"LIVEKIT_BUILD_EXAMPLES": "OFF",
"LIVEKIT_BUILD_TESTS": "ON"
"LIVEKIT_BUILD_TESTS": "ON",
"LIVEKIT_ENABLE_CAPTURE": "ON"
}
}
],
Expand Down
17 changes: 12 additions & 5 deletions build.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,23 @@

// Version injected by CMake (CI should set it). Defaults to 0.0.0 for dev builds.
#define LIVEKIT_BUILD_VERSION "@LIVEKIT_VERSION@"
#define LIVEKIT_BUILD_FLAVOR "cpp"
#define LIVEKIT_BUILD_FLAVOR "cpp"

namespace livekit {

/// @brief Whether the Rust FFI was built with capture-source support.
inline constexpr bool kCaptureEnabled = "@LIVEKIT_CAPTURE_ENABLED@"[0] == '1';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer #define LIVEKIT_CAPTURE_ENABLED over a global bool, even though more out dated

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in #276


} // namespace livekit

#ifdef DEBUG
#define LIVEKIT_BUILD_SUFFIX "-debug"
#define LIVEKIT_BUILD_SUFFIX "-debug"
#else
#define LIVEKIT_BUILD_SUFFIX "-release"
#define LIVEKIT_BUILD_SUFFIX "-release"
#endif

// Follow params are auto generated by CMakeLists.txt
#define LIVEKIT_BUILD_DATE "@BUILD_DATE@"
#define LIVEKIT_BUILD_DATE "@BUILD_DATE@"
#define LIVEKIT_BUILD_COMMIT "@GIT_COMMIT@"

#define LIVEKIT_BUILD_VERSION_FULL LIVEKIT_BUILD_VERSION LIVEKIT_BUILD_SUFFIX
#define LIVEKIT_BUILD_VERSION_FULL LIVEKIT_BUILD_VERSION LIVEKIT_BUILD_SUFFIX
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Additional documentation for the SDK.

- [Building](building.md) — prerequisites, build scripts, CMake presets,
vcpkg, integration into your CMake project, troubleshooting.
- [Built-in capture sources](capture-sources.md) — pattern and clock sources,
publishing, and capture lifecycle.
- [Authentication](authentication.md) — generating tokens, token source types
(initial connect only), and in-session token refresh.
- [Logging](logging.md) — compile-time vs runtime filtering, log levels,
Expand Down
1 change: 1 addition & 0 deletions docs/building.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ cmake --build build
| Option | Default | Description |
|--------|---------|-------------|
| `LIVEKIT_BUILD_EXAMPLES` | OFF | Build example applications |
| `LIVEKIT_ENABLE_CAPTURE` | OFF | Build Rust pattern and clock capture sources; the repository's build presets enable this option |
| `LIVEKIT_USE_SYSTEM_PROTOBUF` | OFF | Use system Protobuf instead of vcpkg's |
| `LIVEKIT_LOG_LEVEL` | `TRACE` | Compile-time log threshold (see [logging.md](logging.md)) |
| `LIVEKIT_VERSION` | repo-derived | SDK version string baked into the binary |
Expand Down
42 changes: 42 additions & 0 deletions docs/capture-sources.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Built-in capture sources

`CaptureSource` connects a Rust video producer to a regular C++ `VideoSource`.
Pattern and clock sources render offscreen on the GPU. Their frames stay in
Rust until they enter the WebRTC video source, so C++ does not send one FFI
request per frame.

The pattern source offers `Pattern::Gradient` and `Pattern::Logo`. The clock
source displays the local time with millisecond precision.

Only the repository's `*-tests` and `*-all` build presets enable
`LIVEKIT_ENABLE_CAPTURE`. Other presets and custom CMake configurations must set
`-DLIVEKIT_ENABLE_CAPTURE=ON`; otherwise source creation fails. A GPU adapter must be available at runtime.

Create a source, publish its video source as a local track, and start capture
after publication:

```cpp
#include <livekit/livekit.h>

auto capture = livekit::CaptureSource::create(
livekit::PatternVideoSourceConfig{{1280, 720}, 30, livekit::Pattern::Gradient}).get();
// For a local wall clock, use:
// livekit::CaptureSource::create(livekit::ClockVideoSourceConfig{{1280, 720}, 30}).get();

auto track = livekit::LocalVideoTrack::createLocalVideoTrack("capture", capture->videoSource());
auto participant = room.localParticipant().lock();
participant->publishTrack(track, capture->publishOptions());

capture->setOnFinishedCallback([](const livekit::CaptureResult& result) {
// Runs on the FFI callback thread. Hand work to another thread if it blocks.
});
if (!capture->start()) {
// Handle the start failure.
}
```

The example assumes `room` is connected and the SDK is initialized. Keep
`capture` alive while its track is published. Call `stop()` to finish capture;
the callback reports the exit reason and number of captured frames. Destroying
the source also stops capture. The clock displays the local time of the machine
running the Rust FFI.
Loading
Loading