VideoSDK namespace
Static entry point for the SDK. Use it to join rooms and enumerate input/output devices. Everything else hangs off the Room object returned by join.
Blueprint
Full surface at a glance β every static method on this namespace, plus pre-call surface (permissions, devices, tests, preview streams).
Methods
join async
Connects to a room and returns a fully-connected Room. The Promise resolves only after the join handshake is complete β there is no "before-join" window where listener registration could be silently dropped.
optsβ JoinOptions with token, roomId, optional name/metadata, optional pre-warmed event channels, and optional waiting-lobby hooks (onWaiting,signal) for tokens withjoinPolicy.mode === 'ask'.
Promise that resolves with a connected Room. Rejects with an SDKError. See Errors β VideoSDK.join() for the full inventory of 21 codes this method can reject with (including the four lobby codes: ENTRY_DENIED, ENTRY_TIMEOUT, ENTRY_RATE_LIMITED, INVALID_ENTRY_CLAIM).
Async publish failures (camera busy, mic in use, etc.) are not in this inventory. Publish runs async after join() resolves (decision 36) β failures fire on me.on('stream-publish-failed', failure) instead. See Errors β stream-publish-failed.
import { VideoSDK } from '@videosdk/js';
try {
const room = await VideoSDK.join({
token: getAuthToken(),
roomId: 'team-standup',
name: 'Alice',
metadata: { role: 'host' },
subscribeEvents: ['active-speaker-changed'], // pre-warm lazy events
});
console.log('connected', room.localParticipant.id);
} catch (err) {
if (err.kind === ErrorKind.Auth) showLoginUI(); // any auth failure
else if (err.code === 'TIMEOUT') showNetworkError();
else if (err.code === 'ROOM_FULL') showRoomFullToast();
else showGenericError(err);
}
const cameras = await VideoSDK.getCameras();
const mics = await VideoSDK.getMicrophones();
const room = await VideoSDK.join({
token: getAuthToken(),
roomId: 'team-standup',
name: 'Alice',
publishVideo: { device: cameras[0], resolution: 'h720' },
publishAudio: { device: mics[0], noiseSuppression: true },
});
// Connected AND publishing β single round-trip.
const cameras = await VideoSDK.getCameras();
// Pre-call screen
const previewVideo = await VideoSDK.createVideoStream({ device: cameras[0], resolution: 'h720' });
const previewAudio = await VideoSDK.createAudioStream({ device: (await VideoSDK.getMicrophones())[0] });
previewVideo.attach(previewEl);
await VideoSDK.testNetwork();
// Join β local streams auto-promote
const room = await VideoSDK.join({ token, roomId, name });
// previewVideo === room.localParticipant.video (no re-acquisition, no flicker)
joinPolicy.mode === 'ask')// Same join() call. With an 'ask' token, the Promise stays pending while the
// joiner is in the lobby β onWaiting fires once so you can render the lobby UI.
const controller = new AbortController();
ui.onCancelClick = () => controller.abort(); // explicit "Cancel" button
try {
const room = await VideoSDK.join({
token,
roomId,
name: 'Patient 9',
onWaiting: () => ui.showLobbyScreen('Waiting for the doctorβ¦'),
signal: controller.signal,
});
ui.showCallScreen(room);
} catch (err) {
if (err.code === 'ENTRY_DENIED') ui.showDeniedScreen();
else if (err.code === 'ENTRY_TIMEOUT') ui.showTimeoutScreen();
else if (err.name === 'AbortError') ui.showHome();
else showGenericError(err);
}
// Tab close needs no code β the SDK's socket disconnect cancels the lobby entry.
// Full reference: waiting-lobby.html
Related: Q14 β One-step vs two-step join Q15 β Pre-warm signaling
getCameras async
Lists available cameras. Returns typed CameraDeviceInfo objects (not plain MediaDeviceInfo) so that camera references are nominally distinct from microphones / speakers β strongly-typed languages catch passing the wrong kind at compile time.
const cameras = await VideoSDK.getCameras();
const front = cameras.find(c => c.label.includes('FaceTime'));
await me.video.setInputDevice(front); // pass the typed object
getMicrophones async
Lists available microphones. Returns typed MicrophoneDeviceInfo objects.
const mics = await VideoSDK.getMicrophones();
console.log(mics.map(m => m.label));
getSpeakers async
Lists available output devices. Returns typed SpeakerDeviceInfo objects. Pass one to room.setOutputDevice() to route remote audio to a specific speaker / headset.
const speakers = await VideoSDK.getSpeakers();
const headset = speakers.find(s => s.label.toLowerCase().includes('headset'));
await room.setOutputDevice(headset);
Permissions
checkPermissions async
Query the browser's current permission state for the listed kinds. Doesn't prompt the user β just reads cached permission state via navigator.permissions.query().
Map keyed by kind, value is true if previously granted, false if denied or not yet prompted.
const perms = await VideoSDK.checkPermissions([MediaKind.Audio, MediaKind.Video]);
if (!perms.get(MediaKind.Video)) showRequestUI();
requestPermissions async
Prompts the user to grant access to the listed kinds. Acquires a temporary getUserMedia stream just to trigger the permission dialog, then releases it. Use this if you want explicit "allow access" UX before showing pre-call.
Map keyed by kind with the result of the prompt. Doesn't reject on denial β denied permissions return false.
const granted = await VideoSDK.requestPermissions([MediaKind.Audio, MediaKind.Video]);
if (granted.get(MediaKind.Video)) {
const preview = await VideoSDK.createVideoStream({ deviceId: 'default' });
preview.attach(previewEl);
}
Pre-call test
runPreCallTest async
Comprehensive pre-call diagnostic β runs server reachability + SFU connection (connectivity), device validation (media), and uplink/downlink quality measurement (quality) in a single shared session. Returns a stoppable promise β call .stop() to skip remaining phases.
A StoppablePromise β same shape as a Promise (you can await it) plus a .stop() method to abort early. Resolves with a PreCallTestResult containing per-phase results, errors, and metadata. Rejects only on fatal init failures (invalid token, browser unsupported, etc.).
const result = await VideoSDK.runPreCallTest({ token });
if (result.quality?.audioOnly) {
showWarning('Network is too slow for video β audio-only recommended');
}
if (result.connectivity?.turnUsed) {
showWarning('Using TURN relay β may add latency');
}
const test = VideoSDK.runPreCallTest({
token,
onProgress: ({ phase, data }) => {
if (phase === 'media') renderDeviceStatus(data);
if (phase === 'connectivity') renderServerStatus(data);
},
onError: ({ code, message, phase }) => {
showWarning(`[${phase}] ${message}`);
},
});
joinButton.onclick = async () => {
test.stop(); // skip remaining quality test
const result = await test; // resolves with partial results, aborted: true
await VideoSDK.join({ token, roomId, name }); // local streams auto-promote
};
const result = await VideoSDK.runPreCallTest({
token,
tests: [PreCallPhase.Connectivity],
});
console.log('Region:', result.connectivity?.region);
console.log('Protocol:', result.connectivity?.protocol);
console.log('TURN used:', result.connectivity?.turnUsed);
Concurrency. Only one runPreCallTest can be in flight at a time. Calling it while one is running rejects with TEST_ALREADY_RUNNING. Call .stop() on the existing test first.
Error handling. Three layers: (1) Promise rejection for fatal init errors (invalid token, browser not supported); (2) onError callback for non-fatal phase errors as they happen; (3) result.errors[] array for post-test inspection. See PreCallTestResult.
Local pre-call streams (singleton-tracked)
Pre-call local streams. The SDK keeps an internal slot per kind; join auto-promotes whichever local streams exist into room.localParticipant.video / audio. No flicker β the same track that drove the pre-call preview becomes the published one.
createVideoStream while one already exists throws STREAM_ALREADY_EXISTS. To swap configurations, call stop() on the existing one first. To swap devices, use the existing stream's setInputDevice() instead.precallPreview({ render }) method with auto-dispose on join. See Q18 β Pre-call preview lifecycle for the comparison and the use cases driving the discussion.createVideoStream async
Acquires the camera and returns a LocalVideoStream. The next join auto-promotes this stream β same instance becomes room.localParticipant.video.
Resolves with the LocalVideoStream. Rejects on permission denied, device busy, no such device, or if a video stream already exists (STREAM_ALREADY_EXISTS).
const cameras = await VideoSDK.getCameras();
const mics = await VideoSDK.getMicrophones();
// Show pre-call UI with live local stream
const camStream = await VideoSDK.createVideoStream({ device: cameras[0], resolution: 'h720' });
const micStream = await VideoSDK.createAudioStream({ device: mics[0] });
camStream.attach(previewEl);
levelMeter.bind(() => micStream.audioLevel);
await camStream.setInputDevice(cameras[1]); // hot-swap to another camera
await VideoSDK.runPreCallTest({ token });
// Join β local streams auto-promote, no flicker
const room = await VideoSDK.join({ token, roomId, name });
// camStream === room.localParticipant.video
Related open questions: Q18 β Pre-call preview lifecycle (singleton vs render-only)
createAudioStream async
Acquires the microphone and returns a LocalAudioStream. The next join auto-promotes this stream.
videoStream / audioStream readonly
Read-only sync getters that return the currently active local stream (or null). Useful for debugging, conditional UI ("show pre-call screen if a local stream exists"), and React component integration.
After join, the same stream instance is also reachable via room.localParticipant.video / room.localParticipant.audio. While in pre-call (no join yet), use these getters.
Processors
Sticky, per-kind frame transform pipelines for camera and microphone. Set once β applies to every camera / mic stream of the local participant for as long as it's set. Survives publish, unpublish, re-publish, setInputDevice swaps, and reconnects.
processor in opts.The VideoFrameProcessor / AudioFrameProcessor interface is a single-method contract β the app implements process(frame). The SDK calls it once per captured frame; the returned frame is what gets encoded and published. App owns the lifecycle of its own resources (models, canvases, WASM, etc.) β the SDK never touches them.
applyVideoProcessor async
Sets the active video processor. Replaces any previously-set one. Applies immediately to the current me.video (if publishing) and to any future camera stream. Resolves once the processor is in place and the frame pipeline is wired through it.
// User clicks "Beach background"
const beachBg: VideoFrameProcessor = {
async process(frame) {
const mask = await segmentationModel.segment(frame);
return composite(frame, mask, beachImage);
},
};
await VideoSDK.applyVideoProcessor(beachBg);
// me.video now publishes with beach background β no flicker, no unpublish
applyAudioProcessor async
Sets the active audio processor. Replaces any previously-set one. Applies immediately to me.audio (if publishing) and to any future mic stream.
const noiseSuppress: AudioFrameProcessor = {
process(frame) { return rnnoise.run(frame); },
};
await VideoSDK.applyAudioProcessor(noiseSuppress);
removeVideoProcessor / removeAudioProcessor async
Removes the current processor for that kind. Streams revert to publishing raw camera / mic frames. Resolves once the SDK has unwired the processor β subsequent frames flow without per-frame app code.
After this resolves, the SDK no longer calls processor.process(frame). App can dispose its own resources (models, WebGL contexts, WASM memory) whenever it wants β the SDK doesn't manage them.
videoProcessor / audioProcessor readonly
Read-only sync getters that return the currently active processor (or null). Useful for conditional UI ("show 'Effect: Beach' badge if a processor is active") and debugging.
Events
devices-changed
Fires when the system's set of media devices changes β e.g. user plugs in a USB headset, an HDMI display is connected, or a Bluetooth device pairs. Apps that show device dropdowns should re-fetch via getCameras / getMicrophones / getSpeakers when this fires.
VideoSDK.on('devices-changed', async () => {
cameraSelect.options = await VideoSDK.getCameras();
micSelect.options = await VideoSDK.getMicrophones();
});
Pre-call lifecycle & auto-promote rules
Per kind (video / audio / screen), join resolves what to publish in this order:
- Local stream exists (from createVideoStream / createAudioStream) β auto-promoted; same instance becomes
me.video/me.audio. - Else if JoinOptions
.publishVideo / .publishAudiois set β SDK acquires device and publishes during the join handshake (one server round-trip). - Else β no publish for that kind. App can call
me.publishX(...)later.
Conflict: if both a local stream AND JoinOptions opts are provided for the same kind, join rejects with CONFLICTING_PUBLISH_CONFIG. To resolve, call VideoSDK.videoStream.stop() (or audioStream.stop()) first.
Logging
setLogLevel
Sets the SDK's logging verbosity globally. Affects all SDK logs (console + dashboard / telemetry). Apps usually call this once at startup; can also dial up to Debug when collecting bug reports.
levelβ see LogLevel for values. DefaultLogLevel.Info.
import { VideoSDK, LogLevel } from '@videosdk/js';
// At app startup β silence SDK logs in production
VideoSDK.setLogLevel(LogLevel.Warn);
// Bump up when collecting a bug report
debugButton.onclick = () => VideoSDK.setLogLevel(LogLevel.Debug);
See also: Room JoinOptions LocalParticipant