DEV Community

Hazem Hamdy
Hazem Hamdy

Posted on

Build a Flutter Live Streaming App Using ZEGOCLOUD (Like Chat & Video Call Apps)

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


🧭 App Flow Overview

The flow is intentionally designed to be similar to chat & video call apps:

  1. User opens the app
  2. Enters a Room ID
  3. Chooses:
  • 🎥 Start Live (Broadcaster)
  • 👀 Watch Live (Audience)
    1. 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
Enter fullscreen mode Exit fullscreen mode

📸 Project structure screenshot


🧩 Step 1 — Install Required Packages

Open pubspec.yaml and add:

pubspec.yaml

Then run:

flutter pub get
Enter fullscreen mode Exit fullscreen mode

🔐 Step 2 — Android Permissions Setup

Live streaming requires camera, microphone, and network access.

Add the following permissions to:

android/app/src/main/AndroidManifest.xml
Enter fullscreen mode Exit fullscreen mode

Android permissions


⚙️ Step 3 — Android Build Configuration

Make sure your compileSdk, ndkVersion, and Java version are configured correctly:

Build config


🛡 Step 4 — Proguard Configuration (Release Mode)

Enable Proguard in build.gradle:

Proguard enable

Create or edit:

android/app/proguard-rules.pro
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}

Enter fullscreen mode Exit fullscreen mode

📸 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();
}
Enter fullscreen mode Exit fullscreen mode

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"}';
  }
}

Enter fullscreen mode Exit fullscreen mode

🟣 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(),
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

and create the file

class KeyCenter {
  static const int appID = 0;
  static String appSign =
      '***********';
  static String serverSecret = "*************";
  static String token =
      "******************************************************";
}


Enter fullscreen mode Exit fullscreen mode

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),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

📌 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,
          );
        },
      ),
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

📸 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;
      });
    }
  }
}


Enter fullscreen mode Exit fullscreen mode

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

Join Room Screen


🎥 Live Streaming (Host View)

The broadcaster starts the live stream and publishes video in real time.

Live Streaming Host


👀 Live Streaming (Audience View)

Audience joins the same room and watches the live stream instantly.

Live Streaming Audience


  • 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)