Manage Participants - Flutter
1. Local Participant (self)
The local participant is you: it sends your video and audio. it contains information about local participant such as displayName, id, videoQuality and streams Map.
You can access localParticipant from the room object. It exists from createRoom(). Its id and name are the ones the room assigned once the roomJoined event fires.
Participant object properties
| Property Name | Type | Description |
|---|---|---|
| id | String | Unique id of the participant. |
| displayName | String | The name you define during the room initialization. |
| isLocal | bool | Indicates the participant is local or not. |
| videoQuality | VideoQuality? | The quality you requested with setQuality(), or null. |
| streams | Map<String, RTCStream> | Returns Video & Audio Streams, keyed by stream id. |
Stream Object properties
| Property Name | Type | Description |
|---|---|---|
| id | String | Unique id |
| kind | MediaType | Stream Kind: MediaType.AUDIO, MediaType.VIDEO, MediaType.SHARE or MediaType.SHARE_AUDIO. |
| participantId | String | The id of the participant the stream belongs to. |
| isLocal | bool | Whether the stream belongs to the local participant. |
| renderer | RTCVideoRenderer? | Pass it to RTCVideoView to show the video. It is null for audio streams. |
2. Other Participants
Other participants Map is used to get all the other participants in the room at any given time. room.participants holds the remote participants only. You are room.localParticipant.
Other participants Map contains same properties as LocalParticipant.
ParticipantTile
Stream events are delivered on the room, so the tile takes the Room and listens with room.on(). stream.participantId tells the tile whether a stream belongs to its participant. You can also listen with participant.on(), which delivers only that participant's streamEnabled and streamDisabled.
import 'package:flutter/material.dart';
import 'package:videosdk/videosdk.dart';
import '../../utils/toast.dart';
class ParticipantTile extends StatefulWidget {
final Room room;
final Participant participant;
final bool isLocalParticipant;
const ParticipantTile({
Key? key,
required this.room,
required this.participant,
this.isLocalParticipant = false,
}): super(key: key);
@override
State<ParticipantTile> createState() => _ParticipantTileState();
}
class _ParticipantTileState extends State<ParticipantTile> {
RTCStream? shareStream;
RTCStream? videoStream;
RTCStream? audioStream;
String? quality;
@override
void initState() {
super.initState();
// Streams the participant already has when the tile is built
widget.participant.streams.values.forEach(_setStream);
_initStreamListeners();
}
@override
void dispose() {
// The room outlives the tile, so take the handlers off it
widget.room.off(Events.streamEnabled, _onStreamEnabled);
widget.room.off(Events.streamDisabled, _onStreamDisabled);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.all(4.0),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
border: Border.all(color: Colors.white38),
),
child: AspectRatio(
aspectRatio: 1,
child: Padding(
padding: const EdgeInsets.all(4.0),
child: Stack(
children: [
videoStream != null
? RTCVideoView(
videoStream?.renderer as RTCVideoRenderer,
objectFit:
RTCVideoViewObjectFit.RTCVideoViewObjectFitCover,
)
: const Center(
child: Icon(
Icons.person,
size: 180.0,
color: Color.fromARGB(140, 255, 255, 255),
),
),
Positioned(
bottom: 0,
left: 0,
child: FittedBox(
fit: BoxFit.scaleDown,
child: Container(
padding: const EdgeInsets.all(2.0),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface.withValues(alpha: 0.2),
border: Border.all(color: Colors.white24),
borderRadius: BorderRadius.circular(4.0),
),
child: Text(
widget.participant.displayName,
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontSize: 10.0,
),
),
),
),
),
Positioned(
top: 0,
left: 0,
child: InkWell(
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: audioStream != null
? Theme.of(context).colorScheme.surface
: Colors.red,
),
child: Icon(
audioStream != null ? Icons.mic : Icons.mic_off,
size: 16,
),
),
onTap: widget.isLocalParticipant || widget.participant.isAgent()
? null
: () => _toggleMic(),
),
),
Positioned(
top: 0,
right: 0,
child: InkWell(
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: videoStream != null
? Theme.of(context).colorScheme.surface
: Colors.red,
),
child: Icon(
videoStream != null
? Icons.videocam
: Icons.videocam_off,
size: 16,
),
),
onTap: widget.isLocalParticipant || widget.participant.isAgent()
? null
: () => _toggleCam(),
),
),
if (!widget.isLocalParticipant)
Positioned(
bottom: 0,
right: 0,
child: InkWell(
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: Colors.red,
),
child: const Icon(
Icons.logout,
size: 16,
),
),
onTap: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text("Are you sure ?"),
actions: [
TextButton(
child: const Text("Yes"),
onPressed: () async {
Navigator.of(context).pop();
try {
await widget.participant.remove();
toastMsg("Participant removed");
} on VideoSDKError catch (error) {
toastMsg("Could not remove the participant: ${error.name}");
}
},
),
TextButton(
child: const Text("No"),
onPressed: () => Navigator.of(context).pop(),
)
],
));
},
),
),
],
),
),
),
);
}
// Turns the remote participant's mic off, or asks them to turn it on
Future<void> _toggleMic() async {
try {
if (audioStream != null) {
await widget.participant.disableMic();
} else {
await widget.participant.enableMic();
}
} on VideoSDKError catch (error) {
toastMsg("Could not change the mic: ${error.name}");
}
}
// Turns the remote participant's camera off, or asks them to turn it on
Future<void> _toggleCam() async {
try {
if (videoStream != null) {
await widget.participant.disableCam();
} else {
await widget.participant.enableCam();
}
} on VideoSDKError catch (error) {
toastMsg("Could not change the camera: ${error.name}");
}
}
_initStreamListeners() {
widget.room.on(Events.streamEnabled, _onStreamEnabled);
widget.room.on(Events.streamDisabled, _onStreamDisabled);
}
void _setStream(RTCStream stream) {
if (stream.kind == MediaType.VIDEO) {
videoStream = stream;
} else if (stream.kind == MediaType.AUDIO) {
audioStream = stream;
} else if (stream.kind == MediaType.SHARE) {
shareStream = stream;
}
}
// Called for every participant's streams; keep only this participant's
void _onStreamEnabled(RTCStream stream) {
if (stream.participantId != widget.participant.id) return;
setState(() => _setStream(stream));
}
void _onStreamDisabled(RTCStream stream) {
if (stream.participantId != widget.participant.id) return;
setState(() {
if (stream.kind == MediaType.VIDEO && videoStream?.id == stream.id) {
videoStream = null;
} else if (stream.kind == MediaType.AUDIO && audioStream?.id == stream.id) {
audioStream = null;
} else if (stream.kind == MediaType.SHARE && shareStream?.id == stream.id) {
shareStream = null;
}
});
}
}
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:videosdk/videosdk.dart';
import 'participant_tile.dart';
class ParticipantGridView extends StatefulWidget {
final Room room;
const ParticipantGridView({
Key? key,
required this.room,
}) : super(key: key);
@override
State<ParticipantGridView> createState() => _ParticipantGridViewState();
}
class _ParticipantGridViewState extends State<ParticipantGridView> {
String? activeSpeakerId;
Map<String, Participant> participants = {};
@override
void initState() {
super.initState();
// Build this grid once the room is joined. room.participants holds
// the remote participants only, so add yourself first.
participants = {
widget.room.localParticipant.id: widget.room.localParticipant,
...widget.room.participants,
};
// Setting room event listeners
setRoomListeners(widget.room);
}
@override
void dispose() {
widget.room.off(Events.participantJoined, _onParticipantJoined);
widget.room.off(Events.participantLeft, _onParticipantLeft);
widget.room.off(Events.speakerChanged, _onSpeakerChanged);
super.dispose();
}
@override
Widget build(BuildContext context) {
return GridView.count(
crossAxisCount: 2,
children: participants.values
.map((participant) => ParticipantTile(
key: ValueKey(participant.id),
room: widget.room,
participant: participant,
isLocalParticipant: participant.isLocal,
))
.toList(),
);
}
void setRoomListeners(Room room) {
// Called when participant joined room
room.on(Events.participantJoined, _onParticipantJoined);
// Called when participant left room
room.on(Events.participantLeft, _onParticipantLeft);
// Called when speaker is changed
room.on(Events.speakerChanged, _onSpeakerChanged);
}
void _onParticipantJoined(Participant participant) {
setState(() {
participants[participant.id] = participant;
});
}
void _onParticipantLeft(Participant participant, LeaveReason reason) {
setState(() {
participants.remove(participant.id);
});
}
void _onSpeakerChanged(String? _activeSpeakerId) {
setState(() {
activeSpeakerId = _activeSpeakerId;
});
}
}
3. Participant Related Events
-
participantJoined - Whenever any new participant join the room,
participantJoinedevent will trigger. For example, the room is running with Alice and Bob, then Eve join that room, after thatparticipantJoinedevent trigger and return the participant object. -
participantLeft - Whenever any participant leave/exit the room,
participantLeftevent will trigger. For example, the room is running with Alice and Bob, then Bob leave that room, after thatparticipantLeftevent trigger and return theParticipantwho left and aLeaveReason. -
speakerChanged - Whenever any participant starts actively speaking in room,
speakerChangedevent will trigger and return the speakerparticipantId. -
presenterChanged - Whenever any participant present/screenshare their screen/window in room,
presenterChangedevent will trigger and return the presenterparticipantId. -
streamEnabled - Whenever any participant enabled mic/camera in room,
streamEnabledevent will trigger on the room and return aRTCStream.stream.participantIdis the id of that participant. -
streamDisabled - Whenever any participant disabled mic/camera in room,
streamDisabledevent will trigger on the room and return aRTCStream.
// Adding event listener
room.on(Events.participantJoined, (Participant participant) {
print("new participant => ${participant.id}");
},
);
room.on(Events.participantLeft, (Participant participant, LeaveReason reason) {
print("participant left => ${participant.id}");
},
);
room.on(Events.presenterChanged, (String? activePresenterId) {
print("presenter changed => $activePresenterId");
},
);
Got a Question? Ask us on discord

