media: disable leaking mjpeg normalizer by default
This commit is contained in:
parent
260cb263f6
commit
980332a5cb
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.22.47"
|
version = "0.22.48"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
@ -1686,7 +1686,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.22.47"
|
version = "0.22.48"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
@ -1698,7 +1698,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.22.47"
|
version = "0.22.48"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.22.47"
|
version = "0.22.48"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.22.47"
|
version = "0.22.48"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
@ -609,11 +609,12 @@ These entries are intentionally concise because most are manual lab or CI harnes
|
|||||||
| `LESAVKA_UPSTREAM_AUTO_HEAL_AFTER_MS` | client live bundled-upstream startup heal delay; defaults to `3000`ms before issuing the safe audio-epoch recovery |
|
| `LESAVKA_UPSTREAM_AUTO_HEAL_AFTER_MS` | client live bundled-upstream startup heal delay; defaults to `3000`ms before issuing the safe audio-epoch recovery |
|
||||||
| `LESAVKA_UPSTREAM_SOURCE_LEAD_CAP_MS` | server upstream media timing override; bounds live source lead or playout behavior while tuning client-to-server transport |
|
| `LESAVKA_UPSTREAM_SOURCE_LEAD_CAP_MS` | server upstream media timing override; bounds live source lead or playout behavior while tuning client-to-server transport |
|
||||||
| `LESAVKA_UVC_CONFIGFS_BASE` | server UVC gadget mode/configfs override used by runtime reconfiguration and hardware-in-the-loop probes |
|
| `LESAVKA_UVC_CONFIGFS_BASE` | server UVC gadget mode/configfs override used by runtime reconfiguration and hardware-in-the-loop probes |
|
||||||
| `LESAVKA_UVC_DIRECT_MJPEG_JPEG_QUALITY` | server direct-MJPEG normalization JPEG quality; defaults to `72` to reduce browser-facing UVC bitstream pressure |
|
| `LESAVKA_UVC_DIRECT_MJPEG_JPEG_QUALITY` | server direct-MJPEG normalization JPEG quality; defaults to `72` when the opt-in normalizer is enabled |
|
||||||
| `LESAVKA_UVC_DIRECT_MJPEG_MIN_REFERENCE_BYTES` | server direct-MJPEG guard baseline; frames smaller than this do not establish the last-good reference |
|
| `LESAVKA_UVC_DIRECT_MJPEG_MIN_REFERENCE_BYTES` | server direct-MJPEG guard baseline; frames smaller than this do not establish the last-good reference |
|
||||||
| `LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE` | server direct-MJPEG normalization toggle; defaults on so camera MJPEG is decoded/re-encoded before the UVC helper |
|
| `LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE` | server direct-MJPEG normalization toggle; defaults off because the native GStreamer decode/re-encode branch can retain RSS during long calls |
|
||||||
| `LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_MISS_LIMIT` | server direct-MJPEG normalization recovery threshold; after this many consecutive empty pulls, the session falls back to guarded passthrough |
|
| `LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_MISS_LIMIT` | server direct-MJPEG normalization recovery threshold; after this many consecutive empty pulls, the session falls back to guarded passthrough |
|
||||||
| `LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_PULL_TIMEOUT_MS` | server direct-MJPEG normalization appsink timeout; defaults to `25`ms and is capped at `50`ms to avoid live backlog |
|
| `LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_PULL_TIMEOUT_MS` | server direct-MJPEG normalization appsink timeout; defaults to `25`ms and is capped at `50`ms to avoid live backlog |
|
||||||
|
| `LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_RSS_LIMIT_MB` | server direct-MJPEG normalization RSS safety ceiling; defaults to `768`, and `0` disables this opt-in branch guard |
|
||||||
| `LESAVKA_UVC_DIRECT_MJPEG_SIZE_DROP_PCT` | server direct-MJPEG corruption guard threshold; frames below this percentage of the last good reference are frozen out |
|
| `LESAVKA_UVC_DIRECT_MJPEG_SIZE_DROP_PCT` | server direct-MJPEG corruption guard threshold; frames below this percentage of the last good reference are frozen out |
|
||||||
| `LESAVKA_UVC_DIRECT_MJPEG_VISUAL_GUARD` | server direct-MJPEG corruption guard toggle; defaults on so obvious collapsed or flat payloads freeze the last good frame |
|
| `LESAVKA_UVC_DIRECT_MJPEG_VISUAL_GUARD` | server direct-MJPEG corruption guard toggle; defaults on so obvious collapsed or flat payloads freeze the last good frame |
|
||||||
| `LESAVKA_UVC_HEVC_DOMINANT_BYTE_PCT` | server HEVC-to-MJPEG corruption guard threshold; flat decoded MJPEG payloads with one byte at or above this percentage are frozen out, default `92` |
|
| `LESAVKA_UVC_HEVC_DOMINANT_BYTE_PCT` | server HEVC-to-MJPEG corruption guard threshold; flat decoded MJPEG payloads with one byte at or above this percentage are frozen out, default `92` |
|
||||||
|
|||||||
@ -1609,10 +1609,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_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_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_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:-1}"
|
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_JPEG_QUALITY=%s\n' "${LESAVKA_UVC_DIRECT_MJPEG_JPEG_QUALITY:-72}"
|
||||||
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_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_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_VISUAL_GUARD=%s\n' "${LESAVKA_UVC_DIRECT_MJPEG_VISUAL_GUARD:-1}"
|
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_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}"
|
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]
|
[package]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.22.47"
|
version = "0.22.48"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
autobins = false
|
autobins = false
|
||||||
|
|
||||||
|
|||||||
@ -116,6 +116,36 @@ impl CameraRuntime {
|
|||||||
self.generation.load(Ordering::Relaxed) == session_id
|
self.generation.load(Ordering::Relaxed) == session_id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Release the active relay when the owning stream has ended.
|
||||||
|
///
|
||||||
|
/// Inputs: the camera session id returned by `activate`.
|
||||||
|
/// Outputs: true only when that session was still current and was
|
||||||
|
/// superseded. Why: keeping a completed UVC session's GStreamer pipeline
|
||||||
|
/// alive preserves native buffers and makes allocator retention look like
|
||||||
|
/// a server leak after the client UI disconnects.
|
||||||
|
pub async fn release_if_active(&self, session_id: u64) -> bool {
|
||||||
|
if self
|
||||||
|
.generation
|
||||||
|
.compare_exchange(
|
||||||
|
session_id,
|
||||||
|
session_id.saturating_add(1),
|
||||||
|
Ordering::SeqCst,
|
||||||
|
Ordering::Relaxed,
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut slot = self.slot.lock().await;
|
||||||
|
let released = slot.take().is_some();
|
||||||
|
info!(
|
||||||
|
session_id,
|
||||||
|
released, "🎥 camera relay released after stream lifecycle ended"
|
||||||
|
);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
/// Supersede the active camera stream and drop the userspace relay sink.
|
/// Supersede the active camera stream and drop the userspace relay sink.
|
||||||
///
|
///
|
||||||
/// Inputs: none.
|
/// Inputs: none.
|
||||||
|
|||||||
@ -262,6 +262,7 @@ impl Handler {
|
|||||||
relay.feed(pkt); // ← all logging inside video.rs
|
relay.feed(pkt); // ← all logging inside video.rs
|
||||||
upstream_media_rt.mark_video_presented(presented_pts, plan.due_at);
|
upstream_media_rt.mark_video_presented(presented_pts, plan.due_at);
|
||||||
}
|
}
|
||||||
|
camera_rt.release_if_active(camera_session_id).await;
|
||||||
tx.send(Ok(Empty {})).await.ok();
|
tx.send(Ok(Empty {})).await.ok();
|
||||||
Ok::<(), Status>(())
|
Ok::<(), Status>(())
|
||||||
});
|
});
|
||||||
|
|||||||
@ -377,6 +377,7 @@ impl Handler {
|
|||||||
}
|
}
|
||||||
upstream_media_rt.close_camera(camera_lease.generation);
|
upstream_media_rt.close_camera(camera_lease.generation);
|
||||||
upstream_media_rt.close_microphone(microphone_lease.generation);
|
upstream_media_rt.close_microphone(microphone_lease.generation);
|
||||||
|
camera_rt.release_if_active(camera_session_id).await;
|
||||||
info!(
|
info!(
|
||||||
rpc_id,
|
rpc_id,
|
||||||
session_id = camera_lease.session_id,
|
session_id = camera_lease.session_id,
|
||||||
|
|||||||
@ -8,10 +8,11 @@ const DEFAULT_HEVC_DOMINANT_BYTE_PCT: u32 = 92;
|
|||||||
const DEFAULT_DIRECT_MJPEG_SIZE_DROP_PCT: u32 = 18;
|
const DEFAULT_DIRECT_MJPEG_SIZE_DROP_PCT: u32 = 18;
|
||||||
const DEFAULT_DIRECT_MJPEG_MIN_REFERENCE_BYTES: u32 = 48 * 1024;
|
const DEFAULT_DIRECT_MJPEG_MIN_REFERENCE_BYTES: u32 = 48 * 1024;
|
||||||
const DEFAULT_DIRECT_MJPEG_PROFILE_MISMATCH_REJECT: bool = false;
|
const DEFAULT_DIRECT_MJPEG_PROFILE_MISMATCH_REJECT: bool = false;
|
||||||
const DEFAULT_DIRECT_MJPEG_NORMALIZE: bool = true;
|
const DEFAULT_DIRECT_MJPEG_NORMALIZE: bool = false;
|
||||||
const DEFAULT_DIRECT_MJPEG_JPEG_QUALITY: u32 = 72;
|
const DEFAULT_DIRECT_MJPEG_JPEG_QUALITY: u32 = 72;
|
||||||
const DEFAULT_DIRECT_MJPEG_NORMALIZE_PULL_TIMEOUT_MS: u32 = 25;
|
const DEFAULT_DIRECT_MJPEG_NORMALIZE_PULL_TIMEOUT_MS: u32 = 25;
|
||||||
const DEFAULT_DIRECT_MJPEG_NORMALIZE_MISS_LIMIT: u32 = 30;
|
const DEFAULT_DIRECT_MJPEG_NORMALIZE_MISS_LIMIT: u32 = 30;
|
||||||
|
const DEFAULT_DIRECT_MJPEG_NORMALIZE_RSS_LIMIT_MB: u32 = 768;
|
||||||
|
|
||||||
/// Summarizes one compressed MJPEG frame without fully decoding pixels.
|
/// Summarizes one compressed MJPEG frame without fully decoding pixels.
|
||||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
@ -192,18 +193,19 @@ pub(super) fn direct_mjpeg_reject_profile_mismatch_enabled() -> bool {
|
|||||||
/// Decide whether direct MJPEG should be normalized before UVC spool.
|
/// Decide whether direct MJPEG should be normalized before UVC spool.
|
||||||
///
|
///
|
||||||
/// Inputs: optional `LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE`. Output: true unless
|
/// Inputs: optional `LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE`. Output: true unless
|
||||||
/// explicitly disabled. Why: Google Meet/Firefox can expose lower-half grey
|
/// explicitly enabled. Why: this path runs native GStreamer JPEG decode/encode
|
||||||
/// slabs from otherwise complete camera JPEGs; a local decode/re-encode gives
|
/// inside `lesavka-server`; it is useful for lab comparisons, but field
|
||||||
/// the RCT a simpler, freshly bounded MJPEG bitstream.
|
/// evidence showed long-running RSS growth from the native allocator/buffer
|
||||||
|
/// pools, so guarded passthrough is the production-safe default.
|
||||||
pub(super) fn direct_mjpeg_normalize_enabled() -> bool {
|
pub(super) fn direct_mjpeg_normalize_enabled() -> bool {
|
||||||
std::env::var("LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE")
|
std::env::var("LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE")
|
||||||
.ok()
|
.ok()
|
||||||
.map(|value| {
|
.map(|value| {
|
||||||
let trimmed = value.trim();
|
let trimmed = value.trim();
|
||||||
!(trimmed.eq_ignore_ascii_case("0")
|
trimmed.eq_ignore_ascii_case("1")
|
||||||
|| trimmed.eq_ignore_ascii_case("false")
|
|| trimmed.eq_ignore_ascii_case("true")
|
||||||
|| trimmed.eq_ignore_ascii_case("no")
|
|| trimmed.eq_ignore_ascii_case("yes")
|
||||||
|| trimmed.eq_ignore_ascii_case("off"))
|
|| trimmed.eq_ignore_ascii_case("on")
|
||||||
})
|
})
|
||||||
.unwrap_or(DEFAULT_DIRECT_MJPEG_NORMALIZE)
|
.unwrap_or(DEFAULT_DIRECT_MJPEG_NORMALIZE)
|
||||||
}
|
}
|
||||||
@ -249,6 +251,21 @@ pub(super) fn direct_mjpeg_normalize_miss_limit() -> u32 {
|
|||||||
.clamp(1, 300)
|
.clamp(1, 300)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve the RSS ceiling for the optional direct-MJPEG normalizer.
|
||||||
|
///
|
||||||
|
/// Inputs: optional `LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_RSS_LIMIT_MB`.
|
||||||
|
/// Output: `None` when set to zero, otherwise a kilobyte ceiling. Why: if an
|
||||||
|
/// operator enables the native JPEG normalizer for lab diagnostics, the server
|
||||||
|
/// should still self-disable that branch before allocator retention threatens
|
||||||
|
/// the Pi.
|
||||||
|
pub(super) fn direct_mjpeg_normalize_rss_limit_kb() -> Option<u64> {
|
||||||
|
let mb = env_u32(
|
||||||
|
"LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_RSS_LIMIT_MB",
|
||||||
|
DEFAULT_DIRECT_MJPEG_NORMALIZE_RSS_LIMIT_MB,
|
||||||
|
);
|
||||||
|
(mb > 0).then_some(u64::from(mb) * 1024)
|
||||||
|
}
|
||||||
|
|
||||||
/// Return whether a decoded buffer looks like one complete JPEG image.
|
/// Return whether a decoded buffer looks like one complete JPEG image.
|
||||||
///
|
///
|
||||||
/// Inputs: decoded MJPEG bytes. Output: true when SOI, SOS, and EOI markers
|
/// Inputs: decoded MJPEG bytes. Output: true when SOI, SOS, and EOI markers
|
||||||
@ -534,7 +551,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn direct_mjpeg_normalization_defaults_on_and_clamps_tuning() {
|
fn direct_mjpeg_normalization_defaults_off_and_clamps_tuning() {
|
||||||
temp_env::with_vars(
|
temp_env::with_vars(
|
||||||
[
|
[
|
||||||
("LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE", None::<&str>),
|
("LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE", None::<&str>),
|
||||||
@ -547,18 +564,26 @@ mod tests {
|
|||||||
"LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_MISS_LIMIT",
|
"LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_MISS_LIMIT",
|
||||||
None::<&str>,
|
None::<&str>,
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_RSS_LIMIT_MB",
|
||||||
|
None::<&str>,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
|| {
|
|| {
|
||||||
assert!(super::direct_mjpeg_normalize_enabled());
|
assert!(!super::direct_mjpeg_normalize_enabled());
|
||||||
assert_eq!(super::direct_mjpeg_jpeg_quality(), 72);
|
assert_eq!(super::direct_mjpeg_jpeg_quality(), 72);
|
||||||
assert_eq!(super::direct_mjpeg_normalize_pull_timeout_ms(), 25);
|
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_miss_limit(), 30);
|
||||||
|
assert_eq!(
|
||||||
|
super::direct_mjpeg_normalize_rss_limit_kb(),
|
||||||
|
Some(768 * 1024)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
temp_env::with_vars(
|
temp_env::with_vars(
|
||||||
[
|
[
|
||||||
("LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE", Some("off")),
|
("LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE", Some("on")),
|
||||||
("LESAVKA_UVC_DIRECT_MJPEG_JPEG_QUALITY", Some("101")),
|
("LESAVKA_UVC_DIRECT_MJPEG_JPEG_QUALITY", Some("101")),
|
||||||
(
|
(
|
||||||
"LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_PULL_TIMEOUT_MS",
|
"LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_PULL_TIMEOUT_MS",
|
||||||
@ -568,12 +593,17 @@ mod tests {
|
|||||||
"LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_MISS_LIMIT",
|
"LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_MISS_LIMIT",
|
||||||
Some("999"),
|
Some("999"),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_RSS_LIMIT_MB",
|
||||||
|
Some("0"),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
|| {
|
|| {
|
||||||
assert!(!super::direct_mjpeg_normalize_enabled());
|
assert!(super::direct_mjpeg_normalize_enabled());
|
||||||
assert_eq!(super::direct_mjpeg_jpeg_quality(), 100);
|
assert_eq!(super::direct_mjpeg_jpeg_quality(), 100);
|
||||||
assert_eq!(super::direct_mjpeg_normalize_pull_timeout_ms(), 50);
|
assert_eq!(super::direct_mjpeg_normalize_pull_timeout_ms(), 50);
|
||||||
assert_eq!(super::direct_mjpeg_normalize_miss_limit(), 300);
|
assert_eq!(super::direct_mjpeg_normalize_miss_limit(), 300);
|
||||||
|
assert_eq!(super::direct_mjpeg_normalize_rss_limit_kb(), None);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -54,6 +54,7 @@ pub struct WebcamSink {
|
|||||||
last_decoded_mjpeg_bytes: AtomicU64,
|
last_decoded_mjpeg_bytes: AtomicU64,
|
||||||
direct_mjpeg_normalize_bypassed: AtomicBool,
|
direct_mjpeg_normalize_bypassed: AtomicBool,
|
||||||
normalized_mjpeg_miss_count: AtomicU64,
|
normalized_mjpeg_miss_count: AtomicU64,
|
||||||
|
normalized_mjpeg_memory_check_count: AtomicU64,
|
||||||
decoded_mjpeg_miss_count: AtomicU64,
|
decoded_mjpeg_miss_count: AtomicU64,
|
||||||
decode_recovery_needs_irap: AtomicBool,
|
decode_recovery_needs_irap: AtomicBool,
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
@ -181,6 +182,15 @@ fn direct_mjpeg_normalize_pull_timeout() -> gst::ClockTime {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
fn current_process_rss_kb() -> Option<u64> {
|
||||||
|
let status = fs::read_to_string("/proc/self/status").ok()?;
|
||||||
|
status.lines().find_map(|line| {
|
||||||
|
let rest = line.strip_prefix("VmRSS:")?;
|
||||||
|
rest.split_whitespace().next()?.parse::<u64>().ok()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Drain normalized direct-MJPEG output down to the freshest sample.
|
/// Drain normalized direct-MJPEG output down to the freshest sample.
|
||||||
///
|
///
|
||||||
/// Inputs: the direct-MJPEG normalization appsink. Output: newest available
|
/// Inputs: the direct-MJPEG normalization appsink. Output: newest available
|
||||||
@ -503,6 +513,7 @@ impl WebcamSink {
|
|||||||
last_decoded_mjpeg_bytes: AtomicU64::new(0),
|
last_decoded_mjpeg_bytes: AtomicU64::new(0),
|
||||||
direct_mjpeg_normalize_bypassed: AtomicBool::new(false),
|
direct_mjpeg_normalize_bypassed: AtomicBool::new(false),
|
||||||
normalized_mjpeg_miss_count: AtomicU64::new(0),
|
normalized_mjpeg_miss_count: AtomicU64::new(0),
|
||||||
|
normalized_mjpeg_memory_check_count: AtomicU64::new(0),
|
||||||
decoded_mjpeg_miss_count: AtomicU64::new(0),
|
decoded_mjpeg_miss_count: AtomicU64::new(0),
|
||||||
decode_recovery_needs_irap: AtomicBool::new(false),
|
decode_recovery_needs_irap: AtomicBool::new(false),
|
||||||
})
|
})
|
||||||
@ -807,6 +818,7 @@ impl WebcamSink {
|
|||||||
last_decoded_mjpeg_bytes: AtomicU64::new(0),
|
last_decoded_mjpeg_bytes: AtomicU64::new(0),
|
||||||
direct_mjpeg_normalize_bypassed: AtomicBool::new(false),
|
direct_mjpeg_normalize_bypassed: AtomicBool::new(false),
|
||||||
normalized_mjpeg_miss_count: AtomicU64::new(0),
|
normalized_mjpeg_miss_count: AtomicU64::new(0),
|
||||||
|
normalized_mjpeg_memory_check_count: AtomicU64::new(0),
|
||||||
decoded_mjpeg_miss_count: AtomicU64::new(0),
|
decoded_mjpeg_miss_count: AtomicU64::new(0),
|
||||||
decode_recovery_needs_irap: AtomicBool::new(false),
|
decode_recovery_needs_irap: AtomicBool::new(false),
|
||||||
_bus_watch: bus_watch,
|
_bus_watch: bus_watch,
|
||||||
@ -1027,6 +1039,26 @@ impl WebcamSink {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if self
|
||||||
|
.normalized_mjpeg_memory_check_count
|
||||||
|
.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
|
||||||
|
.is_multiple_of(150)
|
||||||
|
&& let Some(limit_kb) = hevc_mjpeg_guard::direct_mjpeg_normalize_rss_limit_kb()
|
||||||
|
&& let Some(rss_kb) = current_process_rss_kb()
|
||||||
|
&& rss_kb > limit_kb
|
||||||
|
{
|
||||||
|
self.direct_mjpeg_normalize_bypassed
|
||||||
|
.store(true, std::sync::atomic::Ordering::Relaxed);
|
||||||
|
warn!(
|
||||||
|
target:"lesavka_server::video",
|
||||||
|
rss_kb,
|
||||||
|
limit_kb,
|
||||||
|
"📸⚠️ direct MJPEG normalization disabled because server RSS exceeded its safety limit"
|
||||||
|
);
|
||||||
|
self.spool_passthrough_direct_mjpeg_frame(path, pkt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let buf = gst::Buffer::from_slice(pkt.data.clone());
|
let buf = gst::Buffer::from_slice(pkt.data.clone());
|
||||||
if let Err(err) = src.push_buffer(buf) {
|
if let Err(err) = src.push_buffer(buf) {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
|
|||||||
@ -125,6 +125,8 @@ fn server_hevc_recovery_and_freshest_spool_paths_remain_wired() {
|
|||||||
"direct_mjpeg_normalize_src",
|
"direct_mjpeg_normalize_src",
|
||||||
"direct_mjpeg_normalize_bypassed",
|
"direct_mjpeg_normalize_bypassed",
|
||||||
"mjpeg_normalized",
|
"mjpeg_normalized",
|
||||||
|
"current_process_rss_kb",
|
||||||
|
"direct_mjpeg_normalize_rss_limit_kb",
|
||||||
"should_freeze_decoded_mjpeg_frame(previous_bytes, map.as_slice())",
|
"should_freeze_decoded_mjpeg_frame(previous_bytes, map.as_slice())",
|
||||||
"direct_mjpeg_reject_reason(",
|
"direct_mjpeg_reject_reason(",
|
||||||
"spool_direct_mjpeg_frame",
|
"spool_direct_mjpeg_frame",
|
||||||
|
|||||||
@ -21,6 +21,18 @@ fn default_runtime_starts_without_an_active_generation() {
|
|||||||
assert!(!runtime.is_active(1));
|
assert!(!runtime.is_active(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_if_active_supersedes_only_the_current_generation() {
|
||||||
|
let runtime = CameraRuntime::new();
|
||||||
|
let rt = Runtime::new().expect("runtime");
|
||||||
|
|
||||||
|
assert!(rt.block_on(runtime.release_if_active(0)));
|
||||||
|
assert!(!runtime.is_active(0));
|
||||||
|
assert!(runtime.is_active(1));
|
||||||
|
assert!(!rt.block_on(runtime.release_if_active(0)));
|
||||||
|
assert!(runtime.is_active(1));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
#[serial]
|
||||||
fn activate_rejects_uvc_when_disabled_and_bumps_generation() {
|
fn activate_rejects_uvc_when_disabled_and_bumps_generation() {
|
||||||
|
|||||||
@ -39,6 +39,7 @@ fn server_install_pins_hdmi_camera_and_display_defaults() {
|
|||||||
"LESAVKA_UVC_DIRECT_MJPEG_JPEG_QUALITY=%s",
|
"LESAVKA_UVC_DIRECT_MJPEG_JPEG_QUALITY=%s",
|
||||||
"LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_PULL_TIMEOUT_MS=%s",
|
"LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_PULL_TIMEOUT_MS=%s",
|
||||||
"LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_MISS_LIMIT=%s",
|
"LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_MISS_LIMIT=%s",
|
||||||
|
"LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_RSS_LIMIT_MB=%s",
|
||||||
"LESAVKA_SERVER_BIND_ADDR=%s",
|
"LESAVKA_SERVER_BIND_ADDR=%s",
|
||||||
"/etc/lesavka/uvc.env",
|
"/etc/lesavka/uvc.env",
|
||||||
"LESAVKA_UVC_MAXPACKET=",
|
"LESAVKA_UVC_MAXPACKET=",
|
||||||
@ -173,10 +174,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_SPOOL_PULL_TIMEOUT_MS:-20}"));
|
||||||
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_HEVC_FRESHNESS_QUEUE_BUFFERS:-2}"));
|
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_HEVC_DECODE_MISS_LIMIT:-15}"));
|
||||||
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_JPEG_QUALITY:-72}"));
|
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_DIRECT_MJPEG_JPEG_QUALITY:-72}"));
|
||||||
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_PULL_TIMEOUT_MS:-25}"));
|
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_MISS_LIMIT:-30}"));
|
||||||
|
assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_RSS_LIMIT_MB:-768}"));
|
||||||
assert!(
|
assert!(
|
||||||
SERVER_INSTALL.contains("lesavka_server::video=info"),
|
SERVER_INSTALL.contains("lesavka_server::video=info"),
|
||||||
"server installs should not leave the hot webcam frame path at debug logging by default"
|
"server installs should not leave the hot webcam frame path at debug logging by default"
|
||||||
|
|||||||
@ -87,6 +87,7 @@ fn server_env_persists_runtime_profile_and_tls_settings() {
|
|||||||
"LESAVKA_UVC_HEVC_FREEZE_ON_SIZE_DROP=%s",
|
"LESAVKA_UVC_HEVC_FREEZE_ON_SIZE_DROP=%s",
|
||||||
"LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE=%s",
|
"LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE=%s",
|
||||||
"LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_MISS_LIMIT=%s",
|
"LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_MISS_LIMIT=%s",
|
||||||
|
"LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_RSS_LIMIT_MB=%s",
|
||||||
"LESAVKA_SERVER_BIND_ADDR=%s",
|
"LESAVKA_SERVER_BIND_ADDR=%s",
|
||||||
"LESAVKA_REQUIRE_TLS=%s",
|
"LESAVKA_REQUIRE_TLS=%s",
|
||||||
"LESAVKA_TLS_CLIENT_CA=%s",
|
"LESAVKA_TLS_CLIENT_CA=%s",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user