Skip to content

Data tracks UniFFI#1034

Open
ladvoc wants to merge 62 commits into
mainfrom
ladvoc/data-tracks-uniffi
Open

Data tracks UniFFI#1034
ladvoc wants to merge 62 commits into
mainfrom
ladvoc/data-tracks-uniffi

Conversation

@ladvoc

@ladvoc ladvoc commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Summary of changes:

  • Exposes data tracks core functionality through livekit-uniffi
    • This will eventually enable the following clients to share the Rust implementation: Swift, Kotlin, React Native, Flutter
  • Minor changes to the data tracks crate to support this

Resolves CLT-2472

@pblazej pblazej requested review from davidliu and removed request for reenboog May 21, 2026 14:41
@pblazej

pblazej commented May 21, 2026

Copy link
Copy Markdown
Contributor

@davidliu tagging you as it would be great to get some approval before the actual "0.1" release.

Let's make sure the DTOs etc. look fine at least on 2 platforms, the async things work, etc.

@davidliu

Copy link
Copy Markdown
Contributor

@pblazej looks good, got it compiled for Android and played around with the API. Seems like everything would integrate fairly seamlessly into the Android SDK.

@ladvoc I did have to add the "macros" feature to this line in the Cargo.toml to get it building:

tokio = { workspace = true, default-features = false, features = ["macros", "sync"] }

I'm not sure if this is android specific or affects other builds as well.

@ladvoc ladvoc force-pushed the ladvoc/data-tracks-uniffi branch from 6c06943 to de6198f Compare June 23, 2026 23:38
pblazej and others added 3 commits June 25, 2026 13:24
0.31.2's Swift backend emits `nonisolated(unsafe) static let` for callback-interface
vtables (Swift 6 strict-concurrency clean) and supports methods on records/enums.
0.30.0 required consumers to compile the generated bindings in Swift 5 language mode.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
UniFFI drives exported async functions from the foreign bindings' executor, on a thread
with no tokio runtime. Anything that reaches a reactor — `publish_track` via
`tokio::time::timeout`, the receive path (`subscribe`/`DataTrackStream::next`) via
`livekit_runtime::timeout` in the depacketizer — then panics "there is no reactor running".

Annotate the data track impl blocks with `#[uniffi::export(async_runtime = "tokio")]`, which
wraps each future in `async_compat::Compat` so it is polled within a tokio runtime context.
Applied uniformly, since which exports reach a reactor is not locally auditable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cargo-swift mispackages a UniFFI library with more than one component (livekit-uniffi +
livekit-datatrack): wrong xcframework name and unimportable framework modules. Pin the
swift-xcframework task to the fork branch that fixes this
(https://github.com/livekit/cargo-swift/tree/fix/multi-crate-framework), and drop
`--debug-symbols` since that branch is based on main without the dSYM-embedding support.

Add a swift-workarounds task that drops the duplicate `Bytes` FfiConverter from
livekit_datatrack.swift — the `Bytes` custom type is registered in both crates, so UniFFI
emits it in both component files and they collide ("invalid redeclaration of 'Bytes'") when
compiled into one Swift module.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pblazej pblazej force-pushed the ladvoc/data-tracks-uniffi branch from 46759f4 to 8bd470c Compare June 26, 2026 07:49
The fork's fix/multi-crate-framework branch was rebased onto cargo-swift main as a single
commit (open upstream as antoniusnaumann/cargo-swift#101) and no longer carries the
uniffi_bindgen 0.31.2 bump — that went to its own PR (antoniusnaumann/cargo-swift#102).
Re-pin the swift-xcframework task to the new revision.

Without that bindgen bump the generated callback-interface vtables are plain `static let`,
which the Swift 6 language mode rejects, so restore `swiftLanguageModes: [.v5]` in the
package templates until cargo-swift ships uniffi 0.31.2.

Give livekit-datatrack a uniffi.toml setting ffi_module_name = "RustLiveKitDataTrack" so its
generated FFI header matches the RustLiveKitUniFFI naming instead of the default
livekit_datatrackFFI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pblazej

pblazej commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Wiring up the Swift SDK — hit two gaps in the binding surface for join/reconnect @ladvoc @1egoman:

Join-time tracks. No join handler on RemoteDataTrackManager, even though the reference SDK runs event_from_join on connect (rtc_session.rs:531) — so a foreign client joining a room with existing data tracks won't see them. Either add a handle_sfu_join, or just feed join.other_participants through the existing handle_sfu_participant_update (what JS effectively does). Identical under the hood — both hit event_from_participant_info. Preference?

Quick-reconnect sync state. No way to build SyncState.publishDataTracks from the bindings. query_tracks() is exposed, but the uniffi DataTrackInfo drops pub_handle, so I can't construct PublishDataTrackResponse client-side the way JS/Rust do (their info carries the handle). Either expose pub_handle on the uniffi DataTrackInfo, or add a publish_responses_for_sync_state() returning the serialized responses (keeps the handle internal — probably cleaner).

Caveats: no released livekit-server (incl. 1.13.2) fills ParticipantInfo.data_tracks in the join, so join discovery isn't verifiable e2e atm — is the SFU side still unreleased?

Query tracks is now used internally and not exported
@ladvoc

ladvoc commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

Quick-reconnect sync state. No way to build SyncState.publishDataTracks from the bindings. query_tracks() is exposed, but the uniffi DataTrackInfo drops pub_handle, so I can't construct PublishDataTrackResponse client-side the way JS/Rust do (their info carries the handle). Either expose pub_handle on the uniffi DataTrackInfo, or add a publish_responses_for_sync_state() returning the serialized responses (keeps the handle internal — probably cleaner).

Makes sense, I implemented the latter in 4f18c9c. This replaces the query_tracks method that was previously exported.

@ladvoc ladvoc closed this Jun 30, 2026
@ladvoc ladvoc reopened this Jun 30, 2026
@ladvoc

ladvoc commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

Join-time tracks. No join handler on RemoteDataTrackManager, even though the reference SDK runs event_from_join on connect (rtc_session.rs:531) — so a foreign client joining a room with existing data tracks won't see them. Either add a handle_sfu_join, or just feed join.other_participants through the existing handle_sfu_participant_update (what JS effectively does). Identical under the hood — both hit event_from_participant_info. Preference?

Rust has these additional helpers to keep the interface between the rest of the client SDK and the data tracks managers as uniform as possible; instead of caring about specific fields in each response type, you hand over the whole response, and the logic of how the required information is extracted is kept as an implementation detail. A few other advantages:

  • If the data track managers ever need additional information not extracted from participant info, no update to this interface will be needed.
  • Local participant identity isn't needed when handling a join response but it is when handling a participant update; this gets modeled in the signature of each method.

I implemented handle_sfu_join_response in 8abcf90. While these helpers don't add any overhead in the Rust client integration since they accept the responses by reference, in an FFI context, there is additional overhead because the whole response must be serialized rather than just the relevant fields—I'm open to changing this if the additional overhead is a concern.

@ladvoc

ladvoc commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

Caveats: no released livekit-server (incl. 1.13.2) fills ParticipantInfo.data_tracks in the join, so join discovery isn't verifiable e2e atm — is the SFU side still unreleased?

Data tracks is fully released in OSS and Cloud now so this would be unexpected. Do you have any repro steps for this?

@pblazej

pblazej commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Do you have any repro steps for this?

No repro — false alarm on my side. The join does carry data_tracks on 1.13.2; my test was discarding the returned LocalDataTrack (_ = try await publishDataTrack(...)), which unpublishes it on drop, so the track was already gone before the late joiner arrived. Holding it, join-time discovery works. Both handle_sfu_join_response and publish_responses_for_sync_state are wired up and green e2e now.

That RAII-on-drop is really the last open question: should FFI consumers mirror Rust's semantics (drop = unpublish) or go "publication-like" (JS keeps the publication in its manager until an explicit unpublish)? We went publication-like on the Swift side so callers aren't forced to retain the handle — this diff shows it: livekit/client-sdk-swift@3e3043e

Framing it as "what should the convention be" rather than "should Rust change": the core's RAII is correct/idiomatic for Rust consumers — the divergence is about what the FFI bindings present. JS and (now) Swift both land on publication-like, so there's a de-facto convention worth making explicit.

For concreteness, the Swift lifecycle surface:

API Semantics
publishDataTrack(name:) publication-like — SDK retains it; discarding the handle does not unpublish
track.unpublish() / await track.waitForUnpublish() explicit unpublish + await
withDataTrack(name:) { … } scoped RAII — auto-unpublishes when the block exits

withDataTrack is the compromise: it keeps Rust's RAII where it's genuinely a feature (an explicit lexical scope where auto-cleanup is exactly what you want), while the default publishDataTrack stays publication-like so discarding the handle doesn't surprise-unpublish.

@pblazej pblazej marked this pull request as ready for review July 1, 2026 08:06
Add subscribe_with_options on RemoteDataTrack and a DataTrackSubscribeOptions
record (buffer_size) so foreign consumers can tune the internal frame buffer,
matching the core and JS subscribe APIs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@pblazej pblazej left a comment

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.

I don't see any functional gaps here preventing Swift integration

The rest is mostly consumer-side discussion (like the RAII pattern mentioned above).

Before merging please update both SPM_SIZE_LIMIT_BYTES and ANDROID_SIZE_LIMIT_BYTES with the final sizes.

Approximate sizes @ the latest commit:

  • SPM_SIZE_LIMIT_BYTES = 1152136
  • ANDROID_SIZE_LIMIT_BYTES = 1250710

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants