media: steady hevc to mjpeg uvc handoff
This commit is contained in:
parent
1eabd30fa1
commit
ef9701f6e9
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_client"
|
||||
version = "0.22.16"
|
||||
version = "0.22.17"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@ -1686,7 +1686,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_common"
|
||||
version = "0.22.16"
|
||||
version = "0.22.17"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@ -1698,7 +1698,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_server"
|
||||
version = "0.22.16"
|
||||
version = "0.22.17"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
|
||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.22.16"
|
||||
version = "0.22.17"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.22.16"
|
||||
version = "0.22.17"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@ -333,7 +333,8 @@ from `LESAVKA_CLIENT_PKI_SSH_SOURCE` over SSH. Runtime clients require the insta
|
||||
| `LESAVKA_UVC_FRAME_MAX_BYTES` | UVC helper MJPEG frame-size guard; explicit maximum accepted frame bytes, where `0` disables the guard and otherwise oversized frames are frozen out |
|
||||
| `LESAVKA_UVC_FRAME_SIZE` | server hardware/device override |
|
||||
| `LESAVKA_UVC_HEIGHT` | server hardware/device override |
|
||||
| `LESAVKA_UVC_HEVC_SPOOL_PULL_TIMEOUT_MS` | server HEVC decode-to-MJPEG freshness override; appsink pull wait for decoded MJPEG handoff before publishing newest frame to the UVC helper, defaults to `5` and is capped at `50` |
|
||||
| `LESAVKA_UVC_HEVC_SPOOL_PULL_TIMEOUT_MS` | server HEVC decode-to-MJPEG freshness override; appsink pull wait for decoded MJPEG handoff before publishing newest frame to the UVC helper, defaults to `20` and is capped at `50` |
|
||||
| `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_LIMIT_PCT` | server hardware/device override |
|
||||
|
||||
@ -1535,7 +1535,8 @@ SERVER_ENV_TMP=$(mktemp)
|
||||
printf 'LESAVKA_UVC_HEVC_MIN_REFERENCE_BYTES=%s\n' "${LESAVKA_UVC_HEVC_MIN_REFERENCE_BYTES:-65536}"
|
||||
printf 'LESAVKA_UVC_HEVC_MIN_PAYLOAD_DISTINCT_BYTES=%s\n' "${LESAVKA_UVC_HEVC_MIN_PAYLOAD_DISTINCT_BYTES:-12}"
|
||||
printf 'LESAVKA_UVC_HEVC_DOMINANT_BYTE_PCT=%s\n' "${LESAVKA_UVC_HEVC_DOMINANT_BYTE_PCT:-92}"
|
||||
printf 'LESAVKA_UVC_HEVC_FRESHNESS_QUEUE_BUFFERS=%s\n' "${LESAVKA_UVC_HEVC_FRESHNESS_QUEUE_BUFFERS:-1}"
|
||||
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_SERVER_BIND_ADDR=%s\n' "${INSTALL_SERVER_BIND_ADDR}"
|
||||
printf 'LESAVKA_UVC_CODEC=%s\n' "${INSTALL_UVC_CODEC}"
|
||||
|
||||
@ -10,7 +10,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.22.16"
|
||||
version = "0.22.17"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
@ -126,7 +126,7 @@ pub(super) fn decoded_mjpeg_pull_timeout() -> gst::ClockTime {
|
||||
let timeout_ms = std::env::var("LESAVKA_UVC_HEVC_SPOOL_PULL_TIMEOUT_MS")
|
||||
.ok()
|
||||
.and_then(|value| value.trim().parse::<u64>().ok())
|
||||
.unwrap_or(5)
|
||||
.unwrap_or(20)
|
||||
.min(50);
|
||||
gst::ClockTime::from_mseconds(timeout_ms)
|
||||
}
|
||||
@ -248,15 +248,15 @@ pub(super) fn spool_mjpeg_frame_with_timing(
|
||||
mod tests {
|
||||
/// Verifies HEVC decoded-frame polling defaults to a freshness-first wait.
|
||||
///
|
||||
/// Input: unset timeout env var. Output: 5ms appsink poll timeout. Why:
|
||||
/// server-side decode should keep enough patience for normal scheduling
|
||||
/// jitter without letting an HEVC backlog accumulate behind UVC playback.
|
||||
/// Input: unset timeout env var. Output: 20ms appsink poll timeout. Why:
|
||||
/// server-side hardware decode/JPEG encode often lands inside one 30fps
|
||||
/// frame interval, and a 5ms poll was starving Meet-visible UVC output.
|
||||
#[test]
|
||||
fn decoded_mjpeg_pull_timeout_defaults_to_short_bounded_wait() {
|
||||
temp_env::with_var_unset("LESAVKA_UVC_HEVC_SPOOL_PULL_TIMEOUT_MS", || {
|
||||
assert_eq!(
|
||||
super::decoded_mjpeg_pull_timeout(),
|
||||
gstreamer::ClockTime::from_mseconds(5)
|
||||
gstreamer::ClockTime::from_mseconds(20)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -104,7 +104,7 @@ fn uvc_appsrc_leaky_type() -> String {
|
||||
}
|
||||
|
||||
fn uvc_hevc_freshness_queue_buffers() -> u32 {
|
||||
positive_u64_env("LESAVKA_UVC_HEVC_FRESHNESS_QUEUE_BUFFERS", 1)
|
||||
positive_u64_env("LESAVKA_UVC_HEVC_FRESHNESS_QUEUE_BUFFERS", 2)
|
||||
.min(4)
|
||||
.max(1) as u32
|
||||
}
|
||||
@ -134,12 +134,13 @@ fn configure_uvc_appsrc(appsrc: &gst_app::AppSrc) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a single-frame leaky queue for the decoded HEVC handoff branch.
|
||||
/// Build a tiny leaky queue for the decoded HEVC handoff branch.
|
||||
///
|
||||
/// Inputs: a stable queue name. Output: configured GStreamer queue element.
|
||||
/// Why: hidden raw/JPEG backlogs are memory leaks in a live webcam path; after
|
||||
/// HEVC is decoded it is safe to drop stale raw frames and keep only the newest
|
||||
/// candidate for MJPEG publication.
|
||||
/// candidate for MJPEG publication while absorbing normal decoder scheduling
|
||||
/// jitter.
|
||||
#[cfg(not(coverage))]
|
||||
fn build_hevc_freshness_queue(name: &str) -> anyhow::Result<gst::Element> {
|
||||
let queue = gst::ElementFactory::make("queue")
|
||||
@ -152,6 +153,22 @@ fn build_hevc_freshness_queue(name: &str) -> anyhow::Result<gst::Element> {
|
||||
Ok(queue)
|
||||
}
|
||||
|
||||
/// Configure conservative recovery knobs on hardware HEVC decoders.
|
||||
///
|
||||
/// Inputs: a decoder element selected by `require_hevc_decoder`. Output:
|
||||
/// side-effect-only property updates when the element supports them. Why: the
|
||||
/// Pi stateless decoder can otherwise hold onto corrupt or dependency-missing
|
||||
/// pictures after live HEVC packet drops, starving the MJPEG UVC handoff.
|
||||
#[cfg(not(coverage))]
|
||||
fn configure_hevc_decoder(decoder: &gst::Element) {
|
||||
if decoder.has_property("discard-corrupted-frames", None) {
|
||||
decoder.set_property("discard-corrupted-frames", true);
|
||||
}
|
||||
if decoder.has_property("automatic-request-sync-points", None) {
|
||||
decoder.set_property("automatic-request-sync-points", true);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(coverage))]
|
||||
struct WebcamBusWatchHandle {
|
||||
alive: Arc<AtomicBool>,
|
||||
@ -372,6 +389,7 @@ impl WebcamSink {
|
||||
let decoder = gst::ElementFactory::make(decoder_name)
|
||||
.build()
|
||||
.with_context(|| format!("building HEVC decoder element {decoder_name}"))?;
|
||||
configure_hevc_decoder(&decoder);
|
||||
let decoded_queue = build_hevc_freshness_queue("hevc_mjpeg_decoded_queue")?;
|
||||
let convert = gst::ElementFactory::make("videoconvert").build()?;
|
||||
let encoder = gst::ElementFactory::make("jpegenc")
|
||||
@ -751,10 +769,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hevc_spool_freshness_bounds_default_to_single_frame_recovery() {
|
||||
fn hevc_spool_freshness_bounds_default_to_tiny_live_handoff() {
|
||||
temp_env::with_var_unset("LESAVKA_UVC_HEVC_FRESHNESS_QUEUE_BUFFERS", || {
|
||||
temp_env::with_var_unset("LESAVKA_UVC_HEVC_DECODE_MISS_LIMIT", || {
|
||||
assert_eq!(super::uvc_hevc_freshness_queue_buffers(), 1);
|
||||
assert_eq!(super::uvc_hevc_freshness_queue_buffers(), 2);
|
||||
assert_eq!(super::uvc_hevc_decode_miss_limit(), 15);
|
||||
});
|
||||
});
|
||||
@ -775,7 +793,7 @@ mod tests {
|
||||
|
||||
temp_env::with_var("LESAVKA_UVC_HEVC_FRESHNESS_QUEUE_BUFFERS", Some("0"), || {
|
||||
temp_env::with_var("LESAVKA_UVC_HEVC_DECODE_MISS_LIMIT", Some("0"), || {
|
||||
assert_eq!(super::uvc_hevc_freshness_queue_buffers(), 1);
|
||||
assert_eq!(super::uvc_hevc_freshness_queue_buffers(), 2);
|
||||
assert_eq!(super::uvc_hevc_decode_miss_limit(), 15);
|
||||
});
|
||||
});
|
||||
|
||||
@ -117,6 +117,9 @@ fn hevc_ingress_decodes_to_existing_mjpeg_uvc_path() {
|
||||
"video/x-h265",
|
||||
"h265parse",
|
||||
"require_hevc_decoder()",
|
||||
"configure_hevc_decoder(&decoder)",
|
||||
"discard-corrupted-frames",
|
||||
"automatic-request-sync-points",
|
||||
"jpegenc",
|
||||
"LESAVKA_UVC_HEVC_JPEG_QUALITY",
|
||||
"LESAVKA_UVC_HEVC_SPOOL_PULL_TIMEOUT_MS",
|
||||
|
||||
@ -32,6 +32,7 @@ fn server_install_pins_hdmi_camera_and_display_defaults() {
|
||||
"LESAVKA_UPSTREAM_PAIR_SLACK_US=%s",
|
||||
"LESAVKA_UPSTREAM_AUDIO_MASTER_WAIT_GRACE_MS=%s",
|
||||
"LESAVKA_UPSTREAM_STALE_DROP_MS=%s",
|
||||
"LESAVKA_UVC_HEVC_SPOOL_PULL_TIMEOUT_MS=%s",
|
||||
"LESAVKA_UVC_HEVC_FRESHNESS_QUEUE_BUFFERS=%s",
|
||||
"LESAVKA_UVC_HEVC_DECODE_MISS_LIMIT=%s",
|
||||
"LESAVKA_SERVER_BIND_ADDR=%s",
|
||||
@ -149,7 +150,8 @@ fn server_install_pins_hdmi_camera_and_display_defaults() {
|
||||
assert!(SERVER_INSTALL.contains("${LESAVKA_UPSTREAM_PAIR_SLACK_US:-80000}"));
|
||||
assert!(SERVER_INSTALL.contains("${LESAVKA_UPSTREAM_AUDIO_MASTER_WAIT_GRACE_MS:-350}"));
|
||||
assert!(SERVER_INSTALL.contains("${LESAVKA_UPSTREAM_STALE_DROP_MS:-80}"));
|
||||
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_HEVC_FRESHNESS_QUEUE_BUFFERS:-1}"));
|
||||
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_server::video=info"),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user