media: cap isochronous uvc mjpeg frames
This commit is contained in:
parent
b2e7a4cb38
commit
58e4b4f5b7
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_client"
|
||||
version = "0.22.36"
|
||||
version = "0.22.37"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@ -1686,7 +1686,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_common"
|
||||
version = "0.22.36"
|
||||
version = "0.22.37"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@ -1698,7 +1698,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_server"
|
||||
version = "0.22.36"
|
||||
version = "0.22.37"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
|
||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.22.36"
|
||||
version = "0.22.37"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.22.36"
|
||||
version = "0.22.37"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@ -338,12 +338,13 @@ from `LESAVKA_CLIENT_PKI_SSH_SOURCE` over SSH. Runtime clients require the insta
|
||||
| `LESAVKA_UVC_HEVC_FRESHNESS_QUEUE_BUFFERS` | server HEVC decode-to-MJPEG branch queue depth; defaults to `2` and is capped at `4` so decode/JPEG scheduling jitter does not starve the UVC helper while stale frames still get dropped |
|
||||
| `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_ISOCHRONOUS_LIMIT_PCT` | UVC MJPEG isochronous safety cap; defaults to `85`, limiting advertised/runtime frame bytes to a safe fraction of high-speed isochronous payload capacity when bulk UVC is unavailable |
|
||||
| `LESAVKA_UVC_LIMIT_PCT` | server hardware/device override |
|
||||
| `LESAVKA_UVC_MAXBURST` | server hardware/device override |
|
||||
| `LESAVKA_UVC_MAXPACKET` | server hardware/device override |
|
||||
| `LESAVKA_UVC_MAXPAYLOAD_LIMIT` | server hardware/device override |
|
||||
| `LESAVKA_UVC_MJPEG` | server hardware/device override |
|
||||
| `LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC` | UVC helper MJPEG budget guard; derives a per-frame byte cap from target FPS when `LESAVKA_UVC_FRAME_MAX_BYTES` is unset |
|
||||
| `LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC` | UVC helper MJPEG budget guard; derives a per-frame byte cap from target FPS when `LESAVKA_UVC_FRAME_MAX_BYTES` is unset; non-bulk UVC is additionally capped by `LESAVKA_UVC_ISOCHRONOUS_LIMIT_PCT` |
|
||||
| `LESAVKA_UVC_SKIP_UDEV` | server hardware/device override |
|
||||
| `LESAVKA_UVC_STATS_INTERVAL_MS` | UVC helper telemetry interval for queued/reloaded/rejected MJPEG frame counters; defaults to `5000`, `0` disables |
|
||||
| `LESAVKA_UVC_STATS_PATH` | UVC helper JSON stats snapshot path for queued/reloaded/rejected MJPEG frame counters; defaults to `/run/lesavka-uvc-video-stats.json`, set `0` or empty to disable file snapshots |
|
||||
|
||||
@ -322,13 +322,33 @@ 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_ISOCHRONOUS_LIMIT_PCT=${LESAVKA_UVC_ISOCHRONOUS_LIMIT_PCT:-85}
|
||||
|
||||
uvc_isochronous_budget_bytes_per_sec() {
|
||||
local maxpacket=${UVC_MAXPACKET:-1024}
|
||||
local pct=${UVC_ISOCHRONOUS_LIMIT_PCT:-85}
|
||||
if ((pct < 1)); then
|
||||
pct=1
|
||||
elif ((pct > 100)); then
|
||||
pct=100
|
||||
fi
|
||||
echo $((maxpacket * 8000 * pct / 100))
|
||||
}
|
||||
|
||||
uvc_mjpeg_frame_size_for_fps() {
|
||||
local fps=$1
|
||||
if ((fps < 1)); then
|
||||
fps=1
|
||||
fi
|
||||
local per_frame=$((UVC_MJPEG_BUDGET_BYTES_PER_SEC / fps))
|
||||
local budget=$UVC_MJPEG_BUDGET_BYTES_PER_SEC
|
||||
if [[ -z $UVC_BULK ]]; then
|
||||
local isoch_budget
|
||||
isoch_budget="$(uvc_isochronous_budget_bytes_per_sec)"
|
||||
if ((isoch_budget > 0 && budget > isoch_budget)); then
|
||||
budget=$isoch_budget
|
||||
fi
|
||||
fi
|
||||
local per_frame=$((budget / fps))
|
||||
if ((per_frame < 65536)); then
|
||||
per_frame=65536
|
||||
elif ((per_frame > 8388608)); then
|
||||
|
||||
@ -361,6 +361,7 @@ 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_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
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.22.36"
|
||||
version = "0.22.37"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
@ -52,6 +52,8 @@ 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;
|
||||
const DEFAULT_UVC_MJPEG_BUDGET_BYTES_PER_SEC: u32 = 10_000_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;
|
||||
const DEFAULT_UVC_STATS_PATH: &str = "/run/lesavka-uvc-video-stats.json";
|
||||
|
||||
@ -191,6 +193,7 @@ struct UvcConfig {
|
||||
interval: u32,
|
||||
max_packet: u32,
|
||||
frame_size: u32,
|
||||
bulk: bool,
|
||||
}
|
||||
|
||||
struct PayloadCap {
|
||||
@ -247,6 +250,8 @@ struct UvcVideoStream {
|
||||
frame_period: Option<Duration>,
|
||||
next_queue_at: Option<Instant>,
|
||||
streaming: bool,
|
||||
transport_bulk: bool,
|
||||
max_packet: u32,
|
||||
stats: UvcVideoStats,
|
||||
}
|
||||
|
||||
@ -259,6 +264,8 @@ struct UvcVideoStats {
|
||||
rejected_invalid: u64,
|
||||
fallback_idle: u64,
|
||||
latest_bytes: usize,
|
||||
last_rejected_oversize_bytes: usize,
|
||||
last_rejected_oversize_cap: usize,
|
||||
paced_sleeps: u64,
|
||||
paced_sleep_ms: u64,
|
||||
last_report: Option<Instant>,
|
||||
@ -275,6 +282,8 @@ impl UvcVideoStream {
|
||||
frame_period: None,
|
||||
next_queue_at: None,
|
||||
streaming: false,
|
||||
transport_bulk: false,
|
||||
max_packet: 0,
|
||||
stats: UvcVideoStats::default(),
|
||||
}
|
||||
}
|
||||
@ -284,6 +293,8 @@ impl UvcVideoStream {
|
||||
self.frame_max_bytes = uvc_frame_max_bytes(cfg);
|
||||
self.frame_period = uvc_queue_period(cfg.fps);
|
||||
self.next_queue_at = None;
|
||||
self.transport_bulk = cfg.bulk;
|
||||
self.max_packet = cfg.max_packet;
|
||||
self.set_format(cfg)?;
|
||||
self.request_buffers(uvc_buffer_count())?;
|
||||
for index in 0..self.buffers.len() {
|
||||
@ -296,9 +307,17 @@ impl UvcVideoStream {
|
||||
return Err(std::io::Error::last_os_error()).context("VIDIOC_STREAMON");
|
||||
}
|
||||
self.streaming = true;
|
||||
let transport = if self.transport_bulk {
|
||||
"bulk"
|
||||
} else {
|
||||
"isochronous"
|
||||
};
|
||||
eprintln!(
|
||||
"[lesavka-uvc] video stream started with {} buffers; frame_path={}",
|
||||
"[lesavka-uvc] video stream started with {} buffers; transport={} max_packet={} frame_cap={} frame_path={}",
|
||||
self.buffers.len(),
|
||||
transport,
|
||||
self.max_packet,
|
||||
self.frame_payload_limit(),
|
||||
self.frame_path.display()
|
||||
);
|
||||
Ok(())
|
||||
@ -494,6 +513,8 @@ impl UvcVideoStream {
|
||||
}
|
||||
Ok(frame) if frame.len() > max_frame_bytes => {
|
||||
self.stats.rejected_oversize += 1;
|
||||
self.stats.last_rejected_oversize_bytes = frame.len();
|
||||
self.stats.last_rejected_oversize_cap = max_frame_bytes;
|
||||
}
|
||||
Ok(frame) => {
|
||||
self.stats.reloaded += 1;
|
||||
@ -540,7 +561,7 @@ impl UvcVideoStream {
|
||||
}
|
||||
self.stats.last_report = Some(now);
|
||||
eprintln!(
|
||||
"[lesavka-uvc] video stats queued={} reloaded={} stale_replay={} rejected_oversize={} rejected_invalid={} fallback_idle={} latest_bytes={} frame_cap={} paced_sleeps={} paced_sleep_ms={}",
|
||||
"[lesavka-uvc] video stats queued={} reloaded={} stale_replay={} rejected_oversize={} rejected_invalid={} fallback_idle={} latest_bytes={} frame_cap={} last_rejected_oversize_bytes={} last_rejected_oversize_cap={} paced_sleeps={} paced_sleep_ms={}",
|
||||
self.stats.queued,
|
||||
self.stats.reloaded,
|
||||
self.stats.replayed_stale,
|
||||
@ -549,6 +570,8 @@ impl UvcVideoStream {
|
||||
self.stats.fallback_idle,
|
||||
self.stats.latest_bytes,
|
||||
self.frame_payload_limit(),
|
||||
self.stats.last_rejected_oversize_bytes,
|
||||
self.stats.last_rejected_oversize_cap,
|
||||
self.stats.paced_sleeps,
|
||||
self.stats.paced_sleep_ms
|
||||
);
|
||||
@ -664,18 +687,42 @@ fn uvc_frame_max_bytes(cfg: UvcConfig) -> usize {
|
||||
}
|
||||
|
||||
fn derived_uvc_frame_max_bytes(cfg: UvcConfig) -> usize {
|
||||
derived_uvc_frame_max_bytes_for_fps(cfg.fps)
|
||||
derived_uvc_frame_max_bytes_for_transport(cfg.fps, cfg.max_packet, cfg.bulk)
|
||||
}
|
||||
|
||||
fn derived_uvc_frame_max_bytes_for_fps(fps: u32) -> usize {
|
||||
fn derived_uvc_frame_max_bytes_for_transport(fps: u32, max_packet: u32, bulk: bool) -> usize {
|
||||
let fps = fps.max(1);
|
||||
let budget_per_sec = env_u32(
|
||||
let budget_per_sec = effective_uvc_mjpeg_budget_bytes_per_sec(max_packet, bulk);
|
||||
let per_frame = (budget_per_sec / fps).max(64 * 1024);
|
||||
per_frame.min(MAX_MJPEG_FRAME_BYTES as u32) as usize
|
||||
}
|
||||
|
||||
fn effective_uvc_mjpeg_budget_bytes_per_sec(max_packet: u32, bulk: bool) -> u32 {
|
||||
let configured = env_u32(
|
||||
"LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC",
|
||||
DEFAULT_UVC_MJPEG_BUDGET_BYTES_PER_SEC,
|
||||
)
|
||||
.max(1);
|
||||
let per_frame = (budget_per_sec / fps).max(64 * 1024);
|
||||
per_frame.min(MAX_MJPEG_FRAME_BYTES as u32) as usize
|
||||
if bulk {
|
||||
return configured;
|
||||
}
|
||||
|
||||
configured
|
||||
.min(uvc_isochronous_budget_bytes_per_sec(max_packet))
|
||||
.max(1)
|
||||
}
|
||||
|
||||
fn uvc_isochronous_budget_bytes_per_sec(max_packet: u32) -> u32 {
|
||||
let pct = env_u32(
|
||||
"LESAVKA_UVC_ISOCHRONOUS_LIMIT_PCT",
|
||||
DEFAULT_UVC_ISOCHRONOUS_LIMIT_PCT,
|
||||
)
|
||||
.clamp(1, 100);
|
||||
let bytes = u64::from(max_packet)
|
||||
.saturating_mul(u64::from(HIGH_SPEED_ISOCHRONOUS_MICROFRAMES_PER_SEC))
|
||||
.saturating_mul(u64::from(pct))
|
||||
/ 100;
|
||||
bytes.min(u64::from(u32::MAX)) as u32
|
||||
}
|
||||
|
||||
fn env_flag_enabled(name: &str, default: bool) -> bool {
|
||||
@ -720,9 +767,18 @@ fn uvc_bulk_transfer_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn uvc_frame_size_for_active_mode(width: u32, height: u32, fps: u32) -> u32 {
|
||||
fn uvc_frame_size_for_active_mode(
|
||||
width: u32,
|
||||
height: u32,
|
||||
fps: u32,
|
||||
max_packet: u32,
|
||||
bulk: bool,
|
||||
) -> u32 {
|
||||
env_u32_opt("LESAVKA_UVC_FRAME_SIZE")
|
||||
.unwrap_or_else(|| derived_uvc_frame_max_bytes_for_fps(fps).min(u32::MAX as usize) as u32)
|
||||
.unwrap_or_else(|| {
|
||||
derived_uvc_frame_max_bytes_for_transport(fps, max_packet, bulk)
|
||||
.min(u32::MAX as usize) as u32
|
||||
})
|
||||
.max((width.saturating_mul(height) / 32).max(64 * 1024))
|
||||
}
|
||||
|
||||
@ -738,7 +794,7 @@ fn write_atomic_text(path: &std::path::Path, text: &str) -> Result<()> {
|
||||
|
||||
fn uvc_stats_snapshot_json(stats: &UvcVideoStats, frame_cap: usize) -> String {
|
||||
format!(
|
||||
"{{\"queued\":{},\"reloaded\":{},\"stale_replay\":{},\"rejected_oversize\":{},\"rejected_invalid\":{},\"fallback_idle\":{},\"latest_bytes\":{},\"frame_cap\":{},\"paced_sleeps\":{},\"paced_sleep_ms\":{}}}\n",
|
||||
"{{\"queued\":{},\"reloaded\":{},\"stale_replay\":{},\"rejected_oversize\":{},\"rejected_invalid\":{},\"fallback_idle\":{},\"latest_bytes\":{},\"frame_cap\":{},\"last_rejected_oversize_bytes\":{},\"last_rejected_oversize_cap\":{},\"paced_sleeps\":{},\"paced_sleep_ms\":{}}}\n",
|
||||
stats.queued,
|
||||
stats.reloaded,
|
||||
stats.replayed_stale,
|
||||
@ -747,6 +803,8 @@ fn uvc_stats_snapshot_json(stats: &UvcVideoStats, frame_cap: usize) -> String {
|
||||
stats.fallback_idle,
|
||||
stats.latest_bytes,
|
||||
frame_cap,
|
||||
stats.last_rejected_oversize_bytes,
|
||||
stats.last_rejected_oversize_cap,
|
||||
stats.paced_sleeps,
|
||||
stats.paced_sleep_ms
|
||||
)
|
||||
@ -1029,9 +1087,8 @@ impl UvcConfig {
|
||||
interval = live_interval;
|
||||
}
|
||||
}
|
||||
let mut max_packet = env_u32("LESAVKA_UVC_MAXPACKET", 1024);
|
||||
let frame_size = uvc_frame_size_for_active_mode(width, height, fps);
|
||||
let bulk = uvc_bulk_transfer_enabled();
|
||||
let mut max_packet = env_u32("LESAVKA_UVC_MAXPACKET", 1024);
|
||||
if let Some(cap) = compute_payload_cap(bulk) {
|
||||
if max_packet > cap.limit {
|
||||
eprintln!(
|
||||
@ -1081,6 +1138,7 @@ impl UvcConfig {
|
||||
} else {
|
||||
max_packet = max_packet.min(1024);
|
||||
}
|
||||
let frame_size = uvc_frame_size_for_active_mode(width, height, fps, max_packet, bulk);
|
||||
|
||||
Self {
|
||||
width,
|
||||
@ -1089,6 +1147,7 @@ impl UvcConfig {
|
||||
interval,
|
||||
max_packet,
|
||||
frame_size,
|
||||
bulk,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -58,6 +58,10 @@ const DEFAULT_UVC_FRAME_MAX_AGE_MS: u64 = 1_000;
|
||||
#[cfg(coverage)]
|
||||
const DEFAULT_UVC_MJPEG_BUDGET_BYTES_PER_SEC: u32 = 10_000_000;
|
||||
#[cfg(coverage)]
|
||||
const DEFAULT_UVC_ISOCHRONOUS_LIMIT_PCT: u32 = 85;
|
||||
#[cfg(coverage)]
|
||||
const HIGH_SPEED_ISOCHRONOUS_MICROFRAMES_PER_SEC: u32 = 8_000;
|
||||
#[cfg(coverage)]
|
||||
const DEFAULT_UVC_STATS_INTERVAL_MS: u64 = 5_000;
|
||||
#[cfg(coverage)]
|
||||
const DEFAULT_UVC_STATS_PATH: &str = "/run/lesavka-uvc-video-stats.json";
|
||||
@ -103,6 +107,7 @@ struct UvcConfig {
|
||||
interval: u32,
|
||||
max_packet: u32,
|
||||
frame_size: u32,
|
||||
bulk: bool,
|
||||
}
|
||||
|
||||
#[cfg(coverage)]
|
||||
@ -164,6 +169,8 @@ struct UvcVideoStream {
|
||||
frame_max_bytes: usize,
|
||||
frame_period: Option<std::time::Duration>,
|
||||
next_queue_at: Option<std::time::Instant>,
|
||||
transport_bulk: bool,
|
||||
max_packet: u32,
|
||||
}
|
||||
|
||||
#[cfg(coverage)]
|
||||
@ -176,6 +183,8 @@ struct UvcVideoStats {
|
||||
rejected_invalid: u64,
|
||||
fallback_idle: u64,
|
||||
latest_bytes: usize,
|
||||
last_rejected_oversize_bytes: usize,
|
||||
last_rejected_oversize_cap: usize,
|
||||
paced_sleeps: u64,
|
||||
paced_sleep_ms: u64,
|
||||
}
|
||||
@ -190,6 +199,8 @@ impl UvcVideoStream {
|
||||
frame_max_bytes: MAX_MJPEG_FRAME_BYTES,
|
||||
frame_period: None,
|
||||
next_queue_at: None,
|
||||
transport_bulk: false,
|
||||
max_packet: 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -38,7 +38,6 @@ impl UvcConfig {
|
||||
let width = env_u32("LESAVKA_UVC_WIDTH", 1280);
|
||||
let height = env_u32("LESAVKA_UVC_HEIGHT", 720);
|
||||
let fps = env_u32("LESAVKA_UVC_FPS", 30).max(1);
|
||||
let frame_size = uvc_frame_size_for_active_mode(width, height, fps);
|
||||
let interval = env_u32("LESAVKA_UVC_INTERVAL", 0);
|
||||
let bulk = uvc_bulk_transfer_enabled();
|
||||
let mut max_packet = env_u32("LESAVKA_UVC_MAXPACKET", 1024);
|
||||
@ -51,6 +50,7 @@ impl UvcConfig {
|
||||
} else {
|
||||
max_packet.min(1024)
|
||||
};
|
||||
let frame_size = uvc_frame_size_for_active_mode(width, height, fps, max_packet, bulk);
|
||||
|
||||
let interval = if interval == 0 {
|
||||
10_000_000 / fps
|
||||
@ -65,6 +65,7 @@ impl UvcConfig {
|
||||
interval,
|
||||
max_packet,
|
||||
frame_size,
|
||||
bulk,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -189,19 +190,45 @@ fn uvc_frame_max_bytes(cfg: UvcConfig) -> usize {
|
||||
|
||||
#[cfg(coverage)]
|
||||
fn derived_uvc_frame_max_bytes(cfg: UvcConfig) -> usize {
|
||||
derived_uvc_frame_max_bytes_for_fps(cfg.fps)
|
||||
derived_uvc_frame_max_bytes_for_transport(cfg.fps, cfg.max_packet, cfg.bulk)
|
||||
}
|
||||
|
||||
#[cfg(coverage)]
|
||||
fn derived_uvc_frame_max_bytes_for_fps(fps: u32) -> usize {
|
||||
fn derived_uvc_frame_max_bytes_for_transport(fps: u32, max_packet: u32, bulk: bool) -> usize {
|
||||
let fps = fps.max(1);
|
||||
let budget_per_sec = env_u32(
|
||||
let budget_per_sec = effective_uvc_mjpeg_budget_bytes_per_sec(max_packet, bulk);
|
||||
let per_frame = (budget_per_sec / fps).max(64 * 1024);
|
||||
per_frame.min(MAX_MJPEG_FRAME_BYTES as u32) as usize
|
||||
}
|
||||
|
||||
#[cfg(coverage)]
|
||||
fn effective_uvc_mjpeg_budget_bytes_per_sec(max_packet: u32, bulk: bool) -> u32 {
|
||||
let configured = env_u32(
|
||||
"LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC",
|
||||
DEFAULT_UVC_MJPEG_BUDGET_BYTES_PER_SEC,
|
||||
)
|
||||
.max(1);
|
||||
let per_frame = (budget_per_sec / fps).max(64 * 1024);
|
||||
per_frame.min(MAX_MJPEG_FRAME_BYTES as u32) as usize
|
||||
if bulk {
|
||||
return configured;
|
||||
}
|
||||
|
||||
configured
|
||||
.min(uvc_isochronous_budget_bytes_per_sec(max_packet))
|
||||
.max(1)
|
||||
}
|
||||
|
||||
#[cfg(coverage)]
|
||||
fn uvc_isochronous_budget_bytes_per_sec(max_packet: u32) -> u32 {
|
||||
let pct = env_u32(
|
||||
"LESAVKA_UVC_ISOCHRONOUS_LIMIT_PCT",
|
||||
DEFAULT_UVC_ISOCHRONOUS_LIMIT_PCT,
|
||||
)
|
||||
.clamp(1, 100);
|
||||
let bytes = u64::from(max_packet)
|
||||
.saturating_mul(u64::from(HIGH_SPEED_ISOCHRONOUS_MICROFRAMES_PER_SEC))
|
||||
.saturating_mul(u64::from(pct))
|
||||
/ 100;
|
||||
bytes.min(u64::from(u32::MAX)) as u32
|
||||
}
|
||||
|
||||
#[cfg(coverage)]
|
||||
@ -247,9 +274,18 @@ fn uvc_bulk_transfer_enabled() -> bool {
|
||||
}
|
||||
|
||||
#[cfg(coverage)]
|
||||
fn uvc_frame_size_for_active_mode(width: u32, height: u32, fps: u32) -> u32 {
|
||||
fn uvc_frame_size_for_active_mode(
|
||||
width: u32,
|
||||
height: u32,
|
||||
fps: u32,
|
||||
max_packet: u32,
|
||||
bulk: bool,
|
||||
) -> u32 {
|
||||
env_u32_opt("LESAVKA_UVC_FRAME_SIZE")
|
||||
.unwrap_or_else(|| derived_uvc_frame_max_bytes_for_fps(fps).min(u32::MAX as usize) as u32)
|
||||
.unwrap_or_else(|| {
|
||||
derived_uvc_frame_max_bytes_for_transport(fps, max_packet, bulk)
|
||||
.min(u32::MAX as usize) as u32
|
||||
})
|
||||
.max((width.saturating_mul(height) / 32).max(64 * 1024))
|
||||
}
|
||||
|
||||
@ -282,7 +318,7 @@ fn write_atomic_text(path: &std::path::Path, text: &str) -> Result<()> {
|
||||
#[cfg(coverage)]
|
||||
fn uvc_stats_snapshot_json(stats: &UvcVideoStats, frame_cap: usize) -> String {
|
||||
format!(
|
||||
"{{\"queued\":{},\"reloaded\":{},\"stale_replay\":{},\"rejected_oversize\":{},\"rejected_invalid\":{},\"fallback_idle\":{},\"latest_bytes\":{},\"frame_cap\":{},\"paced_sleeps\":{},\"paced_sleep_ms\":{}}}\n",
|
||||
"{{\"queued\":{},\"reloaded\":{},\"stale_replay\":{},\"rejected_oversize\":{},\"rejected_invalid\":{},\"fallback_idle\":{},\"latest_bytes\":{},\"frame_cap\":{},\"last_rejected_oversize_bytes\":{},\"last_rejected_oversize_cap\":{},\"paced_sleeps\":{},\"paced_sleep_ms\":{}}}\n",
|
||||
stats.queued,
|
||||
stats.reloaded,
|
||||
stats.replayed_stale,
|
||||
@ -291,6 +327,8 @@ fn uvc_stats_snapshot_json(stats: &UvcVideoStats, frame_cap: usize) -> String {
|
||||
stats.fallback_idle,
|
||||
stats.latest_bytes,
|
||||
frame_cap,
|
||||
stats.last_rejected_oversize_bytes,
|
||||
stats.last_rejected_oversize_cap,
|
||||
stats.paced_sleeps,
|
||||
stats.paced_sleep_ms
|
||||
)
|
||||
|
||||
@ -12,6 +12,7 @@ fn sample_cfg() -> UvcConfig {
|
||||
interval: 400_000,
|
||||
max_packet: 1024,
|
||||
frame_size: 1_843_200,
|
||||
bulk: true,
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,6 +114,27 @@ fn uvc_frame_max_bytes_defaults_to_freshness_budget_and_allows_override() {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn uvc_frame_budget_caps_isochronous_transport() {
|
||||
with_vars(
|
||||
[
|
||||
("LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC", Some("10000000")),
|
||||
("LESAVKA_UVC_ISOCHRONOUS_LIMIT_PCT", None::<&str>),
|
||||
],
|
||||
|| {
|
||||
assert_eq!(
|
||||
derived_uvc_frame_max_bytes_for_transport(30, 1024, false),
|
||||
232_106
|
||||
);
|
||||
assert_eq!(
|
||||
derived_uvc_frame_max_bytes_for_transport(30, 1024, true),
|
||||
333_333
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn main_coverage_mode_returns_error_for_non_uvc_node() {
|
||||
|
||||
@ -11,6 +11,9 @@ 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_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";
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(super) struct MjpegSpoolTiming {
|
||||
@ -107,6 +110,56 @@ fn env_flag_enabled(name: &str, default: bool) -> bool {
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
fn read_u32_file(path: impl AsRef<Path>) -> Option<u32> {
|
||||
fs::read_to_string(path)
|
||||
.ok()
|
||||
.and_then(|value| value.trim().parse::<u32>().ok())
|
||||
}
|
||||
|
||||
fn uvc_bulk_transfer_enabled() -> bool {
|
||||
if !env_flag_enabled("LESAVKA_UVC_BULK", true) {
|
||||
return false;
|
||||
}
|
||||
let base = Path::new(CONFIGFS_UVC_BASE);
|
||||
!(base.exists() && !base.join("streaming_bulk").exists())
|
||||
}
|
||||
|
||||
fn uvc_streaming_maxpacket(bulk: bool) -> u32 {
|
||||
let mut maxpacket = env_u32_opt("LESAVKA_UVC_MAXPACKET").unwrap_or(1024);
|
||||
if let Some(live) = read_u32_file(Path::new(CONFIGFS_UVC_BASE).join("streaming_maxpacket")) {
|
||||
maxpacket = maxpacket.min(live);
|
||||
}
|
||||
if bulk {
|
||||
maxpacket.min(512)
|
||||
} else {
|
||||
maxpacket.min(1024)
|
||||
}
|
||||
}
|
||||
|
||||
fn uvc_isochronous_budget_bytes_per_sec(maxpacket: u32) -> u32 {
|
||||
let pct = env_u32_opt("LESAVKA_UVC_ISOCHRONOUS_LIMIT_PCT")
|
||||
.unwrap_or(DEFAULT_UVC_ISOCHRONOUS_LIMIT_PCT)
|
||||
.clamp(1, 100);
|
||||
let bytes = u64::from(maxpacket)
|
||||
.saturating_mul(u64::from(HIGH_SPEED_ISOCHRONOUS_MICROFRAMES_PER_SEC))
|
||||
.saturating_mul(u64::from(pct))
|
||||
/ 100;
|
||||
bytes.min(u64::from(u32::MAX)) as u32
|
||||
}
|
||||
|
||||
fn effective_mjpeg_budget_bytes_per_sec() -> u32 {
|
||||
let configured = env_u32_opt("LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC")
|
||||
.unwrap_or(DEFAULT_UVC_MJPEG_BUDGET_BYTES_PER_SEC)
|
||||
.max(1);
|
||||
if uvc_bulk_transfer_enabled() {
|
||||
configured
|
||||
} else {
|
||||
configured
|
||||
.min(uvc_isochronous_budget_bytes_per_sec(uvc_streaming_maxpacket(false)))
|
||||
.max(1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve the MJPEG byte budget used before publishing to the helper.
|
||||
///
|
||||
/// Inputs: active FPS plus `LESAVKA_UVC_FRAME_MAX_BYTES`,
|
||||
@ -125,9 +178,7 @@ pub(super) fn mjpeg_spool_frame_max_bytes(fps: u32) -> usize {
|
||||
}
|
||||
|
||||
let fps = fps.max(1);
|
||||
let budget_per_sec = env_u32_opt("LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC")
|
||||
.unwrap_or(DEFAULT_UVC_MJPEG_BUDGET_BYTES_PER_SEC)
|
||||
.max(1);
|
||||
let budget_per_sec = effective_mjpeg_budget_bytes_per_sec();
|
||||
let per_frame = (budget_per_sec / fps).max(64 * 1024);
|
||||
per_frame.min(MAX_MJPEG_FRAME_BYTES as u32) as usize
|
||||
}
|
||||
@ -368,6 +419,20 @@ mod tests {
|
||||
assert_eq!(super::mjpeg_spool_frame_max_bytes(30), 65_536);
|
||||
},
|
||||
);
|
||||
|
||||
temp_env::with_vars(
|
||||
[
|
||||
("LESAVKA_UVC_FRAME_SIZE_GUARD", Some("1")),
|
||||
("LESAVKA_UVC_FRAME_MAX_BYTES", None::<&str>),
|
||||
("LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC", Some("10000000")),
|
||||
("LESAVKA_UVC_BULK", Some("0")),
|
||||
("LESAVKA_UVC_MAXPACKET", Some("1024")),
|
||||
("LESAVKA_UVC_ISOCHRONOUS_LIMIT_PCT", None::<&str>),
|
||||
],
|
||||
|| {
|
||||
assert_eq!(super::mjpeg_spool_frame_max_bytes(30), 232_106);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -102,6 +102,9 @@ fn core_script_keeps_uvc_output_on_supported_mjpeg_descriptor() {
|
||||
"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_ISOCHRONOUS_LIMIT_PCT=${LESAVKA_UVC_ISOCHRONOUS_LIMIT_PCT:-85}",
|
||||
"uvc_isochronous_budget_bytes_per_sec()",
|
||||
"isoch_budget=\"$(uvc_isochronous_budget_bytes_per_sec)\"",
|
||||
"UVC_FRAME_SIZE=\"$(uvc_mjpeg_frame_size_for_fps \"$UVC_FPS\")\"",
|
||||
"write_mjpeg_frame_descriptor 1080p 1920 1080",
|
||||
"write_mjpeg_frame_descriptor 720p 1280 720",
|
||||
|
||||
@ -29,6 +29,7 @@ mod uvc_binary {
|
||||
interval: 400_000,
|
||||
max_packet: 1024,
|
||||
frame_size: 1_843_200,
|
||||
bulk: true,
|
||||
}
|
||||
}
|
||||
|
||||
@ -332,6 +333,7 @@ mod uvc_binary {
|
||||
assert_eq!(cfg.interval, 10_000_000 / 30);
|
||||
assert_eq!(cfg.frame_size, 300_000);
|
||||
assert_eq!(cfg.max_packet, 512);
|
||||
assert!(cfg.bulk);
|
||||
assert!(uvc_bulk_transfer_enabled());
|
||||
},
|
||||
);
|
||||
@ -341,6 +343,31 @@ mod uvc_binary {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn uvc_isochronous_transport_caps_mjpeg_budget_to_wire_rate() {
|
||||
with_vars(
|
||||
[
|
||||
("LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC", Some("10000000")),
|
||||
("LESAVKA_UVC_ISOCHRONOUS_LIMIT_PCT", None::<&str>),
|
||||
],
|
||||
|| {
|
||||
assert_eq!(
|
||||
derived_uvc_frame_max_bytes_for_transport(30, 1024, false),
|
||||
232_106
|
||||
);
|
||||
assert_eq!(
|
||||
derived_uvc_frame_max_bytes_for_transport(20, 1024, false),
|
||||
348_160
|
||||
);
|
||||
assert_eq!(
|
||||
derived_uvc_frame_max_bytes_for_transport(30, 1024, true),
|
||||
333_333
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uvc_bulk_mode_is_tied_to_live_configfs_support() {
|
||||
let source = include_str!("../../../../server/src/bin/lesavka-uvc.real.inc");
|
||||
@ -360,6 +387,8 @@ mod uvc_binary {
|
||||
rejected_invalid: 3,
|
||||
fallback_idle: 4,
|
||||
latest_bytes: 77_036,
|
||||
last_rejected_oversize_bytes: 300_001,
|
||||
last_rejected_oversize_cap: 300_000,
|
||||
paced_sleeps: 5,
|
||||
paced_sleep_ms: 123,
|
||||
last_report: None,
|
||||
@ -368,6 +397,8 @@ mod uvc_binary {
|
||||
assert!(json.contains("\"queued\":7"));
|
||||
assert!(json.contains("\"latest_bytes\":77036"));
|
||||
assert!(json.contains("\"frame_cap\":333333"));
|
||||
assert!(json.contains("\"last_rejected_oversize_bytes\":300001"));
|
||||
assert!(json.contains("\"last_rejected_oversize_cap\":300000"));
|
||||
assert!(json.contains("\"paced_sleeps\":5"));
|
||||
assert!(json.contains("\"paced_sleep_ms\":123"));
|
||||
|
||||
|
||||
@ -28,6 +28,7 @@ mod uvc_binary_extra {
|
||||
interval: 400_000,
|
||||
max_packet: 1024,
|
||||
frame_size: 1_843_200,
|
||||
bulk: true,
|
||||
}
|
||||
}
|
||||
|
||||
@ -427,6 +428,9 @@ mod uvc_binary_extra {
|
||||
stream.refresh_latest_frame();
|
||||
|
||||
assert_eq!(stream.latest_frame, vec![0xff, 0xd8, 0x11, 0xff, 0xd9]);
|
||||
assert_eq!(stream.stats.rejected_oversize, 1);
|
||||
assert_eq!(stream.stats.last_rejected_oversize_bytes, 12);
|
||||
assert_eq!(stream.stats.last_rejected_oversize_cap, 8);
|
||||
assert_eq!(stream.frame_payload_limit(), 8);
|
||||
assert_eq!(stream.frame_for_buffer(4), MINIMAL_MJPEG_FRAME);
|
||||
assert_eq!(
|
||||
|
||||
@ -197,6 +197,7 @@ fn server_install_pins_hdmi_camera_and_display_defaults() {
|
||||
assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_BULK 1"));
|
||||
assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_FRAME_SIZE_GUARD 1"));
|
||||
assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_FRAME_MAX_BYTES 0"));
|
||||
assert!(SERVER_INSTALL.contains("uvc_env_value LESAVKA_UVC_ISOCHRONOUS_LIMIT_PCT 85"));
|
||||
assert!(
|
||||
SERVER_INSTALL
|
||||
.contains("uvc_env_value LESAVKA_UVC_STATS_PATH /run/lesavka-uvc-video-stats.json")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user