> Fetch clean Markdown by appending `.md` to any page URL under https://signalwire.com/docs or requesting it with the HTTP header `Accept: text/markdown`. The root index at https://signalwire.com/docs/llms.txt lists the available documentation indexes. # Screen Sharing Screen sharing is a method on the `SelfParticipant`. Call `startScreenShare()` to add a screen-share track to the active call; call `stopScreenShare()` to remove it. The SDK calls `getDisplayMedia()` under the hood, negotiates the additional track, and pushes status changes through an observable. There's no separate "screen share call" — the share is added to the *existing* call alongside the camera, as a second video stream. > **Info** > > **You'll need an active call.** Screen share attaches to an existing > `Call` — start one of these first. #### [Set up the client](/docs/browser-sdk/v4/guides/overview) Install the SDK and create a `SignalWire` client with a credential provider. #### [Place an outbound call](/docs/platform/voice/make-and-receive-calls#place-a-call-from-a-browser) Dial a destination with `client.dial()` to get a `Call` instance. #### [Answer an inbound call](/docs/browser-sdk/v4/guides/inbound-calls) Subscribe to `client.session.incomingCalls$` and `call.answer()`. ## Start / stop ```js call.self$.subscribe(async (self) => { if (!self) return; shareButton.onclick = async () => { try { if (self.screenShareStatus === "started") { await self.stopScreenShare(); } else { await self.startScreenShare(); } } catch (err) { handleShareError(err); } }; }); ``` `startScreenShare()` triggers the browser's screen-picker. The promise rejects on cancel *and* on permission denial — you need to tell them apart to avoid showing a scary error toast for what was a deliberate user choice. A call carries at most one screen share. Starting a second one throws [`ScreenShareAlreadyActiveError`](/docs/browser-sdk/v4/reference/errors/screen-share-already-active-error) and leaves the live share untouched — stop the first one before replacing it. Read `screenShareStatus` before calling and treat `'starting'` and `'stopping'` as busy. A screen-share failure is never fatal: whatever goes wrong with the share, the call itself keeps running. The picker is human time and sits outside the SDK's connect budget, so a user who takes a minute to choose a window is not timed out; only the 15 seconds of negotiation that follow are bounded, with [`AuxiliaryLegTimeoutError`](/docs/browser-sdk/v4/reference/errors/auxiliary-leg-timeout-error) if they run out. ## Handling permission and cancel The browser surfaces both outcomes as a `DOMException` from `getDisplayMedia()`. Use `err.name` to distinguish: | `err.name` | What happened | UX | | ------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------- | | `NotAllowedError` | User clicked Cancel in the picker, **or** the OS / browser denied the permission entirely | Silent — no toast on cancel | | `NotFoundError` | No source was available to share (rare — usually a misconfigured kiosk environment) | Tell the user "no shareable screen" | | `NotReadableError` | OS-level capture failure (another app holds the screen, hardware error) | Suggest retry / closing other apps | | `AbortError` | The session ended before capture started | Silent | | `NotSupportedError` | `getDisplayMedia()` isn't available (iOS Safari, some embedded WebViews) | Hide the share button entirely | `NotAllowedError` is the one to watch — its message contains `"Permission denied"` when the OS or browser blocked the prompt, vs `"Permission denied by user"` (Chromium) or an empty message (Firefox / Safari) when the user clicked Cancel. The distinction is fuzzy across browsers, so the safest UX is: **silent on `NotAllowedError`** and rely on the user re-trying, since they just made an explicit choice. ```js import { AuxiliaryLegCancelledError, ScreenShareAlreadyActiveError, } from "@signalwire/js"; function handleShareError(err) { if (err instanceof AuxiliaryLegCancelledError) { // Your own stopScreenShare() beat the leg to the finish — nothing to report. return; } if (err instanceof ScreenShareAlreadyActiveError) { showToast("Already sharing. Stop the current share first."); return; } if (err?.name === "NotAllowedError") { // Cancel or denied — don't surface a toast either way. return; } if (err?.name === "NotFoundError" || err?.name === "NotReadableError") { showToast("Couldn't start screen share. Close other capture apps and try again."); return; } if (err?.name === "NotSupportedError") { showToast("Screen sharing isn't supported in this browser."); return; } console.error("Unexpected screen share error:", err); } ``` ### Detect support before showing the button `getDisplayMedia()` is unavailable on iOS Safari and some embedded WebViews. Gate the button on the capability and the API existence so the user never sees a control that can't work: ```js const supported = typeof navigator.mediaDevices?.getDisplayMedia === "function"; call.self?.capabilities.screenshare$.subscribe((canShare) => { shareButton.hidden = !(supported && canShare); }); ``` ## Observing share state `screenShareStatus$` is the reactive form. Use it instead of polling [`screenShareStatus`] so your UI reflects auto-stop (user clicked "Stop sharing" in the browser bar, OS revoked permission, etc.): ```ts type ScreenShareStatus = 'none' | 'starting' | 'started' | 'stopping'; ``` ```js self.screenShareStatus$.subscribe((status) => { shareButton.classList.toggle("active", status === "started"); shareButton.disabled = status === "starting" || status === "stopping"; }); ``` ## How the share appears to other participants The screen-share track is delivered as a separate participant entry in `call.participants$` — usually with a name like `Screen` or the sharer's name suffixed with `(Screen)`. The local participant doesn't need to render their own share to see it; the SDK doesn't mirror the local capture into `remoteStream$`. To detect which participants are screen shares specifically, check the participant's metadata or `type`. In the kitchen-sink demo, the share state is read from `self.screenShareStatus` directly: ```js const isSharing = call.self?.screenShareStatus === "started"; ``` ## Audio with the share Pass `{ audio: true }` to also request the shared surface's audio. The default is video only — the share never carries the local microphone, which stays on the main connection: ```js await self.startScreenShare({ audio: true }); ``` Whether audio is actually captured depends on the browser, the OS, and the surface the user picks. A share the user grants without audio yields a video-only stream, and the share still starts normally — so treat surface audio as best-effort, not a guarantee. In practice: * Chrome / Edge: offered for tabs and windows; system audio depends on the OS and version. * Firefox: video only. * Safari: video only (and screen sharing requires explicit user-initiated permission per session). ## Browser quirks * **User gesture required.** `startScreenShare()` must originate from a click or keyboard event — calling it on a timer or after an async chain that didn't start from a gesture will fail. * **iOS Safari.** Tab screen sharing isn't supported. iOS has its own system-wide screen-broadcast flow which is outside the browser's reach. * **Multiple displays.** The picker lists each display separately; selecting "Entire screen" on a multi-monitor setup shares only the picked display. ## Stopping from outside the page The user can stop sharing from the browser's "Stop sharing" toolbar. When they do, the underlying track ends and the SDK transitions `screenShareStatus$` to `'none'` automatically — no action required on your side. Subscribe to `screenShareStatus$` and your UI will update without polling. ## Reference * [`SelfParticipant.startScreenShare()`] — open picker, add the share track * [`SelfParticipant.stopScreenShare()`] — remove the share track * [`SelfParticipant.screenShareStatus$`] / [`screenShareStatus`] — reactive status (`'none' | 'starting' | 'started' | 'stopping'`) * [`SelfCapabilities.screenshare$`] — capability gate [`SelfParticipant.startScreenShare()`]: /docs/browser-sdk/v4/reference/self-participant/start-screen-share [`SelfParticipant.stopScreenShare()`]: /docs/browser-sdk/v4/reference/self-participant/stop-screen-share [`SelfParticipant.screenShareStatus$`]: /docs/browser-sdk/v4/reference/self-participant/screen-share-status$ [`screenShareStatus`]: /docs/browser-sdk/v4/reference/self-participant/screen-share-status$ [`SelfCapabilities.screenshare$`]: /docs/browser-sdk/v4/reference/self-capabilities/screenshare$