LocalVideoStream interface
extends VideoStream LOCAL
Your camera stream. Adds device control, settings updates, and one-shot frame capture on top of VideoStream. Access via room.localParticipant.video after publishVideo(). Frame processors are managed globally via VideoSDK.applyVideoProcessor() โ not on the stream.
Blueprint
Members added on top of VideoStream.
Properties (added)
Plus all properties inherited from VideoStream: id, codec, dimensions, frameRate, contentHint, isPlaying, isPaused, isEnded.
inputDevice
Currently selected camera as a CameraDeviceInfo (combines deviceId, groupId, label). Use me.video.inputDevice.label for "Camera: X" UI; me.video.inputDevice.deviceId for storage / comparison.
Methods (added)
Plus all methods inherited from VideoStream: attach / detach, createElement, getStats, getMediaStreamTrack.
setInputDevice async
Hot-swap to a different camera. The stream instance is preserved โ SDK swaps the underlying track and rebinds all attached <video> elements transparently. Strongly typed โ passing a MicrophoneDeviceInfo or SpeakerDeviceInfo is a compile-time error.
Resolves when the new device is acquired and publishing on the new track. Rejects on permission denied, device busy, or if the device is no longer present.
const cameras = await VideoSDK.getCameras();
const back = cameras.find(c => c.label.toLowerCase().includes('back'));
if (back) await me.video.setInputDevice(back);
// Attached <video> elements continue rendering โ no re-attach needed
updateSettings async
Change resolution and/or frame rate mid-stream without re-publishing. Internally calls applyConstraints() on the underlying track.
await me.video.updateSettings({ resolution: 'h1080', frameRate: 60 });
setContentHint
Encoder optimization hint. "motion" for high-motion content (gameplay, sports). "detail" for sharpness-critical static content (slide presentations, document cameras).
VideoSDK.applyVideoProcessor() to apply a frame transform. It survives publish, unpublish, re-publish, and setInputDevice swaps. No per-stream setProcessor method.getInputCapabilities async
Reports what the current device can do โ max resolution, frame rates, facingModes, etc. Wraps the browser's MediaStreamTrack.getCapabilities().
const caps = await me.video.getInputCapabilities();
console.log(`Max ${caps.width.max}ร${caps.height.max} @ ${caps.frameRate.max}fps`);
console.log('Facing modes:', caps.facingMode);
stop async PRE-CALL
Aborts a pre-call preview stream. Releases the underlying camera (MediaStreamTrack stopped) and clears the VideoSDK.videoStream singleton slot so the next createVideoStream succeeds. After stop(), all methods on this instance throw STREAM_STOPPED.
me.video, use me.unpublishVideo() instead โ it does full teardown (unpublish + release device).const preview = await VideoSDK.createVideoStream({ device: cameras[0] });
preview.attach(previewEl);
// User clicks "Cancel" instead of "Join"
await preview.stop();
// camera released, VideoSDK.videoStream is null again
Related open questions: Q18 โ Pre-call preview lifecycle (singleton vs render-only)
captureFrame async LOCAL ONLY
One-shot snapshot of the current frame as a Base64-encoded data URL. Useful for thumbnails, profile-photo capture, AI vision input, or sending a snapshot to a server.
Not available on remote streams โ to snapshot a remote video, use canvas.drawImage() on the user-attached <video> element.
Two ways to size the output โ pick whichever is more natural. aspectRatio (with an optional single dimension) gives you a target shape; width / height give you exact pixel dimensions. The resolution rules:
| Inputs | Resolved output dimensions |
|---|---|
{} (no sizing options) | Source frame's natural dimensions |
{ width, height } | Exactly width ร height |
{ width } only | width ร (width / sourceRatio) โ preserves source ratio |
{ height } only | (height ร sourceRatio) ร height โ preserves source ratio |
{ aspectRatio } only | sourceWidth ร (sourceWidth / aspectRatio) |
{ aspectRatio, width } | width ร (width / aspectRatio) |
{ aspectRatio, height } | (height ร aspectRatio) ร height |
{ aspectRatio, width, height } | โ throws INVALID_CAPTURE_OPTIONS โ pick one approach |
opts.aspectRatioโ Target aspect ratio. Accepts a number (16/9) or a"W:H"string ('16:9','4:3','1:1'). Cannot be combined with BOTHwidthandheight.opts.width/opts.heightโ Output dimensions in pixels. If only one is provided (with noaspectRatio), the other is derived from the source frame's natural ratio.opts.fitโ How to fit the source frame into the output canvas when aspect ratios differ. Default'cover'.'cover'(default) โ scale to fully fill; crop overflow centered. Same default as CSSobject-fit: cover.'contain'โ scale to fit inside; pad with black bars (JPEG-compatible). Preserves the entire source.'stretch'โ scale to canvas dimensions exactly. Distorts when ratios differ.
opts.qualityโ0..1, default0.9. JPEG quality.
Resolves with a data URL string: "data:image/jpeg;base64,..." โ assignable directly to img.src, postable to a server, or savable to disk. Rejects with INVALID_CAPTURE_OPTIONS if options conflict or values are invalid.
{ aspectRatio: '16:9' } โ output is 1024ร576 (16:9 at the video's natural width). The three fit options produce different visual results:
'cover'โ scales source 1:1; crops 96 px off top and 96 px off bottom. Fills the frame.'contain'โ scales source down to 768ร576; adds 128 px black bars on left and right. Preserves the full frame.'stretch'โ squishes source from 768 โ 576 height; faces appear wider. Generally not what you want.
// 256ร256 from any source aspect, cropping to the center (default 'cover')
const dataUrl = await me.video.captureFrame({
aspectRatio: '1:1',
width: 256,
});
document.querySelector('#avatar').src = dataUrl;
// Force 1280ร720 (16:9); keep the entire source visible with bars if needed
const dataUrl = await me.video.captureFrame({
aspectRatio: '16:9',
width: 1280,
fit: 'contain', // black bars instead of cropping
quality: 0.85,
});
// No sizing options โ source frame's natural dimensions
const dataUrl = await me.video.captureFrame({ quality: 0.85 });
document.querySelector('#preview').src = dataUrl;
// Upload
await fetch('/snapshot', {
method: 'POST',
body: JSON.stringify({ image: dataUrl }),
});
// Download as file
const a = document.createElement('a');
a.href = dataUrl;
a.download = 'snapshot.jpg';
a.click();
Events
There are no publish/unpublish events on LocalParticipant โ the publishVideo Promise delivers the stream directly.
| Event | Payload | When |
|---|---|---|
ended | { timestamp: number } | Camera source ended โ device unplugged, OS revoked permission, browser tab lost device access. |
frozen / stuck) and routing transitions (paused / ended) live on the merged streamState + state-changed event of RemoteVideoStream. Local streams have no routing state โ they describe the local capture pipeline, not a receive-side decoder.See also: VideoStream LocalParticipant VideoFrameProcessor RemoteVideoStream