Recording¶
The recording module adds video output to your tests. It exposes a small surface
backed by a handful of platform-specific implementations and a router that picks the
right one per call.
External dependencies
The recording backends shell out to platform tools — ffmpeg everywhere, plus a
bundled Swift helper on macOS and a small Rust helper on Linux Wayland. See
Recording limitations for the per-platform notes.
The Recorder interface¶
interface Recorder {
fun start(
region: Rectangle,
output: Path,
options: RecordingOptions = RecordingOptions(),
): RecordingHandle
}
You start a recording, you get a RecordingHandle, you stop the handle when you're
done. Implementations must spawn the underlying process eagerly so frames are landing
in output by the time start() returns.
AutoRecorder — pick a backend per call¶
AutoRecorder is the entry point you should reach for first. It looks at what you pass
and picks the appropriate backend:
import dev.sebastiano.spectre.recording.AutoRecorder
import dev.sebastiano.spectre.recording.RecordingOptions
import dev.sebastiano.spectre.recording.screencapturekit.asTitledWindow
import java.awt.Rectangle
import java.nio.file.Path
val recorder = AutoRecorder()
val handle = recorder.start(
window = composeWindow.asTitledWindow(), // any java.awt.Frame works
region = Rectangle(100, 100, 800, 600),
output = Path.of("build/recordings/my-test.mp4"),
options = RecordingOptions(),
)
try {
// ...drive the UI
} finally {
handle.stop()
}
TitledWindow is an interface. The production adapter is the Frame.asTitledWindow()
extension shown above; tests typically wire a small in-memory implementation.
The routing is platform-keyed. Read the row that matches your OS:
| Platform | window |
Backend chosen |
|---|---|---|
| macOS | non-null | ScreenCaptureKitRecorder (bundled Swift helper). |
| macOS | null |
FfmpegRecorder region capture (avfoundation). |
| Windows | non-null with a non-blank title | FfmpegWindowRecorder (gdigrab title=). |
| Windows | null, or non-null with no title |
FfmpegRecorder region capture (gdigrab). |
| Linux Xorg | any | FfmpegRecorder region capture (x11grab). |
| Linux Wayland | any | WaylandPortalRecorder (xdg-desktop-portal + bundled Rust helper). |
A few details worth knowing:
- macOS SCK helper fallback. If the Swift helper isn't bundled in the running
JAR (e.g., you built on Linux but ran on macOS), the macOS + window path falls
back to
FfmpegRecorderregion capture with a warning on stderr. Operational SCK failures — permission denied, target window not found, helper crashed during init — propagate as exceptions rather than falling back silently, so you see the real cause. - Linux Wayland always uses the portal (when a portal recorder is wired up),
regardless of whether
windowis null, becauseLinuxX11Grabrefuses to run on Wayland sessions. The portal flow captures the user-picked monitor and crops to the requested region. The first call pops the compositor's "share your screen" dialog; subsequent calls in the same JVM run reuse the grant. - Linux Wayland helper. The portal recorder runs a small Rust binary
(
spectre-wayland-helper) bundled in therecordingartifact. It drivesxdg-desktop-portal's ScreenCast interface and hands a PipeWire FD togst-launch-1.0.
Lower-level backends¶
If you know exactly which backend you want, instantiate it directly and skip the router:
| Backend | Use it for |
|---|---|
FfmpegRecorder |
Region capture on macOS, Windows, and Linux Xorg. (Throws on Wayland — use WaylandPortalRecorder there.) Default for "no window in mind". |
FfmpegWindowRecorder |
Windows-only window-targeted capture via gdigrab title=. |
ScreenCaptureKitRecorder |
macOS-only window-targeted capture via the bundled Swift helper. |
WaylandPortalRecorder |
Linux Wayland-only via xdg-desktop-portal and a bundled Rust helper. |
Per-OS prerequisites¶
macOS has the most involved setup; the others are short. macOS first:
macOS¶
ffmpegonPATH.- The Swift ScreenCaptureKit helper is bundled inside
recording's artifact. Universal-binary opt-in is available via./gradlew :recording:assembleScreenCaptureKitHelper -PuniversalHelper. - Screen Recording TCC permission, granted under System Settings → Privacy & Security → Screen Recording.
macOS attributes TCC to the responsible parent process — the binary that
launched the JVM, not java itself. Grant the permission to whichever app opened
the test JVM:
- Running tests from IntelliJ IDEA → grant IntelliJ IDEA.
./gradlew testfrom a terminal → grant Terminal.app (or iTerm, etc.).- A standalone
javainvocation from a third-party launcher → grant that launcher. - On CI (e.g., GitHub Actions macOS runners) → the runner image either needs to be pre-granted or use a notarised wrapper that has its own TCC entry.
macOS doesn't refresh TCC for already-running processes, so after granting, fully quit and relaunch the parent app — not just the JVM child — for the permission to take effect. The typical first-time flow is "run, fail, grant, relaunch parent app, run again". See Troubleshooting for failure modes when permission is missing or attached to the wrong binary.
Other platforms¶
- Windows —
ffmpegonPATH.gdigrabships withffmpeg. - Linux Xorg —
ffmpegwith thex11grabinput enabled (default in distro builds). - Linux Wayland —
gst-launch-1.0plus the GStreamer plugins for H.264/Matroska. The Rust helper is bundled insiderecording's artifact. Recording requires a Wayland compositor that exposesorg.freedesktop.portal.ScreenCast; tested against GNOME/mutter on Ubuntu 22.04 and 24.04.
For the full set of trade-offs (frame drop behaviour, minimum dimensions, crop pitfalls, audio support) see Recording limitations.
Stopping cleanly¶
RecordingHandle.stop() is synchronous and waits for the encoder to flush. If your
test fails before reaching the stop call, wrap the recording in try/finally:
Otherwise, the spawned ffmpeg (or helper, or gst-launch) keeps writing until it's
killed, and you may end up with truncated or unfinalised output files.