Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b49270d
refactor: move all notion of "internal" data streams into livekit crate
1egoman Jul 31, 2026
c57611d
refactor: rename topic_of
1egoman Jul 31, 2026
6370f63
feat: add data streams to livekit-uniffi
1egoman Jul 24, 2026
95775ba
feat: add example testing script for data streams v2 uniffi
1egoman Jul 27, 2026
429de0b
feat: make data stream error not a flat error
1egoman Jul 30, 2026
8d44a7c
fix: drop ByteStreamReader::write_to_file
1egoman Jul 30, 2026
76f1e16
fix: fix compile error
1egoman Jul 30, 2026
38503a8
feat(data-stream): abort_all_streams / abort_streams_from on incoming…
1egoman Aug 5, 2026
9746b08
feat(data-stream): expose writer is_open over the FFI
1egoman Aug 5, 2026
1fe0eba
fix(uniffi): make the data stream bindings compile for Kotlin
1egoman Aug 6, 2026
e28e15f
fix: temporarily switch over to personal uniffi-dart fork
1egoman Aug 10, 2026
92087ba
feat: add data streams dart polling manager adapter
1egoman Aug 10, 2026
ae0f205
Create data_streams_v2_uniffi.md
1egoman Aug 10, 2026
46aace1
fix: add override for close method name for dart uniffi helper
1egoman Aug 11, 2026
c6b320d
fix: remove kotlin checksums for now
1egoman Aug 11, 2026
e3c6be5
fix: add livekit-datatrack to knope changeset
1egoman Aug 11, 2026
9dbb89b
feat: emit StreamClosed for incoming data streams and forward it over…
1egoman Aug 17, 2026
1dd54d0
feat: propagate transport errors through the outgoing delegate and ba…
1egoman Aug 17, 2026
da55afa
feat: take the wire encryption type in handle_packet_received and car…
1egoman Aug 17, 2026
d7a2b09
docs: call out that max_payload_byte_length is fixed at construction
1egoman Aug 17, 2026
6ea6605
feat: expose open_stream_count on the incoming data stream manager
1egoman Aug 17, 2026
7a2505f
feat(uniffi): make on_packets_available async so hosts can honor its …
pblazej Aug 18, 2026
1da01b4
fix: hold stream trailers to the stream's encryption type
1egoman Aug 18, 2026
a3bc76a
chore(uniffi): raise the Android size budget to 1.5 MiB
1egoman Aug 18, 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
9 changes: 9 additions & 0 deletions .changeset/data_streams_v2_uniffi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
livekit: patch
livekit-data-stream: patch
livekit-ffi: patch
livekit-uniffi: patch
livekit-datatrack: patch
---

Add data streams v2 to exposed uniffi interface - #1286 (@1egoman)
11 changes: 7 additions & 4 deletions Cargo.lock

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

9 changes: 9 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@ serde_json = "1.0"
thiserror = "2"
tokio = { version = "1", default-features = false }
tokio-stream = "0.1"
# Test on a 64-bit ARM device before you change this version.
#
# The Kotlin bindings from uniffi 0.31.2 and 0.32.0 compare each checksum incorrectly on 64-bit
# ARM. Every affected method then fails. See https://github.com/mozilla/uniffi-rs/pull/2897, which
# introduced the defect. Version 0.31.1 has a related defect on 32-bit ARM, and it also does not
# build here, because uniffi-dart requires 0.31.2 or later. mozilla/uniffi-rs#2935 corrects both
# defects, but no release (as of mid august 2026) contains that change.
#
# For this reason, livekit-uniffi sets `omit_checksums` for Kotlin. See livekit-uniffi/uniffi.toml.
uniffi = "0.31"

# For examples
Expand Down
73 changes: 73 additions & 0 deletions datastream_uniffi_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import asyncio
import livekit_uniffi

class OutgoingDelegate(livekit_uniffi.OutgoingDataStreamManagerDelegate):
def on_packets_available(self, packets):
print('PACKETS:', packets)

class RemoteParticipantRegistry(livekit_uniffi.RemoteParticipantRegistryDelegate):
def remote_capabilities(self, identity):
return [] # typing.List[ClientCapability]

def remote_client_protocol(self, identity):
return 2

def remote_identities(self):
return ["alice", "bob", "randy"]

class IncomingDelegate(livekit_uniffi.IncomingDataStreamManagerDelegate):
"""Forwards opened readers onto the main asyncio loop.

Delegate callbacks fire on a Rust tokio thread, so they must not block or await;
hand the reader off to the main loop and let it drive the async reads.
"""

def __init__(self, loop: asyncio.AbstractEventLoop, opened: asyncio.Queue):
self._loop = loop
self._opened = opened

def on_byte_stream_opened(self, reader, identity: str):
self._loop.call_soon_threadsafe(self._opened.put_nowait, ("byte", reader, identity))

def on_text_stream_opened(self, reader, identity: str):
self._loop.call_soon_threadsafe(self._opened.put_nowait, ("text", reader, identity))

# Encoded livekit.DataPacket envelopes (participant_identity = "alice") carrying a
# DataStream.Header / Chunk / Trailer for an 11-byte "hello world" text stream.
DATA_STREAM_HEADER_BYTES = b'"\x05alicej@\n\x11example-stream-id\x10\xad\xf5\xcb\xae\xf93\x1a\x08my-topic"\ntext/plain(\x0bB\n\n\x03foo\x12\x03barJ\x00'
DATA_STREAM_CHUNK_BYTES = b'"\x05alicer \n\x11example-stream-id\x1a\x0bhello world'
DATA_STREAM_TRAILER_BYTES = b'"\x05alicez\'\n\x11example-stream-id\x1a\x12\n\x06status\x12\x08complete'

async def main():
opened = asyncio.Queue()

print("--- OUTGOING:")
outgoing_delegate = OutgoingDelegate()
remote_participant_registry = RemoteParticipantRegistry()
outgoing = livekit_uniffi.OutgoingDataStreamManager(outgoing_delegate, remote_participant_registry)
await outgoing.send_text('hello world', livekit_uniffi.StreamTextOptions(
topic="test",
attributes={},
# destination_identities: 'typing.List[str]' = <object object at 0x10089cc40>,
# id: 'typing.Optional[str]' = <object object at 0x10089cc40>,
# operation_type: 'typing.Optional[OperationType]' = <object object at 0x10089cc40>,
# version: 'typing.Optional[int]' = <object object at 0x10089cc40>,
# reply_to_stream_id: 'typing.Optional[str]' = <object object at 0x10089cc40>,
# attached_stream_ids: 'typing.List[str]' = <object object at 0x10089cc40>,
# generated: 'typing.Optional[bool]' = <object object at 0x10089cc40>,
# compress: 'typing.Optional[bool]' = <object object at 0x10089cc40>,
# sender_identity: 'typing.Optional[str]' = <object object at 0x10089cc40>
))

print("--- INCOMING:")
incoming_delegate = IncomingDelegate(asyncio.get_running_loop(), opened)
incoming = livekit_uniffi.IncomingDataStreamManager(incoming_delegate, [], None)
incoming.handle_packet_received(DATA_STREAM_HEADER_BYTES)
incoming.handle_packet_received(DATA_STREAM_CHUNK_BYTES)
incoming.handle_packet_received(DATA_STREAM_TRAILER_BYTES)

kind, reader, identity = await asyncio.wait_for(opened.get(), timeout=5)
print(f"{kind.upper()} STREAM OPENED:", identity, "CONTENTS:", await reader.read_all())

if __name__ == '__main__':
asyncio.run(main())
35 changes: 34 additions & 1 deletion livekit-data-stream/src/incoming/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use livekit_common::ParticipantIdentity;

use crate::{
incoming::AnyStreamReader,
types::{Chunk, Packet, Trailer},
types::{Chunk, Packet, StreamId, Trailer},
};

pub struct PacketReceived {
Expand All @@ -39,6 +39,14 @@ pub enum InputEvent {
PacketReceived(PacketReceived),
/// Abort every open stream sent by this participant (they disconnected mid-send).
AbortStreamsFrom(ParticipantIdentity),
/// Abort every open stream (e.g. the local connection is going away). Unlike
/// [`InputEvent::Shutdown`], the run loop keeps going so streams opened later are still handled.
AbortAllStreams,
/// Reply with the number of currently open streams (registered by a header and awaiting more
/// packets). Processed in order with the other events, so the answer reflects everything
/// enqueued before it.
#[from_variants(skip)]
QueryOpenStreamCount(tokio::sync::oneshot::Sender<usize>),
/// Stop the run loop.
Shutdown,
}
Expand All @@ -50,25 +58,50 @@ pub struct StreamOpened {
pub participant_identity: ParticipantIdentity,
}

/// A stream previously announced via [`StreamOpened`] has terminated and will produce no further
/// data: its trailer arrived, its inline payload completed, it failed with an error, or it was
/// aborted.
///
/// Emitted exactly once per opened stream. Hosts delivering streams on ordered topics use this to
/// know when a stream's handler can be considered finished on the wire (a trailer alone is not
/// enough: inline single-packet streams never receive one).
pub struct StreamClosed {
pub stream_id: StreamId,
pub participant_identity: ParticipantIdentity,
/// Topic the stream was opened on.
pub topic: String,
}

/// A "raw chunk received" notification, which is used to trigger
/// the deprecated [RoomEvent:::StreamChunkReceived] event.
pub struct ChunkReceived {
pub chunk: Chunk,
pub participant_identity: ParticipantIdentity,

/// Topic of the stream this chunk belongs to, or `None` if the associated stream id could
/// not be mapped to a topic.
pub topic: Option<String>,
}

/// A "raw trailer received" notification, which is used to trigger
/// the deprecated [RoomEvent:::StreamTrailerReceived] event.
pub struct TrailerReceived {
pub trailer: Trailer,
pub participant_identity: ParticipantIdentity,

/// Topic of the stream this chunk belongs to, or `None` if the associated stream id could
/// not be mapped to a topic.
///
/// See [`ChunkReceived::topic`].
pub topic: Option<String>,
}

/// An event emitted by [`IncomingStreamManager::run`] for the host crate to surface. The manager
/// stays decoupled from `RoomEvent`; the host maps these onto its own event types.
#[derive(FromVariants)]
pub enum OutputEvent {
StreamOpened(StreamOpened),
StreamClosed(StreamClosed),
ChunkReceived(ChunkReceived),
TrailerReceived(TrailerReceived),
}
Loading