Room class
A connected room. Returned by VideoSDK.join(). Holds your LocalParticipant, the map of RemoteParticipants, and room-level controls (output device routing, room events).
Blueprint
Full surface at a glance β every property, method, and event on this class.
Properties
remoteParticipants
Map of remote participants in the speaker tier (on-stage). Updated as speakers join and leave. The SDK auto-removes a participant from this map after firing participant-left.
participant-count-changed (live count) and room.getParticipants({ tier: 'viewer' }) (paginated enumeration). See Tier & participant events for the underlying model.participant-joined event (below) fires retroactively for already-present speakers, so a single handler covers both already-present and future speakers. Use this map for one-shot lookups by id (e.g. room.remoteParticipants.get(someId)).pubsub
Topic-based messaging primitive. Powers chat and general realtime events. Three methods: publish, subscribe, getHistory. See PubSub for the full surface.
await room.pubsub.publish('chat', { text: 'hello' });
const sub = await room.pubsub.subscribe('chat', (msg) => {
console.log(msg.from, msg.payload);
});
state readonly
Current ConnectionState of the room. Synchronous getter β useful for one-shot UI queries ("is the room reconnecting right now?") without subscribing to the event. Always one of connected / reconnecting / disconnected β the Room is only returned by VideoSDK.join() after it's connected, so idle / connecting states are pre-Room and not observable here.
if (room.state === ConnectionState.Connected) {
// Send chat, publish track, etc.
} else {
showToast('Reconnectingβ¦');
}
recordingState readonly
Current run state of cloud recording, as a ServiceState. Synchronous getter β useful for one-shot UI queries ("is recording running right now?") without subscribing to the event. Observe transitions via the recording-state-changed event.
hlsState readonly
Current run state of HLS streaming, as a ServiceState. Synchronous getter β useful for one-shot UI queries without subscribing to the event. Observe transitions via the hls-state-changed event.
ServiceState lifecycle. Unlike recording / livestream / transcription, HLS passes through Playable between Started and Stopping: stopped β starting β started β playable β stopping β stopped. started = pipeline is up and hlsUrls are minted, but the segment buffer (~6 s) hasn't filled yet β viewers can't play. playable = buffer is ready; URLs are safe to hand to hls.js. Render the player only after playable.livestreamState readonly
Current run state of the RTMP livestream, as a ServiceState. Synchronous getter β useful for one-shot UI queries without subscribing to the event. Observe transitions via the livestream-state-changed event.
transcriptionState readonly
Current run state of live transcription, as a ServiceState. Synchronous getter β useful for one-shot UI queries without subscribing to the event. Observe transitions via the transcription-state-changed event.
hlsUrls readonly
The two HLS URLs β playbackUrl and liveUrl β while hlsState is started or playable; null otherwise. The same HlsUrls object is also delivered on the hls-state-changed event from started onward.
playbackUrl in an HLS player and never interacts with the room β does not need to call VideoSDK.join(), doesn't need a token, and doesn't appear in participant-count-changed or getParticipants. Hand them the URL, drop it into hls.js (or any HLS player), done. The SDK is only for interactive participants (chat, polls, count, future grant-promotion). If you need viewers to be counted or chat-capable, mint them an audience-tier token (isViewer: true) and use the SDK normally β that's the interactive audience tier, distinct from pure HLS playback.entryRequests readonly
Snapshot of pending waiting-lobby requests this room currently knows about. Visible only to participants with canModerate β an empty array otherwise. The entry-requested event covers most flows (and replays for any already-pending requests on subscribe); this getter is the matching snapshot for declarative renders (React list bindings, count badges).
The same EntryRequest objects appear in the event and in this array β .admit() / .deny() work either way, no id juggling.
Methods
leave async
Leaves the room. Stops all SDK-owned local streams, releases all attached rendering targets (DOM elements, native views), removes all listeners, and tears down the SFU connection.
window.addEventListener('beforeunload', () => room.leave());
getParticipants async
Paginated query for participants by tier. Primary use: enumerating the viewer tier (which isn't tracked in remoteParticipants) β but works for speakers too, or both at once. Cursor-based, mirrors pubsub.getHistory.
opts.tierβ'speaker' | 'viewer'; omit for both (speakers first, then viewers).opts.limitβ page size; default 50, max 100.opts.cursorβ opaque token from a previous call'snextCursor.
{ participants, nextCursor?, total } β see GetParticipantsResult. nextCursor is present iff there are more pages in the requested filter. total is the total participant count in this filter β saves a parallel listen on participant-count-changed just to render a "X attendees" badge.
isViewer: true; calling subscribe() / publishX() / similar on them rejects with INVALID_PERMISSIONS. Speakers behave normally.let cursor;
do {
const page = await room.getParticipants({ tier: 'viewer', limit: 100, cursor });
for (const p of page.participants) renderViewerRow(p);
cursor = page.nextCursor;
} while (cursor);
const { total } = await room.getParticipants({ limit: 1 }); // returns 1 participant, but `total` covers all
ui.attendeesBadge.text(`${total} attendees`);
setOutputDevice async
Routes all SDK-managed remote audio playback to a specific output device. Internally calls setSinkId() on every internal <audio> element the SDK owns.
For users who attach their own <audio> via audio.attach(el), call el.setSinkId(deviceId) directly on those user-owned elements.
deviceβ output device from VideoSDK.getSpeakers(). Strongly typed β passing a camera or microphone is a compile-time error.
const speakers = await VideoSDK.getSpeakers();
await room.setOutputDevice(speakers[0]);
start*()rejects withALREADY_STARTEDunless the service is currentlystoppedorfailed.stop*()rejects withNOT_STARTEDunless it isstartingorstarted. Both checks are SDK-local β the call is rejected before any round-trip to the server.start*()resolves when the server accepts the request, not when the service is live. The service reachingstartedarrives later via the matching*-state-changedevent β server-side spin-up is not instant.- On a failure, a
*-state-changedevent fires with{ state: 'failed', error: SDKError }.
startRecording async
Starts server-side cloud recording of the room. opts is optional β when omitted, the project's configured recording defaults apply. Resolves once the server accepts the request; recording reaching started arrives later on recording-state-changed.
optsβ optional RecordingOptions. Omit to use project defaults.
await room.startRecording({ quality: 'high' });
stopRecording async
Stops server-side cloud recording. Rejects with NOT_STARTED if recording is not currently starting or started.
await room.stopRecording();
startHls async
Starts HLS streaming of the room. opts is optional β when omitted, the project's configured HLS defaults apply. Resolves once the server accepts the request; HLS reaching started (and the HlsUrls) arrives later on hls-state-changed.
optsβ optional HlsOptions. Omit to use project defaults.
await room.startHls();
stopHls async
Stops HLS streaming. Rejects with NOT_STARTED if HLS is not currently starting or started.
await room.stopHls();
startLivestream async
Starts an RTMP livestream of the room to one or more external destinations. opts is required β it must carry the outputs (RTMP URL + stream key per destination). Resolves once the server accepts the request; the livestream reaching started arrives later on livestream-state-changed.
optsβ required LivestreamOptions, including theoutputsarray of RTMP destinations.
await room.startLivestream({
outputs: [
{ url: 'rtmp://a.rtmp.youtube.com/live2', streamKey: 'xxxx' },
],
});
stopLivestream async
Stops the RTMP livestream. Rejects with NOT_STARTED if the livestream is not currently starting or started.
await room.stopLivestream();
startTranscription async
Starts live transcription of the room. config is optional β when omitted, the project's configured transcription defaults apply. Resolves once the server accepts the request; transcription reaching started arrives later on transcription-state-changed. Transcript snippets then stream in on transcription-text.
configβ optional TranscriptionConfig. Omit to use project defaults.
await room.startTranscription();
room.on('transcription-text', (e) => {
if (e.isFinal) appendCaption(e.participantId, e.text);
});
stopTranscription async
Stops live transcription. Rejects with NOT_STARTED if transcription is not currently starting or started.
await room.stopTranscription();
on / off
Register / remove room-level event listeners. See the events section below for the catalog.
Events
Every room event is lazy in v1. Fired on the room object β but the server streams a given event only while at least one listener is registered for it. The SDK refcount-gates every event automatically: registering the first listener subscribes upstream, removing the last one unsubscribes.
| Event | Payload | Notes |
|---|---|---|
participant-joined | (p: RemoteParticipant) | Speakers only. Viewer-tier joins do not fire this event (see remoteParticipants) β use participant-count-changed + getParticipants for viewers. Fires retroactively for speakers already in the room when you joined β register one handler to cover both already-present and future speakers. |
participant-left | (p: RemoteParticipant) | Speakers only. SDK auto-cleans listeners on the leaving participant. Viewer-tier exits are reflected only in participant-count-changed. |
participant-count-changed | { speaker: number, viewer: number, total: number } | Coalesced count update covering both tiers. Always available (lazy). Server emits at most one event per coalescing window (~250ms) so bursts don't flood. Use this for "X attendees" UI; pair with getParticipants to enumerate when needed. |
connection-state-changed | (e: ConnectionEvent) | Single event for every transition between ConnectionState values. Per-state data (reconnect attempt, disconnect reason) lives in nested optional blocks β only set when relevant. |
active-speaker-changed | { speakerId: string | null } | |
recording-state-changed | { state: ServiceState, error?: SDKError } | error set only when state is failed |
hls-state-changed | { state: ServiceState, urls?: HlsUrls, error?: SDKError } | HLS extends the standard lifecycle with playable β fires for every transition in stopped β starting β started β playable β stopping β stopped. urls set from started onward (minted but not yet playable) and remain through playable (safe to render). error set only when state is failed. Render the player on playable, not started β see hlsState. |
livestream-state-changed | { state: ServiceState, error?: SDKError } | error set only when state is failed |
transcription-state-changed | { state: ServiceState, error?: SDKError } | error set only when state is failed |
transcription-text | { participantId: string, text: string, isFinal: boolean, timestamp: Date } | Fires once per transcript snippet (per-message, not batched). isFinal: false = interim/live partial; true = finalized text. |
entry-requested | (req: EntryRequest) | Lobby request. Only delivered to participants with canModerate. Replays for pending requests on subscribe β a late-joining moderator's single handler covers backlog + live. Call req.admit() / req.deny(). See Waiting Lobby. |
entry-resolved | { participantId: string, by: string } | Lobby resolution. Fires when a request was acted on (by this or another moderator). Drop the row from a "pending" list in multi-moderator UIs. by = participantId of the moderator who admitted / denied. |
entry-canceled | { participantId: string, reason: 'aborted' | 'disconnected' | 'timeout' } | Lobby cancellation β request went away without a moderator decision. 'aborted' = joiner's AbortSignal fired; 'disconnected' = socket dropped past the grace window; 'timeout' = server ttl fired. |
Note β no publish/subscribe events on Room. Publish/unpublish/subscribe events fire on the participant they relate to (LocalParticipant for your own publishes, RemoteParticipant for remote). Whether to mirror them to room is tracked as Q11 β Event mirroring.
room.on('joined', β¦) or room.on('left', β¦) event. Both halves of the room lifecycle are signalled without a dedicated event:
- Joined β
await VideoSDK.join()resolves to a connected Room; the act of getting the room object is the "joined" signal. No event is needed (the lobby'sonWaitingcovers the special "waiting in lobby" sub-phase if you opt in). - Voluntary leave β
await room.leave()resolves when you're out. No event is needed. - Involuntary leave β removed by a moderator, room ended by the host, network gave up, or "joined elsewhere" β all route through the existing
connection-state-changedevent with the appropriateDisconnectReason(RemovePeer/RoomClose/WebsocketConnectionAttemptsExhausted/DuplicateParticipant).
participant-left is for other people leaving β your view of them β not for you leaving the room.)
// One handler covers both cases β SDK fires participant-joined retroactively
// for participants who were already in the room before you joined.
room.on('participant-joined', async (p) => {
await p.subscribe([MediaKind.Audio, MediaKind.Video]);
const tile = p.video.createElement();
tile.dataset.participantId = p.id;
document.querySelector('#grid').appendChild(tile);
});
room.on('participant-left', (p) => {
document.querySelector(`[data-participant-id="${p.id}"]`)?.remove();
// SDK has already invalidated p's streams and removed listeners on p
});
room.on('connection-state-changed', (e) => {
console.log(`${e.previous} β ${e.state}`);
if (e.state === ConnectionState.Reconnecting) {
// e.reconnecting is set; e.disconnected is undefined
showBanner(`Reconnecting⦠(attempt ${e.reconnecting.attempt}/${e.reconnecting.maxAttempts})`);
}
if (e.state === ConnectionState.Connected) {
hideBanner();
}
if (e.state === ConnectionState.Disconnected) {
// e.disconnected is set; e.reconnecting is undefined
switch (e.disconnected.reason) {
case DisconnectReason.ManualLeaveCalled: return; // user clicked Leave
case DisconnectReason.RemovePeer: showToast('You were removed'); break;
case DisconnectReason.RoomClose: showToast('Room ended'); break;
case DisconnectReason.WebsocketConnectionAttemptsExhausted: showToast('Connection lost'); break;
case DisconnectReason.DuplicateParticipant: showToast('Joined elsewhere'); break;
default: showToast('Disconnected');
}
navigateToHome();
}
});
See also: VideoSDK LocalParticipant RemoteParticipant
Related open questions: Q11 β Event mirroring on room