Screenshare
A screen share is a LiveKit Track.Source.ScreenShare track published from the local participant — ephemeral media like audio, with no DB columns or server state. The token already grants screen_share + screen_share_audio publish sources (calls).
How it works
- Start: the Share Screen button calls
room.localParticipant.setScreenShareEnabled(true, { audio: true, resolution: 1920×1080@15 })— the browser's nativegetDisplayMediapicker opens; picking a window/tab/monitor publishes the track (plus a separateScreenShareAudiotrack if the user checked "Share audio"). - Stop:
setScreenShareEnabled(false)unpublishes; the layout reverts to the video grid. - Remote clients receive the stream via
RoomEvent.TrackSubscribed(sourceScreenShare) intocall/media.ts'sremoteScreenShareStreams. - Starting a share auto-pops the call into picture-in-picture so the sharer keeps watching the call while presenting; the activation-ordering constraint behind this lives in picture-in-picture.
- Feature-detected: the button is hidden when
getDisplayMediais unavailable (iOS Safari).
The track is the only state there is, so every layout and moderation behaviour below is downstream of one publish:
flowchart TD
BUTTON["Share Screen"] --> PICKER["getDisplayMedia picker"]
PICKER -->|"cancelled"| NOTHING["nothing published"]
PICKER -->|"window, tab or monitor"| PUBLISH["setScreenShareEnabled true"]
PUBLISH --> TRACK["ScreenShare track, plus ScreenShareAudio when opted in"]
TRACK --> PIP["auto-pop to picture-in-picture"]
TRACK --> LOCAL["local layout switches to presenter"]
TRACK --> REMOTE["RoomEvent.TrackSubscribed on every other client"]
REMOTE --> REMOTELAYOUT["remoteScreenShareStreams, same presenter layout"]
TRACK --> STOP{"how it ends"}
STOP -->|"sharer stops"| UNPUBLISH["setScreenShareEnabled false"]
STOP -->|"moderator acts"| ADMIN["StopScreenShare revokes the publish source"]
ADMIN --> UNPUBLISH
UNPUBLISH --> REVERT["back to the grid"]
Presenter layout
When any participant publishes a screen track, the shared MessageContentCallStage <main> switches from flex-col to flex-row: the screenshare stage is the left hero (flex-1, full height), and participant tiles move into a shrink-0 right sidebar — a vertical scrollable column of h-32 aspect-video tiles (h-20 in the PiP window's isDense mode).
┌────────────────────────────────────────┬─────────────┐
│ SCREEN SHARE (left, flex-1) │ [tile] │
│ presenter name bottom-left │ [tile] │ ← right sidebar (scrolls)
│ │ [tile] │
├────────────────────────────────────────┴─────────────┤
│ [🎤] [🎧] [🖥] [📞] (control bar) │
└──────────────────────────────────────────────────────┘
- Clicking the stage requests native fullscreen of the whole
MessageContentCallViewroot (Google Meet model), not the<video>— the Fullscreen API isolates rendering to the target's subtree, so fullscreening the video alone would drop the tiles and controls.MessageContentCallScreenShareStageemitsfullscreen;MessageContentCallViewowns the root ref and callsrequestFullscreen(). In the PiP window the stage is non-interactive (no click-to-fullscreen), matching Meet. - Multiple simultaneous sharers: tabs above the main area; the active tab is the focused share.
- Pin/spotlight: clicking any tile pins it (
pinnedParticipantIdincall/media.ts, values are LiveKit identities = auth session ids); local-only, not broadcast. With nothing pinned,activeScreenShareParticipantIdprefers your own share and otherwise takes the first sharer in the presenter list.
Moderation
AdminActionType.StopScreenShare (gate: MuteMembers — conceptually the same as force-mute) enforces server-side via the LiveKit Admin API (updateLiveKitTrackSources, which force-mute shares): revoke the target's screen-share publish sources and mute active screen tracks; the target client also runs setScreenShare(false) + snackbar through the admin action hook. See moderation.
Key files
| File | Role |
|---|---|
apps/web/app/components/Message/Content/Call/ScreenShare/Stage.vue | presenter <video> stage |
apps/web/app/components/Message/Content/Call/ScreenShare/Button.vue | start/stop toggle |
apps/web/app/store/message/room/call/media.ts | screenshare/pin state + local/remote streams |
apps/web/app/store/message/room/liveKit.ts | setScreenShare + screen track event bridge |