Skip to content
Cloudflare Docs

Local Participant

Manage local user media devices, control audio, video, and screenshare, and handle events in RealtimeKit meetings.

Introduction

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.

Properties

Metadata Properties

Access participant identifiers and display information:

import { useRealtimeKitSelector } from "@cloudflare/realtimekit-react";
// Participant identifiers
const 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);

Media Properties

Access the local user's media tracks and states:

// Media state flags
const 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 user
const mediaPermissions = useRealtimeKitSelector((m) => m.self.mediaPermissions);

State Properties

Access room state and participant status:

// Room state
const roomJoined = useRealtimeKitSelector((m) => m.self.roomJoined);
const roomState = useRealtimeKitSelector((m) => m.self.roomState);
const isPinned = useRealtimeKitSelector((m) => m.self.isPinned);
// Permissions and config
const 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:

  • '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

Media Controls

Audio control

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>
);
}

Video control

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>
);
}

Screen share control

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 display name

Update the display name before joining the meeting:

await meeting.self.setName("New Name");

Manage media devices

Get available devices

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 used:

const currentDevices = meeting.self.getCurrentDevices();
// Returns: { audio: MediaDeviceInfo, video: MediaDeviceInfo, speaker: MediaDeviceInfo }

Change device

Switch to a different media device:

Use the device selector example from the previous section. The handleDeviceChange function demonstrates how to switch devices.

Display local video

Use UI Kit component

Display local video with the UI Kit 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} />;
}

Manage video element manually

Create custom video element 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" }}
/>
);
}

Screen share setup (iOS)

Events

Room joined

Fires when the local user joins the meeting:

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]);

Room left

Fires when the local user leaves the 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");
}
});

Video update

Fires when video is enabled or disabled:

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]);

Audio update

Fires when audio is enabled or disabled:

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]);

Screen share update

Fires when screen sharing starts or stops:

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]);

Device update

Fires when the active device changes:

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]);

Device List Update

Triggered when the list of available devices changes (device plugged in or out):

useEffect(() => {
if (!meeting) return;
const handleDeviceListUpdate = ({ added, removed, devices }) => {
console.log("Device list updated");
console.log("Added devices:", added);
console.log("Removed devices:", removed);
console.log("All devices:", devices);
};
meeting.self.on("deviceListUpdate", handleDeviceListUpdate);
return () => {
meeting.self.off("deviceListUpdate", handleDeviceListUpdate);
};
}, [meeting]);

Network Quality Score

Monitor your own network quality:

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]);

Permission Updates

Triggered when permissions are updated dynamically:

Monitor permissions using selectors:

const permissions = useRealtimeKitSelector((m) => m.self.permissions);
useEffect(() => {
console.log("Permissions updated:", permissions);
}, [permissions]);

Media Permission Errors

Triggered when media permissions are denied or media capture fails:

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]);

Waitlist Status

For meetings with waiting room enabled:

const roomState = useRealtimeKitSelector((m) => m.self.roomState);
useEffect(() => {
if (roomState === "waitlisted") {
console.log("Waiting for host to admit you");
}
}, [roomState]);

iOS-Specific Events

The iOS SDK provides additional platform-specific events:

Pin and Unpin

Pin or unpin yourself in the meeting (requires appropriate permissions):

Web SDK does not currently support pinning the local participant.

Update Media Constraints

Update video or screenshare resolution at runtime:

Web SDK does not currently expose runtime constraint updates for local participant.