fix: tighten UVC freshness path
This commit is contained in:
parent
6236292f56
commit
365876a152
@ -99,6 +99,10 @@ path.
|
||||
defaults, preventing browsers from receiving frames outside negotiated caps.
|
||||
- [x] Make the UVC control helper answer probe/commit requests from the same
|
||||
live descriptor so Firefox/Chrome negotiation matches server frame output.
|
||||
- [x] Bound UVC helper buffering and stale MJPEG replay so server-to-host video
|
||||
freshness can be tightened without changing the bundled sync architecture.
|
||||
- [x] Make direct UVC/UAC output-delay application absolute by default so stale
|
||||
legacy calibration does not keep a hidden multi-second video delay alive.
|
||||
- [x] Continue reporting client timing and sink handoff diagnostics from bundled packets.
|
||||
- [ ] Add bundled-mode counters for first bundle, first audio push, first video feed,
|
||||
dropped stale bundles, and bundle queue age.
|
||||
|
||||
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_client"
|
||||
version = "0.19.4"
|
||||
version = "0.19.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@ -1686,7 +1686,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_common"
|
||||
version = "0.19.4"
|
||||
version = "0.19.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@ -1698,7 +1698,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_server"
|
||||
version = "0.19.4"
|
||||
version = "0.19.5"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
|
||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.19.4"
|
||||
version = "0.19.5"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.19.4"
|
||||
version = "0.19.5"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@ -176,6 +176,7 @@ from `LESAVKA_CLIENT_PKI_SSH_SOURCE` over SSH. Runtime clients require the insta
|
||||
| `LESAVKA_MIC_TEST_SOURCE_DESC` | client media capture/playback override |
|
||||
| `LESAVKA_MOUSE_DEVICE` | input routing/clipboard override |
|
||||
| `LESAVKA_OUTPUT_DELAY_APPLY` | manual direct UVC/UAC probe override; apply the measured server output-delay correction through the calibration API when the probe gates pass |
|
||||
| `LESAVKA_OUTPUT_DELAY_APPLY_MODE` | manual direct UVC/UAC probe override; `absolute` sets the active output-path baseline to the measured device delay, while `relative` preserves legacy nudge behavior |
|
||||
| `LESAVKA_OUTPUT_DELAY_CALIBRATION` | manual direct UVC/UAC probe override; emit `output-delay-calibration.json` from a lab-attached USB host capture of server-generated signatures, defaults to enabled |
|
||||
| `LESAVKA_OUTPUT_DELAY_GAIN` | manual direct UVC/UAC probe override; scales measured output-delay correction before applying, defaults to `1.0` |
|
||||
| `LESAVKA_OUTPUT_DELAY_MAX_ABS_SKEW_MS` | manual direct UVC/UAC probe safety limit; refuses to apply/save implausibly large measured device skew, defaults to `5000` |
|
||||
@ -278,6 +279,7 @@ from `LESAVKA_CLIENT_PKI_SSH_SOURCE` over SSH. Runtime clients require the insta
|
||||
| `LESAVKA_UVC_APP_BLOCK` | server hardware/device override |
|
||||
| `LESAVKA_UVC_BLOCKING` | server hardware/device override |
|
||||
| `LESAVKA_UVC_BULK` | server hardware/device override |
|
||||
| `LESAVKA_UVC_BUFFER_COUNT` | UVC helper freshness override; number of queued gadget output buffers, defaults to `2` for live-call freshness |
|
||||
| `LESAVKA_UVC_BY_PATH_ROOT` | server hardware/device override |
|
||||
| `LESAVKA_UVC_CODEC` | server hardware/device override |
|
||||
| `LESAVKA_UVC_CTRL_BIN` | server hardware/device override |
|
||||
@ -289,8 +291,10 @@ from `LESAVKA_CLIENT_PKI_SSH_SOURCE` over SSH. Runtime clients require the insta
|
||||
| `LESAVKA_UVC_EXTERNAL` | server hardware/device override |
|
||||
| `LESAVKA_UVC_FALLBACK` | server hardware/device override |
|
||||
| `LESAVKA_UVC_FPS` | server hardware/device override |
|
||||
| `LESAVKA_UVC_FRAME_MAX_AGE_MS` | UVC helper freshness override; stale spooled MJPEG frames older than this are not replayed, defaults to `1000`; `0` disables TTL |
|
||||
| `LESAVKA_UVC_FRAME_SIZE` | server hardware/device override |
|
||||
| `LESAVKA_UVC_HEIGHT` | server hardware/device override |
|
||||
| `LESAVKA_UVC_IDLE_PUMP_MS` | UVC helper freshness override; idle poll sleep while pumping host-returned buffers, defaults to `2` |
|
||||
| `LESAVKA_UVC_INTERVAL` | server hardware/device override |
|
||||
| `LESAVKA_UVC_LIMIT_PCT` | server hardware/device override |
|
||||
| `LESAVKA_UVC_MAXBURST` | server hardware/device override |
|
||||
|
||||
@ -55,6 +55,7 @@ REMOTE_EXPECT_CAM_OUTPUT=${REMOTE_EXPECT_CAM_OUTPUT:-uvc}
|
||||
REMOTE_EXPECT_UVC_CODEC=${REMOTE_EXPECT_UVC_CODEC:-mjpeg}
|
||||
LESAVKA_OUTPUT_DELAY_CALIBRATION=${LESAVKA_OUTPUT_DELAY_CALIBRATION:-1}
|
||||
LESAVKA_OUTPUT_DELAY_APPLY=${LESAVKA_OUTPUT_DELAY_APPLY:-0}
|
||||
LESAVKA_OUTPUT_DELAY_APPLY_MODE=${LESAVKA_OUTPUT_DELAY_APPLY_MODE:-absolute}
|
||||
LESAVKA_OUTPUT_DELAY_SAVE=${LESAVKA_OUTPUT_DELAY_SAVE:-0}
|
||||
LESAVKA_OUTPUT_DELAY_TARGET=${LESAVKA_OUTPUT_DELAY_TARGET:-video}
|
||||
LESAVKA_OUTPUT_DELAY_MIN_PAIRS=${LESAVKA_OUTPUT_DELAY_MIN_PAIRS:-8}
|
||||
@ -273,6 +274,7 @@ write_output_delay_calibration() {
|
||||
"${LESAVKA_OUTPUT_DELAY_GAIN}" \
|
||||
"${LESAVKA_OUTPUT_DELAY_MAX_STEP_US}" \
|
||||
"${LESAVKA_OUTPUT_DELAY_APPLY}" \
|
||||
"${LESAVKA_OUTPUT_DELAY_APPLY_MODE}" \
|
||||
"${LESAVKA_OUTPUT_DELAY_SAVE}"
|
||||
import json
|
||||
import math
|
||||
@ -292,6 +294,7 @@ import sys
|
||||
gain_raw,
|
||||
max_step_raw,
|
||||
apply_raw,
|
||||
apply_mode_raw,
|
||||
save_raw,
|
||||
) = sys.argv[1:]
|
||||
|
||||
@ -323,6 +326,7 @@ report = json.loads(pathlib.Path(report_path).read_text())
|
||||
verdict = report.get("verdict") or {}
|
||||
|
||||
target = target.strip().lower()
|
||||
apply_mode = apply_mode_raw.strip().lower()
|
||||
min_pairs = max(1, as_int(min_pairs_raw, 8))
|
||||
max_abs_skew_ms = max(1.0, as_float(max_abs_skew_raw, 5000.0))
|
||||
max_drift_ms = max(0.0, as_float(max_drift_raw, 80.0))
|
||||
@ -353,6 +357,11 @@ elif target == "audio":
|
||||
else:
|
||||
refusal_reasons.append(f"unsupported target {target!r}; use video or audio")
|
||||
|
||||
if apply_mode not in {"absolute", "relative"}:
|
||||
refusal_reasons.append(
|
||||
f"unsupported apply mode {apply_mode!r}; use absolute or relative"
|
||||
)
|
||||
|
||||
if paired < min_pairs:
|
||||
refusal_reasons.append(f"paired_event_count {paired} < {min_pairs}")
|
||||
if max_abs_observed_ms > max_abs_skew_ms:
|
||||
@ -387,6 +396,7 @@ artifact = {
|
||||
"ready": ready,
|
||||
"decision": decision,
|
||||
"apply_enabled": as_bool(apply_raw),
|
||||
"apply_mode": apply_mode,
|
||||
"save_enabled": as_bool(save_raw),
|
||||
"paired_event_count": paired,
|
||||
"min_pairs": min_pairs,
|
||||
@ -402,6 +412,8 @@ artifact = {
|
||||
"bounded_device_delta_us": bounded_delta_us,
|
||||
"audio_offset_adjust_us": audio_delta_us,
|
||||
"video_offset_adjust_us": video_delta_us,
|
||||
"audio_target_offset_us": audio_delta_us,
|
||||
"video_target_offset_us": video_delta_us,
|
||||
"refusal_reasons": refusal_reasons,
|
||||
"note": note,
|
||||
}
|
||||
@ -413,9 +425,12 @@ env_values = {
|
||||
"output_delay_target": target,
|
||||
"output_delay_audio_delta_us": audio_delta_us,
|
||||
"output_delay_video_delta_us": video_delta_us,
|
||||
"output_delay_audio_target_offset_us": audio_delta_us,
|
||||
"output_delay_video_target_offset_us": video_delta_us,
|
||||
"output_delay_measured_skew_ms": f"{median_skew_ms:.3f}",
|
||||
"output_delay_paired_event_count": paired,
|
||||
"output_delay_drift_ms": f"{drift_ms:.3f}",
|
||||
"output_delay_apply_mode": apply_mode,
|
||||
"output_delay_note": note,
|
||||
}
|
||||
with pathlib.Path(output_env_path).open("w") as handle:
|
||||
@ -668,6 +683,9 @@ maybe_apply_output_delay_calibration() {
|
||||
echo " ↪ output_delay_drift_ms=${output_delay_drift_ms:-0.0}"
|
||||
echo " ↪ output_delay_audio_delta_us=${output_delay_audio_delta_us:-0}"
|
||||
echo " ↪ output_delay_video_delta_us=${output_delay_video_delta_us:-0}"
|
||||
echo " ↪ output_delay_audio_target_offset_us=${output_delay_audio_target_offset_us:-0}"
|
||||
echo " ↪ output_delay_video_target_offset_us=${output_delay_video_target_offset_us:-0}"
|
||||
echo " ↪ output_delay_apply_mode=${output_delay_apply_mode:-${LESAVKA_OUTPUT_DELAY_APPLY_MODE}}"
|
||||
echo " ↪ output_delay_note=${output_delay_note:-}"
|
||||
|
||||
if [[ "${output_delay_ready:-false}" != "true" ]]; then
|
||||
@ -680,13 +698,49 @@ maybe_apply_output_delay_calibration() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
local apply_mode="${output_delay_apply_mode:-${LESAVKA_OUTPUT_DELAY_APPLY_MODE}}"
|
||||
local apply_audio_delta="${output_delay_audio_delta_us:-0}"
|
||||
local apply_video_delta="${output_delay_video_delta_us:-0}"
|
||||
|
||||
if [[ "${apply_mode}" == "absolute" ]]; then
|
||||
local calibration_output current_audio_offset_us current_video_offset_us
|
||||
if ! calibration_output="$(
|
||||
LESAVKA_TLS_DOMAIN="${LESAVKA_TLS_DOMAIN}" \
|
||||
"${REPO_ROOT}/target/debug/lesavka-relayctl" \
|
||||
--server "${RESOLVED_LESAVKA_SERVER_ADDR}" \
|
||||
calibration 2>&1
|
||||
)"; then
|
||||
echo " ↪ output delay calibration apply refused: current calibration query failed"
|
||||
echo "${calibration_output}" >&2
|
||||
return 0
|
||||
fi
|
||||
current_audio_offset_us="$(awk -F= '/^calibration_active_audio_offset_us=/{print $2; exit}' <<<"${calibration_output}")"
|
||||
current_video_offset_us="$(awk -F= '/^calibration_active_video_offset_us=/{print $2; exit}' <<<"${calibration_output}")"
|
||||
if [[ ! "${current_audio_offset_us}" =~ ^-?[0-9]+$ || ! "${current_video_offset_us}" =~ ^-?[0-9]+$ ]]; then
|
||||
echo " ↪ output delay calibration apply refused: could not parse active calibration offsets"
|
||||
echo "${calibration_output}" >&2
|
||||
return 0
|
||||
fi
|
||||
apply_audio_delta=$(( ${output_delay_audio_target_offset_us:-0} - current_audio_offset_us ))
|
||||
apply_video_delta=$(( ${output_delay_video_target_offset_us:-0} - current_video_offset_us ))
|
||||
echo " ↪ current_active_audio_offset_us=${current_audio_offset_us}"
|
||||
echo " ↪ current_active_video_offset_us=${current_video_offset_us}"
|
||||
echo " ↪ absolute_target_audio_offset_us=${output_delay_audio_target_offset_us:-0}"
|
||||
echo " ↪ absolute_target_video_offset_us=${output_delay_video_target_offset_us:-0}"
|
||||
elif [[ "${apply_mode}" != "relative" ]]; then
|
||||
echo " ↪ output delay calibration apply refused: unsupported apply mode ${apply_mode}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo " ↪ calibration_apply_audio_delta_us=${apply_audio_delta}"
|
||||
echo " ↪ calibration_apply_video_delta_us=${apply_video_delta}"
|
||||
echo "==> applying measured UVC/UAC output-delay calibration"
|
||||
LESAVKA_TLS_DOMAIN="${LESAVKA_TLS_DOMAIN}" \
|
||||
"${REPO_ROOT}/target/debug/lesavka-relayctl" \
|
||||
--server "${RESOLVED_LESAVKA_SERVER_ADDR}" \
|
||||
calibrate \
|
||||
"${output_delay_audio_delta_us:-0}" \
|
||||
"${output_delay_video_delta_us:-0}" \
|
||||
"${apply_audio_delta}" \
|
||||
"${apply_video_delta}" \
|
||||
"${output_delay_note:-direct UVC/UAC output-delay calibration}"
|
||||
|
||||
if [[ "${LESAVKA_OUTPUT_DELAY_SAVE}" != "0" ]]; then
|
||||
|
||||
@ -10,7 +10,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.19.4"
|
||||
version = "0.19.5"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ use std::fs::{File, OpenOptions};
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
use std::os::unix::io::{AsRawFd, RawFd};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
const STREAM_CTRL_SIZE_11: usize = 26;
|
||||
const STREAM_CTRL_SIZE_15: usize = 34;
|
||||
@ -47,6 +47,9 @@ const V4L2_FIELD_NONE: u32 = 1;
|
||||
const V4L2_PIX_FMT_MJPEG: u32 = u32::from_le_bytes(*b"MJPG");
|
||||
const MAX_MJPEG_FRAME_BYTES: usize = 1024 * 1024;
|
||||
const EMPTY_MJPEG_FRAME: &[u8] = &[0xff, 0xd8, 0xff, 0xd9];
|
||||
const DEFAULT_UVC_BUFFER_COUNT: u32 = 2;
|
||||
const DEFAULT_UVC_IDLE_PUMP_MS: u64 = 2;
|
||||
const DEFAULT_UVC_FRAME_MAX_AGE_MS: u64 = 1_000;
|
||||
|
||||
#[repr(C)]
|
||||
struct V4l2EventSubscription {
|
||||
@ -253,7 +256,7 @@ impl UvcVideoStream {
|
||||
fn start(&mut self, cfg: UvcConfig) -> Result<()> {
|
||||
self.stop();
|
||||
self.set_format(cfg)?;
|
||||
self.request_buffers(4)?;
|
||||
self.request_buffers(uvc_buffer_count())?;
|
||||
for index in 0..self.buffers.len() {
|
||||
self.queue_buffer(index as u32)?;
|
||||
}
|
||||
@ -421,6 +424,10 @@ impl UvcVideoStream {
|
||||
}
|
||||
|
||||
fn refresh_latest_frame(&mut self) {
|
||||
if frame_spool_is_stale(&self.frame_path, frame_spool_max_age()) {
|
||||
self.latest_frame = EMPTY_MJPEG_FRAME.to_vec();
|
||||
return;
|
||||
}
|
||||
if let Ok(frame) = std::fs::read(&self.frame_path)
|
||||
&& !frame.is_empty()
|
||||
&& frame.len() <= MAX_MJPEG_FRAME_BYTES
|
||||
@ -471,6 +478,43 @@ fn frame_spool_path() -> std::path::PathBuf {
|
||||
.unwrap_or_else(|_| std::path::PathBuf::from("/run/lesavka-uvc-frame.mjpg"))
|
||||
}
|
||||
|
||||
fn uvc_buffer_count() -> u32 {
|
||||
env_u32("LESAVKA_UVC_BUFFER_COUNT", DEFAULT_UVC_BUFFER_COUNT).clamp(1, 8)
|
||||
}
|
||||
|
||||
fn uvc_idle_pump_sleep() -> Duration {
|
||||
Duration::from_millis(env_u64(
|
||||
"LESAVKA_UVC_IDLE_PUMP_MS",
|
||||
DEFAULT_UVC_IDLE_PUMP_MS,
|
||||
))
|
||||
}
|
||||
|
||||
fn frame_spool_max_age() -> Option<Duration> {
|
||||
match env_u64(
|
||||
"LESAVKA_UVC_FRAME_MAX_AGE_MS",
|
||||
DEFAULT_UVC_FRAME_MAX_AGE_MS,
|
||||
) {
|
||||
0 => None,
|
||||
value => Some(Duration::from_millis(value)),
|
||||
}
|
||||
}
|
||||
|
||||
fn frame_spool_is_stale(path: &std::path::Path, max_age: Option<Duration>) -> bool {
|
||||
let Some(max_age) = max_age else {
|
||||
return false;
|
||||
};
|
||||
let Ok(metadata) = std::fs::metadata(path) else {
|
||||
return true;
|
||||
};
|
||||
let Ok(modified) = metadata.modified() else {
|
||||
return false;
|
||||
};
|
||||
SystemTime::now()
|
||||
.duration_since(modified)
|
||||
.map(|age| age > max_age)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let (dev, cfg) = parse_args()?;
|
||||
let _singleton = acquire_singleton_lock()?;
|
||||
@ -482,7 +526,16 @@ fn main() -> Result<()> {
|
||||
);
|
||||
|
||||
let debug = env::var("LESAVKA_UVC_DEBUG").is_ok();
|
||||
let idle_sleep = uvc_idle_pump_sleep();
|
||||
eprintln!("[lesavka-uvc] nonblock=1");
|
||||
eprintln!(
|
||||
"[lesavka-uvc] freshness buffers={} idle_pump_ms={} frame_max_age_ms={}",
|
||||
uvc_buffer_count(),
|
||||
idle_sleep.as_millis(),
|
||||
frame_spool_max_age()
|
||||
.map(|duration| duration.as_millis().to_string())
|
||||
.unwrap_or_else(|| "disabled".to_string())
|
||||
);
|
||||
let mut setup_seen: u64 = 0;
|
||||
let mut data_seen: u64 = 0;
|
||||
let mut dq_err_seen: u64 = 0;
|
||||
@ -548,7 +601,7 @@ fn main() -> Result<()> {
|
||||
dq_err_last = code;
|
||||
}
|
||||
}
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
thread::sleep(idle_sleep);
|
||||
continue;
|
||||
}
|
||||
Some(libc::ENODEV) | Some(libc::EBADF) | Some(libc::EIO) => {
|
||||
@ -1258,6 +1311,13 @@ fn env_u32(name: &str, default: u32) -> u32 {
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
fn env_u64(name: &str, default: u64) -> u64 {
|
||||
env::var(name)
|
||||
.ok()
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
fn env_u8(name: &str) -> Option<u8> {
|
||||
env::var(name).ok().and_then(|v| v.parse::<u8>().ok())
|
||||
}
|
||||
|
||||
@ -41,6 +41,7 @@ fn upstream_sync_script_tunnels_auto_server_addr_through_ssh() {
|
||||
"LOCAL_OUTPUT_DELAY_ENV=\"${LOCAL_REPORT_DIR}/output-delay-calibration.env\"",
|
||||
"LESAVKA_OUTPUT_DELAY_CALIBRATION=${LESAVKA_OUTPUT_DELAY_CALIBRATION:-1}",
|
||||
"LESAVKA_OUTPUT_DELAY_APPLY=${LESAVKA_OUTPUT_DELAY_APPLY:-0}",
|
||||
"LESAVKA_OUTPUT_DELAY_APPLY_MODE=${LESAVKA_OUTPUT_DELAY_APPLY_MODE:-absolute}",
|
||||
"LESAVKA_OUTPUT_DELAY_SAVE=${LESAVKA_OUTPUT_DELAY_SAVE:-0}",
|
||||
"LESAVKA_OUTPUT_DELAY_TARGET=${LESAVKA_OUTPUT_DELAY_TARGET:-video}",
|
||||
"LESAVKA_OUTPUT_DELAY_MIN_PAIRS=${LESAVKA_OUTPUT_DELAY_MIN_PAIRS:-8}",
|
||||
@ -65,6 +66,13 @@ fn upstream_sync_script_tunnels_auto_server_addr_through_ssh() {
|
||||
"probe_media_origin\": \"server-generated\"",
|
||||
"probe_media_path\": \"server generated signatures -> UVC/UAC sinks -> lab host capture\"",
|
||||
"audio_after_video_positive",
|
||||
"audio_target_offset_us",
|
||||
"video_target_offset_us",
|
||||
"output_delay_audio_target_offset_us",
|
||||
"output_delay_video_target_offset_us",
|
||||
"calibration_active_video_offset_us",
|
||||
"absolute_target_video_offset_us",
|
||||
"calibration_apply_video_delta_us",
|
||||
"PROBE_EVENT_WIDTH_CODES=${PROBE_EVENT_WIDTH_CODES:-1,2,1,3",
|
||||
"\"${LESAVKA_OUTPUT_DELAY_PROBE_AUDIO_DELAY_US}\"",
|
||||
"\"${LESAVKA_OUTPUT_DELAY_PROBE_VIDEO_DELAY_US}\"",
|
||||
|
||||
@ -351,6 +351,55 @@ mod uvc_binary_extra {
|
||||
assert_eq!(read_fifo_min(missing.to_str().expect("missing")), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn uvc_freshness_defaults_bound_live_video_backlog() {
|
||||
with_var("LESAVKA_UVC_BUFFER_COUNT", None::<&str>, || {
|
||||
with_var("LESAVKA_UVC_IDLE_PUMP_MS", None::<&str>, || {
|
||||
with_var("LESAVKA_UVC_FRAME_MAX_AGE_MS", None::<&str>, || {
|
||||
assert_eq!(uvc_buffer_count(), 2);
|
||||
assert_eq!(uvc_idle_pump_sleep(), std::time::Duration::from_millis(2));
|
||||
assert_eq!(
|
||||
frame_spool_max_age(),
|
||||
Some(std::time::Duration::from_millis(1_000))
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn uvc_freshness_env_clamps_buffers_and_allows_disabling_frame_ttl() {
|
||||
with_var("LESAVKA_UVC_BUFFER_COUNT", Some("99"), || {
|
||||
with_var("LESAVKA_UVC_IDLE_PUMP_MS", Some("11"), || {
|
||||
with_var("LESAVKA_UVC_FRAME_MAX_AGE_MS", Some("0"), || {
|
||||
assert_eq!(uvc_buffer_count(), 8);
|
||||
assert_eq!(uvc_idle_pump_sleep(), std::time::Duration::from_millis(11));
|
||||
assert_eq!(frame_spool_max_age(), None);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_spool_staleness_uses_mtime_and_respects_disabled_ttl() {
|
||||
let frame = NamedTempFile::new().expect("tmp frame");
|
||||
fs::write(frame.path(), [0xff, 0xd8, 0xff, 0xd9]).expect("write frame");
|
||||
|
||||
assert!(!frame_spool_is_stale(frame.path(), None));
|
||||
let missing = PathBuf::from("/tmp/lesavka-definitely-missing-frame.mjpg");
|
||||
assert!(frame_spool_is_stale(
|
||||
missing.as_path(),
|
||||
Some(std::time::Duration::from_millis(1))
|
||||
));
|
||||
std::thread::sleep(std::time::Duration::from_millis(2));
|
||||
assert!(frame_spool_is_stale(
|
||||
frame.path(),
|
||||
Some(std::time::Duration::from_millis(1))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compute_payload_cap_clamps_limit_pct_bounds() {
|
||||
with_var("LESAVKA_UVC_MAXPAYLOAD_LIMIT", None::<&str>, || {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user