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/uniffi-rust-core-dev-wiring
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
patch type="added" "Wire in the livekit_uniffi Rust core behind a native-only facade, delivered as a bundled cdylib via Native Assets"
22 changes: 22 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,28 @@ CI (`build.yaml`) runs all of the above plus example-app builds for every platfo

Web/native divergence is handled with conditional imports (e.g. `track/processor_native.dart` vs `processor_web.dart`) — new platform-specific code should follow that pattern.

## The Rust core (`livekit_uniffi`)

`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`.

### Local development loop

`livekit_uniffi` is not on pub.dev yet, so both `pubspec.yaml` and `example/pubspec.yaml` override it to a path in a sibling `rust-sdks` checkout (overrides don't propagate from a dependency, hence both). To produce or refresh it:

```sh
cd ../rust-sdks/livekit-uniffi
cargo make dart-package # generates packages/dart/: bindings, pubspec, build hook, host cdylib
cd -
flutter pub get
flutter test test/uniffi/ # smoke test: calls buildVersion() across the FFI boundary
```

Requires `cargo-make`, `protoc` and `tera`. Re-run `cargo make dart-package` whenever the crate's exported surface changes — the build hook tracks the copied library, so a stale one won't be silently reused.

Two things to know about that hook: it picks the locally built library purely on target *OS*, not architecture, so a host build can be bundled into an iOS or Android build by mistake — verify the desktop target first when debugging. And its download mode (used when no local library is present) fetches `build-<triple>.zip` from a `livekit-uniffi` GitHub release; no release currently carries those assets, so download mode fails until the `cdylib` job is re-enabled in `rust-sdks`.

## Common pitfalls (from issue history)

- `flutter_webrtc` is pinned to an exact version on purpose: livekit_client and flutter_webrtc must agree on the same WebRTC-SDK native pods, and mismatches break user builds (CocoaPods conflicts). Bump it only in sync with a matching WebRTC-SDK version.
Expand Down
4 changes: 3 additions & 1 deletion analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ analyzer:
avoid_print: ignore
deprecated_member_use_from_same_package: ignore

# Exclude protobuf files
# Exclude generated files: protobuf, and json_serializable output. Neither is
# hand-edited, so lint hits there can only be fixed by changing the generator.
exclude:
- "**/*.pb.dart"
- "**/*.pbenum.dart"
- "**/*.pbjson.dart"
- "**/*.pbserver.dart"
- "**/*.g.dart"
# - 'web/*.dart'
# Xcode vendors Swift package checkouts under build when Swift Package
# Manager is enabled and this package ships Package.swift.
Expand Down
7 changes: 7 additions & 0 deletions example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ dependencies:
livekit_client:
path: ../

dependency_overrides:
# Overrides do not propagate from a dependency, so livekit_client's override of
# livekit_uniffi has to be repeated here. Drop both once it is on pub.dev.
livekit_uniffi:
path: ../../rust-sdks/livekit-uniffi/packages/dart


dev_dependencies:
flutter_test:
sdk: flutter
Expand Down
45 changes: 45 additions & 0 deletions lib/src/uniffi/uniffi.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// 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.

import 'uniffi_io.dart' if (dart.library.js_interop) 'uniffi_web.dart' as impl;

/// Facade over the Rust core exposed by the `livekit_uniffi` package.
///
/// `livekit_uniffi` reaches Rust through Dart's Native Assets: its build hook
/// bundles a `cdylib` into the host app and the generated bindings call into it
/// with `@Native`. None of that exists on the web, where there is no dynamic
/// library to load, so every entry point here is split native/web through the
/// same conditional-import pattern the rest of the SDK uses (see
/// `support/platform.dart`). Web builds must never reach the generated
/// bindings -- importing them at all would break `dart compile js`/`wasm`.
///
/// Callers get [isAvailable] to branch on, and platform-specific code paths
/// stay out of the public API surface.
abstract final class LiveKitUniffi {
/// Whether the Rust core can be called on this platform.
///
/// False on web. Every other member throws [UnsupportedError] when this is
/// false, rather than returning a silently wrong value.
static bool get isAvailable => impl.isAvailable;

/// Version string reported by the Rust core.
///
/// The simplest possible round trip -- a synchronous, argument-free call
/// returning a string -- so it doubles as the smoke test that the whole
/// chain is wired up: build hook resolved the library, `@Native` bound the
/// symbol, and a value came back across the FFI boundary.
///
/// Throws [UnsupportedError] on web.
static String get buildVersion => impl.buildVersion();
}
Comment on lines +17 to +45

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.

Here's the newly introduced LiveKitUniffi facade. This allows all these bits of functionality to be turned into noops on web.

23 changes: 23 additions & 0 deletions lib/src/uniffi/uniffi_io.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// 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.

import 'package:livekit_uniffi/livekit_uniffi.dart' as uniffi;

/// Native implementation of [LiveKitUniffi]. See `uniffi.dart`.
///
/// This is the only file in the SDK that may import the generated bindings:
/// the conditional import in `uniffi.dart` keeps it out of web builds.
const bool isAvailable = true;

String buildVersion() => uniffi.buildVersion();
27 changes: 27 additions & 0 deletions lib/src/uniffi/uniffi_web.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// 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.

/// Web implementation of [LiveKitUniffi]. See `uniffi.dart`.
///
/// Native Assets bundles a `cdylib`, which the web has no way to load, so the
/// Rust core is simply absent here. This file deliberately does not import
/// `package:livekit_uniffi/...` -- doing so would pull `dart:ffi` into a web
/// compile and fail the build.
const bool isAvailable = false;

Never buildVersion() => throw UnsupportedError(
'LiveKitUniffi.buildVersion is not available on web: the Rust core is '
'delivered as a native library. Guard calls with '
'LiveKitUniffi.isAvailable.',
);
23 changes: 23 additions & 0 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "13.0.0"
archive:
dependency: transitive
description:
name: archive
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
url: "https://pub.dev"
source: hosted
version: "4.0.9"
args:
dependency: transitive
description:
Expand Down Expand Up @@ -432,6 +440,13 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.0"
livekit_uniffi:
dependency: "direct main"
description:
path: "../rust-sdks/livekit-uniffi/packages/dart"
relative: true
source: path
version: "0.1.8"
logger:
dependency: transitive
description:
Expand Down Expand Up @@ -616,6 +631,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.5.2"
posix:
dependency: transitive
description:
name: posix
sha256: bc1bad54ad2b735816e31f8d4600cfde6c7839975085ddfbca48b6c9f7c4044e
url: "https://pub.dev"
source: hosted
version: "6.5.2"
protobuf:
dependency: "direct main"
description:
Expand Down
11 changes: 11 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,17 @@ dependencies:
flutter_webrtc: 1.6.0
dart_webrtc: ^1.8.0

# Rust core (livekit-uniffi), delivered as a bundled cdylib via Native Assets.
# Native platforms only — see lib/src/uniffi/.
livekit_uniffi: ^0.1.7

# livekit_uniffi is not published to pub.dev yet, so it resolves out of a sibling
# rust-sdks checkout produced by `cargo make dart-package`. See AGENTS.md. Drop
# this once the package is published.
dependency_overrides:
livekit_uniffi:
path: ../rust-sdks/livekit-uniffi/packages/dart

dev_dependencies:
flutter_test:
sdk: flutter
Expand Down
40 changes: 40 additions & 0 deletions test/uniffi/uniffi_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// 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.

@TestOn('vm')
library;

import 'package:flutter_test/flutter_test.dart';

import 'package:livekit_client/src/uniffi/uniffi.dart';

void main() {
// Exercises the whole delivery chain rather than any particular API: the
// build hook resolved a cdylib for this target, Native Assets bundled it,
// `@Native` bound the symbol, and a value crossed back from Rust. If the
// bindgen or the hook regresses, this is what fails first.
group('livekit_uniffi', () {
test('is available on native platforms', () {
expect(LiveKitUniffi.isAvailable, isTrue);
});

test('buildVersion returns the Rust core version', () {
final version = LiveKitUniffi.buildVersion;
expect(version, isNotEmpty);
// The crate stamps its own semver, so assert the shape rather than a
// literal that would need bumping on every livekit-uniffi release.
expect(version, matches(RegExp(r'^\d+\.\d+\.\d+')));
});
});
}
Loading