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.

class Room { // Participants localParticipant: LocalParticipant; remoteParticipants: Map<string, RemoteParticipant>; // Messaging β€” publish / subscribe / getHistory on topic-based messaging pubsub: PubSub; // Connection state β€” read sync; observe transitions via 'connection-state-changed' readonly state: ConnectionState; // Server-side feature state β€” read sync; observe via the *-state-changed events readonly recordingState: ServiceState; readonly hlsState: ServiceState; readonly livestreamState: ServiceState; readonly transcriptionState: ServiceState; readonly hlsUrls: HlsUrls | null; // Methods leave(): Promise<void>; setOutputDevice(device: SpeakerDeviceInfo): Promise<void>; // Server-side features β€” start / stop startRecording(opts?: RecordingOptions): Promise<void>; stopRecording(): Promise<void>; startHls(opts?: HlsOptions): Promise<void>; stopHls(): Promise<void>; startLivestream(opts: LivestreamOptions): Promise<void>; stopLivestream(): Promise<void>; startTranscription(config?: TranscriptionConfig): Promise<void>; stopTranscription(): Promise<void>; // Event handlers on(event: string, listener: Function): void; off(event: string, listener: Function): void; // === Events fired === // 'participant-joined' (p: RemoteParticipant) β€” fires retroactively on join // 'participant-left' (p: RemoteParticipant) β€” SDK auto-cleans listeners // 'connection-state-changed' (e: ConnectionEvent) β€” single event for all state transitions // 'active-speaker-changed' ({ speakerId }) β€” lazy // 'recording-state-changed' (e: { state: ServiceState, error?: SDKError }) // 'hls-state-changed' (e: { state: ServiceState, urls?: HlsUrls, error?: SDKError }) // 'livestream-state-changed' (e: { state: ServiceState, error?: SDKError }) // 'transcription-state-changed' (e: { state: ServiceState, error?: SDKError }) // 'transcription-text' (e: { participantId, text, isFinal, timestamp }) }

Properties

localParticipant

You β€” the local participant in this room.

localParticipant: LocalParticipant

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.

remoteParticipants: Map<string, RemoteParticipant>
Speakers only β€” viewers intentionally not in this map. Viewer-tier participants (audience members) are not tracked client-side as individual RemoteParticipant objects β€” that's the model Decision 7 commits to so large rooms (1,000+ viewers) stay scalable. To work with viewers, use participant-count-changed (live count) and room.getParticipants({ tier: 'viewer' }) (paginated enumeration). See Tier & participant events for the underlying model.
You usually don't need to iterate this map. The 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.

pubsub: PubSub
Example β€” send a chat message
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.

readonly state: ConnectionState
Example β€” guard a UI action that requires an active connection
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.

readonly recordingState: ServiceState

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.

readonly hlsState: ServiceState
HLS uses the extended 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.

readonly livestreamState: ServiceState

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.

readonly transcriptionState: ServiceState

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.

readonly hlsUrls: HlsUrls | null
HLS-only viewers are out of SDK scope. A "pure HLS viewer" β€” someone who just opens 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).

readonly entryRequests: EntryRequest[]

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.

leave(): Promise<void>
Example
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.

getParticipants(opts?: GetParticipantsOpts): Promise<GetParticipantsResult>
Parameters
  • 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's nextCursor.
Returns

{ 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.

One RemoteParticipant class for both tiers. Viewer-tier instances carry isViewer: true; calling subscribe() / publishX() / similar on them rejects with INVALID_PERMISSIONS. Speakers behave normally.
Example β€” enumerate all viewers
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);
Example β€” render the "1,213 attendees" badge without a parallel listen
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.

setOutputDevice(device: SpeakerDeviceInfo): Promise<void>
Parameters
  • device β€” output device from VideoSDK.getSpeakers(). Strongly typed β€” passing a camera or microphone is a compile-time error.
Example
const speakers = await VideoSDK.getSpeakers();
await room.setOutputDevice(speakers[0]);
Shared behavior of all four start/stop pairs β€” recording, HLS, livestream, and transcription.
  • start*() rejects with ALREADY_STARTED unless the service is currently stopped or failed. stop*() rejects with NOT_STARTED unless it is starting or started. 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 reaching started arrives later via the matching *-state-changed event β€” server-side spin-up is not instant.
  • On a failure, a *-state-changed event 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.

startRecording(opts?: RecordingOptions): Promise<void>
Parameters
Example
await room.startRecording({ quality: 'high' });

stopRecording async

Stops server-side cloud recording. Rejects with NOT_STARTED if recording is not currently starting or started.

stopRecording(): Promise<void>
Example
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.

startHls(opts?: HlsOptions): Promise<void>
Parameters
  • opts β€” optional HlsOptions. Omit to use project defaults.
Example
await room.startHls();

stopHls async

Stops HLS streaming. Rejects with NOT_STARTED if HLS is not currently starting or started.

stopHls(): Promise<void>
Example
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.

startLivestream(opts: LivestreamOptions): Promise<void>
Parameters
  • opts β€” required LivestreamOptions, including the outputs array of RTMP destinations.
Example
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.

stopLivestream(): Promise<void>
Example
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.

startTranscription(config?: TranscriptionConfig): Promise<void>
Parameters
Example
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.

stopTranscription(): Promise<void>
Example
await room.stopTranscription();

on / off

Register / remove room-level event listeners. See the events section below for the catalog.

on(eventName: string, listener: Function): void off(eventName: string, listener: Function): void

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.

EventPayloadNotes
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.

Note β€” there is no 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's onWaiting covers 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-changed event with the appropriate DisconnectReason (RemovePeer / RoomClose / WebsocketConnectionAttemptsExhausted / DuplicateParticipant).
One event surface for every disconnect cause, plus a Promise for every deliberate action β€” no parallel handlers to wire, no "which fires first" anxiety. (participant-left is for other people leaving β€” your view of them β€” not for you leaving the room.)
Example β€” render every remote participant (already-present and future)
// 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
});
Example β€” connection state UX (reconnecting banner + disconnect routing)
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