Build
Screen sharing
Share a screen beside your camera, with adaptive detail and motion modes, optional tab audio and automatic stop handling.
SDK 0.5.0 adds a screen beside the camera in an RTC room. Hosts and co-hosts can publish it; viewers and moderators can watch. Screen sharing uses the same participant-minute balance as the rest of the room. All plans include every video quality.
Browser setup
Call publishScreen() from a button click on an HTTPS page. The browser asks the person to choose a tab, window or display each time a new share starts. A reconnect reuses a screen that is still live, without reopening the picker.
import { StreamClient } from "@voltkit-rtc/client";
client.on("screenShareStarted", ({ stream, mode }) => {
screenPreview.srcObject = stream;
screenPreview.muted = true;
showSharingState(mode);
});
client.on("screenShareEnded", ({ reason }) => {
screenPreview.srcObject = null;
showSharingStopped(reason); // "stopped" or "track-ended"
});
shareButton.addEventListener("click", async () => {
try {
await client.publishScreen({ mode: "detail", audio: true });
} catch (error) {
showCaptureError(error);
}
});
stopButton.addEventListener("click", () => client.unpublishScreen());
publishScreen({ mode, audio, stream }) accepts an existing display MediaStream instead of opening the picker. mode defaults to "detail". audio defaults to false when the SDK opens the picker; an existing stream's audio is used unless audio: false is passed. The SDK stops the supplied screen tracks when sharing ends. Keep a separate stream if another feature needs its own capture lifetime.
unpublishScreen() stops only screen video and its audio. Camera, microphone and music continue. unpublish() and close() stop all publishing. Clicking the browser's Stop sharing automatically closes both screen producers and emits screenShareEnded once.
Detail and motion
| Mode | Best for | Top video | Smaller video | Publisher upload, before audio |
|---|---|---|---|---|
detail (default) | Slides, documents, code | Up to native 1920×1080, 15 fps, 2.5 Mbps | Half size, 15 fps, 0.6 Mbps | About 3.1 Mbps |
motion | Video, games, movement | Up to native 1920×1080, 30 fps, 6 Mbps | Half size, 30 fps, 0.6 Mbps | About 6.6 Mbps |
The image is never enlarged. Portrait displays keep their shape; very small sources send one layer. A viewer receives one adaptive copy, not the sum. Add up to 128 kbps when sharing tab/system audio. Camera upload is additional when both are live. Prefer detail for readable text; motion uses more bandwidth and encoding work.
Viewer video and audio
Screen video arrives through the ordinary track event with source: "screen". Shared audio is a separate audio producer with source: "screen-audio"; play it through its own audio element, like music.
client.on("track", ({ producerId, source, track }) => {
if (source === "screen") {
showScreen(producerId, new MediaStream([track]));
} else if (source === "screen-audio") {
playScreenAudio(producerId, new MediaStream([track]));
}
});
client.on("trackEnded", ({ producerId }) => removeMedia(producerId));
await client.setReceiveQuality(screenProducerId, "480p");
await client.setReceiveQuality(screenProducerId, "auto"); // recommended
The menu can show Auto (recommended) / 1080p / 720p / 480p. Each choice selects an actual copy no larger than that ceiling, or the smallest available if none fits. It does not create a new resolution: a two-layer 1080p screen has 540p and 1080p copies, so 480p selects 540p. A weak connection can still receive less. The choice survives reconnects while that producer exists. Older publishers without layer metadata support Auto; explicit presets report quality-unavailable.
Flutter setup
Camera, screen, mic, music and optional screen audio have independent producers.
Only hosts/co-hosts can publish. setCamEnabled and setMicEnabled leave a
screen share alone; unpublishScreen() leaves camera/mic/music alone.
unpublish() and close() stop all publishing.
client.onScreenShareStarted.listen((stream) => screenPreview.srcObject = stream);
client.onScreenShareEnded.listen((_) => screenPreview.srcObject = null);
await client.publishScreen(
options: const ScreenPublishOptions(mode: 'detail', audio: true),
stream: displayStream, // optional: a screen capture the app already owns
);
await client.unpublishScreen();
screenSharing reports whether a capture is held; screenStream exposes it.
detail sends up to 1920×1080 at 15 fps, 2.5 Mbps plus a 600 kbps half-size
layer (~3.1 Mbps total). motion sends 30 fps at 6 Mbps plus 600 kbps (~6.6
Mbps total). Captures keep their native aspect ratio and are never upscaled.
The sender uses maintain-resolution for detail and maintain-framerate for
motion. A screen received on onTrack has source == 'screen'; granted
screen audio has source == 'screen-audio' and should have its own audio
renderer/volume, like music. Requesting audio does not guarantee it: when no
track is granted screen-audio-unavailable is reported on onPublishWarning
and video continues. Android's current flutter_webrtc capture provides video
only. Do not substitute the microphone for system audio.
A still-live screen is republished after reconnect, with the same profile.
Platform track onEnded automatically calls unpublishScreen; the application's
existing callback is preserved. flutter_webrtc 1.6.2 does not forward Android
MediaProjection's OS stop callback to Dart. Keep a visible Stop sharing
button wired to unpublishScreen; an app with a native projection-stop bridge
must call that method from its stop callback too. Do not infer that a static
slide has ended from a lack of new frames. Ordinary iOS camera/mic rooms
continue to work; full-screen capture has a separate opt-in preview below.
iOS ReplayKit preview (prepared SDK 0.6.0)
This preview is disabled by default. Native compilation and Apple-device behavior are unverified. SDK 0.6.0 is prepared in the repository; use the local SDK when evaluating it until that version is published. SDK 0.5.0's Android/desktop APIs remain available. No Apple-hardware test is assigned to the current owner, who has no Mac or Apple device.
The prepared Flutter package contains a native lifecycle plugin, the Broadcast
Upload Extension sources and tool/prepare_ios_screen_share.py. In the consuming
app's Xcode project, create a Broadcast Upload Extension without UI, embed it in
Runner, and enable the same signed App Group on both targets (iOS 14+). Add the
template's SampleHandler.swift, FrameSender.swift and shared
ReplayKitSession.swift to the extension target only. Merge these keys into
Runner's existing Info.plist; the extension Info.plist must use the same group:
<key>RTCAppGroupIdentifier</key>
<string>group.com.example.yourapp</string>
<key>RTCScreenSharingExtension</key>
<string>com.example.yourapp.ScreenShare</string>
The SDK's doc/IOS-SCREEN-SHARING.md supplies the complete target, entitlements,
signing, background-mode and future native-validation instructions. Rebuild the
application after installing the plugin. No RTC secret belongs in the extension.
client.onScreenSharePaused.listen((paused) => showScreenPaused(paused));
await client.publishScreen(
options: const ScreenPublishOptions(
iosBroadcastEnabled: true, // opt in only when integrating the preview
mode: 'detail',
iosStartTimeout: Duration(seconds: 60),
),
);
The person must select Start Broadcast in the system picker. The preview handles session-scoped stop, pause/resume, timeout and cancelled-start cleanup. The default 60-second start timeout is configurable up to four minutes; iOS does not provide an immediate picker-cancel callback. Native capture is video only: requesting audio warns and continues without system audio. Camera/mic/music stay independent. Background execution, rotation, native compilation and device performance remain unverified; keep the preview opt-in until a Mac-capable integrator completes that gate.
Android foreground service
The application owns a mediaProjection foreground service and its visible
notification. For example, using flutter_background, add these entries:
<!-- AndroidManifest.xml, outside <application> -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PROJECTION" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Inside <application>; the class is supplied by flutter_background -->
<service
android:name="de.julianassmann.flutter_background.IsolateHolderService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="mediaProjection" />
Initialize the app's foreground-service plugin with an appropriate notification. Then pass its start/stop functions (or equivalent functions for your own native service). VoltKit requests user projection consent before invoking start, as modern Android requires:
// Import package:flutter_background/flutter_background.dart in your app.
await FlutterBackground.initialize(
androidConfig: const FlutterBackgroundAndroidConfig(
notificationTitle: 'Screen sharing',
notificationText: 'Your screen is being shared in this room',
),
);
await client.publishScreen(
options: const ScreenPublishOptions(mode: 'detail'),
startForegroundService: () async {
if (!await FlutterBackground.enableBackgroundExecution()) {
throw StateError('Foreground screen-capture service could not start');
}
},
stopForegroundService: () async {
await FlutterBackground.disableBackgroundExecution();
},
);
Missing callbacks are rejected with screen-share-service-required. If the
app passes stream, it owns permissions and its service, and stops both after
unpublishScreen; the SDK does not dispose caller-owned capture tracks.
Make stop callbacks idempotent: the prepared 0.6.0 SDK also attempts cleanup
when service startup partially succeeds and then throws. Cancelling during
consent prevents late service/capture startup. Individual cleanup failures warn
with screen-stop-failed while the remaining tracks, stream and service are
still released; they do not prevent leaving the room.
Windows, macOS and Linux
Use desktopCapturer.getSources(types: [SourceType.Screen, SourceType.Window])
from flutter_webrtc, show a picker, and pass the source the user selects:
await client.publishScreen(
options: ScreenPublishOptions(mode: 'motion', sourceId: selectedSource.id),
);
Missing selection reports screen-share-source-required. macOS needs Screen
Recording permission in System Settings. Linux support depends on the display
server/desktop capture support in flutter_webrtc; validate your target desktop.
A capture that cannot produce measurable frames reports
screen-share-size-unavailable instead of advertising invented dimensions.
Other capture failures report screen-share-failed, permission denial can
report screen-share-denied, and starting twice reports screen-share-active.
See screen sharing for platform setup
and troubleshooting for connection,
quality and device problems.
Browser permissions and limits
Most phone browsers do not offer display capture. The SDK rejects with screen-share-unsupported before asking for capture when the API is absent. Use the browser's feature detection and offer the supported native app path where appropriate.
The browser decides which tabs, windows and audio sources are shareable. audio: true is a request: screen video can succeed without an audio track. Tell the user to select Share tab audio if their browser offers it. System audio availability varies by browser and operating system. Viewers may need to tap Enable sound because of autoplay rules.
A cross-origin iframe needs an appropriate display-capture Permissions Policy and allow="display-capture; camera; microphone". The parent and child pages must both permit it. Browser permission is always required; the SDK does not bypass the picker.
Troubleshooting
screen-share-unsupported: use a supported desktop browser or the native screen-share path.NotAllowedError: the person cancelled or denied capture, or Permissions Policy blocked it. Offer another explicit attempt; do not loop permission prompts.screen-share-active: stop the current screen before starting another one.screen-share-no-video: the supplied stream contains no live screen video.forbidden: only the host or a co-host can publish; wait for promotion and reconnection before capture.- No shared sound: check whether the captured stream has an audio track, the selected source supports audio, and the viewer has enabled playback.
- Soft text: choose
detail, share the relevant window, and use Auto on the viewer. Selecting 480p intentionally reduces available detail. - Frozen capture: check whether the source window is minimised, the device has locked, or the browser has suspended the page.
See Browser SDK, Flutter SDK, Video quality and Troubleshooting.
Last updated . Something unclear or wrong? Tell us.