121 lines
4.7 KiB
Rust
121 lines
4.7 KiB
Rust
use crate::video_support::env_u32;
|
|
|
|
const DEFAULT_HEVC_JPEG_QUALITY: u32 = 82;
|
|
const DEFAULT_HEVC_SIZE_DROP_PCT: u32 = 45;
|
|
const DEFAULT_HEVC_MIN_REFERENCE_BYTES: u32 = 64 * 1024;
|
|
|
|
/// Resolve the JPEG quality used after HEVC decode.
|
|
///
|
|
/// Inputs: optional `LESAVKA_UVC_HEVC_JPEG_QUALITY`, clamped to 1..=100.
|
|
/// Output: the `jpegenc` quality value. Why: HEVC ingress must become MJPEG
|
|
/// for the existing UVC gadget path, and a slightly smaller JPEG lowers USB
|
|
/// and browser pressure without changing the calibrated A/V timing model.
|
|
pub(super) fn hevc_jpeg_quality() -> u32 {
|
|
env_u32("LESAVKA_UVC_HEVC_JPEG_QUALITY", DEFAULT_HEVC_JPEG_QUALITY).clamp(1, 100)
|
|
}
|
|
|
|
/// Decide whether suspicious decoded-frame freezing is enabled.
|
|
///
|
|
/// Inputs: optional `LESAVKA_UVC_HEVC_FREEZE_ON_SIZE_DROP`. Output: true unless
|
|
/// explicitly disabled. Why: when one damaged decoded MJPEG frame appears, a
|
|
/// short freeze is less disruptive than showing a grey slab or torn half-frame.
|
|
pub(super) fn freeze_on_size_drop_enabled() -> bool {
|
|
std::env::var("LESAVKA_UVC_HEVC_FREEZE_ON_SIZE_DROP")
|
|
.ok()
|
|
.map(|value| {
|
|
let trimmed = value.trim();
|
|
!(trimmed.eq_ignore_ascii_case("0")
|
|
|| trimmed.eq_ignore_ascii_case("false")
|
|
|| trimmed.eq_ignore_ascii_case("no")
|
|
|| trimmed.eq_ignore_ascii_case("off"))
|
|
})
|
|
.unwrap_or(true)
|
|
}
|
|
|
|
/// Resolve the frame-size drop percentage that triggers a freeze.
|
|
///
|
|
/// Inputs: optional `LESAVKA_UVC_HEVC_SIZE_DROP_PCT`, clamped to 1..=95.
|
|
/// Output: next-frame size percentage of the last good frame. Why: the guard
|
|
/// should catch sudden damaged-frame collapses while still allowing normal
|
|
/// bitrate variation from scene motion and encoder decisions.
|
|
pub(super) fn size_drop_pct() -> u32 {
|
|
env_u32("LESAVKA_UVC_HEVC_SIZE_DROP_PCT", DEFAULT_HEVC_SIZE_DROP_PCT).clamp(1, 95)
|
|
}
|
|
|
|
/// Resolve the minimum reference frame size for collapse detection.
|
|
///
|
|
/// Inputs: optional `LESAVKA_UVC_HEVC_MIN_REFERENCE_BYTES`. Output: byte count.
|
|
/// Why: tiny synthetic or blank frames should not establish a baseline that
|
|
/// causes later healthy low-detail frames to be frozen.
|
|
pub(super) fn min_reference_bytes() -> u32 {
|
|
env_u32(
|
|
"LESAVKA_UVC_HEVC_MIN_REFERENCE_BYTES",
|
|
DEFAULT_HEVC_MIN_REFERENCE_BYTES,
|
|
)
|
|
.max(1)
|
|
}
|
|
|
|
/// Decide whether a decoded HEVC-to-MJPEG frame should be frozen out.
|
|
///
|
|
/// Inputs: byte length of the last successfully spooled decoded MJPEG and the
|
|
/// next decoded MJPEG. Output: true when the next frame looks like a damaged
|
|
/// collapse. Why: keeping the last good frame preserves freshness and sync
|
|
/// better than forwarding a syntactically valid but visually corrupted JPEG.
|
|
pub(super) fn should_freeze_decoded_mjpeg(previous_bytes: u64, next_bytes: usize) -> bool {
|
|
if !freeze_on_size_drop_enabled() || previous_bytes < u64::from(min_reference_bytes()) {
|
|
return false;
|
|
}
|
|
|
|
let next_bytes = next_bytes as u64;
|
|
let threshold_bytes = previous_bytes.saturating_mul(u64::from(size_drop_pct())) / 100;
|
|
next_bytes < threshold_bytes
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
#[test]
|
|
fn hevc_jpeg_quality_defaults_to_moderate_transport_pressure() {
|
|
temp_env::with_var_unset("LESAVKA_UVC_HEVC_JPEG_QUALITY", || {
|
|
assert_eq!(super::hevc_jpeg_quality(), 82);
|
|
});
|
|
|
|
temp_env::with_var("LESAVKA_UVC_HEVC_JPEG_QUALITY", Some("101"), || {
|
|
assert_eq!(super::hevc_jpeg_quality(), 100);
|
|
});
|
|
|
|
temp_env::with_var("LESAVKA_UVC_HEVC_JPEG_QUALITY", Some("0"), || {
|
|
assert_eq!(super::hevc_jpeg_quality(), 1);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn freeze_guard_catches_large_decoded_frame_collapses() {
|
|
temp_env::with_vars(
|
|
[
|
|
("LESAVKA_UVC_HEVC_FREEZE_ON_SIZE_DROP", Some("1")),
|
|
("LESAVKA_UVC_HEVC_SIZE_DROP_PCT", Some("45")),
|
|
("LESAVKA_UVC_HEVC_MIN_REFERENCE_BYTES", Some("65536")),
|
|
],
|
|
|| {
|
|
assert!(super::should_freeze_decoded_mjpeg(200_000, 80_000));
|
|
assert!(!super::should_freeze_decoded_mjpeg(200_000, 110_000));
|
|
assert!(!super::should_freeze_decoded_mjpeg(20_000, 1_000));
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn freeze_guard_can_be_disabled_for_diagnostics() {
|
|
temp_env::with_vars(
|
|
[
|
|
("LESAVKA_UVC_HEVC_FREEZE_ON_SIZE_DROP", Some("0")),
|
|
("LESAVKA_UVC_HEVC_SIZE_DROP_PCT", Some("95")),
|
|
("LESAVKA_UVC_HEVC_MIN_REFERENCE_BYTES", Some("1")),
|
|
],
|
|
|| {
|
|
assert!(!super::should_freeze_decoded_mjpeg(200_000, 1_000));
|
|
},
|
|
);
|
|
}
|
|
}
|