Skip to content
Draft
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 .changes/connect-room-options-ignored
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
patch type="fixed" "Room.connect no longer ignores the roomOptions argument passed to it"
1 change: 1 addition & 0 deletions .changes/data-stream-options
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
minor type="added" "ConnectOptions.dataStream with maxPayloadByteLength, bounding the payload a single incoming data stream may deliver"
1 change: 1 addition & 0 deletions .changes/data-streams-v2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
minor type="changed" "Data streams are now backed by the Rust core (livekit-uniffi) on native platforms, adding data streams v2: single-packet inline sends, deflate-raw compression and MTU-bounded headers. Adds LocalParticipant.sendBytes, a compress option, Participant.capabilities and ClientProtocolVersion.v2. Web keeps the existing Dart implementation and interoperates as a pre-v2 peer."
11 changes: 10 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,16 @@ Web/native divergence is handled with conditional imports (e.g. `track/processor

`lib/src/uniffi/` wraps `livekit_uniffi`, a Dart package generated from the `livekit-uniffi` crate in the sibling `rust-sdks` repo. It reaches Rust through Dart's Native Assets: the package's `hook/build.dart` bundles a `cdylib` into the host app and the generated bindings call into it with `@Native`. This is why the SDK requires Flutter >= 3.38 / Dart >= 3.10.

There is no dynamic library to load on the web, so `uniffi.dart` splits native/web the same way the rest of the SDK does. **`uniffi_io.dart` is the only file allowed to import `package:livekit_uniffi/...`** — importing it from anywhere reachable on web pulls `dart:ffi` into a web compile and breaks `flutter build web`/`--wasm`. Guard calls with `LiveKitUniffi.isAvailable`.
There is no dynamic library to load on the web, so `uniffi.dart` splits native/web the same way the rest of the SDK does. **Only `uniffi_io.dart` and files under `lib/src/data_stream/` whose names end in `_native.dart` (plus `ffi_bridged.dart`) may import `package:livekit_uniffi/...`** — importing it from anywhere reachable on web pulls `dart:ffi` into a web compile and breaks `flutter build web`/`--wasm`. No generated uniffi type may appear in a public API signature; convert at the boundary (`data_stream/ffi_bridged.dart`). Guard calls with `LiveKitUniffi.isAvailable`.

### Data streams

`lib/src/data_stream/` has two implementations behind one interface (`data_streams.dart`, conditional import): `data_streams_native.dart` delegates to the Rust core, which implements **data streams v2** (inline single-packet sends, deflate-raw compression, UTF-8-aware chunking, MTU-bounded headers); `data_streams_web.dart` is the original Dart v1 code, kept because the cdylib can't run in a browser. Web advertises `ClientProtocolVersion.v1` and no capabilities, so v2 senders fall back to uncompressed multi-packet for it.

Two things to know when touching the native path:

- **The core's push delegates cannot be used from Dart.** uniffi compiles a callback interface to `Pointer.fromFunction`, which is only valid on the isolate's thread, and the core invokes those delegates from its tokio runtime — the VM aborts with `Cannot invoke native callback outside an isolate`, which is not catchable. The managers are therefore built through the crate's `polled*` adapters (`livekit-uniffi/src/data_stream/polled.rs`), which implement the delegates *in Rust*, buffer into a channel, and expose an `async fn next_*` we await. `RemoteParticipantRegistryDelegate` is the one callback we implement directly, and it is safe: it is only called synchronously inside a `send*` future, which uniffi polls from the calling (Dart) thread.
- **Whoever awaits a uniffi object is the only thing that may dispose it.** Freeing the Rust handle while a `next()`/`nextPackets()` is in flight is a use-after-free that surfaces as a SIGBUS with no Dart stack. Hence readers are disposed by their pump rather than from a subscription's `onCancel`, and `dispose()` calls `close()` on the queues to wake their pumps instead of releasing them directly.

### Local development loop

Expand Down
2 changes: 2 additions & 0 deletions lib/livekit_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export 'src/connection_check/events.dart';
export 'src/constants.dart';
export 'src/core/room.dart';
export 'src/core/room_preconnect.dart';
export 'src/data_stream/errors.dart';
export 'src/data_stream/stream_reader.dart';
export 'src/data_stream/stream_writer.dart';
export 'src/e2ee/e2ee_manager.dart';
Expand Down Expand Up @@ -69,6 +70,7 @@ export 'src/track/remote/remote.dart';
export 'src/track/remote/video.dart';
export 'src/track/track.dart';
export 'src/json/agent_attributes.dart';
export 'src/types/client_capability.dart';
export 'src/types/data_stream.dart';
export 'src/types/audio_encoding.dart';
export 'src/types/other.dart';
Expand Down
28 changes: 6 additions & 22 deletions lib/src/core/engine.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1024,29 +1024,13 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
identity: dp.participantIdentity,
),
);
} else if (dp.whichValue() == lk_models.DataPacket_Value.streamHeader) {
// Data Stream Header
} else if (dp.whichValue() == lk_models.DataPacket_Value.streamHeader ||
dp.whichValue() == lk_models.DataPacket_Value.streamChunk ||
dp.whichValue() == lk_models.DataPacket_Value.streamTrailer) {
// Data stream header / chunk / trailer, forwarded whole — see EngineDataStreamPacketEvent.
events.emit(
EngineDataStreamHeaderEvent(
header: dp.streamHeader,
identity: dp.participantIdentity,
encryptionType: encryptionType,
),
);
} else if (dp.whichValue() == lk_models.DataPacket_Value.streamChunk) {
// Data Stream Chunk
events.emit(
EngineDataStreamChunkEvent(
chunk: dp.streamChunk,
identity: dp.participantIdentity,
encryptionType: encryptionType,
),
);
} else if (dp.whichValue() == lk_models.DataPacket_Value.streamTrailer) {
// Data Stream trailer
events.emit(
EngineDataStreamTrailerEvent(
trailer: dp.streamTrailer,
EngineDataStreamPacketEvent(
packet: dp,
identity: dp.participantIdentity,
encryptionType: encryptionType,
),
Expand Down
Loading