Types

Option objects and device-info classes passed across the API. All optional fields use sensible defaults β€” pass only what you want to override.

Device info

Three distinct, nominally-typed classes returned by VideoSDK.getCameras() / getMicrophones() / getSpeakers(). Same shape, different types β€” strongly-typed languages catch passing the wrong kind to a setter (e.g. a microphone where a camera is expected) at compile time.

CameraDeviceInfo

class CameraDeviceInfo { readonly deviceId: string; readonly groupId: string; readonly label: string; }

Returned by VideoSDK.getCameras(). Pass the whole object to setInputDevice, PublishVideoOpts.device, etc.

MicrophoneDeviceInfo

class MicrophoneDeviceInfo { readonly deviceId: string; readonly groupId: string; readonly label: string; }

Returned by VideoSDK.getMicrophones(). Pass to setInputDevice, PublishAudioOpts.device, etc.

SpeakerDeviceInfo

class SpeakerDeviceInfo { readonly deviceId: string; readonly groupId: string; readonly label: string; }

Returned by VideoSDK.getSpeakers(). Pass to room.setOutputDevice, VideoSDK.testSpeaker.

No kind field, no shared base class. The class identity is the discriminator β€” cameras[0] instanceof CameraDeviceInfo is the only check needed (and your type system gives it to you for free). Capability fields (e.g. facingMode, maxResolution) can be added per class later as additive non-breaking changes.

LocalPublication

Payload of the stream-published event on LocalParticipant. Represents your local stream that just published. Exactly one of video / audio / screen is set, matching kind.

interface LocalPublication { kind: MediaKind; video?: LocalVideoStream; // set iff kind === MediaKind.Video audio?: LocalAudioStream; // set iff kind === MediaKind.Audio screen?: LocalScreenStream; // set iff kind === MediaKind.Screen }

Fires for every publish path: explicit me.publishVideo(), async JoinOptions.publishVideo, and pre-call auto-promote (createVideoStream β†’ join).

Subscription

Payload of the stream-subscribed event on RemoteParticipant. Represents the freshly-subscribed remote stream. Exactly one of video / audio / screen is set, matching kind.

interface Subscription { kind: MediaKind; video?: RemoteVideoStream; audio?: RemoteAudioStream; screen?: RemoteScreenStream; }

Fires once per kind as that kind's subscribe pipeline becomes ready. Subscribing to [Audio, Video] fires twice β€” once with kind: Audio, once with kind: Video.

PublishRequest

Payload of the stream-publish-requested event on LocalParticipant. Fires when another participant calls requestPublishX() targeting you. You decide β€” call accept() to publish (returns a LocalPublication with the published stream) or reject() to silently decline.

interface PublishRequest { kind: MediaKind; from: RemoteParticipant; // who asked accept: () => Promise<LocalPublication>; // SDK publishes with defaults; resolves with the publication reject: () => void; // silently declines; requester sees no event }

accept() takes no parameters β€” uses SDK defaults. Equivalent to me.publishVideo({}) / me.publishAudio({}) / me.publishScreen({}) based on kind. The Promise rejects with an SDKError on failure (camera busy, permission denied, etc.) β€” see Errors β†’ stream-publish-failed.

No timer. The request stays pending until you call accept() / reject(), or until either participant leaves the room.

UnpublishRequest

Payload of the stream-unpublish-requested event on LocalParticipant. Fires when another participant calls requestUnpublishX() targeting you. Decline with reject(); honor with accept().

interface UnpublishRequest { kind: MediaKind; from: RemoteParticipant; accept: () => Promise<void>; // SDK unpublishes the kind reject: () => void; }

This is not a hard mute. The recipient (you) can decline β€” there is no force-mute path on this surface. v1 may add moderator/host force-mute APIs later as a separate concern.

PublishFailure

Payload of the stream-publish-failed event on LocalParticipant. Covers both initial publish failures and mid-call runtime failures of a previously-live stream.

interface PublishFailure { kind: MediaKind; error: SDKError; }

For initial publish failures, the corresponding Promise (e.g. me.publishVideo()) also rejects with the same SDKError β€” both channels deliver the error; app picks. For mid-call failures (camera unplug, encoder die, OS revoke), this event is the only delivery channel since there's no Promise to reject. After the event fires, me.<kind> is null; app can retry. Inventory: Errors β†’ publish-failed.

SDKError

Every error the SDK surfaces β€” Promise rejections (join, publishVideo, etc.) and event payloads (stream-publish-failed, the cause field on ConnectionEvent) β€” uses this shape. Codes are stable strings; categorize via kind for handler reuse.

interface SDKError { code: string; // stable identifier β€” switch on this; never rely on `message` message: string; // human-readable, may change between SDK versions kind: ErrorKind; // category β€” for grouped handlers (auth / media / network / ...) retriable?: boolean; // true β†’ calling the same method again with the same input might succeed }
FieldDescription
codeStable identifier like 'INVALID_TOKEN' / 'CAMERA_BUSY'. This is what apps switch on. Codes are SCREAMING_SNAKE_CASE and never change between SDK versions; new codes can be added (additive). See the per-method docs for which codes each surface can fire.
messageHuman-readable description. Suitable for logs; not for switching. The wording may improve between SDK versions.
kindErrorKind β€” coarse category. Lets apps write one handler per category (e.g. if (err.kind === ErrorKind.Auth) showLoginScreen()) without enumerating every code in that group.
retriableOptional. true when calling the same method again with the same input has a reasonable chance of succeeding (e.g. NETWORK_ERROR, TIMEOUT, CAMERA_BUSY). false for inherently terminal errors (INVALID_TOKEN, BROWSER_UNSUPPORTED). When omitted, treat as false.
What's not on this shape β€” and why.
  • fatal flag β€” would be !retriable in 95% of cases. Dropped to keep the surface lean.
  • details bag β€” Record<string, any> defeats type-safety and the loose shape ends up under-documented in practice. Apps that need structured per-error context can request specific codes get richer payloads as additive changes later.
Example β€” generic retry helper using `retriable`
async function joinWithRetry(opts, maxAttempts = 3) {
  for (let i = 0; i < maxAttempts; i++) {
    try { return await VideoSDK.join(opts); }
    catch (err) {
      if (!err.retriable || i === maxAttempts - 1) throw err;
      await sleep(2 ** i * 1000);   // exponential backoff
    }
  }
}
Example β€” categorical handler using `kind`
try {
  const room = await VideoSDK.join({ token, roomId });
} catch (err) {
  switch (err.kind) {
    case ErrorKind.Auth:       return showLoginScreen();
    case ErrorKind.Network:    return showRetryButton();
    case ErrorKind.Media:      return showDevicePicker();
    case ErrorKind.Permission: return showPermissionUI();
    case ErrorKind.Server:     return showSupportContact();
    case ErrorKind.Config:     console.error('SDK misuse:', err); throw err;
  }
}

ConnectionEvent

Payload of the connection-state-changed event. Single shape for every transition; per-state data lives in nested optional blocks (reconnecting, disconnected) which are set only when the new state is the matching value.

interface ConnectionEvent { state: ConnectionState; previous: ConnectionState; // Set only when state === ConnectionState.Reconnecting reconnecting?: { attempt: number; // 1, 2, 3 ... maxAttempts: number; cause?: SDKError; // low-level cause that triggered the reconnect }; // Set only when state === ConnectionState.Disconnected disconnected?: { reason: DisconnectReason; cause?: SDKError; // low-level cause if reason is network-related }; }
FieldWhen setDescription
statealwaysThe new ConnectionState after the transition.
previousalwaysThe state the room was in before this transition. Useful for distinguishing first connect vs recovery (e.g. previous reconnecting β†’ state connected means recovery).
reconnectingiff state === ReconnectingPer-attempt detail: which attempt this is, the cap, and the underlying cause if known.
disconnectediff state === DisconnectedWhy the connection ended. reason is one of the v0-parity values in DisconnectReason.
Why nested blocks instead of flat optionals. A flat shape with optional attempt, maxAttempts, reason, cause would put fields on the event that don't apply to most states. Nesting under reconnecting / disconnected keeps the top-level shape minimal and makes the per-state data discoverable. Translates uniformly across TS / Kotlin / Swift / Dart with native optional / nullable struct support β€” no discriminated unions or sealed classes required.

JoinOptions

Argument to VideoSDK.join().

interface JoinOptions { // ── Identity / auth ── token: string; roomId: string; name?: string; participantId?: string; // app-supplied stable id (SDK generates if omitted) metadata?: any; // ── Network ── preferredProtocol?: Protocol; // default Protocol.UdpOverTcp signalingBaseUrl?: string; // custom signaling endpoint // ── Subscription model ── autoSubscribe?: boolean; // default false; true = SDK subscribes to every remote stream // ── Pre-warmed event channels ── subscribeEvents?: string[]; // ── Waiting lobby (fires only when token.joinPolicy.mode === 'ask') ── onWaiting?: () => void; // fired once when the joiner is placed in the lobby signal?: AbortSignal; // abort() β†’ join() rejects with AbortError // ── Publish during join handshake (one round-trip) ── // Ignored per kind if a local stream of that kind exists (from createVideoStream / createAudioStream). // CONFLICTING_PUBLISH_CONFIG if both a local stream AND opts are set for same kind. publishVideo?: PublishVideoOpts; publishAudio?: PublishAudioOpts; }
FieldTypeDescription
tokenstringAuth token issued by your backend.
roomIdstringRoom identifier to join.
namestring?Display name shown to other participants.
participantIdstring?Stable participant id supplied by your app (e.g. user id from your DB). SDK generates a random one if omitted. Useful when the app already has user identity.
metadataany?Custom data attached to your participant. Visible to others as p.metadata.
preferredProtocolProtocol?Network transport preference β€” UdpOnly, UdpOverTcp (default), or TcpOnly. Useful in restrictive networks where UDP is blocked.
signalingBaseUrlstring?Override SDK's signaling endpoint. Default "api.videosdk.live".
autoSubscribeboolean?Default false. When true, the SDK auto-subscribes to every remote stream as it appears β€” bandwidth scales with room size. When false (default), the app subscribes per-stream via stream.subscribe() / stream.attach(el) β€” bandwidth scales with rendered tiles. Pairs naturally with a subscribe-only token (grant canPublish: false, canSubscribe: true) for viewer-only clients that want every remote without writing per-tile subscribe.
subscribeEventsstring[]?List of lazy event names to pre-warm. SDK sends the upstream subscribe as part of the join handshake β€” eliminates the gap between join completing and your handlers being registered.
onWaiting() => void?Lobby callback β€” fired exactly once if the joiner is placed in a waiting lobby (token's joinPolicy.mode === 'ask'). Use to render the waiting UI. Never fires for direct tokens or ask tokens admitted instantly. The join() Promise still resolves on admit and rejects on ENTRY_DENIED / ENTRY_TIMEOUT.
signalAbortSignal?For an explicit "Cancel" button in the lobby UI β€” call controller.abort() and join() rejects with AbortError. Not needed for tab close: the SDK's socket disconnect handles cleanup automatically (server fires entry-canceled with reason: 'disconnected' for moderators).
publishVideoPublishVideoOpts?Publish video as part of the join handshake (one round-trip). Ignored if a local stream from createVideoStream already exists. Throws if both are set for the same kind. Pass {} for defaults; pass { resolution: 'h720', ... } to customize.
publishAudioPublishAudioOpts?Same shape for audio. Pass {} for defaults.
Three flows for publishing on join:
  • Pre-call β€” call VideoSDK.createVideoStream(opts) first; join({}) auto-promotes it. Same track from preview to publish (no flicker).
  • One-shot β€” pass publishVideo: { ... } directly to join({}). SDK acquires + publishes during the join handshake (one round-trip).
  • Deferred β€” call join({}) with no publish opts; later call me.publishVideo(...) (two round-trips).
Example
const room = await VideoSDK.join({
  token: getAuthToken(),
  roomId: 'team-standup',
  name: 'Alice',
  metadata: { role: 'host', avatarUrl: '...' },
  subscribeEvents: ['active-speaker-changed', 'transcription'],
});

Related open questions: Q14 β€” One-step vs two-step join Q16 β€” Boolean shortcut for publishVideo / publishAudio Q17 β€” When does join() Promise resolve? (RESOLVED)

JoinPolicy

Value of the token's joinPolicy claim β€” a sibling of grant. Decides whether the participant goes straight in or is held in a waiting lobby until a moderator admits them. Default { mode: 'direct' } if the claim is absent.

type JoinPolicy = | { mode: 'direct' } // valid token β†’ straight in (default) | { mode: 'ask'; ttl?: number }; // lobby β€” moderator admits; ttl seconds (10..600, default 60)
FieldTypeDescription
mode'direct' | 'ask''direct' (default) bypasses the lobby. 'ask' requires a participant with canModerate to admit before the joiner enters.
ttlnumber?Only valid with mode: 'ask'. How long the joiner waits (seconds) before the server times them out. Default 60; clamped to 10 ≀ ttl ≀ 600. Setting ttl with mode: 'direct', or outside bounds, is rejected as INVALID_ENTRY_CLAIM.
mode: 'ask' + canModerate is rejected. A moderator can't knock on their own door. The backend's token-mint helper throws INVALID_ENTRY_CLAIM before signing the JWT (primary path); the server re-validates as a safety net for hand-rolled JWTs.

EntryRequest

One pending lobby request, delivered to canModerate holders. Read the fields to render; call the methods to act. The same object appears on the entry-requested event and in the room.entryRequests snapshot β€” one type for both inline-handler and list-render UIs.

interface EntryRequest { // === Identity === participantId: string; // stable id from the joiner's token (or SDK-generated) name?: string; // JoinOptions.name on the joiner metadata?: unknown; // optional app payload (shape TBD) // === Lifecycle === requestedAt: Date; // when the knock landed at the server expiresAt: Date; // requestedAt + ttl β€” when the server will auto-timeout // === Actions β€” gated by canModerate === admit(): Promise<void>; // joiner's join() resolves to Room deny(): Promise<void>; // joiner's join() rejects with ENTRY_DENIED }
MemberTypeDescription
participantIdstringStable identity from the joiner's token, or a fresh SDK-generated id if the token omits participantId.
namestring?Display name from the joiner's JoinOptions.name.
metadataunknown?Optional app-defined payload from the joiner (shape TBD).
requestedAtDateWhen the knock landed at the server.
expiresAtDateWhen the server's ttl will auto-timeout this request β€” equals requestedAt + ttl. Render a countdown if your UI wants one.
admit()Promise<void>Admit the joiner. Resolves on server ack; the joiner's VideoSDK.join() resolves to a Room. Rejects with INVALID_PERMISSIONS if the caller lacks canModerate. No-op (resolves without effect) if the request was already resolved by another moderator.
deny()Promise<void>Deny the joiner. Resolves on server ack; the joiner's join() rejects with ENTRY_DENIED. Same permission and idempotency rules as admit().

See also: Waiting Lobby room β€” entry-requested event room.entryRequests

GetParticipantsOpts

Argument to room.getParticipants(). All fields optional.

interface GetParticipantsOpts { tier?: 'speaker' | 'viewer'; // omit = both (speakers first, then viewers) limit?: number; // page size; default 50, max 100 (clamped) cursor?: string; // opaque token from a previous call's nextCursor }
FieldTypeDescription
tier'speaker' | 'viewer'?Filter to one tier. Omit to query both β€” speakers come first in the result, then viewers. The strings match the keys in participant-count-changed's payload.
limitnumber?Page size; default 50, max 100. Values outside the range are clamped, not rejected.
cursorstring?Opaque token from a previous call's nextCursor. Omit on the first page.

GetParticipantsResult

Return value of room.getParticipants().

interface GetParticipantsResult { participants: RemoteParticipant[]; nextCursor?: string; // present iff more pages exist in this filter total: number; // total participant count in this filter (not just this page) }
FieldTypeDescription
participantsRemoteParticipant[]Page of participants. Both speaker- and viewer-tier instances share the RemoteParticipant class β€” viewer instances have isViewer: true, and calling subscribe() / publishX() on them rejects with INVALID_PERMISSIONS.
nextCursorstring?Pass to the next call to fetch the following page. Absent β†’ no more pages in this filter.
totalnumberTotal participant count within this filter (not just the current page). With tier: 'viewer', this is the audience count. With no filter, this equals participant-count-changed.total.
One class, two tiers. A separate ViewerParticipant class was considered and rejected β€” the isViewer flag plus permission-aware methods is simpler than two parallel class hierarchies. See MEETING_DECISIONS_V1.md β€” Large rooms / ILS.

PublishVideoOpts

Argument to LocalParticipant.publishVideo().

interface PublishVideoOpts { device?: CameraDeviceInfo; // from VideoSDK.getCameras() resolution?: "h180" | "h360" | "h720" | "h1080" | number; // height; width = height * aspectRatio aspectRatio?: number; // default 16/9 facingMode?: "user" | "environment"; frameRate?: number; multiStream?: boolean; // simulcast on/off, default true maxLayer?: 1 | 2 | 3; // number of simulcast layers, default 3 bitrateMode?: "bandwidth_optimized" | "balanced" | "high_quality"; // default "balanced" maxBitrate?: number; // kbps cap on top layer codec?: VideoCodec; degradationPreference?: DegradationPreference; // default Balanced contentHint?: ContentHint; // No processor field β€” set via VideoSDK.applyVideoProcessor() (sticky, global). }
FieldDescription
deviceCameraDeviceInfo from VideoSDK.getCameras(). Omit for default. Strongly typed β€” passing a microphone or speaker is a compile-time error.
resolutionCapture height. Use the named presets ("h720" = 720p tall) or pass any number for custom heights. The SDK derives width = height Γ— aspectRatio.
aspectRatioWidth-to-height ratio (e.g. 16/9, 4/3, 1 for square). Default 16/9.
facingModeMobile camera facing β€” "user" (front) or "environment" (back).
frameRateTarget fps. e.g. 30, 60.
multiStreamWhether to publish multiple simulcast layers (so subscribers can pick high/medium/low). true by default. Set false to publish a single layer (saves CPU + bandwidth on the publisher).
maxLayerNumber of simulcast layers β€” 1, 2, or 3. Default 3. Ignored if multiStream is false.
bitrateModePreset bitrate-ladder profile. "bandwidth_optimized" minimizes upload, "balanced" (default) is a sensible middle, "high_quality" uploads more for visual quality.
maxBitrateHard cap (kbps) on the top simulcast layer. Overrides the preset's top if set. Lower layers scale proportionally.
codecPreferred video codec β€” see VideoCodec. Best-effort: if unavailable on the browser/SFU, falls back silently. Apps that need a guarantee check stream.codec after publish.
degradationPreferenceWhat to drop first under bandwidth pressure β€” see DegradationPreference. Default Balanced.
contentHintEncoder optimization hint β€” see ContentHint. Motion for high-motion video, Detail for general sharpness, Text for high-detail static content (slides, code).
No processor field. Frame processors are sticky and set globally via VideoSDK.applyVideoProcessor() β€” they apply automatically to any camera stream of the local participant. Setting once survives publish / unpublish / device swap.
Example
const cameras = await VideoSDK.getCameras();

await me.publishVideo({
  device:      cameras[0],               // CameraDeviceInfo, not just an id
  resolution:  'h720',                   // 720p height; default aspectRatio 16/9 β†’ 1280Γ—720
  aspectRatio: 16/9,                     // optional; this is the default
  facingMode:  'user',
  frameRate:   30,

  multiStream: true,                     // simulcast on
  maxLayer:    3,                        // 3 layers

  bitrateMode: 'balanced',
  maxBitrate:  2500,                     // 2.5 Mbps cap

  codec:                  VideoCodec.VP9,
  degradationPreference:  DegradationPreference.Balanced,
  contentHint:            ContentHint.Motion,
});

PublishAudioOpts

Argument to LocalParticipant.publishAudio().

interface PublishAudioOpts { device?: MicrophoneDeviceInfo; noiseSuppression?: boolean; echoCancellation?: boolean; autoGainControl?: boolean; }
FieldDescription
deviceMicrophoneDeviceInfo from VideoSDK.getMicrophones(). Omit for default.
noiseSuppressionBrowser-level noise suppression constraint.
echoCancellationBrowser-level echo cancellation.
autoGainControlBrowser-level AGC.
No processor field. Audio processors are sticky and set globally via VideoSDK.applyAudioProcessor().

PublishScreenOpts

Argument to LocalParticipant.publishScreen(). Triggers the browser's getDisplayMedia picker.

interface PublishScreenOpts { audio?: boolean; // include screen audio resolution?: "h180" | "h360" | "h720" | "h1080" | number; // height aspectRatio?: number; // default 16/9 frameRate?: number; }
FieldDescription
audioIf true, capture system audio along with the screen. Browser support varies (Chrome on tab share, etc.).
resolutionCapture height. Named preset or any number; SDK derives width = height Γ— aspectRatio.
aspectRatioWidth-to-height ratio. Default 16/9.
frameRateTarget fps. Lower values (e.g. 5–15) save bandwidth for static content like presentations.
Future-additive. The following fields may be added to PublishScreenOpts in a future release for parity with PublishVideoOpts β€” they're intentionally omitted in v1 to keep the screen-share surface minimal:
  • multiStream β€” simulcast on/off
  • maxLayer β€” number of simulcast layers
  • bitrateMode β€” preset bitrate-ladder profile
  • maxBitrate β€” kbps cap
  • codec β€” preferred video codec
  • degradationPreference β€” what to drop under bandwidth pressure
  • contentHint β€” encoder optimization hint (often Text for screen-share)
For now, screen-share uses SDK defaults for these. No screen processor in v1 β€” screen pixels rarely benefit from per-frame transformation.

StreamElementOpts

Argument to VideoStream.createElement() β€” controls the SDK-created <video> element.

interface StreamElementOpts { containerStyle?: Partial<CSSStyleDeclaration>; videoStyle?: Partial<CSSStyleDeclaration>; autoAdaptiveSub?: boolean; // default true (pause when offscreen) }
FieldDescription
containerStyleInline styles applied to the wrapping <div>.
videoStyleInline styles applied to the inner <video> element.
autoAdaptiveSubIf true (default), the SDK uses an IntersectionObserver to auto-pause the stream when the element scrolls offscreen.
Example
const tile = p.video.createElement({
  containerStyle: { height: '300px', width: '300px', borderRadius: '8px' },
  videoStyle: { objectFit: 'cover' },
  autoAdaptiveSub: true,
});
document.querySelector('#grid').appendChild(tile);

PreCallTestOpts

Argument to VideoSDK.runPreCallTest(). Selects which phases run, optionally specifies stream config (or reuses pending streams), wires progress / error callbacks.

interface PreCallTestOpts { token: string; // Which phases to run. Default: all three. tests?: PreCallPhase[]; // Stream config β€” used only when no pending stream of that kind exists. // If createVideoStream / createAudioStream was already called, those pending // streams are reused β€” no double device acquisition. videoOpts?: PublishVideoOpts; audioOpts?: PublishAudioOpts; // Skip video entirely (audio-only call apps) audioOnly?: boolean; // Network preferences preferredProtocol?: "UDP_ONLY" | "UDP_OVER_TCP" | "TCP_ONLY"; signalingBaseUrl?: string; timeoutMs?: number; // default 15000 // Callbacks onProgress?: (update: { phase: PreCallPhase; data: any }) => void; onError?: (error: { code: string; message: string; phase: PreCallPhase }) => void; }
FieldDescription
tokenAuth token (same as JoinOptions).
testsSubset of PreCallPhase values to run. Default runs all. Quality implicitly runs Connectivity and Media internally (it needs the SFU connection and tracks).
videoOptsUsed only if no pending video stream exists. Same shape as PublishVideoOpts (typed device, height-only resolution, etc.).
audioOptsSame for audio.
audioOnlyIf true, skip video entirely β€” no camera permission requested, video stats not collected.
preferredProtocolNetwork transport preference. Default "UDP_OVER_TCP".
signalingBaseUrlOverride SDK's signaling endpoint. Default "api.videosdk.live".
timeoutMsPer-phase timeout. Default 15000.
onProgressFires once per completed phase with phase-specific data.
onErrorFires immediately on a non-fatal error in any phase. Same errors are also collected in result.errors[].

PreCallTestResult

Resolution value of the StoppablePromise returned by VideoSDK.runPreCallTest(). Per-phase results are present only for phases that ran (or were implicitly run).

interface PreCallTestResult { // Per-phase results β€” present when that phase ran connectivity?: ConnectivityResult; media?: MediaCheckResult; quality?: QualityResult; // Non-fatal errors collected during the test errors: Array<{ code: string; message: string; phase: PreCallPhase }>; // Metadata durationMs: number; testsRun: PreCallPhase[]; aborted: boolean; // true if .stop() was called } interface ConnectivityResult { connected: boolean; region: string | null; turnUsed: boolean | null; protocol: "udp" | "tcp" | null; latencyMs: number; connectionTimeMs: number | null; } interface MediaCheckResult { camera: { status: boolean; resolution: string | null; codec: VideoCodec | null }; microphone: { status: boolean }; video: LocalVideoStream | null; audio: LocalAudioStream | null; } interface QualityResult { audioOnly: boolean; // true if video is recommended off (poor network) uplink: { quality: NetworkQuality; rttMs: number | null; factors: QualityFactor[]; audio: { quality: NetworkQuality; bitrateBps: number | null; packetLossPct: number | null; jitterMs: number | null; bytesSent: number }; video: { quality: NetworkQuality; bitrateBps: number | null; packetLossPct: number | null; jitterMs: number | null; fps: number | null; bytesSent: number; qualityLimitationReason: "bandwidth" | "cpu" | "none" } | null; }; downlink: { quality: NetworkQuality; rttMs: number | null; factors: QualityFactor[]; audio: { quality: NetworkQuality; bitrateBps: number | null; packetLossPct: number | null; jitterMs: number | null; bytesReceived: number }; video: { quality: NetworkQuality; bitrateBps: number | null; packetLossPct: number | null; jitterMs: number | null; framesDropped: number; freezeCount: number; bytesReceived: number } | null; }; }
Stream reuse, not track reuse. The media.video / media.audio fields are the same LocalVideoStream / LocalAudioStream instances either reused from pending pre-call streams or freshly created by the test. They remain "pending" β€” the next VideoSDK.join auto-promotes them. No raw MediaStreamTrack handoff.

PubSub types

Types used by PubSub. See PubSub for the methods that use these.

PubSubMessage

Returned by publish and delivered to subscribe handlers. Same shape on both ends β€” apps don't fork render code.

interface PubSubMessage<T = unknown> { id: string; // server-assigned snowflake (timestamp-prefixed) topic: string; // topic the message was published to payload: T; // app-defined; generic for type narrowing metadata?: Record<string, unknown>; // app-level extras (routing, telemetry, etc) from: string; // sender participantId timestamp: Date; // server-assigned wall clock }
FieldDescription
idServer-assigned globally-unique ID. Snowflake-style (timestamp-prefixed) β€” sortable and time-orderable across messages.
topicThe topic this message was published to. Useful when one handler covers messages from multiple topics (future v1.x multi-topic subscribe).
payloadApp-defined message body. Whatever was passed to publish(topic, payload). Generic type T narrows it for type-safe consumers.
metadataOptional app-level extras passed via PublishOpts.metadata. Echoed back unchanged to subscribers. Use for routing hints, telemetry IDs, app version, etc.
fromSender's participantId. Same as room.localParticipant.id for the sender, or a remote participant's id.
timestampServer-assigned wall clock. Use this for ordering / display β€” local clocks can drift.

PublishOpts

Optional argument to publish. All fields optional.

interface PublishOpts { persist?: boolean; // default true β€” store in history to?: string[]; // participantIds β€” targeted delivery metadata?: Record<string, unknown>; // app-level extra fields, echoed on PubSubMessage }
FieldDescription
persistIf true (default), the message is stored in topic history. Set false for ephemeral topics like cursor positions or signals β€” won't appear in getHistory, saves storage.
toArray of participantIds. Only listed participants receive on their subscribe handlers (and see this message in their history). Other subscribers on the same topic don't see it. Omit for normal broadcast (all subscribers receive).
metadataApp-level fields separate from payload. Echoed back unchanged on the delivered PubSubMessage. Use for routing, filtering, telemetry, app version β€” anything that's "about the message" rather than "the message content". Forward-compatible with future server-side filter expressions.
No idempotencyKey in v1. The SDK generates and tracks idempotency keys internally for retry safety. App-level dedup (e.g., double-click guards) is the app's responsibility β€” disable the Send button while a publish is pending.

SubscribeOpts

Optional argument to subscribe. All fields optional.

interface SubscribeOpts { limit?: number; // max messages/sec delivered to handler (rate cap) }
FieldDescription
limitMaximum messages per second delivered to this handler. Server respects max(active subs' limits) on the topic; SDK filters per-sub locally. When burst exceeds cap: drop oldest, keep latest N/sec. Omit (or undefined) for no cap β€” receive full firehose.
Multi-sub limit semantics. When multiple subscribes on the same topic have different limits, the server uses max across all active subs (so the highest-cap sub gets what it needs). Each subscription's handler is filtered locally to its own limit. If any sub has no limit, the server sends the full firehose for that topic.

PubSubSubscription

Returned by subscribe. Each call returns its own handle; multiple subscribes on the same topic create independent subscriptions.

interface PubSubSubscription<T = unknown> { readonly topic: string; unsubscribe(): Promise<void>; // idempotent }
MemberDescription
topicThe topic this subscription is for.
unsubscribe()Stops calling the handler for this subscription. SDK refcountβ€”; when refcount reaches 0 on a topic, SDK tells server to stop sending. Idempotent β€” safe to call multiple times; second call is a no-op.
No historyAvailable flag. An earlier draft put a snapshot boolean here so apps could skip getHistory on empty topics. Dropped β€” it coupled the live-subscription handle to a history concern, and the call it saved parallelizes with subscribe anyway. getHistory is the single source of truth for whether a topic has history (empty messages + hasMore: false). See CHAT_DECISIONS_V1.md Decision 7.

HistoryOpts

Optional argument to getHistory. All fields optional. Pass before or after β€” not both.

interface HistoryOpts { limit?: number; // page size β€” default 25, max 50 before?: string; // message id β€” fetch messages OLDER than this after?: string; // message id β€” fetch messages NEWER than this from?: string[]; // filter β€” only messages from these participantIds }
FieldDescription
limitMaximum messages in the returned page. Default 25, max 50. A value above 50, or ≤ 0, rejects with INVALID_LIMIT β€” SDK-local, before any round trip.
beforeA message id. Returns the page of messages immediately older than it β€” scroll-up / load-earlier. Omit (with no after) to get the newest page.
afterA message id. Returns the page of messages immediately newer than it β€” reconnect gap-fill. The id can come from any message the client holds: a subscribe callback, a publish return value, or a prior history page.
fromServer-side filter β€” restricts the page to messages sent by the listed participantIds. Combines with before/after: the server filters first, then paginates the filtered set, so hasMore and page edges reflect the filtered set. An empty array rejects with INVALID_HISTORY_OPTS.
The anchor is a message id, not an opaque cursor. before / after take a PubSubMessage.id (a snowflake). One anchor concept across the whole API β€” every message from every source carries an id usable here. Pass before or after, never both β€” that rejects with INVALID_HISTORY_OPTS. See CHAT_DECISIONS_V1.md Decision 22.

HistoryPage

Returned by getHistory. Plain data β€” no methods. Paginate by re-calling getHistory with a page-edge message id.

interface HistoryPage<T = unknown> { messages: PubSubMessage<T>[]; // oldest-first within the page hasMore: boolean; // more messages exist in the direction queried }
FieldDescription
messagesThe page of messages, always oldest-first (chronological) regardless of direction. messages[0] is the oldest, the last element the newest.
hasMoretrue if more messages exist in the direction this call queried β€” older for a before (or default) call, newer for an after call.
Pagination β€” re-call with a page-edge id. To load the next older page, call getHistory(topic, { before: page.messages[0].id }). To continue newer, use the last message's id as after. There is no Paginator object and no cursor field β€” the message id already in the page is the anchor.

Recording & streaming types

Option objects for the server-side media features on Room β€” consumed by Room.startRecording(), startHls(), startLivestream(), and startTranscription(). These configure server-side compositors and pipelines β€” no client tracks are involved.

LayoutConfig

Controls how the server compositor arranges participant tiles for a recording / HLS / livestream feed. Shared by RecordingOptions, HlsOptions, and LivestreamOptions via their layout field. Omit entirely for a sensible default grid.

interface LayoutConfig { type?: 'grid' | 'spotlight' | 'sidebar'; // default 'grid' priority?: 'speaker' | 'pin'; // default 'speaker' gridSize?: number; // server-enforced max per feature β€” recording 4, HLS 25 }
gridSize caps differ per feature. The maximum is server-enforced, not validated client-side: recording allows up to 4 tiles, HLS up to 25. A value above the feature's cap is clamped server-side. Tune it to the layout β€” a spotlight layout typically wants a small gridSize, a grid layout the feature maximum.

SummaryConfig

Configures the AI summary generated alongside a transcription. Used as the summary field of TranscriptionConfig.

interface SummaryConfig { prompt?: string; // optional custom summary prompt }
Presence-based, like transcription itself. There is no enabled field β€” the summary object being present on TranscriptionConfig is what turns the AI summary on. Pass summary: {} for the default prompt, or summary: { prompt: '...' } to customize it.

TranscriptionConfig

Configures live transcription (and optionally an AI summary). Passed directly to Room.startTranscription(), and reused as the transcription field of RecordingOptions / HlsOptions / LivestreamOptions to transcribe those feeds.

interface TranscriptionConfig { webhookUrl?: string; // where transcription / summary results are delivered summary?: SummaryConfig; // present β†’ also generate an AI summary }
Presence is the switch. When TranscriptionConfig appears as the transcription field of a recording / HLS / livestream option object, its mere presence means transcription is on for that feed β€” there is no enabled boolean. Likewise the nested summary: present β†’ summary on, absent β†’ off. See SummaryConfig.

RtmpOutput

A single RTMP destination for a livestream. LivestreamOptions.outputs is an array of these β€” one entry per platform you broadcast to.

interface RtmpOutput { url: string; // e.g. 'rtmp://a.rtmp.youtube.com/live2' streamKey: string; }

HlsUrls

The pair of playback URLs produced when HLS streaming is active β€” surfaced via Room state / events once startHls() has spun up the stream.

interface HlsUrls { playbackUrl: string; // HLS with DVR β€” viewers can pause / scrub back liveUrl: string; // HLS live edge only β€” lower latency, no scrub-back }
Two URLs, two trade-offs. playbackUrl exposes a DVR window so viewers can pause and rewind; liveUrl pins playback to the live edge for the lowest latency at the cost of scrub-back. Pick per viewer experience.

RecordingOptions

Optional argument to Room.startRecording(). All fields optional β€” call startRecording() with no argument to record with SDK defaults (grid layout, project default storage).

interface RecordingOptions { layout?: LayoutConfig; templateUrl?: string; // custom layout template β€” overrides `layout` theme?: 'dark' | 'light' | 'default'; mode?: 'video-and-audio' | 'audio'; quality?: MediaQuality; orientation?: 'portrait' | 'landscape'; transcription?: TranscriptionConfig; webhookUrl?: string; // bring-your-own recording webhook awsDirPath?: string; // bring-your-own S3 path preSignedUrl?: string; // bring-your-own storage URL }
FieldDescription
layoutLayoutConfig for the server compositor. Omit for a default grid. The recording gridSize cap is 4.
templateUrlURL of a custom layout template page. When set, it overrides layout β€” the template fully controls composition.
themeVisual theme for the composited output β€” 'dark', 'light', or 'default'.
mode'video-and-audio' records a composited video file; 'audio' records an audio-only file.
qualityOutput quality β€” see MediaQuality.
orientation'portrait' or 'landscape' aspect for the composited output.
transcriptionTranscriptionConfig. Presence-based β€” set it to also transcribe the recording; omit to skip transcription.
webhookUrlOptional. Bring-your-own webhook for recording lifecycle / result delivery. Omit to use the project default.
awsDirPathOptional. Bring-your-own S3 directory path for the stored file. Omit to use the project's default storage.
preSignedUrlOptional. Bring-your-own pre-signed storage URL to upload the file to. Omit to use the project's default storage.
Bring-your-own storage is optional. webhookUrl, awsDirPath, and preSignedUrl let you route recordings to your own storage / notification infrastructure. Omit all three to use the project's default storage. HLS and livestream have no storage params β€” their output is a live stream, not a stored file.

HlsOptions

Optional argument to Room.startHls(). All fields optional β€” call startHls() with no argument to stream with SDK defaults. Output is a live HLS stream (see HlsUrls), not a stored file.

interface HlsOptions { layout?: LayoutConfig; templateUrl?: string; theme?: 'dark' | 'light' | 'default'; mode?: 'video-and-audio' | 'audio'; quality?: MediaQuality; orientation?: 'portrait' | 'landscape'; transcription?: TranscriptionConfig; recording?: boolean; // also record the meeting while HLS-streaming }
FieldDescription
layoutLayoutConfig for the server compositor. Omit for a default grid. The HLS gridSize cap is 25.
templateUrlURL of a custom layout template page. When set, it overrides layout.
themeVisual theme for the composited stream β€” 'dark', 'light', or 'default'.
mode'video-and-audio' or 'audio'-only HLS.
qualityOutput quality β€” see MediaQuality.
orientation'portrait' or 'landscape' aspect for the stream.
transcriptionTranscriptionConfig. Presence-based β€” set it to also transcribe the HLS feed.
recordingWhen true, the server also records the meeting to a stored file while HLS-streaming β€” combining startHls() and startRecording() into one pipeline.
No storage params. HLS produces a live stream, not a stored file, so there are no webhookUrl / awsDirPath / preSignedUrl fields. Set recording: true to also capture a stored recording β€” that recording uses the project's default storage.
No autoStart field in the client SDK. v0's autoStartConfig (HLS auto-starts when the first speaker joins) is a REST / project-config concern β€” set server-side via the management API. The client never initiates auto-start; if the project has it enabled, HLS appears in starting β†’ started β†’ playable exactly as if a client had called startHls(). Intentionally out of HlsOptions.

LivestreamOptions

Argument to Room.startLivestream(). Unlike recording and HLS, this argument is required β€” outputs must list at least one RTMP destination.

interface LivestreamOptions { outputs: RtmpOutput[]; // REQUIRED β€” RTMP destinations layout?: LayoutConfig; templateUrl?: string; theme?: 'dark' | 'light' | 'default'; mode?: 'video-and-audio' | 'audio'; quality?: MediaQuality; orientation?: 'portrait' | 'landscape'; transcription?: TranscriptionConfig; // requires recording: true recording?: boolean; // also record while livestreaming }
FieldDescription
outputsRequired. Array of RtmpOutput β€” the RTMP destinations to broadcast to (YouTube, Twitch, etc.). One entry per platform. Must be non-empty.
layoutLayoutConfig for the server compositor. Omit for a default grid.
templateUrlURL of a custom layout template page. When set, it overrides layout.
themeVisual theme for the composited stream β€” 'dark', 'light', or 'default'.
mode'video-and-audio' or 'audio'-only broadcast.
qualityOutput quality β€” see MediaQuality.
orientation'portrait' or 'landscape' aspect for the broadcast.
transcriptionTranscriptionConfig. Presence-based. Requires recording: true β€” transcription on a livestream is derived from the stored recording pipeline.
recordingWhen true, the server also records the meeting to a stored file while livestreaming. Must be true if transcription is set.
outputs is required; transcription requires recording: true. startLivestream() must be given a non-empty outputs array of RtmpOutput destinations. Transcription on a livestream rides on the recording pipeline, so setting transcription without recording: true is rejected. Like HLS, livestream has no storage params β€” set recording: true to also capture a stored file (project default storage).

See also: Enums VideoSDK LocalParticipant PubSub Room