Video Conference in Flutter App with ZEGOCLOUD Kit
Hello Flutter Enthusiast!, it's time to add feature Video Conference in your own Company App or for your client. We will use Video Conference SDK Kit from ZEGOCLOUD. Let's get into it.
Prepare
You can get 10,000 minutes for registering new account to zegocloud. Login to zegocloud console and create zegocloud project.
- Create your zegocloud project.
- Select Video Conference.
- Input project name, e.g., "VConference_App".
- Open Detail Project ZEGOCLOUD in Project Information Tab, you will find AppID and AppSign. Kepp this secret and don't share to public.
- Please Re-Check your Local Flutter Development, because for some config in any platform can be different according to version you are using to develope.
- Flutter: 3.29.0
- IDE: Visual Studio Code
- OS: Microsoft Windows 11
- Android Studio: 2024.2
- Create Flutter project.
- Run this command to add uikit video conference package from zegocloud.
flutter pub add zego_uikit_prebuilt_video_conference
- Create new Class to store ZEGOCLOUD Secret keys.
class ZegocloudKeys {
static const appId = 0;
static const appSign = '';
}
Configure Project
Android
Check here to setup Android Gradle which are using Android Studio latest version and Flutter latest version. And check here to determine what version you should use.
Add these permission to Android manifest inside manifest tag.
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
Prevent code obfuscation for Android.
-keep class **.zego.** { *; }
# to shrink your code with R8 add below
-dontwarn com.itgsa.opensdk.mediaunit.KaraokeMediaHelper
- .gradle.kts
- .gradle
...
android {
...
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
}
}
}
...
...
android {
...
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
...
iOS
Add permission to Info.plist inside dict tag.
<key>NSCameraUsageDescription</key>
<string>We require camera access to connect</string>
<key>NSMicrophoneUsageDescription</key>
<string>We require microphone access to connect</string>
To setup Podfile, you can open xcode and add code below
# Start of the permission_handler configuration
target.build_configurations.each do |config|
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
'PERMISSION_CAMERA=1',
'PERMISSION_MICROPHONE=1',
]
end
# End of the permission_handler configuration
But, if you are using windows like me, you need to install Ruby and Cocoapods then you can generate Podfile. Run pod init
to generate Podfile.
Podfile above is not like in XCode. Edit Podfile like this:
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'
# this target config need tore-configure like in the XCode
target 'Runner' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
# Pods for Runner
target 'RunnerTests' do
inherit! :search_paths
# Pods for testing
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
# Start of the permission_handler configuration
target.build_configurations.each do |config|
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
'PERMISSION_CAMERA=1',
'PERMISSION_MICROPHONE=1',
]
end
# End of the permission_handler configuration
end
end
Login Page
First we create an entry point page to get user id and username. We will use Auth Template, then edit so we can get userId
, userName
, and conferenceId
.
lib/pages/login_page.dart
- d_input 1.3.0
- d_input 2.0.0
import 'package:flutter/material.dart';
import 'package:d_input/d_input.dart';
import 'package:gap/gap.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _userIdController = TextEditingController();
final _userNameController = TextEditingController();
final _conferenceIdController = TextEditingController();
void _executeLogin() {
final userId = _userIdController.text;
if (userId == '') {
return _showInvalidMessage('User ID must be filled');
}
final userName = _userNameController.text;
if (userName == '') {
return _showInvalidMessage('User Name must be filled');
}
final conferenceId = _conferenceIdController.text;
if (conferenceId == '') {
return _showInvalidMessage('Conference ID must be filled');
}
Navigator.pushNamed(
context,
'/conference',
arguments: {
'userId': userId,
'userName': userName,
'conferenceId': conferenceId,
},
);
}
void dispose() {
_userIdController.dispose();
_userNameController.dispose();
_conferenceIdController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: Padding(
padding: const EdgeInsets.all(30),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircleAvatar(
radius: 80,
child: Icon(Icons.video_camera_front_rounded, size: 80),
),
Gap(30),
Text(
'VConference App',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 30,
color: Theme.of(context).primaryColor,
),
),
Gap(70),
DInput(controller: _userIdController, hint: 'User ID'),
Gap(20),
DInput(
controller: _userNameController,
hint: 'User Name',
),
Gap(20),
DInput(
controller: _conferenceIdController,
hint: 'Conference ID',
),
Gap(20),
SizedBox(
width: double.infinity,
height: 50,
child: FilledButton(
style: ButtonStyle(
shape: WidgetStatePropertyAll(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
),
onPressed: _executeLogin,
child: Text('Login'),
),
),
],
),
),
),
);
},
),
),
);
}
void _showInvalidMessage(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message, style: TextStyle(color: Colors.white)),
duration: Duration(seconds: 2),
backgroundColor: Colors.red,
),
);
}
}
import 'package:flutter/material.dart';
import 'package:d_input/d_input.dart';
import 'package:gap/gap.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final _userIdController = TextEditingController();
final _userNameController = TextEditingController();
final _conferenceIdController = TextEditingController();
void _executeLogin() {
final userId = _userIdController.text;
if (userId == '') {
return _showInvalidMessage('User ID must be filled');
}
final userName = _userNameController.text;
if (userName == '') {
return _showInvalidMessage('User Name must be filled');
}
final conferenceId = _conferenceIdController.text;
if (conferenceId == '') {
return _showInvalidMessage('Conference ID must be filled');
}
Navigator.pushNamed(
context,
'/conference',
arguments: {
'userId': userId,
'userName': userName,
'conferenceId': conferenceId,
},
);
}
void dispose() {
_userIdController.dispose();
_userNameController.dispose();
_conferenceIdController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: Padding(
padding: const EdgeInsets.all(30),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircleAvatar(
radius: 80,
child: Icon(Icons.video_camera_front_rounded, size: 80),
),
Gap(30),
Text(
'VConference App',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 30,
color: Theme.of(context).primaryColor,
),
),
Gap(70),
DInput(
inputSpec: InputSpec(
controller: _userIdController,
hint: 'User ID',
),
),
Gap(20),
DInput(
inputSpec: InputSpec(
controller: _userNameController,
hint: 'User Name',
),
),
Gap(20),
DInput(
inputSpec: InputSpec(
controller: _conferenceIdController,
hint: 'Conference ID',
),
),
Gap(20),
SizedBox(
width: double.infinity,
height: 50,
child: FilledButton(
style: ButtonStyle(
shape: WidgetStatePropertyAll(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
),
onPressed: _executeLogin,
child: Text('Login'),
),
),
],
),
),
),
);
},
),
),
);
}
void _showInvalidMessage(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message, style: TextStyle(color: Colors.white)),
duration: Duration(seconds: 2),
backgroundColor: Colors.red,
),
);
}
}
Run flutter pub add d_input gap
to add required package. Inside Material App, setup launch route to Login page.
import 'package:flutter/material.dart';
import 'package:flutter_zegocloud_video_conference/pages/login_page.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
const MainApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
routes: {'/': (context) => LoginPage()},
);
}
}
Login Page will be like this:


Conference Page
Arguments
Create Conference Page Widget and create 3 argument to receive data were sent from Login Page.
...
const ConferencePage({
super.key,
required this.userId,
required this.userName,
required this.conferenceId,
});
final String userId;
final String userName;
final String conferenceId;
...
Register conference page to route in MaterialApp.
...
return MaterialApp(
debugShowCheckedModeBanner: false,
routes: {
'/': (context) => LoginPage(),
'/conference': (context) {
final data = ModalRoute.settingsOf(context)?.arguments as Map?;
if (data == null) {
return Scaffold(appBar: AppBar(title: Text('Invalid Argument')));
}
return ConferencePage(
userId: data['userId'],
userName: data['userName'],
conferenceId: data['conferenceId'],
);
},
},
);
...
Build Method
Create UI for conference from ZEGOCLOUD UIKit
Widget build(BuildContext context) {
return SafeArea(
child: ZegoUIKitPrebuiltVideoConference(
appID: ZegocloudKeys.appId,
appSign: ZegocloudKeys.appSign,
conferenceID: conferenceId,
userID: userId,
userName: userName,
config: ZegoUIKitPrebuiltVideoConferenceConfig(),
),
);
}
Conference Full Code
lib/pages/conference_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_zegocloud_video_conference/config/zegocloud_keys.dart';
import 'package:zego_uikit_prebuilt_video_conference/zego_uikit_prebuilt_video_conference.dart';
class ConferencePage extends StatelessWidget {
const ConferencePage({
super.key,
required this.userId,
required this.userName,
required this.conferenceId,
});
final String userId;
final String userName;
final String conferenceId;
Widget build(BuildContext context) {
return SafeArea(
child: ZegoUIKitPrebuiltVideoConference(
appID: ZegocloudKeys.appId,
appSign: ZegocloudKeys.appSign,
conferenceID: conferenceId,
userID: userId,
userName: userName,
config: ZegoUIKitPrebuiltVideoConferenceConfig(),
),
);
}
}
Flow
Sequence |
---|
User Open App |
↓ |
Redirect to Login Page |
↓ |
Conference Page |
↓ |
Join Room Meet |
↓ |
Conference Start |
Final Result
Here are some views of the final results. Test using Real Device Android 10 and Emulator Android 15.
Robin Login | Robin Join | Nami Login | Nami Join |
---|---|---|---|
![]() | ![]() | ![]() | ![]() |
Nami Back Camera | Member | Chat | Share Screen |
![]() | ![]() | ![]() | ![]() |