Pin/Unpin Participants - Flutter
In a live stream setup, you often want to control which participant is actively visible on screen — whether it’s the host, a special guest, or someone screen sharing. To manage this dynamically, the Participant class provides two key methods:
-
pin()– Focus on a specific participant’s camera or screen share -
unpin()– Remove that focus and return to the default layout
This allows you to programmatically decide who gets the spotlight in your live stream layout.
Both methods return a Future that throws a VideoSDKError when the request fails, so await them inside try / catch.
pin()
The pin() method makes it easy to bring a host camera or screen share front and center in the broadcast layout. Once pinned, you can write the layout logic to place this host in the primary frame, e.g. fullscreen or in a highlighted position.
unpin()
When a host is no longer the focus (e.g. their segment is over), you can reset their pinned state using unpin(). This allows your live stream UI to revert to a default grid view or automatically promote the next speaker.
Events associated with pin/unpin
Following callbacks are received when a host is pinned or unpinned in the livestream.
- The
pinStateChangedevent will be emitted on theRoomwhen any participant pin state gets changed. - It will pass
participantId,state, andpinnedByasMap<String, dynamic>as an event handler parameter.stateholds two booleans,camandshare.
The example below pins the local participant while they are a host (SEND_AND_RECV), and unpins every attendee. participant.mode is already updated when participantModeChanged fires, so the screen updates the attendees then. Every pin and unpin reaches all participants, so the example skips the call when nothing would change.
import 'dart:async' show unawaited;
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:videosdk/videosdk.dart';
class SpeakerScreen extends StatefulWidget {
...
}
class _SpeakerScreenState extends State<SpeakerScreen> {
late Room room;
Map<String, Participant> _attendees = {};
@override
void initState() {
super.initState();
... // create the room
setUpLiveStreamEventListeners();
... // join the room
}
void setUpLiveStreamEventListeners() {
room.on(Events.roomJoined, () => unawaited(updateAttendees()));
room.on(Events.pinStateChanged, (Map<String, dynamic> data) {
final String participantId = data['participantId'];
final ParticipantPinState state = data['state'];
// state.cam and state.share say what is pinned now
});
room.on(Events.participantModeChanged, (Map<String, dynamic> data) {
unawaited(updateAttendees());
});
}
@override
Widget build(BuildContext context) {
return Column(
children:[
...
]
);
}
// room.pinnedParticipants holds the pin state of each participant
bool isPinned(Participant participant) {
final ParticipantPinState? state = room.pinnedParticipants[participant.id];
return state != null && (state.cam || state.share);
}
Future<void> updateAttendees() async {
Map<String, Participant> attendees = {};
// room.participants holds remote participants only, so add yourself
final everyone = [room.localParticipant, ...room.participants.values];
for (final participant in everyone) {
final bool isAttendee = participant.mode != Mode.SEND_AND_RECV;
if (isAttendee) {
attendees.putIfAbsent(participant.id, () => participant);
}
try {
if (isAttendee && isPinned(participant)) {
await participant.unpin();
} else if (!isAttendee && participant.isLocal && !isPinned(participant)) {
//we will pin the local participant if mode is SEND_AND_RECV
await participant.pin();
}
} on VideoSDKError catch (error) {
log("Could not update the pin state: ${error.code} ${error.name}");
}
}
if (mounted) setState(() => _attendees = attendees);
}
}
API Reference
The API references for all the methods utilized in this guide are provided below.
Got a Question? Ask us on discord

