fix: normalize UVC MJPEG before gadget handoff
This commit is contained in:
parent
3c7ccfdbcc
commit
29a565f9e2
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -1658,7 +1658,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_client"
|
||||
version = "0.25.3"
|
||||
version = "0.25.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@ -1692,7 +1692,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_common"
|
||||
version = "0.25.3"
|
||||
version = "0.25.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@ -1704,7 +1704,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_server"
|
||||
version = "0.25.3"
|
||||
version = "0.25.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
|
||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.25.3"
|
||||
version = "0.25.4"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.25.3"
|
||||
version = "0.25.4"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@ -321,7 +321,7 @@ fi
|
||||
UVC_FRAME_SIZE=${LESAVKA_UVC_FRAME_SIZE:-$((UVC_WIDTH * UVC_HEIGHT * 2))}
|
||||
UVC_INTERVAL_30=${LESAVKA_UVC_INTERVAL_30:-333333}
|
||||
UVC_INTERVAL_20=${LESAVKA_UVC_INTERVAL_20:-500000}
|
||||
UVC_MJPEG_BUDGET_BYTES_PER_SEC=${LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC:-10000000}
|
||||
UVC_MJPEG_BUDGET_BYTES_PER_SEC=${LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC:-4500000}
|
||||
UVC_ISOCHRONOUS_LIMIT_PCT=${LESAVKA_UVC_ISOCHRONOUS_LIMIT_PCT:-85}
|
||||
|
||||
uvc_isochronous_budget_bytes_per_sec() {
|
||||
|
||||
@ -388,7 +388,7 @@ LESAVKA_UVC_MAXBURST=$(uvc_env_value LESAVKA_UVC_MAXBURST 0)
|
||||
LESAVKA_UVC_BULK=$(uvc_env_value LESAVKA_UVC_BULK 1)
|
||||
LESAVKA_UVC_FRAME_SIZE_GUARD=$(uvc_env_value LESAVKA_UVC_FRAME_SIZE_GUARD 1)
|
||||
LESAVKA_UVC_FRAME_MAX_BYTES=$(uvc_env_value LESAVKA_UVC_FRAME_MAX_BYTES 0)
|
||||
LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC=$(uvc_env_value LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC 10000000)
|
||||
LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC=$(uvc_env_value LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC 4500000)
|
||||
LESAVKA_UVC_ISOCHRONOUS_LIMIT_PCT=$(uvc_env_value LESAVKA_UVC_ISOCHRONOUS_LIMIT_PCT 85)
|
||||
LESAVKA_UVC_STATS_PATH=$(uvc_env_value LESAVKA_UVC_STATS_PATH /run/lesavka-uvc-video-stats.json)
|
||||
EOF
|
||||
@ -1645,11 +1645,11 @@ SERVER_ENV_TMP=$(mktemp)
|
||||
printf 'LESAVKA_UVC_HEVC_SPOOL_PULL_TIMEOUT_MS=%s\n' "${LESAVKA_UVC_HEVC_SPOOL_PULL_TIMEOUT_MS:-20}"
|
||||
printf 'LESAVKA_UVC_HEVC_FRESHNESS_QUEUE_BUFFERS=%s\n' "${LESAVKA_UVC_HEVC_FRESHNESS_QUEUE_BUFFERS:-2}"
|
||||
printf 'LESAVKA_UVC_HEVC_DECODE_MISS_LIMIT=%s\n' "${LESAVKA_UVC_HEVC_DECODE_MISS_LIMIT:-15}"
|
||||
printf 'LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE=%s\n' "${LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE:-0}"
|
||||
printf 'LESAVKA_UVC_DIRECT_MJPEG_JPEG_QUALITY=%s\n' "${LESAVKA_UVC_DIRECT_MJPEG_JPEG_QUALITY:-72}"
|
||||
printf 'LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE=%s\n' "${LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE:-1}"
|
||||
printf 'LESAVKA_UVC_DIRECT_MJPEG_JPEG_QUALITY=%s\n' "${LESAVKA_UVC_DIRECT_MJPEG_JPEG_QUALITY:-60}"
|
||||
printf 'LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_PULL_TIMEOUT_MS=%s\n' "${LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_PULL_TIMEOUT_MS:-25}"
|
||||
printf 'LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_MISS_LIMIT=%s\n' "${LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_MISS_LIMIT:-30}"
|
||||
printf 'LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_RSS_LIMIT_MB=%s\n' "${LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_RSS_LIMIT_MB:-768}"
|
||||
printf 'LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_RSS_LIMIT_MB=%s\n' "${LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_RSS_LIMIT_MB:-384}"
|
||||
printf 'LESAVKA_UVC_DIRECT_MJPEG_VISUAL_GUARD=%s\n' "${LESAVKA_UVC_DIRECT_MJPEG_VISUAL_GUARD:-1}"
|
||||
printf 'LESAVKA_UVC_DIRECT_MJPEG_SIZE_DROP_PCT=%s\n' "${LESAVKA_UVC_DIRECT_MJPEG_SIZE_DROP_PCT:-18}"
|
||||
printf 'LESAVKA_UVC_DIRECT_MJPEG_MIN_REFERENCE_BYTES=%s\n' "${LESAVKA_UVC_DIRECT_MJPEG_MIN_REFERENCE_BYTES:-49152}"
|
||||
|
||||
@ -16,7 +16,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.25.3"
|
||||
version = "0.25.4"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
@ -54,7 +54,7 @@ const DEFAULT_UVC_BUFFER_COUNT: u32 = 1;
|
||||
const DEFAULT_UVC_IDLE_PUMP_MS: u64 = 2;
|
||||
const DEFAULT_UVC_FRAME_MAX_AGE_MS: u64 = 1_000;
|
||||
const DEFAULT_UVC_QUEUE_PACING: bool = true;
|
||||
const DEFAULT_UVC_MJPEG_BUDGET_BYTES_PER_SEC: u32 = 10_000_000;
|
||||
const DEFAULT_UVC_MJPEG_BUDGET_BYTES_PER_SEC: u32 = 4_500_000;
|
||||
const DEFAULT_UVC_ISOCHRONOUS_LIMIT_PCT: u32 = 85;
|
||||
const HIGH_SPEED_ISOCHRONOUS_MICROFRAMES_PER_SEC: u32 = 8_000;
|
||||
const DEFAULT_UVC_STATS_INTERVAL_MS: u64 = 5_000;
|
||||
|
||||
@ -59,7 +59,7 @@ const DEFAULT_UVC_FRAME_MAX_AGE_MS: u64 = 1_000;
|
||||
#[cfg(coverage)]
|
||||
const DEFAULT_UVC_QUEUE_PACING: bool = true;
|
||||
#[cfg(coverage)]
|
||||
const DEFAULT_UVC_MJPEG_BUDGET_BYTES_PER_SEC: u32 = 10_000_000;
|
||||
const DEFAULT_UVC_MJPEG_BUDGET_BYTES_PER_SEC: u32 = 4_500_000;
|
||||
#[cfg(coverage)]
|
||||
const DEFAULT_UVC_ISOCHRONOUS_LIMIT_PCT: u32 = 85;
|
||||
#[cfg(coverage)]
|
||||
|
||||
@ -80,7 +80,7 @@ fn uvc_frame_max_bytes_defaults_to_freshness_budget_and_allows_override() {
|
||||
],
|
||||
|| {
|
||||
let cfg = sample_cfg();
|
||||
assert_eq!(uvc_frame_max_bytes(cfg), 400_000);
|
||||
assert_eq!(uvc_frame_max_bytes(cfg), 180_000);
|
||||
},
|
||||
);
|
||||
|
||||
@ -96,7 +96,7 @@ fn uvc_frame_max_bytes_defaults_to_freshness_budget_and_allows_override() {
|
||||
);
|
||||
|
||||
with_var("LESAVKA_UVC_FRAME_MAX_BYTES", Some("0"), || {
|
||||
assert_eq!(uvc_frame_max_bytes(sample_cfg()), 400_000);
|
||||
assert_eq!(uvc_frame_max_bytes(sample_cfg()), 180_000);
|
||||
});
|
||||
|
||||
with_vars(
|
||||
|
||||
@ -16,11 +16,11 @@ const DEFAULT_HEVC_DOMINANT_BYTE_PCT: u32 = 92;
|
||||
const DEFAULT_DIRECT_MJPEG_SIZE_DROP_PCT: u32 = 18;
|
||||
const DEFAULT_DIRECT_MJPEG_MIN_REFERENCE_BYTES: u32 = 48 * 1024;
|
||||
const DEFAULT_DIRECT_MJPEG_PROFILE_MISMATCH_REJECT: bool = false;
|
||||
const DEFAULT_DIRECT_MJPEG_NORMALIZE: bool = false;
|
||||
const DEFAULT_DIRECT_MJPEG_JPEG_QUALITY: u32 = 72;
|
||||
const DEFAULT_DIRECT_MJPEG_NORMALIZE: bool = true;
|
||||
const DEFAULT_DIRECT_MJPEG_JPEG_QUALITY: u32 = 60;
|
||||
const DEFAULT_DIRECT_MJPEG_NORMALIZE_PULL_TIMEOUT_MS: u32 = 25;
|
||||
const DEFAULT_DIRECT_MJPEG_NORMALIZE_MISS_LIMIT: u32 = 30;
|
||||
const DEFAULT_DIRECT_MJPEG_NORMALIZE_RSS_LIMIT_MB: u32 = 768;
|
||||
const DEFAULT_DIRECT_MJPEG_NORMALIZE_RSS_LIMIT_MB: u32 = 384;
|
||||
|
||||
/// Explains why a direct MJPEG frame was frozen before UVC handoff.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@ -191,10 +191,9 @@ pub(super) fn direct_mjpeg_reject_profile_mismatch_enabled() -> bool {
|
||||
/// Decide whether direct MJPEG should be normalized before UVC spool.
|
||||
///
|
||||
/// Inputs: optional `LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE`. Output: true unless
|
||||
/// explicitly enabled. Why: this path runs native GStreamer JPEG decode/encode
|
||||
/// inside `lesavka-server`; it is useful for lab comparisons, but field
|
||||
/// evidence showed long-running RSS growth from the native allocator/buffer
|
||||
/// pools, so guarded passthrough is the production-safe default.
|
||||
/// explicitly disabled. Why: direct MJPEG camera frames can still stress the
|
||||
/// high-speed isochronous UVC link; normalizing them to a simpler, smaller
|
||||
/// JPEG bitstream before the gadget is safer than trusting passthrough bytes.
|
||||
pub(super) fn direct_mjpeg_normalize_enabled() -> bool {
|
||||
std::env::var("LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE")
|
||||
.ok()
|
||||
|
||||
@ -14,7 +14,7 @@ fn hevc_jpeg_quality_defaults_to_moderate_transport_pressure() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_mjpeg_normalization_defaults_off_and_clamps_tuning() {
|
||||
fn direct_mjpeg_normalization_defaults_on_and_clamps_tuning() {
|
||||
temp_env::with_vars(
|
||||
[
|
||||
("LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE", None::<&str>),
|
||||
@ -33,13 +33,13 @@ fn direct_mjpeg_normalization_defaults_off_and_clamps_tuning() {
|
||||
),
|
||||
],
|
||||
|| {
|
||||
assert!(!super::direct_mjpeg_normalize_enabled());
|
||||
assert_eq!(super::direct_mjpeg_jpeg_quality(), 72);
|
||||
assert!(super::direct_mjpeg_normalize_enabled());
|
||||
assert_eq!(super::direct_mjpeg_jpeg_quality(), 60);
|
||||
assert_eq!(super::direct_mjpeg_normalize_pull_timeout_ms(), 25);
|
||||
assert_eq!(super::direct_mjpeg_normalize_miss_limit(), 30);
|
||||
assert_eq!(
|
||||
super::direct_mjpeg_normalize_rss_limit_kb(),
|
||||
Some(768 * 1024)
|
||||
Some(384 * 1024)
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@ -13,7 +13,7 @@ mod audit;
|
||||
static SPOOL_SEQUENCE: AtomicU64 = AtomicU64::new(1);
|
||||
static SPOOL_TEMP_SEQUENCE: AtomicU64 = AtomicU64::new(1);
|
||||
const MAX_MJPEG_FRAME_BYTES: usize = 8 * 1024 * 1024;
|
||||
const DEFAULT_UVC_MJPEG_BUDGET_BYTES_PER_SEC: u32 = 10_000_000;
|
||||
const DEFAULT_UVC_MJPEG_BUDGET_BYTES_PER_SEC: u32 = 4_500_000;
|
||||
const DEFAULT_UVC_ISOCHRONOUS_LIMIT_PCT: u32 = 85;
|
||||
const HIGH_SPEED_ISOCHRONOUS_MICROFRAMES_PER_SEC: u32 = 8_000;
|
||||
const CONFIGFS_UVC_BASE: &str = "/sys/kernel/config/usb_gadget/lesavka/functions/uvc.usb0";
|
||||
@ -377,8 +377,8 @@ pub(super) fn spool_mjpeg_frame_with_timing(
|
||||
}
|
||||
|
||||
if let (Some(dir), Some(sequence)) = (audit_dir, sequence)
|
||||
&& audit::claim_spool_audit_frame(sequence)
|
||||
&& let Err(err) = audit::write_mjpeg_spool_audit_frame(&dir, sequence, data, timing)
|
||||
&& let Some(slot) = audit::claim_spool_audit_frame(sequence)
|
||||
&& let Err(err) = audit::write_mjpeg_spool_audit_frame(&dir, sequence, slot, data, timing)
|
||||
{
|
||||
tracing::warn!(
|
||||
target: "lesavka_server::video",
|
||||
|
||||
@ -10,9 +10,15 @@ static AUDIT_TEMP_SEQUENCE: AtomicU64 = AtomicU64::new(1);
|
||||
static AUDIT_SAVED_FRAMES: AtomicU64 = AtomicU64::new(0);
|
||||
const DEFAULT_UVC_FRAME_AUDIT_EVERY: u32 = 1;
|
||||
const DEFAULT_UVC_FRAME_AUDIT_MAX_FRAMES: u32 = 1800;
|
||||
const DEFAULT_UVC_FRAME_AUDIT_ROLLING: bool = true;
|
||||
const FNV1A64_OFFSET: u64 = 0xcbf29ce484222325;
|
||||
const FNV1A64_PRIME: u64 = 0x100000001b3;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(super) fn reset_spool_audit_counter_for_test() {
|
||||
AUDIT_SAVED_FRAMES.store(0, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Parse an optional unsigned tuning value.
|
||||
///
|
||||
/// Inputs: environment variable name. Output: parsed `u32` when present.
|
||||
@ -90,6 +96,24 @@ pub(super) fn mjpeg_spool_audit_max_frames() -> u64 {
|
||||
)
|
||||
}
|
||||
|
||||
/// Resolve whether bounded audits keep the latest frames instead of stopping.
|
||||
///
|
||||
/// Inputs: `LESAVKA_UVC_FRAME_AUDIT_ROLLING`. Output: true unless explicitly
|
||||
/// disabled. Why: long live-debug sessions need the frames around the latest
|
||||
/// recording, not stale startup evidence from hours earlier.
|
||||
pub(super) fn mjpeg_spool_audit_rolling_enabled() -> bool {
|
||||
std::env::var("LESAVKA_UVC_FRAME_AUDIT_ROLLING")
|
||||
.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(DEFAULT_UVC_FRAME_AUDIT_ROLLING)
|
||||
}
|
||||
|
||||
/// Resolve the audit JSONL path.
|
||||
///
|
||||
/// Inputs: audit directory plus `LESAVKA_UVC_FRAME_AUDIT_LOG_PATH`. Output:
|
||||
@ -126,17 +150,24 @@ pub(super) fn should_sample_spool_audit_frame(sequence: u64, every: u64) -> bool
|
||||
offset.is_multiple_of(every.max(1))
|
||||
}
|
||||
|
||||
/// Decide whether the current spooled frame should be preserved.
|
||||
/// Decide where the current spooled frame should be preserved.
|
||||
///
|
||||
/// Inputs: global spool sequence. Output: true if sampling and max-frame budget
|
||||
/// allow a save. Why: the max cap must count saved audit frames, not every
|
||||
/// frame the long-running server emitted before the audit started.
|
||||
pub(super) fn claim_spool_audit_frame(sequence: u64) -> bool {
|
||||
/// Inputs: global spool sequence. Output: the bounded audit file slot, if
|
||||
/// sampling and retention allow a save. Why: rolling audits must stay disk
|
||||
/// bounded while still keeping the frames nearest the user's recording.
|
||||
pub(super) fn claim_spool_audit_frame(sequence: u64) -> Option<u64> {
|
||||
if !should_sample_spool_audit_frame(sequence, mjpeg_spool_audit_every()) {
|
||||
return false;
|
||||
return None;
|
||||
}
|
||||
let max_frames = mjpeg_spool_audit_max_frames();
|
||||
max_frames == 0 || AUDIT_SAVED_FRAMES.fetch_add(1, Ordering::Relaxed) < max_frames
|
||||
let saved = AUDIT_SAVED_FRAMES.fetch_add(1, Ordering::Relaxed);
|
||||
if max_frames == 0 {
|
||||
return Some(sequence);
|
||||
}
|
||||
if mjpeg_spool_audit_rolling_enabled() {
|
||||
return Some(saved % max_frames + 1);
|
||||
}
|
||||
(saved < max_frames).then_some(sequence)
|
||||
}
|
||||
|
||||
/// Format one exact-frame audit index record.
|
||||
@ -146,6 +177,7 @@ pub(super) fn claim_spool_audit_frame(sequence: u64) -> bool {
|
||||
/// RCT frame with the server-boundary payload that preceded it.
|
||||
fn format_audit_record(
|
||||
sequence: u64,
|
||||
slot: u64,
|
||||
data: &[u8],
|
||||
timing: Option<MjpegSpoolTiming>,
|
||||
frame_file: &str,
|
||||
@ -154,8 +186,9 @@ fn format_audit_record(
|
||||
let source_pts_us = timing.and_then(|value| value.source_pts_us);
|
||||
let decoded_pts_us = timing.and_then(|value| value.decoded_pts_us);
|
||||
format!(
|
||||
"{{\"schema\":\"lesavka.uvc-mjpeg-spool-audit.v1\",\"sequence\":{},\"profile\":\"{}\",\"bytes\":{},\"source_pts_us\":{},\"decoded_pts_us\":{},\"spool_unix_ns\":{},\"fnv1a64\":\"{}\",\"file\":\"{}\"}}\n",
|
||||
"{{\"schema\":\"lesavka.uvc-mjpeg-spool-audit.v1\",\"sequence\":{},\"slot\":{},\"profile\":\"{}\",\"bytes\":{},\"source_pts_us\":{},\"decoded_pts_us\":{},\"spool_unix_ns\":{},\"fnv1a64\":\"{}\",\"file\":\"{}\"}}\n",
|
||||
sequence,
|
||||
slot,
|
||||
json_escape(profile),
|
||||
data.len(),
|
||||
json_number_or_null(source_pts_us),
|
||||
@ -174,11 +207,12 @@ fn format_audit_record(
|
||||
pub(super) fn write_mjpeg_spool_audit_frame(
|
||||
dir: &Path,
|
||||
sequence: u64,
|
||||
slot: u64,
|
||||
data: &[u8],
|
||||
timing: Option<MjpegSpoolTiming>,
|
||||
) -> anyhow::Result<()> {
|
||||
fs::create_dir_all(dir)?;
|
||||
let frame_file = format!("frame-{sequence:012}.mjpg");
|
||||
let frame_file = format!("frame-{slot:012}.mjpg");
|
||||
let tmp = dir.join(format!(
|
||||
".{frame_file}.{}.{}.tmp",
|
||||
std::process::id(),
|
||||
@ -187,6 +221,6 @@ pub(super) fn write_mjpeg_spool_audit_frame(
|
||||
fs::write(&tmp, data)?;
|
||||
fs::rename(&tmp, dir.join(&frame_file))?;
|
||||
|
||||
let record = format_audit_record(sequence, data, timing, &frame_file);
|
||||
let record = format_audit_record(sequence, slot, data, timing, &frame_file);
|
||||
append_record(&mjpeg_spool_audit_log_path(dir), &record)
|
||||
}
|
||||
|
||||
@ -44,7 +44,7 @@ fn mjpeg_spool_frame_budget_uses_live_budget_when_zero_or_unset() {
|
||||
("LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC", None::<&str>),
|
||||
],
|
||||
|| {
|
||||
assert_eq!(super::mjpeg_spool_frame_max_bytes(30), 333_333);
|
||||
assert_eq!(super::mjpeg_spool_frame_max_bytes(30), 150_000);
|
||||
},
|
||||
);
|
||||
|
||||
@ -151,11 +151,13 @@ fn mjpeg_spool_audit_knobs_are_opt_in_and_bounded() {
|
||||
("LESAVKA_UVC_FRAME_AUDIT_DIR", None::<&str>),
|
||||
("LESAVKA_UVC_FRAME_AUDIT_EVERY", None::<&str>),
|
||||
("LESAVKA_UVC_FRAME_AUDIT_MAX_FRAMES", None::<&str>),
|
||||
("LESAVKA_UVC_FRAME_AUDIT_ROLLING", None::<&str>),
|
||||
],
|
||||
|| {
|
||||
assert_eq!(super::audit::mjpeg_spool_audit_dir(), None);
|
||||
assert_eq!(super::audit::mjpeg_spool_audit_every(), 1);
|
||||
assert_eq!(super::audit::mjpeg_spool_audit_max_frames(), 1800);
|
||||
assert!(super::audit::mjpeg_spool_audit_rolling_enabled());
|
||||
},
|
||||
);
|
||||
|
||||
@ -164,6 +166,7 @@ fn mjpeg_spool_audit_knobs_are_opt_in_and_bounded() {
|
||||
("LESAVKA_UVC_FRAME_AUDIT_DIR", Some("/tmp/uvc-audit")),
|
||||
("LESAVKA_UVC_FRAME_AUDIT_EVERY", Some("0")),
|
||||
("LESAVKA_UVC_FRAME_AUDIT_MAX_FRAMES", Some("0")),
|
||||
("LESAVKA_UVC_FRAME_AUDIT_ROLLING", Some("off")),
|
||||
],
|
||||
|| {
|
||||
assert_eq!(
|
||||
@ -172,6 +175,7 @@ fn mjpeg_spool_audit_knobs_are_opt_in_and_bounded() {
|
||||
);
|
||||
assert_eq!(super::audit::mjpeg_spool_audit_every(), 1);
|
||||
assert_eq!(super::audit::mjpeg_spool_audit_max_frames(), 0);
|
||||
assert!(!super::audit::mjpeg_spool_audit_rolling_enabled());
|
||||
},
|
||||
);
|
||||
|
||||
@ -324,7 +328,9 @@ fn spool_mjpeg_frame_writes_enabled_sidecar_with_timing() {
|
||||
/// JSONL timing/hash record. Why: this is the evidence needed to decide
|
||||
/// whether corruption exists before or after the server-to-UVC boundary.
|
||||
#[test]
|
||||
#[serial_test::serial(audit)]
|
||||
fn spool_mjpeg_frame_writes_enabled_boundary_audit() {
|
||||
super::audit::reset_spool_audit_counter_for_test();
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let frame = dir.path().join("frame.mjpg");
|
||||
let audit = dir.path().join("audit");
|
||||
@ -338,6 +344,7 @@ fn spool_mjpeg_frame_writes_enabled_boundary_audit() {
|
||||
),
|
||||
("LESAVKA_UVC_FRAME_AUDIT_EVERY", Some("1")),
|
||||
("LESAVKA_UVC_FRAME_AUDIT_MAX_FRAMES", Some("1")),
|
||||
("LESAVKA_UVC_FRAME_AUDIT_ROLLING", Some("0")),
|
||||
],
|
||||
|| {
|
||||
super::spool_mjpeg_frame_with_timing(
|
||||
@ -372,3 +379,61 @@ fn spool_mjpeg_frame_writes_enabled_boundary_audit() {
|
||||
assert!(index.contains("\"source_pts_us\":222"));
|
||||
assert!(index.contains("\"file\":\"frame-"));
|
||||
}
|
||||
|
||||
/// Verifies rolling audit keeps the latest bounded frame evidence.
|
||||
///
|
||||
/// Input: a one-frame rolling audit and two spooled MJPEG payloads. Output:
|
||||
/// one overwritten frame file plus two index rows with distinct source
|
||||
/// sequences. Why: long live repro sessions must not leave only stale startup
|
||||
/// frames when the user records a later artifact.
|
||||
#[test]
|
||||
#[serial_test::serial(audit)]
|
||||
fn spool_mjpeg_frame_rolls_enabled_boundary_audit() {
|
||||
super::audit::reset_spool_audit_counter_for_test();
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let frame = dir.path().join("frame.mjpg");
|
||||
let audit = dir.path().join("audit");
|
||||
|
||||
temp_env::with_vars(
|
||||
[
|
||||
("LESAVKA_UVC_FRAME_META", None::<&str>),
|
||||
(
|
||||
"LESAVKA_UVC_FRAME_AUDIT_DIR",
|
||||
Some(audit.to_str().expect("utf8 path")),
|
||||
),
|
||||
("LESAVKA_UVC_FRAME_AUDIT_EVERY", Some("1")),
|
||||
("LESAVKA_UVC_FRAME_AUDIT_MAX_FRAMES", Some("1")),
|
||||
("LESAVKA_UVC_FRAME_AUDIT_ROLLING", Some("1")),
|
||||
],
|
||||
|| {
|
||||
super::spool_mjpeg_frame_with_timing(
|
||||
&frame,
|
||||
b"first-jpeg",
|
||||
Some(super::MjpegSpoolTiming::mjpeg_passthrough(111)),
|
||||
)
|
||||
.expect("spool first audited frame");
|
||||
super::spool_mjpeg_frame_with_timing(
|
||||
&frame,
|
||||
b"latest-jpeg",
|
||||
Some(super::MjpegSpoolTiming::mjpeg_passthrough(222)),
|
||||
)
|
||||
.expect("spool rolling audited frame");
|
||||
},
|
||||
);
|
||||
|
||||
let audited_frames = std::fs::read_dir(&audit)
|
||||
.expect("audit dir")
|
||||
.filter_map(Result::ok)
|
||||
.filter(|entry| entry.file_name().to_string_lossy().ends_with(".mjpg"))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(audited_frames.len(), 1);
|
||||
assert_eq!(
|
||||
std::fs::read(audited_frames[0].path()).expect("audit frame"),
|
||||
b"latest-jpeg"
|
||||
);
|
||||
let index = std::fs::read_to_string(audit.join("spool-audit.jsonl")).expect("audit index");
|
||||
assert_eq!(index.lines().count(), 2);
|
||||
assert!(index.contains("\"slot\":1"));
|
||||
assert!(index.contains("\"source_pts_us\":111"));
|
||||
assert!(index.contains("\"source_pts_us\":222"));
|
||||
}
|
||||
|
||||
@ -101,7 +101,7 @@ fn core_script_keeps_uvc_output_on_supported_mjpeg_descriptor() {
|
||||
"apply_uvc_payload_limits",
|
||||
"UVC_MAXPACKET=${LESAVKA_UVC_MAXPACKET:-1024}",
|
||||
"uvc_mjpeg_frame_size_for_fps()",
|
||||
"UVC_MJPEG_BUDGET_BYTES_PER_SEC=${LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC:-10000000}",
|
||||
"UVC_MJPEG_BUDGET_BYTES_PER_SEC=${LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC:-4500000}",
|
||||
"UVC_ISOCHRONOUS_LIMIT_PCT=${LESAVKA_UVC_ISOCHRONOUS_LIMIT_PCT:-85}",
|
||||
"uvc_isochronous_budget_bytes_per_sec()",
|
||||
"isoch_budget=\"$(uvc_isochronous_budget_bytes_per_sec)\"",
|
||||
|
||||
@ -192,11 +192,11 @@ fn server_install_pins_hdmi_camera_and_display_defaults() {
|
||||
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_HEVC_SPOOL_PULL_TIMEOUT_MS:-20}"));
|
||||
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_HEVC_FRESHNESS_QUEUE_BUFFERS:-2}"));
|
||||
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_HEVC_DECODE_MISS_LIMIT:-15}"));
|
||||
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE:-0}"));
|
||||
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_DIRECT_MJPEG_JPEG_QUALITY:-72}"));
|
||||
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE:-1}"));
|
||||
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_DIRECT_MJPEG_JPEG_QUALITY:-60}"));
|
||||
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_PULL_TIMEOUT_MS:-25}"));
|
||||
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_MISS_LIMIT:-30}"));
|
||||
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_RSS_LIMIT_MB:-768}"));
|
||||
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_RSS_LIMIT_MB:-384}"));
|
||||
assert!(
|
||||
SERVER_INSTALL.contains("lesavka_server::video=info"),
|
||||
"server installs should not leave the hot webcam frame path at debug logging by default"
|
||||
|
||||
@ -76,7 +76,7 @@ fn assert_ordered(haystack: &str, earlier: &str, later: &str) {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_mjpeg_normalizer_is_opt_in_after_native_rss_leak() {
|
||||
fn direct_mjpeg_normalizer_defaults_on_with_rss_fuse_after_native_rss_leak() {
|
||||
temp_env::with_vars(
|
||||
[
|
||||
("LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE", None::<&str>),
|
||||
@ -87,10 +87,10 @@ fn direct_mjpeg_normalizer_is_opt_in_after_native_rss_leak() {
|
||||
],
|
||||
|| {
|
||||
assert!(
|
||||
!guard::normalizer_enabled(),
|
||||
"direct MJPEG normalization must stay opt-in after the native RSS leak"
|
||||
guard::normalizer_enabled(),
|
||||
"direct MJPEG normalization should be active but RSS-fused while chasing UVC artifacts"
|
||||
);
|
||||
assert_eq!(guard::normalizer_rss_limit_kb(), Some(768 * 1024));
|
||||
assert_eq!(guard::normalizer_rss_limit_kb(), Some(384 * 1024));
|
||||
},
|
||||
);
|
||||
|
||||
@ -107,10 +107,10 @@ fn direct_mjpeg_normalizer_is_opt_in_after_native_rss_leak() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn installer_keeps_the_leaking_native_normalizer_disabled_by_default() {
|
||||
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE:-0}"));
|
||||
assert!(!SERVER_INSTALL.contains("${LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE:-1}"));
|
||||
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_RSS_LIMIT_MB:-768}"));
|
||||
fn installer_keeps_the_native_normalizer_memory_bounded_by_default() {
|
||||
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE:-1}"));
|
||||
assert!(!SERVER_INSTALL.contains("${LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE:-0}"));
|
||||
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_RSS_LIMIT_MB:-384}"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user