Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
3c87f5f
feat: commit initial data streams v2 implementation
1egoman Jun 26, 2026
f135aa5
fix: Create data_streams_v2.md
1egoman Jun 26, 2026
b330aea
fix: remove ClientCapability from prelude
1egoman Jul 1, 2026
2888a0a
feat: allow data stream compress option to be toggled via livekit-ffi
1egoman Jul 1, 2026
02df9da
fix: run cargo fmt
1egoman Jul 1, 2026
5e38e18
generated protobuf
github-actions[bot] Jul 1, 2026
4321e31
fix: address toc-tou issue with decompressor state
1egoman Jul 1, 2026
61c6c9e
fix: cleanup data stream v2 test fixture initialization
1egoman Jul 1, 2026
6209bc7
feat: make client capabilities proto deserialization use TryFrom
1egoman Jul 1, 2026
2c85219
fix: run cargo fmt
1egoman Jul 1, 2026
28e7b77
fix: remove dead code
1egoman Jul 1, 2026
040b0e6
generated protobuf
github-actions[bot] Jul 1, 2026
cf79cc6
fix: downgrade protocol to before data track schema metadata
1egoman Jul 1, 2026
9a87b66
refactor: convert data stream open tuple to struct and add is_compres…
1egoman Jul 1, 2026
3630dbe
feat: add is_compressed / is_inline to ByteStreamInfo / TextStreamInfo
1egoman Jul 1, 2026
1d29c7c
Revert "refactor: convert data stream open tuple to struct and add is…
1egoman Jul 1, 2026
1f31fb5
feat: explicitly check is_compressed / is_inline in e2e tests
1egoman Jul 1, 2026
1c94ab5
fix: add e2e test that tests compress=false path
1egoman Jul 1, 2026
434ae6f
feat: add e2e feature on compressed / inline fields
1egoman Jul 1, 2026
f6dcba9
feat: get random bytes e2e test to pass by using much higher quality …
1egoman Jul 1, 2026
043342a
refactor: move RemoteParticipantRegistry to participant/registry.rs
1egoman Jul 2, 2026
96427be
refactor: restructure send_text / send_bytes
1egoman Jul 2, 2026
a9517db
feat: throw error when remove participant disconnects halfway through…
1egoman Jul 2, 2026
8017b70
fix: adjust send_text / send_bytes to make the disambiguation between…
1egoman Jul 2, 2026
cbc3d1e
fix: clean up incoming stream manager
1egoman Jul 2, 2026
9ae07ee
fix: preload buffer length
1egoman Jul 2, 2026
b16440d
fix: add reason labels on e2e test assertions
1egoman Jul 2, 2026
7d9c827
refactor: depend on livekit_common::RemoteParticipantRegistry
1egoman Jul 2, 2026
ec2f0c7
fix: remove arc wrapping around reserved_topics
1egoman Jul 6, 2026
4d96df6
refactor: rename is_topic_internal -> is_internal_topic, is_stream_in…
1egoman Jul 6, 2026
1e0f40b
refactor: split up livekit-data-stream crate into multiple smaller mo…
1egoman Jul 6, 2026
9dbf2a1
fix: run cargo fmt
1egoman Jul 6, 2026
880dda1
fix: add missing OperationType reexport
1egoman Jul 6, 2026
1cf1a34
refactor: make pseudo_random_text easier to understand
1egoman Jul 6, 2026
1ea2ccf
fix: run cargo fmt
1egoman Jul 6, 2026
5c166f8
fix: drop recv_reader
1egoman Jul 6, 2026
67cb6a3
fix: add MaybeCompressed docstring
1egoman Jul 6, 2026
d0cbfc3
refactor: reformat somewhat_compressible to use rand
1egoman Jul 6, 2026
b0ffd9e
Revert "refactor: drop unused ClientCapability, RemoteParticipantRegi…
1egoman Jul 8, 2026
73e7afd
Revert "refactor: merge livekit-data-stream __e2e-test feature into t…
1egoman Jul 8, 2026
ad00257
fix: drop duplicate outgoing test helpers introduced by rebase merge
1egoman Jul 8, 2026
1902c2a
refactor: use livekit_common::enum_dispatch in data-stream info dispatch
1egoman Jul 8, 2026
7960420
feat: add async-compression and thread it through via the new MaybeCo…
1egoman Jul 8, 2026
cffaf36
feat: add new local struct / enum layer in livekit-data-stream mirror…
1egoman Jul 9, 2026
c95bd3f
refactor: port outgoing data stream manager to use new packet types
1egoman Jul 9, 2026
0df6add
feat: convert incoming data stream manager to actor
1egoman Jul 9, 2026
05ad3b5
fix: address typos in test only data stream fields
1egoman Jul 9, 2026
44c6bce
refactor: rename {Incoming,Outgoing}StreamManager -> *DataStreamManager
1egoman Jul 9, 2026
2552a56
fix: run cargo fmt
1egoman Jul 9, 2026
08c9e9a
docs: remove out of date data stream comments
1egoman Jul 9, 2026
7000fcd
refactor: re-organize data streams v2 changes to more closely match d…
1egoman Jul 10, 2026
0ad5c31
refactor: move INTERNAL_DATA_STREAM_TOPICS to reduce diff complexity
1egoman Jul 10, 2026
4bf538d
fix: swap custom random generation for standard `rand` crate usage
1egoman Jul 10, 2026
5308924
fix: rename identifier to have a unit
1egoman Jul 10, 2026
71b3843
refactor: centralize data streams test random noise generation helpers
1egoman Jul 10, 2026
c1e6867
refactor: remove flate2 from livekit-data-streams
1egoman Jul 10, 2026
b1817d4
fix: remove remnants of __e2e-test livekit-data-stream feature
1egoman Jul 10, 2026
dd6a9d1
refactor: change variable names and reindent
1egoman Jul 10, 2026
2dc9945
fix: remove prost typos
1egoman Jul 10, 2026
571c558
fix: add missing test_utils module
1egoman Jul 10, 2026
16c39e7
fix: run cargo fmt
1egoman Jul 10, 2026
9ea67bc
fix: ensure that data stream outgoing manager is shut down when forwa…
1egoman Jul 10, 2026
aba4232
fix: add docstring for data stream options id
1egoman Jul 13, 2026
9762623
fix: broke apart matches! conditional
1egoman Jul 13, 2026
ab84115
fix: remove pub(crate)
1egoman Jul 13, 2026
8217d1f
feat: add builder methods for StreamByteOptions / StreamTextOptions
1egoman Jul 13, 2026
1de743b
feat: port to livekit-data-stream builder pattern crate wide
1egoman Jul 13, 2026
f45696f
feat: wire up progress events for data streams
1egoman Jul 13, 2026
614678e
fix: bump year in copyright header
1egoman Jul 15, 2026
dfef02d
refactor: simplify needless variable assignment
1egoman Jul 15, 2026
9aebaa9
fix: drop non needed comment
1egoman Jul 15, 2026
c0305f0
fix: unify use statements
1egoman Jul 15, 2026
19bcfc4
refactor: replace .drain().collect() with .split_off() to get rid of …
1egoman Jul 15, 2026
4cedd53
fix: remove pub on StreamId.0
1egoman Jul 15, 2026
501611e
feat: add close_streams_with_error to get rid of extra allocation
1egoman Jul 15, 2026
2bb2911
refactor: rename close_streams_with_error -> close_matching_streams_w…
1egoman Jul 15, 2026
bafd8ad
feat: add max data stream size limit configurable on room level
1egoman Jul 15, 2026
cef3476
fix: ensure that decompress state tracks output bytes, not input bytes
1egoman Jul 15, 2026
569e364
refactor: adjust StreamByteOptions / StreamTextOptions to use builder…
1egoman Jul 15, 2026
bf3a161
feat: add e2e test to verify max data stream room length option works
1egoman Jul 15, 2026
9d0d559
refactor: rename on_ -> handle_
1egoman Jul 15, 2026
43d5087
fix: run cargo fmt
1egoman Jul 15, 2026
3edacae
docs: add livekit-data-stream AGENTS.md file
1egoman Jul 15, 2026
cc6af0d
feat: add data stream max size limit to inline payload path as well
1egoman Jul 15, 2026
d5384dd
fix: use StreamByteOptions builder in example
1egoman Jul 15, 2026
53e837c
feat: add enforce_header_size unit tests
1egoman Jul 15, 2026
82b804d
fix: update remaining examples to use new_with_topic
1egoman Jul 15, 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
10 changes: 10 additions & 0 deletions .changeset/data_streams_v2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
livekit: patch
livekit-api: patch
livekit-datatrack: patch
livekit-ffi: patch
livekit-protocol: patch
livekit-uniffi: patch
---

Add data streams v2 - #1192 (@1egoman)
43 changes: 38 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion examples/basic_text_stream/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
async fn run_sender(room: Room) -> Result<(), Box<dyn Error>> {
println!("Running as sender");
loop {
let options = StreamTextOptions { topic: TOPIC.to_string(), ..Default::default() };
let options = StreamTextOptions::new_with_topic(TOPIC);
let writer = room.local_participant().stream_text(options).await?;
println!("Opened new stream");

Expand Down
5 changes: 1 addition & 4 deletions examples/encrypted_text_stream/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,7 @@ async fn run_interactive_chat(
}

// Send the message
let options = StreamTextOptions {
topic: "lk.chat".to_string(),
..Default::default()
};
let options = StreamTextOptions::new_with_topic("lk.chat");

match room.local_participant().send_text(&input, options).await {
Ok(_) => {
Expand Down
2 changes: 1 addition & 1 deletion examples/send_bytes/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ async fn run_sender(room: Room) -> Result<(), Box<dyn Error>> {

println!("[tx] {}", packet);

let options = StreamByteOptions { topic: LED_CONTROL_TOPIC.into(), ..Default::default() };
let options = StreamByteOptions::new_with_topic(LED_CONTROL_TOPIC);
let be_bytes = packet.into_bits().to_be_bytes();
room.local_participant().send_bytes(&be_bytes, options).await?;

Expand Down
17 changes: 12 additions & 5 deletions livekit-api/src/signal_client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,21 @@
pub const PROTOCOL_VERSION: u32 = 17;

/// Capabilities the Rust SDK advertises to the SFU at connect time.
const CLIENT_CAPABILITIES: &[proto::client_info::Capability] =
&[proto::client_info::Capability::CapPacketTrailer];

pub use livekit_common::{CLIENT_PROTOCOL_DATA_STREAM_RPC, CLIENT_PROTOCOL_DEFAULT};
///
/// `CapCompressionDeflateRaw` is always advertised because the SDK's deflate-raw codec
/// (flate2/miniz_oxide) is pure-Rust and compiled in unconditionally.
const CLIENT_CAPABILITIES: &[proto::client_info::Capability] = &[
proto::client_info::Capability::CapPacketTrailer,
proto::client_info::Capability::CapCompressionDeflateRaw,
];

pub use livekit_common::{
CLIENT_PROTOCOL_DATA_STREAM_RPC, CLIENT_PROTOCOL_DATA_STREAM_V2, CLIENT_PROTOCOL_DEFAULT,
};

/// The client protocol which is sent to other clients and indicates the set of apis that other
/// clients should assume this client supports.
const CLIENT_PROTOCOL_VERSION: i32 = CLIENT_PROTOCOL_DATA_STREAM_RPC;
const CLIENT_PROTOCOL_VERSION: i32 = CLIENT_PROTOCOL_DATA_STREAM_V2;

#[derive(Error, Debug)]
pub enum SignalError {
Expand Down Expand Up @@ -1013,7 +1020,7 @@
/// in `send`. The stream slot is None so any actual write would be dropped,
/// which is fine — these tests only assert which side of the queue each
/// message lands on.
fn make_stub_inner() -> Arc<SignalInner> {

Check warning on line 1023 in livekit-api/src/signal_client/mod.rs

View workflow job for this annotation

GitHub Actions / livekit-api (signal-client (async), cargo test -p livekit-api --no-default-features --features si...

function `make_stub_inner` is never used
Arc::new(SignalInner {
stream: AsyncRwLock::new(None),
token: Mutex::new(String::new()),
Expand Down
69 changes: 68 additions & 1 deletion livekit-common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
// limitations under the License.

//! Foundational types shared across LiveKit crates: participant identities, the
//! encryption enum, and client-protocol constants.
//! encryption/capability enums, client-protocol constants, and the remote-participant
//! registry trait consulted by the data-stream and RPC send paths.

use std::fmt::Display;

Expand All @@ -31,6 +32,9 @@ pub const CLIENT_PROTOCOL_DEFAULT: i32 = 0;
/// RPC v2 (see RPC spec).
pub const CLIENT_PROTOCOL_DATA_STREAM_RPC: i32 = 1;

/// Understands inline single-packet data streams (data streams v2).
pub const CLIENT_PROTOCOL_DATA_STREAM_V2: i32 = 2;

// -------------------------------------------------------------------------------------------------
// ParticipantIdentity
// -------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -109,3 +113,66 @@ impl From<EncryptionType> for i32 {
}
}
}

// -------------------------------------------------------------------------------------------------
// ClientCapability
// -------------------------------------------------------------------------------------------------

/// A capability a participant's client advertises, mirroring the `ClientInfo.Capability` protobuf
/// enum.
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[non_exhaustive]
pub enum ClientCapability {
Unused,
PacketTrailer,
CompressionDeflateRaw,
}

impl TryFrom<i32> for ClientCapability {
type Error = &'static str;

fn try_from(value: i32) -> Result<Self, Self::Error> {
match proto::client_info::Capability::try_from(value) {
Ok(proto::client_info::Capability::CapPacketTrailer) => Ok(Self::PacketTrailer),
Ok(proto::client_info::Capability::CapCompressionDeflateRaw) => {
Ok(Self::CompressionDeflateRaw)
}
Ok(proto::client_info::Capability::CapUnused) => Ok(Self::Unused),
Err(_) => Err("unknown client capability"),
}
}
}

impl From<ClientCapability> for i32 {
fn from(value: ClientCapability) -> Self {
match value {
ClientCapability::Unused => proto::client_info::Capability::CapUnused as i32,
ClientCapability::PacketTrailer => {
proto::client_info::Capability::CapPacketTrailer as i32
}
ClientCapability::CompressionDeflateRaw => {
proto::client_info::Capability::CapCompressionDeflateRaw as i32
}
}
}
}

// -------------------------------------------------------------------------------------------------
// RemoteParticipantRegistry
// -------------------------------------------------------------------------------------------------

/// Read access to remote participants' advertised protocol and capabilities.
///
/// Used by downstream modules like the the RPC transport (v1/v2 transport selection) and
/// the data-stream send path (inline / compression eligibility) to determine what level of support
/// a participant has for protocol level features.
pub trait RemoteParticipantRegistry: Send + Sync {
/// A remote participant's `client_protocol`, or `CLIENT_PROTOCOL_DEFAULT` (0) if unknown.
fn remote_client_protocol(&self, identity: &ParticipantIdentity) -> i32;

/// A remote participant's advertised capabilities, or empty if unknown.
fn remote_capabilities(&self, identity: &ParticipantIdentity) -> Vec<ClientCapability>;

/// The identities of every remote participant, used to resolve a broadcast send.
fn remote_identities(&self) -> Vec<ParticipantIdentity>;
}
44 changes: 44 additions & 0 deletions livekit-data-stream/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# AGENTS.md

## Architectural overview

- This crate encapsulates the business logic and public APIs for the data streams feature: [text streams](https://docs.livekit.io/transport/data/text-streams/) and [byte streams](https://docs.livekit.io/transport/data/byte-streams/)
- Not for direct consumption by developers
- Unlike most SDK features which live directly in the [`livekit`](../livekit/) crate, data streams are intentionally isolated here for several reasons:
- Enforces decoupling from other components (e.g., data channel, signaling client, etc.)
- Enables proper integration testing
- Enables shared implementation amongst multiple _consumers_:
- [`livekit`](../livekit/): Rust client SDK
- [`livekit-uniffi`](../livekit-uniffi/): will eventually power downstream client SDKs such as Swift and Kotlin

## Incoming vs. outgoing split

- The crate is organized into two halves:
- `incoming/`: receiving streams from remote participants (produces readers)
- `outgoing/`: sending streams to remote participants (produces writers)
- The two halves never communicate with each other or share state
- Shared types live at the crate root rather than inside either side:
- wire/domain packet types (`Header`, `Chunk`, `Trailer`, `Packet`, `StreamId`, ...) in `types/`
- `ByteStreamInfo` / `TextStreamInfo` in `info.rs`
- `StreamError`, `StreamResult`, `StreamProgress`, `SendError` in `utils.rs`
- helpers such as UTF-8-aware chunking in `utf8_chunk.rs`

### The halves are deliberately *not* symmetric

Unlike the mirror-image `local/`/`remote/` split in [`livekit-datatrack`](../livekit-datatrack/AGENTS.md), `incoming/` and `outgoing/` do **not** share a parallel shape. Each has a `manager`, but they are built differently on purpose:

- The **incoming** manager (`incoming/manager.rs`) is an **actor**. It owns all receive-side state on a single task and is driven by `InputEvent`s fed over a channel, emitting `OutputEvent`s for the host to surface (see `incoming/events.rs`). This is required because inbound packets arrive from the engine's event loop in a context that cannot `.await` into the manager, inbound chunks must never be dropped (a dropped chunk is an unrecoverable `MissedChunk`), and processing must not head-of-line-block that loop. Owning its state directly also lets its handlers `.await` decompression on the run-loop task without holding a lock across the await point.
- The **outgoing** manager (`outgoing/manager.rs`) is a plain struct with `async` methods that callers `.await` directly. It runs in an already-async context (publishing), so awaiting into it is fine and the actor machinery (events, channels, a run loop) would add substantial complexity for no benefit.

This asymmetry is a conscious choice, **not** an oversight or unfinished work. Do not "symmetrize" the outgoing side into an actor preemptively. When changing behavior on one side, there is often no mirror to update on the other — verify rather than assume.

## Boundaries

- Two public modules get exported from this crate (see `lib.rs`):
1. `api`: public APIs that get re-exported by _consumers_ and made available to developers (readers, writers, stream options, stream infos, `StreamError`, ...)
2. `backend`: managers and supporting wire types used internally by _consumers_ to power the feature (`backend::incoming`, `backend::outgoing`, and the domain packet types)
- The incoming actor's events are decoupled from protocol messages for several reasons:
- Protobuf is a wire format and cannot express Rust-level invariants
- Events can carry in-process types proto cannot (e.g., the channel senders used to feed a reader)
- Allows the protocol to evolve independently
- Wire <-> domain conversion lives in `types/packet.rs`, which owns the `From`/`TryFrom` impls between `livekit_protocol::data_stream` messages and this crate's own packet types. Consumers convert inbound proto into domain `Packet`s before feeding the incoming actor, so the incoming side only ever sees domain types.
12 changes: 7 additions & 5 deletions livekit-data-stream/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ edition.workspace = true
repository.workspace = true

[features]
# Exposes test-only hooks used by downstream crates' test suites (e.g.
# `TextStreamReader::new_for_test`). Forwarded from the `livekit` crate's `__lk-e2e-test` feature.
test-utils = []
test-utils = ["dep:rand"]

[dependencies]
livekit-common = { workspace = true }
Expand All @@ -20,12 +18,16 @@ thiserror = { workspace = true }
parking_lot = { workspace = true }
bytes = { workspace = true }
tokio = { workspace = true, default-features = false, features = ["sync", "fs", "io-util", "rt"] }
futures-util = { workspace = true, default-features = false, features = ["sink"] }
tokio-stream = { workspace = true, features = ["sync"] }
futures-util = { workspace = true, default-features = false, features = ["sink", "io"] }
prost = "0.12"
chrono = "0.4.38"
flate2 = "1"
bmrng = "0.5.2"
uuid = { version = "1", features = ["v4"] }
async-compression = { version = "0.4.42", features = ["deflate", "futures-io"] }
from_variants.workspace = true
rand = { workspace = true, optional = true }

[dev-dependencies]
tokio = { workspace = true, default-features = false, features = ["macros", "rt", "rt-multi-thread", "time"] }
rand = { workspace = true }
Loading
Loading