livqenoDocs

SDKsFlutter

Flutter SDK

livqeno_rtc, livqeno_chat and livqeno_live — calls, chat and live streaming in Dart.

The Flutter SDK is for iOS and Android apps built with Flutter. It adds calls, chat, and live streaming. It has the same ideas as the web and React Native SDKs, written the Dart way.

It runs on the user's phone and holds only a token, never your API key. Your backend makes the token with the TypeScript, Python, Go, Java or PHP SDK. Your app fetches it from your own endpoint (for example POST /join-room, using the http package) and passes it in.

Why a backend, and how to write it: the API key can't live in an app, so one small route on your server makes the token. Build your token endpoint has the full route in TypeScript, Python, Go, Java, PHP and Dart, plus the plain HTTP call for any other language. You don't need Node.js unless your backend already uses it.

The shortest version, in your Flutter app:

final livqeno = Livqeno(
  token: token,
  endpoint: endpoint,
  iceServers: iceServers, // from the grant: List<LivqenoIceServer>, see the Quickstart
);
final room = await livqeno.join('room_123');
 
await room.enableCamera();
await room.enableMicrophone();

Always pass iceServers from the grant. The Quickstart Flutter tab shows how to read it with LivqenoIceServer.fromJson. Without it there's no relay (TURN), and calls on mobile data often fail.

There are three packages. Install only what you need:

PackageFor
livqeno_rtcVideo and audio calls
livqeno_chatChat
livqeno_liveLive streaming (uses the other two)

A video app doesn't pull in chat code, and a chat app doesn't pull in WebRTC.

Install

dependencies:
  livqeno_rtc: ^0.2.2
  livqeno_chat: ^0.1.1                    # only if you want messaging

Or run flutter pub add livqeno_rtc livqeno_chat.

Add permissions

Phones won't let an app use the camera or mic unless the app declares it up front. These lines go in your native project files, which the SDK can't edit for you.

iOS: ios/Runner/Info.plist

<key>NSCameraUsageDescription</key>
<string>Camera access is used for video calls.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Microphone access is used for calls.</string>

Android: android/app/src/main/AndroidManifest.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 minimum versions

  • Android: minSdkVersion 23 in android/app/build.gradle.
  • iOS: deployment target 13.0 or later.

WebRTC won't build below these.

Idiomatic Dart, identical concepts

The ideas are the same as the JavaScript SDKs (@livqeno/rtc, @livqeno/chat). Only the style is Dart. This means docs written for the web (like Rooms & participants) still apply; you just read events from streams instead of callbacks:

  • LivqenoRoom and LivqenoChat are ChangeNotifiers, so they work with ListenableBuilder.
  • Events come as typed streams, instead of room.on('event', handler).
// In the widget tree
ListenableBuilder(listenable: room, builder: (context, _) => ...)
 
// Outside it
room.connectionStateChanges.listen((state) => ...);
room.participantChanges.listen((participants) => ...);
chat.messages.listen((message) => ...);

Camera, microphone, screen share

await room.enableCamera();
await room.disableCamera();
await room.enableMicrophone();
await room.disableMicrophone();
await room.switchCamera();          // front <-> rear
await room.enableScreenShare();
  • switchCamera() flips between front and rear cameras. (The web has no equivalent.)
  • Screen sharing works out of the box on Android.
  • On iOS, screen sharing needs a Broadcast Upload Extension. You add it as a target in Xcode; no package can do it for you. Without it, enableScreenShare() throws.

Show video

LivqenoVideoView(
  participant: participant,
  room: room,                     // enables automatic updates
  kind: LivqenoTrackKind.camera,
  fit: LivqenoVideoFit.cover,
  placeholder: const ColoredBox(color: Colors.black),
)

Pass room so the widget updates itself when tracks change. It also cleans up its native video texture when the widget goes away, so a scrolling grid doesn't leak memory.

Pass visible: false for a tile that's scrolled away or covered, and the server stops sending that video until it's visible again. A zero-size or disposed view counts as hidden too, and a person shown in two views stays on while either is visible.

LivqenoVideoView(participant: p, room: room, visible: isOnScreen)
 
room.setVideoEnabled(p, LivqenoTrackKind.camera, false); // pause by hand; wins over everything
Livqeno(..., autoSubscribe: false);                      // start remote video paused

Speaking and connection quality

room.activeSpeakers;                       // loudest first, you included
room.activeSpeakerChanges.listen((speakers) { });
participant.isSpeaking;                    // and participant.audioLevel, 0..1
participant.connectionQuality;             // LivqenoConnectionQuality.good, …
room.connectionQualityChanges.listen((update) { });
room.subscriptionStateChanges.listen((update) { }); // a video paused or resumed

Your camera only sends the qualities somebody is watching (dynacast, on by default).

Permissions

final granted = await LivqenoPermissions.request();
// { LivqenoPermission.camera: true, LivqenoPermission.microphone: false }
 
await LivqenoPermissions.require(); // throws LivqenoPermissionException
MethodWhat it does
request()Shows the OS prompt and returns what was granted.
require()Same, but throws LivqenoPermissionException if refused.

Ask for permission at a moment that makes sense to the user, for example when they tap Join call, not when the app first opens. People are more likely to say yes when they know why.

This package can't check permission status without prompting. If you need that, or need to know "denied" vs "permanently denied", add permission_handler:

final status = await Permission.camera.status;
if (status.isPermanentlyDenied) {
  await openAppSettings();
} else {
  await LivqenoPermissions.require([LivqenoPermission.camera]);
}

Chat

Chat uses its own chat token, separate from the call token. Your backend makes it (and adds the user to the conversation first). In your Flutter app:

final chat = LivqenoChat(token: chatToken, apiUrl: apiUrl);
await chat.connect('room_123');
 
chat.messages.listen((message) => print('${message.senderId}: ${message.text}'));
await chat.send('Hello everyone!');

More things you can do:

final page = await chat.history(limit: 50);
await chat.edit(messageId, 'Updated');
await chat.delete(messageId);
await chat.addReaction(messageId, '👍');
await chat.markAsRead(messageId);
await chat.startTyping();
 
final thread = await chat.thread(messageId);
final present = await chat.getPresence();

Streams you can listen to: messages, messageUpdates, messageDeletions, typing, presence, reactions, readReceipts, connectionStateChanges, errors.

send() finishes only after the message is safely saved. It adds an idempotency key for you, so retries don't create duplicates. See Chat Overview.

Reconnection

Phones drop connection often: a lift, a tunnel, leaving Wi-Fi. Reconnecting is done for you. LivqenoChat retries with backoff and jitter, re-joins its rooms, and stops after a maximum number of tries.

A rejected token is never retried. Close codes 4401 and 4403 are final. Retrying would just fail again, so instead ask your backend for a new token and connect again.

Live streaming

Use this for broadcasts, like a creator going live from their phone. Your backend decides each person's role first: addHost() for a host, createViewerToken() for a viewer.

Add livqeno_live. It uses livqeno_rtc and livqeno_chat under the hood, so a stream's room and chat are a normal LivqenoRoom and LivqenoChat.

dependencies:
  livqeno_live: ^0.2.0

Or run flutter pub add livqeno_live.

import 'package:livqeno_live/livqeno_live.dart';
 
final stream = await LivqenoLiveStream.join(credentials);
 
if (stream.isHost) {
  await stream.room.enableCamera();
  await stream.room.enableMicrophone();
}
 
stream.room.participantChanges.listen((participants) => ...);
stream.chat?.messages.listen((message) => ...);
 
await stream.react('❤️');
await stream.leave();

Large audiences (HLS playback)

If a stream is created with edgeScaling, or more people try to watch than the RTC room can hold, Livqeno moves those viewers to HLS instead — a video format phones and browsers can play without joining the call's room at all. For an HLS viewer, stream.delivery is 'hls' (instead of 'rtc'), and calling stream.room raises an error. Play the stream with LivqenoLivePlayer instead:

if (stream.delivery == 'hls') {
  LivqenoLivePlayer(
    url: stream.playbackUrl!,
    lowLatencyUrl: stream.lowLatencyPlaybackUrl, // LL-HLS, tried first
    beacon: stream.playbackBeacon, // watch-time heartbeat for viewer-minute billing
  )
} else {
  // normal RTC viewer: stream.room
}

Mint these credentials on your server with createViewerToken(streamId, identity, acceptPlayback: true) — see TypeScript → Live streaming. LivqenoLivePlayer keeps retrying every retryEvery (2 seconds by default) until the stream actually goes live, and shows placeholder until the first frame arrives.

The stage

stream.stageEvents tells you when someone is invited onto the stage, promoted to co-host, or demoted back to viewer — these travel as special messages through the stream's chat. Only your server can send invited, promoted, or demoted (an app can't fake one of these), but a viewer's own app can send raise_hand and lower_hand:

stream.stageEvents.listen((event) {
  if (event.action == 'promoted' && event.forMe) {
    // The server already minted host credentials for you; rejoin with them.
  }
});

Each LivqenoLiveStageEvent has four fields: action, identity, forMe (true when the event is about you), and role (only set on promoted).

Production

  • iOS screen sharing needs a Broadcast Upload Extension target in Xcode. There's no written guide yet. Contact support if you get stuck.
  • minSdkVersion 23 and iOS 13+ are hard minimums. WebRTC won't build below them.

Next steps

  1. Build the backend endpoint and a working call: Quickstart (Flutter tab).
  2. Set up camera and mic permissions in depth: Permissions.
  3. Learn the call API (same ideas as Dart): Rooms & participants.
  4. Add chat: Chat quickstart.