Live streaming is no longer limited to social media platforms. Today, it’s a core feature in chat apps, video call apps, online education, live events, and real-time collaboration tools.
Building a live streaming system from scratch requires handling:
- Media engines
- Real-time networking
- Encoding & decoding
- Signaling servers
- Permissions & device compatibility
That’s a lot of complexity.
Fortunately, ZEGOCLOUD provides a production-ready Live Streaming SDK that allows Flutter developers to integrate real-time streaming with zero backend setup.
In this article, we’ll build a Flutter Live Streaming App that works just like chat & video call apps:
- One user starts a live stream
- Other users join instantly as viewers
🎯 What You’ll Build
By the end of this tutorial, you’ll have:
✅ A Flutter live streaming app
✅ Host (Broadcaster) & Audience roles
✅ Real-time video streaming
✅ Token-based authentication
✅ Clean and scalable project structure
✅ No WebRTC
✅ No custom backend
🔍 What Is ZEGOCLOUD?
ZEGOCLOUD is a real-time communication platform that provides SDKs for:
- 🎥 Live Streaming
- 📞 Video Calls
- 🎧 Voice Calls
- 💬 Real-time Chat
Instead of building and maintaining media servers, you only need:
- AppID
- Token (secure access)
ZEGOCLOUD handles everything else.
🔗 Official Links
- 🌐 Website: https://www.zegocloud.com
- 🎥 Live Streaming SDK: https://www.zegocloud.com/product/live-streaming
- 📚 Documentation: https://www.zegocloud.com/docs
🧭 App Flow Overview
The flow is intentionally designed to be similar to chat & video call apps:
- User opens the app
- Enters a Room ID
- Chooses:
- 🎥 Start Live (Broadcaster)
- 👀 Watch Live (Audience)
- Stream starts instantly
📸 Join Room Screen Screenshot
🗂 Project Structure
To keep the project clean and scalable:
lib/
├─ utils/
│ ├─ permission.dart
│ ├─ permission_io.dart
│ ├─ permission_web.dart
│ ├─ zegocloud_token.dart
│
├─ key_center.dart
├─ main.dart
├─ login.dart
├─ home_page.dart
└─ live_page.dart
📸 Project structure screenshot
🧩 Step 1 — Install Required Packages
Open pubspec.yaml and add:
Then run:
flutter pub get
🔐 Step 2 — Android Permissions Setup
Live streaming requires camera, microphone, and network access.
Add the following permissions to:
android/app/src/main/AndroidManifest.xml
⚙️ Step 3 — Android Build Configuration
Make sure your compileSdk, ndkVersion, and Java version are configured correctly:
🛡 Step 4 — Proguard Configuration (Release Mode)
Enable Proguard in build.gradle:
Create or edit:
android/app/proguard-rules.pro
🎙 Step 5 — Runtime Permissions
1- add the code in the file
lib\utils\permission_web.dart
We request microphone and camera permissions dynamically before streaming.
Future<bool> requestPermission() async {
return true;
}
2- add the code in the file
lib\utils\permission_io.dart
import 'package:flutter/foundation.dart';
import 'package:permission_handler/permission_handler.dart';
Future<bool> requestPermission() async {
debugPrint("requestPermission...");
try {
PermissionStatus microphoneStatus = await Permission.microphone.request();
if (microphoneStatus != PermissionStatus.granted) {
debugPrint('Error: Microphone permission not granted!!!');
return false;
}
} on Exception catch (error) {
debugPrint("[ERROR], request microphone permission exception, $error");
}
try {
PermissionStatus cameraStatus = await Permission.camera.request();
if (cameraStatus != PermissionStatus.granted) {
debugPrint('Error: Camera permission not granted!!!');
return false;
}
} on Exception catch (error) {
debugPrint("[ERROR], request camera permission exception, $error");
}
return true;
}
📸 Permission dialog screenshot
3-add the code in path #lib\utils\permission.dart
import 'permission_io.dart' if (dart.library.html) 'permission_web.dart' as impl;
Future<bool> requestPermission() async {
return impl.requestPermission();
}
4-add the code in path #lib\utils\zegocloud_token.dart
// ! ** Warning: ZegoTokenUtils is only for use during testing. When your application goes live,
// ! ** tokens must be generated by the server side. Please do not generate tokens on the client side!
import 'dart:convert';
import 'dart:core';
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:encrypt/encrypt.dart' as encrypt;
import 'package:flutter/cupertino.dart';
class ZegoTokenUtils {
static String generateToken(
int appid,
String serverSecret,
String userID, {
int effectiveTimeInSeconds = 60 * 60 * 24,
String payload = '',
}) {
debugPrint('! ${'*' * 80}');
debugPrint(
'! ** Warning: ZegoTokenUtils is only for use during testing. When your application goes live,',
);
debugPrint(
'! ** tokens must be generated by the server side. Please do not generate tokens on the client side!',
);
debugPrint('! ${'*' * 80}');
if (appid == 0) {
throw Exception('appid Invalid');
}
if (userID == '') {
throw Exception('userID Invalid');
}
if (serverSecret.length != 32) {
throw Exception('serverSecret Invalid');
}
if (effectiveTimeInSeconds <= 0) {
throw Exception('effectiveTimeInSeconds Invalid');
}
final tokenInfo = TokenInfo04(
appid: appid,
userID: userID,
nonce: math.Random().nextInt(math.pow(2, 31).toInt()),
ctime: DateTime.now().millisecondsSinceEpoch ~/ 1000,
expire: 0,
payload: payload,
);
tokenInfo.expire = tokenInfo.ctime + effectiveTimeInSeconds;
// Convert token information to json
final tokenJson = tokenInfo.toJson();
// Randomly generated 16-byte string, used as AES encryption vector,
// before the ciphertext for Base64 encoding to generate the final token
final ivStr = createRandomString(16);
final iv = encrypt.IV.fromUtf8(ivStr);
final key = encrypt.Key.fromUtf8(serverSecret);
final encrypter = encrypt.Encrypter(
encrypt.AES(key, mode: encrypt.AESMode.cbc),
);
final encrypted = encrypter.encrypt(tokenJson, iv: iv);
final bytes1 = createUint8ListFromInt(tokenInfo.expire);
final bytes2 = Uint8List.fromList([0, 16]);
final bytes3 = Uint8List.fromList(utf8.encode(ivStr));
final bytes4 = Uint8List.fromList([0, encrypted.bytes.length]);
final bytes5 = encrypted.bytes;
final bytes = Uint8List(4) + bytes1 + bytes2 + bytes3 + bytes4 + bytes5;
final ret = '04${base64.encode(bytes)}';
return ret;
}
static final _random = math.Random();
static const _defaultPool =
'ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW';
static String createRandomString(int size, {String pool = _defaultPool}) {
final len = pool.length;
var id = '';
var i = size;
while (0 < i--) {
id += pool[_random.nextInt(len)];
}
return id;
}
/// Creates a `Uint8List` by a hex string.
static Uint8List createUint8ListFromHexString(String hex) {
final result = Uint8List(hex.length ~/ 2);
for (var i = 0; i < hex.length; i += 2) {
final num = hex.substring(i, i + 2);
final byte = int.parse(num, radix: 16);
result[i ~/ 2] = byte;
}
return result;
}
/// Returns a hex string by a `Uint8List`.
static String formatBytesAsHexString(Uint8List bytes) {
final result = StringBuffer();
for (var i = 0; i < bytes.lengthInBytes; i++) {
final part = bytes[i];
result.write('${part < 16 ? '0' : ''}${part.toRadixString(16)}');
}
return result.toString();
}
static Uint8List createUint8ListFromInt(int hex) {
return createUint8ListFromHexString(hex.toRadixString(16));
}
}
class TokenInfo04 {
TokenInfo04({
required this.appid,
required this.userID,
required this.ctime,
required this.expire,
required this.nonce,
required this.payload,
});
int appid;
String userID;
int nonce;
int ctime;
int expire;
String payload;
String toJson() {
return '{"app_id":$appid,"user_id":"$userID","nonce":$nonce,"ctime":$ctime,"expire":$expire,"payload":"$payload"}';
}
}
🟣 main.dart — App Entry Point
import 'package:flutter/material.dart';
import 'login.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Live Streaming App',
debugShowCheckedModeBanner: false,
theme: ThemeData(primarySwatch: Colors.blue),
home: const LoginPage(),
);
}
}
and create the file
class KeyCenter {
static const int appID = 0;
static String appSign =
'***********';
static String serverSecret = "*************";
static String token =
"******************************************************";
}
you cade get the server Secret in the web site ZEGOCLUOD
Basic Configurations =>Temporary token => Make Generate
🟡 Login Page — Engine Initialization
On the login screen, we:
- Generate a unique User ID
- Request permissions
- Initialize ZEGOCLOUD engine
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:zego_express_engine/zego_express_engine.dart';
import 'utils/permission.dart';
import 'home_page.dart';
import 'key_center.dart';
Future<void> createEngine() async {
WidgetsFlutterBinding.ensureInitialized();
// Get your AppID and AppSign from ZEGOCLOUD Console
//[My Projects -> AppID] : https://console.zegocloud.com/project
await ZegoExpressEngine.createEngineWithProfile(
ZegoEngineProfile(
KeyCenter.appID,
ZegoScenario.Default,
appSign: null, // Force Token Auth
),
);
}
void jumpToHomePage(
BuildContext context, {
required String localUserID,
required String localUserName,
}) async {
await createEngine();
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) =>
HomePage(localUserID: localUserID, localUserName: localUserName),
),
);
}
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
/// Users who use the same roomID can join the same live streaming.
final userIDTextCtrl = TextEditingController(
text: Random().nextInt(100000).toString(),
);
final userNameTextCtrl = TextEditingController();
@override
void initState() {
super.initState();
requestPermission();
userNameTextCtrl.text = 'user_${userIDTextCtrl.text}';
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: AnnotatedRegion<SystemUiOverlayStyle>(
value: const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.light,
),
child: Container(
height: double.infinity,
width: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF2E3192), Color(0xFF1BFFFF)],
),
),
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.live_tv_rounded,
size: 100,
color: Colors.white,
),
const SizedBox(height: 20),
Text(
'Live Streaming',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 40),
Card(
elevation: 8,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
color: Colors.white.withOpacity(0.95),
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
children: [
TextFormField(
controller: userIDTextCtrl,
decoration: InputDecoration(
labelText: 'User ID',
prefixIcon: const Icon(Icons.person_outline),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
filled: true,
fillColor: Colors.grey[50],
),
),
const SizedBox(height: 20),
TextFormField(
controller: userNameTextCtrl,
decoration: InputDecoration(
labelText: 'User Name',
prefixIcon: const Icon(Icons.badge_outlined),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
filled: true,
fillColor: Colors.grey[50],
),
),
const SizedBox(height: 30),
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2E3192),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 2,
),
onPressed: () => jumpToHomePage(
context,
localUserID: userIDTextCtrl.text,
localUserName: userNameTextCtrl.text,
),
child: const Text(
'Start Streaming',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
),
const SizedBox(height: 20),
const Text(
'Please test with two or more devices',
style: TextStyle(color: Colors.white70),
),
],
),
),
),
),
),
);
}
}
📌 We intentionally force token authentication for security.
🏠 Home Page — Start or Watch Live
Users can:
- 🎥 Start Live (Host)
- 👀 Watch Live (Audience)
import 'package:flutter/material.dart';
import 'utils/permission.dart';
import 'live_page.dart';
class HomePage extends StatefulWidget {
const HomePage({
super.key,
required this.localUserID,
required this.localUserName,
});
final String localUserID;
final String localUserName;
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with TickerProviderStateMixin {
/// Users who use the same roomID can join the same live streaming.
final roomTextCtrl = TextEditingController();
@override
void initState() {
super.initState();
requestPermission();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
height: double.infinity,
width: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF2E3192), Color(0xFF1BFFFF)],
),
),
child: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(
backgroundColor: Colors.white,
radius: 30,
child: Text(
widget.localUserName.isNotEmpty
? widget.localUserName[0].toUpperCase()
: '?',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Color(0xFF2E3192),
),
),
),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Welcome back,',
style: TextStyle(color: Colors.white70, fontSize: 14),
),
Text(
widget.localUserName,
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
),
],
),
const SizedBox(height: 40),
Card(
elevation: 8,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
color: Colors.white.withOpacity(0.95),
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Join a Room',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 16),
TextFormField(
controller: roomTextCtrl,
decoration: InputDecoration(
labelText: 'Room ID',
prefixIcon: const Icon(Icons.meeting_room),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
filled: true,
fillColor: Colors.grey[50],
),
),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2E3192),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
vertical: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
onPressed: () => jumpToLivePage(
context,
isHost: true,
localUserID: widget.localUserID,
localUserName: widget.localUserName,
roomID: roomTextCtrl.text,
),
icon: const Icon(Icons.videocam),
label: const Text('Start Live'),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: const Color(0xFF2E3192),
padding: const EdgeInsets.symmetric(
vertical: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: const BorderSide(
color: Color(0xFF2E3192),
),
),
),
onPressed: () => jumpToLivePage(
context,
isHost: false,
localUserID: widget.localUserID,
localUserName: widget.localUserName,
roomID: roomTextCtrl.text,
),
icon: const Icon(Icons.remove_red_eye),
label: const Text('Watch Live'),
),
),
],
),
],
),
),
),
const SizedBox(height: 20),
Center(
child: Text(
'Your ID: ${widget.localUserID}',
style: const TextStyle(color: Colors.white54),
),
),
],
),
),
),
),
);
}
void jumpToLivePage(
BuildContext context, {
required String roomID,
required bool isHost,
required String localUserID,
required String localUserName,
}) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) {
return LivePage(
isHost: isHost,
localUserID: localUserID,
localUserName: localUserName,
roomID: roomID,
);
},
),
);
}
}
📸 Home page screenshot
🔴 Live Page — Core Streaming Logic
Login to Room with Token
import 'package:flutter/material.dart';
import 'package:zego_express_engine/zego_express_engine.dart';
import 'key_center.dart';
import 'utils/zegocloud_token.dart';
class LivePage extends StatefulWidget {
const LivePage({
super.key,
required this.isHost,
required this.localUserID,
required this.localUserName,
required this.roomID,
});
final bool isHost;
final String localUserID;
final String localUserName;
final String roomID;
@override
State<LivePage> createState() => _LivePageState();
}
class _LivePageState extends State<LivePage> {
Widget? localView;
int? localViewID;
Widget? remoteView;
int? remoteViewID;
@override
void initState() {
startListenEvent();
loginRoom();
super.initState();
}
@override
void dispose() {
stopListenEvent();
logoutRoom();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
title: const Text("Live Streaming"),
backgroundColor: Colors.transparent,
elevation: 0,
centerTitle: true,
),
body: Container(
height: double.infinity,
width: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF2E3192), Color(0xFF1BFFFF)],
),
),
child: Stack(
children: [
// Video views
Positioned.fill(
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Colors.black26,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 15,
spreadRadius: 5,
),
],
),
clipBehavior: Clip.antiAlias,
child:
(widget.isHost ? localView : remoteView) ??
const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: Colors.white,
),
SizedBox(height: 20),
Text(
'Waiting for stream...',
style: TextStyle(color: Colors.white70),
),
],
),
),
),
),
],
),
),
),
),
// UI Overlays
Positioned(
top: 100,
left: 24,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
decoration: BoxDecoration(
color: Colors.black45,
borderRadius: BorderRadius.circular(25),
),
child: Row(
children: [
const CircleAvatar(
radius: 12,
backgroundColor: Colors.redAccent,
child: Icon(Icons.live_tv, size: 14, color: Colors.white),
),
const SizedBox(width: 8),
Text(
'Room: ${widget.roomID}',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
),
),
),
Positioned(
bottom: 50,
left: 0,
right: 0,
child: Center(
child: ElevatedButton.icon(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.redAccent,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 40,
vertical: 15,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(30),
),
elevation: 5,
),
icon: Icon(
widget.isHost ? Icons.stop_circle : Icons.exit_to_app,
),
label: Text(
widget.isHost ? 'End Live' : 'Leave Live',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
),
),
);
}
Future<ZegoRoomLoginResult> loginRoom() async {
debugPrint('--- loginRoom Debug ---');
debugPrint('AppID: ${KeyCenter.appID}');
debugPrint('RoomID: |${widget.roomID}|');
debugPrint('UserID: |${widget.localUserID}|');
debugPrint('UserName: |${widget.localUserName}|');
// The value of `userID` is generated locally and must be globally unique.
final user = ZegoUser(widget.localUserID, widget.localUserName);
// The value of `roomID` is generated locally and must be globally unique.
final roomID = widget.roomID;
// onRoomUserUpdate callback can be received when "isUserStatusNotify" parameter value is "true".
ZegoRoomConfig roomConfig = ZegoRoomConfig.defaultConfig()
..isUserStatusNotify = true;
roomConfig.token = ZegoTokenUtils.generateToken(
KeyCenter.appID,
KeyCenter.serverSecret,
widget.localUserID,
);
debugPrint('Generated Token Length: ${roomConfig.token?.length}');
// log in to a room
return ZegoExpressEngine.instance
.loginRoom(roomID, user, config: roomConfig)
.then((ZegoRoomLoginResult loginRoomResult) {
debugPrint(
'loginRoom Result - errorCode:${loginRoomResult.errorCode}',
);
if (loginRoomResult.errorCode == 0) {
if (widget.isHost) {
startPreview();
startPublish();
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Error ${loginRoomResult.errorCode}: Check Room/User ID',
),
backgroundColor: Colors.redAccent,
),
);
}
return loginRoomResult;
});
}
Future<ZegoRoomLogoutResult> logoutRoom() async {
stopPreview();
stopPublish();
return ZegoExpressEngine.instance.logoutRoom(widget.roomID);
}
void startListenEvent() {
// Callback for updates on the status of other users in the room.
// Users can only receive callbacks when the isUserStatusNotify property of ZegoRoomConfig is set to `true` when logging in to the room (loginRoom).
ZegoExpressEngine
.onRoomUserUpdate = (roomID, updateType, List<ZegoUser> userList) {
debugPrint(
'onRoomUserUpdate: roomID: $roomID, updateType: ${updateType.name}, userList: ${userList.map((e) => e.userID)}',
);
};
// Callback for updates on the status of the streams in the room.
ZegoExpressEngine.onRoomStreamUpdate =
(roomID, updateType, List<ZegoStream> streamList, extendedData) {
debugPrint(
'onRoomStreamUpdate: roomID: $roomID, updateType: $updateType, streamList: ${streamList.map((e) => e.streamID)}, extendedData: $extendedData',
);
if (updateType == ZegoUpdateType.Add) {
for (final stream in streamList) {
startPlayStream(stream.streamID);
}
} else {
for (final stream in streamList) {
stopPlayStream(stream.streamID);
}
}
};
// Callback for updates on the current user's room connection status.
ZegoExpressEngine
.onRoomStateUpdate = (roomID, state, errorCode, extendedData) {
debugPrint(
'onRoomStateUpdate: roomID: $roomID, state: ${state.name}, errorCode: $errorCode, extendedData: $extendedData',
);
};
// Callback for updates on the current user's stream publishing changes.
ZegoExpressEngine
.onPublisherStateUpdate = (streamID, state, errorCode, extendedData) {
debugPrint(
'onPublisherStateUpdate: streamID: $streamID, state: ${state.name}, errorCode: $errorCode, extendedData: $extendedData',
);
};
}
void stopListenEvent() {
ZegoExpressEngine.onRoomUserUpdate = null;
ZegoExpressEngine.onRoomStreamUpdate = null;
ZegoExpressEngine.onRoomStateUpdate = null;
ZegoExpressEngine.onPublisherStateUpdate = null;
}
Future<void> startPreview() async {
await ZegoExpressEngine.instance
.createCanvasView((viewID) {
localViewID = viewID;
ZegoCanvas previewCanvas = ZegoCanvas(
viewID,
viewMode: ZegoViewMode.AspectFill,
);
ZegoExpressEngine.instance.startPreview(canvas: previewCanvas);
})
.then((canvasViewWidget) {
setState(() => localView = canvasViewWidget);
});
}
Future<void> stopPreview() async {
ZegoExpressEngine.instance.stopPreview();
if (localViewID != null) {
await ZegoExpressEngine.instance.destroyCanvasView(localViewID!);
setState(() {
localViewID = null;
localView = null;
});
}
}
Future<void> startPublish() async {
// After calling the `loginRoom` method, call this method to publish streams.
// The StreamID must be unique in the room.
String streamID = '${widget.roomID}_${widget.localUserID}_call';
return ZegoExpressEngine.instance.startPublishingStream(streamID);
}
Future<void> stopPublish() async {
return ZegoExpressEngine.instance.stopPublishingStream();
}
Future<void> startPlayStream(String streamID) async {
// Start to play streams. Set the view for rendering the remote streams.
await ZegoExpressEngine.instance
.createCanvasView((viewID) {
remoteViewID = viewID;
ZegoCanvas canvas = ZegoCanvas(
viewID,
viewMode: ZegoViewMode.AspectFill,
);
ZegoPlayerConfig config = ZegoPlayerConfig.defaultConfig();
config.resourceMode =
ZegoStreamResourceMode.Default; // live streaming
// config.resourceMode = ZegoStreamResourceMode.OnlyL3; // interactive live streaming
ZegoExpressEngine.instance.startPlayingStream(
streamID,
canvas: canvas,
config: config,
);
})
.then((canvasViewWidget) {
setState(() => remoteView = canvasViewWidget);
});
}
Future<void> stopPlayStream(String streamID) async {
ZegoExpressEngine.instance.stopPlayingStream(streamID);
if (remoteViewID != null) {
ZegoExpressEngine.instance.destroyCanvasView(remoteViewID!);
setState(() {
remoteViewID = null;
remoteView = null;
});
}
}
}
📸 Screenshots
Below are some screenshots from the live streaming app showing different stages of the flow — from joining a room to watching a live stream in real time.
🧾 Join Room Screen
User enters the Room ID and chooses whether to start or watch a live stream.
🎥 Live Streaming (Host View)
The broadcaster starts the live stream and publishes video in real time.
👀 Live Streaming (Audience View)
Audience joins the same room and watches the live stream instantly.
- All screenshots are captured from real devices
- Tested with two different users in the same room
- Streaming works instantly without any backend setup
⚠️ Important
n here is for testing only.
In production, tokens must be generated server-side.
- Opens camera
- Starts publishing live stream
This is what enables real-time updates, exactly like chat & video call apps.
🧠 Why This Architecture Works
✅ No WebRTC setup
✅ No signaling server
✅ Ultra-low latency
✅ Flutter-first SDK
✅ Production-ready
Perfect for:
- Chat apps with live rooms
- Social live streaming
- Online classes
- Events & webinars
🎉 Final Result
You’ve built a real-time Flutter Live Streaming app using ZEGOCLOUD that:
✔ Works like chat & video call apps
✔ Supports host & audience roles
✔ Uses secure token-based auth
✔ Requires no backend
🚀 Run it on two devices:
- One starts the live stream
- The other watches instantly
🔁 What’s Next?
You can extend this app by adding:
✨ Live chat inside the stream
✨ Multiple broadcasters
✨ Gifts & reactions
✨ Stream recording
✨ Firebase authentication
🏁 Conclusion
You’ve just built a real-time live streaming app in Flutter without touching media servers, WebRTC, or low-level streaming logic.
Thanks to ZEGOCLOUD, the hard parts are already solved — so you can focus on building features users actually care about.
🚀 Happy coding!









Top comments (0)