Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
eddb9ef
feat: add data streams to livekit-uniffi
1egoman Jul 24, 2026
0844071
feat: add example testing script for data streams v2 uniffi
1egoman Jul 27, 2026
39590fa
feat: make data stream error not a flat error
1egoman Jul 30, 2026
ee63000
fix: drop ByteStreamReader::write_to_file
1egoman Jul 30, 2026
4d42599
fix: fix compile error
1egoman Jul 30, 2026
906681f
feat(data-stream): abort_all_streams / abort_streams_from on incoming…
1egoman Aug 5, 2026
1c5b373
feat(data-stream): expose writer is_open over the FFI
1egoman Aug 5, 2026
0986569
fix(uniffi): make the data stream bindings compile for Kotlin
1egoman Aug 6, 2026
04fc6ed
fix: temporarily switch over to personal uniffi-dart fork
1egoman Aug 10, 2026
2b71355
feat: add data streams dart polling manager adapter
1egoman Aug 10, 2026
322bb1c
Create data_streams_v2_uniffi.md
1egoman Aug 10, 2026
2fa0249
fix: add override for close method name for dart uniffi helper
1egoman Aug 11, 2026
f2e17a1
fix: remove kotlin checksums for now
1egoman Aug 11, 2026
d899a8d
fix: add livekit-datatrack to knope changeset
1egoman Aug 11, 2026
90fd57b
feat: emit StreamClosed for incoming data streams and forward it over…
1egoman Aug 17, 2026
7dda1f9
feat: propagate transport errors through the outgoing delegate and ba…
1egoman Aug 17, 2026
d9db55b
feat: take the wire encryption type in handle_packet_received and car…
1egoman Aug 17, 2026
750074e
docs: call out that max_payload_byte_length is fixed at construction
1egoman Aug 17, 2026
a728b87
feat: expose open_stream_count on the incoming data stream manager
1egoman Aug 17, 2026
ec57462
feat(uniffi): make on_packets_available async so hosts can honor its …
pblazej Aug 18, 2026
c714f3d
fix: hold stream trailers to the stream's encryption type
1egoman Aug 18, 2026
84146ce
chore(uniffi): raise the Android size budget to 1.5 MiB
1egoman Aug 18, 2026
f8b3fed
fix: remove duplicate livekit-common entry
1egoman Aug 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
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)
10 changes: 6 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 @@ -89,6 +89,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())
25 changes: 24 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,6 +58,20 @@ 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 {
Expand Down Expand Up @@ -79,6 +101,7 @@ pub struct TrailerReceived {
#[derive(FromVariants)]
pub enum OutputEvent {
StreamOpened(StreamOpened),
StreamClosed(StreamClosed),
ChunkReceived(ChunkReceived),
TrailerReceived(TrailerReceived),
}
Loading
Loading