In this guide you'll add a screen-share button to a call you already have. Screen sharing lets a user show their screen, a window, or a browser tab: a teacher showing slides, a developer walking through code, a support agent showing where to click.
Where this fits: you have a working call. Screen sharing doesn't need a new token or a new connection. It's one more track (a stream of media) that the user sends, next to their camera.
What you'll build
A Share screen button. When sharing is on, the screen shows big and the camera becomes a small thumbnail.
What you need
- A working call. See Build a video call.
- A token with
publish: true. A user without it can watch shares but can't start one. - Web or Flutter. React Native can't capture the screen. See Screen sharing.
Steps
1. Turn sharing on and off
In your app, one function toggles sharing. It checks whether you're already sharing, then starts or stops:
async function toggleScreenShare() {
const existing = room.localParticipant.tracks.find((t) => t.kind === 'screenShare');
if (existing) {
await room.disableScreenShare();
} else {
await room.enableScreenShare(); // triggers the browser's picker
}
}Future<void> toggleScreenShare(LivqenoRoom room) async {
final sharing = room.localParticipant.tracks.any(
(t) => t.kind == LivqenoTrackKind.screenShare,
);
if (sharing) {
await room.disableScreenShare();
} else {
await room.enableScreenShare();
}
}enableScreenShare() opens the browser's picker, where the user chooses a
screen, window, or tab. It returns the shared track (LocalTrack). If you're
already sharing, it returns undefined.
You should now see the browser's picker when you click your button. Once the user picks something, everyone else in the room receives the share.
2. Only show the button if it can work
Mobile browsers can't share the screen (they don't have getDisplayMedia,
the browser API for screen capture). A button that always fails is worse than
no button. Check first in your app, and hide the button if it's not
supported:
// Livqeno's own check covers the three APIs a call needs at all:
import { getBrowserSupportDetails } from '@livqeno/rtc';
const { supported, missing } = getBrowserSupportDetails();
if (!supported) showUnsupportedBrowserNotice(missing);
// Screen capture is a separate capability and not part of that check,
// so test for it directly:
const canShareScreen = typeof navigator.mediaDevices?.getDisplayMedia === 'function';
if (!canShareScreen) hideScreenShareButton();If you call it anyway on an unsupported device:
- You get
NOT_SUPPORTED. The device can't do it. Don't retry. - This is different from
MEDIA_ERROR, which is worth retrying.
3. Show it big
People want to read a shared screen, so it should be the biggest thing on
the page. A screen share is a track with kind === 'screenShare'. When one
arrives, put it in the main area and everything else in thumbnails:
room.on('trackSubscribed', (track, participant) => {
const target = track.kind === 'screenShare' ? stageEl : thumbnailsEl;
target.append(track.attach());
});import { ParticipantView, useRemoteParticipants } from '@livqeno/react';
function Stage() {
const remote = useRemoteParticipants();
const sharer = remote.find((p) => p.tracks.some((t) => t.kind === 'screenShare'));
// ParticipantView renders whichever video track the participant has,
// preferring camera or screen share — so promoting the sharer to the
// stage is all this needs.
return sharer ? <ParticipantView participant={sharer} /> : <CameraGrid />;
}4. Handle the browser's own "Stop sharing" button
People often miss this step. While sharing, the browser shows its own Stop sharing bar, outside your page. Users click it as often as your button.
On the web, the SDK notices when that happens. It stops sending the share,
so everyone else's view updates on its own, and it fires
localTrackUnpublished in your app. Your UI is the part you still have to
update. Listen for the event in your app:
room.on('localTrackUnpublished', (track) => {
if (track.kind === 'screenShare') setSharing(false);
});Without this, your button keeps saying "Stop sharing" after sharing has already stopped. The same event also fires when your own button stops the share, so one listener covers both.
How it works
- A screen share is a normal track, labelled as a screen. WebRTC (the
browser technology underneath calls) can't tell a camera from a screen on
its own. Livqeno sends the label along with the track, so other people's
apps see
kind === 'screenShare'and know to show it big. - It's a second video track. The camera keeps going. Others receive both, so your layout needs a rule for which one is the main view.
- Nobody else's call is interrupted. The track is added to the existing connection.
Production tips
- Screen shares are heavy. A shared 4K screen uses much more bandwidth than a camera. Consider limiting it. It's the first thing to suffer on a bad connection.
- One share per person. Calling
enableScreenShare()twice returnsundefinedthe second time. - No React Native. The platform can't capture the screen. Don't show a button that can't work.
- No audio.
enableScreenShare()shares video only. Tab audio isn't captured.
Next steps
- Screen sharing: details for each platform.
- Build a group call: a share plus many cameras needs a layout rule. This guide shows the grid side.
- Tracks & publishing · Browser support