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:
- Create a sample app — instructions to create a flutter app quickly with a complete code sample.
- 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:
- A 100ms account if you don't have one already.
- Flutter
3.3.0
or higher - Dart
2.12.0
or above - Use VS code, Android Studio, or any other IDE that supports Flutter. For more information on setting up an IDE, check Flutter's official guide.
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:
-
Allow application to use below features by adding the below snippet to the
AndroidManifest.xml
file (at the application tag level).
<!-- Required for devices above android 12 for disabling mute while receving the call--> <uses-permission android:name="android.permission.READ_PHONE_STATE"/> <uses-permission android:name="android.permission.READ_PHONE_NUMBERS" /> <!--Required for android 14 and above --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" /> <!-- This is required if the application uses foreground service for android 14 and above--> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
-
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:
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:
- Navigate to your 100ms dashboard or create an account if you don't have one.
- Use the
Audio room
template to create a room with a default template assigned to it to test this app quickly. - Go to the Rooms page in your dashboard, click on the
Room Id
of the room you created above, and click on theJoin Room
button on the top right. - 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 theauthToken
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.
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), ), ), ), ), ), ); }
Widget
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
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.