HLS Streaming QuickStart Guide
Overview
This guide will walk you through simple instructions to create a HLS Live Streaming Application 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.
Checkout the github repository for quickstart application here
When to use HLS?
- HLS Streaming content is delayed by 8 to 10 seconds behind the live edge.
- To view HLS content you have to create an instance of
VideoPlayerController
and pass it the .m3u8 URL. - This is useful in scenarios where there is a large number of Peers in Room and you don't need everyone to be in sync.
- For example, in a large scale webinar where there are 1000s of viewers, you can use HLS to view the stream with a delay of 8 to 10 seconds.
- This will reduce the load on the server and the client.
- If you want to always view the live stream instead of the delayed stream then use the
HMSVideoView
instead ofVideoPlayerController
. - Ensure that you have the correct Authentication Token for the Real Time Viewer role.
- Refer this Guide if you want to always view the live stream instead of the delayed stream.
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,permission_handler package (to handle audio/video permissions from microphone and camera) and video player(to play the HLS Stream) to your app.
- Add the below snippet to the
pubspec.yaml
.
# 100ms SDK and permissions_handler hmssdk_flutter: permission_handler: video_player:
- 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
Now that your project setup is complete let's replace the code in the lib/main.dart
file with the complete code sample below.
After running the below code your application will look similar to this:
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'; import 'package:video_player/video_player.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); Widget build(BuildContext context) { return MaterialApp( title: '100ms HLS Quickstart Guide', theme: ThemeData( primarySwatch: Colors.blue, ), home: const MyHomePage(title: '100ms HLS Integration 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; String userName = "Test"; static Future<bool> getPermissions() async { if (Platform.isIOS) return true; await Permission.camera.request(); await Permission.microphone.request(); await Permission.bluetoothConnect.request(); while ((await Permission.camera.isDenied)) { await Permission.camera.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( appBar: AppBar( title: Text(widget.title), ), body: Container( width: MediaQuery.of(context).size.width, color: Colors.black, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ //Button to join as broadcaster 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: (_) => MeetingPage( authToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXJzaW9uIjoyLCJ0eXBlIjoiYXBwIiwiYXBwX2RhdGEiOm51bGwsImFjY2Vzc19rZXkiOiI2MThiNTU5MGJlNmMzYzBiMzUxNTBhYmEiLCJyb2xlIjoiaG9zdCIsInJvb21faWQiOiI2MThiNTVkOGJlNmMzYzBiMzUxNTBhYmQiLCJ1c2VyX2lkIjoiMThmYzFlODgtM2YzNS00MDhkLTg3OTYtNDE5YjI3MTEyMTk0IiwiZXhwIjoxNjgxMTkzNTU0LCJqdGkiOiI5ZTkwZDczMS0wYThhLTQ5NWEtOTQxNC1mY2Q1YWFhMTY5NTkiLCJpYXQiOjE2ODExMDcxNTQsImlzcyI6IjYxOGI1NTkwYmU2YzNjMGIzNTE1MGFiOCIsIm5iZiI6MTY4MTEwNzE1NCwic3ViIjoiYXBpIn0.-QobyOO90TVMXfgnWkbJ9a8X3U9Sc3vWKwGoLkv9s4w", userName: userName))) }, child: const Padding( padding: EdgeInsets.symmetric(vertical: 20, horizontal: 20), child: Text( 'Join as Broadcaster', style: TextStyle(fontSize: 20), ), ), ), const SizedBox( height: 20, ), //Button to join as HLSViewer 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: (_) => HLSViewerPage( authToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXJzaW9uIjoyLCJ0eXBlIjoiYXBwIiwiYXBwX2RhdGEiOm51bGwsImFjY2Vzc19rZXkiOiI2MThiNTU5MGJlNmMzYzBiMzUxNTBhYmEiLCJyb2xlIjoiaGxzLXZpZXdlciIsInJvb21faWQiOiI2MThiNTVkOGJlNmMzYzBiMzUxNTBhYmQiLCJ1c2VyX2lkIjoiZmUwOGQyYjQtM2Y4ZS00OTliLThlNzctNTkyYWQ4ZDQwOWRmIiwiZXhwIjoxNjgxMTk1NzM1LCJqdGkiOiI0OTk2NDQxMi0zYjlkLTRjZDQtOTg1YS0xODQxZTliNWJiOWEiLCJpYXQiOjE2ODExMDkzMzUsImlzcyI6IjYxOGI1NTkwYmU2YzNjMGIzNTE1MGFiOCIsIm5iZiI6MTY4MTEwOTMzNSwic3ViIjoiYXBpIn0.6GeiCmweoCAUv1xLD6icsSkSi1dixMS3YZMGYFrtNos", userName: userName))) }, child: const Padding( padding: EdgeInsets.symmetric(vertical: 20, horizontal: 20), child: Text( 'Join as HLS Viewer', style: TextStyle(fontSize: 20), ), ), ), ], ), ), ); } } class MeetingPage extends StatefulWidget { final String authToken; final String userName; const MeetingPage( {super.key, required this.authToken, required this.userName}); State<MeetingPage> createState() => _MeetingPageState(); } class _MeetingPageState extends State<MeetingPage> implements HMSUpdateListener { late HMSSDK _hmsSDK; bool _isHLSRunning = false, _isLoading = false; HMSPeer? _localPeer, _remotePeer; HMSVideoTrack? _localPeerVideoTrack, _remotePeerVideoTrack; 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: widget.authToken, userName: widget.userName)); } void dispose() { _remotePeer = null; _remotePeerVideoTrack = null; _localPeer = null; _localPeerVideoTrack = null; super.dispose(); } void onJoin({required HMSRoom room}) { room.peers?.forEach((peer) { if (peer.isLocal) { _localPeer = peer; if (peer.videoTrack != null) { _localPeerVideoTrack = peer.videoTrack; } if (mounted) { WidgetsBinding.instance.addPostFrameCallback((_) { setState(() {}); }); } } }); } void onPeerUpdate({required HMSPeer peer, required HMSPeerUpdate update}) { if (update == HMSPeerUpdate.networkQualityUpdated) { return; } if (update == HMSPeerUpdate.peerJoined) { if (!peer.isLocal && !peer.role.name.contains("recorder")) { if (mounted) { WidgetsBinding.instance.addPostFrameCallback((_) { setState(() { _remotePeer = peer; }); }); } } } else if (update == HMSPeerUpdate.peerLeft) { if (!peer.isLocal && !peer.role.name.contains("recorder")) { if (mounted) { setState(() { _remotePeer = null; }); } } else { if (mounted) { setState(() { _localPeer = null; }); } } } } void onTrackUpdate( {required HMSTrack track, required HMSTrackUpdate trackUpdate, required HMSPeer peer}) { if (track.kind == HMSTrackKind.kHMSTrackKindVideo) { if (trackUpdate == HMSTrackUpdate.trackRemoved) { if (peer.isLocal) { if (mounted) { setState(() { _localPeerVideoTrack = null; }); } } else if (!peer.role.name.contains("recorder")) { if (mounted) { setState(() { _remotePeerVideoTrack = null; }); } } return; } if (peer.isLocal) { if (mounted) { setState(() { _localPeerVideoTrack = track as HMSVideoTrack; }); } } else if (!peer.role.name.contains("recorder")) { if (mounted) { setState(() { _remotePeerVideoTrack = track as HMSVideoTrack; }); } } } } 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 onSessionStoreAvailable( {HMSSessionStore? hmsSessionStore}) { // Get notified when the Session Store is available for usage. Read more about Session Store here: https://www.100ms.live/docs/flutter/v2/how-to-guides/interact-with-room/room/session-store } 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 if (update == HMSRoomUpdate.hlsStreamingStateUpdated) { if (room.hmshlsStreamingState?.running ?? false) { _isHLSRunning = true; } else { _isHLSRunning = false; } _isLoading = false; setState(() {}); } } 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 } void _leaveRoom() { _hmsSDK.leave(); _hmsSDK.removeUpdateListener(listener: this); } Widget build(BuildContext context) { return WillPopScope( onWillPop: () async { _leaveRoom(); Navigator.pop(context); return true; }, child: SafeArea( child: Scaffold( body: Stack( children: [ Container( color: Colors.black, height: MediaQuery.of(context).size.height, child: GridView( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( mainAxisExtent: (_remotePeerVideoTrack == null) ? MediaQuery.of(context).size.height : MediaQuery.of(context).size.height / 2, crossAxisCount: 1), children: [ if (_remotePeerVideoTrack != null && _remotePeer != null) peerTile( Key(_remotePeerVideoTrack?.trackId ?? "" "mainVideo"), _remotePeerVideoTrack, _remotePeer, context), peerTile( Key(_localPeerVideoTrack?.trackId ?? "" "mainVideo"), _localPeerVideoTrack, _localPeer, context) ], )), Align( alignment: Alignment.bottomCenter, child: Container( color: Colors.black, child: Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Column( mainAxisSize: MainAxisSize.min, children: [ GestureDetector( onTap: () async { setState(() { _isLoading = true; }); if (_isHLSRunning) { _hmsSDK.stopHlsStreaming(); return; } _hmsSDK.startHlsStreaming(); }, child: Container( decoration: BoxDecoration( shape: BoxShape.circle, boxShadow: [ BoxShadow( color: _isHLSRunning ? Colors.red.withAlpha(60) : Colors.blue.withAlpha(60), blurRadius: 3.0, spreadRadius: 5.0, ), ]), child: CircleAvatar( radius: 25, backgroundColor: _isHLSRunning ? Colors.red : Colors.blue, child: _isLoading ? const CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ) : const Icon( Icons.broadcast_on_personal_outlined, color: Colors.white), ), ), ), const SizedBox(height: 3,), Text(_isHLSRunning?"STOP HLS":"START HLS",style: const TextStyle(color: Colors.white),), ], ), Column( mainAxisSize: MainAxisSize.min, children: [ GestureDetector( onTap: () async { _leaveRoom(); Navigator.pop(context); }, child: Container( decoration: BoxDecoration(shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.red.withAlpha(60), blurRadius: 3.0, spreadRadius: 5.0, ), ]), child: const CircleAvatar( radius: 25, backgroundColor: Colors.red, child: Icon(Icons.call_end, color: Colors.white), ), ), ), const SizedBox(height: 3,), const Text("Leave Room",style: TextStyle(color: Colors.white),), ], ), ], ), ), ), ), ], ), )), ); } Widget peerTile( Key key, HMSVideoTrack? videoTrack, HMSPeer? peer, BuildContext context) { return Container( key: key, color: Colors.black, child: (videoTrack != null && !(videoTrack.isMute)) // To know more about HMSVideoView checkout the docs here: https://www.100ms.live/docs/flutter/v2/how--to-guides/set-up-video-conferencing/render-video/overview ? HMSVideoView( track: videoTrack, ) : Center( child: Container( decoration: BoxDecoration( color: Colors.blue.withAlpha(4), shape: BoxShape.circle, boxShadow: const [ BoxShadow( color: Colors.blue, blurRadius: 20.0, spreadRadius: 5.0, ), ], ), child: Text( peer?.name.substring(0, 1) ?? "D", style: const TextStyle( color: Colors.white, fontSize: 24, fontWeight: FontWeight.w600), ), ), ), ); } } class HLSViewerPage extends StatefulWidget { final String authToken; final String userName; const HLSViewerPage( {super.key, required this.authToken, required this.userName}); State<HLSViewerPage> createState() => _HLSViewerPageState(); } class _HLSViewerPageState extends State<HLSViewerPage> implements HMSUpdateListener { late HMSSDK _hmsSDK; VideoPlayerController? _controller; late Future<void> _initializeVideoPlayerFuture; 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: widget.authToken, userName: widget.userName)); } void dispose() { _controller?.dispose(); _controller = null; super.dispose(); } void onJoin({required HMSRoom room}) { if (room.hmshlsStreamingState?.running ?? false) { if (room.hmshlsStreamingState?.variants[0]?.hlsStreamUrl != null) { _controller = VideoPlayerController.network( room.hmshlsStreamingState!.variants[0]!.hlsStreamUrl!, ); _initializeVideoPlayerFuture = _controller!.initialize(); _controller!.play(); _controller!.setLooping(true); } } setState(() {}); } void onPeerUpdate({required HMSPeer peer, required HMSPeerUpdate update}) {} void onTrackUpdate( {required HMSTrack track, required HMSTrackUpdate trackUpdate, required HMSPeer peer}) {} 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 onSessionStoreAvailable( {HMSSessionStore? hmsSessionStore}) { // Get notified when the Session Store is available for usage. Read more about Session Store here: https://www.100ms.live/docs/flutter/v2/how-to-guides/interact-with-room/room/session-store } 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 if (update == HMSRoomUpdate.hlsStreamingStateUpdated) { if (room.hmshlsStreamingState?.running ?? false) { if (room.hmshlsStreamingState?.variants[0]?.hlsStreamUrl != null) { _controller = VideoPlayerController.network( room.hmshlsStreamingState!.variants[0]!.hlsStreamUrl!, ); _initializeVideoPlayerFuture = _controller!.initialize(); _controller!.play(); _controller!.setLooping(true); } } else { _controller?.dispose(); _controller = null; } setState(() {}); } } 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 } void _leaveRoom() { _hmsSDK.leave(); _hmsSDK.removeUpdateListener(listener: this); } Widget build(BuildContext context) { return WillPopScope( onWillPop: () async { _leaveRoom(); Navigator.pop(context); return true; }, child: SafeArea( child: Scaffold( body: Stack( children: [ Container( color: Colors.black, height: MediaQuery.of(context).size.height, child: (_controller == null) ? const Center( child: Text( "Please wait for the stream to start", style: TextStyle(color: Colors.white), ), ) : FutureBuilder( future: _initializeVideoPlayerFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { return Center( child: Transform.scale( scaleX: 1.1, scaleY: 1.3, child: AspectRatio( aspectRatio: _controller!.value.aspectRatio, child: VideoPlayer(_controller!), ), ), ); } else { return const Center( child: CircularProgressIndicator(), ); } }, ), ), Align( alignment: Alignment.bottomCenter, child: Container( color: Colors.black, child: Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Column( mainAxisSize: MainAxisSize.min, children: [ GestureDetector( onTap: () async { _leaveRoom(); Navigator.pop(context); }, child: Container( decoration: BoxDecoration(shape: BoxShape.circle, boxShadow: [ BoxShadow( color: Colors.red.withAlpha(60), blurRadius: 3.0, spreadRadius: 5.0, ), ]), child: const CircleAvatar( radius: 25, backgroundColor: Colors.red, child: Icon(Icons.call_end, color: Colors.white), ), ), ), const SizedBox(height: 3,), const Text("Leave Room",style: TextStyle(color: Colors.white),), ], ), ], ), ), ), ), ], ), )), ); } }
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
Live Streaming
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
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: permission_handler: video_player:
Add permissions for android and iOS
Add the permissions for microphone,camera 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 HomePage extends StatelessWidget { const HomePage({super.key}); Future<bool> getPermissions() async { if (Platform.isIOS) return true; await Permission.camera.request(); await Permission.microphone.request(); await Permission.bluetoothConnect.request(); while ((await Permission.camera.isDenied)) { await Permission.camera.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.
// UI to render join screen Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Container( width: MediaQuery.of(context).size.width, color: Colors.black, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ //Button to join as broadcaster 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: (_) => MeetingPage( authToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXJzaW9uIjoyLCJ0eXBlIjoiYXBwIiwiYXBwX2RhdGEiOm51bGwsImFjY2Vzc19rZXkiOiI2MThiNTU5MGJlNmMzYzBiMzUxNTBhYmEiLCJyb2xlIjoiaG9zdCIsInJvb21faWQiOiI2MThiNTVkOGJlNmMzYzBiMzUxNTBhYmQiLCJ1c2VyX2lkIjoiMThmYzFlODgtM2YzNS00MDhkLTg3OTYtNDE5YjI3MTEyMTk0IiwiZXhwIjoxNjgxMTkzNTU0LCJqdGkiOiI5ZTkwZDczMS0wYThhLTQ5NWEtOTQxNC1mY2Q1YWFhMTY5NTkiLCJpYXQiOjE2ODExMDcxNTQsImlzcyI6IjYxOGI1NTkwYmU2YzNjMGIzNTE1MGFiOCIsIm5iZiI6MTY4MTEwNzE1NCwic3ViIjoiYXBpIn0.-QobyOO90TVMXfgnWkbJ9a8X3U9Sc3vWKwGoLkv9s4w", userName: userName))) }, child: const Padding( padding: EdgeInsets.symmetric(vertical: 20, horizontal: 20), child: Text( 'Join as Broadcaster', style: TextStyle(fontSize: 20), ), ), ), const SizedBox( height: 20, ), //Button to join as HLSViewer 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: (_) => HLSViewerPage( authToken: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2ZXJzaW9uIjoyLCJ0eXBlIjoiYXBwIiwiYXBwX2RhdGEiOm51bGwsImFjY2Vzc19rZXkiOiI2MThiNTU5MGJlNmMzYzBiMzUxNTBhYmEiLCJyb2xlIjoiaGxzLXZpZXdlciIsInJvb21faWQiOiI2MThiNTVkOGJlNmMzYzBiMzUxNTBhYmQiLCJ1c2VyX2lkIjoiZmUwOGQyYjQtM2Y4ZS00OTliLThlNzctNTkyYWQ4ZDQwOWRmIiwiZXhwIjoxNjgxMTk1NzM1LCJqdGkiOiI0OTk2NDQxMi0zYjlkLTRjZDQtOTg1YS0xODQxZTliNWJiOWEiLCJpYXQiOjE2ODExMDkzMzUsImlzcyI6IjYxOGI1NTkwYmU2YzNjMGIzNTE1MGFiOCIsIm5iZiI6MTY4MTEwOTMzNSwic3ViIjoiYXBpIn0.6GeiCmweoCAUv1xLD6icsSkSi1dixMS3YZMGYFrtNos", userName: userName))) }, child: const Padding( padding: EdgeInsets.symmetric(vertical: 20, horizontal: 20), child: Text( 'Join as HLS Viewer', style: TextStyle(fontSize: 20), ), ), ), ], ), ), ); }
Implement meeting page
You can check the below snippet to create a widget as the user interface to show the video tiles of local and remote peers. HMSUpdateListener
plays a significant role in rendering video or displaying any 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 MeetingPage extends StatefulWidget { final String authToken; final String userName; const MeetingPage( {super.key, required this.authToken, required this.userName}); State<MeetingPage> createState() => _MeetingPageState(); } class _MeetingPageState extends State<MeetingPage> implements HMSUpdateListener { //SDK late HMSSDK hmsSDK; // Variables required for rendering video and peer info bool _isHLSRunning = false, _isLoading = false; HMSPeer? _localPeer, _remotePeer; HMSVideoTrack? _localPeerVideoTrack, _remotePeerVideoTrack; // Initialize variables and join room void initState() { super.initState(); initHMSSDK(); } void initHMSSDK() async { hmsSDK = HMSSDK(); await hmsSDK.build(); // ensure to await while invoking the `build` method hmsSDK.addUpdateListener(listener: this); _hmsSDK.join( config: HMSConfig(authToken: widget.authToken, userName: widget.userName)); } // Clear all variables void dispose() { _remotePeer = null; _remotePeerVideoTrack = null; _localPeer = null; _localPeerVideoTrack = null; 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.
// Called when peer joined the room - get current state of room by using HMSRoom obj void onJoin({required HMSRoom room}) { room.peers?.forEach((peer) { if (peer.isLocal) { _localPeer = peer; if (peer.videoTrack != null) { _localPeerVideoTrack = peer.videoTrack; } if (mounted) { WidgetsBinding.instance.addPostFrameCallback((_) { setState(() {}); }); } } }); } // Called when there's a peer update - use to update local & remote peer variables void onPeerUpdate({required HMSPeer peer, required HMSPeerUpdate update}) { if (update == HMSPeerUpdate.networkQualityUpdated) { return; } if (update == HMSPeerUpdate.peerJoined) { if (!peer.isLocal && !peer.role.name.contains("recorder")) { if (mounted) { WidgetsBinding.instance.addPostFrameCallback((_) { setState(() { _remotePeer = peer; }); }); } } } else if (update == HMSPeerUpdate.peerLeft) { if (!peer.isLocal && !peer.role.name.contains("recorder")) { if (mounted) { setState(() { _remotePeer = null; }); } } else { if (mounted) { setState(() { _localPeer = null; }); } } } }
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.
// Called when there's a track update - use to update local & remote track variables void onTrackUpdate( {required HMSTrack track, required HMSTrackUpdate trackUpdate, required HMSPeer peer}) { if (track.kind == HMSTrackKind.kHMSTrackKindVideo) { if (trackUpdate == HMSTrackUpdate.trackRemoved) { if (peer.isLocal) { if (mounted) { setState(() { _localPeerVideoTrack = null; }); } } else if (!peer.role.name.contains("recorder")) { if (mounted) { setState(() { _remotePeerVideoTrack = null; }); } } return; } if (peer.isLocal) { if (mounted) { setState(() { _localPeerVideoTrack = track as HMSVideoTrack; }); } } else if (!peer.role.name.contains("recorder")) { if (mounted) { setState(() { _remotePeerVideoTrack = track as HMSVideoTrack; }); } } } }
Listen to room updates
100ms SDK also sends updates to the application about any change in the roomState
i.e. whether peerCount
is updated, HLS Stream
is started or ended, Recording
is started or ended etc.
Our application must listen to the corresponding updates in onRoomUpdate
. Check the Update Listeners documentation to understand the types of updates emitted by the SDK for room updates.
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 if (update == HMSRoomUpdate.hlsStreamingStateUpdated) { if (room.hmshlsStreamingState?.running ?? false) { _isHLSRunning = true; } else { _isHLSRunning = false; } _isLoading = false; setState(() {}); } }
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.
// More callbacks - no need to implement for quickstart void onAudioDeviceChanged( {HMSAudioDevice? currentAudioDevice, List<HMSAudioDevice>? availableAudioDevice}) {} void onSessionStoreAvailable( {HMSSessionStore? hmsSessionStore}) {} void onChangeTrackStateRequest( {required HMSTrackChangeRequest hmsTrackChangeRequest}) {} void onHMSError({required HMSException error}) {} void onMessage({required HMSMessage message}) {} void onReconnected() {} void onReconnecting() {} void onRemovedFromRoom( {required HMSPeerRemovedFromPeer hmsPeerRemovedFromPeer}) {} void onRoleChangeRequest({required HMSRoleChangeRequest roleChangeRequest}) {} void onUpdateSpeakers({required List<HMSSpeaker> updateSpeakers}) {}
Render video in a tile
We had initialized the HMSUpdateListener
class in the Implement meeting page section; now, we can use the same to render video tracks in a tile.
To display a video track, first get the HMSVideoTrack
& pass it on to HMSVideoView
.
Ensure to add the HMSVideoView
to your app's Widget tree. Check the render video guide for more information.
// Widget to render a single video tile Widget peerTile(Key key, HMSVideoTrack? videoTrack, HMSPeer? peer) { return Container( key: key, child: (videoTrack != null && !(videoTrack.isMute)) // Actual widget to render video ? HMSVideoView( track: videoTrack, ) : Center( child: Container( decoration: BoxDecoration( color: Colors.blue.withAlpha(4), shape: BoxShape.circle, boxShadow: const [ BoxShadow( color: Colors.blue, blurRadius: 20.0, spreadRadius: 5.0, ), ], ), child: Text( peer?.name.substring(0, 1) ?? "D", style: const TextStyle( color: Colors.white, fontSize: 24, fontWeight: FontWeight.w600), ), ), ), ); }
Render video tiles for remote peer
This section will help you build the user interface that renders the video tracks of local and remote peer in a grid.
Add this in MeetingPage
class.For more info about implementation check the complete code above
// Widget to render grid of peer tiles and a end button Widget build(BuildContext context) { return WillPopScope( // Used to call "leave room" upon clicking back button [in android] onWillPop: () async { hmsSDK.leave(); Navigator.pop(context); return true; }, child: SafeArea( child: Scaffold( backgroundColor: Colors.black, body: Stack( children: [ // Grid of peer tiles Container( height: MediaQuery.of(context).size.height, child: GridView( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( mainAxisExtent: (remotePeerVideoTrack == null) ? MediaQuery.of(context).size.height : MediaQuery.of(context).size.height / 2, crossAxisCount: 1), children: [ if (remotePeerVideoTrack != null && remotePeer != null) peerTile( Key(remotePeerVideoTrack?.trackId ?? "" "mainVideo"), remotePeerVideoTrack, remotePeer), peerTile( Key(localPeerVideoTrack?.trackId ?? "" "mainVideo"), localPeerVideoTrack, localPeer) ], ), ), // End button to leave the room Align( alignment: Alignment.bottomCenter, child: RawMaterialButton( onPressed: () { hmsSDK.leave(); Navigator.pop(context); }, elevation: 2.0, fillColor: Colors.red, padding: const EdgeInsets.all(15.0), shape: const CircleBorder(), child: const Icon( Icons.call_end, size: 25.0, color: Colors.white, ), ), ), ], ), ), ), ); }
Implement HLS Viewer page
You can check the below snippet to create a widget as the user interface to show the HLS stream. HMSUpdateListener
plays a significant role in rendering stream or displaying any information regarding the room.
class HLSViewerPage extends StatefulWidget { final String authToken; final String userName; const HLSViewerPage( {super.key, required this.authToken, required this.userName}); State<HLSViewerPage> createState() => _HLSViewerPageState(); } class _HLSViewerPageState extends State<HLSViewerPage> implements HMSUpdateListener { late HMSSDK _hmsSDK; VideoPlayerController? _controller; late Future<void> _initializeVideoPlayerFuture; 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: widget.authToken, userName: widget.userName)); } void dispose() { _controller?.dispose(); _controller = null; super.dispose(); } }
Now in the same class we will override the HMSUpdateListener
methods to listen to updates.
Listen to room updates
We will listen to hlsStreamingStateUpdated
room update or onJoin
callback to get the update regarding HLS playback.
void onJoin({required HMSRoom room}) { if (room.hmshlsStreamingState?.running ?? false) { if (room.hmshlsStreamingState?.variants[0]?.hlsStreamUrl != null) { _controller = VideoPlayerController.network( room.hmshlsStreamingState!.variants[0]!.hlsStreamUrl!, ); _initializeVideoPlayerFuture = _controller!.initialize(); _controller!.play(); _controller!.setLooping(true); } } setState(() {}); } 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 if (update == HMSRoomUpdate.hlsStreamingStateUpdated) { if (room.hmshlsStreamingState?.running ?? false) { if (room.hmshlsStreamingState?.variants[0]?.hlsStreamUrl != null) { _controller = VideoPlayerController.network( room.hmshlsStreamingState!.variants[0]!.hlsStreamUrl!, ); _initializeVideoPlayerFuture = _controller!.initialize(); _controller!.play(); _controller!.setLooping(true); } } else { _controller?.dispose(); _controller = null; } setState(() {}); } }
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 onPeerUpdate({required HMSPeer peer, required HMSPeerUpdate update}) {} void onTrackUpdate( {required HMSTrack track, required HMSTrackUpdate trackUpdate, required HMSPeer peer}) {} 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 onSessionStoreAvailable( {HMSSessionStore? hmsSessionStore}) { // Get notified when the Session Store is available for usage. Read more about Session Store here: https://www.100ms.live/docs/flutter/v2/how-to-guides/interact-with-room/room/session-store } 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 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 }
Display HLS Stream
To display HLS Stream we will be using video_player
package. To display the HLS stream we need m3u8 URL, which we
are fetching above in onRoomUpdate
.
FutureBuilder( future: _initializeVideoPlayerFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { return Center( child: Transform.scale( scaleX: 1.1, scaleY: 1.3, child: AspectRatio( aspectRatio: _controller!.value.aspectRatio, child: VideoPlayer(_controller!), ), ), ); } else { return const Center( child: CircularProgressIndicator(), ); } }, )
To know more about how to modify the resolution of stream or the view checkout the docs here
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.
HLS Streaming blog
Checkout the HLS Streaming blog 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.