#!/usr/bin/env bash # Manual: local HEVC encoder throughput preflight for upstream transport work; not part of CI. set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" STAMP="$(date +%Y%m%d-%H%M%S)" OUTPUT_DIR="${LESAVKA_LOCAL_HEVC_ENCODER_PREFLIGHT_OUTPUT_DIR:-/tmp/lesavka-local-hevc-encoder-preflight-${STAMP}}" SUMMARY_JSON="${LESAVKA_LOCAL_HEVC_ENCODER_PREFLIGHT_JSON:-${OUTPUT_DIR}/hevc-encoder-preflight.json}" RUN_LOG="${OUTPUT_DIR}/hevc-encoder-preflight.log" MODES="${LESAVKA_LOCAL_HEVC_ENCODER_PREFLIGHT_MODES:-1280x720@20,1280x720@30,1920x1080@20,1920x1080@30}" DURATION_SECONDS="${LESAVKA_LOCAL_HEVC_ENCODER_PREFLIGHT_SECONDS:-5}" BITRATE_KBIT="${LESAVKA_LOCAL_HEVC_ENCODER_PREFLIGHT_KBIT:-3000}" MIN_REALTIME_FACTOR="${LESAVKA_LOCAL_HEVC_ENCODER_PREFLIGHT_MIN_REALTIME_FACTOR:-1.05}" ENCODER="${LESAVKA_LOCAL_HEVC_ENCODER:-auto}" mkdir -p "${OUTPUT_DIR}" echo "==> local HEVC encoder preflight" echo " ↪ artifact_dir=${OUTPUT_DIR}" echo " ↪ summary_json=${SUMMARY_JSON}" echo " ↪ run_log=${RUN_LOG}" echo " ↪ modes=${MODES}" echo " ↪ duration=${DURATION_SECONDS}s bitrate=${BITRATE_KBIT}kbit min_realtime_factor=${MIN_REALTIME_FACTOR}" echo " ↪ no remote host, sudo, tunnel, or RCT capture is used" ( cd "${REPO_ROOT}" python3 - "$OUTPUT_DIR" "$SUMMARY_JSON" "$MODES" "$DURATION_SECONDS" "$BITRATE_KBIT" "$MIN_REALTIME_FACTOR" "$ENCODER" <<'PY' import json import os import re import subprocess import sys import time from pathlib import Path output_dir = Path(sys.argv[1]) summary_json = Path(sys.argv[2]) modes = [mode.strip() for mode in sys.argv[3].split(",") if mode.strip()] duration_seconds = float(sys.argv[4]) bitrate_kbit = int(sys.argv[5]) min_realtime_factor = float(sys.argv[6]) encoder_override = sys.argv[7].strip() ENCODER_ORDER = ["nvh265enc", "vah265enc", "vaapih265enc", "v4l2h265enc", "x265enc"] MODE_RE = re.compile(r"^([0-9]+)x([0-9]+)@([0-9]+)$") def gst_has(element: str) -> bool: return subprocess.run( ["gst-inspect-1.0", element], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False, ).returncode == 0 def pick_encoder() -> str: if encoder_override and encoder_override != "auto": if not gst_has(encoder_override): raise SystemExit(f"requested HEVC encoder is unavailable: {encoder_override}") return encoder_override for candidate in ENCODER_ORDER: if gst_has(candidate): return candidate raise SystemExit("no supported HEVC encoder found") def encoder_chain(encoder: str, fps: int) -> str: if encoder == "x265enc": return ( f"x265enc tune=zerolatency speed-preset=ultrafast " f"bitrate={bitrate_kbit} key-int-max={max(fps, 1)} log-level=none" ) # Hardware encoder property names vary by driver. Use the bare element for # this availability/throughput preflight rather than failing on a missing # low-latency property that the runtime selector already probes separately. return encoder def has_annex_b(path: Path) -> bool: data = path.read_bytes() return b"\x00\x00\x00\x01" in data or b"\x00\x00\x01" in data def run_mode(encoder: str, mode: str) -> dict: match = MODE_RE.match(mode) if not match: raise SystemExit(f"invalid mode: {mode}") width, height, fps = map(int, match.groups()) frame_count = max(1, int(round(duration_seconds * fps))) media_seconds = frame_count / fps out_path = output_dir / f"{mode.replace('@', '_')}-{encoder}.h265" pipeline = ( f"videotestsrc num-buffers={frame_count} is-live=false pattern=smpte ! " f"video/x-raw,format=I420,width={width},height={height},framerate={fps}/1 ! " f"{encoder_chain(encoder, fps)} ! " "h265parse config-interval=-1 ! " "video/x-h265,stream-format=byte-stream,alignment=au ! " f"filesink location={out_path}" ) timeout_seconds = max(10.0, media_seconds * 4.0) start = time.monotonic() proc = subprocess.run( ["timeout", f"{timeout_seconds:.1f}", "gst-launch-1.0", "-q", *pipeline.split()], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False, ) elapsed = max(time.monotonic() - start, 0.001) encoded_bytes = out_path.stat().st_size if out_path.exists() else 0 realtime_factor = media_seconds / elapsed annex_b = encoded_bytes > 0 and has_annex_b(out_path) status = ( "pass" if proc.returncode == 0 and encoded_bytes > 0 and annex_b and realtime_factor >= min_realtime_factor else "fail" ) return { "mode": mode, "width": width, "height": height, "fps": fps, "frames": frame_count, "media_seconds": round(media_seconds, 3), "elapsed_seconds": round(elapsed, 3), "realtime_factor": round(realtime_factor, 3), "bytes": encoded_bytes, "annex_b": annex_b, "status": status, "artifact": str(out_path), "stderr_tail": "\n".join(proc.stderr.splitlines()[-12:]), } encoder = pick_encoder() results = [run_mode(encoder, mode) for mode in modes] summary = { "schema": "lesavka.local-hevc-encoder-preflight.v1", "encoder": encoder, "duration_seconds": duration_seconds, "bitrate_kbit": bitrate_kbit, "min_realtime_factor": min_realtime_factor, "verdict": "pass" if all(row["status"] == "pass" for row in results) else "fail", "results": results, } summary_json.parent.mkdir(parents=True, exist_ok=True) summary_json.write_text(json.dumps(summary, indent=2) + "\n") print(f"encoder={encoder}") for row in results: print( f"{row['status'].upper()} {row['mode']}: " f"frames={row['frames']} elapsed={row['elapsed_seconds']:.3f}s " f"rtx={row['realtime_factor']:.3f} bytes={row['bytes']} annex_b={row['annex_b']}" ) if summary["verdict"] != "pass": raise SystemExit("local HEVC encoder preflight failed") PY ) 2>&1 | tee "${RUN_LOG}" echo "==> done" echo "artifact_dir: ${OUTPUT_DIR}" echo "summary_json: ${SUMMARY_JSON}" echo "run_log: ${RUN_LOG}"