Skip to main content

How to Implement Video Calls in Flutter with Twilio

Video calls have become an essential feature in modern applications. This guide walks you through implementing video calls in Flutter using Twilio's Programmable Video SDK.

Prerequisites

Before starting, ensure you have:

  • Flutter SDK (3.16.0 or later) installed
  • An IDE (VS Code or Android Studio)
  • A Twilio account
  • Basic knowledge of Dart and Flutter

Project Setup

Create New Flutter Project

flutter create video_call_app
cd video_call_app

Twilio Setup

  1. Create Twilio Account Sign up at Twilio Console Navigate to Programmable Video Create a new API Key and Secret
  2. Configure Permissions Add these permissions to your Android and iOS configurations:
<manifest>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<!-- ... rest of your manifest file -->
</manifest>

Implementation

Video Call Service Create a service to handle Twilio video calls:

import 'package:twilio_programmable_video/twilio_programmable_video.dart';
import 'package:permission_handler/permission_handler.dart';

class VideoCallService {
Room? _room;

Future<bool> requestPermissions() async {
await [
Permission.camera,
Permission.microphone,
].request();

return await Permission.camera.isGranted &&
await Permission.microphone.isGranted;
}

Future<Room?> connectToRoom(String accessToken, String roomName) async {
try {
final connectOptions = ConnectOptions(
accessToken,
roomName: roomName,
enableNetworkQuality: true,
enableDominantSpeaker: true,
preferredAudioCodecs: [OpusCodec()],
preferredVideoCodecs: [H264Codec()],
enableAutomaticSubscription: true,
);

_room = await TwilioProgrammableVideo.connect(connectOptions);
return _room;
} catch (e) {
print('Error connecting to room: $e');
return null;
}
}

Future<void> disconnect() async {
try {
await _room?.disconnect();
_room = null;
} catch (e) {
print('Error disconnecting from room: $e');
}
}
}

Video Call Screen Create a screen to display the video call:

import 'package:flutter/material.dart';
import 'package:twilio_programmable_video/twilio_programmable_video.dart';

class VideoCallScreen extends StatefulWidget {
final String accessToken;
final String roomName;

const VideoCallScreen({
super.key,
required this.accessToken,
required this.roomName,
});


_VideoCallScreenState createState() => _VideoCallScreenState();
}

class _VideoCallScreenState extends State<VideoCallScreen> {
Room? _room;
LocalVideoTrack? _localVideoTrack;
List<RemoteParticipant> _remoteParticipants = [];


void initState() {
super.initState();
_connectToRoom();
}

Future<void> _connectToRoom() async {
try {
final cameraCapturer = CameraCapturer(CameraSource.fromMap(
{
'source': 'camera',
'cameraSource': 'back',
},
));
_localVideoTrack = LocalVideoTrack(true, cameraCapturer);
final connectOptions = ConnectOptions(
widget.accessToken,
roomName: widget.roomName,
videoTracks: [_localVideoTrack!],
);
_room = await TwilioProgrammableVideo.connect(connectOptions);
setState(() {
_remoteParticipants = _room?.remoteParticipants ?? [];
});
} catch (e) {
print('Error connecting to room: $e');
}
}


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Video Call'),
actions: [
IconButton(
icon: const Icon(Icons.call_end),
color: Colors.red,
onPressed: () async {
await _room?.disconnect();
if(!context.mounted){
return;
} Navigator.pop(context);
},
),
],
),
body: SafeArea(
child: Column(
children: [
// Local video
Expanded(
flex: 1,
child: _localVideoTrack != null
? _LocalVideoTrackWidget(_localVideoTrack!)
: Center(child: CircularProgressIndicator()),
),
// Remote videos
Expanded(
flex: 3,
child: ListView.builder(
itemCount: _remoteParticipants.length,
itemBuilder: (context, index) {
return _RemoteParticipantWidget(
_remoteParticipants[index],
);
},
),
),
],
),
),
);
}


void dispose() {
_localVideoTrack?.release();
_room?.disconnect();
super.dispose();
}
}


class _LocalVideoTrackWidget extends StatelessWidget {
final LocalVideoTrack localVideoTrack;

const _LocalVideoTrackWidget(this.localVideoTrack, {super.key});


Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.blueAccent),
borderRadius: BorderRadius.circular(10),
),
margin: const EdgeInsets.all(10),
padding: const EdgeInsets.all(10),
child: localVideoTrack.widget(),
);
}
}

class _RemoteParticipantWidget extends StatelessWidget {
final RemoteParticipant participant;

const _RemoteParticipantWidget(this.participant, {super.key});


Widget build(BuildContext context) {
List<Widget> videoWidgets = participant.remoteVideoTracks.map(
(track) {
if (track.isTrackEnabled) {
return Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.redAccent),
borderRadius: BorderRadius.circular(10),
),
margin: const EdgeInsets.all(10),
padding: const EdgeInsets.all(10),
child: track.remoteVideoTrack?.widget(),
);
} else {
return Container();
}
}).toList();

return Column(
children: videoWidgets,
);
}
}

Usage Example Here's how to use the video call implementation:

import 'package:flutter/material.dart';
import 'package:video_call_app/video_call_screen.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});


Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const VideoCallScreen(
accessToken: 'YOUR_ACCESS_TOKEN',
roomName: 'ROOM_NAME',
),
),
);
},
child: const Text('Start Video Call'),
),
),
),
);
}
}