diff --git a/crates/clipbin-probe/src/main.rs b/crates/clipbin-probe/src/main.rs index 3982218..77aa9fb 100644 --- a/crates/clipbin-probe/src/main.rs +++ b/crates/clipbin-probe/src/main.rs @@ -324,6 +324,7 @@ fn record(options: &Options) -> Result> { Duration::from_secs(options.segment_seconds as u64), ); let mut status = RecordingStatus::Starting; + let mut recorded = false; println!("t={} status={}", elapsed(), status.label()); let stop_at = started + Duration::from_secs(options.seconds); @@ -349,6 +350,7 @@ fn record(options: &Options) -> Result> { health.written_bytes ); report_status(&mut status, tracker.sample(&health, now), &elapsed()); + recorded |= status == RecordingStatus::Recording; } if !stopping && (now >= stop_at || matches!(status, RecordingStatus::Failed { .. })) { stopping = true; @@ -382,7 +384,10 @@ fn record(options: &Options) -> Result> { if let Some(token) = recording.restore_token() { println!("restore_token={token}"); } - Ok(outcome == Some(StopReason::Success) && !matches!(status, RecordingStatus::Failed { .. })) + println!("recorded={recorded}"); + Ok(recorded + && outcome == Some(StopReason::Success) + && !matches!(status, RecordingStatus::Failed { .. })) } fn report_status(shown: &mut RecordingStatus, current: &RecordingStatus, elapsed: &str) { diff --git a/scripts/validate/linux-rig.sh b/scripts/validate/linux-rig.sh new file mode 100755 index 0000000..70c3a73 --- /dev/null +++ b/scripts/validate/linux-rig.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# Runs the recording validation scenarios in the headless rig and reports +# PASS/FAIL for each. Needs the Linux OBS runtime (scripts/obs/build-linux.sh), +# a VA-API or NVENC capable GPU, and the rig packages listed in +# docs/validation.md. +# +# scripts/validate/linux-rig.sh [output-dir] +set -uo pipefail + +repo="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +out="${1:-${TMPDIR:-/tmp}/clipbin-validation}" +rig="${repo}/scripts/rig/headless.sh" +probe="${repo}/target/debug/clipbin-probe" +rm -rf "${out}" && mkdir -p "${out}" + +cargo build --locked -p clipbin-probe --manifest-path "${repo}/Cargo.toml" || exit 1 + +failures=0 +result() { + local name="$1" ok="$2" detail="${3:-}" + if [[ "${ok}" == 0 ]]; then + echo "PASS ${name} ${detail}" + else + echo "FAIL ${name} ${detail} (log: ${out}/${name}.log)" + failures=$((failures + 1)) + fi +} +# has : the probe printed a line matching pattern. +has() { grep -qE "$2" "$1"; } + +# 1. Hardware encoders: H.264 must be usable; HEVC and AV1 are reported. +log="${out}/encoders.log" +"${rig}" "${probe}" encoders --verify --out "${out}/encoders" > "${log}" 2> "${out}/encoders.obs.log" +result encoders "$?" "$(grep -E '^usable\.' "${log}" | tr '\n' ' ')" + +# 2. Wayland monitor capture through the portal: segments, remux, audio. +for codec in h264 hevc; do + if has "${log}" "^usable\.${codec}=none"; then + echo "SKIP wayland-${codec} (no ${codec} hardware encoder)" + continue + fi + name="wayland-${codec}" + dir="${out}/${name}" + "${rig}" "${probe}" record --codec "${codec}" --seconds 9 --out "${dir}" > "${out}/${name}.log" 2>&1 \ + && "${probe}" check --codec "${codec}" --out "${dir}" >> "${out}/${name}.log" 2>&1 + ok=$? + volume="$(ffmpeg -v info -i "${dir}/session/segment-000001.mkv" -map 0:a -af volumedetect -f null - 2>&1 \ + | awk '/max_volume/ { print $5 }')" + if [[ "${ok}" == 0 ]] && awk -v v="${volume:--999}" 'BEGIN { exit !(v > -40) }'; then ok=0; else ok=1; fi + result "${name}" "${ok}" "audio_max_volume=${volume:-none}dB" +done + +# 3. X11 window capture through Xcomposite (Xwayland in the rig). +name=x11-window +"${rig}" --x11 bash -c "'${probe}' record --target window --window \"\${CLIPBIN_RIG_GAME_WINDOW}\" --seconds 9 --out '${out}/${name}'" \ + > "${out}/${name}.log" 2>&1 \ + && "${probe}" check --out "${out}/${name}" >> "${out}/${name}.log" 2>&1 +result "${name}" "$?" + +# expect_failure : recording started, then failed honestly. +expect_failure() { + local name="$1" reason="$2" log="${out}/$1.log" + if [[ "${probe_exit}" != 0 ]] && has "${log}" "status=Recording" && has "${log}" "status=failed reason=${reason}"; then + result "${name}" 0 "$(grep -m1 -o 'status=failed reason=.*' "${log}")" + else + result "${name}" 1 + fi +} + +# 4. The PipeWire stream ends mid-recording (portal backend exits). +name=wayland-source-lost +"${rig}" bash -c "'${probe}' record --seconds 15 --out '${out}/${name}' & p=\$!; sleep 5; kill \$(cat \"\${CLIPBIN_RIG_DIR}/xdpw.pid\"); wait \$p" \ + > "${out}/${name}.log" 2>&1 +probe_exit=$? +expect_failure "${name}" "the capture source stopped" + +# 5. The captured X11 window closes mid-recording. +name=x11-window-closed +"${rig}" --x11 bash -c "'${probe}' record --target window --window \"\${CLIPBIN_RIG_GAME_WINDOW}\" --seconds 15 --out '${out}/${name}' & p=\$!; sleep 5; kill \$(cat \"\${CLIPBIN_RIG_DIR}/game.pid\"); wait \$p" \ + > "${out}/${name}.log" 2>&1 +probe_exit=$? +expect_failure "${name}" "the capture source stopped" + +# 6. Segments can no longer be written (directory made read-only). +name=write-failure +"${rig}" bash -c "'${probe}' record --seconds 20 --out '${out}/${name}' & p=\$!; sleep 4; chmod 500 '${out}/${name}/session'; wait \$p" \ + > "${out}/${name}.log" 2>&1 +probe_exit=$? +chmod 700 "${out}/${name}/session" 2>/dev/null +expect_failure "${name}" "(no footage was written|Error)" + +# 7. Free space falls below the safety margin. +name=low-disk-space +"${rig}" bash -c "'${probe}' record --seconds 10 --out '${out}/${name}' --min-free-mb \$(( \$(df --output=avail -B 1000000 '${out}' | tail -1) + 1 ))" \ + > "${out}/${name}.log" 2>&1 +probe_exit=$? +if [[ "${probe_exit}" != 0 ]] && has "${out}/${name}.log" "status=failed reason=not enough disk space" \ + && ! has "${out}/${name}.log" "status=Recording"; then + result "${name}" 0 "$(grep -m1 -o 'status=failed reason=.*' "${out}/${name}.log")" +else + result "${name}" 1 +fi + +# 8. The portal grants nothing usable: recording never starts or claims to. +name=portal-no-source +CLIPBIN_RIG_PORTAL_OUTPUT=NO-SUCH-OUTPUT "${rig}" "${probe}" record --seconds 8 --out "${out}/${name}" \ + > "${out}/${name}.log" 2>&1 +probe_exit=$? +if [[ "${probe_exit}" != 0 ]] && ! has "${out}/${name}.log" "status=Recording"; then + result "${name}" 0 "$(grep 'status=' "${out}/${name}.log" | tail -1)" +else + result "${name}" 1 +fi + +echo +if [[ "${failures}" == 0 ]]; then + echo "All scenarios passed. Logs in ${out}" +else + echo "${failures} scenario(s) failed. Logs in ${out}" +fi +exit $(( failures > 0 ))