diff --git a/Cargo.lock b/Cargo.lock index 9460fdc..850481f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1658,7 +1658,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lesavka_client" -version = "0.27.7" +version = "0.27.8" dependencies = [ "anyhow", "async-stream", @@ -1692,7 +1692,7 @@ dependencies = [ [[package]] name = "lesavka_common" -version = "0.27.7" +version = "0.27.8" dependencies = [ "anyhow", "base64", @@ -1704,7 +1704,7 @@ dependencies = [ [[package]] name = "lesavka_server" -version = "0.27.7" +version = "0.27.8" dependencies = [ "anyhow", "base64", diff --git a/client/Cargo.toml b/client/Cargo.toml index 6b425d8..c974668 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,7 +4,7 @@ path = "src/main.rs" [package] name = "lesavka_client" -version = "0.27.7" +version = "0.27.8" edition = "2024" [dependencies] diff --git a/client/src/input/camera/encoder_selection.rs b/client/src/input/camera/encoder_selection.rs index eaf0dc0..338f20a 100644 --- a/client/src/input/camera/encoder_selection.rs +++ b/client/src/input/camera/encoder_selection.rs @@ -55,7 +55,7 @@ impl CameraCapture { if buildable_encoder("vah264enc") { return Ok(( "vah264enc", - supported_encoder_property("vah264enc", &["keyframe-period"]), + supported_encoder_property("vah264enc", &["key-int-max", "keyframe-period"]), )); } if buildable_encoder("vaapih264enc") { @@ -110,7 +110,7 @@ impl CameraCapture { "nvautogpuh265enc", &["iframeinterval", "idrinterval", "gop-size"][..], ), - ("vah265enc", &["keyframe-period"][..]), + ("vah265enc", &["key-int-max", "keyframe-period"][..]), ("vaapih265enc", &["keyframe-period"][..]), ("v4l2h265enc", &["idrcount"][..]), ] { @@ -157,8 +157,30 @@ impl CameraCapture { #[cfg(not(coverage))] fn ffmpeg_hevc_nvenc_available() -> bool { + if !Path::new("/dev/nvidiactl").exists() { + return false; + } Command::new("ffmpeg") - .args(["-hide_banner", "-loglevel", "error", "-h", "encoder=hevc_nvenc"]) + .args([ + "-hide_banner", + "-loglevel", + "error", + "-f", + "lavfi", + "-i", + "color=c=black:s=128x128:r=1", + "-frames:v", + "1", + "-an", + "-pix_fmt", + "nv12", + "-c:v", + "hevc_nvenc", + "-f", + "null", + "-", + ]) + .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()) .status() diff --git a/client/src/input/camera/tests/mod.rs b/client/src/input/camera/tests/mod.rs index 2132650..b772d7f 100644 --- a/client/src/input/camera/tests/mod.rs +++ b/client/src/input/camera/tests/mod.rs @@ -186,7 +186,7 @@ fn coverage_h264_encoder_choice_honors_stable_test_overrides() { ("nvh264enc", ("nvh264enc", None)), ("nvautogpuh264enc", ("nvautogpuh264enc", None)), ("vulkanh264enc", ("vulkanh264enc", Some("idr-period"))), - ("vah264enc", ("vah264enc", Some("keyframe-period"))), + ("vah264enc", ("vah264enc", Some("key-int-max"))), ("vaapih264enc", ("vaapih264enc", Some("keyframe-period"))), ("v4l2h264enc", ("v4l2h264enc", Some("idrcount"))), ("unknown", ("x264enc", Some("key-int-max"))), diff --git a/common/Cargo.toml b/common/Cargo.toml index 14d9539..3e7bcd9 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lesavka_common" -version = "0.27.7" +version = "0.27.8" edition = "2024" build = "build.rs" diff --git a/docs/architecture-and-media-readiness.md b/docs/architecture-and-media-readiness.md index d18c3d9..a33978d 100644 --- a/docs/architecture-and-media-readiness.md +++ b/docs/architecture-and-media-readiness.md @@ -293,6 +293,17 @@ gadget has successfully queued a verified frame. The honest long-term answer is to retain a small Linux verification host in the lab when possible; otherwise the calibrated server telemetry is the available integrity contract. +### The `0.27.8` Proof Candidate + +End-to-end preparation exposed a separate client hardware-selection defect: +FFmpeg advertising `hevc_nvenc` did not prove that CUDA could encode a frame. +On the operator workstation the NVIDIA device was unavailable while the Intel +VA-API HEVC/H.264 route was healthy. Release 0.27.8 runtime-probes NVENC before +selection, falls back to the working GStreamer hardware encoder, and uses the +modern `key-int-max` property for `vah264enc` and `vah265enc`. The hardware +smoke harness now exercises the same usable route and retains encoded streams +plus hardware-decoded proof frames. + ## 7. The Current Downstream Video Failure The current blank downstream feeds fail before transport or decoding. @@ -356,7 +367,7 @@ The safe completion sequence for this incident is: 6. Run `scripts/install/server.sh` as the trusted deployment path. Preserve the already-attached USB gadget unless a controlled rebuild is explicitly required. -7. Confirm Theia reports server version `0.27.7`, the pushed release revision, +7. Confirm Theia reports server version `0.27.8`, the pushed release revision, direct MJPEG normalizer timeout `0`, and a coherent UVC contract. 8. Open both downstream RPCs and prove that each emits changing, decodable H.264 frames. @@ -374,7 +385,7 @@ hardware contract is repeatable. The remaining work falls into five groups. ### A. Install And Version Parity -- Push and deploy `0.27.7` through the client/server install scripts. +- Push and deploy `0.27.8` through the client/server install scripts. - Confirm client/server version and revision in every hardware probe artifact. - Eliminate the current state where a fixed client talks to an unfixed server. @@ -440,6 +451,6 @@ host repair: 9. disconnect/reconnect and device changes recover without stale media; and 10. diagnostics identify the failed physical stage when any item breaks. -Until that sequence passes on the installed `0.27.7` client/server pair, the +Until that sequence passes on the installed `0.27.8` client/server pair, the current release should be described as a validated code correction awaiting hardware deployment and end-to-end acceptance, not as a completed product fix. diff --git a/scripts/install/client.sh b/scripts/install/client.sh index 209d455..c791b64 100755 --- a/scripts/install/client.sh +++ b/scripts/install/client.sh @@ -147,6 +147,14 @@ first_available_gst_element() { return 1 } +ffmpeg_hevc_nvenc_usable() { + [[ -e /dev/nvidiactl ]] || return 1 + run_as_user ffmpeg -hide_banner -loglevel error \ + -f lavfi -i color=c=black:s=128x128:r=1 \ + -frames:v 1 -an -pix_fmt nv12 -c:v hevc_nvenc -f null - \ + >/dev/null 2>&1 +} + report_client_media_acceleration() { log "1f. Inspecting client media acceleration routes" @@ -166,8 +174,7 @@ report_client_media_acceleration() { vah265enc \ vaapih265enc \ v4l2h265enc || true) - if command -v ffmpeg >/dev/null 2>&1 \ - && ffmpeg -hide_banner -loglevel error -h encoder=hevc_nvenc >/dev/null 2>&1; then + if command -v ffmpeg >/dev/null 2>&1 && ffmpeg_hevc_nvenc_usable; then ffmpeg_hevc_encoder="hevc_nvenc" fi h264_encoder=$(first_available_gst_element \ @@ -209,7 +216,7 @@ report_client_media_acceleration() { if gst-inspect-1.0 nvcodec 2>&1 | grep -q 'Unable to initialize CUDA library'; then echo "⚠️ NVIDIA nvcodec is installed but CUDA initialization failed; NVENC HEVC is unavailable to GStreamer." if [[ -n $ffmpeg_hevc_encoder ]]; then - echo " FFmpeg hevc_nvenc is available and will be used for HEVC upstream hardware encode." + echo " FFmpeg hevc_nvenc encoded a probe frame and will be used for HEVC upstream hardware encode." else echo " Install a working NVENC/VAAPI/V4L2 HEVC route before enabling HEVC upstream." fi diff --git a/scripts/manual/run_hardware_media_smoke.sh b/scripts/manual/run_hardware_media_smoke.sh index 434fb74..541c842 100755 --- a/scripts/manual/run_hardware_media_smoke.sh +++ b/scripts/manual/run_hardware_media_smoke.sh @@ -10,10 +10,10 @@ RESULTS_TSV="${ARTIFACT_DIR}/results.tsv" SUMMARY_JSON="${ARTIFACT_DIR}/summary.json" SUMMARY_TXT="${ARTIFACT_DIR}/summary.txt" -UPSTREAM_HEVC_FILE="${ARTIFACT_DIR}/upstream-hevc-nvenc.hevc" -UPSTREAM_HEVC_FRAME="${ARTIFACT_DIR}/upstream-hevc-cuda-frame.png" -DOWNSTREAM_H264_FILE="${ARTIFACT_DIR}/downstream-h264-nvenc.h264" -DOWNSTREAM_H264_FRAME="${ARTIFACT_DIR}/downstream-h264-cuda-frame.png" +UPSTREAM_HEVC_FILE="${ARTIFACT_DIR}/upstream-hevc-hardware.hevc" +UPSTREAM_HEVC_FRAME="${ARTIFACT_DIR}/upstream-hevc-hardware-frame.png" +DOWNSTREAM_H264_FILE="${ARTIFACT_DIR}/downstream-h264-hardware.h264" +DOWNSTREAM_H264_FRAME="${ARTIFACT_DIR}/downstream-h264-hardware-frame.png" AUDIO_WAV="${ARTIFACT_DIR}/audio-aac-roundtrip.wav" AUDIO_RMS_JSON="${ARTIFACT_DIR}/audio-aac-roundtrip-rms.json" SMOKE_WIDTH="${LESAVKA_HARDWARE_SMOKE_WIDTH:-1280}" @@ -57,10 +57,13 @@ ffmpeg_has_encoder() { | awk -v codec="${codec}" '$2 == codec { found = 1 } END { exit found ? 0 : 1 }' } -ffmpeg_has_decoder() { - local codec="$1" - ffmpeg -hide_banner -decoders 2>/dev/null \ - | awk -v codec="${codec}" '$2 == codec { found = 1 } END { exit found ? 0 : 1 }' +ffmpeg_nvenc_usable() { + local encoder="$1" + [[ -e /dev/nvidiactl ]] || return 1 + ffmpeg -hide_banner -loglevel error -nostdin \ + -f lavfi -i color=c=black:s=128x128:r=1 \ + -frames:v 1 -an -pix_fmt nv12 -c:v "${encoder}" -f null - \ + >/dev/null 2>&1 } run_logged() { @@ -108,9 +111,9 @@ summary_txt = pathlib.Path(sys.argv[3]) artifact_dir = pathlib.Path(sys.argv[4]) artifact_paths = { "upstream_hevc_stream": sys.argv[5], - "upstream_hevc_cuda_frame": sys.argv[6], + "upstream_hevc_hardware_frame": sys.argv[6], "downstream_h264_stream": sys.argv[7], - "downstream_h264_cuda_frame": sys.argv[8], + "downstream_h264_hardware_frame": sys.argv[8], "audio_aac_roundtrip_wav": sys.argv[9], "audio_aac_roundtrip_rms": sys.argv[10], } @@ -190,6 +193,39 @@ select_gst_h264_decoder() { return 1 } +select_gst_hevc_decoder() { + local decoder + for decoder in nvh265dec vah265dec vaapih265dec v4l2h265dec v4l2slh265dec; do + if gst_has "${decoder}"; then + printf '%s\n' "${decoder}" + return 0 + fi + done + return 1 +} + +select_gst_hevc_encoder() { + local encoder + for encoder in nvh265enc nvautogpuh265enc vah265enc vaapih265enc v4l2h265enc; do + if gst_has "${encoder}"; then + printf '%s\n' "${encoder}" + return 0 + fi + done + return 1 +} + +select_gst_h264_encoder() { + local encoder + for encoder in nvh264enc nvautogpuh264enc vah264enc vaapih264enc v4l2h264enc; do + if gst_has "${encoder}"; then + printf '%s\n' "${encoder}" + return 0 + fi + done + return 1 +} + gst_h264_decode_chain() { case "$1" in vulkanh264dec) @@ -204,6 +240,17 @@ gst_h264_decode_chain() { esac } +gst_hevc_decode_chain() { + case "$1" in + nvh265dec|vah265dec|vaapih265dec|v4l2h265dec|v4l2slh265dec) + printf '%s\n' "$1" + ;; + *) + return 1 + ;; + esac +} + select_gst_aac_encoder() { local encoder for encoder in fdkaacenc voaacenc avenc_aac; do @@ -226,36 +273,49 @@ main() { exit 1 fi - if ! ffmpeg_has_encoder hevc_nvenc; then - record_result "client_upstream_hevc_nvenc_available" "fail" "ffmpeg hevc_nvenc encoder is missing" "" - OVERALL=1 - else - record_result "client_upstream_hevc_nvenc_available" "pass" "ffmpeg hevc_nvenc encoder is available" "" - fi - - if ! ffmpeg_has_encoder h264_nvenc; then - record_result "client_downstream_h264_nvenc_source_available" "fail" "ffmpeg h264_nvenc encoder is missing for downstream test source" "" - OVERALL=1 - else - record_result "client_downstream_h264_nvenc_source_available" "pass" "ffmpeg h264_nvenc encoder is available for downstream test source" "" - fi - - if ! ffmpeg_has_decoder hevc_cuvid; then - record_result "client_hevc_cuvid_visual_decode_available" "fail" "ffmpeg hevc_cuvid decoder is missing for visual evidence frame" "" - OVERALL=1 - else - record_result "client_hevc_cuvid_visual_decode_available" "pass" "ffmpeg hevc_cuvid decoder is available for visual evidence frame" "" - fi - - if ! ffmpeg_has_decoder h264_cuvid; then - record_result "client_h264_cuvid_visual_decode_available" "fail" "ffmpeg h264_cuvid decoder is missing for visual evidence frame" "" - OVERALL=1 - else - record_result "client_h264_cuvid_visual_decode_available" "pass" "ffmpeg h264_cuvid decoder is available for visual evidence frame" "" - fi - + local hevc_encoder="" + local hevc_encoder_route="" + local h264_encoder="" + local h264_encoder_route="" + local gst_hevc_decoder="" + local gst_hevc_chain="" local gst_h264_decoder="" local gst_h264_chain="" + + if ffmpeg_has_encoder hevc_nvenc && ffmpeg_nvenc_usable hevc_nvenc; then + hevc_encoder="hevc_nvenc" + hevc_encoder_route="ffmpeg" + elif hevc_encoder="$(select_gst_hevc_encoder)"; then + hevc_encoder_route="gstreamer" + else + record_result "client_upstream_hevc_hardware_encoder" "fail" "no runtime-usable HEVC hardware encoder found" "" + OVERALL=1 + fi + if [[ -n ${hevc_encoder} ]]; then + record_result "client_upstream_hevc_hardware_encoder" "pass" "selected ${hevc_encoder_route}:${hevc_encoder}" "" + fi + + if ffmpeg_has_encoder h264_nvenc && ffmpeg_nvenc_usable h264_nvenc; then + h264_encoder="h264_nvenc" + h264_encoder_route="ffmpeg" + elif h264_encoder="$(select_gst_h264_encoder)"; then + h264_encoder_route="gstreamer" + else + record_result "client_downstream_h264_hardware_source" "fail" "no runtime-usable H.264 hardware encoder found" "" + OVERALL=1 + fi + if [[ -n ${h264_encoder} ]]; then + record_result "client_downstream_h264_hardware_source" "pass" "selected ${h264_encoder_route}:${h264_encoder}" "" + fi + + if gst_hevc_decoder="$(select_gst_hevc_decoder)"; then + gst_hevc_chain="$(gst_hevc_decode_chain "${gst_hevc_decoder}")" + record_result "client_upstream_gstreamer_hevc_hw_decoder_available" "pass" "selected ${gst_hevc_decoder}" "" + else + record_result "client_upstream_gstreamer_hevc_hw_decoder_available" "fail" "no GStreamer hardware H.265 decoder found" "" + OVERALL=1 + fi + if gst_h264_decoder="$(select_gst_h264_decoder)"; then gst_h264_chain="$(gst_h264_decode_chain "${gst_h264_decoder}")" record_result "client_downstream_gstreamer_h264_hw_decoder_available" "pass" "selected ${gst_h264_decoder}" "" @@ -273,29 +333,41 @@ main() { fi if [[ ${OVERALL} -eq 0 ]]; then - run_shell "client_upstream_hevc_nvenc_file" \ - "GPU encodes a synthetic upstream HEVC stream with hevc_nvenc" \ - "ffmpeg -hide_banner -loglevel warning -y -nostdin -f lavfi -i testsrc2=size=${SMOKE_WIDTH}x${SMOKE_HEIGHT}:rate=${SMOKE_FPS} -frames:v ${SMOKE_FRAMES} -an -sn -dn -vf format=nv12 -c:v hevc_nvenc -preset p1 -tune ll -rc cbr -b:v ${SMOKE_BITRATE_KBPS}k -maxrate ${SMOKE_BITRATE_KBPS}k -bufsize ${SMOKE_BITRATE_KBPS}k -g ${SMOKE_FPS} -bf 0 -forced-idr 1 -f hevc '${UPSTREAM_HEVC_FILE}'" + if [[ ${hevc_encoder_route} == ffmpeg ]]; then + run_shell "client_upstream_hevc_hardware_file" \ + "GPU encodes a synthetic upstream HEVC stream with ${hevc_encoder}" \ + "ffmpeg -hide_banner -loglevel warning -y -nostdin -f lavfi -i testsrc2=size=${SMOKE_WIDTH}x${SMOKE_HEIGHT}:rate=${SMOKE_FPS} -frames:v ${SMOKE_FRAMES} -an -sn -dn -vf format=nv12 -c:v ${hevc_encoder} -preset p1 -tune ll -rc cbr -b:v ${SMOKE_BITRATE_KBPS}k -maxrate ${SMOKE_BITRATE_KBPS}k -bufsize ${SMOKE_BITRATE_KBPS}k -g ${SMOKE_FPS} -bf 0 -forced-idr 1 -f hevc '${UPSTREAM_HEVC_FILE}'" + else + run_shell "client_upstream_hevc_hardware_file" \ + "GPU encodes a synthetic upstream HEVC stream with ${hevc_encoder}" \ + "gst-launch-1.0 -q videotestsrc num-buffers=${SMOKE_FRAMES} is-live=false pattern=smpte ! video/x-raw,width=${SMOKE_WIDTH},height=${SMOKE_HEIGHT},framerate=${SMOKE_FPS}/1 ! videoconvert ! video/x-raw,format=NV12 ! ${hevc_encoder} ! h265parse config-interval=-1 ! video/x-h265,stream-format=byte-stream,alignment=au ! filesink location='${UPSTREAM_HEVC_FILE}'" + fi run_shell "client_upstream_hevc_gstreamer_parse" \ "GStreamer accepts the HEVC elementary stream shape used by the upstream bundle path" \ "gst-launch-1.0 -q filesrc location='${UPSTREAM_HEVC_FILE}' ! h265parse config-interval=-1 ! video/x-h265,stream-format=byte-stream,alignment=au ! fakesink sync=false" - run_shell "client_upstream_hevc_cuvid_frame" \ - "CUDA decodes one visual proof frame from the upstream HEVC stream" \ - "ffmpeg -hide_banner -loglevel warning -y -nostdin -c:v hevc_cuvid -i '${UPSTREAM_HEVC_FILE}' -frames:v 1 '${UPSTREAM_HEVC_FRAME}'" + run_shell "client_upstream_hevc_hardware_frame" \ + "GStreamer hardware decoder ${gst_hevc_decoder} emits one visual HEVC proof frame" \ + "gst-launch-1.0 -q filesrc location='${UPSTREAM_HEVC_FILE}' ! h265parse ! ${gst_hevc_chain} ! videoconvert ! pngenc snapshot=true ! filesink location='${UPSTREAM_HEVC_FRAME}'" - run_shell "client_downstream_h264_nvenc_file" \ - "GPU creates a downstream-like H.264 elementary stream for decoder verification" \ - "ffmpeg -hide_banner -loglevel warning -y -nostdin -f lavfi -i testsrc2=size=${SMOKE_WIDTH}x${SMOKE_HEIGHT}:rate=${SMOKE_FPS} -frames:v ${SMOKE_FRAMES} -an -sn -dn -vf format=nv12 -c:v h264_nvenc -preset p1 -tune ll -rc cbr -b:v ${SMOKE_BITRATE_KBPS}k -maxrate ${SMOKE_BITRATE_KBPS}k -bufsize ${SMOKE_BITRATE_KBPS}k -g ${SMOKE_FPS} -bf 0 -forced-idr 1 -f h264 '${DOWNSTREAM_H264_FILE}'" + if [[ ${h264_encoder_route} == ffmpeg ]]; then + run_shell "client_downstream_h264_hardware_file" \ + "GPU creates a downstream-like H.264 elementary stream with ${h264_encoder}" \ + "ffmpeg -hide_banner -loglevel warning -y -nostdin -f lavfi -i testsrc2=size=${SMOKE_WIDTH}x${SMOKE_HEIGHT}:rate=${SMOKE_FPS} -frames:v ${SMOKE_FRAMES} -an -sn -dn -vf format=nv12 -c:v ${h264_encoder} -preset p1 -tune ll -rc cbr -b:v ${SMOKE_BITRATE_KBPS}k -maxrate ${SMOKE_BITRATE_KBPS}k -bufsize ${SMOKE_BITRATE_KBPS}k -g ${SMOKE_FPS} -bf 0 -forced-idr 1 -f h264 '${DOWNSTREAM_H264_FILE}'" + else + run_shell "client_downstream_h264_hardware_file" \ + "GPU creates a downstream-like H.264 elementary stream with ${h264_encoder}" \ + "gst-launch-1.0 -q videotestsrc num-buffers=${SMOKE_FRAMES} is-live=false pattern=smpte ! video/x-raw,width=${SMOKE_WIDTH},height=${SMOKE_HEIGHT},framerate=${SMOKE_FPS}/1 ! videoconvert ! video/x-raw,format=NV12 ! ${h264_encoder} ! h264parse config-interval=-1 ! video/x-h264,stream-format=byte-stream,alignment=au ! filesink location='${DOWNSTREAM_H264_FILE}'" + fi run_shell "client_downstream_h264_gstreamer_hw_decode" \ "GStreamer decodes H.264 with hardware decoder ${gst_h264_decoder}" \ "gst-launch-1.0 -q filesrc location='${DOWNSTREAM_H264_FILE}' ! h264parse config-interval=-1 ! video/x-h264,stream-format=byte-stream,alignment=au ! ${gst_h264_chain} ! videoconvert ! fakesink sync=false" - run_shell "client_downstream_h264_cuvid_frame" \ - "CUDA decodes one visual proof frame from the downstream H.264 stream" \ - "ffmpeg -hide_banner -loglevel warning -y -nostdin -c:v h264_cuvid -i '${DOWNSTREAM_H264_FILE}' -frames:v 1 '${DOWNSTREAM_H264_FRAME}'" + run_shell "client_downstream_h264_hardware_frame" \ + "GStreamer hardware decoder ${gst_h264_decoder} emits one visual H.264 proof frame" \ + "gst-launch-1.0 -q filesrc location='${DOWNSTREAM_H264_FILE}' ! h264parse ! ${gst_h264_chain} ! videoconvert ! pngenc snapshot=true ! filesink location='${DOWNSTREAM_H264_FRAME}'" run_shell "audio_aac_roundtrip_wav" \ "GStreamer encodes and decodes a 1 kHz tone to a WAV artifact for audio-path sanity" \ diff --git a/server/Cargo.toml b/server/Cargo.toml index 4a0d254..05bb98e 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -16,7 +16,7 @@ bench = false [package] name = "lesavka_server" -version = "0.27.7" +version = "0.27.8" edition = "2024" autobins = false diff --git a/tests/contract/client/input/camera/client_camera_include_contract.rs b/tests/contract/client/input/camera/client_camera_include_contract.rs index 61a14ca..5b86898 100644 --- a/tests/contract/client/input/camera/client_camera_include_contract.rs +++ b/tests/contract/client/input/camera/client_camera_include_contract.rs @@ -216,13 +216,18 @@ mod camera_include_contract { } #[test] - fn hevc_prefers_proven_ffmpeg_nvenc_unless_gstreamer_is_explicit() { + fn hevc_prefers_runtime_proven_ffmpeg_nvenc_unless_gstreamer_is_explicit() { let encoder_source = include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/client/src/input/camera/encoder_selection.rs" )); for expected in [ + "Path::new(\"/dev/nvidiactl\").exists()", + "color=c=black:s=128x128:r=1", + "\"hevc_nvenc\"", + "\"-f\",", + "\"null\"", "Some(\"ffmpeg_hevc_nvenc\" | \"hevc_nvenc\" | \"nvenc\")", "Some(\"gstreamer\" | \"gst\") => false", "_ => Self::ffmpeg_hevc_nvenc_available()", @@ -236,6 +241,16 @@ mod camera_include_contract { !encoder_source.contains("!Self::gstreamer_hevc_hardware_encoder_available()"), "a present GStreamer HEVC encoder must not suppress the proven FFmpeg/NVENC path" ); + assert!( + encoder_source.contains( + "supported_encoder_property(\"vah264enc\", &[\"key-int-max\", \"keyframe-period\"])", + ), + "modern vah264enc must receive its real short-GOP property" + ); + assert!( + encoder_source.contains("(\"vah265enc\", &[\"key-int-max\", \"keyframe-period\"][..])"), + "modern vah265enc must receive its real short-GOP property" + ); } #[test] diff --git a/tests/contract/scripts/install/client_install_script_contract.rs b/tests/contract/scripts/install/client_install_script_contract.rs index 1be6f97..d92ee5b 100644 --- a/tests/contract/scripts/install/client_install_script_contract.rs +++ b/tests/contract/scripts/install/client_install_script_contract.rs @@ -103,7 +103,9 @@ fn client_install_reports_nvidia_and_open_source_media_routes() { "first_available_gst_element", "nvidia-smi is available", "NVIDIA nvcodec is installed but CUDA initialization failed", - "FFmpeg hevc_nvenc is available", + "FFmpeg hevc_nvenc encoded a probe frame", + "ffmpeg_hevc_nvenc_usable", + "/dev/nvidiactl", "proprietary NVIDIA GStreamer route", "Vulkan/VAAPI/V4L2 GStreamer route", "nvh265enc", diff --git a/tests/manual/hardware_media/hardware_media_smoke_contract.rs b/tests/manual/hardware_media/hardware_media_smoke_contract.rs index f43a47a..48b8349 100644 --- a/tests/manual/hardware_media/hardware_media_smoke_contract.rs +++ b/tests/manual/hardware_media/hardware_media_smoke_contract.rs @@ -17,10 +17,10 @@ fn hardware_media_smoke_stays_manual_and_artifact_backed() { "Manual: local/remote hardware media smoke evidence; not part of CI.", "summary_json: ${SUMMARY_JSON}", "summary_txt: ${SUMMARY_TXT}", - "upstream-hevc-nvenc.hevc", - "upstream-hevc-cuda-frame.png", - "downstream-h264-nvenc.h264", - "downstream-h264-cuda-frame.png", + "upstream-hevc-hardware.hevc", + "upstream-hevc-hardware-frame.png", + "downstream-h264-hardware.h264", + "downstream-h264-hardware-frame.png", "audio-aac-roundtrip.wav", "audio-aac-roundtrip-rms.json", ] { @@ -36,8 +36,9 @@ fn hardware_media_smoke_uses_accelerated_video_paths() { for marker in [ "hevc_nvenc", "h264_nvenc", - "hevc_cuvid", - "h264_cuvid", + "ffmpeg_nvenc_usable", + "vah265enc", + "vah265dec", "vah264dec", "vulkanh264dec", "v4l2slh264dec",