diff --git a/Cargo.lock b/Cargo.lock index de0141a..9d19d66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lesavka_client" -version = "0.21.13" +version = "0.21.14" dependencies = [ "anyhow", "async-stream", @@ -1686,7 +1686,7 @@ dependencies = [ [[package]] name = "lesavka_common" -version = "0.21.13" +version = "0.21.14" dependencies = [ "anyhow", "base64", @@ -1698,7 +1698,7 @@ dependencies = [ [[package]] name = "lesavka_server" -version = "0.21.13" +version = "0.21.14" dependencies = [ "anyhow", "base64", diff --git a/client/Cargo.toml b/client/Cargo.toml index 6008613..a882f47 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,7 +4,7 @@ path = "src/main.rs" [package] name = "lesavka_client" -version = "0.21.13" +version = "0.21.14" edition = "2024" [dependencies] diff --git a/client/src/app/uplink_media/tests/mod.rs b/client/src/app/uplink_media/tests/mod.rs index 6c08262..7cfdfea 100644 --- a/client/src/app/uplink_media/tests/mod.rs +++ b/client/src/app/uplink_media/tests/mod.rs @@ -185,6 +185,17 @@ use super::*; 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. /// /// Inputs: pre-stamped HEVC video plus two nearby audio packets, exactly as diff --git a/client/src/app/uplink_media/video_keyframes.rs b/client/src/app/uplink_media/video_keyframes.rs index a566f9a..4d69aef 100644 --- a/client/src/app/uplink_media/video_keyframes.rs +++ b/client/src/app/uplink_media/video_keyframes.rs @@ -73,6 +73,19 @@ fn bundle_has_hevc_recovery_keyframe(bundle: &UpstreamMediaBundle) -> bool { .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))] /// Resolve whether the active upstream camera codec needs HEVC recovery. /// diff --git a/client/src/app/uplink_media/webcam_media_loop.rs b/client/src/app/uplink_media/webcam_media_loop.rs index 252bea5..f94fd1e 100644 --- a/client/src/app/uplink_media/webcam_media_loop.rs +++ b/client/src/app/uplink_media/webcam_media_loop.rs @@ -239,6 +239,11 @@ impl LesavkaClientApp { } Err(std::sync::mpsc::TrySendError::Disconnected(_)) => break, } + } else { + note_hevc_capture_gap( + recover_hevc_after_drops, + &mut waiting_for_hevc_keyframe, + ); } } }) diff --git a/client/src/input/camera.rs b/client/src/input/camera.rs index f285514..e696215 100644 --- a/client/src/input/camera.rs +++ b/client/src/input/camera.rs @@ -205,17 +205,17 @@ mod tests { ("LESAVKA_CAM_HEVC_KEYFRAME_INTERVAL", None::<&str>), ], || { - assert_eq!(hevc_keyframe_interval(30), 3); - assert_eq!(hevc_keyframe_interval(2), 2); + assert_eq!(hevc_keyframe_interval(30), 1); + assert_eq!(hevc_keyframe_interval(2), 1); }, ); temp_env::with_vars( [ ("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); }, ); } diff --git a/client/src/input/camera/capture_pipeline.rs b/client/src/input/camera/capture_pipeline.rs index d0bd4d8..135bf5b 100644 --- a/client/src/input/camera/capture_pipeline.rs +++ b/client/src/input/camera/capture_pipeline.rs @@ -352,14 +352,14 @@ fn env_flag_enabled(name: &str) -> bool { /// Choose the live HEVC keyframe cadence. /// -/// Inputs: target FPS plus optional `LESAVKA_CAM_KEYFRAME_INTERVAL` and -/// `LESAVKA_CAM_HEVC_KEYFRAME_INTERVAL` overrides. Output: GOP length in -/// frames. Why: the uplink intentionally drops stale HEVC bundles for -/// freshness, so short GOPs keep decoder recovery below a human-visible blink. +/// Inputs: target FPS plus optional `LESAVKA_CAM_HEVC_KEYFRAME_INTERVAL` +/// override. Output: GOP length in frames. Why: the upstream relay is +/// freshness-first and may intentionally discard video; defaulting HEVC to +/// 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 { let fps = fps.max(1); - let generic_default = env_u32("LESAVKA_CAM_KEYFRAME_INTERVAL", fps.min(3)); - env_u32("LESAVKA_CAM_HEVC_KEYFRAME_INTERVAL", generic_default).clamp(1, fps) + env_u32("LESAVKA_CAM_HEVC_KEYFRAME_INTERVAL", 1).clamp(1, fps) } /// Keeps `log_camera_first_packet` explicit because it sits on camera selection, where negotiated profiles must match the server output contract. diff --git a/common/Cargo.toml b/common/Cargo.toml index 8c3a988..0e0e917 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lesavka_common" -version = "0.21.13" +version = "0.21.14" edition = "2024" build = "build.rs" diff --git a/server/Cargo.toml b/server/Cargo.toml index f80d839..165a781 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,7 +10,7 @@ bench = false [package] name = "lesavka_server" -version = "0.21.13" +version = "0.21.14" edition = "2024" autobins = false