Remote Participant
This guide explains how to access participant data, display videos, handle events, and manage participant permissions in your RealtimeKit meetings.
The data regarding all meeting participants is stored under meeting.participants. These do not include the local user.
The meeting.participants object contains the following maps:
joined- All participants currently in the meeting (excluding the local user)waitlisted- All participants waiting to join the meetingactive- All participants whose media is subscribed to (participants that should be displayed on screen)pinned- All pinned participants in the meeting
If you're building a video/audio grid, you'd use the active map. To display a list of all participants, use the joined map.
Each participant in these maps is of type RTKParticipant.
// Get all joined participantsconst joinedParticipants = meeting.participants.joined;
// Get active participants (those on screen)const activeParticipants = meeting.participants.active;
// Get pinned participantsconst pinnedParticipants = meeting.participants.pinned;
// Get waitlisted participantsconst waitlistedParticipants = meeting.participants.waitlisted;Each participant map emits participantJoined and participantLeft events:
// Listen for when a participant gets pinnedmeeting.participants.pinned.on("participantJoined", (participant) => { console.log(`Participant ${participant.name} got pinned`);});
// Listen for when a participant gets unpinnedmeeting.participants.pinned.on("participantLeft", (participant) => { console.log(`Participant ${participant.name} got unpinned`);});// Number of participants joined in the meetingconsole.log(meeting.participants.count);
// Number of pages available in paginated modeconsole.log(meeting.participants.pageCount);
// Maximum number of participants in active stateconsole.log(meeting.participants.maxActiveParticipantsCount);
// ParticipantId of the last participant who spokeconsole.log(meeting.participants.lastActiveSpeaker);The data regarding all meeting participants is stored under meeting.participants. These do not include the local user.
The meeting.participants object contains the following maps:
joined- All participants currently in the meeting (excluding the local user)waitlisted- All participants waiting to join the meetingactive- All participants whose media is subscribed to (participants that should be displayed on screen)pinned- All pinned participants in the meeting
If you're building a video/audio grid, you'd use the active map. To display a list of all participants, use the joined map.
Each participant in these maps is of type RTKParticipant.
Use the useRealtimeKitSelector hook to access participant maps:
import { useRealtimeKitSelector } from "@cloudflare/realtimekit-react";
// Get all joined participantsconst joinedParticipants = useRealtimeKitSelector((m) => m.participants.joined);
// Get active participants (those on screen)const activeParticipants = useRealtimeKitSelector((m) => m.participants.active);
// Get pinned participantsconst pinnedParticipants = useRealtimeKitSelector((m) => m.participants.pinned);
// Get waitlisted participantsconst waitlistedParticipants = useRealtimeKitSelector( (m) => m.participants.waitlisted,);You can also use event listeners for specific actions:
import { useRealtimeKitClient } from "@cloudflare/realtimekit-react";
function ParticipantListener() { const [meeting] = useRealtimeKitClient();
useEffect(() => { if (!meeting) return;
const handleParticipantPinned = (participant) => { console.log(`Participant ${participant.name} got pinned`); };
meeting.participants.pinned.on( "participantJoined", handleParticipantPinned, );
return () => { meeting.participants.pinned.off( "participantJoined", handleParticipantPinned, ); }; }, [meeting]);}// Number of participants joined in the meetingconst participantCount = useRealtimeKitSelector((m) => m.participants.count);
// Number of pages available in paginated modeconst pageCount = useRealtimeKitSelector((m) => m.participants.pageCount);
// Maximum number of participants in active stateconst maxActiveCount = useRealtimeKitSelector( (m) => m.participants.maxActiveParticipantsCount,);
// ParticipantId of the last participant who spokeconst lastActiveSpeaker = useRealtimeKitSelector( (m) => m.participants.lastActiveSpeaker,);The view mode indicates whether participants are populated in ACTIVE_GRID mode or PAGINATED mode.
ACTIVE_GRIDmode - Participants are automatically replaced inmeeting.participants.activebased on who is speaking or who has their video turned onPAGINATEDmode - Participants inmeeting.participants.activeare fixed. UsesetPage()to change the active participants
// Set the view mode to paginatedawait meeting.participants.setViewMode("PAGINATED");
// Set the view mode to active gridawait meeting.participants.setViewMode("ACTIVE_GRID");// Switch to second pageawait meeting.participants.setPage(2);// Set the view mode to paginatedawait meeting.participants.setViewMode("PAGINATED");
// Set the view mode to active gridawait meeting.participants.setViewMode("ACTIVE_GRID");// Switch to second pageawait meeting.participants.setPage(2);const viewMode = useRealtimeKitSelector((m) => m.participants.viewMode);const currentPage = useRealtimeKitSelector((m) => m.participants.currentPage);await meeting.participants.acceptWaitingRoomRequest(participantId);await meeting.participants.rejectWaitingRoomRequest(participantId);await meeting.participants.acceptWaitingRoomRequest(participantId);await meeting.participants.rejectWaitingRoomRequest(participantId);The participant object contains all information related to a particular participant, including their video/audio/screenshare streams, name, and state variables.
Media Properties:
videoEnabled- Set totrueif the participant's camera is onaudioEnabled- Set totrueif the participant is unmutedscreenShareEnabled- Set totrueif the participant is sharing their screenvideoTrack- The video track of the participantaudioTrack- The audio track of the participantscreenShareTracks- The video and audio tracks of the participant's screen share
Metadata Properties:
id- TheparticipantIdof the participant (akapeerId)userId- TheuserIdof the participantname- The participant's namepicture- The participant's picture (if any)customParticipantId- An arbitrary ID that can be set to identify the participantisPinned- Set totrueif the participant is pinnedpresetName- Name of the preset associated with the participant
const participant = meeting.participants.joined.get(participantId);
// Access participant propertiesconsole.log(participant.name);console.log(participant.videoEnabled);console.log(participant.audioEnabled);Each participant object is an event emitter:
meeting.participants.joined .get(participantId) .on("audioUpdate", ({ audioEnabled, audioTrack }) => { console.log( "The participant with id", participantId, "has toggled their mic to", audioEnabled, ); });Alternatively, listen on the participant map for all participants:
meeting.participants.joined.on( "audioUpdate", (participant, { audioEnabled, audioTrack }) => { console.log( "The participant with id", participant.id, "has toggled their mic to", audioEnabled, ); },);If you have the relevant permissions, you can control participant media:
const participant = meeting.participants.joined.get(participantId);
// Disable a participant's video streamparticipant.disableVideo();
// Disable a participant's audio streamparticipant.disableAudio();
// Kick a participant from the meetingparticipant.kick();const participant = meeting.participants.joined.get(participantId);
// Pin a participantawait participant.pin();
// Unpin a participantawait participant.unpin();// Get a specific participantconst participant = useRealtimeKitSelector((m) => m.participants.joined.get(participantId),);
// Access participant propertiesconst participantName = participant?.name;const isVideoEnabled = participant?.videoEnabled;const isAudioEnabled = participant?.audioEnabled;import { useRealtimeKitClient } from "@cloudflare/realtimekit-react";
function ParticipantAudioListener({ participantId }) { const [meeting] = useRealtimeKitClient();
useEffect(() => { if (!meeting) return;
const handleAudioUpdate = ({ audioEnabled, audioTrack }) => { console.log( "The participant with id", participantId, "has toggled their mic to", audioEnabled, ); };
const participant = meeting.participants.joined.get(participantId); participant.on("audioUpdate", handleAudioUpdate);
return () => { participant.off("audioUpdate", handleAudioUpdate); }; }, [meeting, participantId]);}Or use the selector for specific properties:
const audioEnabled = useRealtimeKitSelector( (m) => m.participants.joined.get(participantId)?.audioEnabled,);If you have the relevant permissions, you can control participant media:
const participant = meeting.participants.joined.get(participantId);
// Disable a participant's video streamparticipant.disableVideo();
// Disable a participant's audio streamparticipant.disableAudio();
// Kick a participant from the meetingparticipant.kick();const participant = meeting.participants.joined.get(participantId);
// Pin a participantawait participant.pin();
// Unpin a participantawait participant.unpin();To play a participant's video track on a <video> element:
<video class="participant-video" id="participant-video"></video>// Get the video elementconst videoElement = document.getElementById("participant-video");
// Get the participantconst participant = meeting.participants.joined.get(participantId);
// Register the video elementparticipant.registerVideoElement(videoElement);For local user preview (video not sent to other users):
meeting.self.registerVideoElement(videoElement, true);Clean up when the video element is no longer needed:
participant.deregisterVideoElement(videoElement);RealtimeKit provides ready-to-use grid components for displaying participant videos:
import { RtkGrid } from "@cloudflare/realtimekit-react-ui";
function MeetingGrid() { return <RtkGrid />;}The RtkGrid component handles all grid-related events and data automatically. It supports:
RtkSimpleGrid- Renders participant tiles in a simple gridRtkSpotlightGrid- Renders pinned participants in a spotlight area with others in a smaller gridRtkMixedGrid- Renders screenshares and plugins in the main view with a configurable smaller grid
For custom video rendering, use the RtkParticipantTile component:
import { RtkParticipantTile } from "@cloudflare/realtimekit-react-ui";import { useRealtimeKitSelector } from "@cloudflare/realtimekit-react";
function CustomVideoGrid() { const participants = useRealtimeKitSelector((m) => m.participants.active.toArray(), );
return ( <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))", }} > {participants.map((participant) => ( <RtkParticipantTile key={participant.id} participant={participant} /> ))} </div> );}Triggered when the view mode changes:
meeting.participants.on( "viewModeChanged", ({ viewMode, currentPage, pageCount }) => { console.log("view mode changed", viewMode); },);Triggered when the page changes in paginated mode:
meeting.participants.on( "pageChanged", ({ viewMode, currentPage, pageCount }) => { console.log("page changed", currentPage); },);Triggered when a participant starts speaking:
meeting.participants.on("activeSpeaker", (participant) => { console.log(`${participant.id} is currently speaking`);});Triggered when any participant joins the meeting:
meeting.participants.joined.on("participantJoined", (participant) => { console.log(`A participant with id "${participant.id}" has joined`);});Triggered when any participant leaves the meeting:
meeting.participants.joined.on("participantLeft", (participant) => { console.log(`A participant with id "${participant.id}" has left the meeting`);});Triggered when a participant is pinned:
meeting.participants.joined.on("pinned", (participant) => { console.log(`Participant with id "${participant.id}" was pinned`);});Triggered when a participant is unpinned:
meeting.participants.joined.on("unpinned", (participant) => { console.log(`Participant with id "${participant.id}" was unpinned`);});Triggered when any participant starts/stops video:
meeting.participants.joined.on("videoUpdate", (participant) => { console.log( `A participant with id "${participant.id}" updated their video track`, );
if (participant.videoEnabled) { // Use participant.videoTrack } else { // Handle stop video }});Triggered when any participant starts/stops audio:
meeting.participants.joined.on("audioUpdate", (participant) => { console.log( `A participant with id "${participant.id}" updated their audio track`, );
if (participant.audioEnabled) { // Use participant.audioTrack } else { // Handle stop audio }});Triggered when any participant starts/stops screen share:
meeting.participants.joined.on("screenShareUpdate", (participant) => { console.log( `A participant with id "${participant.id}" updated their screen share`, );
if (participant.screenShareEnabled) { // Use participant.screenShareTracks } else { // Handle stop screen share }});Monitor participant network quality:
meeting.participants.joined.on( "mediaScoreUpdate", ({ participantId, kind, isScreenshare, score, scoreStats }) => { if (kind === "video") { console.log( `Participant ${participantId}'s ${isScreenshare ? "screenshare" : "video"} quality score is`, score, ); }
if (kind === "audio") { console.log( `Participant ${participantId}'s audio quality score is`, score, ); }
if (score < 5) { console.log(`Participant ${participantId}'s media quality is poor`); } },);const viewMode = useRealtimeKitSelector((m) => m.participants.viewMode);Or use event listener:
meeting.participants.on( "viewModeChanged", ({ viewMode, currentPage, pageCount }) => { console.log("view mode changed", viewMode); },);const currentPage = useRealtimeKitSelector((m) => m.participants.currentPage);const pageCount = useRealtimeKitSelector((m) => m.participants.pageCount);const activeSpeaker = useRealtimeKitSelector( (m) => m.participants.lastActiveSpeaker,);Or use event listener:
meeting.participants.on("activeSpeaker", (participant) => { console.log(`${participant.id} is currently speaking`);});const joinedParticipants = useRealtimeKitSelector((m) => m.participants.joined);Or use event listener:
meeting.participants.joined.on("participantJoined", (participant) => { console.log(`A participant with id "${participant.id}" has joined`);});const joinedParticipants = useRealtimeKitSelector((m) => m.participants.joined);Or use event listener:
meeting.participants.joined.on("participantLeft", (participant) => { console.log(`A participant with id "${participant.id}" has left the meeting`);});const pinnedParticipants = useRealtimeKitSelector((m) => m.participants.pinned);// Check for one participantconst videoEnabled = useRealtimeKitSelector( (m) => m.participants.joined.get(participantId)?.videoEnabled,);
// All video enabled participantsconst videoEnabledParticipants = useRealtimeKitSelector((m) => m.participants.joined.toArray().filter((p) => p.videoEnabled),);// Check for one participantconst audioEnabled = useRealtimeKitSelector( (m) => m.participants.joined.get(participantId)?.audioEnabled,);
// All audio enabled participantsconst audioEnabledParticipants = useRealtimeKitSelector((m) => m.participants.joined.toArray().filter((p) => p.audioEnabled),);// Check for one participantconst screensharingParticipant = useRealtimeKitSelector((m) => m.participants.joined.toArray().find((p) => p.screenShareEnabled),);
// All screen sharing participantsconst screenSharingParticipants = useRealtimeKitSelector((m) => m.participants.joined.toArray().filter((p) => p.screenShareEnabled),);// Use event listener for media score updatesuseEffect(() => { if (!meeting) return;
const handleMediaScoreUpdate = ({ participantId, kind, isScreenshare, score, scoreStats, }) => { if (kind === "video") { console.log( `Participant ${participantId}'s ${isScreenshare ? "screenshare" : "video"} quality score is`, score, ); }
if (score < 5) { console.log(`Participant ${participantId}'s media quality is poor`); } };
meeting.participants.joined.on("mediaScoreUpdate", handleMediaScoreUpdate);
return () => { meeting.participants.joined.off("mediaScoreUpdate", handleMediaScoreUpdate); };}, [meeting]);Picture-in-Picture API allows you to render meeting.participants.active participant's video as a floating tile outside of the current webpage's context.
const isSupported = meeting.participants.pip.isSupported();await meeting.participants.pip.enable();await meeting.participants.pip.disable();const isSupported = meeting.participants.pip.isSupported();await meeting.participants.pip.enable();await meeting.participants.pip.disable();Permissions for a participant are defined by the preset, but can be updated during a meeting by calling updatePermissions for remote participants.
const participantIds = meeting.participants.joined .toArray() .filter((e) => e.name.startsWith("John")) .map((p) => p.id);// Allow file upload permissions in public chatconst newPermissions = { chat: { public: { files: true, }, },};
meeting.participants.updatePermissions(participantIds, newPermissions);interface UpdatedPermissions { polls?: { canCreate?: boolean; canVote?: boolean; }; plugins?: { canClose?: boolean; canStart?: boolean; }; chat?: { public?: { canSend?: boolean; text?: boolean; files?: boolean; }; private?: { canSend?: boolean; text?: boolean; files?: boolean; }; };}const participantIds = meeting.participants.joined .toArray() .filter((e) => e.name.startsWith("John")) .map((p) => p.id);// Allow file upload permissions in public chatconst newPermissions = { chat: { public: { files: true, }, },};
meeting.participants.updatePermissions(participantIds, newPermissions);interface UpdatedPermissions { polls?: { canCreate?: boolean; canVote?: boolean; }; plugins?: { canClose?: boolean; canStart?: boolean; }; chat?: { public?: { canSend?: boolean; text?: boolean; files?: boolean; }; private?: { canSend?: boolean; text?: boolean; files?: boolean; }; };}Was this helpful?
- Resources
- API
- New to Cloudflare?
- Directory
- Sponsorships
- Open Source
- Support
- Help Center
- System Status
- Compliance
- GDPR
- Company
- cloudflare.com
- Our team
- Careers
- © 2025 Cloudflare, Inc.
- Privacy Policy
- Terms of Use
- Report Security Issues
- Trademark
-