Skip to main content
Version: 5.x.x

Quick Start for Http Live Streaming(HLS) in Flutter

VideoSDK enables you to embed the livestreaming experience into your Flutter application in minutes.

In this quickstart, we are going to explore interactive livestreaming feature of Video SDK. We will go through step by step guide of integrating interactive livestreaming with Flutter Video SDK.

info

For low-latency interactive live streaming (under 100ms), follow this documentation.

Prerequisites​

Before proceeding, ensure that your development environment meets the following requirements:

  • Video SDK Developer Account (Not having one, follow Video SDK Dashboard)
  • The basic understanding of Flutter.
  • Flutter Video SDK
  • Have Flutter 3.41 or later installed on your device.
important

One should have a VideoSDK account to generate token. Visit VideoSDK dashboard to generate token

Getting Started with the Code!​

Follow the steps to create the environment necessary to add video calls into your app. Also you can find the code sample for quickstart here (this example uses an older SDK version).

info

Important Changes Flutter SDK in Version v1.3.0

  • The following modes have been deprecated:
    • CONFERENCE has been replaced by SEND_AND_RECV
    • VIEWER has been replaced by SIGNALLING_ONLY

Please update your implementation to use the new modes.

⚠️ Compatibility Notice:
To ensure a seamless meeting experience, all participants must use the same SDK version.
Do not mix version v1.3.0 + with older versions, as it may cause significant conflicts.

Create a new Flutter project.​

Create a new Flutter App using the below command.

$ flutter create videosdk_flutter_quickstart_hls

Install Video SDK​

Install the VideoSDK using the below-mentioned flutter command. Make sure you are in your flutter app directory before you run this command.

$ flutter pub add videosdk:^5.0.0

Then pin permission_handler, which the SDK uses to ask for camera and microphone permission, to version 12. Version 13 brings in permission_handler_android 14, which builds only with Android Gradle Plugin 9, so an app created with Flutter 3.41 fails to build for Android.

$ flutter pub add permission_handler:^12.0.1

Run the following command to add the http library, which is used to make the network call that generates a roomId.

$ flutter pub add http

Run the following command to add the flutter_vlc_player library, which is used to play the HLS stream in your app.

$ flutter pub add flutter_vlc_player

The Flutter SDK runs on Android and iOS. Web, macOS and Windows are not supported.

Structure of the project​

Your project structure should look like this.

Project Structure
root
├── android
├── ios
├── lib
├── api_call.dart
├── hls_screen.dart
├── hls_speaker_view.dart
├── hls_viewer_view.dart
├── join_screen.dart
├── livestream_player.dart
├── main.dart
├── room_controls.dart
├── participant_tile.dart

We are going to create following flutter widgets JoinScreen, HLSScreen, HLSSpeakerView, RoomControls, ParticipantTile, HLSViewerView, LivestreamPlayer for the quickstart app which will cover both speaker and viewer part of the app.

App Structure​

App widget will contain JoinScreen and HLSScreen screens. HLSScreen will render the HLSSpeakerView or HLSViewerView based on the participants mode. HLSSpeakerView will have RoomControls and ParticipantTile

VideoSDK Flutter HLS Quick Start Architecture

Configure Project​

For Android​

  • Update the /android/app/src/main/AndroidManifest.xml for the permissions we will be using to implement the audio and video features.
AndroidManifest.xml
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.INTERNET"/>
  • Also you will need to set your build settings to Java 17, because recent versions of the Android Gradle Plugin and the SDK's Android dependencies require it. Just add this to your app-level /android/app/build.gradle.kts.
android/app/build.gradle.kts
android {
//...
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
  • If necessary, in the same build.gradle.kts you will need to increase minSdk of defaultConfig up to 24 (currently default Flutter generator set it to 24).

  • The SDK ships its native libraries for arm64-v8a and armeabi-v7a, so it does not run on x86 and x86_64 emulators. Test on an ARM device or on an emulator with an arm64-v8a system image. To build your app for arm64-v8a and armeabi-v7a only, add an ABI filter to defaultConfig in the same build.gradle.kts.

android/app/build.gradle.kts
android {
defaultConfig {
//...
ndk {
abiFilters += listOf("arm64-v8a", "armeabi-v7a")
}
}
}
  • Then add this line to /android/gradle.properties. Without it, Flutter adds its own ABIs to the filter and yours has no effect.
android/gradle.properties
disable-abi-filtering=true
  • For release builds, keep the SDK's classes. Flutter shrinks every Android release build with R8, which removes or renames the videosdk.org classes that the SDK loads by name. Add these rules to /android/app/proguard-rules.pro, and create the file if it does not exist. Keep the -dontwarn line, because the keep rule alone fails the build.
android/app/proguard-rules.pro
-keep class videosdk.org.** { *; }
-dontwarn videosdk.org.**
note

If your project still uses the Groovy DSL (android/app/build.gradle), the same property assignments work there too. The older compileSdkVersion and minSdkVersion method syntax is deprecated as of Android Gradle Plugin 9 and will warn during the build.

For iOS​

  • Add the following entries which allow your app to access the camera and microphone to your /ios/Runner/Info.plist file :
<key>NSCameraUsageDescription</key>
<string>$(PRODUCT_NAME) Camera Usage!</string>
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) Microphone Usage!</string>
  • The SDK's iOS plugin ships as a Swift Package, so it needs Flutter 3.41 or later with Swift Package Manager turned on. Turn it on for your project by adding config: under the existing flutter: section of your app's pubspec.yaml, or run flutter config --enable-swift-package-manager on every machine that builds your app.
pubspec.yaml
flutter:
config:
enable-swift-package-manager: true

Step 1: Get started with api_call.dart​

Before jumping to anything else, you will write a function to generate a unique room ID. You will require auth token, you can generate it using either by using videosdk-rtc-api-server-examples or generate it from the Video SDK Dashboard for development.

api_call.dart
import 'dart:convert';
import 'package:http/http.dart' as http;

//Auth token we will use to generate a room and connect to it
String token = "<Generated-from-dashboard>";

// API call to create room
Future<String> createRoomId() async {
final http.Response httpResponse = await http.post(
Uri.parse("https://api.videosdk.live/v2/rooms"),
headers: {'Authorization': token},
);

//Destructuring the roomId from the response
return json.decode(httpResponse.body)['roomId'];
}

Step 2: Creating the JoinScreen​

Let's create join_screen.dart file in lib directory and create JoinScreen StatelessWidget.

The JoinScreen will consist of:

  • Create Room Button - This button will create a new room for speaker and join it with mode SEND_AND_RECV.
  • Room ID TextField - This text field will contain the room ID, you want to join.
  • Join Room as Host Button - This button will join the room in SEND_AND_RECV mode for the speakers, which you have provided.
  • Join Room as Viewer Button - This button will join the room in SIGNALLING_ONLY mode for the viewers, which you have provided.
join_screen.dart
import 'package:flutter/material.dart';
import 'api_call.dart';
import 'hls_screen.dart';
import 'package:videosdk/videosdk.dart';

class JoinScreen extends StatelessWidget {
final _roomIdController = TextEditingController();

JoinScreen({super.key});

//Creates new Room Id and joins it in SEND_AND_RECV mode.
void onCreateButtonPressed(BuildContext context) async {
// call api to create room and navigate to HLSScreen with roomId,token and mode
await createRoomId().then((roomId) {
if (!context.mounted) return;
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => HLSScreen(
roomId: roomId,
token: token,
mode: Mode.SEND_AND_RECV,
),
),
);
});
}

//Join the provided room with given Mode and roomId
void onJoinButtonPressed(BuildContext context, Mode mode) {
// check room id is not null or invalid
// if room id is valid then navigate to HLSScreen with roomId, token and mode
String roomId = _roomIdController.text;
var re = RegExp("\\w{4}\\-\\w{4}\\-\\w{4}");
if (roomId.isNotEmpty && re.hasMatch(roomId)) {
_roomIdController.clear();
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => HLSScreen(
roomId: roomId,
token: token,
mode: mode,
),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text("Please enter valid room id"),
));
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
title: const Text('VideoSDK HLS QuickStart'),
),
body: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
//Creating a new room
ElevatedButton(
onPressed: () => onCreateButtonPressed(context),
child: const Text('Create Room'),
),
const SizedBox(height: 40),
TextField(
style: const TextStyle(color: Colors.white),
decoration: const InputDecoration(
hintText: 'Enter Room Id',
border: OutlineInputBorder(),
hintStyle: TextStyle(color: Colors.white),
),
controller: _roomIdController,
),
//Joining the room as host
ElevatedButton(
onPressed: () => onJoinButtonPressed(context, Mode.SEND_AND_RECV),
child: const Text('Join Room as Host'),
),
//Joining the room as viewer
ElevatedButton(
onPressed: () => onJoinButtonPressed(context, Mode.SIGNALLING_ONLY),
child: const Text('Join Room as Viewer'),
),
],
),
),
);
}
}
  • Update the app entry to JoinScreen from the main.dart file.
main.dart
import 'package:flutter/material.dart';
import 'join_screen.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'VideoSDK QuickStart',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: JoinScreen(),
);
}
}

Output​

Step 3: Creating the HLSScreen​

Let's create hls_screen.dart file and create HLSScreen StatefulWidget which will take the roomId, token and mode of the participant in the constructor.

We will create a new room using the createRoom method and render the HLSSpeakerView or HLSViewerView based on the passed participant mode.

The SDK does not ask for camera and microphone permission on Android, so a speaker asks with VideoSDK.requestPermissions() before joining. join(), leave() and pin() return a Future that throws a VideoSDKError when the action fails, so the screen awaits them inside try / catch. The room releases itself after it leaves, so there is nothing to clean up when the screen closes.

hls_screen.dart
import 'dart:async' show unawaited;
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:videosdk/videosdk.dart';
import './hls_speaker_view.dart';
import './hls_viewer_view.dart';

class HLSScreen extends StatefulWidget {
final String roomId;
final String token;
final Mode mode;

const HLSScreen(
{super.key,
required this.roomId,
required this.token,
required this.mode});

@override
State<HLSScreen> createState() => _HLSScreenState();
}

class _HLSScreenState extends State<HLSScreen> {
late Room _room;
bool isJoined = false;

@override
void initState() {
super.initState();
// create room when widget loads
_room = VideoSDK.createRoom(
roomId: widget.roomId,
token: widget.token,
participantName: "John Doe",
micEnabled: true,
camEnabled: true,
defaultCamera: FacingMode.FRONT, // camera used when the room starts
mode: widget.mode,
);

// setting the event listener for join and leave events
setRoomEventListener();

// Joining room
unawaited(joinRoom());
}

Future<void> joinRoom() async {
// On Android, the SDK does not ask for camera and microphone permission.
if (widget.mode == Mode.SEND_AND_RECV) {
await VideoSDK.requestPermissions();
}
try {
await _room.join();
} on VideoSDKError catch (error) {
log("Could not join the room: ${error.code} ${error.name}");
}
}

Future<void> pinLocalParticipant() async {
try {
await _room.localParticipant.pin();
} on VideoSDKError catch (error) {
log("Could not pin: ${error.code} ${error.name}");
}
}

// listening to room events
void setRoomEventListener() {
//Setting the joining flag to true when room is joined
_room.on(Events.roomJoined, () {
if (widget.mode == Mode.SEND_AND_RECV) {
unawaited(pinLocalParticipant());
}
setState(() {
isJoined = true;
});
});

//Handling navigation when room is left
_room.on(Events.roomLeft, () {
Navigator.popUntil(context, ModalRoute.withName('/'));
});
}

Future<void> leaveRoom() async {
try {
await _room.leave();
} on VideoSDKError catch (error) {
// 3054: the room has already left.
if (error.code != 3054) {
log("Could not leave the room: ${error.code} ${error.name}");
}
}
}

@override
Widget build(BuildContext context) {
return PopScope(
// leave the room when the back button closes the screen
onPopInvokedWithResult: (didPop, result) {
if (didPop) unawaited(leaveRoom());
},
child: Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
title: const Text('VideoSDK HLS QuickStart'),
),
//Showing the Speaker or Viewer View based on the mode
body: isJoined
? widget.mode == Mode.SEND_AND_RECV
? HLSSpeakerView(room: _room)
: widget.mode == Mode.SIGNALLING_ONLY
? HLSViewerView(room: _room)
: null
: const Center(
child: Text("Joining...",
style: TextStyle(color: Colors.white),
),
),
),
);
}
}

Step 4: Implementing Speaker View​

Let's create the HLSSpeakerView which will show the RoomControls and the ParticipantTile in grid.

  1. Let us start off by creating the StatefulWidget named ParticipantTile in participant_tile.dart file. It will consist of RTCVideoView which will show participant's video stream. Stream events are delivered on the Room, so the tile takes the room and keeps only the events for its own participant.
participant_tile.dart
import 'package:flutter/material.dart';
import 'package:videosdk/videosdk.dart';

class ParticipantTile extends StatefulWidget {
final Room room;
final Participant participant;
const ParticipantTile(
{super.key, required this.room, required this.participant});

@override
State<ParticipantTile> createState() => _ParticipantTileState();
}

class _ParticipantTileState extends State<ParticipantTile> {
RTCStream? videoStream;

@override
void initState() {
super.initState();
// initialze video stream for the participant if they are present
videoStream = widget.participant.streams.values
.where((stream) => stream.kind == MediaType.VIDEO)
.firstOrNull;
_initStreamListeners();
}

//Event listener for the video stream of the participant.
//stream.participantId is the id of the participant the stream belongs to.
void _initStreamListeners() {
widget.room.on(Events.streamEnabled, _onStreamEnabled);
widget.room.on(Events.streamDisabled, _onStreamDisabled);
}

bool _isVideoOfThisParticipant(RTCStream stream) =>
stream.participantId == widget.participant.id &&
stream.kind == MediaType.VIDEO;

void _onStreamEnabled(RTCStream stream) {
if (_isVideoOfThisParticipant(stream)) {
setState(() => videoStream = stream);
}
}

void _onStreamDisabled(RTCStream stream) {
if (_isVideoOfThisParticipant(stream)) {
setState(() => videoStream = null);
}
}

@override
void dispose() {
// The room outlives the tile, so remove the handlers from it.
widget.room.off(Events.streamEnabled, _onStreamEnabled);
widget.room.off(Events.streamDisabled, _onStreamDisabled);
super.dispose();
}

@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
//Rending the Video Stream of the participant
child: videoStream != null
? RTCVideoView(
videoStream?.renderer as RTCVideoRenderer,
objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover,
)
: Container(
color: Colors.grey.shade800,
child: const Center(
child: Icon(
Icons.person,
size: 100,
),
),
),
);
}
}
  1. Next let us add the StatelessWidget named RoomControls in the room_controls.dart file. This widget will accept the callback handlers for all the room control buttons and the current HLS state of the room.
room_controls.dart
import 'package:flutter/material.dart';

class RoomControls extends StatelessWidget {
final String hlsState;
final void Function() onToggleMicButtonPressed;
final void Function() onToggleCameraButtonPressed;
final void Function() onHLSButtonPressed;

const RoomControls(
{super.key,
required this.hlsState,
required this.onToggleMicButtonPressed,
required this.onToggleCameraButtonPressed,
required this.onHLSButtonPressed});

@override
Widget build(BuildContext context) {
return Wrap(
children: [
ElevatedButton(
onPressed: onToggleMicButtonPressed,
child: const Text('Toggle Mic')),
const SizedBox(width: 10),
ElevatedButton(
onPressed: onToggleCameraButtonPressed,
child: const Text('Toggle WebCam')),
const SizedBox(width: 10),
ElevatedButton(
onPressed: onHLSButtonPressed,
child: Text(hlsState == "HLS_STOPPED"
? 'Start HLS'
: hlsState == "HLS_STARTING"
? "Starting HLS"
: hlsState == "HLS_STARTED" || hlsState == "HLS_PLAYABLE"
? "Stop HLS"
: hlsState == "HLS_STOPPING"
? "Stopping HLS"
: "Start HLS")),
],
);
}
}
  1. Lets finally put all these widget in the StatefulWidget named HLSSpeakerView in hls_speaker_view.dart file. This widget will listen to the hlsStateChanged, participantJoined and participantLeft events. It will render the participants and the room controls like leave, toggle mic and webcam as well as start and stop HLS.
hls_speaker_view.dart
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:videosdk/videosdk.dart';
import './room_controls.dart';
import './participant_tile.dart';

class HLSSpeakerView extends StatefulWidget {
final Room room;
const HLSSpeakerView({super.key, required this.room});

@override
State<HLSSpeakerView> createState() => _HLSSpeakerViewState();
}

class _HLSSpeakerViewState extends State<HLSSpeakerView> {
var micEnabled = true;
var camEnabled = true;
String hlsState = "HLS_STOPPED";

Map<String, Participant> participants = {};

@override
void initState() {
super.initState();
//Setting up the event listeners and initializing the participants and hls state
setRoomEventListener();
participants.putIfAbsent(
widget.room.localParticipant.id, () => widget.room.localParticipant);
//filtering the SEND_AND_RECV participants to be shown in the grid
widget.room.participants.values.forEach((participant) {
if (participant.mode == Mode.SEND_AND_RECV) {
participants.putIfAbsent(participant.id, () => participant);
}
});
hlsState = widget.room.hlsState;
}

@override
void setState(VoidCallback fn) {
if (mounted) {
super.setState(fn);
}
}

Future<void> leave() async {
try {
await widget.room.leave();
} on VideoSDKError catch (error) {
// 3054: the room has already left.
if (error.code != 3054) {
log("Could not leave the room: ${error.code} ${error.name}");
}
}
}

Future<void> toggleMic() async {
try {
micEnabled ? await widget.room.muteMic() : await widget.room.unmuteMic();
micEnabled = !micEnabled;
} on VideoSDKError catch (error) {
log("Could not toggle the mic: ${error.code} ${error.name}");
}
}

Future<void> toggleCamera() async {
try {
camEnabled ? await widget.room.disableCam() : await widget.room.enableCam();
camEnabled = !camEnabled;
} on VideoSDKError catch (error) {
log("Could not toggle the camera: ${error.code} ${error.name}");
}
}

//HLS handler which will start and stop HLS
Future<void> toggleHls() async {
try {
if (hlsState == "HLS_STOPPED") {
await widget.room.startHLS(
config: const HLSConfig(
layout: ConfigLayout(
type: ConfigLayoutGrid.SPOTLIGHT,
priority: ConfigLayoutPriority.PIN,
gridSize: 20,
),
mode: ConfigMode.VIDEO_AND_AUDIO,
theme: ConfigTheme.DARK,
quality: ConfigQuality.HIGH,
orientation: ConfigOrientation.PORTRAIT,
),
);
} else if (hlsState == "HLS_STARTED" || hlsState == "HLS_PLAYABLE") {
await widget.room.stopHLS();
}
} on VideoSDKError catch (error) {
log("Could not start or stop HLS: ${error.code} ${error.name}");
}
}

@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Row(
children: [
Expanded(
child: Text(
widget.room.id,
style: const TextStyle(color: Colors.white),
)),
ElevatedButton(
onPressed: () => {
Clipboard.setData(ClipboardData(text: widget.room.id)),
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text("Room Id Coppied"),
))
},
child: const Text("Copy Room Id"),
),
const SizedBox(width: 10),
ElevatedButton(
onPressed: leave,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
child: const Text("Leave"),
)
],
),
const SizedBox(
height: 20,
),
//Showing the current HLS state
Text(
"Current HLS State: ${hlsState == "HLS_STARTED" || hlsState == "HLS_PLAYABLE" ? "Livestream is Started" : hlsState == "HLS_STARTING" ? "Livestream is starting" : hlsState == "HLS_STOPPING" ? "Livestream is stopping" : "Livestream is stopped"}",
style: const TextStyle(color: Colors.white),
),
//render all participants in the room
Expanded(
child: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 1,
),
itemBuilder: (context, index) {
return ParticipantTile(
key: Key(participants.values.elementAt(index).id),
room: widget.room,
participant: participants.values.elementAt(index));
},
itemCount: participants.length,
),
),
//Rending the room Controls
RoomControls(
hlsState: hlsState,
onToggleMicButtonPressed: toggleMic,
onToggleCameraButtonPressed: toggleCamera,
onHLSButtonPressed: toggleHls,
),
],
),
);
}

// listening to room events for participants join, left and hls state changes
void setRoomEventListener() {
widget.room.on(
Events.participantJoined,
(Participant participant) {
//Adding only SEND_AND_RECV participant to show in grid
if (participant.mode == Mode.SEND_AND_RECV) {
setState(
() => participants.putIfAbsent(participant.id, () => participant),
);
}
},
);

widget.room.on(Events.participantLeft,
(Participant participant, LeaveReason reason) {
if (participants.containsKey(participant.id)) {
setState(
() => participants.remove(participant.id),
);
}
});
widget.room.on(
Events.hlsStateChanged,
(String state, HLSUrl? hlsUrl) {
setState(
() => hlsState = state,
);
},
);
}
}

Output of Speaker View​

Step 5: Implementing the Viewer View​

Let's create StatefulWidget named HLSViewerView in the hls_viewer_view.dart file.

HLSViewerView will listen to the hlsStateChanged event and if the HLS is started, it will show the livestream using the flutter_vlc_player library.

hls_viewer_view.dart
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:videosdk/videosdk.dart';
import './livestream_player.dart';

class HLSViewerView extends StatefulWidget {
final Room room;
const HLSViewerView({super.key, required this.room});

@override
State<HLSViewerView> createState() => _HLSViewerViewState();
}

class _HLSViewerViewState extends State<HLSViewerView> {
String hlsState = "HLS_STOPPED";
String? playbackHlsUrl;

@override
void initState() {
super.initState();
//initialize the room's hls state and hls's playbackHlsUrl
hlsState = widget.room.hlsState;
playbackHlsUrl = widget.room.hlsUrl?.playbackHlsUrl;
//add the hlsStateChanged event listener
setRoomEventListener();
}

Future<void> leave() async {
try {
await widget.room.leave();
} on VideoSDKError catch (error) {
// 3054: the room has already left.
if (error.code != 3054) {
log("Could not leave the room: ${error.code} ${error.name}");
}
}
}

@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
Expanded(
child: Text(widget.room.id,
style: const TextStyle(color: Colors.white))),
ElevatedButton(
onPressed: () =>
{Clipboard.setData(ClipboardData(text: widget.room.id))},
child: const Text("Copy Room Id"),
),
const SizedBox(width: 10),
ElevatedButton(
onPressed: leave,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
child: const Text("Leave"),
)
],
),
),
//Show the livestream player if the playbackHlsUrl is present
playbackHlsUrl != null
? LivestreamPlayer(playbackHlsUrl: playbackHlsUrl!)
: const Text("Host has not started the HLS",
style: TextStyle(color: Colors.white)),
],
),
);
}

void setRoomEventListener() {
// listening to hlsStateChanged events and updating the hlsState and playbackHlsUrl
widget.room.on(
Events.hlsStateChanged,
(String state, HLSUrl? hlsUrl) {
if (mounted) {
setState(() {
hlsState = state;
if (state == "HLS_PLAYABLE") {
playbackHlsUrl = hlsUrl?.playbackHlsUrl;
} else if (state == "HLS_STOPPED") {
playbackHlsUrl = null;
}
});
}
},
);
}
}
  • Let us use the flutter_vlc_player library to play the HLS stream on the Viewer side. For these we will create a StatefulWidget named LivestreamPlayer in the livestream_player.dart file.
livestream_player.dart
import 'package:flutter/material.dart';
import 'package:flutter_vlc_player/flutter_vlc_player.dart';

class LivestreamPlayer extends StatefulWidget {
final String playbackHlsUrl;
const LivestreamPlayer({
Key? key,
required this.playbackHlsUrl,
}) : super(key: key);

@override
LivestreamPlayerState createState() => LivestreamPlayerState();
}

class LivestreamPlayerState extends State<LivestreamPlayer>
with AutomaticKeepAliveClientMixin {
late VlcPlayerController _controller;

@override
bool get wantKeepAlive => true;

@override
void initState() {
super.initState();
//initializing the player
_controller = VlcPlayerController.network(widget.playbackHlsUrl,
options: VlcPlayerOptions());
}

@override
void dispose() async {
_controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
super.build(context);
//rendering the VlcPlayer
return VlcPlayer(
controller: _controller,
aspectRatio: 9 / 16,
placeholder: const Center(child: CircularProgressIndicator()),
);
}
}

Output of Viewer View​

Run and Test​

The app is all set to test. Make sure to update the token in api_call.dart

Your app should look like this after the implementation.

Speaker View Output​

Viewer View Output​


caution

Run the app on an ARM Android device or an arm64-v8a emulator, and on an iPhone, an iPad or the iOS Simulator on a Mac with Apple silicon. x86 and x86_64 Android emulators and the iOS Simulator on an Intel Mac are not supported. See Known Issues.

tip

You can checkout the complete quick start example here (this example uses an older SDK version).

Got a Question? Ask us on discord