fix(client): prove hardware encoder availability

This commit is contained in:
Brad Stein 2026-08-12 11:47:19 -03:00
parent 75df8c94e1
commit 164ef497f0
12 changed files with 204 additions and 74 deletions

6
Cargo.lock generated
View File

@ -1658,7 +1658,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]] [[package]]
name = "lesavka_client" name = "lesavka_client"
version = "0.27.7" version = "0.27.8"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-stream", "async-stream",
@ -1692,7 +1692,7 @@ dependencies = [
[[package]] [[package]]
name = "lesavka_common" name = "lesavka_common"
version = "0.27.7" version = "0.27.8"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
@ -1704,7 +1704,7 @@ dependencies = [
[[package]] [[package]]
name = "lesavka_server" name = "lesavka_server"
version = "0.27.7" version = "0.27.8"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",

View File

@ -4,7 +4,7 @@ path = "src/main.rs"
[package] [package]
name = "lesavka_client" name = "lesavka_client"
version = "0.27.7" version = "0.27.8"
edition = "2024" edition = "2024"
[dependencies] [dependencies]

View File

@ -55,7 +55,7 @@ impl CameraCapture {
if buildable_encoder("vah264enc") { if buildable_encoder("vah264enc") {
return Ok(( return Ok((
"vah264enc", "vah264enc",
supported_encoder_property("vah264enc", &["keyframe-period"]), supported_encoder_property("vah264enc", &["key-int-max", "keyframe-period"]),
)); ));
} }
if buildable_encoder("vaapih264enc") { if buildable_encoder("vaapih264enc") {
@ -110,7 +110,7 @@ impl CameraCapture {
"nvautogpuh265enc", "nvautogpuh265enc",
&["iframeinterval", "idrinterval", "gop-size"][..], &["iframeinterval", "idrinterval", "gop-size"][..],
), ),
("vah265enc", &["keyframe-period"][..]), ("vah265enc", &["key-int-max", "keyframe-period"][..]),
("vaapih265enc", &["keyframe-period"][..]), ("vaapih265enc", &["keyframe-period"][..]),
("v4l2h265enc", &["idrcount"][..]), ("v4l2h265enc", &["idrcount"][..]),
] { ] {
@ -157,8 +157,30 @@ impl CameraCapture {
#[cfg(not(coverage))] #[cfg(not(coverage))]
fn ffmpeg_hevc_nvenc_available() -> bool { fn ffmpeg_hevc_nvenc_available() -> bool {
if !Path::new("/dev/nvidiactl").exists() {
return false;
}
Command::new("ffmpeg") 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()) .stdout(Stdio::null())
.stderr(Stdio::null()) .stderr(Stdio::null())
.status() .status()

View File

@ -186,7 +186,7 @@ fn coverage_h264_encoder_choice_honors_stable_test_overrides() {
("nvh264enc", ("nvh264enc", None)), ("nvh264enc", ("nvh264enc", None)),
("nvautogpuh264enc", ("nvautogpuh264enc", None)), ("nvautogpuh264enc", ("nvautogpuh264enc", None)),
("vulkanh264enc", ("vulkanh264enc", Some("idr-period"))), ("vulkanh264enc", ("vulkanh264enc", Some("idr-period"))),
("vah264enc", ("vah264enc", Some("keyframe-period"))), ("vah264enc", ("vah264enc", Some("key-int-max"))),
("vaapih264enc", ("vaapih264enc", Some("keyframe-period"))), ("vaapih264enc", ("vaapih264enc", Some("keyframe-period"))),
("v4l2h264enc", ("v4l2h264enc", Some("idrcount"))), ("v4l2h264enc", ("v4l2h264enc", Some("idrcount"))),
("unknown", ("x264enc", Some("key-int-max"))), ("unknown", ("x264enc", Some("key-int-max"))),

View File

@ -1,6 +1,6 @@
[package] [package]
name = "lesavka_common" name = "lesavka_common"
version = "0.27.7" version = "0.27.8"
edition = "2024" edition = "2024"
build = "build.rs" build = "build.rs"

View File

@ -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 to retain a small Linux verification host in the lab when possible; otherwise
the calibrated server telemetry is the available integrity contract. 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 ## 7. The Current Downstream Video Failure
The current blank downstream feeds fail before transport or decoding. 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 6. Run `scripts/install/server.sh` as the trusted deployment path. Preserve the
already-attached USB gadget unless a controlled rebuild is explicitly already-attached USB gadget unless a controlled rebuild is explicitly
required. 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. direct MJPEG normalizer timeout `0`, and a coherent UVC contract.
8. Open both downstream RPCs and prove that each emits changing, decodable H.264 8. Open both downstream RPCs and prove that each emits changing, decodable H.264
frames. frames.
@ -374,7 +385,7 @@ hardware contract is repeatable. The remaining work falls into five groups.
### A. Install And Version Parity ### 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. - Confirm client/server version and revision in every hardware probe artifact.
- Eliminate the current state where a fixed client talks to an unfixed server. - 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 9. disconnect/reconnect and device changes recover without stale media; and
10. diagnostics identify the failed physical stage when any item breaks. 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 current release should be described as a validated code correction awaiting
hardware deployment and end-to-end acceptance, not as a completed product fix. hardware deployment and end-to-end acceptance, not as a completed product fix.

View File

@ -147,6 +147,14 @@ first_available_gst_element() {
return 1 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() { report_client_media_acceleration() {
log "1f. Inspecting client media acceleration routes" log "1f. Inspecting client media acceleration routes"
@ -166,8 +174,7 @@ report_client_media_acceleration() {
vah265enc \ vah265enc \
vaapih265enc \ vaapih265enc \
v4l2h265enc || true) v4l2h265enc || true)
if command -v ffmpeg >/dev/null 2>&1 \ if command -v ffmpeg >/dev/null 2>&1 && ffmpeg_hevc_nvenc_usable; then
&& ffmpeg -hide_banner -loglevel error -h encoder=hevc_nvenc >/dev/null 2>&1; then
ffmpeg_hevc_encoder="hevc_nvenc" ffmpeg_hevc_encoder="hevc_nvenc"
fi fi
h264_encoder=$(first_available_gst_element \ 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 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." echo "⚠️ NVIDIA nvcodec is installed but CUDA initialization failed; NVENC HEVC is unavailable to GStreamer."
if [[ -n $ffmpeg_hevc_encoder ]]; then 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 else
echo " Install a working NVENC/VAAPI/V4L2 HEVC route before enabling HEVC upstream." echo " Install a working NVENC/VAAPI/V4L2 HEVC route before enabling HEVC upstream."
fi fi

View File

@ -10,10 +10,10 @@ RESULTS_TSV="${ARTIFACT_DIR}/results.tsv"
SUMMARY_JSON="${ARTIFACT_DIR}/summary.json" SUMMARY_JSON="${ARTIFACT_DIR}/summary.json"
SUMMARY_TXT="${ARTIFACT_DIR}/summary.txt" SUMMARY_TXT="${ARTIFACT_DIR}/summary.txt"
UPSTREAM_HEVC_FILE="${ARTIFACT_DIR}/upstream-hevc-nvenc.hevc" UPSTREAM_HEVC_FILE="${ARTIFACT_DIR}/upstream-hevc-hardware.hevc"
UPSTREAM_HEVC_FRAME="${ARTIFACT_DIR}/upstream-hevc-cuda-frame.png" UPSTREAM_HEVC_FRAME="${ARTIFACT_DIR}/upstream-hevc-hardware-frame.png"
DOWNSTREAM_H264_FILE="${ARTIFACT_DIR}/downstream-h264-nvenc.h264" DOWNSTREAM_H264_FILE="${ARTIFACT_DIR}/downstream-h264-hardware.h264"
DOWNSTREAM_H264_FRAME="${ARTIFACT_DIR}/downstream-h264-cuda-frame.png" DOWNSTREAM_H264_FRAME="${ARTIFACT_DIR}/downstream-h264-hardware-frame.png"
AUDIO_WAV="${ARTIFACT_DIR}/audio-aac-roundtrip.wav" AUDIO_WAV="${ARTIFACT_DIR}/audio-aac-roundtrip.wav"
AUDIO_RMS_JSON="${ARTIFACT_DIR}/audio-aac-roundtrip-rms.json" AUDIO_RMS_JSON="${ARTIFACT_DIR}/audio-aac-roundtrip-rms.json"
SMOKE_WIDTH="${LESAVKA_HARDWARE_SMOKE_WIDTH:-1280}" 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 }' | awk -v codec="${codec}" '$2 == codec { found = 1 } END { exit found ? 0 : 1 }'
} }
ffmpeg_has_decoder() { ffmpeg_nvenc_usable() {
local codec="$1" local encoder="$1"
ffmpeg -hide_banner -decoders 2>/dev/null \ [[ -e /dev/nvidiactl ]] || return 1
| awk -v codec="${codec}" '$2 == codec { found = 1 } END { exit found ? 0 : 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() { run_logged() {
@ -108,9 +111,9 @@ summary_txt = pathlib.Path(sys.argv[3])
artifact_dir = pathlib.Path(sys.argv[4]) artifact_dir = pathlib.Path(sys.argv[4])
artifact_paths = { artifact_paths = {
"upstream_hevc_stream": sys.argv[5], "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_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_wav": sys.argv[9],
"audio_aac_roundtrip_rms": sys.argv[10], "audio_aac_roundtrip_rms": sys.argv[10],
} }
@ -190,6 +193,39 @@ select_gst_h264_decoder() {
return 1 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() { gst_h264_decode_chain() {
case "$1" in case "$1" in
vulkanh264dec) vulkanh264dec)
@ -204,6 +240,17 @@ gst_h264_decode_chain() {
esac esac
} }
gst_hevc_decode_chain() {
case "$1" in
nvh265dec|vah265dec|vaapih265dec|v4l2h265dec|v4l2slh265dec)
printf '%s\n' "$1"
;;
*)
return 1
;;
esac
}
select_gst_aac_encoder() { select_gst_aac_encoder() {
local encoder local encoder
for encoder in fdkaacenc voaacenc avenc_aac; do for encoder in fdkaacenc voaacenc avenc_aac; do
@ -226,36 +273,49 @@ main() {
exit 1 exit 1
fi fi
if ! ffmpeg_has_encoder hevc_nvenc; then local hevc_encoder=""
record_result "client_upstream_hevc_nvenc_available" "fail" "ffmpeg hevc_nvenc encoder is missing" "" local hevc_encoder_route=""
OVERALL=1 local h264_encoder=""
else local h264_encoder_route=""
record_result "client_upstream_hevc_nvenc_available" "pass" "ffmpeg hevc_nvenc encoder is available" "" local gst_hevc_decoder=""
fi local gst_hevc_chain=""
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 gst_h264_decoder="" local gst_h264_decoder=""
local gst_h264_chain="" 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 if gst_h264_decoder="$(select_gst_h264_decoder)"; then
gst_h264_chain="$(gst_h264_decode_chain "${gst_h264_decoder}")" gst_h264_chain="$(gst_h264_decode_chain "${gst_h264_decoder}")"
record_result "client_downstream_gstreamer_h264_hw_decoder_available" "pass" "selected ${gst_h264_decoder}" "" record_result "client_downstream_gstreamer_h264_hw_decoder_available" "pass" "selected ${gst_h264_decoder}" ""
@ -273,29 +333,41 @@ main() {
fi fi
if [[ ${OVERALL} -eq 0 ]]; then if [[ ${OVERALL} -eq 0 ]]; then
run_shell "client_upstream_hevc_nvenc_file" \ if [[ ${hevc_encoder_route} == ffmpeg ]]; then
"GPU encodes a synthetic upstream HEVC stream with hevc_nvenc" \ run_shell "client_upstream_hevc_hardware_file" \
"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}'" "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" \ run_shell "client_upstream_hevc_gstreamer_parse" \
"GStreamer accepts the HEVC elementary stream shape used by the upstream bundle path" \ "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" "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" \ run_shell "client_upstream_hevc_hardware_frame" \
"CUDA decodes one visual proof frame from the upstream HEVC stream" \ "GStreamer hardware decoder ${gst_hevc_decoder} emits one visual HEVC proof frame" \
"ffmpeg -hide_banner -loglevel warning -y -nostdin -c:v hevc_cuvid -i '${UPSTREAM_HEVC_FILE}' -frames:v 1 '${UPSTREAM_HEVC_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" \ if [[ ${h264_encoder_route} == ffmpeg ]]; then
"GPU creates a downstream-like H.264 elementary stream for decoder verification" \ run_shell "client_downstream_h264_hardware_file" \
"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}'" "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" \ run_shell "client_downstream_h264_gstreamer_hw_decode" \
"GStreamer decodes H.264 with hardware decoder ${gst_h264_decoder}" \ "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" "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" \ run_shell "client_downstream_h264_hardware_frame" \
"CUDA decodes one visual proof frame from the downstream H.264 stream" \ "GStreamer hardware decoder ${gst_h264_decoder} emits one visual H.264 proof frame" \
"ffmpeg -hide_banner -loglevel warning -y -nostdin -c:v h264_cuvid -i '${DOWNSTREAM_H264_FILE}' -frames:v 1 '${DOWNSTREAM_H264_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" \ run_shell "audio_aac_roundtrip_wav" \
"GStreamer encodes and decodes a 1 kHz tone to a WAV artifact for audio-path sanity" \ "GStreamer encodes and decodes a 1 kHz tone to a WAV artifact for audio-path sanity" \

View File

@ -16,7 +16,7 @@ bench = false
[package] [package]
name = "lesavka_server" name = "lesavka_server"
version = "0.27.7" version = "0.27.8"
edition = "2024" edition = "2024"
autobins = false autobins = false

View File

@ -216,13 +216,18 @@ mod camera_include_contract {
} }
#[test] #[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!( let encoder_source = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"), env!("CARGO_MANIFEST_DIR"),
"/client/src/input/camera/encoder_selection.rs" "/client/src/input/camera/encoder_selection.rs"
)); ));
for expected in [ 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(\"ffmpeg_hevc_nvenc\" | \"hevc_nvenc\" | \"nvenc\")",
"Some(\"gstreamer\" | \"gst\") => false", "Some(\"gstreamer\" | \"gst\") => false",
"_ => Self::ffmpeg_hevc_nvenc_available()", "_ => Self::ffmpeg_hevc_nvenc_available()",
@ -236,6 +241,16 @@ mod camera_include_contract {
!encoder_source.contains("!Self::gstreamer_hevc_hardware_encoder_available()"), !encoder_source.contains("!Self::gstreamer_hevc_hardware_encoder_available()"),
"a present GStreamer HEVC encoder must not suppress the proven FFmpeg/NVENC path" "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] #[test]

View File

@ -103,7 +103,9 @@ fn client_install_reports_nvidia_and_open_source_media_routes() {
"first_available_gst_element", "first_available_gst_element",
"nvidia-smi is available", "nvidia-smi is available",
"NVIDIA nvcodec is installed but CUDA initialization failed", "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", "proprietary NVIDIA GStreamer route",
"Vulkan/VAAPI/V4L2 GStreamer route", "Vulkan/VAAPI/V4L2 GStreamer route",
"nvh265enc", "nvh265enc",

View File

@ -17,10 +17,10 @@ fn hardware_media_smoke_stays_manual_and_artifact_backed() {
"Manual: local/remote hardware media smoke evidence; not part of CI.", "Manual: local/remote hardware media smoke evidence; not part of CI.",
"summary_json: ${SUMMARY_JSON}", "summary_json: ${SUMMARY_JSON}",
"summary_txt: ${SUMMARY_TXT}", "summary_txt: ${SUMMARY_TXT}",
"upstream-hevc-nvenc.hevc", "upstream-hevc-hardware.hevc",
"upstream-hevc-cuda-frame.png", "upstream-hevc-hardware-frame.png",
"downstream-h264-nvenc.h264", "downstream-h264-hardware.h264",
"downstream-h264-cuda-frame.png", "downstream-h264-hardware-frame.png",
"audio-aac-roundtrip.wav", "audio-aac-roundtrip.wav",
"audio-aac-roundtrip-rms.json", "audio-aac-roundtrip-rms.json",
] { ] {
@ -36,8 +36,9 @@ fn hardware_media_smoke_uses_accelerated_video_paths() {
for marker in [ for marker in [
"hevc_nvenc", "hevc_nvenc",
"h264_nvenc", "h264_nvenc",
"hevc_cuvid", "ffmpeg_nvenc_usable",
"h264_cuvid", "vah265enc",
"vah265dec",
"vah264dec", "vah264dec",
"vulkanh264dec", "vulkanh264dec",
"v4l2slh264dec", "v4l2slh264dec",