Encoders and the segment output are created when the capture source first has a picture, at its own size (portrait monitors record portrait) unless a resolution is configured, so nothing is written while waiting for a source. SPEC.md records both decisions; validation docs cover how sessions should end when the game closes.
187 lines
10 KiB
Markdown
187 lines
10 KiB
Markdown
# Recording foundation validation
|
||
|
||
The first recording milestone proves the OBS capture, encoder and segment
|
||
pipeline before the UI is built (SPEC.md, "Implementation validation before
|
||
building out the UI"). `clipbin-probe` drives the real `clipbin-obs` engine and
|
||
prints `key=value` lines; the scripts in `scripts/validate/` turn them into
|
||
pass/fail results.
|
||
|
||
| Where | Script | Status |
|
||
| --- | --- | --- |
|
||
| Linux, headless rig (Wayland portal + Xwayland) | `scripts/validate/linux-rig.sh` | All 9 scenarios pass on devbox (Intel UHD 770, VA-API) |
|
||
| Linux, real session (CachyOS, Hyprland, RX 9070 XT, 1440p60) | `scripts/validate/linux-hardware.sh` | Passed 2026-10-08 (run 4, dc9cbd6) |
|
||
| Windows (Game Capture, NVENC/AMF/QSV) | `scripts/validate/windows.ps1` | Not run yet; script untested |
|
||
|
||
## Probe commands
|
||
|
||
```sh
|
||
cargo run -p clipbin-probe -- encoders --verify # hardware encoders per codec, proven by encoding
|
||
cargo run -p clipbin-probe -- record --target monitor # records; prints status, health and segments
|
||
cargo run -p clipbin-probe -- check # keyframes, codec, durations, cross-boundary remux
|
||
```
|
||
|
||
`record` targets `monitor`, `window` or `game` (Game Capture on Windows; the
|
||
same as `window` elsewhere). On Windows and X11, `--window` takes OBS's window
|
||
identifier; on Wayland the portal picker chooses and `--restore-token` reuses
|
||
an earlier choice. See the top of `crates/clipbin-probe/src/main.rs` for all
|
||
options. `check` needs FFmpeg's `ffprobe` and `ffmpeg` on `PATH`.
|
||
|
||
## Linux headless rig
|
||
|
||
`scripts/rig/headless.sh [--x11] <command>` runs a command inside a private
|
||
D-Bus session with PipeWire, a virtual audio sink, headless Sway on the GPU,
|
||
xdg-desktop-portal-wlr (auto-selecting the only output) and a 60 FPS test
|
||
pattern with a tone. `--x11` runs the game and command on Sway's Xwayland.
|
||
|
||
Arch packages: `sway xdg-desktop-portal xdg-desktop-portal-wlr xorg-xwayland
|
||
xorg-xwininfo pipewire pipewire-pulse wireplumber libpulse ffmpeg` plus a VA-API
|
||
driver (`intel-media-driver` or `libva-mesa-driver`).
|
||
|
||
The suite covers:
|
||
|
||
1. Encoder verification: H.264 must be usable; HEVC and AV1 are reported.
|
||
2. Wayland monitor capture in H.264 and HEVC: segments start on keyframes,
|
||
last 2 s, remux to MP4 across boundaries, decode cleanly, carry the tone.
|
||
3. X11 window capture (Xcomposite).
|
||
4. Failures that must end in `status=failed`: PipeWire stream lost, captured
|
||
X11 window closed, segment directory unwritable, free space below the margin.
|
||
5. A portal that refuses the request fails with an actionable reason and
|
||
never reports `Recording`.
|
||
|
||
Segment checks run with `--expect-motion`, so black or frozen footage (which
|
||
still makes valid segments) fails.
|
||
|
||
## Linux hardware sign-off
|
||
|
||
Run `scripts/validate/linux-hardware.sh` from the Hyprland session. It verifies
|
||
encoders at 1440p60, records the monitor with each usable codec (the first run
|
||
shows the portal picker; later runs pass the restore token and ask whether the
|
||
picker stayed away), records a window, and asks you to close a window
|
||
mid-recording. Send back `report.txt` from the output directory.
|
||
|
||
### First hardware run (2026-10-08)
|
||
|
||
CachyOS 7.2.9, Hyprland, RX 9070 XT, 1440p60, OBS 32.0.4:
|
||
|
||
- H.264, HEVC and AV1 all encode through VA-API (`*_ffmpeg_vaapi_tex`).
|
||
Monitor and window capture recorded keyframe-aligned 2 s segments that
|
||
remuxed across boundaries with no decode errors.
|
||
- The restore token came back on one of three runs, probably because the
|
||
"Allow a restore token" box in the picker was ticked only once. The script
|
||
now has a dedicated restore step.
|
||
- One run never got a portal selection: the picker either did not appear or
|
||
was not answered, and the probe stayed in `Waiting for capture` for 20 s
|
||
while writing black segments that `check` passed. `check --expect-motion`
|
||
now catches black footage; a refused request now fails at once.
|
||
- Closing the captured window (Super+W) is not visible to capture: xdph keeps
|
||
the PipeWire stream `streaming` and OBS repeats the window's last frame.
|
||
See findings.
|
||
|
||
### Second hardware run (2026-10-08)
|
||
|
||
- The restore token works: the restored run started recording in 2 s without
|
||
showing the picker.
|
||
- Hyprland's portal backend (xdg-desktop-portal-hyprland) wedged in
|
||
`SelectSources` during this run and answered nothing afterwards, until
|
||
restarted; the earlier "no selection" runs were likely the same hang.
|
||
xdph shows its picker during `SelectSources`, so a hang there looks the same
|
||
as a user still choosing. A 10 s timeout tried in afc54b3 would cut users
|
||
off mid-pick and was removed. Recording stays honestly in `Waiting for
|
||
capture`; the app needs a prompt to choose a source (or restart the portal)
|
||
while it waits.
|
||
- The motion check sampled a segment from before capture started (the user
|
||
took 16 s in the picker); it now uses the segment with the most motion.
|
||
- A portrait monitor (Hyprland transform 3) is captured as 1440x2560 and
|
||
pillarboxed in the 2560x1440 canvas.
|
||
|
||
### Fourth hardware run: pass (2026-10-08, dc9cbd6)
|
||
|
||
After restarting xdg-desktop-portal-hyprland, every step passed: H.264, HEVC
|
||
and AV1 through VA-API at 1440p60, monitor and window capture with motion,
|
||
keyframe-aligned segments that remux across boundaries, and a restored
|
||
selection that skipped the picker. Closing a window was again not detected
|
||
(informational, see findings).
|
||
|
||
## Windows
|
||
|
||
Run `scripts/validate/windows.ps1 -GameWindow "<title>:<class>:<exe>"` with a
|
||
game running. It assembles the official OBS 32.0.4 runtime, checks the Game
|
||
Capture hook signatures, builds the probe, verifies NVENC/AMF/QSV encoders and
|
||
records Monitor, Game and Window Capture.
|
||
|
||
Windows status:
|
||
|
||
- `clipbin-core`, `clipbin-obs` and `clipbin-probe` pass `cargo clippy` for
|
||
`x86_64-pc-windows-msvc` (cross-checked from Linux). Nothing has been linked
|
||
or run on Windows.
|
||
- The GPUI desktop crate has not been checked for Windows; its `ring`
|
||
dependency needs the MSVC C toolchain.
|
||
- The assembled runtime keeps the six Game Capture hook binaries byte-identical
|
||
to the release, with their Authenticode signatures present; signature
|
||
validity has not been checked on Windows.
|
||
- Unvalidated: encoder ids and settings for NVENC, AMF and QSV; Game Capture
|
||
hooking and its loss detection (the engine expects the source size to drop
|
||
to 0); WASAPI audio; `obs-ffmpeg-mux.exe` beside the executable; linking
|
||
`dstr_vprintf` through the libobs crate's `obs.lib`.
|
||
|
||
## Findings for the next milestones
|
||
|
||
- **Plugin loading.** libobs always adds default plugin paths: the build-time
|
||
install prefix on Linux and `../../obs-plugins/64bit` relative to the
|
||
working directory on Windows. Searching would load other OBS installations'
|
||
plugins or load ours twice, so the engine opens exactly the bundled
|
||
allowlist (`runtime::PLUGINS`) by path.
|
||
- **Segment timestamps.** The FFmpeg muxer restarts each segment's video and
|
||
audio timestamps at zero independently. Concatenating segments naively leaves
|
||
22–43 ms video gaps at boundaries. Video is constant frame rate and AAC
|
||
packets are continuous, so a segment's exact session offset is the count of
|
||
earlier frames and audio packets; the A/V offset stays within one AAC packet
|
||
(≤21 ms) and does not drift. The timeline and export work must place
|
||
segments this way.
|
||
- **Availability.** With 1 s keyframes and 2 s segments, splits happen on
|
||
schedule every 2.0 s, so the newest complete footage is about 2 s behind
|
||
plus finalisation: inside the spec's 2–5 s window.
|
||
- **Honest status needs more than OBS's counters.** OBS kept counting bytes
|
||
into a dead muxer process, and once reported a clean stop after the disk
|
||
filled. Status therefore requires footage to grow on disk, and recording
|
||
stops while 1 GB remains so the last segment can be finalised.
|
||
- **PipeWire source loss.** OBS's PipeWire capture keeps its size and renders
|
||
its last frame after the stream stops, and only logs the stream state. The
|
||
engine reads that log line (stable for the pinned OBS version). A more robust
|
||
follow-up is watching the PipeWire node state directly.
|
||
- **Ending a session when the game closes.** With Hyprland's portal, closing a
|
||
captured window leaves the stream running on its last frame, with no state
|
||
change or error, so capture cannot tell it from a static window. Monitor
|
||
capture never sees the game close at all. The session therefore ends from
|
||
the game's process exiting (SPEC: "Stop/finalise the session when its game
|
||
exits"); capture loss stays a failure signal, not the normal end:
|
||
- Linux: Steam runs each game under `reaper SteamLaunch AppId=<id>`, which
|
||
lives exactly as long as the game (Proton included). Watch that process
|
||
with a pidfd (`pidfd_open` + poll) rather than polling `/proc`.
|
||
- Windows: watch the game's process handle (`OpenProcess` +
|
||
`WaitForSingleObject`), found from Steam's `RunningAppID` registry value.
|
||
Game Capture reports size 0 when its game closes; the exit must be seen
|
||
first so a normal exit finalises the session instead of failing it.
|
||
- Hyprland's IPC does announce window closes, but it is compositor-specific
|
||
and cannot map a portal stream to a window, so it is not used.
|
||
- **Nothing is written before capture works.** Encoders and the segment
|
||
output are created only once the source has a picture, so waiting in the
|
||
picker or for a stalled portal writes no footage.
|
||
- **Recording size.** By default each recording is the captured window's or
|
||
monitor's own size, set when capture first has a picture (portrait monitors
|
||
record portrait). If the window is resized mid-session the size stays and
|
||
the picture is scaled to fit.
|
||
- **Audio capture failures are not detected yet.** When the PulseAudio server
|
||
was unreachable, recording continued with a silent track.
|
||
- **Linux runtime dependencies.** The Linux OBS build links the system's
|
||
FFmpeg, PipeWire and other libraries, so it is tied to the distribution it
|
||
was built on. Packaging (for example Flatpak, or bundling FFmpeg) is an open
|
||
decision; OBS 32.0 needs mbedtls 3 (`mbedtls3` on Arch).
|
||
- **OBS objects.** Scenes and sources are created private (OBS 31+ otherwise
|
||
registers scenes with the main canvas, which keeps them alive), unknown
|
||
source types fail instead of becoming placeholders, and shutdown drains
|
||
libobs's destroy queue first.
|
||
- **Plugin configuration.** libobs is started without a module config path;
|
||
set one under the app's config directory before plugins that save settings
|
||
are added.
|