diff --git a/Cargo.lock b/Cargo.lock index 65e1bfa..6e9ace3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lesavka_client" -version = "0.22.19" +version = "0.22.20" dependencies = [ "anyhow", "async-stream", @@ -1686,7 +1686,7 @@ dependencies = [ [[package]] name = "lesavka_common" -version = "0.22.19" +version = "0.22.20" dependencies = [ "anyhow", "base64", @@ -1698,7 +1698,7 @@ dependencies = [ [[package]] name = "lesavka_server" -version = "0.22.19" +version = "0.22.20" dependencies = [ "anyhow", "base64", diff --git a/client/Cargo.toml b/client/Cargo.toml index 250ea59..3e9ac9c 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,7 +4,7 @@ path = "src/main.rs" [package] name = "lesavka_client" -version = "0.22.19" +version = "0.22.20" edition = "2024" [dependencies] diff --git a/common/Cargo.toml b/common/Cargo.toml index 9a53bd4..0166bc1 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lesavka_common" -version = "0.22.19" +version = "0.22.20" edition = "2024" build = "build.rs" diff --git a/scripts/install/server.sh b/scripts/install/server.sh index d9646df..4b29e67 100755 --- a/scripts/install/server.sh +++ b/scripts/install/server.sh @@ -39,6 +39,22 @@ normalize_uvc_codec() { esac } +normalize_cam_codec() { + local raw=${1:-mjpeg} + case "${raw,,}" in + mjpeg|mjpg|jpeg|"") + echo "mjpeg" + ;; + hevc|h265|h.265) + echo "hevc" + ;; + *) + echo "❌ unsupported upstream camera codec '${raw}'. Use mjpeg or hevc." >&2 + exit 1 + ;; + esac +} + uvc_env_value() { local key=$1 local default=$2 @@ -61,7 +77,7 @@ if [[ "${REQUESTED_UVC_CODEC,,}" != "${INSTALL_UVC_CODEC}" ]]; then echo "⚠️ UVC gadget output codec '${REQUESTED_UVC_CODEC}' is not supported by the MJPEG UVC helper; using '${INSTALL_UVC_CODEC}' for the host-facing gadget." echo " Use LESAVKA_INSTALL_CAM_CODEC=hevc to choose HEVC for the client-to-server upstream transport." fi -INSTALL_CAM_CODEC=${LESAVKA_INSTALL_CAM_CODEC:-${LESAVKA_CAM_CODEC:-hevc}} +INSTALL_CAM_CODEC=$(normalize_cam_codec "${LESAVKA_INSTALL_CAM_CODEC:-${LESAVKA_CAM_CODEC:-mjpeg}}") INSTALL_UPLINK_AUDIO_CODEC=${LESAVKA_INSTALL_UPLINK_AUDIO_CODEC:-${LESAVKA_UPLINK_AUDIO_CODEC:-pcm}} INSTALL_UVC_FRAME_META=${LESAVKA_INSTALL_UVC_FRAME_META:-${LESAVKA_UVC_FRAME_META:-0}} INSTALL_UVC_FRAME_META_LOG_PATH=${LESAVKA_INSTALL_UVC_FRAME_META_LOG_PATH:-${LESAVKA_UVC_FRAME_META_LOG_PATH:-/tmp/lesavka-uvc-frame-meta.jsonl}} @@ -214,7 +230,7 @@ ensure_hevc_decode_support() { else echo "❌ no hardware HEVC decoder exposed to GStreamer; Lesavka will not fall back to avdec_h265 in production." >&2 if [[ "$INSTALL_UVC_CODEC" == "hevc" || "$INSTALL_CAM_CODEC" == "hevc" ]]; then - echo " Install/repair v4l2slh265dec or set a non-HEVC UVC codec before running the server installer." >&2 + echo " Install/repair v4l2slh265dec or set LESAVKA_INSTALL_CAM_CODEC=mjpeg before running the server installer." >&2 exit 1 fi fi @@ -232,11 +248,19 @@ ensure_hevc_decode_support() { >"$hevc_smoke_log" 2>&1; then echo "✅ hardware HEVC decoder passed a real 1280x720 decode smoke: $hevc_decoder" else + if [[ "$INSTALL_CAM_CODEC" == "hevc" ]]; then + echo "❌ hardware HEVC decoder is exposed but failed a real 1280x720 decode smoke: $hevc_decoder" >&2 + echo " smoke log: $hevc_smoke_log" >&2 + sed -n '1,120p' "$hevc_smoke_log" >&2 || true + echo " Refusing HEVC upstream install because production video decode must be hardware-accelerated and proven." >&2 + echo " Use LESAVKA_INSTALL_CAM_CODEC=mjpeg while the HEVC decoder stack is repaired." >&2 + exit 1 + fi echo "⚠️ hardware HEVC decoder is exposed but the synthetic 1280x720 decode smoke failed: $hevc_decoder" >&2 echo " smoke log: $hevc_smoke_log" >&2 sed -n '1,120p' "$hevc_smoke_log" >&2 || true - echo " Continuing because gst-launch synthetic streams can be a false negative on the Pi stateless decoder." >&2 - echo " Runtime still requires a buildable hardware HEVC decoder and will not use software fallback in production." >&2 + echo " Continuing because this install is not selecting HEVC upstream." >&2 + echo " Explicit HEVC installs require a passing hardware decode smoke and will not use software fallback in production." >&2 echo " Use scripts/manual/run_hardware_media_smoke.sh for artifact-backed follow-up evidence." >&2 fi fi diff --git a/server/Cargo.toml b/server/Cargo.toml index 2d4b87e..af21493 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,7 +10,7 @@ bench = false [package] name = "lesavka_server" -version = "0.22.19" +version = "0.22.20" edition = "2024" autobins = false diff --git a/tests/installer/scripts/install/server_install_script_contract.rs b/tests/installer/scripts/install/server_install_script_contract.rs index fa5d8b2..3382a35 100644 --- a/tests/installer/scripts/install/server_install_script_contract.rs +++ b/tests/installer/scripts/install/server_install_script_contract.rs @@ -60,7 +60,10 @@ fn server_install_pins_hdmi_camera_and_display_defaults() { assert!(SERVER_INSTALL.contains("${LESAVKA_CAM_HEIGHT:-1080}")); assert!(SERVER_INSTALL.contains("${LESAVKA_CAM_FPS:-30}")); assert!(SERVER_INSTALL.contains("${LESAVKA_INSTALL_CAM_OUTPUT:-uvc}")); - assert!(SERVER_INSTALL.contains("${LESAVKA_INSTALL_CAM_CODEC:-${LESAVKA_CAM_CODEC:-hevc}}")); + assert!(SERVER_INSTALL.contains("normalize_cam_codec()")); + assert!(SERVER_INSTALL.contains( + "INSTALL_CAM_CODEC=$(normalize_cam_codec \"${LESAVKA_INSTALL_CAM_CODEC:-${LESAVKA_CAM_CODEC:-mjpeg}}\")" + )); assert!( SERVER_INSTALL .contains("${LESAVKA_INSTALL_UPLINK_AUDIO_CODEC:-${LESAVKA_UPLINK_AUDIO_CODEC:-pcm}}") @@ -194,7 +197,7 @@ fn server_install_pins_hdmi_camera_and_display_defaults() { ); assert!( SERVER_INSTALL.contains("ensure_hevc_decode_support"), - "install script should prepare HEVC decode dependencies for the default uplink codec" + "install script should prepare HEVC decode dependencies when HEVC is selected" ); assert!( SERVER_INSTALL.contains("rpi_hevc_dec"), @@ -215,10 +218,17 @@ fn server_install_pins_hdmi_camera_and_display_defaults() { ); assert!( SERVER_INSTALL.contains("synthetic 1280x720 decode smoke failed") - && SERVER_INSTALL.contains("false negative on the Pi stateless decoder") - && SERVER_INSTALL.contains("Runtime still requires a buildable hardware HEVC decoder") + && SERVER_INSTALL + .contains("Continuing because this install is not selecting HEVC upstream") + && SERVER_INSTALL + .contains("Explicit HEVC installs require a passing hardware decode smoke") && SERVER_INSTALL.contains("scripts/manual/run_hardware_media_smoke.sh"), - "install script should keep HEVC smoke failures diagnostic instead of blocking known-good Pi runtime paths" + "install script should keep non-HEVC installs safe while warning about a broken HEVC stack" + ); + assert!( + SERVER_INSTALL.contains("Refusing HEVC upstream install because production video decode must be hardware-accelerated and proven") + && SERVER_INSTALL.contains("Use LESAVKA_INSTALL_CAM_CODEC=mjpeg while the HEVC decoder stack is repaired"), + "explicit HEVC installs should fail loud instead of producing a black UVC webcam feed" ); assert!( !SERVER_INSTALL @@ -349,8 +359,11 @@ fn server_install_pins_hdmi_camera_and_display_defaults() { ); assert!( SERVER_INSTALL.contains("1920x1080) frame_root=\"$function_root/streaming/mjpeg/m/1080p\"") - && SERVER_INSTALL.contains("1280x720) frame_root=\"$function_root/streaming/mjpeg/m/720p\"") - && SERVER_INSTALL.contains("grep -qx \"${LESAVKA_UVC_INTERVAL:-333333}\" \"$frame_root/dwFrameInterval\""), + && SERVER_INSTALL + .contains("1280x720) frame_root=\"$function_root/streaming/mjpeg/m/720p\"") + && SERVER_INSTALL.contains( + "grep -qx \"${LESAVKA_UVC_INTERVAL:-333333}\" \"$frame_root/dwFrameInterval\"" + ), "live descriptor matching should recognize all supported MJPEG UVC profiles instead of collapsing to one 720p frame" ); assert!( diff --git a/tests/regression/install/install_preserves_codec_settings_contract.rs b/tests/regression/install/install_preserves_codec_settings_contract.rs index 1f4509d..cc06bd7 100644 --- a/tests/regression/install/install_preserves_codec_settings_contract.rs +++ b/tests/regression/install/install_preserves_codec_settings_contract.rs @@ -1,10 +1,11 @@ // Regression contract for preserving codec settings across upgrades. // -// Scope: keep HEVC ingress and MJPEG UVC output defaults explicit, while still +// Scope: keep safe MJPEG ingress and MJPEG UVC output defaults explicit, while still // allowing operator-provided install overrides. // Targets: server/client install scripts and client camera capture defaults. // Why: Lesavka now supports both MJPEG and HEVC upstream media, and installer -// reruns must not silently revert the working profile. +// reruns must not silently select a HEVC profile that produces black frames +// when hardware decode is not proven. const SERVER_INSTALL: &str = include_str!(concat!( env!("CARGO_MANIFEST_DIR"), @@ -16,13 +17,14 @@ const CLIENT_CAMERA: &str = include_str!(concat!( )); #[test] -fn server_install_defaults_to_hevc_ingress_and_mjpeg_uvc_output() { +fn server_install_defaults_to_mjpeg_ingress_and_mjpeg_uvc_output() { for marker in [ "PERSISTED_UVC_CODEC=$(persisted_uvc_value LESAVKA_UVC_CODEC || true)", "normalize_uvc_codec()", + "normalize_cam_codec()", "REQUESTED_UVC_CODEC=${LESAVKA_INSTALL_UVC_CODEC:-${PERSISTED_UVC_CODEC:-mjpeg}}", "INSTALL_UVC_CODEC=$(normalize_uvc_codec \"$REQUESTED_UVC_CODEC\")", - "INSTALL_CAM_CODEC=${LESAVKA_INSTALL_CAM_CODEC:-${LESAVKA_CAM_CODEC:-hevc}}", + "INSTALL_CAM_CODEC=$(normalize_cam_codec \"${LESAVKA_INSTALL_CAM_CODEC:-${LESAVKA_CAM_CODEC:-mjpeg}}\")", "printf 'LESAVKA_CAM_CODEC=%s\\n' \"${INSTALL_CAM_CODEC}\"", "printf 'LESAVKA_UVC_CODEC=%s\\n' \"${INSTALL_UVC_CODEC}\"", "\"LESAVKA_UVC_CODEC=${INSTALL_UVC_CODEC}\"", @@ -79,6 +81,7 @@ fn hevc_prerequisites_are_rechecked_idempotently() { "/etc/modules-load.d/lesavka-hevc.conf", "gst-inspect-1.0 v4l2slh265dec", "will not fall back to avdec_h265 in production", + "Refusing HEVC upstream install because production video decode must be hardware-accelerated and proven", ] { assert!( SERVER_INSTALL.contains(marker),