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.

interface VideoStream { // Identity readonly id: string; readonly codec: string; // "vp8" | "vp9" | "h264" | "av1" readonly dimensions: { width: number; height: number } | null; readonly frameRate: number | null; readonly contentHint: ContentHint | null; // Sync state flags readonly isPlaying: boolean; readonly isPaused: boolean; readonly isEnded: boolean; // Render โ€” auto-detach on stream end attach(el: HTMLVideoElement): void; detach(el: HTMLVideoElement): void; // SDK-managed render element createElement(opts?: StreamElementOpts): HTMLDivElement; // Stats getStats(): Promise<VideoStats>; // Escape hatch โ€” raw track for advanced use cases getMediaStreamTrack(): MediaStreamTrack; // No events on the base โ€” see subtypes for state-changed / quality-changed / ended }

Properties

id

Server-assigned stream id (also called the SID). Stable across reconnects within the same publish.

readonly id: string

codec

Codec currently in use. Useful for debug overlays and codec-specific UI.

readonly codec: string // "vp8" | "vp9" | "h264" | "av1"

dimensions

Current video dimensions. Null if not yet known (pre-first-frame).

readonly dimensions: { width: number; height: number } | null

frameRate

Current frames-per-second. Null if not yet known.

readonly frameRate: number | null

contentHint

Encoder hint. "motion" optimizes for high-motion content; "detail" for sharpness in static content (e.g. shared documents).

readonly contentHint: "motion" | "detail" | null

isPlaying / isPaused / isEnded

Sync booleans reflecting the local rendering state of this stream.

readonly isPlaying: boolean readonly isPaused: boolean readonly isEnded: boolean

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.

attach(el: HTMLVideoElement): void detach(el: HTMLVideoElement): void

Lifecycle & flow

  1. Create or obtain a <video> element. The SDK doesn't care if it's already in the DOM, mounted later, or has user-supplied style/class attributes. autoplay and (on mobile) playsinline should be set by you on the element.
  2. Call stream.attach(el). SDK sets el.srcObject to a managed MediaStream wrapping the stream's track and registers the element internally.
  3. Track changes (device swap, reconnect) are transparent. The SDK rebinds srcObject on every registered element when the underlying track is replaced โ€” no app action required.
  4. Call stream.detach(el) only to re-route. Removes el from the registry and clears its srcObject. Other attached elements keep rendering. You don't need to detach for cleanup โ€” the SDK handles that on stream end (see auto-detach above).
  5. Stream ends (publisher unpublished, network died, you unsubscribed). SDK iterates the registry, clears srcObject on every attached element, and fires video-unsubscribed on the participant. App removes the tile element in that handler.
Example โ€” full attach / detach flow
// 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]);
Roadmap โ€” framework-specific render components. Imperative 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.

createElement(opts?: StreamElementOpts): HTMLDivElement
Example
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.

getStats(): Promise<VideoStats>
Example โ€” quality indicator
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.

getMediaStreamTrack(): MediaStreamTrack
Lifecycle contract. The returned 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().
Example โ€” record local video to disk
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:

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