Quick Start in Flutter using BLoC
VideoSDK enables you to embed video calling functionality into your Flutter applications within minutes.
In this quick start, we’ll walk through integrating group video calling using Flutter Video SDK with BLoC for state management. You’ll learn how to architect your application using BLoCto manage state cleanly and efficiently.
This guide will get you up and running with VideoSDK’s video & audio calling along with BLoC state management 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)
- Familiarity with Flutter and basic BLoC concepts.
- Flutter Video SDK
- Have Flutter 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.
Create a new Flutter project.
Create a new Flutter App using the below command.
$ flutter create flutter_bloc_quickstart
Install Required Dependencies
Navigate to your project directory and run the following commands to install the necessary dependencies:
$ flutter pub add videosdk
//run this command to add http library to perform network call to generate roomId
$ flutter pub add http
// Add BLoC-related packages for state management
flutter pub add flutter_bloc
flutter pub add equatable
Video SDK Compatibility
Android and iOS | Web (Beta) | Desktop (Beta) | Safari |
---|---|---|---|
Structure of the project
Your project structure should look like this.
root
├── android
├── ios
├── lib
├── bloc
│ ├── meeting_cubit.dart
│ └── meeting_state.dart
├── api_call.dart
├── join_screen.dart
├── main.dart
├── meeting_controls.dart
├── meeting_screen.dart
└── participant_tile.dart
We are going to create flutter widgets (JoinScreen, MeetingScreen, MeetingControls, ParticipantTile, meeting_cubit, meeting_state).
App Structure
App widget will contain JoinScreen
and MeetingScreen
widget. MeetingScreen
will have MeetingControls
and ParticipantTile
widget.
Configure Project
For Android
- Update the
/android/app/src/main/AndroidManifest.xml
for 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" />
For iOS
- Add the following entries which allow your app to access the camera and microphone to your
/ios/Runner/Info.plist
file :
<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) Camera Usage!</string>
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) Microphone Usage!</string>
- Uncomment the following line to define a global platform for your project in
/ios/Podfile
:
# platform :ios, '12.0'
For MacOS
- Add the following entries to your
/macos/Runner/Info.plist
file which allow your app to access the camera and microphone.
<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) Camera Usage!</string>
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) Microphone Usage!</string>
- Add the following entries to your
/macos/Runner/DebugProfile.entitlements
file which allow your app to access the camera, microphone and open outgoing network connections.
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.device.microphone</key>
<true/>
- Add the following entries to your
/macos/Runner/Release.entitlements
file which allow your app to access the camera, microphone and open outgoing network connections.
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.device.microphone</key>
<true/>
Step 1: Get started with api_call.dart
Before jumping to anything else, you will write a function to generate a unique meetingId. 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 meeting and connect to it
String token = "<Generated-from-dashboard>";
// API call to create meeting
Future<String> createMeeting() 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: Create BLoC Files
To manage meeting logic and state cleanly, we’ll use BLoC architecture. This includes two key files:
meeting_cubit.dart
Handles all meeting-related logic:
- Initializes the VideoSDK
Room
. - Listens to meeting events (
roomJoined
,participantJoined
, etc.). - Manages local/remote participant states and their video streams.
- Provides functions like
toggleMic
,toggleCam
,leaveMeeting
.
This acts as the controller for our app's meeting behavior.
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:videosdk/videosdk.dart';
part 'meeting_state.dart';
/// Cubit to manage video meeting state using VideoSDK
class MeetingCubit extends Cubit<MeetingState> {
// The room instance from VideoSDK
late Room _room;
/// Initializes with default state
MeetingCubit()
: super(
MeetingState(
isMicOff: false,
isVideoOff: false,
participants: {},
participantVideoStreams: {},
),
);
void initializeRoom(String token, String meetingId, BuildContext context) {
// create room
_room = VideoSDK.createRoom(
roomId: meetingId,
token: token,
displayName: "John Doe",
micEnabled: true,
camEnabled: true,
defaultCameraIndex: kIsWeb ? 0 : 1,
);
// Set room and add listeners
setRoom(_room, context);
// Join room
_room.join();
}
/// Sets the room and registers meeting event listeners
void setRoom(Room room, BuildContext context) {
_room = room;
_setMeetingEventListener(context);
}
/// Handles room and participant lifecycle events
void _setMeetingEventListener(BuildContext context) {
// Triggered when the local participant successfully joins the room.
_room.on(Events.roomJoined, () {
final updated = Map<String, Participant>.from(state.participants);
updated[_room.localParticipant.id] = _room.localParticipant;
_initializeParticipantStreams(_room.localParticipant);
emit(state.copyWith(participants: updated));
});
// Triggered when a remote participant joins the room.
_room.on(Events.participantJoined, (Participant participant) {
final updated = Map<String, Participant>.from(state.participants);
updated[participant.id] = participant;
_initializeParticipantStreams(participant);
emit(state.copyWith(participants: updated));
});
// Triggered when a remote participant left the room.
_room.on(Events.participantLeft, (String participantId) {
final updated = Map<String, Participant>.from(state.participants);
final updatedStreams = Map<String, Stream?>.from(
state.participantVideoStreams,
);
updated.remove(participantId);
updatedStreams.remove(participantId);
emit(
state.copyWith(
participants: updated,
participantVideoStreams: updatedStreams,
),
);
});
// Triggered when the local participant successfully leave the room.
_room.on(Events.roomLeft, () {
emit(state.copyWith(participants: {}, participantVideoStreams: {}));
Navigator.popUntil(context, ModalRoute.withName('/'));
});
}
/// Initializes and listens to participant video stream changes
void _initializeParticipantStreams(Participant participant) {
final updatedStreams = Map<String, Stream?>.from(
state.participantVideoStreams,
);
// Initialize video stream
participant.streams.forEach((key, Stream stream) {
if (stream.kind == 'video') {
updatedStreams[participant.id] = stream;
}
});
// Video stream enabled
participant.on(Events.streamEnabled, (Stream stream) {
if (stream.kind == 'video') {
final newStreams = Map<String, Stream?>.from(
state.participantVideoStreams,
);
newStreams[participant.id] = stream;
emit(state.copyWith(participantVideoStreams: newStreams));
}
});
// Video stream disabled
participant.on(Events.streamDisabled, (Stream stream) {
if (stream.kind == 'video') {
final newStreams = Map<String, Stream?>.from(
state.participantVideoStreams,
);
newStreams[participant.id] = null;
emit(state.copyWith(participantVideoStreams: newStreams));
}
});
emit(state.copyWith(participantVideoStreams: updatedStreams));
}
/// Toggles the local participant's mic
void toggleMic() {
if (state.isMicOff) {
_room.unmuteMic();
} else {
_room.muteMic();
}
emit(state.copyWith(isMicOff: !state.isMicOff));
}
/// Toggles the local participant's camera
void toggleCam() {
if (state.isVideoOff) {
_room.enableCam();
} else {
_room.disableCam();
}
emit(state.copyWith(isVideoOff: !state.isVideoOff));
}
/// Leaves the current meeting
void leaveMeeting() {
_room.leave();
}
/// Room accessor for UI or other logic
Room get room => _room;
}
meeting_state.dart
Defines the immutable state used by MeetingCubit
:
isMicOff
: Whether the mic is off.isVideoOff
: Whether the camera is off.participants
: A map of all participants in the room.participantVideoStreams
: A map of each participant's active video stream.
part of 'meeting_cubit.dart';
/// Represents the current state of the meeting
class MeetingState extends Equatable {
final bool isMicOff;
final bool isVideoOff;
final Map<String, Participant> participants;
final Map<String, Stream?> participantVideoStreams;
/// Creates a new immutable MeetingState instance
const MeetingState({
required this.isMicOff,
required this.isVideoOff,
required this.participants,
required this.participantVideoStreams,
});
/// Creates a copy of the current state with updated values
MeetingState copyWith({
bool? isMicOff,
bool? isVideoOff,
Map<String, Participant>? participants,
Map<String, Stream?>? participantVideoStreams,
}) {
return MeetingState(
isMicOff: isMicOff ?? this.isMicOff,
isVideoOff: isVideoOff ?? this.isVideoOff,
participants: participants ?? this.participants,
participantVideoStreams: participantVideoStreams ?? this.participantVideoStreams,
);
}
/// Used by Equatable to compare state instances efficiently
@override
List<Object?> get props => [
isMicOff,
isVideoOff,
participants,
participantVideoStreams,
];
}
Step 3 : Creating the JoinScreen
Let's create join_screen.dart
file in lib
directory and create JoinScreen StatelessWidget
.
The JoinScreen will consist of:
- Create Meeting Button - This button will create a new meeting for you.
- Meeting ID TextField - This text field will contain the meeting ID, you want to join.
- Join Meeting Button - This button will join the meeting, which you have provided.
Both buttons navigate to the MeetingScreen
while injecting the MeetingCubit
using BlocProvider
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'api_call.dart';
import 'meeting_screen.dart';
import 'bloc/meeting_cubit.dart';
class JoinScreen extends StatelessWidget {
final _meetingIdController = TextEditingController();
JoinScreen({super.key});
void onCreateButtonPressed(BuildContext context) async {
// call api to create meeting and then navigate to MeetingScreen with meetingId,token
await createMeeting().then((meetingId) {
if (!context.mounted) return;
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => BlocProvider<MeetingCubit>(
create: (_) => MeetingCubit(),
child: MeetingScreen(meetingId: meetingId, token: token),
),
),
);
});
}
void onJoinButtonPressed(BuildContext context) {
String meetingId = _meetingIdController.text;
var re = RegExp("\\w{4}\\-\\w{4}\\-\\w{4}");
// check meeting id is not null or invaild
// if meeting id is vaild then navigate to MeetingScreen with meetingId,token
if (meetingId.isNotEmpty && re.hasMatch(meetingId)) {
_meetingIdController.clear();
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => BlocProvider<MeetingCubit>(
create: (_) => MeetingCubit(),
child: MeetingScreen(meetingId: meetingId, token: token),
),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Please enter valid meeting 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 Meeting'),
),
Container(
margin: const EdgeInsets.fromLTRB(0, 8.0, 0, 8.0),
child: TextField(
decoration: const InputDecoration(
hintText: 'Meeting Id',
border: OutlineInputBorder(),
),
controller: _meetingIdController,
),
),
ElevatedButton(
onPressed: () => onJoinButtonPressed(context),
child: const Text('Join Meeting'),
),
],
),
),
);
}
}
- 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(),
);
}
}
Step 4 : Creating the MeetingControls
Let's create meeting_controls.dart
file and create MeetingControls StatelessWidget
.
The MeetingControls will consist of:
- Leave Button - This button will leave the meeting.
- Toggle Mic Button - This button will unmute or mute mic.
- Toggle Camera Button - This button will enable or disable camera.
MeetingControls 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 MeetingControls extends StatelessWidget {
final void Function() onToggleMicButtonPressed;
final void Function() onToggleCameraButtonPressed;
final void Function() onLeaveButtonPressed;
final bool isMicOff;
final bool isCamOff;
const MeetingControls({
super.key,
required this.onToggleMicButtonPressed,
required this.onToggleCameraButtonPressed,
required this.onLeaveButtonPressed,
required this.isMicOff,
required this.isCamOff,
});
@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 5 : Creating ParticipantTile
Let's create participant_tile.dart
file and create ParticipantTile StatelessWidget
.
The ParticipantTile will consist of:
- RTCVideoView - This will show participant's video stream.
ParticipantTile will accept Participant
in constructor
- participant - participant of the meeting.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:videosdk/videosdk.dart';
import 'bloc/meeting_cubit.dart';
class ParticipantTile extends StatelessWidget {
final Participant participant;
const ParticipantTile({super.key, required this.participant});
@override
Widget build(BuildContext context) {
return BlocBuilder<MeetingCubit, MeetingState>(
builder: (context, state) {
final videoStream = state.participantVideoStreams[participant.id];
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 6 : Creating the MeetingScreen
Let's create meeting_screen.dart
file and create MeetingScreen StatefulWidget
.
MeetingScreen will accept meetingId and token in constructor
- meetingId - meetingId, you want to join
- token - VideoSdk Auth token
BLoC Usage:
- Uses
MeetingCubit
to initialize and manage the meeting room state. - Calls
initializeRoom
inbuild
method to join the room usingtoken
,meetingId
, andcontext
. - Uses
BlocBuilder
to rebuild UI based on changes inMeetingState
. - Handles meeting controls (toggle mic/cam, leave) by invoking corresponding methods from
MeetingCubit
.
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'participant_tile.dart';
import 'meeting_controls.dart';
import 'bloc/meeting_cubit.dart';
class MeetingScreen extends StatelessWidget {
final String meetingId;
final String token;
const MeetingScreen({
super.key,
required this.meetingId,
required this.token,
});
@override
Widget build(BuildContext context) {
// Initialize room when the screen is first built
context.read<MeetingCubit>().initializeRoom(token, meetingId, context);
return WillPopScope(
onWillPop: () async {
context.read<MeetingCubit>().leaveMeeting();
return true;
},
child: Scaffold(
appBar: AppBar(title: const Text('VideoSDK QuickStart')),
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Text(meetingId),
//render all participant
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: BlocBuilder<MeetingCubit, MeetingState>(
builder: (context, state) {
final participants = state.participants.values.toList();
return GridView.builder(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
mainAxisExtent: 300,
),
itemBuilder: (context, index) {
return ParticipantTile(
key: Key(participants[index].id),
participant: participants[index],
);
},
itemCount: participants.length,
);
},
),
),
),
BlocBuilder<MeetingCubit, MeetingState>(
builder: (context, state) {
return MeetingControls(
onToggleMicButtonPressed: () {
context.read<MeetingCubit>().toggleMic();
},
onToggleCameraButtonPressed: () {
context.read<MeetingCubit>().toggleCam();
},
onLeaveButtonPressed: () {
context.read<MeetingCubit>().leaveMeeting();
},
isMicOff: state.isMicOff,
isCamOff: state.isVideoOff,
);
},
),
],
),
),
),
);
}
}
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.
If you get webrtc/webrtc.h file not found
error at a runtime in ios then check solution here.
You can checkout the complete flutter bloc example here.
Got a Question? Ask us on discord