Skip to content

Latest commit

 

History

History
85 lines (60 loc) · 8.6 KB

AGENTS.md

File metadata and controls

85 lines (60 loc) · 8.6 KB

LiveKit Flutter SDK

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

Commands

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 worker

CI (build.yaml) runs all of the above plus example-app builds for every platform including web/WASM.

Repo layout

  • lib/livekit_client.dart — public API surface; implementation lives in lib/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 sibling protocol repo); excluded from analysis/format, never edit by hand. Same goes for *.g.dart files (json_serializable output in lib/src/json/ and lib/src/token_source/).
  • lib/src/e2ee/ + top-level web/ — 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 in test/mock/ (mock peer connection, data channel, websocket; e2e_container.dart for 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.

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

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.
  • iOS AVAudioSession handling 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.dart propagates the version to .version, pubspec.yaml, README.md, lib/src/livekit.dart, and the podspecs; check_version.dart fails CI on mismatch.

Error-prone areas & anti-patterns (from bug/review history)

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. The unawaited_futures/discarded_futures lints exist for exactly this — write unawaited(...) 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 (double DisconnectedEvent, flags not reset on re-connect). When touching engine/room event flow: use Room.emitWhenConnected(...), createListener(synchronized: true), mind emit-vs-cleanup ordering, and add a regression test (see test/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 with isClosed guards (see DataStreamController), 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 muted defaults to false. 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 (how ensurePublisherConnected is debounced), or use lib/src/support/reusable_completer.dart.
  • Cleanup: extend Disposable and register teardown via onDispose() (run LIFO, each guarded so one failing disposer can't abort the rest); create event listeners through createListener() so they're cancelled with their owner.
  • Native boundary: methods in Native swallow errors and logger.warning by 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.

Code style

  • Lints: package:lints/recommended plus repo rules in analysis_options.yaml — single quotes, prefer_final_locals, unawaited_futures, discarded_futures, avoid_print.
  • dart format with page_width: 120; imports sorted with import_sorter.
  • Every source file carries the Apache-2.0 license header.

Releases

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.