Skip to content

Flutter

Media runs on the livekit_client package. The Tele part is one HTTP call, shown below.

Prebuilt package

tele-flutter wraps all of this: an API client that acts as the signed-in user, a call controller that handles ICE servers, chat, reconnects and rejoins, and a native TeleCallScreen. Add it as a git dependency at ref: v0.2.0. This page shows what it does underneath, if you'd rather build your own.

Install

pubspec.yaml
dependencies:
  livekit_client: ^2.13.1   # ≥ 2.12.0 is required for Bangla/emoji chat
  http: ^1.2.0
  app_links: ^6.0.0          # if you open calls from invite links

Platform setup

<key>NSCameraUsageDescription</key><string>Video calls</string>
<key>NSMicrophoneUsageDescription</key><string>Audio calls</string>
<key>UIBackgroundModes</key><array><string>audio</string></array>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>

To keep a call alive in the background on Android 14+, you need a foreground service of type microphone|camera.

Join and connect

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:livekit_client/livekit_client.dart';

const teleApi = 'https://tele-api.cloudguys.io';

Future<Room> joinCall(String inviteCode, Future<String> Function() teleToken) async {
  final res = await http.post(
    Uri.parse('$teleApi/v1/invites/${Uri.encodeComponent(inviteCode)}/join'),
    headers: {'Authorization': 'Bearer ${await teleToken()}', 'X-Client-Platform': 'flutter'},
  );
  final body = jsonDecode(res.body) as Map<String, dynamic>;
  if (res.statusCode != 200) {
    throw TeleError(body['error']['code'], body['error']['message']);
  }

  final ice = (body['ice_servers'] as List?)
      ?.map((s) => RTCIceServer(
            urls: List<String>.from(s['urls']),
            username: s['username'],
            credential: s['credential'],
          ))
      .toList();

  final room = Room(roomOptions: const RoomOptions(adaptiveStream: true, dynacast: true));
  await room.connect(
    body['ws_url'],
    body['token'],
    connectOptions: ConnectOptions(
      // Tele's TURN servers replace the SDK defaults; omit when null.
      rtcConfiguration: ice == null ? const RTCConfiguration() : RTCConfiguration(iceServers: ice),
    ),
  );
  await room.localParticipant?.setMicrophoneEnabled(true);
  await room.localParticipant?.setCameraEnabled(true);
  return room;
}

class TeleError implements Exception {
  TeleError(this.code, this.message);
  final String code, message;
}

Render video with VideoTrackRenderer from livekit_client, or with the widgets in livekit_components.

Handle your own invite URL (for example https://yourapp.example/call/{code}) with app_links, extract the code, preview it with GET /v1/invites/{code}, then call joinCall. See Scheduling & invites.

Chat

Use text streams on lk.chat with your own small chat UI. See Chat. Don't use the prebuilt chat widget in livekit_components; it uses an older message format.