Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
04c4db0
Add livekit-datatrack as dependency
ladvoc Apr 9, 2026
95bf51c
Create module
ladvoc Apr 9, 2026
18cacdd
Make public
ladvoc Apr 10, 2026
0635e4b
Type alias, use error enum
ladvoc Apr 10, 2026
16791dd
Add common module
ladvoc Apr 10, 2026
9b39329
Wip
ladvoc Apr 10, 2026
0c621f9
Wip
ladvoc Apr 10, 2026
5943438
Make fields public
ladvoc Apr 10, 2026
895b034
Clarify docs
ladvoc Apr 10, 2026
9b275de
Expose local track methods
ladvoc Apr 10, 2026
d254497
Move common types to top level mod
ladvoc Apr 10, 2026
e275186
Support encryption provider
ladvoc Apr 10, 2026
2e91900
Docs
ladvoc Apr 10, 2026
af84b22
Apply Clippy
ladvoc Apr 21, 2026
22d3a35
Expose unpublish
ladvoc Apr 21, 2026
00a6e99
Remote track support
ladvoc Apr 21, 2026
40b8cc3
Error handling for signal response
ladvoc Apr 21, 2026
5ba3dc5
E2EE in separate mod, rename
ladvoc Apr 21, 2026
78f27cb
Organization
ladvoc Apr 21, 2026
26b61c7
Rename
ladvoc Apr 21, 2026
e1aba7d
Add thiserror as dependency
ladvoc Apr 21, 2026
ca653fb
Doc
ladvoc Apr 21, 2026
cc742da
Handle error
ladvoc Apr 21, 2026
0911f3f
Consistent namespacing
ladvoc Apr 21, 2026
25446d3
Organization, style
ladvoc Apr 21, 2026
807305a
Make manager output concrete type
ladvoc Apr 21, 2026
c06705c
Use actor pattern
ladvoc Apr 21, 2026
f2815ea
Comment
ladvoc Apr 21, 2026
91d251c
Expose wait for unpublish
ladvoc Apr 21, 2026
66d365e
Move comment
ladvoc Apr 22, 2026
bd81965
Docs
ladvoc Apr 22, 2026
eea0ac7
Common module
ladvoc Apr 22, 2026
d428f66
Format
ladvoc Apr 22, 2026
7a35faf
Consistent access control
ladvoc Apr 22, 2026
8c39c41
Doc
ladvoc Apr 22, 2026
bea2560
Add enum case
ladvoc Apr 22, 2026
1db92e3
Changeset
ladvoc Apr 22, 2026
bf80cf4
Fix typos
ladvoc Apr 22, 2026
4079e77
Remove unnecessary async marker
ladvoc Apr 22, 2026
69b5b03
Use lock instead of try_lock
ladvoc Apr 22, 2026
def2f97
Doc
ladvoc Apr 22, 2026
0533a38
Cargo lock
ladvoc Apr 22, 2026
9f89c44
Changeset
ladvoc Apr 22, 2026
90cf95a
Async runtime
ladvoc Apr 22, 2026
cdc2a7e
Changeset
ladvoc Apr 22, 2026
b449306
Separate handlers per signal response type
ladvoc Apr 29, 2026
6451ece
Merge remote-tracking branch 'origin/main' into ladvoc/data-tracks-un…
ladvoc May 20, 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
6 changes: 6 additions & 0 deletions .changeset/data_track_public_fields.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
livekit-datatrack: patch
livekit: patch
---

# Make some fields public for data track types
6 changes: 6 additions & 0 deletions .changeset/make_data_track_e2ee_errors_enums.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
livekit: patch
livekit-datatrack: patch
---

# Make data track E2EE errors enums
5 changes: 5 additions & 0 deletions .changeset/uniffi_data_tracks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
livekit-uniffi: minor
---

# Expose data tracks core functionality
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
livekit-datatrack: patch
livekit: patch
livekit-ffi: patch
---

# Use concrete type for data track manager output events
6 changes: 6 additions & 0 deletions Cargo.lock

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

17 changes: 12 additions & 5 deletions livekit-datatrack/src/e2ee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,29 @@ use thiserror::Error;
// TODO: If a core module for end-to-end encryption is created in the future
// (livekit-e2ee), these traits should be moved to there.

/// Twelve byte AES initialization vector (IV).
pub type InitializationVector = [u8; 12];

/// Encrypted payload and metadata required for decryption.
pub struct EncryptedPayload {
pub payload: Bytes,
pub iv: [u8; 12],
pub iv: InitializationVector,
pub key_index: u8,
}

/// An error indicating a payload could not be encrypted.
#[derive(Debug, Error)]
#[error("Encryption failed")]
pub struct EncryptionError;
pub enum EncryptionError {
#[error("Encryption failed")]
Failed,
}

/// An error indicating a payload could not be decrypted.
#[derive(Debug, Error)]
#[error("Decryption failed")]
pub struct DecryptionError;
pub enum DecryptionError {
#[error("Decryption failed")]
Failed,
}

/// Provider for encrypting payloads for E2EE.
pub trait EncryptionProvider: Send + Sync + Debug {
Expand Down
4 changes: 2 additions & 2 deletions livekit-datatrack/src/frame.rs
Copy link
Copy Markdown
Contributor

@pblazej pblazej Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is not an uniffi::Object we won't get withTimestampNow etc. for free, right? As it basically maps to raw struct:

public struct DataTrackFrame: Equatable, Hashable, Sendable {
  public var payload: Data
  public var userTimestamp: UInt64?
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applies to other "DTOs" as well, so good to discuss that now.

Copy link
Copy Markdown
Contributor Author

@ladvoc ladvoc Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems this type of "DTO" is more naturally modeled as a value type on the Swift side. I don't think you can currently have associated functions on a uniffi::Record (even though this makes sense in a Swift context), so I see two options:

  1. Use uniffi::Object
  2. Export standalone helper functions (e.g., fn with_user_timestamp(frame: DataTrackFrame) → DataTrackFrame) and define an extension DataTrackFrame on the Swift side to make it an associated function

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good news: UniFFI v0.31.0 added support for methods on records and enums!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup let's avoid handwritten extensions and just try bumping UniFFI.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edit: tried that locally, and it does not work for #[uniffi::remote(Record)] e.g. DataTrackFrame

Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
///
#[derive(Clone, Default)]
pub struct DataTrackFrame {
pub(crate) payload: Bytes,
pub(crate) user_timestamp: Option<u64>,
pub payload: Bytes,
pub user_timestamp: Option<u64>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: shall we expose duration_since_timestamp e.g. for benchmarks?

}

impl DataTrackFrame {
Expand Down
24 changes: 21 additions & 3 deletions livekit-datatrack/src/local/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,13 @@ use crate::{
};
use anyhow::{anyhow, Context};
use futures_core::Stream;
use std::{collections::HashMap, sync::Arc, time::Duration};
use std::{
collections::HashMap,
pin::Pin,
sync::Arc,
task::{Context as TaskContext, Poll},
time::Duration,
};
use tokio::sync::{mpsc, oneshot, watch};
use tokio_stream::wrappers::ReceiverStream;

Expand Down Expand Up @@ -58,7 +64,7 @@ impl Manager {
/// - Channel for sending [`InputEvent`]s to be processed by the manager.
/// - Stream for receiving [`OutputEvent`]s produced by the manager.
///
pub fn new(options: ManagerOptions) -> (Self, ManagerInput, impl Stream<Item = OutputEvent>) {
pub fn new(options: ManagerOptions) -> (Self, ManagerInput, ManagerOutput) {
let (event_in_tx, event_in_rx) = mpsc::channel(Self::EVENT_BUFFER_COUNT);
let (event_out_tx, event_out_rx) = mpsc::channel(Self::EVENT_BUFFER_COUNT);

Expand All @@ -72,7 +78,7 @@ impl Manager {
descriptors: HashMap::new(),
};

let event_out = ReceiverStream::new(event_out_rx);
let event_out = ManagerOutput(ReceiverStream::new(event_out_rx));
(manager, event_in, event_out)
}

Expand Down Expand Up @@ -400,6 +406,18 @@ pub struct ManagerInput {
_drop_guard: Arc<DropGuard>,
}

/// Stream of [`OutputEvent`]s produced by [`Manager`].
#[derive(Debug)]
pub struct ManagerOutput(ReceiverStream<OutputEvent>);

impl Stream for ManagerOutput {
type Item = OutputEvent;

fn poll_next(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
Pin::new(&mut self.0).poll_next(cx)
}
}

/// Guard that sends shutdown event when the last reference is dropped.
#[derive(Debug)]
struct DropGuard {
Expand Down
2 changes: 1 addition & 1 deletion livekit-datatrack/src/local/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ impl Drop for LocalTrackInner {
///
#[derive(Clone, Debug)]
pub struct DataTrackOptions {
pub(crate) name: String,
pub name: String,
}

impl DataTrackOptions {
Expand Down
18 changes: 16 additions & 2 deletions livekit-datatrack/src/remote/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ use bytes::Bytes;
use std::{
collections::{HashMap, HashSet},
mem,
pin::Pin,
sync::Arc,
task::{Context as TaskContext, Poll},
};
use tokio::sync::{broadcast, mpsc, oneshot, watch};
use tokio_stream::{wrappers::ReceiverStream, Stream};
Expand Down Expand Up @@ -70,7 +72,7 @@ impl Manager {
/// - Channel for sending [`InputEvent`]s to be processed by the manager.
/// - Stream for receiving [`OutputEvent`]s produced by the manager.
///
pub fn new(options: ManagerOptions) -> (Self, ManagerInput, impl Stream<Item = OutputEvent>) {
pub fn new(options: ManagerOptions) -> (Self, ManagerInput, ManagerOutput) {
let (event_in_tx, event_in_rx) = mpsc::channel(Self::EVENT_BUFFER_COUNT);
let (event_out_tx, event_out_rx) = mpsc::channel(Self::EVENT_BUFFER_COUNT);

Expand All @@ -84,7 +86,7 @@ impl Manager {
sub_handles: HashMap::default(),
};

let event_out = ReceiverStream::new(event_out_rx);
let event_out = ManagerOutput(ReceiverStream::new(event_out_rx));
(manager, event_in, event_out)
}

Expand Down Expand Up @@ -443,6 +445,18 @@ pub struct ManagerInput {
_drop_guard: Arc<DropGuard>,
}

/// Stream of [`OutputEvent`]s produced by [`Manager`].
#[derive(Debug)]
pub struct ManagerOutput(ReceiverStream<OutputEvent>);

impl Stream for ManagerOutput {
type Item = OutputEvent;

fn poll_next(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<Option<Self::Item>> {
Pin::new(&mut self.0).poll_next(cx)
}
}

/// Guard that sends shutdown event when the last reference is dropped.
#[derive(Debug)]
struct DropGuard {
Expand Down
2 changes: 1 addition & 1 deletion livekit-datatrack/src/track.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ impl<L> DataTrack<L> {
&self.info
}

/// Whether or not the track is still published.
/// Whether or not the track is currently published.
pub fn is_published(&self) -> bool {
match self.inner.as_ref() {
DataTrackInner::Local(inner) => inner.is_published(),
Expand Down
8 changes: 7 additions & 1 deletion livekit-uniffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,16 @@ publish = false
[dependencies]
livekit-protocol = { workspace = true }
livekit-api = { workspace = true }
livekit-datatrack = { workspace = true }
uniffi = { version = "0.30.0", features = ["cli", "scaffolding-ffi-buffer-fns"] }
log = { workspace = true }
tokio = { workspace = true, features = ["sync"] }
tokio = { workspace = true, features = ["sync", "rt-multi-thread"] }
tokio-util = "0.7.18"
prost = "0.12"
futures-util = { workspace = true, default-features = false, features = ["sink"] }
bytes = { workspace = true }
once_cell = "1.21.3"
thiserror = { workspace = true }

[build-dependencies]
uniffi = { version = "0.30.0", features = ["build", "scaffolding-ffi-buffer-fns"] }
Expand Down
17 changes: 17 additions & 0 deletions livekit-uniffi/src/common.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright 2026 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use bytes::Bytes;

uniffi::custom_type!(Bytes, Vec<u8>, { remote });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General: do we plan to introduce some helpers for encoding/decoding into that or leaving that to experienced users?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This macro handles the conversion from Bytes to Vec<u8> automatically (under the hood it implements impl uniffi::FfiConverter<crate::UniFfiTag> for Bytes), so Swift can just pass Data as expected wherever the API on the Rust side accepts Bytes.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I just mean "something higher-level than Data" like Encodable types (JSON etc.)

67 changes: 67 additions & 0 deletions livekit-uniffi/src/data_track/common.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright 2026 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use bytes::Bytes;
use livekit_datatrack::api::{DataTrackFrame, DataTrackSid};
use livekit_protocol as proto;
use prost::Message;

uniffi::custom_type!(DataTrackSid, String, {
remote,
lower: |s| String::from(s),
try_lift: |s| DataTrackSid::try_from(s).map_err(|e| uniffi::deps::anyhow::anyhow!("{e}")),
});

#[uniffi::remote(Record)]
pub struct DataTrackFrame {
pub payload: Bytes,
pub user_timestamp: Option<u64>,
}

/// Information about a published data track.
#[derive(uniffi::Record)]
pub struct DataTrackInfo {
pub sid: DataTrackSid,
pub name: String,
pub uses_e2ee: bool,
}

impl From<&livekit_datatrack::api::DataTrackInfo> for DataTrackInfo {
fn from(info: &livekit_datatrack::api::DataTrackInfo) -> Self {
Self { sid: info.sid(), name: info.name().to_string(), uses_e2ee: info.uses_e2ee() }
}
}

/// Signal response crossing the FFI boundary could not be processed.
#[derive(uniffi::Error, thiserror::Error, Debug)]
#[uniffi(flat_error)]
pub enum HandleSignalResponseError {
#[error("Response decoding failed: {0}")]
Decode(prost::DecodeError),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought: It is worth making a unique error type here rather than using prost::DecodeError so that the internal protobuf implementation type doesn't leak through the interface?

Copy link
Copy Markdown
Contributor Author

@ladvoc ladvoc Apr 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this uses the #[uniffi(flat_error)] macro (docs), the associated value for each case gets converted to a string via display for the purposes of crossing the FFI boundary so none of the inner error types are exposed—but we still get the enum cases.

#[error("Response container has no message")]
EmptyMessage,
#[error("Unsupported response type in this context")]
UnsupportedType,
#[error(transparent)]
Internal(livekit_datatrack::api::InternalError),
}

/// Deserializes a signal response crossing the FFI boundary, returning the message variant.
pub(crate) fn deserialize_signal_response(
res: &[u8],
) -> Result<proto::signal_response::Message, HandleSignalResponseError> {
let res =
proto::SignalResponse::decode(res).map_err(|err| HandleSignalResponseError::Decode(err))?;
res.message.ok_or(HandleSignalResponseError::EmptyMessage)
}
Loading
Loading