RemoteVideoStream interface
extends VideoStream REMOTE
A remote participant's camera stream. Adds server-driven state (streamState), simulcast quality control, and pause/resume on top of VideoStream. Access via p.video after subscribe().
Blueprint
Members added on top of VideoStream.
Properties (added)
Plus inherited from VideoStream: id, codec, dimensions, frameRate, contentHint, isPlaying, isPaused, isEnded.
streamState
Combined state of this stream โ server-driven routing state and client-observed decoder health, merged into one value. See StreamState.
Routing values (server-authoritative): active, paused, ended. Decoder values (observed from WebRTC stats; only when routing is active): frozen, stuck. Bandwidth-driven simulcast layer changes are signalled separately via currentQuality + the quality-changed event.
function updateBadge(p) {
switch (p.video?.streamState) {
case 'paused': return showBadge(p, 'Paused');
case 'frozen': return showBadge(p, 'Frozen');
case 'stuck': return showBadge(p, 'Buffering');
case 'ended': return removeTile(p);
default: return clearBadge(p); // 'active' = healthy
}
}
currentQuality
The simulcast layer currently being received. Distinct from streamState โ the stream is still active when the layer drops; the layer just changed. Driven by both your setPreferredQuality() calls and server-side bandwidth adaptation.
Methods (added)
Plus inherited from VideoStream: attach, createElement, getStats, getMediaStreamTrack.
setPreferredQuality async
Pick which simulcast layer to receive. Useful when rendering at different sizes โ main pane gets "high", sidebar gets "low".
Requires server-side simulcast. If simulcast is disabled, this call is a no-op (the only available layer is used).
qโ"high"/"medium"/"low"/"auto"."auto"lets SDK pick based on the size of attached elements.
// Main pane
mainPane.video.setPreferredQuality('high');
// Sidebar thumbnails
for (const tile of sidebarTiles) {
tile.video.setPreferredQuality('low');
}
pause / resume async
Stop / restart byte flow without dropping the subscription. On pause(), streamState transitions to "paused". Lighter than unsubscribe + resubscribe (no re-handshake).
const observer = new IntersectionObserver((entries) => {
for (const entry of entries) {
const p = entry.target.dataset.participantId;
const stream = room.remoteParticipants.get(p)?.video;
if (!stream) continue;
if (entry.isIntersecting) stream.resume();
else stream.pause();
}
});
document.querySelectorAll('.tile').forEach(t => observer.observe(t));
Events
Two stream-level events. state-changed covers every transition of streamState โ routing (paused / ended) and decoder observations (frozen / stuck) merged into one signal. quality-changed covers simulcast layer switches (a separate axis from streamState โ the stream is still active when the layer drops). Subscribe-completion events (video-subscribed, video-unsubscribed) fire on RemoteParticipant.
| Event | Payload |
|---|---|
state-changed | { state: StreamState } |
quality-changed | { prev: MediaQuality, current: MediaQuality } |
state-changed fires when streamState changes for any reason: routing transitions (you called pause/resume, publisher unpublished, terminal end) or client-side decoder observations (WebRTC stats detect freeze / stuck). Payload state matches the new value of streamState.
quality-changed fires when the actually received simulcast layer switches โ triggered by your own setPreferredQuality() call OR by server-side bandwidth adaptation. Payload prev/current are "high" / "medium" / "low"; "auto" is only a valid input to setPreferredQuality.
p.video.on('state-changed', ({ state }) => {
switch (state) {
case 'paused': tile.classList.add('paused'); break;
case 'frozen': tile.classList.add('frozen'); break;
case 'stuck': tile.classList.add('stuck'); break;
case 'ended': tile.remove(); break;
case 'active': tile.className = ''; break;
}
});
p.video.on('quality-changed', ({ prev, current }) => {
qualityBadge.textContent = current.toUpperCase(); // 'HIGH' / 'MEDIUM' / 'LOW'
});
frozen / unfrozen / stuck / ended events. They're folded into state-changed via the corresponding state values. Apps wanting "is it healthy?" check state === 'active'; apps wanting cleanup hooks check state === 'ended'.See also: VideoStream RemoteParticipant StreamState MediaQuality LocalVideoStream