Skip to main content

Video Conference in Flutter App with ZEGOCLOUD Kit

· 8 min read

thumbnail

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.

  1. Create your zegocloud project.

alt

  1. Select Video Conference.

alt

  1. Input project name, e.g., "VConference_App".

alt

  1. Open Detail Project ZEGOCLOUD in Project Information Tab, you will find AppID and AppSign. Kepp this secret and don't share to public.

alt

  1. 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.
My Environment
  • Flutter: 3.29.0
  • IDE: Visual Studio Code
  • OS: Microsoft Windows 11
  • Android Studio: 2024.2
  1. Create Flutter project.

alt

alt

alt

alt

  1. Run this command to add uikit video conference package from zegocloud.
flutter pub add zego_uikit_prebuilt_video_conference
  1. Create new Class to store ZEGOCLOUD Secret keys.

alt

lib/config/zegocloud_keys.dart
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.

android/app/src/main/AndroidManifest.xml
<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" />

alt

Prevent code obfuscation for Android.

android/app/proguard-rules.pro
-keep class **.zego.** { *; }
# to shrink your code with R8 add below
-dontwarn com.itgsa.opensdk.mediaunit.KaraokeMediaHelper
android/app/build.gradle.kts
...
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.

ios/Runner/Info.plist
<key>NSCameraUsageDescription</key>
<string>We require camera access to connect</string>
<key>NSMicrophoneUsageDescription</key>
<string>We require microphone access to connect</string>

alt

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.

alt

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
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.

lib/main.dart
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.

lib/pages/conference_page.dart
...

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.

lib/main.dart
...
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 LoginRobin JoinNami LoginNami Join
altaltaltalt
Nami Back CameraMemberChatShare Screen
altaltaltalt