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.
You’ll need an active call. Screen share attaches to an existing
Call — start one of these first.
Install the SDK and create a SignalWire client with a credential provider.
Dial a destination with client.dial() to get a Call instance.
Subscribe to client.session.incomingCalls$ and call.answer().
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.
The browser surfaces both outcomes as a DOMException from
getDisplayMedia(). Use err.name to distinguish:
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.
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:
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.):
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:
getDisplayMedia() can capture system / tab audio on some platforms
(Chromium-based browsers on macOS / Windows). The SDK forwards
whatever the browser provides — there’s no separate “share audio”
toggle. If the user’s browser doesn’t support display audio, only
the video is shared.
In practice:
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.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 'idle' automatically — no action required
on your side. Subscribe to screenShareStatus$ and your UI will
update without polling.
SelfParticipant.startScreenShare() — open picker, add the share trackSelfParticipant.stopScreenShare() — remove the share trackSelfParticipant.screenShareStatus$ / screenShareStatus — reactive status ('idle' | 'starting' | 'started' | 'stopping')SelfCapabilities.screenshare$ — capability gate