The screen recorder bridge records the device’s screen to a video and saves it in a
file root, where the page can play, upload or delete it. It’s built on
Shiny.ScreenRecorder.
In a browser, a page records the screen with getDisplayMedia. The WebViews on iOS and Android don’t offer it, and
desktop WebViews vary, so a page inside the app uses this bridge instead. A page running in a normal browser can keep
using getDisplayMedia, or Shiny.ScreenRecorder on Blazor WebAssembly.
The page chooses nothing about what is recorded. It’s always the primary display, or the app itself on iOS and Mac
Catalyst. Picking a display or window is left out on purpose, because listing them would give the page every other app’s
window titles.
What each device can do beyond recording differs too. GET /_bridge/screenrecorder lists it in capabilities:
Capability
Request field
Notes
Recording
missing where nothing can record, such as Linux without a portal or an encoder
PauseResume
pause / resume routes
not on macOS 15’s capture path
Microphone
includeMicrophone
not on Windows
SystemAudio
includeSystemAudio
everything playing on macOS and Linux; only the app’s own audio on iOS and Android; none on Windows
CursorToggle
showCursor: false
desktops only; touch screens have no pointer
FrameRateControl
frameRate
a ceiling from 1 to 240; a still screen produces no frames
BitrateControl
videoBitrate
bits a second
Downscaling
maxWidth
worth setting on Retina displays and modern phones
A request for something that isn’t listed answers 501 before anyone is asked anything, rather than recording without it.
AddScreenRecorderBridge() also registers Shiny.ScreenRecorder’s recorder for the platform, or the portal recorder on
Linux, so there’s nothing else to call.
ScreenRecorderBridgeOptions
Default
Root
data
the file root recordings are saved into
Folder
screen-recordings
the folder inside Root; each recording is REC_<yyyyMMdd-HHmmss>.mp4 and never overwrites another
MaxDuration
1 hour
the longest any recording runs; longer requests are shortened to it, and null removes the limit
ConfirmStart
null
asked on the main thread before every recording; return false to refuse it
FOREGROUND_SERVICE and FOREGROUND_SERVICE_MEDIA_PROJECTION, plus RECORD_AUDIO for the microphone. The recorder’s service and consent activity merge into the manifest automatically. Word the recording’s notification with an IScreenRecordingNotificationDelegate.
iOS and Mac Catalyst
NSMicrophoneUsageDescription for the microphone. The app has to be in the foreground.
macOS
the OS prompts for Screen Recording permission itself. Add NSMicrophoneUsageDescription for the microphone, and com.apple.security.device.audio-input when sandboxed.
Windows
Windows 10 1903 or later. Packaged apps declare the graphicsCapture capability.
Linux
xdg-desktop-portal with ScreenCast (GNOME, KDE Plasma, wlroots), and GStreamer (gstreamer1.0-tools gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-pipewire) or, on X11, ffmpeg. Without them, the bridge reports no capabilities and answers 501. Flatpak hosts aren’t supported.
Every route, its body and its response is listed under Requests, and both events under Events.
Access ahead of time. Android can’t grant the screen before a recording starts, so POST …/access reports
Unknown there; treat anything but Denied as worth trying.
Errors:501 not_supported (the device can’t record, or can’t honour a setting), 409 recording_busy (one at a
time, including a recording the app started itself), 409 not_recording (stop, pause or resume with nothing
running), 403 permission_denied (the user declined), 403 declined (ConfirmStart said no), 400 for a bad
body.
Stopping takes a moment. Finishing the file and writing its index isn’t instant on a long recording. A page that
gives up waiting doesn’t interrupt it.
The page isn’t the only one that can stop a recording. The user can stop it from Android’s notification, the macOS menu
bar or the compositor. The OS can interrupt it for a call or a locked screen. MaxDuration can run out. Each of these
ends the recording on the device. Whatever could be salvaged is saved just as a stop would have saved it, and
screenrecorder.ended reports it:
reason
recording
Stopped
always: the page stopped it
Cancelled
never
MaxDurationReached
always: the file is complete
RevokedByUser
usually
InterruptedBySystem, TargetLost
when something could be kept
EncoderFailed
rarely; message says what went wrong
Unknown
when something could be kept
Listen for screenrecorder.ended rather than assuming a stop will find a recording running. A stop that comes too late
answers 409 not_recording, and the recording is already in lastRecording.
This bridge records the whole screen on four of the five platforms, so it’s the widest device access any bridge gives.
Every route requires the bridge policy, which by default admits callers on this device
only. Every platform except Windows also asks the user itself.
On Windows, give the user a say with ConfirmStart:
Where recording stands after asking. Android cannot grant the screen ahead of time, so it reports Unknown there and asks when a recording starts: anything but Denied is worth trying.
A file the page can reach through the files bridge: a named root and a path inside it. Bridges that take or return a file use this, so nothing reaches the page that /_bridge/files would not also serve.
Where recording stands after asking. Android cannot grant the screen ahead of time, so it reports Unknown there and asks when a recording starts: anything but Denied is worth trying.
What to record. Every setting but the audio is optional, and each one needs its ScreenRecorderCapability: a request for something this device cannot do answers 501.
includeMicrophone?
boolean
Mix in the microphone. Needs Microphone.
includeSystemAudio?
boolean
Capture what the device is playing. Needs SystemAudio.
showCursor?
boolean | null
Draw the pointer. Null or true draws it; false needs CursorToggle.
frameRate?
number | null
A ceiling, 1–240. Needs FrameRateControl.
videoBitrate?
number | null
Bits a second. Needs BitrateControl.
maxWidth?
number | null
Downscale to at most this many pixels wide. Needs Downscaling.
maxDurationSeconds?
number | null
Stop and file it after this long. Never longer than maxDurationSeconds, which also applies when this is null.