Quick Start for Conference in Flutter
VideoSDK enables you to embed the video calling feature into your Flutter application in minutes.
In this quickstart, we are going to explore group calling feature of Video SDK. We will go through step by step guide of integrating video calling with Flutter Video SDK.
This guide will get you running with the VideoSDK video & audio calling in minutes.
Prerequisites
Before proceeding, ensure that your development environment meets the following requirements:
- Video SDK Developer Account (Not having one, follow Video SDK Dashboard)
- The basic understanding of Flutter.
- Flutter Video SDK
- Have Flutter 3.41 or later installed on your device.
One should have a VideoSDK account to generate token. Visit VideoSDK dashboard to generate token
Getting Started with the Code!
Follow the steps to create the environment necessary to add video calls into your app. Also you can find the code sample for quickstart here (this example uses an older SDK version).
Create a new Flutter project.
Create a new Flutter App using the below command.
$ flutter create videosdk_flutter_quickstart
Install Video SDK
Install the VideoSDK using the below-mentioned flutter command. Make sure you are in your flutter app directory before you run this command.
$ flutter pub add videosdk:^5.0.0
Then pin permission_handler, which the SDK uses to ask for camera and microphone permission, to version 12. Version 13 brings in permission_handler_android 14, which builds only with Android Gradle Plugin 9, so an app created with Flutter 3.41 fails to build for Android.
$ flutter pub add permission_handler:^12.0.1
Run the following command to add the http library, which is used to make the network call that generates a roomId.
$ flutter pub add http
Video SDK Compatibility
The Flutter SDK runs on Android and iOS. Web, macOS and Windows are not supported.
Structure of the project
Your project structure should look like this.
root
├── android
├── ios
├── lib
├── api_call.dart
├── join_screen.dart
├── main.dart
├── room_controls.dart
├── room_screen.dart
├── participant_tile.dart
We are going to create flutter widgets (JoinScreen, RoomScreen, RoomControls, ParticipantTile).
App Structure
App widget will contain JoinScreen and RoomScreen widget. RoomScreen will have RoomControls and ParticipantTile widget.

Configure Project
For Android
- Update the
/android/app/src/main/AndroidManifest.xmlfor the permissions we will be using to implement the audio and video features.
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<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.INTERNET"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />
- Also you will need to set your build settings to Java 17, because recent versions of the Android Gradle Plugin and the SDK's Android dependencies require it. Just add this to your app-level
/android/app/build.gradle.kts.
android {
//...
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
-
If necessary, in the same
build.gradle.ktsyou will need to increaseminSdkofdefaultConfigup to24(currently default Flutter generator set it to24). -
The SDK ships its native libraries for
arm64-v8aandarmeabi-v7a, so it does not run on x86 and x86_64 emulators. Test on an ARM device or on an emulator with anarm64-v8asystem image. To build your app forarm64-v8aandarmeabi-v7aonly, add an ABI filter todefaultConfigin the samebuild.gradle.kts.
android {
defaultConfig {
//...
ndk {
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
}
}
}
- Then add this line to
/android/gradle.properties. Without it, Flutter adds its own ABIs to the filter and yours has no effect.
disable-abi-filtering=true
- For release builds, keep the SDK's classes. Flutter shrinks every Android release build with R8, which removes or renames the
videosdk.orgclasses that the SDK loads by name. Add these rules to/android/app/proguard-rules.pro, and create the file if it does not exist. Keep the-dontwarnline, because the keep rule alone fails the build.
-keep class videosdk.org.** { *; }
-dontwarn videosdk.org.**
If your project still uses the Groovy DSL (android/app/build.gradle), the same property assignments work there too. The older compileSdkVersion and minSdkVersion method syntax is deprecated as of Android Gradle Plugin 9 and will warn during the build.
For iOS
- Add the following entries which allow your app to access the camera and microphone to your
/ios/Runner/Info.plistfile :
<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) Camera Usage!</string>
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) Microphone Usage!</string>
- The SDK's iOS plugin ships as a Swift Package, so it needs Flutter 3.41 or later with Swift Package Manager turned on. Turn it on for your project by adding
config:under the existingflutter:section of your app'spubspec.yaml, or runflutter config --enable-swift-package-manageron every machine that builds your app.
flutter:
config:
enable-swift-package-manager: true
Step 1: Get started with api_call.dart
Before jumping to anything else, you will write a function to generate a unique room ID. You will require auth token, you can generate it using either by using videosdk-rtc-api-server-examples or generate it from the Video SDK Dashboard for development.
import 'dart:convert';
import 'package:http/http.dart' as http;
//Auth token we will use to generate a room and connect to it
String token = "<Generated-from-dashboard>";
// API call to create room
Future<String> createRoomId() async {
final http.Response httpResponse = await http.post(
Uri.parse("https://api.videosdk.live/v2/rooms"),
headers: {'Authorization': token},
);
//Destructuring the roomId from the response
return json.decode(httpResponse.body)['roomId'];
}
Step 2: Creating the JoinScreen
Let's create join_screen.dart file in lib directory and create JoinScreen StatelessWidget.
The JoinScreen will consist of:
- Create Room Button - This button will create a new room for you.
- Room ID TextField - This text field will contain the room ID, you want to join.
- Join Room Button - This button will join the room, which you have provided.
import 'package:flutter/material.dart';
import 'api_call.dart';
import 'room_screen.dart';
class JoinScreen extends StatelessWidget {
final _roomIdController = TextEditingController();
JoinScreen({super.key});
void onCreateButtonPressed(BuildContext context) async {
// call api to create room and then navigate to RoomScreen with roomId,token
await createRoomId().then((roomId) {
if (!context.mounted) return;
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => RoomScreen(
roomId: roomId,
token: token,
),
),
);
});
}
void onJoinButtonPressed(BuildContext context) {
String roomId = _roomIdController.text;
var re = RegExp("\\w{4}\\-\\w{4}\\-\\w{4}");
// check room id is not null or invaild
// if room id is vaild then navigate to RoomScreen with roomId,token
if (roomId.isNotEmpty && re.hasMatch(roomId)) {
_roomIdController.clear();
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => RoomScreen(
roomId: roomId,
token: token,
),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text("Please enter valid room id"),
));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('VideoSDK QuickStart'),
),
body: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => onCreateButtonPressed(context),
child: const Text('Create Room'),
),
Container(
margin: const EdgeInsets.fromLTRB(0, 8.0, 0, 8.0),
child: TextField(
decoration: const InputDecoration(
hintText: 'Room Id',
border: OutlineInputBorder(),
),
controller: _roomIdController,
),
),
ElevatedButton(
onPressed: () => onJoinButtonPressed(context),
child: const Text('Join Room'),
),
],
),
),
);
}
}
- Update the home screen of the app in the
main.dart
import 'package:flutter/material.dart';
import 'join_screen.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'VideoSDK QuickStart',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: JoinScreen(),
);
}
}
Output

Step 3: Creating the RoomControls
Let's create room_controls.dart file and create RoomControls StatelessWidget.
The RoomControls will consist of:
- Leave Button - This button will leave the room.
- Toggle Mic Button - This button will unmute or mute mic.
- Toggle Camera Button - This button will enable or disable camera.
RoomControls will accept 3 functions in constructor
- onLeaveButtonPressed - invoked when Leave button pressed
- onToggleMicButtonPressed - invoked when Toggle Mic button pressed
- onToggleCameraButtonPressed - invoked when Toggle Camera button pressed
import 'package:flutter/material.dart';
class RoomControls extends StatelessWidget {
final void Function() onToggleMicButtonPressed;
final void Function() onToggleCameraButtonPressed;
final void Function() onLeaveButtonPressed;
const RoomControls(
{super.key,
required this.onToggleMicButtonPressed,
required this.onToggleCameraButtonPressed,
required this.onLeaveButtonPressed});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: onLeaveButtonPressed, child: const Text('Leave')),
ElevatedButton(
onPressed: onToggleMicButtonPressed, child: const Text('Toggle Mic')),
ElevatedButton(
onPressed: onToggleCameraButtonPressed,
child: const Text('Toggle WebCam')),
],
);
}
}
Step 4: Creating ParticipantTile
Let's create participant_tile.dart file and create ParticipantTile StatefulWidget.
The ParticipantTile will consist of:
- RTCVideoView - This will show participant's video stream.
ParticipantTile will accept Room and Participant in constructor
- room - the room of the room. It delivers the events for every participant's streams.
- participant - participant of the room.
import 'package:flutter/material.dart';
import 'package:videosdk/videosdk.dart';
class ParticipantTile extends StatefulWidget {
final Room room;
final Participant participant;
const ParticipantTile(
{super.key, required this.room, required this.participant});
@override
State<ParticipantTile> createState() => _ParticipantTileState();
}
class _ParticipantTileState extends State<ParticipantTile> {
RTCStream? videoStream;
@override
void initState() {
super.initState();
// initial video stream for the participant
videoStream = widget.participant.streams.values
.where((stream) => stream.kind == MediaType.VIDEO)
.firstOrNull;
_initStreamListeners();
}
@override
void setState(fn) {
if (mounted) {
super.setState(fn);
}
}
// Stream events are delivered on the room.
// stream.participantId is the id of the participant the stream belongs to.
void _initStreamListeners() {
widget.room.on(Events.streamEnabled, _onStreamEnabled);
widget.room.on(Events.streamDisabled, _onStreamDisabled);
}
bool _isVideoOfThisParticipant(RTCStream stream) =>
stream.participantId == widget.participant.id &&
stream.kind == MediaType.VIDEO;
void _onStreamEnabled(RTCStream stream) {
if (_isVideoOfThisParticipant(stream)) {
setState(() => videoStream = stream);
}
}
void _onStreamDisabled(RTCStream stream) {
if (_isVideoOfThisParticipant(stream)) {
setState(() => videoStream = null);
}
}
@override
void dispose() {
// The room outlives the tile, so remove the handlers from it.
widget.room.off(Events.streamEnabled, _onStreamEnabled);
widget.room.off(Events.streamDisabled, _onStreamDisabled);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: videoStream != null
? RTCVideoView(
videoStream?.renderer as RTCVideoRenderer,
objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover,
)
: Container(
color: Colors.grey.shade800,
child: const Center(
child: Icon(
Icons.person,
size: 100,
),
),
),
);
}
}
Step 5: Creating the RoomScreen
Let's create room_screen.dart file and create RoomScreen StatefulWidget.
RoomScreen will accept room ID and token in constructor
- room ID - room ID, you want to join
- token - VideoSdk Auth token
The SDK does not ask for camera and microphone permission on Android, so the screen asks with VideoSDK.requestPermissions() before it joins. join(), leave() and the mic and camera actions return a Future that throws a VideoSDKError when the action fails, so the screen awaits them inside try / catch. The room releases itself after it leaves, so there is nothing to clean up when the screen closes.
import 'dart:async' show unawaited;
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:videosdk/videosdk.dart';
import './room_controls.dart';
import './participant_tile.dart';
class RoomScreen extends StatefulWidget {
final String roomId;
final String token;
const RoomScreen(
{super.key, required this.roomId, required this.token});
@override
State<RoomScreen> createState() => _RoomScreenState();
}
class _RoomScreenState extends State<RoomScreen> {
late Room _room;
var micEnabled = true;
var camEnabled = true;
Map<String, Participant> participants = {};
@override
void initState() {
super.initState();
// create room
_room = VideoSDK.createRoom(
roomId: widget.roomId,
token: widget.token,
participantName: "John Doe",
micEnabled: micEnabled,
camEnabled: camEnabled,
defaultCamera: FacingMode.FRONT, // camera used when the room starts
);
setRoomEventListener();
// Join room
unawaited(joinRoom());
}
Future<void> joinRoom() async {
// On Android, the SDK does not ask for camera and microphone permission.
await VideoSDK.requestPermissions();
try {
await _room.join();
} on VideoSDKError catch (error) {
log("Could not join the room: ${error.code} ${error.name}");
}
}
@override
void setState(fn) {
if (mounted) {
super.setState(fn);
}
}
// listening to room events
void setRoomEventListener() {
_room.on(Events.roomJoined, () {
setState(() {
participants.putIfAbsent(
_room.localParticipant.id, () => _room.localParticipant);
});
});
_room.on(
Events.participantJoined,
(Participant participant) {
setState(
() => participants.putIfAbsent(participant.id, () => participant),
);
},
);
_room.on(Events.participantLeft,
(Participant participant, LeaveReason reason) {
if (participants.containsKey(participant.id)) {
setState(
() => participants.remove(participant.id),
);
}
});
_room.on(Events.roomLeft, () {
participants.clear();
Navigator.popUntil(context, ModalRoute.withName('/'));
});
}
Future<void> leaveRoom() async {
try {
await _room.leave();
} on VideoSDKError catch (error) {
// 3054: the room has already left.
if (error.code != 3054) {
log("Could not leave the room: ${error.code} ${error.name}");
}
}
}
Future<void> toggleMic() async {
try {
micEnabled ? await _room.muteMic() : await _room.unmuteMic();
micEnabled = !micEnabled;
} on VideoSDKError catch (error) {
log("Could not toggle the mic: ${error.code} ${error.name}");
}
}
Future<void> toggleCamera() async {
try {
camEnabled ? await _room.disableCam() : await _room.enableCam();
camEnabled = !camEnabled;
} on VideoSDKError catch (error) {
log("Could not toggle the camera: ${error.code} ${error.name}");
}
}
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return PopScope(
// leave the room when the back button closes the screen
onPopInvokedWithResult: (didPop, result) {
if (didPop) unawaited(leaveRoom());
},
child: Scaffold(
appBar: AppBar(
title: const Text('VideoSDK QuickStart'),
),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Text(widget.roomId),
//render all participant
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
mainAxisExtent: 300,
),
itemBuilder: (context, index) {
return ParticipantTile(
key: Key(participants.values.elementAt(index).id),
room: _room,
participant: participants.values.elementAt(index));
},
itemCount: participants.length,
),
),
),
RoomControls(
onToggleMicButtonPressed: toggleMic,
onToggleCameraButtonPressed: toggleCamera,
onLeaveButtonPressed: leaveRoom,
),
],
),
),
),
);
}
}
Output

Run and Test
The app is all set to test. Make sure to update the token in api_call.dart
Your app should look like this after the implementation.
Run the app on an ARM Android device or an arm64-v8a emulator, and on an iPhone, an iPad or the iOS Simulator on a Mac with Apple silicon. x86 and x86_64 Android emulators and the iOS Simulator on an Intel Mac are not supported. See Known Issues.
You can checkout the complete quick start example here (this example uses an older SDK version).
Got a Question? Ask us on discord

