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.
Properties
video
Your camera stream. Null when you haven't published video. Becomes non-null after publishVideo() resolves.
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.
me.screen?.audio โ useful when you want flat access without optional-chaining through screen. Both are kept in sync by the SDK.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.
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).
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.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.
pinnedKinds
The kinds you have self-pinned. Sync. Empty if you haven't called me.pin() on yourself.
Identity
Methods
publishVideo async
Acquires the camera and publishes a video stream to the room. Resolves with the LocalVideoStream once the SFU acknowledges the publish.
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).
Resolves with the LocalVideoStream. Rejects on permission denied, device busy, server reject, or timeout.
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.
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.
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).
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).
// 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").
kindsโ array of MediaKind values. OnlyVideoandScreenare pinnable; passingAudiorejects the Promise.
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_KINDifAudiois in the list.
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
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:
- Explicit
me.publishVideo({})/publishAudio/publishScreencalls - Async publish via
JoinOptions.publishVideo/publishAudio - Auto-promote of pre-call streams (
VideoSDK.createVideoStream()โVideoSDK.join())
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) | Payload | When |
|---|---|---|
stream-published | LocalPublication โ { 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-failed | PublishFailure โ { 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.
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({})
});
// 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-requested | PublishRequest โ { kind, from, accept, reject } |
stream-unpublish-requested | UnpublishRequest โ { 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.
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.
me.on('pinned', ({ by, kinds }) => {
if (by.isLocal) return; // your own self-pin โ already reflected in UI
showBadge(`${by.displayName} pinned you`);
});
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