VideoStream interface
Common shape for camera-style video streams. The base interface is also the type of remote-side video; LocalVideoStream and RemoteVideoStream extend it with side-specific functionality. ScreenStream extends it for screen-share streams.
Blueprint
Full surface at a glance โ every property and method on this interface. Subtypes (Local / Remote / Screen) add to this. No events on the base interface.
Properties
id
Server-assigned stream id (also called the SID). Stable across reconnects within the same publish.
codec
Codec currently in use. Useful for debug overlays and codec-specific UI.
dimensions
Current video dimensions. Null if not yet known (pre-first-frame).
frameRate
Current frames-per-second. Null if not yet known.
contentHint
Encoder hint. "motion" optimizes for high-motion content; "detail" for sharpness in static content (e.g. shared documents).
isPlaying / isPaused / isEnded
Sync booleans reflecting the local rendering state of this stream.
Methods
attach / detach
Render this stream into an HTML <video> element. attach is idempotent and time-independent โ call any time, even before subscription completes; the SDK queues the attach and binds when media is available.
The same stream can be attached to multiple elements (main pane + thumbnail). When the underlying track is swapped (device change, reconnect), all attached elements continue to render โ the SDK rebinds them transparently.
Auto-detach on stream end. The SDK tracks every element passed to attach(). When the stream transitions to 'ended' (publisher unpublished, you unsubscribed, network terminated), the SDK iterates the tracked elements and clears their srcObject automatically. You only need to call detach() when re-routing a still-active stream to a different element.
Lifecycle & flow
- Create or obtain a
<video>element. The SDK doesn't care if it's already in the DOM, mounted later, or has user-suppliedstyle/classattributes.autoplayand (on mobile)playsinlineshould be set by you on the element. - Call
stream.attach(el). SDK setsel.srcObjectto a managedMediaStreamwrapping the stream's track and registers the element internally. - Track changes (device swap, reconnect) are transparent. The SDK rebinds
srcObjecton every registered element when the underlying track is replaced โ no app action required. - Call
stream.detach(el)only to re-route. Removeselfrom the registry and clears itssrcObject. Other attached elements keep rendering. You don't need to detach for cleanup โ the SDK handles that on stream end (see auto-detach above). - Stream ends (publisher unpublished, network died, you unsubscribed). SDK iterates the registry, clears
srcObjecton every attached element, and firesvideo-unsubscribedon the participant. App removes the tile element in that handler.
// 1. Wire up the participant โ fires retroactively for already-present participants too.
room.on('participant-joined', (p) => {
// 2. When a stream is delivered, attach to your <video> element(s).
p.on('stream-subscribed', (sub) => {
if (!sub.video) return; // we only handle video here
const stream = sub.video;
const tile = createTile(p); // your UI factory
const main = tile.querySelector('video.main');
const thumb = tile.querySelector('video.thumb');
stream.attach(main); // primary render target
stream.attach(thumb); // optional thumbnail
document.querySelector('#grid').appendChild(tile);
// Optional: react to non-terminal transitions on the same stream.
stream.on('state-changed', ({ state }) => {
tile.classList.toggle('frozen', state === 'frozen');
tile.classList.toggle('paused', state === 'paused');
});
});
// 3. Re-route mid-call (e.g., user pinned this participant โ promote to main pane).
pinButton.onclick = () => {
p.video?.detach(thumb); // remove from thumbnail
p.video?.attach(mainStageEl); // attach to main stage
// SDK keeps `p.video` stable across the move; no re-subscribe needed.
};
// 4. Cleanup. Fires whether YOU unsubscribed or the publisher unpublished.
// By the time this runs, srcObject is already cleared on attached elements.
p.on('stream-unsubscribed', ({ kind }) => {
if (kind === MediaKind.Video) {
document.querySelector(`[data-pid="${p.id}"]`)?.remove();
}
});
});
// 5. Drop the subscription explicitly (offscreen tile, leaving room) โ Promise resolves
// when server acks. The video-unsubscribed event fires immediately after.
await p.unsubscribe([MediaKind.Video]);
attach / detach is the lower-level primitive. On top of it, the SDK will ship declarative components that wrap this flow per framework โ <RemoteVideoView /> for React (web + RN), a Flutter widget (RemoteVideoView), a SwiftUI / iOS UIView (VideoSDKVideoView), and an Android Compose / FrameLayout equivalent. Apps using a framework will typically reach for the component; attach remains for vanilla-JS use, custom render pipelines, and platforms without a first-party component yet. Component shapes are tracked as an open design item โ see Q13 in the rationale doc.createElement
Convenience helper โ SDK creates a pre-configured <video> element wrapped in a <div> container. Sets autoplay, mobile attributes (playsinline, x5-playsinline), srcObject, and IntersectionObserver-based adaptive subscription pause.
Use this when you don't need to control the element's attributes yourself.
const tile = p.video.createElement({
containerStyle: { height: '300px', width: '300px' },
videoStyle: { objectFit: 'cover' },
});
document.querySelector('#grid').appendChild(tile);
getStats async
One-shot statistics snapshot. Includes bitrate, framerate, packet loss, jitter, codec details, and resolution. For live monitoring (per-frame UI), poll at your desired interval.
setInterval(async () => {
const stats = await p.video.getStats();
document.querySelector('#kbps').textContent = `${(stats.bitrate / 1000) | 0} kbps`;
}, 2000);
getMediaStreamTrack escape hatch
Returns the underlying browser MediaStreamTrack for advanced use cases โ MediaRecorder, Insertable Streams API, custom WebRTC integrations, computer vision pipelines.
MediaStreamTrack is owned by the SDK. Do not call .stop(), .clone(), or .applyConstraints() on it โ doing so will break SDK invariants and produce undefined behavior. For lifecycle, use publish/unpublish on the participant. For settings, use updateSettings().const track = me.video.getMediaStreamTrack();
const recorder = new MediaRecorder(new MediaStream([track]));
recorder.ondataavailable = e => uploadChunk(e.data);
recorder.start(1000);
Events
The base interface defines no events. Operational events live on the subtypes:
- RemoteVideoStream โ
state-changed(routing + decoder: paused / frozen / stuck / ended) andquality-changed(simulcast layer switches). - LocalVideoStream โ
ended(camera disconnected, permission revoked).
Lifecycle events (video-published, video-subscribed, etc.) fire on the participant โ see LocalParticipant / RemoteParticipant.
See also: LocalVideoStream RemoteVideoStream ScreenStream AudioStream
Related open questions: Q13 โ Stream object abstraction across platforms