Skip to main content

How to Build Live Streaming App with Flutter and ZEGOCLOUD Kit

· 8 min read

thumbnail

To make streaming app, you need to understand about data stream or realtime data. Basically to consume data stream you can use custom web socket, but this need expert knowledge about handle data stream in the backend and your server. ZEGOCLOUD can be one solution to the problem of handling data streams for video streaming.

Now, we will build a Live Streaming App using ZEGOCOLOUD Kit. If you don't have zegocloud account, zegocloud offers free credit for new users. By registering a new account, you can get 10,000 minutes for free.

Prepare

  1. Create your zegocloud project at zegocloud dashboard.s

alt

  1. Select Live Streaming.

alt

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

alt

  1. Start with UIKits.

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

After create ZEGOCLOUD project for Live Streaming. Move on to creating a Flutter App.

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 live streaming package from zegocloud.
flutter pub add zego_uikit_prebuilt_live_streaming
  1. Create new Class to store ZEGOCLOUD Secret keys.

alt

lib/config/zegocloud_keys.dart
class ZegocloudKeys {
static const appId = 0;
static const appSign = '';
}
  1. Check here to setup Android Gradle which are using Android Studio latest version and Flutter latest version.

Configure Project

Android

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

alt

This Podfile 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'

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. You can also add live id input from the beginning and make logic for login as host or not. We can use Flutter Delux Auth Template for this needs.

Open auth template, copy & paste to login_page.dart also modify it first like below.

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 _liveIdController = TextEditingController();

void _executeLogin(bool isHost) {
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 liveId = _liveIdController.text;
if (liveId == '') return _showInvalidMessage('Live Id must be filled');

Navigator.pushNamed(
context,
'/live',
arguments: {
'userId': userId,
'userName': userName,
'liveId': liveId,
'isHost': isHost,
},
);
}


void dispose() {
_userIdController.dispose();
_userNameController.dispose();
_liveIdController.dispose();
super.dispose();
}


Widget build(BuildContext context) {
return Scaffold(
body: 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.stream, size: 80),
),
Gap(30),
Text(
'LStreaming App',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 30,
color: Theme.of(context).primaryColor,
),
),
Gap(70),
DInputMix(controller: _userIdController, hint: 'User Id'),
Gap(20),
DInputMix(
controller: _userNameController,
hint: 'User Name',
),
Gap(20),
DInputMix(controller: _liveIdController, hint: 'Live Id'),
Gap(10),
SizedBox(
width: double.infinity,
height: 50,
child: FilledButton(
style: ButtonStyle(
shape: WidgetStatePropertyAll(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
),
onPressed: () => _executeLogin(true),
child: Text('LOGIN as Host'),
),
),
Gap(20),
SizedBox(
width: double.infinity,
height: 50,
child: OutlinedButton(
style: ButtonStyle(
shape: WidgetStatePropertyAll(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
),
onPressed: () => _executeLogin(false),
child: Text('LOGIN as Audience'),
),
),
],
),
),
),
);
},
),
);
}

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 package required based on template.

Inside Material App, setup launch route to Login page.

lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_zegocloud_lstreaming/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:

Live Page

Arguments

Create Live Page class with Stateless Widget and create 4 argument to receive data were sent from Login Page.

lib/pages/live_page.dart
...

const LivePage({
super.key,
required this.userId,
required this.userName,
required this.liveId,
required this.isHost,
});
final String userId;
final String userName;
final String liveId;
final bool isHost;

...

Register live page to route in MaterialApp.

lib/main.dart
...
return MaterialApp(
debugShowCheckedModeBanner: false,
routes: {
'/': (context) => LoginPage(),
'/live': (context) {
final data = ModalRoute.settingsOf(context)?.arguments as Map?;
if (data == null) {
return Scaffold(appBar: AppBar(title: Text('Invalid Argument')));
}
return LivePage(
userId: data['userId'],
userName: data['userName'],
liveId: data['liveId'],
isHost: data['isHost'],
);
},
},
);
...

Live Page Full Code

Here for live page full code. Set input from Login page to LiveSTreamingKit Widget. AppId & AppSign was call from ZegocloudKeys class. Set user view based on role when Login with Live Streaming Config.

lib/pages/live_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_zegocloud_lstreaming/config/zegocloud_keys.dart';
import 'package:zego_uikit_prebuilt_live_streaming/zego_uikit_prebuilt_live_streaming.dart';

class LivePage extends StatelessWidget {
const LivePage({
super.key,
required this.userId,
required this.userName,
required this.liveId,
required this.isHost,
});
final String userId;
final String userName;
final String liveId;
final bool isHost;


Widget build(BuildContext context) {
return SafeArea(
child: ZegoUIKitPrebuiltLiveStreaming(
appID: ZegocloudKeys.appId,
appSign: ZegocloudKeys.appSign,
userID: userId,
userName: userName,
liveID: liveId,
config:
isHost
? ZegoUIKitPrebuiltLiveStreamingConfig.host()
: ZegoUIKitPrebuiltLiveStreamingConfig.audience(),
),
);
}
}

Flow

Sequence
User Open App
Redirect to Login Page
Input, Login as Role
HostAudience
Start Live StreamingWaiting Host to Start
Interaction (Such as chats)

Final Result

Here are some views of the final results. Test using OS Android 10 and 15.

Host

LoginPrepare LiveFace EffectLive Started
altaltaltalt
PermissionAudio EffectFront CameraCamera Off
altaltaltalt
Audience Role
alt

Audience

LoginIn LiveTyping chatChats
altaltaltalt
Audience Role
alt