media: prefer hevc freezes over corruption
This commit is contained in:
parent
d4d83a1b36
commit
c3adeb323f
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.21.13"
|
version = "0.21.14"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
@ -1686,7 +1686,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.21.13"
|
version = "0.21.14"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
@ -1698,7 +1698,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.21.13"
|
version = "0.21.14"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.21.13"
|
version = "0.21.14"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -185,6 +185,17 @@ use super::*;
|
|||||||
assert!(bundle_has_hevc_recovery_keyframe(&keyframe_bundle));
|
assert!(bundle_has_hevc_recovery_keyframe(&keyframe_bundle));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Keeps capture-gap recovery explicit because dropped encoded HEVC samples can corrupt every following delta frame.
|
||||||
|
fn hevc_capture_gap_enters_recovery_until_keyframe() {
|
||||||
|
let mut waiting = false;
|
||||||
|
note_hevc_capture_gap(false, &mut waiting);
|
||||||
|
assert!(!waiting);
|
||||||
|
|
||||||
|
note_hevc_capture_gap(true, &mut waiting);
|
||||||
|
assert!(waiting);
|
||||||
|
}
|
||||||
|
|
||||||
/// Verifies the live uplink queue emits one physically bundled HEVC frame and PCM span.
|
/// Verifies the live uplink queue emits one physically bundled HEVC frame and PCM span.
|
||||||
///
|
///
|
||||||
/// Inputs: pre-stamped HEVC video plus two nearby audio packets, exactly as
|
/// Inputs: pre-stamped HEVC video plus two nearby audio packets, exactly as
|
||||||
|
|||||||
@ -73,6 +73,19 @@ fn bundle_has_hevc_recovery_keyframe(bundle: &UpstreamMediaBundle) -> bool {
|
|||||||
.is_some_and(|video| contains_hevc_irap(&video.data))
|
.is_some_and(|video| contains_hevc_irap(&video.data))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(coverage))]
|
||||||
|
/// Mark HEVC as needing recovery after capture produced a gap.
|
||||||
|
///
|
||||||
|
/// Inputs: whether HEVC recovery applies and the mutable wait flag. Output:
|
||||||
|
/// updated state. Why: a `None` pull from the camera can mean the capture layer
|
||||||
|
/// discarded a stale encoded packet before bundling; after that, sending the
|
||||||
|
/// next predictive frame risks black or block-corrupted browser frames.
|
||||||
|
fn note_hevc_capture_gap(recover_hevc_after_drops: bool, waiting_for_keyframe: &mut bool) {
|
||||||
|
if recover_hevc_after_drops {
|
||||||
|
*waiting_for_keyframe = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
/// Resolve whether the active upstream camera codec needs HEVC recovery.
|
/// Resolve whether the active upstream camera codec needs HEVC recovery.
|
||||||
///
|
///
|
||||||
|
|||||||
@ -239,6 +239,11 @@ impl LesavkaClientApp {
|
|||||||
}
|
}
|
||||||
Err(std::sync::mpsc::TrySendError::Disconnected(_)) => break,
|
Err(std::sync::mpsc::TrySendError::Disconnected(_)) => break,
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
note_hevc_capture_gap(
|
||||||
|
recover_hevc_after_drops,
|
||||||
|
&mut waiting_for_hevc_keyframe,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@ -205,17 +205,17 @@ mod tests {
|
|||||||
("LESAVKA_CAM_HEVC_KEYFRAME_INTERVAL", None::<&str>),
|
("LESAVKA_CAM_HEVC_KEYFRAME_INTERVAL", None::<&str>),
|
||||||
],
|
],
|
||||||
|| {
|
|| {
|
||||||
assert_eq!(hevc_keyframe_interval(30), 3);
|
assert_eq!(hevc_keyframe_interval(30), 1);
|
||||||
assert_eq!(hevc_keyframe_interval(2), 2);
|
assert_eq!(hevc_keyframe_interval(2), 1);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
temp_env::with_vars(
|
temp_env::with_vars(
|
||||||
[
|
[
|
||||||
("LESAVKA_CAM_KEYFRAME_INTERVAL", Some("5")),
|
("LESAVKA_CAM_KEYFRAME_INTERVAL", Some("5")),
|
||||||
("LESAVKA_CAM_HEVC_KEYFRAME_INTERVAL", Some("1")),
|
("LESAVKA_CAM_HEVC_KEYFRAME_INTERVAL", Some("2")),
|
||||||
],
|
],
|
||||||
|| {
|
|| {
|
||||||
assert_eq!(hevc_keyframe_interval(30), 1);
|
assert_eq!(hevc_keyframe_interval(30), 2);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -352,14 +352,14 @@ fn env_flag_enabled(name: &str) -> bool {
|
|||||||
|
|
||||||
/// Choose the live HEVC keyframe cadence.
|
/// Choose the live HEVC keyframe cadence.
|
||||||
///
|
///
|
||||||
/// Inputs: target FPS plus optional `LESAVKA_CAM_KEYFRAME_INTERVAL` and
|
/// Inputs: target FPS plus optional `LESAVKA_CAM_HEVC_KEYFRAME_INTERVAL`
|
||||||
/// `LESAVKA_CAM_HEVC_KEYFRAME_INTERVAL` overrides. Output: GOP length in
|
/// override. Output: GOP length in frames. Why: the upstream relay is
|
||||||
/// frames. Why: the uplink intentionally drops stale HEVC bundles for
|
/// freshness-first and may intentionally discard video; defaulting HEVC to
|
||||||
/// freshness, so short GOPs keep decoder recovery below a human-visible blink.
|
/// all-intra is less compression-efficient, but it turns packet loss into a
|
||||||
|
/// freeze/stutter instead of browser-visible block corruption.
|
||||||
fn hevc_keyframe_interval(fps: u32) -> u32 {
|
fn hevc_keyframe_interval(fps: u32) -> u32 {
|
||||||
let fps = fps.max(1);
|
let fps = fps.max(1);
|
||||||
let generic_default = env_u32("LESAVKA_CAM_KEYFRAME_INTERVAL", fps.min(3));
|
env_u32("LESAVKA_CAM_HEVC_KEYFRAME_INTERVAL", 1).clamp(1, fps)
|
||||||
env_u32("LESAVKA_CAM_HEVC_KEYFRAME_INTERVAL", generic_default).clamp(1, fps)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Keeps `log_camera_first_packet` explicit because it sits on camera selection, where negotiated profiles must match the server output contract.
|
/// Keeps `log_camera_first_packet` explicit because it sits on camera selection, where negotiated profiles must match the server output contract.
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.21.13"
|
version = "0.21.14"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ bench = false
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.21.13"
|
version = "0.21.14"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
autobins = false
|
autobins = false
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user