Local Participant
This guide covers how to manage the local user's media devices, control audio/video/screenshare, handle events, and work with media tracks in your RealtimeKit meetings.
The local user is accessible via meeting.self and contains all information and methods related to the current participant. This includes media controls, device management, participant metadata, and state information.
// Participant identifiersmeeting.self.id; // Peer ID (unique per session)meeting.self.userId; // User ID (persistent across sessions)meeting.self.customParticipantId; // Custom identifier set by developermeeting.self.name; // Display namemeeting.self.picture; // Display picture URLThe local user's media tracks and states:
// Media state flagsmeeting.self.audioEnabled; // Boolean: Is audio enabled?meeting.self.videoEnabled; // Boolean: Is video enabled?meeting.self.screenShareEnabled; // Boolean: Is screen share active?
// Media tracks (MediaStreamTrack objects)meeting.self.audioTrack; // Audio MediaStreamTrack (available when audioEnabled is true)meeting.self.videoTrack; // Video MediaStreamTrack (available when videoEnabled is true)meeting.self.screenShareTracks; // Object: { video: MediaStreamTrack, audio?: MediaStreamTrack }
// Permissions granted by usermeeting.self.mediaPermissions; // Current audio/video permissions// Room statemeeting.self.roomJoined; // Boolean: Has joined the meeting?meeting.self.roomState; // Current room state (see possible values below)meeting.self.isPinned; // Boolean: Is the local user pinned?
// Permissions and configmeeting.self.permissions; // Capabilities defined by presetmeeting.self.config; // Configuration for meeting appearanceRoom State Values:
The roomState property can have the following values:
'init'- Initialized but not joined'joined'- Successfully joined the meeting'waitlisted'- Waiting in the waiting room'rejected'- Entry rejected'kicked'- Removed from meeting'left'- Left the meeting'ended'- Meeting has ended'disconnected'- Disconnected from meeting
import { useRealtimeKitSelector } from "@cloudflare/realtimekit-react";
// Participant identifiersconst id = useRealtimeKitSelector((m) => m.self.id);const userId = useRealtimeKitSelector((m) => m.self.userId);const customParticipantId = useRealtimeKitSelector( (m) => m.self.customParticipantId,);const name = useRealtimeKitSelector((m) => m.self.name);const picture = useRealtimeKitSelector((m) => m.self.picture);The local user's media tracks and states:
// Media state flagsconst audioEnabled = useRealtimeKitSelector((m) => m.self.audioEnabled);const videoEnabled = useRealtimeKitSelector((m) => m.self.videoEnabled);const screenShareEnabled = useRealtimeKitSelector( (m) => m.self.screenShareEnabled,);
// Media tracks (MediaStreamTrack objects)const audioTrack = useRealtimeKitSelector((m) => m.self.audioTrack);const videoTrack = useRealtimeKitSelector((m) => m.self.videoTrack);const screenShareTracks = useRealtimeKitSelector( (m) => m.self.screenShareTracks,);
// Permissions granted by userconst mediaPermissions = useRealtimeKitSelector((m) => m.self.mediaPermissions);// Room stateconst roomJoined = useRealtimeKitSelector((m) => m.self.roomJoined);const roomState = useRealtimeKitSelector((m) => m.self.roomState);const isPinned = useRealtimeKitSelector((m) => m.self.isPinned);
// Permissions and configconst permissions = useRealtimeKitSelector((m) => m.self.permissions);const config = useRealtimeKitSelector((m) => m.self.config);Example: Conditional rendering based on room state
const roomState = useRealtimeKitSelector((m) => m.self.roomState);
return ( <> {roomState === "disconnected" && <div>You are disconnected</div>} {roomState === "waitlisted" && <div>Waiting for host to admit you</div>} {roomState === "joined" && <div>You are in the meeting</div>} </>);Room State Values:
The roomState property can have the following values:
'init'- Initialized but not joined'joined'- Successfully joined the meeting'waitlisted'- Waiting in the waiting room'rejected'- Entry rejected'kicked'- Removed from meeting'left'- Left the meeting'ended'- Meeting has ended'disconnected'- Disconnected from meeting
Mute and unmute the microphone:
// Enable audio (unmute)await meeting.self.enableAudio();
// Disable audio (mute)await meeting.self.disableAudio();
// Check current statusconst isAudioEnabled = meeting.self.audioEnabled;Enable and disable the camera:
// Enable videoawait meeting.self.enableVideo();
// Disable videoawait meeting.self.disableVideo();
// Check current statusconst isVideoEnabled = meeting.self.videoEnabled;Start and stop screen sharing:
// Enable screen shareawait meeting.self.enableScreenShare();
// Disable screen shareawait meeting.self.disableScreenShare();
// Check current statusconst isScreenShareEnabled = meeting.self.screenShareEnabled;Change the user's display name (only works before joining):
await meeting.self.setName("New Name");Mute and unmute the microphone:
import { useRealtimeKitClient } from "@cloudflare/realtimekit-react";
function AudioControls() { const [meeting] = useRealtimeKitClient(); const audioEnabled = useRealtimeKitSelector((m) => m.self.audioEnabled);
const toggleAudio = async () => { if (audioEnabled) { await meeting.self.disableAudio(); } else { await meeting.self.enableAudio(); } };
return ( <button onClick={toggleAudio}>{audioEnabled ? "Mute" : "Unmute"}</button> );}Enable and disable the camera:
function VideoControls() { const [meeting] = useRealtimeKitClient(); const videoEnabled = useRealtimeKitSelector((m) => m.self.videoEnabled);
const toggleVideo = async () => { if (videoEnabled) { await meeting.self.disableVideo(); } else { await meeting.self.enableVideo(); } };
return ( <button onClick={toggleVideo}> {videoEnabled ? "Stop Video" : "Start Video"} </button> );}Start and stop screen sharing:
function ScreenShareControls() { const [meeting] = useRealtimeKitClient(); const screenShareEnabled = useRealtimeKitSelector( (m) => m.self.screenShareEnabled, );
const toggleScreenShare = async () => { if (screenShareEnabled) { await meeting.self.disableScreenShare(); } else { await meeting.self.enableScreenShare(); } };
return ( <button onClick={toggleScreenShare}> {screenShareEnabled ? "Stop Sharing" : "Share Screen"} </button> );}Change the user's display name (only works before joining):
await meeting.self.setName("New Name");// Get all media devicesconst devices = await meeting.self.getAllDevices();
// Get all audio input devices (microphones)const audioDevices = await meeting.self.getAudioDevices();
// Get all video input devices (cameras)const videoDevices = await meeting.self.getVideoDevices();
// Get all audio output devices (speakers)const speakerDevices = await meeting.self.getSpeakerDevices();
// Get device by IDconst device = await meeting.self.getDeviceById("device-id", "audio");
// Get current devices being usedconst currentDevices = meeting.self.getCurrentDevices();// Returns: { audio: MediaDeviceInfo, video: MediaDeviceInfo, speaker: MediaDeviceInfo }Switch to a different media device:
// Get all devicesconst devices = await meeting.self.getAllDevices();
// Set a specific device (replaces device of the same kind)await meeting.self.setDevice(devices[0]);import { useRealtimeKitClient } from "@cloudflare/realtimekit-react";import { useState, useEffect } from "react";
function DeviceSelector() { const [meeting] = useRealtimeKitClient(); const [audioDevices, setAudioDevices] = useState([]); const [videoDevices, setVideoDevices] = useState([]);
useEffect(() => { if (!meeting) return;
const loadDevices = async () => { const audio = await meeting.self.getAudioDevices(); const video = await meeting.self.getVideoDevices(); setAudioDevices(audio); setVideoDevices(video); };
loadDevices(); }, [meeting]);
const handleDeviceChange = async (device) => { await meeting.self.setDevice(device); };
return ( <div> <select onChange={(e) => { const device = audioDevices.find( (d) => d.deviceId === e.target.value, ); handleDeviceChange(device); }} > {audioDevices.map((device) => ( <option key={device.deviceId} value={device.deviceId}> {device.label} </option> ))} </select> </div> );}// Get current devices being usedconst currentDevices = meeting.self.getCurrentDevices();// Returns: { audio: MediaDeviceInfo, video: MediaDeviceInfo, speaker: MediaDeviceInfo }Play the local user's video track on a <video> element:
<video id="local-video" autoplay playsinline></video>const videoElement = document.getElementById("local-video");
// Register the video element to display videomeeting.self.registerVideoElement(videoElement);
// For local preview (not sent to other users), pass true as second argumentmeeting.self.registerVideoElement(videoElement, true);Clean up when the video element is no longer needed:
meeting.self.deregisterVideoElement(videoElement);The simplest way to display local video in React is using the UI Kit's video tile component:
import { RtkParticipantTile } from "@cloudflare/realtimekit-react-ui";import { useRealtimeKitSelector } from "@cloudflare/realtimekit-react";
function LocalVideo() { const localUser = useRealtimeKitSelector((m) => m.self);
return <RtkParticipantTile participant={localUser} />;}For custom implementations:
import { useRealtimeKitClient, useRealtimeKitSelector,} from "@cloudflare/realtimekit-react";import { useEffect, useRef } from "react";
function LocalVideoCustom() { const [meeting] = useRealtimeKitClient(); const videoEnabled = useRealtimeKitSelector((m) => m.self.videoEnabled); const videoTrack = useRealtimeKitSelector((m) => m.self.videoTrack); const videoRef = useRef(null);
useEffect(() => { if (!videoRef.current || !meeting) return;
// Register video element meeting.self.registerVideoElement(videoRef.current);
return () => { // Cleanup: deregister on unmount meeting.self.deregisterVideoElement(videoRef.current); }; }, [meeting]);
return ( <video ref={videoRef} autoPlay playsInline muted style={{ display: videoEnabled ? "block" : "none" }} /> );}Triggered when the local user successfully joins the meeting:
meeting.self.on("roomJoined", () => { console.log("Successfully joined the meeting");});Triggered when the local user leaves the meeting:
meeting.self.on("roomLeft", ({ state }) => { console.log("Left the meeting with state:", state);
// Handle different leave states if (state === "left") { console.log("User voluntarily left"); } else if (state === "kicked") { console.log("User was kicked from the meeting"); } else if (state === "ended") { console.log("Meeting has ended"); } else if (state === "disconnected") { console.log("Lost connection to meeting"); }});Possible state values: 'left', 'kicked', 'ended', 'rejected', 'disconnected', 'failed'
Triggered when video is enabled or disabled:
meeting.self.on("videoUpdate", ({ videoEnabled, videoTrack }) => { console.log("Video state:", videoEnabled);
if (videoEnabled) { // Video track is available, can display it const videoElement = document.getElementById("my-video"); const stream = new MediaStream(); stream.addTrack(videoTrack); videoElement.srcObject = stream; videoElement.play(); }});Triggered when audio is enabled or disabled:
meeting.self.on("audioUpdate", ({ audioEnabled, audioTrack }) => { console.log("Audio state:", audioEnabled);
if (audioEnabled) { // Audio track is available console.log("Microphone is on"); }});Triggered when screen sharing starts or stops:
meeting.self.on( "screenShareUpdate", ({ screenShareEnabled, screenShareTracks }) => { console.log("Screen share state:", screenShareEnabled);
if (screenShareEnabled) { // Screen share tracks are available const screenElement = document.getElementById("my-screen-share"); const stream = new MediaStream(); stream.addTrack(screenShareTracks.video); if (screenShareTracks.audio) { stream.addTrack(screenShareTracks.audio); } screenElement.srcObject = stream; screenElement.play(); } },);Triggered when the active device changes:
meeting.self.on("deviceUpdate", ({ device }) => { // Handle device change if (device.kind === "audioinput") { console.log("Microphone changed:", device.label); } else if (device.kind === "videoinput") { console.log("Camera changed:", device.label); } else if (device.kind === "audiooutput") { console.log("Speaker changed:", device.label); }});Triggered when the list of available devices changes (device plugged in/out):
meeting.self.on("deviceListUpdate", ({ added, removed, devices }) => { console.log("Device list updated"); console.log("Added devices:", added); console.log("Removed devices:", removed); console.log("All devices:", devices);});Monitor your own network quality:
meeting.self.on( "mediaScoreUpdate", ({ kind, isScreenshare, score, scoreStats }) => { if (kind === "video") { console.log( `Your ${isScreenshare ? "screenshare" : "video"} quality score is`, score, ); }
if (kind === "audio") { console.log("Your audio quality score is", score); }
if (score < 5) { console.log("Your media quality is poor"); } },);The scoreStats object provides detailed statistics:
// Audio Producer{ "kind": "audio", "isScreenshare": false, "score": 10, "participantId": "meeting.self.id", "scoreStats": { "score": 10, "bitrate": 22452, "packetsLostPercentage": 0, "jitter": 0, "isScreenShare": false }}
// Video Producer{ "kind": "video", "isScreenshare": false, "score": 10, "participantId": "meeting.self.id", "scoreStats": { "score": 10, "frameWidth": 640, "frameHeight": 480, "framesPerSecond": 24, "jitter": 0, "isScreenShare": false, "packetsLostPercentage": 0, "bitrate": 576195, "cpuLimitations": false, "bandwidthLimitations": false }}Triggered when permissions are updated dynamically:
// Listen to specific permission updatesmeeting.self.permissions.on("chatUpdate", () => { console.log("Chat permissions updated"); // Check meeting.self.permissions for updated permissions});
meeting.self.permissions.on("pollsUpdate", () => { console.log("Polls permissions updated");});
meeting.self.permissions.on("pluginsUpdate", () => { console.log("Plugins permissions updated");});
// Listen to all permission updatesmeeting.self.permissions.on("*", () => { console.log("Permissions updated");});Triggered when media permissions are denied or media capture fails:
meeting.self.on("mediaPermissionError", ({ message, kind }) => { console.log(`Failed to capture ${kind}: ${message}`);
// Handle different error types if (message === "DENIED") { console.log("User denied permission"); } else if (message === "SYSTEM_DENIED") { console.log("System denied permission"); } else if (message === "COULD_NOT_START") { console.log("Failed to start media stream"); }});Possible values:
message:'DENIED','SYSTEM_DENIED','COULD_NOT_START'kind:'audio','video','screenshare'
const roomJoined = useRealtimeKitSelector((m) => m.self.roomJoined);
useEffect(() => { if (roomJoined) { console.log("Successfully joined the meeting"); }}, [roomJoined]);Or use event listener:
useEffect(() => { if (!meeting) return;
const handleRoomJoined = () => { console.log("Successfully joined the meeting"); };
meeting.self.on("roomJoined", handleRoomJoined);
return () => { meeting.self.off("roomJoined", handleRoomJoined); };}, [meeting]);const roomJoined = useRealtimeKitSelector((m) => m.self.roomJoined);
useEffect(() => { if (!roomJoined) { console.log("Left the meeting"); }}, [roomJoined]);Or use event listener for detailed state:
meeting.self.on("roomLeft", ({ state }) => { if (state === "left") { console.log("User voluntarily left"); } else if (state === "kicked") { console.log("User was kicked"); }});const videoEnabled = useRealtimeKitSelector((m) => m.self.videoEnabled);const videoTrack = useRealtimeKitSelector((m) => m.self.videoTrack);
useEffect(() => { if (videoEnabled && videoTrack) { console.log("Video is enabled"); // Handle video track }}, [videoEnabled, videoTrack]);const audioEnabled = useRealtimeKitSelector((m) => m.self.audioEnabled);const audioTrack = useRealtimeKitSelector((m) => m.self.audioTrack);
useEffect(() => { if (audioEnabled && audioTrack) { console.log("Audio is enabled"); // Handle audio track }}, [audioEnabled, audioTrack]);const screenShareEnabled = useRealtimeKitSelector( (m) => m.self.screenShareEnabled,);const screenShareTracks = useRealtimeKitSelector( (m) => m.self.screenShareTracks,);
useEffect(() => { if (screenShareEnabled && screenShareTracks) { console.log("Screen sharing is active"); // Handle screen share tracks }}, [screenShareEnabled, screenShareTracks]);useEffect(() => { if (!meeting) return;
const handleDeviceUpdate = ({ device }) => { if (device.kind === "audioinput") { console.log("Microphone changed:", device.label); } else if (device.kind === "videoinput") { console.log("Camera changed:", device.label); } };
meeting.self.on("deviceUpdate", handleDeviceUpdate);
return () => { meeting.self.off("deviceUpdate", handleDeviceUpdate); };}, [meeting]);useEffect(() => { if (!meeting) return;
const handleMediaScoreUpdate = ({ kind, isScreenshare, score, scoreStats, }) => { if (kind === "video") { console.log( `Your ${isScreenshare ? "screenshare" : "video"} quality score is`, score, ); }
if (score < 5) { console.log("Your media quality is poor"); } };
meeting.self.on("mediaScoreUpdate", handleMediaScoreUpdate);
return () => { meeting.self.off("mediaScoreUpdate", handleMediaScoreUpdate); };}, [meeting]);useEffect(() => { if (!meeting) return;
const handlePermissionError = ({ message, kind }) => { console.log(`Failed to capture ${kind}: ${message}`);
if (message === "DENIED") { // Show UI to guide user to grant permissions } };
meeting.self.on("mediaPermissionError", handlePermissionError);
return () => { meeting.self.off("mediaPermissionError", handlePermissionError); };}, [meeting]);Change video or screen share resolution at runtime:
// Update camera resolutionawait meeting.self.updateVideoConstraints({ width: { ideal: 1920 }, height: { ideal: 1080 },});
// Update screen share resolutionawait meeting.self.updateScreenshareConstraints({ width: { ideal: 1920 }, height: { ideal: 1080 },});Add effects and filters to your video stream:
// Create a middleware (e.g., retro filter)function RetroTheme() { return (canvas, ctx) => { ctx.filter = "grayscale(1)"; ctx.shadowColor = "#000"; ctx.shadowBlur = 20; ctx.lineWidth = 50; ctx.strokeStyle = "#000"; ctx.strokeRect(0, 0, canvas.width, canvas.height); };}
// Add the video middlewaremeeting.self.addVideoMiddleware(RetroTheme);
// Remove the video middlewaremeeting.self.removeVideoMiddleware(RetroTheme);Process audio streams with custom middlewares:
// Add audio middlewaremeeting.self.addAudioMiddleware(YourAudioMiddleware);
// Remove audio middlewaremeeting.self.removeAudioMiddleware(YourAudioMiddleware);Pin or unpin yourself in the meeting:
// Pin yourselfawait meeting.self.pin();
// Unpin yourselfawait meeting.self.unpin();
// Check pinned statusconst isPinned = meeting.self.isPinned;Change video or screen share resolution at runtime:
// Update camera resolutionawait meeting.self.updateVideoConstraints({ width: { ideal: 1920 }, height: { ideal: 1080 },});
// Update screen share resolutionawait meeting.self.updateScreenshareConstraints({ width: { ideal: 1920 }, height: { ideal: 1080 },});Add effects and filters to your video stream:
// Create a middleware (e.g., retro filter)function RetroTheme() { return (canvas, ctx) => { ctx.filter = "grayscale(1)"; ctx.shadowColor = "#000"; ctx.shadowBlur = 20; ctx.lineWidth = 50; ctx.strokeStyle = "#000"; ctx.strokeRect(0, 0, canvas.width, canvas.height); };}
function VideoEffects() { const [meeting] = useRealtimeKitClient();
const addRetroEffect = () => { meeting.self.addVideoMiddleware(RetroTheme); };
const removeRetroEffect = () => { meeting.self.removeVideoMiddleware(RetroTheme); };
return ( <> <button onClick={addRetroEffect}>Add Retro Effect</button> <button onClick={removeRetroEffect}>Remove Effect</button> </> );}Process audio streams with custom middlewares:
// Add audio middlewaremeeting.self.addAudioMiddleware(YourAudioMiddleware);
// Remove audio middlewaremeeting.self.removeAudioMiddleware(YourAudioMiddleware);Pin or unpin yourself in the meeting:
function PinControls() { const [meeting] = useRealtimeKitClient(); const isPinned = useRealtimeKitSelector((m) => m.self.isPinned);
const togglePin = async () => { if (isPinned) { await meeting.self.unpin(); } else { await meeting.self.pin(); } };
return ( <button onClick={togglePin}>{isPinned ? "Unpin Self" : "Pin Self"}</button> );}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
-