Skip to main content
Version: 5.x.x

Display Video - Flutter

In this guide, we will take a look at how to render the participant's audio and video on the screen.

Rendering Participant​

The two steps are involve to achieve this process.

  1. Get Mic and Webcam Status
  2. Rendering Video

1. Get Mic and Webcam Status​

We must determine whether the participant's audio or video is on or off before rendering him or her. Hence, if the webcam is not turned on, we will begin by rendering the participant's frames with their name in them; otherwise, we will render the video.

Step 1: Let's get every participants from Room by listening to the participantJoined as well as participantLeft event.

On Android, the SDK does not ask for camera and microphone permission, so the screen asks with VideoSDK.requestPermissions() before it joins. join() and leave() return a Future that throws a VideoSDKError when they fail, so the screen awaits them inside try / catch. When the screen closes, it leaves the room. The room releases itself after Events.roomLeft.

room_screen.dart
import 'dart:async' show unawaited;
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:videosdk/videosdk.dart';
import './participant_tile.dart';

class RoomScreen extends StatefulWidget {
final String roomId;
final String token;

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

@override
State<RoomScreen> createState() => _RoomScreenState();
}

class _RoomScreenState extends State<RoomScreen> {
late Room _room;
var micEnabled = true;
var camEnabled = true;

Map<String, Participant> participants = {};

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

setRoomEventListener();

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

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

// listening to room events
void setRoomEventListener() {
_room.on(Events.roomJoined, () {
setState(() {
participants.putIfAbsent(
_room.localParticipant.id, () => _room.localParticipant);
});
});

_room.on(
Events.participantJoined,
(Participant participant) {
setState(
() => participants.putIfAbsent(participant.id, () => participant),
);
},
);

_room.on(Events.participantLeft, (Participant participant, LeaveReason reason) {
if (participants.containsKey(participant.id)) {
setState(
() => participants.remove(participant.id),
);
}
});

_room.on(Events.roomLeft, () {
participants.clear();
Navigator.popUntil(context, ModalRoute.withName('/'));
});
}

// leave the room when the back button closes the screen
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(
onPopInvokedWithResult: (didPop, result) {
if (didPop) unawaited(leaveRoom());
},
child: Scaffold(
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [],
),
),
),
);
}
}

Step 2: With participant list ready, let render a list of participants with create a straightforward box with each person's name.

ParticipantTile takes the Room as well as the Participant, because the room delivers the stream events that the tile uses in the next step.

room_screen.dart
import 'dart:async' show unawaited;
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:videosdk/videosdk.dart';
import './participant_tile.dart';

class RoomScreen extends StatefulWidget {
...
}

class _RoomScreenState extends State<RoomScreen> {
late Room _room;
var micEnabled = true;
var camEnabled = true;

Map<String, Participant> participants = {};

@override
void initState() {
// create room
...
}

Future<void> joinRoom() async {
...
}

// listening to room events
void setRoomEventListener() {
...
}

// leave the room when the back button closes the screen
Future<void> leaveRoom() async {
...
}


@override
Widget build(BuildContext context) {
return PopScope(
onPopInvokedWithResult: (didPop, result) {
if (didPop) unawaited(leaveRoom());
},
child: Scaffold(
body: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
//render all participant
Expanded(
child: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 1,
),
itemBuilder: (context, index) {
return ParticipantTile(
key: Key(participants.values.elementAt(index).id),
room: _room,
participant: participants.values.elementAt(index));
},
itemCount: participants.length,
),
),
],
),
),
),
);
}
}
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> {

@override
void initState() {
super.initState();
}

@override
Widget build(BuildContext context) {
return Container(
color: Colors.black54,
child: Stack(
children: [
Positioned(
top: 10,
left: 10,
child: Text(
"Name: ${widget.participant.displayName}",
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontSize: 16.0,
),
),
),
],
),
);
}
}

Step 3: To display the status of each participant's microphone and webcam in the list, we will create the local state of audio and video streams of the participant from the streams property and also add the event listener to get notified on status change.

The streamEnabled and streamDisabled events are delivered on the Room, for every participant's streams. stream.participantId is the id of the participant the stream belongs to, so each tile keeps only its own participant's streams. The room outlives the tile, so the tile removes its handlers with room.off() when it goes away.

Here's a code snippet of rendering mic and webcam status:

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> {
//declare stream state
RTCStream? videoStream;
RTCStream? audioStream;

@override
void initState() {
super.initState();
// initial streams of the participant
for (final RTCStream stream in widget.participant.streams.values) {
if (stream.kind == MediaType.VIDEO) {
videoStream = stream;
} else if (stream.kind == MediaType.AUDIO) {
audioStream = stream;
}
}
_initStreamListeners();
}

// Stream events are delivered on the room.
// 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);
}

void _onStreamEnabled(RTCStream stream) {
if (stream.participantId != widget.participant.id) return;
if (stream.kind == MediaType.VIDEO) {
setState(() => videoStream = stream);
} else if (stream.kind == MediaType.AUDIO) {
setState(() => audioStream = stream);
}
}

void _onStreamDisabled(RTCStream stream) {
if (stream.participantId != widget.participant.id) return;
if (stream.kind == MediaType.VIDEO) {
setState(() => videoStream = null);
} else if (stream.kind == MediaType.AUDIO) {
setState(() => audioStream = 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 Container(
color: Colors.black54,
child: Stack(
children: [
Positioned(
top: 10,
left: 10,
//Update the text
child: Text(
"Name : ${widget.participant.displayName}" +
"\nMic :${audioStream == null ? "Off" : "On"}" +
"\nCam :${videoStream == null ? "Off" : "On"}",
style: const TextStyle(
color: Colors.white,
fontSize: 16.0,
),
),
),
],
),
);
}
}

2. Rendering Video​

The status of the webcam and mic is now displayed. If the webcam is turned on, we will use the videoStream we declared earlier and display the participant's video.

Pass the stream's renderer to RTCVideoView. The SDK creates the renderer, so you do not need to initialize or dispose it.

import 'package:flutter/material.dart';
import 'package:videosdk/videosdk.dart';

class ParticipantTile extends StatefulWidget {
final Room room;
final Participant participant;
...
}

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

@override
void initState() {
...
}

void _initStreamListeners() {
...
}

// _onStreamEnabled(), _onStreamDisabled() and dispose() from Step 3

@override
Widget build(BuildContext context) {
return Container(
color: Colors.black54,
child: Stack(
children: [
if (videoStream != null)
RTCVideoView(
videoStream?.renderer as RTCVideoRenderer,
objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover,
),
Positioned(
top: 10,
left: 10,
child: Text(
"Name : ${widget.participant.displayName}" +
"\nWebcam :${videoStream == null ? "Off" : "On"}" +
"\nMic :${audioStream == null ? "Off" : "On"}",
style: const TextStyle(
color: Colors.black,
fontSize: 16.0,
),
),
),
],
),
);
}
}
caution

You don't need to render audio separately. VideoSDK plays the audio of the other participants automatically, so RTCVideoView is only needed for video.

2.1 Mirror Local Video View​

If you wish to show the mirror view of the local participant, you can pass boolean value to mirror property of RTCVideoView. The tile below mirrors only the local participant's video.

import 'package:flutter/material.dart';
import 'package:videosdk/videosdk.dart';

class ParticipantTile extends StatefulWidget {
final Room room;
final Participant participant;
...
}

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

@override
void initState() {
...
}

void _initStreamListeners() {
...
}

// _onStreamEnabled(), _onStreamDisabled() and dispose() from Step 3

@override
Widget build(BuildContext context) {
return Container(
color: Colors.black54,
child: Stack(
children: [
if (videoStream != null)
RTCVideoView(
videoStream?.renderer as RTCVideoRenderer,
objectFit: RTCVideoViewObjectFit.RTCVideoViewObjectFitCover,
mirror: widget.participant.isLocal,
),
],
),
);
}
}

To change the mirroring while the tile is on screen, call setMirror() on the renderer. It applies to every RTCVideoView that shows that renderer, and it overrides the view's mirror: parameter.

// Show the local video unmirrored, for example while sharing a whiteboard.
videoStream?.renderer?.setMirror(false);
Sample of mirror view video​

mirror view

API Reference​

The API references for all the methods and events utilised in this guide are provided below.

Got a Question? Ask us on discord