#!/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}" --expect-motion --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 --expect-motion --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|EncodeError)" # 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 refuses the request: an actionable failure, never Recording, # and no footage written (output starts only once capture has a picture). name=portal-refused 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" \ && has "${out}/${name}.log" "status=failed reason=screen capture was not allowed" \ && [[ -z "$(ls -A "${out}/${name}/session" 2>/dev/null)" ]]; then result "${name}" 0 "$(grep -m1 -o 'status=failed reason=.*' "${out}/${name}.log")" else result "${name}" 1 fi # 9. Capture audio disappears while video/output remain healthy. name=audio-source-lost "${rig}" bash -c "'${probe}' record --seconds 15 --out '${out}/${name}' & p=\$!; sleep 6; kill \$(cat \"\${CLIPBIN_RIG_DIR}/pulse.pid\"); wait \$p" \ > "${out}/${name}.log" 2>&1 probe_exit=$? if [[ "${probe_exit}" == 0 ]] && has "${out}/${name}.log" "status=Recording" \ && has "${out}/${name}.log" "audio capture lost" \ && ! has "${out}/${name}.log" "status=failed"; then result "${name}" 0 "video continued; audio loss surfaced as degraded" 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 ))