Recording limitations¶
Two recording modes¶
Before reading the rest of this page, it's worth being explicit about what "recording"
means here. The recording module exposes two structurally different ways of producing
a video file, and each has its own limitations:
- Region capture — records a fixed
Rectangleof the virtual desktop, frame by frame. The source of pixels is the OS framebuffer (or the platform's nearest equivalent —avfoundationon macOS,gdigrabon Windows,x11grabon Linux Xorg, or the Wayland compositor's PipeWire stream on Linux Wayland). Whatever is showing on screen inside that rectangle goes into the file, regardless of which window is there. Region capture is the only path available for embedded surfaces (aComposePanelinside an IntelliJ tool window, aJFrame, etc.) because there's no top-level OS window to target. - Window-targeted capture — captures a specific OS window's pixels directly, not
the screen rectangle the window happens to occupy. The source is the window's own
backing store (
ScreenCaptureKiton macOS) or the OS-levelgdigrab title=capture on Windows. Because the source is the window itself, movement, occlusion, and off-screen position don't break the recording. This path is the right choice when you have a top-levelComposeWindow.
Backend → mode mapping:
| Backend | Mode |
|---|---|
FfmpegRecorder |
Region capture (avfoundation/gdigrab/x11grab). |
WaylandPortalRecorder |
Region capture, sourced from the Wayland portal. |
ScreenCaptureKitRecorder |
Window-targeted (macOS). |
FfmpegWindowRecorder |
Window-targeted (Windows, gdigrab title=). |
AutoRecorder |
Routes to the right one based on TitledWindow?. |
The rest of this document is mostly about region capture's constraints, because that's where the failure modes are. The window-targeted backends exist to sidestep those failure modes, and the section below (Window-targeted capture) explains how they do it.
Platform¶
- macOS —
avfoundationregion capture. Requires the Screen Recording permission. - Windows —
gdigrabregion capture, plus title-based window capture viaFfmpegWindowRecorder. - Linux Xorg sessions —
x11grabregion capture. ReadsDISPLAY. Routine validation has only been on Ubuntu 22.04's Xorg session (one machine, one X server build) and on CI underxvfb-run(Xorg protocol over a virtual framebuffer, no GPU). Other Xorg WMs/distros fall under the "Linux is best-effort, contributions welcome" line in the README. - Linux Wayland sessions —
gst-launch-1.0driven through thexdg-desktop-portalScreenCast interface, with the PipeWire FD passed to the encoder by a small Rust helper binary (spectre-wayland-helper, sources atrecording/native/linux/). The JVM-sideWaylandPortalRecorderspawns the helper and talks to it over stdin/stdout via newline-delimited JSON. First call within a session pops the compositor's "share your screen" dialog; subsequent calls reuse the grant via the portal'srestore_token. Why the helper: a pure-JVM attempt hit a dbus-java UnixFD-unmarshalling bug that wasn't fixable trivially, and Rust'sstd::processmakes FD inheritance intogst-launcha one-liner where the JVM'sProcessBuilderdoesn't expose the necessaryfcntl(F_SETFD, ...)knob. The helper-as-subprocess shape also matches the macOS SCK helper (recording/native/macos/) — same pattern, same bundling, same recorder-skeleton on the JVM side.
Validated end-to-end on Ubuntu 22.04/GNOME 42/mutter and Ubuntu 24.04/GNOME 46/mutter
(real-pixel mp4 with the smoke runner, 2026-05-02 and 2026-05-03 — the 24.04 run also
confirmed cursor_mode=Embedded composites the system cursor into the captured frames
when RecordingOptions.captureCursor=true, #87). KDE/Plasma, sway, wlroots-based
compositors, non-Ubuntu distros, and other Ubuntu versions aren't part of the
routine validation matrix yet — the
xdg-desktop-portal interface is standardised across compositors so most should "just
work", but bug reports are how we'll find out. See the README for the contribution invite.
Frame-rate fidelity tracks what the compositor delivers. The pipeline runs the
PipeWire stream through a videorate element clamped to RecordingOptions.frameRate
(default 30). When the source delivers fewer frames than the target, videorate pads
the gaps by duplicating the last frame; when it delivers more, the excess gets dropped.
On real hardware with GPU-side compositor composition this is a no-op — the source
comfortably sustains 30 fps and the output is byte-clean. On a Hyper-V/VirtualBox VM
with a software-rendered virtual GPU, the source rate dips into the 5–25 fps range and
the output mp4 contains visible duplicate-frame runs even though the file metadata
reports a flat 30 fps. This is faithful capture, not a recording bug — the compositor
genuinely had no new frame to deliver during those gaps. If your scenario captures from
a VM and the visible stutter matters more than the frame-rate metadata, lower
RecordingOptions.frameRate to match what your VM actually produces (15 is usually a
safe floor for software-rendered VM GPUs).
Region capture¶
- Region capture, not window capture. Region capture records a fixed
Rectangleof the virtual desktop — whatever pixels the screen happens to be showing inside that region land in the file. The region is bound atRecorder.start(...)time and does not follow a window. UseScreenCaptureKitRecorder(macOS) orFfmpegWindowRecorder(Windows) when you have a top-level window to target — the next section covers what the window-targeted backends do differently. - Embedded
ComposePanelsurfaces always fall through to region capture.AutoRecorder.start(window: TitledWindow?, region: Rectangle, …)picks window-targeted capture only whenwindowis non-null and (on Windows) has a non-blank title. TheFrame.asTitledWindow()adapter exposes that title for top-levelComposeWindows, but a panel embedded inside an IntelliJ tool window, aJFrame, aJDialog, or aSwingPanelhost inside Compose has no top-levelFrameto adapt — callers passwindow = nulland get the region path. Practical consequences: - Anything that visually overlaps the panel — other windows, the menu bar, OS notifications, a floating popup that escapes the panel's bounds — appears in the recording.
- The captured region is the panel's screen-space bounds at start. If the host window moves or resizes while recording, the panel's pixels drift out of the captured rectangle and you record whatever is now under the original rectangle (often empty desktop).
- Off-screen panels record black frames.
Window movement and popups under region capture¶
These limitations are specific to the region path; the window-targeted backends below solve them.
- Window movement isn't followed. Move the host window after
start(...)and the recording keeps capturing the original screen rectangle. Stop and restart to follow the new position. - Popups that escape the captured region are clipped. Compose Desktop's
OnWindowpopup layer renders in a separate top-level OS window; if that popup appears outside the recorded rectangle the recording does not include it.OnSameCanvasandOnComponentpopups stay inside the panel / window bounds and are recorded as long as the panel itself is.
Window-targeted capture¶
ScreenCaptureKitRecorder (macOS) and FfmpegWindowRecorder (Windows, gdigrab title=)
target a specific OS window rather than a screen rectangle, which removes the
region-capture failure modes described above:
- Window movement is followed automatically. Drag the window across the screen while recording and the file keeps capturing its pixels — no need to stop and restart.
- Occlusion doesn't matter. Other windows passing in front of the target window do not appear in the recording. The capture reads the target's own backing store, not the composited screen.
- Off-screen target keeps recording. A window dragged off the visible desktop still has its pixels composed, so capture continues. (Compare the region path, which would record whatever's now under the original rectangle.)
OnWindowpopups are not captured by the target's recorder, because the popup is a separate OS window with its own title — it isn't the window being targeted.OnSameCanvasandOnComponentpopups are part of the target window's surface and are recorded normally.
WaylandPortalRecorder is structurally a third path: the portal hands the capture a
PipeWire stream from the compositor, scoped to a user-picked monitor, which then gets
cropped to the requested region. The monitor-level source means it doesn't follow a
specific window the way SCK and FfmpegWindowRecorder do — moving the target window
within the same monitor stays within the captured stream, but moving it off-monitor
or to another display does not. Treat its movement-and-popup behaviour as closer to
region capture than to window-targeted, with the bonus that the source is the
compositor and not raw framebuffer reads.
HiDPI/Retina¶
- The recorded resolution is the screen-pixel size of the region, not the dp size. A 400×300dp region on a 2× display becomes an 800×600 pixel video.
Rectanglecoordinates passed tostart(...)are in screen pixels. If you derive the region fromAutomatorNode.boundsOnScreenyou get the right thing for free; if you derive it fromboundsInWindowyou have to apply the density yourself.
Permissions and process lifecycle¶
- The JVM process needs macOS Screen Recording permission (
MacOsRecordingPermissionsdocuments the path). Without it,ffmpegexits during the startup probe andRecorder.startsurfaces an error rather than handing back a handle. - The
ffmpegsubprocess is spawned eagerly: by the timestart(...)returns, frames are landing in the output file. A failure to spawn (binary not on PATH, codec unavailable, AVFoundation device busy) surfaces as an exception fromstart(...), not as a silent "successful" handle. - The handle MUST be stopped (
RecordingHandle.stop(...)) for the file to be flushed cleanly. A JVM exit without stop leaves a partial/non-finalised file and an orphaned subprocess.
Current non-limitations¶
- Cursor capture is configurable via
RecordingOptions.captureCursorand works under the region path. The cursor pixels are baked into the frames; there is no overlay you can toggle in post-processing. - Audio capture is intentionally absent — Spectre currently records video only.
Window-targeted recording¶
- Window-targeted capture via
ScreenCaptureKitRecorderis the recommended path for top-levelComposeWindowsurfaces on macOS. It removes the "anything overlapping the region appears in the video" class of problems and follows the window across the screen. Windows has the equivalent path viaFfmpegWindowRecorder(gdigrab title=). - The embedded-panel path is still region capture only — there's no host window for SCK
or
gdigrab title=to target.