Flutter client SDK for LiveKit (livekit_client on pub.dev), built on top of flutter_webrtc. Supported platforms: iOS, Android, macOS, Windows, Linux, Web (including WASM).
Requires Dart >= 3.10 / Flutter >= 3.38 (floor set by stable native assets support).
flutter pub get
flutter analyze # static analysis
flutter test # unit tests
dart format . --set-exit-if-changed # format (page width 120)
dart run import_sorter:main --no-comments --exit-if-changed # import order (CI-enforced)
dart run scripts/check_version.dart # version consistency (CI-enforced)
dart run build_runner build # json_serializable codegen
make proto # regen protobufs (needs ../protocol checkout)
dart compile js ./web/e2ee.worker.dart -o ./example/web/e2ee.worker.dart.js # E2EE web workerCI (build.yaml) runs all of the above plus example-app builds for every platform including web/WASM.
lib/livekit_client.dart— public API surface; implementation lives inlib/src/.lib/src/core/—Room,Engine,SignalClient,Transport;lib/src/participant/,lib/src/track/,lib/src/publication/— participants, tracks, publications;lib/src/events.dart+lib/src/managers/event.dart— event system.lib/src/proto/— protoc-generated code (from the siblingprotocolrepo); excluded from analysis/format, never edit by hand. Same goes for*.g.dartfiles (json_serializable output inlib/src/json/andlib/src/token_source/).lib/src/e2ee/+ top-levelweb/— E2EE; the web worker (web/e2ee.worker.dart) must be recompiled to JS (command above) when changed.example/— example app, built for all platforms in CI.test/— unit tests with mock infrastructure intest/mock/(mock peer connection, data channel, websocket;e2e_container.dartfor room-level tests).- Platform code in
android/,ios/,macos/,windows/,linux/,shared_cpp/,shared_swift/.
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.
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.
livekit_uniffi is published on pub.dev and the normal dependency resolves it; its build hook downloads the matching prebuilt library from the livekit-uniffi GitHub release, so no Rust toolchain is needed. To develop against unreleased crate changes, point both pubspec.yaml and example/pubspec.yaml at a sibling rust-sdks checkout with a dependency_overrides entry (path: ../rust-sdks/livekit-uniffi/packages/dart, overrides do not propagate from a dependency, hence both) and produce or refresh the package with:
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 boundaryRequires 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 the livekit-uniffi GitHub release matching the package version and verifies its SHA-256, so a version whose release lacks assets fails at build time rather than at runtime.
flutter_webrtcis 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.- iOS
AVAudioSessionhandling and reconnection-after-network-loss are the most frequent user-reported problem areas — change that code conservatively. - Don't hand-edit version numbers:
scripts/create_version.dartpropagates the version to.version,pubspec.yaml,README.md,lib/src/livekit.dart, and the podspecs;check_version.dartfails CI on mismatch.
Most regressions live in lib/src/core/ (room.dart, engine.dart, signal_client.dart) and participant/local.dart — connection lifecycle and async event plumbing. Recurring mistake classes:
- Un-awaited async state updates: callers observed half-initialized participants/publications because
updateFromInfo()/updateTrack()weren't awaited. Theunawaited_futures/discarded_futureslints exist for exactly this — writeunawaited(...)only as a deliberate choice. Never emit events from constructors or factories; return data and let the caller sequence emissions. - Iterating a collection across an
await: event handlers mutate participants/publications/listener lists mid-iteration (ConcurrentModificationError— one fix swept 5 files). Snapshot with.toList()before iterating; for queues, copy-then-clear before processing. - The reconnect/disconnect state machine runs on mutable flags (
_isClosed,_isReconnecting, ...) and has historically produced duplicate or missing lifecycle events (doubleDisconnectedEvent, flags not reset on re-connect). When touching engine/room event flow: useRoom.emitWhenConnected(...),createListener(synchronized: true), mind emit-vs-cleanup ordering, and add a regression test (seetest/core/room_e2e_test.dart). - Events or media tracks arriving before their dependent state exists: don't throw or drop — queue and flush (see
lib/src/core/pending_track_queue.dart, TTL'd and flushed on connect/participant updates). - Use-after-close on
StreamControllers crashed data streams: wrap controllers withisClosedguards (seeDataStreamController), and when erroring a stream out, also close it. - Don't rely on protobuf field defaults for state: republish-after-reconnect silently unmuted tracks because
muteddefaults tofalse. Pass prior state explicitly and reconcile local flags with server responses. - Duplicate in-flight async operations: cache the future (
_future ??= ...) and clear it on failure/disconnect (howensurePublisherConnectedis debounced), or uselib/src/support/reusable_completer.dart. - Cleanup: extend
Disposableand register teardown viaonDispose()(run LIFO, each guarded so one failing disposer can't abort the rest); create event listeners throughcreateListener()so they're cancelled with their owner. - Native boundary: methods in
Nativeswallow errors andlogger.warningby default — if a failure must propagate (e.g. a mode change the caller depends on), document why. Platform-channel crossings are expensive in hot paths; batch data instead of calling per-frame/per-sample.
- Lints:
package:lints/recommendedplus repo rules inanalysis_options.yaml— single quotes,prefer_final_locals,unawaited_futures,discarded_futures,avoid_print. dart formatwithpage_width: 120; imports sorted withimport_sorter.- Every source file carries the Apache-2.0 license header.
Every PR that affects the published package needs a changeset file in .changes/ (format: patch|minor|major type="fixed|added|changed|..." "description"); CI checks for it and runs dart-apitool against main to require a major changeset for breaking public-API changes. PRs that only touch CI workflows or repo tooling are exempt, a changeset would put noise in the user facing changelog. Releases are tag-driven (vX.Y.Z) and publish to pub.dev.