# Build a Flutter video chat app with Zoom Video SDK [Zoom Video SDK](/docs/video-sdk/) lets you add real-time audio and video to your application easily, backed by the powerful Zoom technology users love. In this guide, we'll explore using the Video SDK to create a video chat application with Flutter, an open source framework that simplifies building natively compiled multi-platform applications from a single codebase. You can find the code for this blog on [GitHub](http://github.com/zoom/VideoSDK-flutter-quickstart). ![two screenshots: one screen to start a session and another screen with two users talking to each other](/img/blog/ekaansharora/flutter-quickstart/collage.png) Prerequisites: - [Flutter development environment](https://docs.flutter.dev/get-started/install) set up - A Zoom [Video SDK Account](/docs/video-sdk/get-credentials/) ## Step 1: Create a new Flutter project To start, we'll create a new Flutter project. Open a terminal and execute: ```bash $ flutter create zoom_flutter_hello_world $ cd zoom_flutter_hello_world ``` This will create a new Flutter project with the following structure: ```bash . ├── android ├── ios ├── lib │ └── main.dart ├── pubspec.yaml ├── test └── web ``` ## Step 2: Configuring the project **Install the dependencies** We will install the Zoom Video SDK for Flutter and the JWT package. Add the following to your `pubspec.yaml` file: ```yaml dependencies: flutter: sdk: flutter flutter_zoom_videosdk: ^1.7.0 dart_jsonwebtoken: ^2.12.1 ``` Run `flutter pub get` to install the dependencies. **Add permissions** For iOS, add the following to your `ios/Runner/Info.plist` file: ```xml NSCameraUsageDescription For video conferencing NSMicrophoneUsageDescription For audio conferencing ``` **Config file** Let's create a `config.dart` file in the `lib` directory to store our Zoom SDK credentials and session details: ```dart const Map configs = { 'ZOOM_SDK_KEY': 'YOUR_ZOOM_SDK_KEY', 'ZOOM_SDK_SECRET': 'YOUR_ZOOM_SDK_SECRET', }; const Map sessionDetails = { 'sessionName': 'TestOne', 'sessionPassword': '', 'displayName': 'Flutter', 'sessionTimeout': '40', 'roleType': '1', }; ``` > **Disclaimer**: _To demo the app, we'll be using the Zoom Video SDK credentials to generate a JWT from within the application. In production use-cases do not store your app key and secret inside your app. This is not a safe operation, instead use a server to generate the tokens and request them in your app instead._ ## Step 3: Setting up the main app Let's update the `main.dart` file to initialize the Zoom Video SDK. We'll start by importing the necessary packages and creating the `Videochat` widget: ```dart import 'package:flutter/material.dart'; import 'package:flutter_zoom_videosdk/native/zoom_videosdk.dart'; import 'package:zoom_flutter_hello_world/videochat.dart'; class ZoomVideoSdkProvider extends StatelessWidget { const ZoomVideoSdkProvider({super.key}); @override Widget build(BuildContext context) { var zoom = ZoomVideoSdk(); InitConfig initConfig = InitConfig( domain: "zoom.us", enableLog: true, ); zoom.initSdk(initConfig); return const Videochat(); } } ``` We'll initialize the Zoom Video SDK in the `ZoomVideoSdkProvider` widget and return the `Videochat` widget. We'll create the `Videochat` widget next. Let's update the `main` function to use the `ZoomVideoSdkProvider` widget: ```dart void main() { runApp( MaterialApp( title: 'Zoom Flutter Hello World', home: const SafeArea( child: ZoomVideoSdkProvider(), ), ), ); } ``` ## Step 4: Building the video chat component Now, let's create a new file `lib/videochat.dart` that will contain the video chat component. We'll add the imports for the different packages we'll be using and defined the `Videochat` widget that will render the video chat UI: ```dart import 'dart:async'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:zoom_flutter_hello_world/config.dart'; import 'package:zoom_flutter_hello_world/utils/jwt.dart'; import 'package:flutter_zoom_videosdk/native/zoom_videosdk.dart'; import 'package:flutter_zoom_videosdk/native/zoom_videosdk_user.dart'; import 'package:flutter_zoom_videosdk/native/zoom_videosdk_event_listener.dart'; import 'package:flutter_zoom_videosdk/flutter_zoom_view.dart' as zoom_view; class Videochat extends StatefulWidget { const Videochat({super.key}); @override State createState() => _VideochatState(); } ``` The `_VideochatState` class will contain the video chat logic: ```dart class _VideochatState extends State { final zoom = ZoomVideoSdk(); final eventListener = ZoomVideoSdkEventListener(); bool isInSession = false; List subscriptions = []; List users = []; bool isMuted = true; bool isVideoOn = false; bool isLoading = false; ``` We define the state variables for our video chat component: `zoom` and `eventListener` to access the Zoom Video SDK and set up event listeners. `subscriptions` to store the list of subscriptions, `isInSession` for session state, `users` to store the list of users in the session, `isMuted` for user mic state, `isVideoOn` for user camera state, and `isLoading` to show a loading state when joining a session. ### Event Listeners The Zoom Video SDK gives you access to events for various actions, like when a user joins a session, leaves a session, etc. We'll set up event listeners to handle events and manage the app state: ```dart _handleSessionJoin(data) async { if (!mounted) return; final mySelf = ZoomVideoSdkUser.fromJson(jsonDecode(data['sessionUser'])); final remoteUsers = await zoom.session.getRemoteUsers() ?? []; final isMutedState = await mySelf.audioStatus?.isMuted() ?? true; final isVideoOnState = await mySelf.videoStatus?.isOn() ?? false; setState(() { isInSession = true; isLoading = false; isMuted = isMutedState; isVideoOn = isVideoOnState; users = [mySelf, ...remoteUsers]; }); } ``` The `_handleSessionJoin` method updates the state when a session is joined. It also updates the user list, adding the self object and the remote users to the list. It also updates the video and audio states based on the user's audio and video status. ```dart _updateUserList(data) async { final mySelf = await zoom.session.getMySelf(); if (mySelf == null) return; final remoteUserList = await zoom.session.getRemoteUsers() ?? []; remoteUserList.insert(0, mySelf); setState(() { users = remoteUserList; }); } ``` The `_updateUserList` method updates the user list when remote users join or leave the session. ```dart _handleVideoChange(data) async { if (!mounted) return; final mySelf = await zoom.session.getMySelf(); final videoStatus = await mySelf?.videoStatus?.isOn() ?? false; setState(() { isVideoOn = videoStatus; }); } ``` The `_handleVideoChange` method updates the local user's camera status when it changes. ```dart _handleAudioChange(data) async { if (!mounted) return; final mySelf = await zoom.session.getMySelf(); final audioStatus = await mySelf?.audioStatus?.isMuted() ?? true; setState(() { isMuted = audioStatus; }); } ``` The `_handleAudioChange` method updates the local user's mic status when it changes. Finally, we'll create a function `_setupEventListeners` to attach the event listeners and add them to the `subscriptions` list: ```dart _setupEventListeners() { subscriptions = [ eventListener.addListener(EventType.onSessionJoin, _handleSessionJoin), eventListener.addListener(EventType.onSessionLeave, handleLeaveSession), eventListener.addListener(EventType.onUserJoin, _updateUserList), eventListener.addListener(EventType.onUserLeave, _updateUserList), eventListener.addListener(EventType.onUserVideoStatusChanged, _handleVideoChange), eventListener.addListener(EventType.onUserAudioStatusChanged, _handleAudioChange), ]; } ``` We can use the `subscriptions` list to cancel the event listeners as part of the cleanup process. ### Session Management Now, let's define methods to start and leave a session: ```dart Future startSession() async { setState(() => isLoading = true); try { final token = generateJwt(sessionDetails['sessionName'], sessionDetails['roleType']); _setupEventListeners(); await zoom.joinSession(JoinSessionConfig( sessionName: sessionDetails['sessionName']!, sessionPassword: sessionDetails['sessionPassword']!, token: token, userName: sessionDetails['displayName']!, audioOptions: {"connect": true, "mute": true}, videoOptions: {"localVideoOn": true}, sessionIdleTimeoutMins: int.parse(sessionDetails['sessionTimeout']!), )); } catch (e) { debugPrint("Error: $e"); setState(() => isLoading = false); } } ``` The `startSession` method generates a JWT token and joins a video session using the `zoom.joinSession` method, passing in the `JoinSessionConfig` object. We can access the `sessionDetails` from the `config.dart` file. Before joining the session, we set up the event listeners by calling the `_setupEventListeners` method. We'll define a callback function `handleLeaveSession` to reset the app state when the user leaves a session and cancel all the event subscriptions: ```dart handleLeaveSession([data]) { setState(() { isInSession = false; isLoading = false; users = []; }); for (var subscription in subscriptions) { subscription.cancel(); } } ``` We'll call this function when the user clicks the leave button. ## Step 5: Building the UI Now, let's build the UI for our video chat screen: ```dart @override Widget build(BuildContext context) { return SafeArea( child: Scaffold( backgroundColor: Colors.grey[900], body: Stack( children: [ if (!isInSession) Center( child: ElevatedButton( onPressed: isLoading ? null : startSession, child: Text(isLoading ? 'Connecting...' : 'Start Session'), ), ) else Stack( children: [ VideoGrid(users: users), ControlBar( isMuted: isMuted, isVideoOn: isVideoOn, onLeaveSession: handleLeaveSession, ), ], ), ], ), ), ); } } ``` We display a button to start the session when the user is not in a session. When the user is in a session, we display a grid of video tiles using the `VideoGrid` component passing in the `users` list. We also display a bar at the bottom of the screen using the `ControlBar` component passing in the `isMuted` & `isVideoOn` states for the mute/unmute audio and video buttons. We also pass in the `handleLeaveSession` method to the `onLeaveSession` prop that we'll call when the user clicks the leave button. ![screenshot of the app showing a button](/img/blog/ekaansharora/flutter-quickstart/btn.png) ### Video Grid Component ![screenshot of the app showing a grid of user videos](/img/blog/ekaansharora/flutter-quickstart/grid.png) Let's create a component to display the video grid: ```dart class VideoGrid extends StatelessWidget { final List users; const VideoGrid({ super.key, required this.users, }); @override Widget build(BuildContext context) { if (users.isEmpty) { return const Center(child: CircularProgressIndicator()); } return GridView.builder( physics: const NeverScrollableScrollPhysics(), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: users.length <= 2 ? 1 : 2, crossAxisSpacing: 2, mainAxisSpacing: 2, ), itemCount: users.length, itemBuilder: (context, index) => _VideoTile(user: users[index]), ); } } ``` The `VideoGrid` component displays a loading indicator when there are no users, and a grid of video tiles when there are users using the `GridView` widget adjusting the size based on the number of users. ### Video Tile Component Now, let's create a component to display individual video tiles: ```dart class _VideoTile extends StatelessWidget { final ZoomVideoSdkUser user; const _VideoTile({required this.user}); @override Widget build(BuildContext context) { return Material( color: Colors.black, child: SizedBox.expand( child: zoom_view.View( key: Key(user.userId), creationParams: { "userId": user.userId, "videoAspect": VideoAspect.FullFilled, "fullScreen": false, }, ), ), ); } } ``` The `_VideoTile` component displays a video tile for a user using the `zoom_view.View` widget. We pass the `user.userId` to the creation params to identify the user. ## Step 6: Building the control bar ![screenshot of the app showing a container with three buttons: mic mute, camera mute and endcall](/img/blog/ekaansharora/flutter-quickstart/control.png) Let's create the final piece of UI, the `ControlBar` component. This component will display buttons to mute/unmute the mic and camera along with a button to leave the session: ```dart class ControlBar extends StatelessWidget { final bool isMuted; final bool isVideoOn; final double circleButtonSize = 40.0; final zoom = ZoomVideoSdk(); final VoidCallback onLeaveSession; ControlBar({ super.key, required this.isMuted, required this.isVideoOn, required this.onLeaveSession, }); ``` The `ControlBar` component uses the `isMuted` and `isVideoOn` variables to track the mute/unmute state of the mic and camera. We also access the `onLeaveSession` callback function that we defined in the `Videochat` component for performing the cleanup after leaving the session. We'll define the `toggleAudio` function to toggle the audio status when the user clicks the mute/unmute audio button: ```dart Future toggleAudio() async { final mySelf = await zoom.session.getMySelf(); if (mySelf?.audioStatus == null) return; final isMuted = await mySelf!.audioStatus!.isMuted(); isMuted ? await zoom.audioHelper.unMuteAudio(mySelf.userId) : await zoom.audioHelper.muteAudio(mySelf.userId); } ``` We use the `muteAudio` and `unMuteAudio` methods on the `audioHelper` object to mute and unmute the audio. We'll define the `toggleVideo` function to toggle the video status when the user clicks the mute/unmute video button: ```dart Future toggleVideo() async { final mySelf = await zoom.session.getMySelf(); if (mySelf?.videoStatus == null) return; final isOn = await mySelf!.videoStatus!.isOn(); isOn ? await zoom.videoHelper.stopVideo() : await zoom.videoHelper.startVideo(); } ``` We use the `stopVideo` and `startVideo` methods on the `videoHelper` object to stop and start the video. Finally, we'll define the `leaveSession` function to leave the session when the user clicks the leave button: ```dart Future leaveSession() async { await zoom.leaveSession(false); onLeaveSession(); } ``` We use the `leaveSession` method to leave the session and call the `onLeaveSession` function to call the `handleLeaveSession` method to reset the app state. We can now build the UI for the `ControlBar` component: ```dart @override Widget build(BuildContext context) { return Align( alignment: Alignment.bottomCenter, child: FractionallySizedBox( child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( onPressed: toggleAudio, icon: Icon( isMuted ? Icons.mic_off : Icons.mic, ), iconSize: circleButtonSize, tooltip: isMuted ? "Unmute" : "Mute", color: Colors.white, ), IconButton( onPressed: toggleVideo, iconSize: circleButtonSize, icon: Icon( isVideoOn ? Icons.videocam : Icons.videocam_off, color: Colors.white, ), ), IconButton( onPressed: leaveSession, iconSize: circleButtonSize, icon: const Icon(Icons.call_end, color: Colors.red), ), ], ), ), ); } } ``` We use `IconButton` to create the mute/unmute audio and video buttons and the leave button. We pass in the `toggleAudio` and `toggleVideo` methods to the `onPressed` prop and the `leaveSession` method to the `onPressed` prop of the leave button. ## Step 7: Running the app You can run the app using the following command: ```bash flutter run ``` This will start the Flutter development server and open the app on your connected device or emulator. > Note: Emulators may not be able to access the camera, you'll see an empty video view. Please use a physical device to demo the video functionality. ## Conclusion Congratulations! You've successfully integrated video and audio conferencing into your Flutter application using the Zoom Video SDK. This is just the beginning of what you can do with the Video SDK! You can add other features like screen sharing, chat, cloud recording, and live transcriptions. More information is available under the _Add Features_ section in the [Zoom Video SDK for Flutter](/docs/video-sdk/flutter/) documentation.