media: auto-heal starved upstream audio
This commit is contained in:
parent
f9058329bc
commit
3aea8b3c16
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.42"
|
version = "0.22.43"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
@ -1686,7 +1686,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.22.42"
|
version = "0.22.43"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
@ -1698,7 +1698,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.22.42"
|
version = "0.22.43"
|
||||||
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.42"
|
version = "0.22.43"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.22.42"
|
version = "0.22.43"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
@ -297,6 +297,7 @@ from `LESAVKA_CLIENT_PKI_SSH_SOURCE` over SSH. Runtime clients require the insta
|
|||||||
| `LESAVKA_UPSTREAM_STARTUP_TIMEOUT_MS` | server upstream startup guard; paired startup must converge before this timeout or fail visibly, defaults to `60000` |
|
| `LESAVKA_UPSTREAM_STARTUP_TIMEOUT_MS` | server upstream startup guard; paired startup must converge before this timeout or fail visibly, defaults to `60000` |
|
||||||
| `LESAVKA_UPSTREAM_STALE_DROP_MS` | server upstream freshness override; late audio/video that miss this budget are dropped instead of silently extending lag, defaults to `80` |
|
| `LESAVKA_UPSTREAM_STALE_DROP_MS` | server upstream freshness override; late audio/video that miss this budget are dropped instead of silently extending lag, defaults to `80` |
|
||||||
| `LESAVKA_UPSTREAM_TIMING_TRACE` | upstream capture/rebase trace override for sync debugging |
|
| `LESAVKA_UPSTREAM_TIMING_TRACE` | upstream capture/rebase trace override for sync debugging |
|
||||||
|
| `LESAVKA_UPSTREAM_V2_AUDIO_STARVATION_HEAL_MS` | v2 bundled webcam watchdog; if video stays live while audio disappears for this long, the server retires the mic epoch and forces a clean bundled reconnect, defaults to `1500`, set `0` to disable |
|
||||||
| `LESAVKA_UPSTREAM_V2_MAX_LIVE_AGE_MS` | v2 bundled webcam freshness ceiling; bundles already older than this are dropped as one unit, defaults to `1000` |
|
| `LESAVKA_UPSTREAM_V2_MAX_LIVE_AGE_MS` | v2 bundled webcam freshness ceiling; bundles already older than this are dropped as one unit, defaults to `1000` |
|
||||||
| `LESAVKA_UPSTREAM_V2_PLAYOUT_DELAY_MS` | v2 optional common playout slack after sync offsets; defaults to `20` and is reduced when needed to protect the live-age budget |
|
| `LESAVKA_UPSTREAM_V2_PLAYOUT_DELAY_MS` | v2 optional common playout slack after sync offsets; defaults to `20` and is reduced when needed to protect the live-age budget |
|
||||||
| `LESAVKA_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US` | server upstream per-UVC-mode output-path map; shipped MJPEG defaults are `1280x720@20=162659,1280x720@30=135090,1920x1080@20=160045,1920x1080@30=127952`; shipped HEVC decode-to-MJPEG defaults are profile-specific under `LESAVKA_UPSTREAM_HEVC_VIDEO_PLAYOUT_MODE_OFFSETS_US` |
|
| `LESAVKA_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US` | server upstream per-UVC-mode output-path map; shipped MJPEG defaults are `1280x720@20=162659,1280x720@30=135090,1920x1080@20=160045,1920x1080@30=127952`; shipped HEVC decode-to-MJPEG defaults are profile-specific under `LESAVKA_UPSTREAM_HEVC_VIDEO_PLAYOUT_MODE_OFFSETS_US` |
|
||||||
@ -314,7 +315,7 @@ from `LESAVKA_CLIENT_PKI_SSH_SOURCE` over SSH. Runtime clients require the insta
|
|||||||
| `LESAVKA_UVC_APP_MAX_TIME_NS` | server UVC appsrc memory guard; defaults to `200000000` ns of queued media |
|
| `LESAVKA_UVC_APP_MAX_TIME_NS` | server UVC appsrc memory guard; defaults to `200000000` ns of queued media |
|
||||||
| `LESAVKA_UVC_BLOCKING` | server hardware/device override |
|
| `LESAVKA_UVC_BLOCKING` | server hardware/device override |
|
||||||
| `LESAVKA_UVC_BULK` | UVC transfer-mode override; defaults to `1` so patched kernels prefer reliable bulk transfer over lossy isochronous MJPEG. If the live kernel does not expose `streaming_bulk`, Lesavka falls back to isochronous descriptors without the 512-byte bulk clamp. Set `0` to force classic isochronous descriptors |
|
| `LESAVKA_UVC_BULK` | UVC transfer-mode override; defaults to `1` so patched kernels prefer reliable bulk transfer over lossy isochronous MJPEG. If the live kernel does not expose `streaming_bulk`, Lesavka falls back to isochronous descriptors without the 512-byte bulk clamp. Set `0` to force classic isochronous descriptors |
|
||||||
| `LESAVKA_UVC_BUFFER_COUNT` | UVC helper freshness override; number of queued gadget output buffers, defaults to `2` for live-call freshness |
|
| `LESAVKA_UVC_BUFFER_COUNT` | UVC helper freshness override; number of queued gadget output buffers, defaults to `4` to keep the gadget fed through host/browser scheduling jitter |
|
||||||
| `LESAVKA_UVC_BY_PATH_ROOT` | server hardware/device override |
|
| `LESAVKA_UVC_BY_PATH_ROOT` | server hardware/device override |
|
||||||
| `LESAVKA_UVC_CODEC` | server hardware/device override |
|
| `LESAVKA_UVC_CODEC` | server hardware/device override |
|
||||||
| `LESAVKA_UVC_CTRL_BIN` | server hardware/device override |
|
| `LESAVKA_UVC_CTRL_BIN` | server hardware/device override |
|
||||||
@ -345,6 +346,7 @@ from `LESAVKA_CLIENT_PKI_SSH_SOURCE` over SSH. Runtime clients require the insta
|
|||||||
| `LESAVKA_UVC_MAXPAYLOAD_LIMIT` | server hardware/device override |
|
| `LESAVKA_UVC_MAXPAYLOAD_LIMIT` | server hardware/device override |
|
||||||
| `LESAVKA_UVC_MJPEG` | 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; non-bulk UVC is additionally capped by `LESAVKA_UVC_ISOCHRONOUS_LIMIT_PCT` |
|
| `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_QUEUE_PACING` | UVC helper queue pacing override; defaults to `0` because the RCT host already paces UVC consumption, and delaying returned buffer requeueing can starve isochronous gadget transfers |
|
||||||
| `LESAVKA_UVC_SKIP_UDEV` | server hardware/device override |
|
| `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_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 |
|
| `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 |
|
||||||
|
|||||||
@ -355,7 +355,7 @@ LESAVKA_UVC_HEIGHT=$(uvc_env_value LESAVKA_UVC_HEIGHT 720)
|
|||||||
LESAVKA_UVC_CODEC=${INSTALL_UVC_CODEC}
|
LESAVKA_UVC_CODEC=${INSTALL_UVC_CODEC}
|
||||||
LESAVKA_UVC_BLOCKING=$(uvc_env_value LESAVKA_UVC_BLOCKING 1)
|
LESAVKA_UVC_BLOCKING=$(uvc_env_value LESAVKA_UVC_BLOCKING 1)
|
||||||
LESAVKA_UVC_CONTROL_READ_ONLY=$(uvc_env_value LESAVKA_UVC_CONTROL_READ_ONLY 0)
|
LESAVKA_UVC_CONTROL_READ_ONLY=$(uvc_env_value LESAVKA_UVC_CONTROL_READ_ONLY 0)
|
||||||
LESAVKA_UVC_QUEUE_PACING=$(uvc_env_value LESAVKA_UVC_QUEUE_PACING 1)
|
LESAVKA_UVC_QUEUE_PACING=$(uvc_env_value LESAVKA_UVC_QUEUE_PACING 0)
|
||||||
LESAVKA_UVC_MAXBURST=$(uvc_env_value LESAVKA_UVC_MAXBURST 0)
|
LESAVKA_UVC_MAXBURST=$(uvc_env_value LESAVKA_UVC_MAXBURST 0)
|
||||||
LESAVKA_UVC_BULK=$(uvc_env_value LESAVKA_UVC_BULK 1)
|
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_SIZE_GUARD=$(uvc_env_value LESAVKA_UVC_FRAME_SIZE_GUARD 1)
|
||||||
|
|||||||
@ -10,7 +10,7 @@ bench = false
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.22.42"
|
version = "0.22.43"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
autobins = false
|
autobins = false
|
||||||
|
|
||||||
|
|||||||
@ -48,7 +48,7 @@ const V4L2_PIX_FMT_MJPEG: u32 = u32::from_le_bytes(*b"MJPG");
|
|||||||
const MAX_MJPEG_FRAME_BYTES: usize = 8 * 1024 * 1024;
|
const MAX_MJPEG_FRAME_BYTES: usize = 8 * 1024 * 1024;
|
||||||
const MINIMAL_MJPEG_FRAME: &[u8] = &[0xff, 0xd8, 0xff, 0xd9];
|
const MINIMAL_MJPEG_FRAME: &[u8] = &[0xff, 0xd8, 0xff, 0xd9];
|
||||||
const IDLE_MJPEG_FRAME: &[u8] = include_bytes!("lesavka_uvc/idle_1280x720_black.jpg");
|
const IDLE_MJPEG_FRAME: &[u8] = include_bytes!("lesavka_uvc/idle_1280x720_black.jpg");
|
||||||
const DEFAULT_UVC_BUFFER_COUNT: u32 = 2;
|
const DEFAULT_UVC_BUFFER_COUNT: u32 = 4;
|
||||||
const DEFAULT_UVC_IDLE_PUMP_MS: u64 = 2;
|
const DEFAULT_UVC_IDLE_PUMP_MS: u64 = 2;
|
||||||
const DEFAULT_UVC_FRAME_MAX_AGE_MS: u64 = 1_000;
|
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_MJPEG_BUDGET_BYTES_PER_SEC: u32 = 10_000_000;
|
||||||
@ -657,7 +657,7 @@ fn uvc_idle_pump_sleep() -> Duration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn uvc_queue_period(fps: u32) -> Option<Duration> {
|
fn uvc_queue_period(fps: u32) -> Option<Duration> {
|
||||||
if !env_flag_enabled("LESAVKA_UVC_QUEUE_PACING", true) {
|
if !env_flag_enabled("LESAVKA_UVC_QUEUE_PACING", false) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let fps = fps.max(1);
|
let fps = fps.max(1);
|
||||||
|
|||||||
@ -50,7 +50,7 @@ const UVC_VS_COMMIT_CONTROL: u8 = 0x02;
|
|||||||
const UVC_VC_REQUEST_ERROR_CODE_CONTROL: u8 = 0x02;
|
const UVC_VC_REQUEST_ERROR_CODE_CONTROL: u8 = 0x02;
|
||||||
|
|
||||||
#[cfg(coverage)]
|
#[cfg(coverage)]
|
||||||
const DEFAULT_UVC_BUFFER_COUNT: u32 = 2;
|
const DEFAULT_UVC_BUFFER_COUNT: u32 = 4;
|
||||||
#[cfg(coverage)]
|
#[cfg(coverage)]
|
||||||
const DEFAULT_UVC_IDLE_PUMP_MS: u64 = 2;
|
const DEFAULT_UVC_IDLE_PUMP_MS: u64 = 2;
|
||||||
#[cfg(coverage)]
|
#[cfg(coverage)]
|
||||||
|
|||||||
@ -159,7 +159,7 @@ fn uvc_idle_pump_sleep() -> std::time::Duration {
|
|||||||
/// `None` when pacing is explicitly disabled. Why: the RCT-facing host must
|
/// `None` when pacing is explicitly disabled. Why: the RCT-facing host must
|
||||||
/// not be overfed faster than the advertised descriptor cadence.
|
/// not be overfed faster than the advertised descriptor cadence.
|
||||||
fn uvc_queue_period(fps: u32) -> Option<std::time::Duration> {
|
fn uvc_queue_period(fps: u32) -> Option<std::time::Duration> {
|
||||||
if !env_flag_enabled("LESAVKA_UVC_QUEUE_PACING", true) {
|
if !env_flag_enabled("LESAVKA_UVC_QUEUE_PACING", false) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let fps = fps.max(1);
|
let fps = fps.max(1);
|
||||||
|
|||||||
@ -135,6 +135,20 @@ fn uvc_frame_budget_caps_isochronous_transport() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn uvc_queue_pacing_defaults_off_but_remains_opt_in() {
|
||||||
|
with_var("LESAVKA_UVC_QUEUE_PACING", None::<&str>, || {
|
||||||
|
assert_eq!(uvc_queue_period(30), None);
|
||||||
|
});
|
||||||
|
|
||||||
|
with_var("LESAVKA_UVC_QUEUE_PACING", Some("1"), || {
|
||||||
|
assert_eq!(
|
||||||
|
uvc_queue_period(30),
|
||||||
|
Some(std::time::Duration::from_nanos(33_333_333))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
#[serial]
|
||||||
fn main_coverage_mode_returns_error_for_non_uvc_node() {
|
fn main_coverage_mode_returns_error_for_non_uvc_node() {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ const MEDIA_V2_DEFAULT_PLAYOUT_DELAY_MS: u64 = 20;
|
|||||||
const MEDIA_V2_DEFAULT_MAX_LIVE_AGE_MS: u64 = 1_000;
|
const MEDIA_V2_DEFAULT_MAX_LIVE_AGE_MS: u64 = 1_000;
|
||||||
const MEDIA_V2_DEFAULT_UAC_START_TIMEOUT_MS: u64 = 750;
|
const MEDIA_V2_DEFAULT_UAC_START_TIMEOUT_MS: u64 = 750;
|
||||||
const MEDIA_V2_DEFAULT_STREAM_IDLE_TIMEOUT_MS: u64 = 5_000;
|
const MEDIA_V2_DEFAULT_STREAM_IDLE_TIMEOUT_MS: u64 = 5_000;
|
||||||
|
const MEDIA_V2_DEFAULT_AUDIO_STARVATION_HEAL_MS: u64 = 1_500;
|
||||||
const MEDIA_V2_MAX_MIXED_CAPTURE_SPAN_US: u64 = 250_000;
|
const MEDIA_V2_MAX_MIXED_CAPTURE_SPAN_US: u64 = 250_000;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
@ -126,6 +127,39 @@ fn media_v2_stream_idle_timeout() -> Duration {
|
|||||||
.unwrap_or_else(|| Duration::from_millis(MEDIA_V2_DEFAULT_STREAM_IDLE_TIMEOUT_MS))
|
.unwrap_or_else(|| Duration::from_millis(MEDIA_V2_DEFAULT_STREAM_IDLE_TIMEOUT_MS))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn media_v2_audio_starvation_heal_after() -> Option<Duration> {
|
||||||
|
let millis = std::env::var("LESAVKA_UPSTREAM_V2_AUDIO_STARVATION_HEAL_MS")
|
||||||
|
.ok()
|
||||||
|
.and_then(|value| value.trim().parse::<u64>().ok())
|
||||||
|
.unwrap_or(MEDIA_V2_DEFAULT_AUDIO_STARVATION_HEAL_MS);
|
||||||
|
(millis > 0).then(|| Duration::from_millis(millis))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Track whether a bundled stream has lost audio while video is still live.
|
||||||
|
///
|
||||||
|
/// Inputs are the latest bundle facts plus the current starvation marker.
|
||||||
|
/// Output is true once the audio-free video run exceeds the heal window. Why:
|
||||||
|
/// a stuck mic capture path can leave the RCT hearing a stale UAC epoch while
|
||||||
|
/// fresh video keeps flowing; retiring the mic generation forces the client to
|
||||||
|
/// rebuild the bundled stream without touching USB descriptors.
|
||||||
|
fn media_v2_audio_starvation_elapsed(
|
||||||
|
audio_enabled: bool,
|
||||||
|
facts: MediaV2BundleFacts,
|
||||||
|
now: tokio::time::Instant,
|
||||||
|
starved_since: &mut Option<tokio::time::Instant>,
|
||||||
|
heal_after: Option<Duration>,
|
||||||
|
) -> bool {
|
||||||
|
if !audio_enabled || !facts.has_video || facts.has_audio {
|
||||||
|
*starved_since = None;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let Some(heal_after) = heal_after else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let since = starved_since.get_or_insert(now);
|
||||||
|
now.saturating_duration_since(*since) >= heal_after
|
||||||
|
}
|
||||||
|
|
||||||
/// Keeps `media_v2_handoff_schedule` explicit because it sits on relay RPC orchestration, where hardware failures must surface without stopping the server.
|
/// Keeps `media_v2_handoff_schedule` explicit because it sits on relay RPC orchestration, where hardware failures must surface without stopping the server.
|
||||||
/// Inputs are the typed parameters; output is the return value or side effect.
|
/// Inputs are the typed parameters; output is the return value or side effect.
|
||||||
fn media_v2_handoff_schedule(
|
fn media_v2_handoff_schedule(
|
||||||
|
|||||||
@ -84,6 +84,8 @@ impl Handler {
|
|||||||
let mut waiting_for_hevc_keyframe = false;
|
let mut waiting_for_hevc_keyframe = false;
|
||||||
let mut outcome = "aborted";
|
let mut outcome = "aborted";
|
||||||
let idle_timeout = media_v2_stream_idle_timeout();
|
let idle_timeout = media_v2_stream_idle_timeout();
|
||||||
|
let audio_starvation_heal_after = media_v2_audio_starvation_heal_after();
|
||||||
|
let mut audio_starved_since = None;
|
||||||
let (mut audio_handoff_tx, audio_worker) =
|
let (mut audio_handoff_tx, audio_worker) =
|
||||||
if let Some((microphone_sink_permit, sink)) = microphone_sink {
|
if let Some((microphone_sink_permit, sink)) = microphone_sink {
|
||||||
let (audio_handoff_tx, audio_handoff_rx) =
|
let (audio_handoff_tx, audio_handoff_rx) =
|
||||||
@ -174,6 +176,29 @@ impl Handler {
|
|||||||
let Some(facts) = summarize_media_v2_bundle(&bundle) else {
|
let Some(facts) = summarize_media_v2_bundle(&bundle) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
if media_v2_audio_starvation_elapsed(
|
||||||
|
audio_enabled,
|
||||||
|
facts,
|
||||||
|
bundle_arrived_at,
|
||||||
|
&mut audio_starved_since,
|
||||||
|
audio_starvation_heal_after,
|
||||||
|
) {
|
||||||
|
let audio_starved_ms = audio_starved_since
|
||||||
|
.map(|since| bundle_arrived_at.saturating_duration_since(since))
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_millis();
|
||||||
|
warn!(
|
||||||
|
rpc_id,
|
||||||
|
session_id = camera_lease.session_id,
|
||||||
|
client_bundle_session_id = bundle.session_id,
|
||||||
|
bundle_seq = bundle.seq,
|
||||||
|
audio_starved_ms,
|
||||||
|
"📦🩺 v2 bundled microphone starved while video stayed live; retiring upstream audio epoch"
|
||||||
|
);
|
||||||
|
upstream_media_rt.soft_recover_microphone();
|
||||||
|
outcome = "audio-starved";
|
||||||
|
break;
|
||||||
|
}
|
||||||
if facts.has_audio && facts.has_video && facts.capture_span_us > MEDIA_V2_MAX_MIXED_CAPTURE_SPAN_US {
|
if facts.has_audio && facts.has_video && facts.capture_span_us > MEDIA_V2_MAX_MIXED_CAPTURE_SPAN_US {
|
||||||
if matches!(camera_cfg.codec, camera::CameraCodec::Hevc) && facts.has_video {
|
if matches!(camera_cfg.codec, camera::CameraCodec::Hevc) && facts.has_video {
|
||||||
waiting_for_hevc_keyframe = true;
|
waiting_for_hevc_keyframe = true;
|
||||||
|
|||||||
@ -2,11 +2,11 @@
|
|||||||
#[allow(clippy::items_after_test_module)]
|
#[allow(clippy::items_after_test_module)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
MediaV2BundleFacts, UpstreamStreamCleanup, media_v2_frame_step_us,
|
MediaV2BundleFacts, UpstreamStreamCleanup, media_v2_audio_starvation_elapsed,
|
||||||
media_v2_handoff_schedule, media_v2_has_hevc_recovery_keyframe,
|
media_v2_audio_starvation_heal_after, media_v2_frame_step_us, media_v2_handoff_schedule,
|
||||||
media_v2_should_hold_hevc_video_for_recovery, prepare_media_v2_audio,
|
media_v2_has_hevc_recovery_keyframe, media_v2_should_hold_hevc_video_for_recovery,
|
||||||
prepare_media_v2_video, retain_freshest_audio_packet, retain_freshest_video_packet,
|
media_v2_uac_start_timeout, prepare_media_v2_audio, prepare_media_v2_video,
|
||||||
summarize_media_v2_bundle, media_v2_uac_start_timeout,
|
retain_freshest_audio_packet, retain_freshest_video_packet, summarize_media_v2_bundle,
|
||||||
};
|
};
|
||||||
use lesavka_common::lesavka::{AudioPacket, UpstreamMediaBundle, VideoPacket};
|
use lesavka_common::lesavka::{AudioPacket, UpstreamMediaBundle, VideoPacket};
|
||||||
use lesavka_server::camera::CameraCodec;
|
use lesavka_server::camera::CameraCodec;
|
||||||
@ -196,6 +196,74 @@ mod tests {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn media_v2_audio_starvation_watchdog_is_short_and_disableable() {
|
||||||
|
temp_env::with_var_unset("LESAVKA_UPSTREAM_V2_AUDIO_STARVATION_HEAL_MS", || {
|
||||||
|
assert_eq!(
|
||||||
|
media_v2_audio_starvation_heal_after(),
|
||||||
|
Some(std::time::Duration::from_millis(1_500))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
temp_env::with_var(
|
||||||
|
"LESAVKA_UPSTREAM_V2_AUDIO_STARVATION_HEAL_MS",
|
||||||
|
Some("0"),
|
||||||
|
|| {
|
||||||
|
assert_eq!(media_v2_audio_starvation_heal_after(), None);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn media_v2_audio_starvation_elapsed_requires_live_video_without_audio() {
|
||||||
|
let mut starved_since = None;
|
||||||
|
let now = tokio::time::Instant::now();
|
||||||
|
let video_only = MediaV2BundleFacts {
|
||||||
|
has_audio: false,
|
||||||
|
has_video: true,
|
||||||
|
capture_start_us: 1_000,
|
||||||
|
capture_end_us: 1_000,
|
||||||
|
capture_span_us: 0,
|
||||||
|
max_queue_age_ms: 0,
|
||||||
|
};
|
||||||
|
let audio_video = MediaV2BundleFacts {
|
||||||
|
has_audio: true,
|
||||||
|
has_video: true,
|
||||||
|
..video_only
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(!media_v2_audio_starvation_elapsed(
|
||||||
|
true,
|
||||||
|
video_only,
|
||||||
|
now,
|
||||||
|
&mut starved_since,
|
||||||
|
Some(std::time::Duration::from_millis(1_500))
|
||||||
|
));
|
||||||
|
assert!(starved_since.is_some());
|
||||||
|
assert!(!media_v2_audio_starvation_elapsed(
|
||||||
|
true,
|
||||||
|
video_only,
|
||||||
|
now + std::time::Duration::from_millis(1_499),
|
||||||
|
&mut starved_since,
|
||||||
|
Some(std::time::Duration::from_millis(1_500))
|
||||||
|
));
|
||||||
|
assert!(media_v2_audio_starvation_elapsed(
|
||||||
|
true,
|
||||||
|
video_only,
|
||||||
|
now + std::time::Duration::from_millis(1_500),
|
||||||
|
&mut starved_since,
|
||||||
|
Some(std::time::Duration::from_millis(1_500))
|
||||||
|
));
|
||||||
|
assert!(!media_v2_audio_starvation_elapsed(
|
||||||
|
true,
|
||||||
|
audio_video,
|
||||||
|
now + std::time::Duration::from_millis(1_600),
|
||||||
|
&mut starved_since,
|
||||||
|
Some(std::time::Duration::from_millis(1_500))
|
||||||
|
));
|
||||||
|
assert!(starved_since.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
/// Keeps server HEVC drop recovery explicit because late-drop freshness can otherwise corrupt decoded video.
|
/// Keeps server HEVC drop recovery explicit because late-drop freshness can otherwise corrupt decoded video.
|
||||||
fn media_v2_hevc_recovery_holds_delta_until_keyframe() {
|
fn media_v2_hevc_recovery_holds_delta_until_keyframe() {
|
||||||
|
|||||||
@ -359,12 +359,9 @@ mod uvc_binary_extra {
|
|||||||
with_var("LESAVKA_UVC_BUFFER_COUNT", None::<&str>, || {
|
with_var("LESAVKA_UVC_BUFFER_COUNT", None::<&str>, || {
|
||||||
with_var("LESAVKA_UVC_IDLE_PUMP_MS", None::<&str>, || {
|
with_var("LESAVKA_UVC_IDLE_PUMP_MS", None::<&str>, || {
|
||||||
with_var("LESAVKA_UVC_FRAME_MAX_AGE_MS", None::<&str>, || {
|
with_var("LESAVKA_UVC_FRAME_MAX_AGE_MS", None::<&str>, || {
|
||||||
assert_eq!(uvc_buffer_count(), 2);
|
assert_eq!(uvc_buffer_count(), 4);
|
||||||
assert_eq!(uvc_idle_pump_sleep(), std::time::Duration::from_millis(2));
|
assert_eq!(uvc_idle_pump_sleep(), std::time::Duration::from_millis(2));
|
||||||
assert_eq!(
|
assert_eq!(uvc_queue_period(30), None);
|
||||||
uvc_queue_period(30),
|
|
||||||
Some(std::time::Duration::from_nanos(33_333_333))
|
|
||||||
);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
frame_spool_max_age(),
|
frame_spool_max_age(),
|
||||||
Some(std::time::Duration::from_millis(1_000))
|
Some(std::time::Duration::from_millis(1_000))
|
||||||
@ -380,10 +377,13 @@ mod uvc_binary_extra {
|
|||||||
with_var("LESAVKA_UVC_BUFFER_COUNT", Some("99"), || {
|
with_var("LESAVKA_UVC_BUFFER_COUNT", Some("99"), || {
|
||||||
with_var("LESAVKA_UVC_IDLE_PUMP_MS", Some("11"), || {
|
with_var("LESAVKA_UVC_IDLE_PUMP_MS", Some("11"), || {
|
||||||
with_var("LESAVKA_UVC_FRAME_MAX_AGE_MS", Some("0"), || {
|
with_var("LESAVKA_UVC_FRAME_MAX_AGE_MS", Some("0"), || {
|
||||||
with_var("LESAVKA_UVC_QUEUE_PACING", Some("0"), || {
|
with_var("LESAVKA_UVC_QUEUE_PACING", Some("1"), || {
|
||||||
assert_eq!(uvc_buffer_count(), 8);
|
assert_eq!(uvc_buffer_count(), 8);
|
||||||
assert_eq!(uvc_idle_pump_sleep(), std::time::Duration::from_millis(11));
|
assert_eq!(uvc_idle_pump_sleep(), std::time::Duration::from_millis(11));
|
||||||
assert_eq!(uvc_queue_period(30), None);
|
assert_eq!(
|
||||||
|
uvc_queue_period(30),
|
||||||
|
Some(std::time::Duration::from_nanos(33_333_333))
|
||||||
|
);
|
||||||
assert_eq!(frame_spool_max_age(), None);
|
assert_eq!(frame_spool_max_age(), None);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user