Audio Room QuickStart Guide

Overview

This guide will walk you through simple instructions to create a audio conferencing app using 100ms Flutter SDK and test it using an emulator or your mobile phone.

Please check our basic concepts guide to understand the concepts like rooms, templates, peers, etc.

This guide contains instructions for two approaches to get you started with 100ms Flutter SDK:

  1. Create a sample app — instructions to create a flutter app quickly with a complete code sample.
  2. Building step-by-step — instructions to walk you through the implementation of the app in a step-by-step manner.

You can also check our basic sample app on GitHub.

Check out the full-fledged example app implementation in the 100ms Flutter SDK GitHub repository showcasing multiple features provided by 100ms. This uses the provider package as the state management library.

We also have other sample apps built using other popular state management libraries :

Create a sample app

This section contains instructions to create a simple Flutter video conferencing app. We will help you with instructions to understand the project setup and complete code sample to implement this quickly.

Prerequisites

To complete this implementation for the Android platform, you must have the following:

Create a Flutter app

Once you have the prerequisites, follow the steps below to create a Flutter app. This guide will use VS code, but you can use any IDE that supports Flutter.

  • Create a Flutter app using the terminal; you can get the Flutter SDK and use the below command:

    flutter create my_app
  • Once the app is created, open it in VS code.

Add 100ms SDK to your project

Once you have created a Flutter app, you must add the 100ms Flutter SDK and permission_handler package (to handle audio/video permissions from microphone and camera) to your app.

For creating audio room we will use the audio room template from dashboard just like below.

  • Add the below snippet to the pubspec.yaml.
# 100ms SDK and permissions_handler hmssdk_flutter: permission_handler:
  • Run flutter pub get to download these dependencies to your app.

Add permissions

Please follow the below instructions to test the app for the android target platform:

  1. Allow recording audio and internet permissions by adding the below snippet to the AndroidManifest.xml file (at the application tag level).


    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/> <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.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
  2. Add minimum SDK version (minSdkVersion 21) in "android/app/build.gradle" file (inside "defaultConfig").

... defaultConfig { ... minSdkVersion 21 ... } ...

You will also need to request camera and record audio permissions at runtime before you join a call or display a preview. Please follow the Android Documentation for runtime permissions.

Complete code example

After running the below code your application will look similar to this:

audio-room-video

import 'dart:developer'; import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:hmssdk_flutter/hmssdk_flutter.dart'; import 'package:permission_handler/permission_handler.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(title: '100ms Audio Room Guide'), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key, required this.title}); final String title; State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { bool res = false; static Future<bool> getPermissions() async { if (Platform.isIOS) return true; await Permission.microphone.request(); await Permission.bluetoothConnect.request(); while ((await Permission.microphone.isDenied)) { await Permission.microphone.request(); } while ((await Permission.bluetoothConnect.isDenied)) { await Permission.bluetoothConnect.request(); } return true; } Widget build(BuildContext context) { return Scaffold( body: Container( color: Colors.black, child: Center( child: ElevatedButton( style: ButtonStyle( shape: MaterialStateProperty.all<RoundedRectangleBorder>( RoundedRectangleBorder( borderRadius: BorderRadius.circular(8.0), ))), onPressed: () async => { res = await getPermissions(), if (res) Navigator.push(context, CupertinoPageRoute(builder: (_) => const MeetingPage())) }, child: const Padding( padding: EdgeInsets.symmetric(vertical: 20, horizontal: 15), child: Text( 'Join Audio Room', style: TextStyle(fontSize: 20), ), ), ), ), ), ); } } class MeetingPage extends StatefulWidget { const MeetingPage({super.key}); State<MeetingPage> createState() => _MeetingPageState(); } class _MeetingPageState extends State<MeetingPage> implements HMSUpdateListener, HMSActionResultListener { late HMSSDK _hmsSDK; //Enter the username and authToken from dashboard for the corresponding role here. String userName = "Enter Username Here"; String authToken = "Enter AuthToken Here"; Offset position = const Offset(5, 5); bool isJoinSuccessful = false; final List<PeerTrackNode> _listeners = []; final List<PeerTrackNode> _speakers = []; bool _isMicrophoneMuted = false; HMSPeer? _localPeer; void initState() { super.initState(); initHMSSDK(); } //To know more about HMSSDK setup and initialization checkout the docs here: https://www.100ms.live/docs/flutter/v2/how--to-guides/install-the-sdk/hmssdk void initHMSSDK() async { _hmsSDK = HMSSDK(); await _hmsSDK.build(); _hmsSDK.addUpdateListener(listener: this); _hmsSDK.join(config: HMSConfig(authToken: authToken, userName: userName)); } void dispose() { //We are clearing the room state here _speakers.clear(); _listeners.clear(); super.dispose(); } //Here we will be getting updates about peer join, leave, role changed, name changed etc. void onPeerUpdate({required HMSPeer peer, required HMSPeerUpdate update}) { if (peer.isLocal) { _localPeer = peer; } switch (update) { case HMSPeerUpdate.peerJoined: switch (peer.role.name) { case "speaker": int index = _speakers .indexWhere((node) => node.uid == "${peer.peerId}speaker"); if (index != -1) { _speakers[index].peer = peer; } else { _speakers.add(PeerTrackNode( uid: "${peer.peerId}speaker", peer: peer, )); } setState(() {}); break; case "listener": int index = _listeners .indexWhere((node) => node.uid == "${peer.peerId}listener"); if (index != -1) { _listeners[index].peer = peer; } else { _listeners.add( PeerTrackNode(uid: "${peer.peerId}listener", peer: peer)); } setState(() {}); break; default: //Handle the case if you have other roles in the room break; } break; case HMSPeerUpdate.peerLeft: switch (peer.role.name) { case "speaker": int index = _speakers .indexWhere((node) => node.uid == "${peer.peerId}speaker"); if (index != -1) { _speakers.removeAt(index); } setState(() {}); break; case "listener": int index = _listeners .indexWhere((node) => node.uid == "${peer.peerId}listener"); if (index != -1) { _listeners.removeAt(index); } setState(() {}); break; default: //Handle the case if you have other roles in the room break; } break; case HMSPeerUpdate.roleUpdated: if (peer.role.name == "speaker") { //This means previously the user must be a listener earlier in our case //So we remove the peer from listener and add it to speaker list int index = _listeners .indexWhere((node) => node.uid == "${peer.peerId}listener"); if (index != -1) { _listeners.removeAt(index); } _speakers.add(PeerTrackNode( uid: "${peer.peerId}speaker", peer: peer, )); if (peer.isLocal) { _isMicrophoneMuted = peer.audioTrack?.isMute ?? true; } setState(() {}); } else if (peer.role.name == "listener") { //This means previously the user must be a speaker earlier in our case //So we remove the peer from speaker and add it to listener list int index = _speakers .indexWhere((node) => node.uid == "${peer.peerId}speaker"); if (index != -1) { _speakers.removeAt(index); } _listeners.add(PeerTrackNode( uid: "${peer.peerId}listener", peer: peer, )); setState(() {}); } break; case HMSPeerUpdate.metadataChanged: switch (peer.role.name) { case "speaker": int index = _speakers .indexWhere((node) => node.uid == "${peer.peerId}speaker"); if (index != -1) { _speakers[index].peer = peer; } setState(() {}); break; case "listener": int index = _listeners .indexWhere((node) => node.uid == "${peer.peerId}listener"); if (index != -1) { _listeners[index].peer = peer; } setState(() {}); break; default: //Handle the case if you have other roles in the room break; } break; case HMSPeerUpdate.nameChanged: switch (peer.role.name) { case "speaker": int index = _speakers .indexWhere((node) => node.uid == "${peer.peerId}speaker"); if (index != -1) { _speakers[index].peer = peer; } setState(() {}); break; case "listener": int index = _listeners .indexWhere((node) => node.uid == "${peer.peerId}listener"); if (index != -1) { _listeners[index].peer = peer; } setState(() {}); break; default: //Handle the case if you have other roles in the room break; } break; case HMSPeerUpdate.defaultUpdate: // TODO: Handle this case. break; case HMSPeerUpdate.networkQualityUpdated: // TODO: Handle this case. break; } } void onTrackUpdate( {required HMSTrack track, required HMSTrackUpdate trackUpdate, required HMSPeer peer}) { switch (peer.role.name) { case "speaker": int index = _speakers.indexWhere((node) => node.uid == "${peer.peerId}speaker"); if (index != -1) { _speakers[index].audioTrack = track; } else { _speakers.add(PeerTrackNode( uid: "${peer.peerId}speaker", peer: peer, audioTrack: track)); } if (peer.isLocal) { _isMicrophoneMuted = track.isMute; } setState(() {}); break; case "listener": int index = _listeners .indexWhere((node) => node.uid == "${peer.peerId}listener"); if (index != -1) { _listeners[index].audioTrack = track; } else { _listeners.add(PeerTrackNode( uid: "${peer.peerId}listener", peer: peer, audioTrack: track)); } setState(() {}); break; default: //Handle the case if you have other roles in the room break; } } void onJoin({required HMSRoom room}) { //Checkout the docs about handling onJoin here: https://www.100ms.live/docs/flutter/v2/how--to-guides/set-up-video-conferencing/join#join-a-room room.peers?.forEach((peer) { if (peer.isLocal) { _localPeer = peer; switch (peer.role.name) { case "speaker": int index = _speakers .indexWhere((node) => node.uid == "${peer.peerId}speaker"); if (index != -1) { _speakers[index].peer = peer; } else { _speakers.add(PeerTrackNode( uid: "${peer.peerId}speaker", peer: peer, )); } setState(() {}); break; case "listener": int index = _listeners .indexWhere((node) => node.uid == "${peer.peerId}listener"); if (index != -1) { _listeners[index].peer = peer; } else { _listeners.add( PeerTrackNode(uid: "${peer.peerId}listener", peer: peer)); } setState(() {}); break; default: //Handle the case if you have other roles in the room break; } } }); } void onAudioDeviceChanged( {HMSAudioDevice? currentAudioDevice, List<HMSAudioDevice>? availableAudioDevice}) { // Checkout the docs about handling onAudioDeviceChanged updates here: https://www.100ms.live/docs/flutter/v2/how--to-guides/listen-to-room-updates/update-listeners } void onChangeTrackStateRequest( {required HMSTrackChangeRequest hmsTrackChangeRequest}) { // Checkout the docs for handling the unmute request here: https://www.100ms.live/docs/flutter/v2/how--to-guides/interact-with-room/track/remote-mute-unmute } void onHMSError({required HMSException error}) { // To know more about handling errors please checkout the docs here: https://www.100ms.live/docs/flutter/v2/how--to-guides/debugging/error-handling } void onMessage({required HMSMessage message}) { // Checkout the docs for chat messaging here: https://www.100ms.live/docs/flutter/v2/how--to-guides/set-up-video-conferencing/chat } void onReconnected() { // Checkout the docs for reconnection handling here: https://www.100ms.live/docs/flutter/v2/how--to-guides/handle-interruptions/reconnection-handling } void onReconnecting() { // Checkout the docs for reconnection handling here: https://www.100ms.live/docs/flutter/v2/how--to-guides/handle-interruptions/reconnection-handling } void onRemovedFromRoom( {required HMSPeerRemovedFromPeer hmsPeerRemovedFromPeer}) { // Checkout the docs for handling the peer removal here: https://www.100ms.live/docs/flutter/v2/how--to-guides/interact-with-room/peer/remove-peer } void onRoleChangeRequest({required HMSRoleChangeRequest roleChangeRequest}) { // Checkout the docs for handling the role change request here: https://www.100ms.live/docs/flutter/v2/how--to-guides/interact-with-room/peer/change-role#accept-role-change-request } void onRoomUpdate({required HMSRoom room, required HMSRoomUpdate update}) { // Checkout the docs for room updates here: https://www.100ms.live/docs/flutter/v2/how--to-guides/listen-to-room-updates/update-listeners } void onUpdateSpeakers({required List<HMSSpeaker> updateSpeakers}) { // Checkout the docs for handling the updates regarding who is currently speaking here: https://www.100ms.live/docs/flutter/v2/how--to-guides/set-up-video-conferencing/render-video/show-audio-level } /// ****************************************************************************************************************************************************** /// Action result listener methods void onException( {required HMSActionResultListenerMethod methodType, Map<String, dynamic>? arguments, required HMSException hmsException}) { switch (methodType) { case HMSActionResultListenerMethod.leave: log("Not able to leave error occured"); break; default: break; } } void onSuccess( {required HMSActionResultListenerMethod methodType, Map<String, dynamic>? arguments}) { switch (methodType) { case HMSActionResultListenerMethod.leave: _hmsSDK.removeUpdateListener(listener: this); _hmsSDK.destroy(); break; default: break; } } /// ****************************************************************************************************************************************************** /// Functions final List<Color> _colors = [ Colors.amber, Colors.blue.shade600, Colors.purple, Colors.lightGreen, Colors.redAccent ]; final RegExp _REGEX_EMOJI = RegExp( r'(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])'); String _getAvatarTitle(String name) { if (name.contains(_REGEX_EMOJI)) { name = name.replaceAll(_REGEX_EMOJI, ''); if (name.trim().isEmpty) { return '😄'; } } List<String>? parts = name.trim().split(" "); if (parts.length == 1) { name = parts[0][0]; } else if (parts.length >= 2) { name = parts[0][0]; if (parts[1] == "" || parts[1] == " ") { name += parts[0][1]; } else { name += parts[1][0]; } } return name.toUpperCase(); } Color _getBackgroundColour(String name) { if (name.isEmpty) return Colors.blue.shade600; if (name.contains(_REGEX_EMOJI)) { name = name.replaceAll(_REGEX_EMOJI, ''); if (name.trim().isEmpty) { return Colors.blue.shade600; } } return _colors[name.toUpperCase().codeUnitAt(0) % _colors.length]; } /// ****************************************************************************************************************************************************** Widget build(BuildContext context) { return WillPopScope( onWillPop: () async { _hmsSDK.leave(hmsActionResultListener: this); Navigator.pop(context); return true; }, child: SafeArea( child: Scaffold( body: Container( color: Colors.grey.shade900, child: Column( children: [ Expanded( child: Padding( padding: const EdgeInsets.all(8.0), /** * We have a custom scroll view to display listeners and speakers * we have divided them in two sections namely listeners and speakers * On the top we show all the speakers, then we have a listener * section where we show all the listeners in the room. */ child: CustomScrollView( slivers: [ const SliverToBoxAdapter( child: Text( "Speakers", style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold), ), ), const SliverToBoxAdapter( child: SizedBox( height: 20, ), ), //This is the list of all the speakers /** * UI is something like this: * * CircleAvatar Widget * SizedBox * Name of the peer(Text) * * We have 4 speakers in a row defined by crossAxisCount * in gridDelegate */ SliverGrid.builder( itemCount: _speakers.length, itemBuilder: (context, index) { return GestureDetector( onLongPress: () {}, child: Column( children: [ CircleAvatar( radius: 25, backgroundColor: _getBackgroundColour( _speakers[index].peer.name), child: Text( _getAvatarTitle( _speakers[index].peer.name), style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold), ), ), const SizedBox( height: 5, ), Text( _speakers[index].peer.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Colors.white), ) ], ), ); }, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, mainAxisSpacing: 5)), const SliverToBoxAdapter( child: SizedBox( height: 20, ), ), const SliverToBoxAdapter( child: Text( "Listener", style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold), ), ), const SliverToBoxAdapter( child: SizedBox( height: 20, ), ), //This is the list of all the speakers /** * UI is something like this: * * CircleAvatar Widget * SizedBox * Name of the peer(Text) * * We have 5 listeners in a row defined by crossAxisCount * in gridDelegate */ SliverGrid.builder( itemCount: _listeners.length, itemBuilder: (context, index) { return GestureDetector( onLongPress: () {}, child: Column( children: [ Expanded( child: CircleAvatar( radius: 20, backgroundColor: _getBackgroundColour( _listeners[index].peer.name), child: Text( _getAvatarTitle( _listeners[index].peer.name), style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold), ), ), ), const SizedBox( height: 5, ), Text( _listeners[index].peer.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Colors.white), ) ], ), ); }, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( mainAxisSpacing: 5, crossAxisCount: 5)), ], ), ), ), //This section takes care of the leave button and the microphone mute/unmute option Padding( padding: const EdgeInsets.all(8.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ OutlinedButton( style: OutlinedButton.styleFrom( backgroundColor: Colors.grey.shade300, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15)), ), onPressed: () { _hmsSDK.leave(hmsActionResultListener: this); Navigator.pop(context); }, child: const Text( '✌️ Leave quietly', style: TextStyle(color: Colors.redAccent), )), const Spacer(), //We only show the mic icon if a peer has permission to publish audio if (_localPeer?.role.publishSettings?.allowed .contains("audio") ?? false) OutlinedButton( style: OutlinedButton.styleFrom( backgroundColor: Colors.grey.shade300, padding: EdgeInsets.zero, shape: const CircleBorder()), onPressed: () { _hmsSDK.toggleMicMuteState(); setState(() { _isMicrophoneMuted = !_isMicrophoneMuted; }); }, child: Icon( _isMicrophoneMuted ? Icons.mic_off : Icons.mic, color: _isMicrophoneMuted ? Colors.red : Colors.green, )), ], ), ) ], ), ), )), ); } } class PeerTrackNode { String uid; HMSPeer peer; bool isRaiseHand; HMSTrack? audioTrack; PeerTrackNode( {required this.uid, required this.peer, this.audioTrack, this.isRaiseHand = false}); String toString() { return 'PeerTrackNode{uid: $uid, peerId: ${peer.peerId},track: $audioTrack}'; } }

Fetch token to join the room

Fetch token using room-code method(Recommended)

We can get the authentication token using room-code from meeting URL.

Let's understand the subdomain and code from the sample URL In this URL: http://100ms-rocks.app.100ms.live/meeting/abc-defg-hij

  • Subdomain is 100ms-rocks
  • Room code is abc-defg-ghi

Now to get the room-code from meeting URL we can write our own logic or use the getCode method from here

To generate token we will be using getAuthTokenByRoomCode method of HMSSDK. This method has roomCode as a required parameter, userId & endPoint as optional parameter.

This method should be called after calling the build method.

Let's checkout the implementation:

//This returns an object of Future<dynamic> which can be either //of HMSException type or String? type based on whether //method execution is completed successfully or not dynamic authToken = await hmsSDK.getAuthTokenByRoomCode(roomCode: 'YOUR_ROOM_CODE'); if(authToken is String){ HMSConfig roomConfig = HMSConfig( authToken: authToken, userName: userName, ); hmsSDK.join(config: roomConfig); } else if(authToken is HMSException){ // Handle the error }

🔑 Note: There will be separate room codes for broadcaster and hls-viewer roles so please use room codes accordingly.

Get temporary token from dashboard

To test audio/video functionality, you need to connect to a 100ms room; please check the following steps for the same:

  1. Navigate to your 100ms dashboard or create an account if you don't have one.
  2. Use the Audio room template to create a room with a default template assigned to it to test this app quickly.
  3. Go to the Rooms page in your dashboard, click on the Room Id of the room you created above, and click on the Join Room button on the top right.
  4. In the Join with SDK section you can find the Auth Token for role ; you can click on the 'copy' icon to copy the authentication token and update the authToken in "lib/main.dart" file for both roles.

Token from 100ms dashboard is for testing purposes only, For production applications you must generate tokens on your own server. Refer to the Management Token section in Authentication and Tokens guide for more information.


Test the app

After adding the required code let's run the app

Build and run the app

  • Once you've made the above changes, your app is ready for testing. You can build the app and run it in an emulator or an actual android device.
  • Go to Run > Start debugging > select a device to use (android emulator or android phone).

Now, after you click Join as Broadcaster, you should be able to see yourself (android emulator doesn't support actual video, you can connect an actual device to see your video in real-time). You can join the room using a browser as the second peer to check audio/video transactions between two or more peers. If you click Join as HLS Viewer, you should be able to watch the stream if it's started or the text Please wait for the stream to start if it's not started yet

Building step-by-step

For creating audio room we will use the audio room template from dashboard. In this audio room quickstart we are handling two roles namely listener and speaker. In this example only speaker can publish audio whereas listener does not putting it in simple words as, the speaker can talk in the room while the listener can only hear to speakers.

In the application UI we have created separate sections containing speakers and listeners.

To join the room:

  • As Speaker : Join the room with speaker auth token
  • As Listener : Join the room with listener auth token

In this section, We'll walk through what the code does.

Add dependencies in pubspec.yaml

In your project pubspec.yaml dependencies add:

# 100ms SDK and permissions_handler hmssdk_flutter: 1.4.0 permission_handler: 10.2.0

Add permissions for android and iOS

Add the permissions for microphone and bluetooth for android and iOS follow the docs above

Handle device runtime permissions

We need permission from the user to access the media from the user's device. We must urge the user to grant permission to access camera, microphone, and bluetooth devices. We use the permission_handler package that provides a cross-platform (iOS, Android) API to request permissions and check their status.

Please ensure to update permissions in the AndroidManifest.xml file for android and info.plist file for iOS. Check Add Permission section for more information. getPermissions takes required permission for microphone, camera and bluetooth.

class _MyHomePageState extends State<MyHomePage> { bool res = false; Future<bool> getPermissions() async { if (Platform.isIOS) return true; await Permission.microphone.request(); await Permission.bluetoothConnect.request(); while ((await Permission.microphone.isDenied)) { await Permission.microphone.request(); } while ((await Permission.bluetoothConnect.isDenied)) { await Permission.bluetoothConnect.request(); } return true; } }

Implement join screen

This section will help you create the join screen user interface. To keep it simple for the quickstart, we have not created many UI elements; you can refer to the sample app implementation for a complete Preview/Join user interface. Add the below code in HomePage class.

Widget build(BuildContext context) { return Scaffold( body: Container( color: Colors.black, child: Center( child: ElevatedButton( style: ButtonStyle( shape: MaterialStateProperty.all<RoundedRectangleBorder>( RoundedRectangleBorder( borderRadius: BorderRadius.circular(8.0), ))), onPressed: () async => { res = await getPermissions(), if (res) Navigator.push(context, CupertinoPageRoute(builder: (_) => const MeetingPage())) }, child: const Padding( padding: EdgeInsets.symmetric(vertical: 20, horizontal: 15), child: Text( 'Join Audio Room', style: TextStyle(fontSize: 20), ), ), ), ), ), ); }

Implement meeting page

You can check the below snippet to create a widget as the user interface to show the audio avatar's of speakers and listeners. HMSUpdateListener plays a significant role in providing information regarding the room.

100ms SDK provides callbacks to the client app about any change or update happening in the room after a user has joined by implementing HMSUpdateListener.

To join a room, you need to create an HMSConfig instance and use that instance to call the join method of HMSSDK.

Note: An Auth token is required to authenticate a room join request from your client-side app. Please ensure to add the authToken by fetching it from your dashboard. Check fetch token to join a room section for more information.
Read more about authentication and tokens in this guide

class _MeetingPageState extends State<MeetingPage> implements HMSUpdateListener, HMSActionResultListener { late HMSSDK _hmsSDK; //Enter the username and authToken from dashboard for the corresponding role here. String userName = "Enter Username Here"; String authToken = "Enter AuthToken Here"; Offset position = const Offset(5, 5); bool isJoinSuccessful = false; final List<PeerTrackNode> _listeners = []; final List<PeerTrackNode> _speakers = []; bool _isMicrophoneMuted = false; HMSPeer? _localPeer; void initState() { super.initState(); initHMSSDK(); } //To know more about HMSSDK setup and initialization checkout the docs here: https://www.100ms.live/docs/flutter/v2/how--to-guides/install-the-sdk/hmssdk void initHMSSDK() async { _hmsSDK = HMSSDK(); await _hmsSDK.build(); _hmsSDK.addUpdateListener(listener: this); _hmsSDK.join(config: HMSConfig(authToken: authToken, userName: userName)); } void dispose() { //We are clearing the room state here _speakers.clear(); _listeners.clear(); super.dispose(); } }

Now in the same class we will override the HMSUpdateListener methods to listen to updates.

Listen to room and peer updates

The 100ms SDK sends updates to the application about any change in HMSPeer and HMSRoom via the callbacks in HMSUpdateListener. Our application must listen to the corresponding updates in onPeerUpdate and onRoomUpdate. Check the Update Listeners documentation to understand the types of updates emitted by the SDK for room and peer updates. We will add these methods in MeetingPage class as they need to override the HMSUpdateListener methods.

void onJoin({required HMSRoom room}) { //Checkout the docs about handling onJoin here: https://www.100ms.live/docs/flutter/v2/how--to-guides/set-up-video-conferencing/join#join-a-room room.peers?.forEach((peer) { if (peer.isLocal) { _localPeer = peer; switch (peer.role.name) { case "speaker": int index = _speakers .indexWhere((node) => node.uid == "${peer.peerId}speaker"); if (index != -1) { _speakers[index].peer = peer; } else { _speakers.add(PeerTrackNode( uid: "${peer.peerId}speaker", peer: peer, )); } setState(() {}); break; case "listener": int index = _listeners .indexWhere((node) => node.uid == "${peer.peerId}listener"); if (index != -1) { _listeners[index].peer = peer; } else { _listeners.add( PeerTrackNode(uid: "${peer.peerId}listener", peer: peer)); } setState(() {}); break; default: //Handle the case if you have other roles in the room break; } } }); } //Here we will be getting updates about peer join, leave, role changed, name changed etc. void onPeerUpdate({required HMSPeer peer, required HMSPeerUpdate update}) { if (peer.isLocal) { _localPeer = peer; } switch (update) { case HMSPeerUpdate.peerJoined: switch (peer.role.name) { case "speaker": int index = _speakers .indexWhere((node) => node.uid == "${peer.peerId}speaker"); if (index != -1) { _speakers[index].peer = peer; } else { _speakers.add(PeerTrackNode( uid: "${peer.peerId}speaker", peer: peer, )); } setState(() {}); break; case "listener": int index = _listeners .indexWhere((node) => node.uid == "${peer.peerId}listener"); if (index != -1) { _listeners[index].peer = peer; } else { _listeners.add( PeerTrackNode(uid: "${peer.peerId}listener", peer: peer)); } setState(() {}); break; default: //Handle the case if you have other roles in the room break; } break; case HMSPeerUpdate.peerLeft: switch (peer.role.name) { case "speaker": int index = _speakers .indexWhere((node) => node.uid == "${peer.peerId}speaker"); if (index != -1) { _speakers.removeAt(index); } setState(() {}); break; case "listener": int index = _listeners .indexWhere((node) => node.uid == "${peer.peerId}listener"); if (index != -1) { _listeners.removeAt(index); } setState(() {}); break; default: //Handle the case if you have other roles in the room break; } break; case HMSPeerUpdate.roleUpdated: if (peer.role.name == "speaker") { //This means previously the user must be a listener earlier in our case //So we remove the peer from listener and add it to speaker list int index = _listeners .indexWhere((node) => node.uid == "${peer.peerId}listener"); if (index != -1) { _listeners.removeAt(index); } _speakers.add(PeerTrackNode( uid: "${peer.peerId}speaker", peer: peer, )); if (peer.isLocal) { _isMicrophoneMuted = peer.audioTrack?.isMute ?? true; } setState(() {}); } else if (peer.role.name == "listener") { //This means previously the user must be a speaker earlier in our case //So we remove the peer from speaker and add it to listener list int index = _speakers .indexWhere((node) => node.uid == "${peer.peerId}speaker"); if (index != -1) { _speakers.removeAt(index); } _listeners.add(PeerTrackNode( uid: "${peer.peerId}listener", peer: peer, )); setState(() {}); } break; case HMSPeerUpdate.metadataChanged: switch (peer.role.name) { case "speaker": int index = _speakers .indexWhere((node) => node.uid == "${peer.peerId}speaker"); if (index != -1) { _speakers[index].peer = peer; } setState(() {}); break; case "listener": int index = _listeners .indexWhere((node) => node.uid == "${peer.peerId}listener"); if (index != -1) { _listeners[index].peer = peer; } setState(() {}); break; default: //Handle the case if you have other roles in the room break; } break; case HMSPeerUpdate.nameChanged: switch (peer.role.name) { case "speaker": int index = _speakers .indexWhere((node) => node.uid == "${peer.peerId}speaker"); if (index != -1) { _speakers[index].peer = peer; } setState(() {}); break; case "listener": int index = _listeners .indexWhere((node) => node.uid == "${peer.peerId}listener"); if (index != -1) { _listeners[index].peer = peer; } setState(() {}); break; default: //Handle the case if you have other roles in the room break; } break; case HMSPeerUpdate.defaultUpdate: // TODO: Handle this case. break; case HMSPeerUpdate.networkQualityUpdated: // TODO: Handle this case. break; } }

Listen to track updates

100ms SDK also sends updates to the application about any change in HMSTrack via the callbacks in HMSUpdateListener. Our application must listen to the corresponding updates in onTrackUpdate. Check the Update Listeners documentation to understand the types of updates emitted by the SDK for track updates.

void onTrackUpdate( {required HMSTrack track, required HMSTrackUpdate trackUpdate, required HMSPeer peer}) { switch (peer.role.name) { case "speaker": int index = _speakers.indexWhere((node) => node.uid == "${peer.peerId}speaker"); if (index != -1) { _speakers[index].audioTrack = track; } else { _speakers.add(PeerTrackNode( uid: "${peer.peerId}speaker", peer: peer, audioTrack: track)); } if (peer.isLocal) { _isMicrophoneMuted = track.isMute; } setState(() {}); break; case "listener": int index = _listeners .indexWhere((node) => node.uid == "${peer.peerId}listener"); if (index != -1) { _listeners[index].audioTrack = track; } else { _listeners.add(PeerTrackNode( uid: "${peer.peerId}listener", peer: peer, audioTrack: track)); } setState(() {}); break; default: //Handle the case if you have other roles in the room break; } }

Other callbacks

100ms SDK provides various other callbacks to handle different scenarios in the app. For example, you can use onAudioDeviceChanged to get updates whenever a new audio device or an audio device is switched. Please check here for more information about these callbacks.

void onAudioDeviceChanged( {HMSAudioDevice? currentAudioDevice, List<HMSAudioDevice>? availableAudioDevice}) { // Checkout the docs about handling onAudioDeviceChanged updates here: https://www.100ms.live/docs/flutter/v2/how--to-guides/listen-to-room-updates/update-listeners } void onChangeTrackStateRequest( {required HMSTrackChangeRequest hmsTrackChangeRequest}) { // Checkout the docs for handling the unmute request here: https://www.100ms.live/docs/flutter/v2/how--to-guides/interact-with-room/track/remote-mute-unmute } void onHMSError({required HMSException error}) { // To know more about handling errors please checkout the docs here: https://www.100ms.live/docs/flutter/v2/how--to-guides/debugging/error-handling } void onMessage({required HMSMessage message}) { // Checkout the docs for chat messaging here: https://www.100ms.live/docs/flutter/v2/how--to-guides/set-up-video-conferencing/chat } void onReconnected() { // Checkout the docs for reconnection handling here: https://www.100ms.live/docs/flutter/v2/how--to-guides/handle-interruptions/reconnection-handling } void onReconnecting() { // Checkout the docs for reconnection handling here: https://www.100ms.live/docs/flutter/v2/how--to-guides/handle-interruptions/reconnection-handling } void onRemovedFromRoom( {required HMSPeerRemovedFromPeer hmsPeerRemovedFromPeer}) { // Checkout the docs for handling the peer removal here: https://www.100ms.live/docs/flutter/v2/how--to-guides/interact-with-room/peer/remove-peer } void onRoleChangeRequest({required HMSRoleChangeRequest roleChangeRequest}) { // Checkout the docs for handling the role change request here: https://www.100ms.live/docs/flutter/v2/how--to-guides/interact-with-room/peer/change-role#accept-role-change-request } void onRoomUpdate({required HMSRoom room, required HMSRoomUpdate update}) { // Checkout the docs for room updates here: https://www.100ms.live/docs/flutter/v2/how--to-guides/listen-to-room-updates/update-listeners } void onUpdateSpeakers({required List<HMSSpeaker> updateSpeakers}) { // Checkout the docs for handling the updates regarding who is currently speaking here: https://www.100ms.live/docs/flutter/v2/how--to-guides/set-up-video-conferencing/render-video/show-audio-level }

Action result listener callbacks

void onException( {required HMSActionResultListenerMethod methodType, Map<String, dynamic>? arguments, required HMSException hmsException}) { switch (methodType) { case HMSActionResultListenerMethod.leave: log("Not able to leave error occured"); break; default: break; } } void onSuccess( {required HMSActionResultListenerMethod methodType, Map<String, dynamic>? arguments}) { switch (methodType) { case HMSActionResultListenerMethod.leave: _hmsSDK.removeUpdateListener(listener: this); _hmsSDK.destroy(); break; default: break; } }

Add Utility functions for UI

final List<Color> _colors = [ Colors.amber, Colors.blue.shade600, Colors.purple, Colors.lightGreen, Colors.redAccent ]; final RegExp _REGEX_EMOJI = RegExp( r'(\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff])'); String _getAvatarTitle(String name) { if (name.contains(_REGEX_EMOJI)) { name = name.replaceAll(_REGEX_EMOJI, ''); if (name.trim().isEmpty) { return '😄'; } } List<String>? parts = name.trim().split(" "); if (parts.length == 1) { name = parts[0][0]; } else if (parts.length >= 2) { name = parts[0][0]; if (parts[1] == "" || parts[1] == " ") { name += parts[0][1]; } else { name += parts[1][0]; } } return name.toUpperCase(); } Color _getBackgroundColour(String name) { if (name.isEmpty) return Colors.blue.shade600; if (name.contains(_REGEX_EMOJI)) { name = name.replaceAll(_REGEX_EMOJI, ''); if (name.trim().isEmpty) { return Colors.blue.shade600; } } return _colors[name.toUpperCase().codeUnitAt(0) % _colors.length]; }

Render peer tile

To render a single peer we are using Column widget.

Column( children: [ CircleAvatar( radius: 25, backgroundColor: _getBackgroundColour( _speakers[index].peer.name), child: Text( _getAvatarTitle( _speakers[index].peer.name), //get first character from peer name to show in circular avatar style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold), ), ), const SizedBox( height: 5, ), Text( _speakers[index].peer.name, //Render peer name here maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Colors.white), ) ], )

Render list of Speakers and Listeners

Column( children: [ Expanded( child: Padding( padding: const EdgeInsets.all(8.0), /** * We have a custom scroll view to display listeners and speakers * we have divided them in two sections namely listeners and speakers * On the top we show all the speakers, then we have a listener * section where we show all the listeners in the room. */ child: CustomScrollView( slivers: [ const SliverToBoxAdapter( child: Text( "Speakers", style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold), ), ), const SliverToBoxAdapter( child: SizedBox( height: 20, ), ), //This is the list of all the speakers /** * UI is something like this: * * CircleAvatar Widget * SizedBox * Name of the peer(Text) * * We have 4 speakers in a row defined by crossAxisCount * in gridDelegate */ SliverGrid.builder( itemCount: _speakers.length, itemBuilder: (context, index) { return GestureDetector( onLongPress: () {}, child: Column( children: [ CircleAvatar( radius: 25, backgroundColor: _getBackgroundColour( _speakers[index].peer.name), child: Text( _getAvatarTitle( _speakers[index].peer.name), style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold), ), ), const SizedBox( height: 5, ), Text( _speakers[index].peer.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Colors.white), ) ], ), ); }, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, mainAxisSpacing: 5)), const SliverToBoxAdapter( child: SizedBox( height: 20, ), ), const SliverToBoxAdapter( child: Text( "Listener", style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold), ), ), const SliverToBoxAdapter( child: SizedBox( height: 20, ), ), //This is the list of all the speakers /** * UI is something like this: * * CircleAvatar Widget * SizedBox * Name of the peer(Text) * * We have 5 listeners in a row defined by crossAxisCount * in gridDelegate */ SliverGrid.builder( itemCount: _listeners.length, itemBuilder: (context, index) { return GestureDetector( onLongPress: () {}, child: Column( children: [ Expanded( child: CircleAvatar( radius: 20, backgroundColor: _getBackgroundColour( _listeners[index].peer.name), child: Text( _getAvatarTitle( _listeners[index].peer.name), style: const TextStyle( color: Colors.white, fontSize: 20, fontWeight: FontWeight.bold), ), ), ), const SizedBox( height: 5, ), Text( _listeners[index].peer.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Colors.white), ) ], ), ); }, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( mainAxisSpacing: 5, crossAxisCount: 5)), ], ), ), ), //This section takes care of the leave button and the microphone mute/unmute option Padding( padding: const EdgeInsets.all(8.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ OutlinedButton( style: OutlinedButton.styleFrom( backgroundColor: Colors.grey.shade300, elevation: 0, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15)), ), onPressed: () { _hmsSDK.leave(hmsActionResultListener: this); Navigator.pop(context); }, child: const Text( '✌️ Leave quietly', style: TextStyle(color: Colors.redAccent), )), const Spacer(), //We only show the mic icon if a peer has permission to publish audio if (_localPeer?.role.publishSettings?.allowed .contains("audio") ?? false) OutlinedButton( style: OutlinedButton.styleFrom( backgroundColor: Colors.grey.shade300, padding: EdgeInsets.zero, shape: const CircleBorder()), onPressed: () { _hmsSDK.toggleMicMuteState(); setState(() { _isMicrophoneMuted = !_isMicrophoneMuted; }); }, child: Icon( _isMicrophoneMuted ? Icons.mic_off : Icons.mic, color: _isMicrophoneMuted ? Colors.red : Colors.green, )), ], ), ) ], )

That's it we are all done, run the application start conversing🚀🚀🚀🚀

You can refer to the test the app section to test your app for android or iOS platforms.

Next steps

We have multiple example apps to get you started with 100ms Flutter SDK.

Clubhouse clone blog

  • Checkout the clubhouse clone here
  • Checkout the repository here

Basic example

For a basic example, see the sample app on GitHub.

Full-fledged example

You can also check out the full-fledged example app implementation in the 100ms Flutter SDK GitHub repository showcasing multiple features provided by 100ms. This uses the provider package as the state management library.

Examples with other state management libraries

For implementations with other state management libraries, visit :

App store / Play store

You can download & check out the 100ms Flutter app -

🤖 Flutter Android app from Google Play Store.

📱 Flutter iOS app from Apple App Store.


Have a suggestion? Recommend changes ->

Was this helpful?

1234