LocalParticipant class

You โ€” the local participant in the room. Holds your local streams and exposes publish/unpublish controls. Access via room.localParticipant.

Blueprint

Full surface at a glance โ€” every property, method, and event on this class. Click any name to jump to its detailed section.

class LocalParticipant { // Identity readonly id: string; readonly displayName: string; readonly metadata: any; readonly isLocal: true; // Authorization โ€” readonly views of the token's claims (see authentication.html) readonly grant: Grant; // what this participant is allowed to do readonly isViewer: boolean; // tier โ€” false = on-stage / speaker; true = audience / viewer // Stream handles โ€” null until you publish that kind video: LocalVideoStream | null; audio: LocalAudioStream | null; screen: LocalScreenStream | null; screenAudio: LocalScreenAudioStream | null; // non-null only when publishScreen({ audio: true }) // Sync state getters readonly isVideoPublished: boolean; readonly isAudioPublished: boolean; readonly isScreenPublished: boolean; readonly pinnedKinds: MediaKind[]; // Publish โ€” Promise resolves with the stream publishVideo(opts: PublishVideoOpts): Promise<LocalVideoStream>; publishAudio(opts: PublishAudioOpts): Promise<LocalAudioStream>; publishScreen(opts: PublishScreenOpts): Promise<LocalScreenStream>; // Unpublish โ€” full teardown: removes the published stream AND releases the underlying device. // Camera LED off, mic released. Re-publish via me.publishX() creates a fresh stream. unpublishVideo(): Promise<void>; unpublishAudio(): Promise<void>; unpublishScreen(): Promise<void>; // Self-pin โ€” broadcast "look at me" signal pin(kinds: MediaKind[]): Promise<void>; unpin(kinds: MediaKind[]): Promise<void>; // Event channel pre-warm (refcount-gated) subscribeEvents(events: string[]): void; unsubscribeEvents(events: string[]): void; // Event handlers on(event: LocalEvent | string, listener: Function): void; off(event: LocalEvent | string, listener: Function): void; // === Events fired === // Publish lifecycle (fire for explicit publishX(), JoinOptions async publish, and pre-call auto-promote): // 'stream-published' (pub: LocalPublication) // 'stream-unpublished' ({ kind: MediaKind }) // 'stream-publish-failed' (failure: PublishFailure) โ€” initial OR mid-call // Incoming requests from other participants: // 'stream-publish-requested' (req: PublishRequest) โ€” { kind, from, accept, reject } // 'stream-unpublish-requested' (req: UnpublishRequest) โ€” { kind, from, accept, reject } // You were pinned/unpinned (self-pin or someone pinned you): // 'pinned' ({ by, kinds }) // 'unpinned' ({ by, kinds }) }

Properties

video

Your camera stream. Null when you haven't published video. Becomes non-null after publishVideo() resolves.

video: LocalVideoStream | null

audio

Your microphone stream.

audio: LocalAudioStream | null

screen

Your screen-share stream.

screen: LocalScreenStream | null

screenAudio

Your bundled screen-share audio stream โ€” convenience getter for me.screen?.audio. Non-null only when publishScreen() was called with { audio: true } AND the browser / source actually delivered system audio (Chrome's tab-share is the most common case). When the screen-share ends, this becomes null alongside screen.

screenAudio: LocalScreenAudioStream | null
Convenience getter. Identical to me.screen?.audio โ€” useful when you want flat access without optional-chaining through screen. Both are kept in sync by the SDK.
Example โ€” check bundled screen audio
await me.publishScreen({ audio: true });

if (me.screenAudio) {
  console.log('Screen audio is publishing โ€” system audio captured');
} else {
  console.log('No screen audio โ€” either { audio: true } was omitted or the source didn\'t deliver audio');
}

State getters

Sync booleans reflecting current publish state. Useful for binding UI without holding state yourself.

readonly isVideoPublished: boolean readonly isAudioPublished: boolean readonly isScreenPublished: boolean

grant readonly

Read-only view of your own capability grant from the token. Use it for UI guards โ€” hide a Record button if !me.grant.canRecord, disable a Publish button if !me.grant.canPublish, etc. The grant is the same shape as the token's grant claim (see Authentication โ†’ The grant).

readonly grant: Grant
Server is the source of truth. This property is a local mirror of the token's grant. Even if you check me.grant.canPublish client-side and gate your UI accordingly, the SFU re-validates on every publish/subscribe/moderate call โ€” a tampered local value can't authorize anything. The property is for UI; the server is for security.
Reflects mid-call changes. If the participant's grant changes during the call (see Q2 โ€” change grant mid-call), this property updates. The exact event / refresh mechanism is still open.
Example โ€” gate UI by grant
if (me.grant.canRecord)      ui.showRecordButton();
if (me.grant.canModerate)    ui.showModeratorPanel();
if (me.grant.canHls)         ui.showGoLiveButton();

// Subscribe to participant-joined only if you're a speaker
if (!me.isViewer) {
  room.on('participant-joined', renderTile);
}

isViewer readonly

Tier marker from your token. false (default) = on-stage / speaker โ€” you're rostered on other clients' remoteParticipants map and fire participant-joined for them. true = audience / viewer โ€” count-only on other clients; not in their remoteParticipants, only enumerable via room.getParticipants({ tier: 'viewer' }). See Authentication โ†’ Tier & participant events.

readonly isViewer: boolean

pinnedKinds

The kinds you have self-pinned. Sync. Empty if you haven't called me.pin() on yourself.

readonly pinnedKinds: MediaKind[]

Identity

readonly id: string readonly displayName: string readonly metadata: any readonly isLocal: true

Methods

publishVideo async

Acquires the camera and publishes a video stream to the room. Resolves with the LocalVideoStream once the SFU acknowledges the publish.

publishVideo(opts: PublishVideoOpts): Promise<LocalVideoStream>

See PublishVideoOpts for the full field list โ€” device (CameraDeviceInfo), resolution (height-only), aspectRatio, facingMode, frameRate, simulcast control (multiStream, maxLayer), bitrate (bitrateMode, maxBitrate), codec, degradationPreference, contentHint. No processor field โ€” set via VideoSDK.applyVideoProcessor() (sticky, global).

Returns

Resolves with the LocalVideoStream. Rejects on permission denied, device busy, server reject, or timeout.

Example
const cameras = await VideoSDK.getCameras();

try {
  const stream = await room.localParticipant.publishVideo({
    device: cameras[0],          // CameraDeviceInfo
    resolution: 'h720',
    facingMode: 'user',
  });
  stream.attach(document.querySelector('#self-view'));
} catch (err) {
  if (err.code === 'PERMISSION_DENIED') showPermissionUI();
}

publishAudio async

Acquires the microphone and publishes an audio stream. Resolves with the LocalAudioStream.

publishAudio(opts: PublishAudioOpts): Promise<LocalAudioStream>
Example
const mics = await VideoSDK.getMicrophones();

const stream = await room.localParticipant.publishAudio({
  device: mics[0],
  noiseSuppression: true,
  echoCancellation: true,
});

// Register stream-level events on the returned stream
stream.on('silent-detected', () => showMicWarning());

publishScreen async

Triggers the browser's screen-share picker (getDisplayMedia) and publishes the selected source. Set audio: true to capture system audio along with the screen video. Resolves with the LocalScreenStream.

publishScreen(opts: PublishScreenOpts): Promise<LocalScreenStream>
Example
const stream = await room.localParticipant.publishScreen({ audio: true, resolution: 'h1080' });
stream.attach(document.querySelector('#screen-view'));

stream.on('ended', () => updateUI({ sharingScreen: false }));   // user clicked browser's stop-sharing

unpublishVideo / unpublishAudio / unpublishScreen async

Full teardown. Removes the published stream from the room (peers receive 'stream-unpublished') AND releases the underlying device โ€” camera LED off, microphone released, screen-share stopped. After this resolves, me.video / me.audio / me.screen is null.

Re-publish creates a fresh stream โ€” call me.publishVideo() / publishAudio() / publishScreen() again to start a new stream. The browser typically caches the permission grant, so no second permission prompt; the device is re-acquired (slight delay ~50-200ms).

unpublishVideo(): Promise<void> unpublishAudio(): Promise<void> unpublishScreen(): Promise<void>
Example โ€” full unpublish + release
await room.localParticipant.unpublishVideo();   // camera LED off
await room.localParticipant.unpublishAudio();   // mic released
await room.localParticipant.unpublishScreen();  // screen-share stopped

// To re-publish later, call publishX() again โ€” creates a fresh stream
await room.localParticipant.publishVideo({ device: cameras[0] });

subscribeEvents / unsubscribeEvents

Pre-warm or release lazy event channels independent of handler registration. Both .on() and these methods feed the same internal refcount โ€” the server stops streaming an event only when refcount hits zero.

Useful for app-level commitment to a channel that outlives any individual UI component (e.g. analytics modules, React StrictMode mount/unmount churn).

subscribeEvents(events: string[]): void unsubscribeEvents(events: string[]): void
Example
// At app init โ€” keep the channel warm for the app lifetime
me.subscribeEvents(['active-speaker-changed']);

// UI components freely register and remove handlers โ€” channel stays open
component.onMount = () => me.on('active-speaker-changed', updateSpeakerRing);
component.onUnmount = () => me.off('active-speaker-changed', updateSpeakerRing);

pin / unpin async

Self-pin or self-unpin โ€” a "look at me" signal broadcast to all participants. Pin is a server-shared layout hint; other apps can render shared awareness UI ("X is featuring themselves").

pin(kinds: MediaKind[]): Promise<void> unpin(kinds: MediaKind[]): Promise<void>
Parameters
  • kinds โ€” array of MediaKind values. Only Video and Screen are pinnable; passing Audio rejects the Promise.
Returns

Resolves when the server acknowledges the change. The event payload is the delta โ€” only the kinds that actually changed.

  • Rejects when the call would be a complete no-op. Error codes: ALREADY_PINNED, NOT_PINNED.
  • Resolves with partial delta when at least one kind would change.
  • Rejects with INVALID_KIND if Audio is in the list.
Example โ€” self-pin while presenting
await me.pin([MediaKind.Screen]);   // "look at my screen"

// Self-unpin when done
await me.unpin([MediaKind.Screen]);

For pinning another participant, use p.pin().

on / off

on(event: LocalEvent | string, listener: Function): void off(event: string, listener: Function): void

Events

For per-stream observations (frozen, stuck, ended, silent-detected) register handlers on the stream object itself โ€” see LocalVideoStream / LocalAudioStream / LocalScreenStream.

Publish lifecycle events

Three consolidated events fire whenever a local stream is published or fails to publish โ€” regardless of how the publish was triggered. One handler covers all three paths:

For explicit calls, the success event fires after the Promise resolves โ€” both channels deliver the same stream. App picks whichever is more convenient; failure for explicit calls also surfaces through Promise rejection AND the stream-publish-failed event.

Event (LocalEvent)PayloadWhen
stream-publishedLocalPublication โ€” { kind, video?, audio?, screen? }A local stream just published (any path). Exactly one of video/audio/screen is set, matching kind.
stream-unpublished{ kind: MediaKind }A local stream just unpublished. me.<kind> is now null.
stream-publish-failedPublishFailure โ€” { kind, error }Any publish failed (any path) โ€” initial failure (no Promise for async path) OR mid-call runtime failure (camera unplugged, encoder died, OS revoked permission). After this fires for a kind, me.<kind> is null.

Error inventory for stream-publish-failed: see Errors โ†’ publish-failed for the full list of 14 codes (initial publish failures + mid-call runtime failures). For explicit me.publishVideo({}) calls, initial-publish codes also surface through the Promise rejection (decision 36). After stream-publish-failed fires for a kind, me.<kind> is null โ€” app can retry via the same publish method.

Example โ€” async publish via JoinOptions (events are the only delivery channel)
const room = await VideoSDK.join({
  token, roomId,
  publishVideo: { resolution: 'h720' },
  publishAudio: {},
});
// At resolve: connection up, but publish still in flight.
// me.video / me.audio === null until 'stream-published' fires.

me.on('stream-published', (pub) => {
  // pub: LocalPublication โ€” exactly one of pub.video / .audio / .screen is set
  if (pub.video)  pub.video.attach(document.querySelector('#self-view'));
  if (pub.screen) pub.screen.attach(document.querySelector('#self-screen'));
  // pub.audio: usually no UI for local audio
});

me.on('stream-unpublished', ({ kind }) => {
  if (kind === MediaKind.Video) document.querySelector('#self-view').srcObject = null;
});

me.on('stream-publish-failed', (failure) => {
  // failure: PublishFailure โ€” { kind, error }
  if (failure.kind === MediaKind.Video) showError(`Camera: ${failure.error.message}`);
  if (failure.kind === MediaKind.Audio) showError(`Mic: ${failure.error.message}`);
  // App can retry: await me.publishVideo({})
});
Example โ€” explicit publish (Promise + event both work)
// Promise-driven
const stream = await me.publishVideo({ resolution: 'h720' });
stream.attach(localEl);

// Event-driven (also fires for the explicit call above)
me.on('stream-published', (pub) => {
  if (pub.video) analytics.track('camera_on', { id: pub.video.id });
});

Incoming-request events

Two consolidated events fire when another participant calls requestPublishX or requestUnpublishX targeting you. The recipient (you) decides whether to honor โ€” call accept() or reject() on the payload.

Event (LocalEvent)Payload
stream-publish-requestedPublishRequest โ€” { kind, from, accept, reject }
stream-unpublish-requestedUnpublishRequest โ€” { kind, from, accept, reject }

accept() publishes (or unpublishes) on your behalf using SDK defaults โ€” no parameters. For stream-publish-requested it resolves with a LocalPublication (same shape as the stream-published event payload); for stream-unpublish-requested it resolves with void. reject() clears the pending request locally; the requester is not notified (silent decline). No timer โ€” pending requests stay open until you respond or someone leaves the room.

Failure handling: publish failures (permission denied, device busy, etc.) propagate through the Promise returned by accept() as an SDKError. Wrap in try/catch if you need to react. The same failure also fires stream-publish-failed.

Example โ€” confirm dialog before honoring a request
const me = room.localParticipant;

me.on('stream-publish-requested', async (req) => {
  // req: PublishRequest โ€” { kind, from, accept, reject }
  const label = req.kind === MediaKind.Video ? 'camera'
              : req.kind === MediaKind.Audio ? 'microphone'
              : 'screen share';
  const ok = await showConfirmDialog(`${req.from.displayName} asks you to enable ${label}`);
  if (!ok) return req.reject();

  try {
    const pub = await req.accept();   // resolves with LocalPublication
    if (pub.video)  pub.video.attach(document.querySelector('#self-cam'));
    if (pub.screen) pub.screen.attach(document.querySelector('#self-screen'));
  } catch (err) {
    if (err.code === 'MEDIA_PERMISSION_DENIED') showPermissionUI();
  }
});

me.on('stream-unpublish-requested', async (req) => {
  // req: UnpublishRequest โ€” { kind, from, accept, reject }
  // Auto-accept mute requests; tighter UX would show a confirm dialog
  await req.accept();
});

Pin events

Fire when you are the pin target โ€” either you self-pinned, or another participant pinned you. Fire retroactively on join for any pins already in effect.

Event (LocalEvent)Payload
pinned{ by: LocalParticipant | RemoteParticipant, kinds: MediaKind[] }
unpinned{ by: LocalParticipant | RemoteParticipant, kinds: MediaKind[] }

Check by.isLocal to distinguish a self-pin from someone else pinning you.

Example โ€” react to being pinned
me.on('pinned', ({ by, kinds }) => {
  if (by.isLocal) return;   // your own self-pin โ€” already reflected in UI
  showBadge(`${by.displayName} pinned you`);
});
Example โ€” typical publish wiring (no request involved)
const cameras = await VideoSDK.getCameras();
const stream = await me.publishVideo({ device: cameras[0] });
stream.attach(document.querySelector('#self-view'));

stream.on('ended', () => showCameraDisconnected());

See also: LocalVideoStream LocalAudioStream LocalScreenStream RemoteParticipant

Related open questions: Q1 โ€” Per-kind events vs single event with kind Q4 โ€” Custom / auxiliary tracks Q11 โ€” Event mirroring on room