fix: normalize UVC MJPEG before gadget handoff

This commit is contained in:
Brad Stein 2026-05-19 11:41:30 -03:00
parent 3c7ccfdbcc
commit 29a565f9e2
17 changed files with 150 additions and 52 deletions

6
Cargo.lock generated
View File

@ -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",

View File

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

View File

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

View File

@ -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() {

View File

@ -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}"

View File

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

View File

@ -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;

View File

@ -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)]

View File

@ -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(

View File

@ -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()

View File

@ -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)
);
},
);

View File

@ -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",

View File

@ -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)
}

View File

@ -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"));
}

View File

@ -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)\"",

View File

@ -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"

View File

@ -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]