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
2 changes: 2 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
- src/**
- include/**
- client-sdk-rust/**
- cmake/**
- CMakeLists.txt
- CMakePresets.json
- build.sh
Expand All @@ -20,6 +21,7 @@ on:
- src/**
- include/**
- client-sdk-rust/**
- cmake/**
- CMakeLists.txt
- CMakePresets.json
- build.sh
Expand Down
28 changes: 27 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,11 +335,37 @@ Adhere to clang-tidy checks configured in `.clang-tidy`. Run `./scripts/clang-ti

| Dependency | Scope | Notes |
|------------|-------|-------|
| protobuf | Private (built-in) | Vendored via FetchContent (Unix) or vcpkg (Windows) |
| protobuf-lite | Private (built-in) | Vendored via FetchContent (Unix) or vcpkg (Windows). The SDK links **only** against `protobuf::libprotobuf-lite`; the generated FFI code uses `MessageLite`. See "Protobuf runtime" below. |
| spdlog | **Private** | FetchContent or system package; must NOT leak into public API |
| client-sdk-rust | Build-time | Git submodule, built via cargo during CMake build |
| Google Test | Test only | FetchContent in `src/tests/CMakeLists.txt` |

### Protobuf runtime

The C++ SDK uses the **protobuf-lite** runtime, not the full protobuf runtime.
The FFI `.proto` files in `client-sdk-rust/livekit-ffi/protocol/` do not opt
into lite themselves (they are shared with the Rust and Node FFI generators).
Instead, the C++ build copies them into the build tree and appends
`option optimize_for = LITE_RUNTIME;` before running `protoc`. See
`cmake/patch_protos_for_lite.cmake` and the `PATCHED_PROTO_FILES` custom
command in the top-level `CMakeLists.txt`. The original `.proto` files in the
submodule are never modified.

Consequences for code in `src/`:

- The generated `proto::*` classes extend `google::protobuf::MessageLite`, not
`Message`. Only the lite subset of the protobuf API is available.
- Allowed: `set_*` / `mutable_*` / `add_*` / `has_*` / `clear_*` / `*_size()` /
oneof `_case()` accessors, `SerializeToString`, `ParseFromArray`, `CopyFrom`,
assignment, `google::protobuf::RepeatedField`/`RepeatedPtrField`.
- **Not available**: `DebugString` / `ShortDebugString` / `Utf8DebugString`,
`TextFormat`, `JsonStringToMessage` / `MessageToJsonString`, `GetReflection`,
`GetDescriptor`, `DescriptorPool`, `MessageFactory`, Well-Known Types
(`Any` / `Timestamp` / `Duration` / `Struct` / `FieldMask` / `Empty`),
reflection-based copy/merge utilities. Do not introduce code that needs them.
- If you ever need a human-readable dump of a proto message for logging, build
a hand-written formatter — do not reach for `DebugString()`.

When adding a new private/vendored dependency to this table, also add a
forbidden symbol pattern for it to
`.github/scripts/check_no_private_symbols.py` so the "Symbol leak check"
Expand Down
92 changes: 66 additions & 26 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -71,25 +71,56 @@ if(LIVEKIT_IS_TOPLEVEL)
endif()

set(FFI_PROTO_DIR ${LIVEKIT_ROOT_DIR}/client-sdk-rust/livekit-ffi/protocol)
set(FFI_PROTO_FILES
${FFI_PROTO_DIR}/handle.proto
${FFI_PROTO_DIR}/ffi.proto
${FFI_PROTO_DIR}/participant.proto
${FFI_PROTO_DIR}/room.proto
${FFI_PROTO_DIR}/track.proto
${FFI_PROTO_DIR}/video_frame.proto
${FFI_PROTO_DIR}/audio_frame.proto
${FFI_PROTO_DIR}/e2ee.proto
${FFI_PROTO_DIR}/stats.proto
${FFI_PROTO_DIR}/data_stream.proto
${FFI_PROTO_DIR}/data_track.proto
${FFI_PROTO_DIR}/rpc.proto
${FFI_PROTO_DIR}/track_publication.proto
set(FFI_PROTO_NAMES
handle.proto
ffi.proto
participant.proto
room.proto
track.proto
video_frame.proto
audio_frame.proto
e2ee.proto
stats.proto
data_stream.proto
data_track.proto
rpc.proto
track_publication.proto
)
set(FFI_PROTO_FILES)
foreach(_p ${FFI_PROTO_NAMES})
list(APPEND FFI_PROTO_FILES "${FFI_PROTO_DIR}/${_p}")
endforeach()
set(PROTO_BINARY_DIR ${LIVEKIT_BINARY_DIR}/generated)
file(MAKE_DIRECTORY ${PROTO_BINARY_DIR})

# Livekit static protobuf.
# Staging area for protobuf-lite-patched copies of the FFI .proto files.
# We never modify the originals (they're shared with the Rust submodule and
# other language wrappers); instead we copy them here and append
# `option optimize_for = LITE_RUNTIME;` so protoc emits MessageLite-based code
# that links only against libprotobuf-lite.
set(PROTO_LITE_SRC_DIR ${PROTO_BINARY_DIR}/proto-lite)
file(MAKE_DIRECTORY ${PROTO_LITE_SRC_DIR})

set(PATCHED_PROTO_FILES)
foreach(_p ${FFI_PROTO_NAMES})
list(APPEND PATCHED_PROTO_FILES "${PROTO_LITE_SRC_DIR}/${_p}")
endforeach()

add_custom_command(
OUTPUT ${PATCHED_PROTO_FILES}
COMMAND ${CMAKE_COMMAND}
-DIN_DIR=${FFI_PROTO_DIR}
-DOUT_DIR=${PROTO_LITE_SRC_DIR}
"-DFILES=${FFI_PROTO_NAMES}"
-P ${LIVEKIT_ROOT_DIR}/cmake/patch_protos_for_lite.cmake
DEPENDS
${FFI_PROTO_FILES}
${LIVEKIT_ROOT_DIR}/cmake/patch_protos_for_lite.cmake
COMMENT "Patching .proto files with optimize_for = LITE_RUNTIME (C++ build only)"
VERBATIM
)

# Protobuf toolchain and lite runtime.
include(protobuf)
# spdlog logging library (PRIVATE dependency).
include(spdlog)
Expand All @@ -101,18 +132,24 @@ elseif(NOT Protobuf_PROTOC_EXECUTABLE)
endif()
message(STATUS "Using protoc: ${Protobuf_PROTOC_EXECUTABLE}")

add_library(livekit_proto OBJECT ${FFI_PROTO_FILES})
add_library(livekit_proto OBJECT ${PATCHED_PROTO_FILES})
# livekit_proto is compiled into liblivekit; apply the same hidden visibility
# policy so generated protobuf code does not leak into the SDK's exported ABI.
set_target_properties(livekit_proto PROPERTIES
CXX_VISIBILITY_PRESET hidden
C_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN ON
)
if(TARGET protobuf::libprotobuf)
set(LIVEKIT_PROTOBUF_TARGET protobuf::libprotobuf)
# The C++ build links against protobuf-lite only. Generated code uses
# MessageLite (no Reflection/Descriptor/TextFormat/JSON), and the SDK does
# not use any of those APIs. See cmake/patch_protos_for_lite.cmake.
if(TARGET protobuf::libprotobuf-lite)
set(LIVEKIT_PROTOBUF_TARGET protobuf::libprotobuf-lite)
else()
message(FATAL_ERROR "No protobuf library target found (expected protobuf::libprotobuf)")
message(FATAL_ERROR
"protobuf::libprotobuf-lite target not found. The LiveKit C++ SDK requires "
"protobuf-lite. On Linux/macOS this comes from the vendored protobuf build; "
"on Windows it comes from the vcpkg protobuf port.")
endif()
set(LIVEKIT_PROTOBUF_DEP_INCLUDE_DIRS ${Protobuf_INCLUDE_DIRS})
if(TARGET absl::base)
Expand All @@ -136,24 +173,27 @@ target_include_directories(livekit_proto PRIVATE
)
target_link_libraries(livekit_proto PRIVATE ${LIVEKIT_PROTOBUF_TARGET})
livekit_disable_warnings(livekit_proto)
message(STATUS "Using protobuf runtime target: ${LIVEKIT_PROTOBUF_TARGET}")

# Manually generate protobuf files to avoid path prefix issues
# Manually generate protobuf files to avoid path prefix issues. The inputs are
# the patched copies in ${PROTO_LITE_SRC_DIR}, not the originals — see the
# PATCHED_PROTO_FILES custom command above.
set(PROTO_SRCS)
set(PROTO_HDRS)
foreach(PROTO_FILE ${FFI_PROTO_FILES})
get_filename_component(PROTO_NAME ${PROTO_FILE} NAME_WE)
foreach(_name ${FFI_PROTO_NAMES})
get_filename_component(PROTO_NAME "${_name}" NAME_WE)
list(APPEND PROTO_SRCS "${PROTO_BINARY_DIR}/${PROTO_NAME}.pb.cc")
list(APPEND PROTO_HDRS "${PROTO_BINARY_DIR}/${PROTO_NAME}.pb.h")
endforeach()

add_custom_command(
OUTPUT ${PROTO_SRCS} ${PROTO_HDRS}
COMMAND ${Protobuf_PROTOC_EXECUTABLE}
--proto_path=${FFI_PROTO_DIR}
--proto_path=${PROTO_LITE_SRC_DIR}
--cpp_out=${PROTO_BINARY_DIR}
${FFI_PROTO_FILES}
DEPENDS ${FFI_PROTO_FILES}
COMMENT "Generating C++ protobuf files"
${PATCHED_PROTO_FILES}
DEPENDS ${PATCHED_PROTO_FILES}
COMMENT "Generating C++ protobuf files (lite runtime)"
VERBATIM
)

Expand Down
10 changes: 5 additions & 5 deletions DEPENDENCIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ The SDK uses different distribution strategies per platform:
✅ **Ready to use** - All dependencies included:
- `livekit.lib` - Main SDK static library
- `livekit_ffi.dll` + `livekit_ffi.dll.lib` - Rust FFI dynamic library
- `libprotobuf.dll` + `libprotobuf.lib` - Protocol Buffers runtime
- `libprotobuf-lite.dll` + `libprotobuf-lite.lib` - Protocol Buffers lite runtime
- `abseil_dll.dll` + `abseil_dll.lib` - Abseil C++ library

**User action**: Copy all DLLs alongside your executable. No additional installation required.
Expand All @@ -25,7 +25,7 @@ The SDK uses different distribution strategies per platform:
⚠️ **Requires system dependencies**:
- `liblivekit.a` - Main SDK static library (included)
- `liblivekit_ffi.so` - Rust FFI dynamic library (included, **must be placed alongside your executable**)
- `libprotobuf` - Must install via `apt install libprotobuf-dev`
- `libprotobuf-lite` - provided by `apt install libprotobuf-dev`
- `libssl` - Must install via `apt install libssl-dev`
- `libabsl` - Only if built with Protobuf 6.0+: `apt install libabsl-dev`

Expand Down Expand Up @@ -175,8 +175,8 @@ target_link_libraries(your_app PRIVATE
livekit
livekit_ffi

# Protobuf (REQUIRED)
protobuf::libprotobuf
# Protobuf lite runtime (REQUIRED)
protobuf::libprotobuf-lite

# Linux system libraries (REQUIRED)
OpenSSL::SSL
Expand All @@ -196,7 +196,7 @@ g++ your_app.cpp \
-I/path/to/livekit/include \
-L/path/to/livekit/lib \
-llivekit -llivekit_ffi \
-lprotobuf -lssl -lcrypto -lpthread -ldl
-lprotobuf-lite -lssl -lcrypto -lpthread -ldl
```

---
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ cd client-sdk-cpp
```

Building requires a stable Rust toolchain and platform-specific build
deps (`protobuf`, `abseil`, `openssl` on Linux). See [docs/building.md](docs/building.md)
deps (`protobuf` for `protoc`/protobuf-lite, `abseil`, `openssl` on Linux). See [docs/building.md](docs/building.md)
for full prerequisites table, Docker recipe, CMake presets, and troubleshooting.

### Hello, LiveKit
Expand Down
61 changes: 61 additions & 0 deletions cmake/patch_protos_for_lite.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# cmake/patch_protos_for_lite.cmake
#
# Copy upstream .proto files (from client-sdk-rust/livekit-ffi/protocol/) into
# a build-tree staging directory and append `option optimize_for = LITE_RUNTIME;`
# to each one. The C++ SDK then compiles the patched copies, which causes
# protoc to emit code linking only against libprotobuf-lite (no reflection,
# no descriptors, no text-format, no JSON).
#
# This patches *only* the copies used by the C++ build. The original .proto
# files in the client-sdk-rust submodule are untouched, so prost (Rust) and
# protobuf-es (Node) generators continue to see the unmodified sources.
#
# Usage (invoked via `cmake -P`):
# cmake -DIN_DIR=<dir> -DOUT_DIR=<dir> "-DFILES=a.proto;b.proto;..." \
# -P patch_protos_for_lite.cmake

if(NOT DEFINED IN_DIR OR IN_DIR STREQUAL "")
message(FATAL_ERROR "patch_protos_for_lite.cmake: IN_DIR is required")
endif()
if(NOT DEFINED OUT_DIR OR OUT_DIR STREQUAL "")
message(FATAL_ERROR "patch_protos_for_lite.cmake: OUT_DIR is required")
endif()
if(NOT DEFINED FILES OR FILES STREQUAL "")
message(FATAL_ERROR "patch_protos_for_lite.cmake: FILES is required")
endif()

file(MAKE_DIRECTORY "${OUT_DIR}")

set(_marker "// --- appended by client-sdk-cpp: force lite runtime for C++ codegen ---")
set(_option_line "option optimize_for = LITE_RUNTIME;")

foreach(_name IN LISTS FILES)
set(_src "${IN_DIR}/${_name}")
set(_dst "${OUT_DIR}/${_name}")

if(NOT EXISTS "${_src}")
message(FATAL_ERROR "patch_protos_for_lite.cmake: missing source ${_src}")
endif()

file(READ "${_src}" _content)

# If upstream already opts into lite, keep the file unchanged. If it chooses
# another runtime, fail rather than silently overriding that choice.
string(REGEX MATCH "(^|\n)[ \t]*option[ \t]+optimize_for[ \t]*=[ \t]*LITE_RUNTIME[ \t]*;" _existing_lite "${_content}")
if(_existing_lite)
file(WRITE "${_dst}" "${_content}")
continue()
endif()

string(REGEX MATCH "(^|\n)[ \t]*option[ \t]+optimize_for[ \t]*=" _existing_runtime "${_content}")
if(_existing_runtime)
message(FATAL_ERROR
"patch_protos_for_lite.cmake: ${_name} declares a non-lite optimize_for. "
"The LiveKit C++ SDK requires LITE_RUNTIME.")
endif()

# Append the option as a trailing line. File-level options have no ordering
# requirement in protobuf grammar, so this is always safe.
file(WRITE "${_dst}"
"${_content}\n${_marker}\n${_option_line}\n")
endforeach()
Loading
Loading