LevelChatLevelChatDocs
Flutter

SDKs

Flutter SDK

flutter_webrtc wrapper.

Preview. The Dart package source is in private development — flutter analyze + flutter test both pass (21/21), and a pub.dev publish is on the v0.3 roadmap. The API surface below is stable; only the distribution channel is in flight. Need source access during the preview window? Email [email protected].

Flutter SDK for LevelChat. Mirrors the @levelchat/web concepts — LevelChat, Room, tracks, events — adapted to idiomatic Dart with Stream, Future, and sealed event hierarchies. Naming follows Dart idiom: events are a sealed class RoomEvent (ParticipantJoined, TrackPublished) on a broadcast Stream, and you publish with room.publishMicrophone(). See the SDK parity matrix for the full per-platform naming table.

MIT-licensed. Real WebRTC via flutter_webrtc.

Install — preview

The standard hosted install (the shape below) lands with v0.3:

YAMLpubspec.yaml (v0.3+, pub.dev)
dependencies:
  levelchat: ^0.3.0
  flutter_webrtc: ^0.11.0 # peer dep
~
flutter pub get

During the preview window the package is distributed under NDA — email

[email protected] with your use case and we'll send back the private mirror credentials + a path-dependency snippet for your pubspec.yaml.

Platform setup

iOS

Add to ios/Runner/Info.plist:

xml
<key>NSCameraUsageDescription</key>
<string>Used for video calls</string>
<key>NSMicrophoneUsageDescription</key>
<string>Used for voice + video calls</string>

Set the deployment target to iOS 12+ in your Podfile:

ruby
platform :ios, '12.0'

Android

Add to android/app/src/main/AndroidManifest.xml:

xml
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />

Set minSdkVersion 21 in android/app/build.gradle.

The end-to-end path

The full lifecycle is the same five steps as every LevelChat SDK: install → token → join → publish → handle events → leave. Here it is, top to bottom.

1. Mint a token

Tokens are minted on your backend with your project API key — never embedded in the app bundle, which can be decompiled. The issueToken helper on the client exists only so getting-started tutorials can run without a backend; production apps fetch the token from their own server. See the Quickstart for the POST /v1/auth/tokens/room request shape.

Dart
// Production: fetch from YOUR backend.
final token = await fetchTokenFromYourBackend();

2. Construct the client + join

LevelChat.joinRoom(roomId, opts) is the public entry point — it resolves once the connection reaches connected. (The client also has joinLive, but joinRoom is the stable, fully exported surface; reach for it first.)

Dart
import 'package:levelchat/levelchat.dart';

final client = LevelChat(const LevelChatConfig(region: 'eu-fsn'));

// `joinRoom` takes the room id first, then the options. The room id is
// whatever your backend scoped the token to.
final room = await client.joinRoom(
  'r_demo',
  JoinRoomOptions(token: token),
);

JoinRoomOptions mirrors the web SDK: token (required), preferredCodec, simulcast (default true), svc, signalingUrl, and iceServers for TURN injection.

3. Handle events

room.events is a broadcast Stream<RoomEvent> — listen anywhere, as many times as you like. RoomEvent is a sealed hierarchy, so a switch (or is checks) covers every case:

Dart
room.events.listen((event) {
  switch (event) {
    case ParticipantJoined(:final participant):
      print('joined: ${participant.identity}');
    case ParticipantLeft(:final participantId):
      print('left: $participantId');
    case TrackSubscribed(:final track):
      // A remote track is ready — bind it to an RTCVideoRenderer.
      attachRenderer(track);
    case TrackUnsubscribed(:final trackId):
      detachRenderer(trackId);
    case QualityChanged(:final score):
      // score ∈ QualityLabel.excellent | good | fair | poor | disconnected
      updateNetworkIndicator(score);
    case RoomReconnecting(:final attempt):
      showReconnectingBanner(attempt);
    case RoomReconnected():
      hideReconnectingBanner();
    case RoomErrorEvent(:final error):
      print('error: $error');
    default:
      break;
  }
});

Connection-state transitions arrive as their own events — RoomConnected, RoomReconnecting, RoomReconnected, RoomDisconnected, RoomClosed — rather than a single state enum, which keeps the switch exhaustive.

4. Publish camera + mic

Note the Dart-idiomatic name: it's publishMicrophone(), not publishMic(). Both methods take an optional options object and return a LocalTrack whose nativeTrack you bind to an RTCVideoRenderer for the local preview:

Dart
final cam = await room.publishCamera(
  const PublishCameraOptions(resolution: '720p'),
);
final mic = await room.publishMicrophone();

// Bind the camera's native track to a renderer for the self-view.
final renderer = RTCVideoRenderer();
await renderer.initialize();
renderer.srcObject = cam.nativeTrack.mediaStream;

Device selection is via the deviceId field on PublishCameraOptions / PublishMicOptions. Each source can only be published once — a second publishCamera() throws RoomError(roomDuplicatePublish).

5. Screen share

Dart
final screen = await room.publishScreen();

Backed by getDisplayMedia() — on iOS this requires a Broadcast Upload Extension target, on Android it triggers the system MediaProjection prompt. The SDK's screen_share module wraps the permission-error mapping for both platforms; handle a thrown LevelChatError if the user declines.

6. Stop a track + leave

unpublish takes the TrackSource (not a track id) — TrackSource.camera, .microphone, or .screen. close() tears the room down completely:

Dart
await room.unpublish(TrackSource.camera);

// When the user leaves the call:
await room.close();

UI Kit

The package exports brand-pinned Flutter widgets matching the React web kit's component set.

Dart
import 'package:levelchat/levelchat.dart';

VideoTile(
  name: 'Mert',
  quality: NetworkQuality.good,
  micOn: true,
  isSpeaking: speaker == myId,
);

The widget kit ships VideoTile, ControlBar, PreJoin, SpeakerStage, Chat, PollPanel, QAPanel, and Whiteboard — every component the React web kit exports has a Flutter counterpart with the same name and brand tokens.

Public API

  • class LevelChat — entrypoint; joinRoom(roomId, opts) / issueToken (dev only).
  • class Roomconnect / close / publishCamera / publishMicrophone / publishScreen / unpublish(TrackSource) / setSpeakerphoneOn.
  • Stream<RoomEvent> room.events — sealed event hierarchy (ParticipantJoined, TrackSubscribed, QualityChanged, RoomReconnecting, RoomErrorEvent, …).

In-call chat and server-side recording are exposed on Web / iOS / Android today; the Flutter Room surface is publish + subscribe + lifecycle. Track the gap on the SDK parity matrix.

Audio session

Auto-configured via flutter_webrtc's Helper.setSpeakerphoneOn wrapper. Toggle the speaker at runtime with room.setSpeakerphoneOn(true).

Network resilience

The SDK auto-reconnects on transient signaling loss and restarts ICE on resume — you don't drive recovery yourself. Surface it in your UI by listening for the RoomReconnecting / RoomReconnected events (see step 3), and render a per-tile network indicator from QualityChanged.

License

The SDK itself is MIT-licensed — embed in any app, commercial or personal, no royalties. The LevelChat server is BUSL-licensed and requires a paid commercial license for production use beyond the trial threshold. See Licensing.