release: ship lesavka 0.16.3
This commit is contained in:
parent
b0d1bba1eb
commit
bbf799ea42
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.16.2"
|
version = "0.16.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
@ -1686,7 +1686,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.16.2"
|
version = "0.16.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
@ -1698,7 +1698,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.16.2"
|
version = "0.16.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.16.2"
|
version = "0.16.3"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.16.2"
|
version = "0.16.3"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
@ -21,12 +21,13 @@ from `LESAVKA_CLIENT_PKI_SSH_SOURCE` over SSH. Runtime clients require the insta
|
|||||||
| `LESAVKA_AUDIO_DISABLE` | client media capture/playback override |
|
| `LESAVKA_AUDIO_DISABLE` | client media capture/playback override |
|
||||||
| `LESAVKA_AUDIO_GAIN` | client media capture/playback override |
|
| `LESAVKA_AUDIO_GAIN` | client media capture/playback override |
|
||||||
| `LESAVKA_AUDIO_GAIN_CONTROL` | client media capture/playback override |
|
| `LESAVKA_AUDIO_GAIN_CONTROL` | client media capture/playback override |
|
||||||
|
| `LESAVKA_AUDIO_FAIL_ON_IDLE` | server remote-speaker capture debug guardrail; default keeps idle UAC streams open |
|
||||||
| `LESAVKA_AUDIO_INIT_ATTEMPTS` | client media capture/playback override |
|
| `LESAVKA_AUDIO_INIT_ATTEMPTS` | client media capture/playback override |
|
||||||
| `LESAVKA_AUDIO_INIT_DELAY_MS` | client media capture/playback override |
|
| `LESAVKA_AUDIO_INIT_DELAY_MS` | client media capture/playback override |
|
||||||
| `LESAVKA_AUDIO_MIN_PACKETS_PER_SEC` | client media capture/playback override |
|
| `LESAVKA_AUDIO_MIN_PACKETS_PER_SEC` | server remote-speaker capture debug guardrail; unset disables packet-rate hard failures |
|
||||||
| `LESAVKA_AUDIO_SINK` | client media capture/playback override |
|
| `LESAVKA_AUDIO_SINK` | client media capture/playback override |
|
||||||
| `LESAVKA_AUDIO_SOURCE_GRACE_MS` | client media capture/playback override |
|
| `LESAVKA_AUDIO_SOURCE_GRACE_MS` | server remote-speaker capture startup grace before optional watchdog checks |
|
||||||
| `LESAVKA_AUDIO_SOURCE_IDLE_MS` | client media capture/playback override |
|
| `LESAVKA_AUDIO_SOURCE_IDLE_MS` | server remote-speaker capture idle window used only when idle hard-fail is enabled |
|
||||||
| `LESAVKA_BOOL_TEST` | test/build contract variable; not runtime operator config |
|
| `LESAVKA_BOOL_TEST` | test/build contract variable; not runtime operator config |
|
||||||
| `LESAVKA_BREAKOUT_PREVIEW_HEIGHT` | eye preview/video transport override |
|
| `LESAVKA_BREAKOUT_PREVIEW_HEIGHT` | eye preview/video transport override |
|
||||||
| `LESAVKA_BREAKOUT_PREVIEW_MAX_KBIT` | eye preview/video transport override |
|
| `LESAVKA_BREAKOUT_PREVIEW_MAX_KBIT` | eye preview/video transport override |
|
||||||
|
|||||||
@ -10,7 +10,7 @@ bench = false
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.16.2"
|
version = "0.16.3"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
autobins = false
|
autobins = false
|
||||||
|
|
||||||
|
|||||||
@ -367,25 +367,6 @@ impl AudioSourceHealth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(coverage))]
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
struct AudioWatchdogPolicy {
|
|
||||||
startup_grace: Duration,
|
|
||||||
idle_timeout: Duration,
|
|
||||||
min_packets_per_second: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(coverage))]
|
|
||||||
impl AudioWatchdogPolicy {
|
|
||||||
fn from_env() -> Self {
|
|
||||||
Self {
|
|
||||||
startup_grace: env_duration_ms("LESAVKA_AUDIO_SOURCE_GRACE_MS", 3_000),
|
|
||||||
idle_timeout: env_duration_ms("LESAVKA_AUDIO_SOURCE_IDLE_MS", 1_500),
|
|
||||||
min_packets_per_second: env_u64("LESAVKA_AUDIO_MIN_PACKETS_PER_SEC", 20),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
fn env_duration_ms(name: &str, default_ms: u64) -> Duration {
|
fn env_duration_ms(name: &str, default_ms: u64) -> Duration {
|
||||||
Duration::from_millis(env_u64(name, default_ms))
|
Duration::from_millis(env_u64(name, default_ms))
|
||||||
@ -400,8 +381,14 @@ fn env_u64(name: &str, default: u64) -> u64 {
|
|||||||
.unwrap_or(default)
|
.unwrap_or(default)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Watch the remote speaker capture source and fail fast when the USB audio
|
#[cfg(not(coverage))]
|
||||||
/// gadget is open but not producing real-time packets.
|
fn env_bool(name: &str) -> bool {
|
||||||
|
matches!(std::env::var(name).as_deref(), Ok("1" | "true" | "yes" | "on"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Watch the remote speaker capture source without turning normal UAC silence
|
||||||
|
/// into a reconnect loop. Hard-fail idle/cadence checks only when an operator
|
||||||
|
/// explicitly enables those lab guardrails through the documented env knobs.
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
fn spawn_audio_source_watchdog(
|
fn spawn_audio_source_watchdog(
|
||||||
pipeline: gst::Pipeline,
|
pipeline: gst::Pipeline,
|
||||||
@ -409,7 +396,10 @@ fn spawn_audio_source_watchdog(
|
|||||||
tx: tokio::sync::mpsc::Sender<Result<AudioPacket, Status>>,
|
tx: tokio::sync::mpsc::Sender<Result<AudioPacket, Status>>,
|
||||||
alsa_dev: String,
|
alsa_dev: String,
|
||||||
) {
|
) {
|
||||||
let policy = AudioWatchdogPolicy::from_env();
|
let startup_grace = env_duration_ms("LESAVKA_AUDIO_SOURCE_GRACE_MS", 3_000);
|
||||||
|
let idle_timeout = env_duration_ms("LESAVKA_AUDIO_SOURCE_IDLE_MS", 1_500);
|
||||||
|
let fail_on_idle = env_bool("LESAVKA_AUDIO_FAIL_ON_IDLE");
|
||||||
|
let min_packets_per_second = env_u64("LESAVKA_AUDIO_MIN_PACKETS_PER_SEC", 0);
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
loop {
|
loop {
|
||||||
std::thread::sleep(Duration::from_millis(250));
|
std::thread::sleep(Duration::from_millis(250));
|
||||||
@ -418,7 +408,7 @@ fn spawn_audio_source_watchdog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let elapsed = health.elapsed();
|
let elapsed = health.elapsed();
|
||||||
if elapsed < policy.startup_grace {
|
if elapsed < startup_grace {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -426,20 +416,22 @@ fn spawn_audio_source_watchdog(
|
|||||||
let idle_for = health.idle_for();
|
let idle_for = health.idle_for();
|
||||||
let rate = packets as f64 / elapsed.as_secs_f64().max(0.001);
|
let rate = packets as f64 / elapsed.as_secs_f64().max(0.001);
|
||||||
|
|
||||||
let failure = if packets == 0 {
|
let failure = if fail_on_idle && packets == 0 {
|
||||||
Some(format!(
|
Some(format!(
|
||||||
"remote speaker capture produced no audio samples after {} ms on {alsa_dev}",
|
"remote speaker capture produced no audio samples after {} ms on {alsa_dev}",
|
||||||
elapsed.as_millis()
|
elapsed.as_millis()
|
||||||
))
|
))
|
||||||
} else if idle_for >= policy.idle_timeout {
|
} else if fail_on_idle && idle_for >= idle_timeout {
|
||||||
Some(format!(
|
Some(format!(
|
||||||
"remote speaker capture stalled for {} ms on {alsa_dev}",
|
"remote speaker capture stalled for {} ms on {alsa_dev}",
|
||||||
idle_for.as_millis()
|
idle_for.as_millis()
|
||||||
))
|
))
|
||||||
} else if (packets / elapsed.as_secs().max(1)) < policy.min_packets_per_second {
|
} else if min_packets_per_second > 0
|
||||||
|
&& packets / elapsed.as_secs().max(1) < min_packets_per_second
|
||||||
|
{
|
||||||
Some(format!(
|
Some(format!(
|
||||||
"remote speaker capture cadence is too low on {alsa_dev}: {rate:.1} packets/s, expected at least {} packets/s",
|
"remote speaker capture cadence is too low on {alsa_dev}: {rate:.1} packets/s, expected at least {} packets/s",
|
||||||
policy.min_packets_per_second
|
min_packets_per_second
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|||||||
@ -51,6 +51,14 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn speaker_capture_watchdog_does_not_hard_fail_idle_audio_by_default() {
|
||||||
|
assert!(AUDIO_SRC.contains("LESAVKA_AUDIO_FAIL_ON_IDLE"));
|
||||||
|
assert!(AUDIO_SRC.contains("let fail_on_idle = env_bool(\"LESAVKA_AUDIO_FAIL_ON_IDLE\")"));
|
||||||
|
assert!(AUDIO_SRC.contains("env_u64(\"LESAVKA_AUDIO_MIN_PACKETS_PER_SEC\", 0)"));
|
||||||
|
assert!(AUDIO_SRC.contains("remote speaker capture cadence is too low"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
#[serial]
|
||||||
fn start_pipeline_or_reset_starts_empty_pipeline() {
|
fn start_pipeline_or_reset_starts_empty_pipeline() {
|
||||||
|
|||||||
@ -290,6 +290,10 @@ mod server_upstream_media_pairing {
|
|||||||
with_var("LESAVKA_CAPTURE_POWER_UNIT", Some("none"), || {
|
with_var("LESAVKA_CAPTURE_POWER_UNIT", Some("none"), || {
|
||||||
with_var("LESAVKA_DISABLE_UVC", None::<&str>, || {
|
with_var("LESAVKA_DISABLE_UVC", None::<&str>, || {
|
||||||
with_var("LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS", Some("80"), || {
|
with_var("LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS", Some("80"), || {
|
||||||
|
with_var(
|
||||||
|
"LESAVKA_UPSTREAM_CAMERA_STARTUP_GRACE_US",
|
||||||
|
Some("0"),
|
||||||
|
|| {
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
let (_dir, handler) = build_handler_for_tests();
|
let (_dir, handler) = build_handler_for_tests();
|
||||||
let (server, mut cli) = serve_handler(handler).await;
|
let (server, mut cli) = serve_handler(handler).await;
|
||||||
@ -361,6 +365,8 @@ mod server_upstream_media_pairing {
|
|||||||
|
|
||||||
server.abort();
|
server.abort();
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -373,6 +379,10 @@ mod server_upstream_media_pairing {
|
|||||||
with_var("LESAVKA_CAPTURE_POWER_UNIT", Some("none"), || {
|
with_var("LESAVKA_CAPTURE_POWER_UNIT", Some("none"), || {
|
||||||
with_var("LESAVKA_DISABLE_UVC", None::<&str>, || {
|
with_var("LESAVKA_DISABLE_UVC", None::<&str>, || {
|
||||||
with_var("LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS", Some("80"), || {
|
with_var("LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS", Some("80"), || {
|
||||||
|
with_var(
|
||||||
|
"LESAVKA_UPSTREAM_CAMERA_STARTUP_GRACE_US",
|
||||||
|
Some("0"),
|
||||||
|
|| {
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
let (_dir, handler) = build_handler_for_tests();
|
let (_dir, handler) = build_handler_for_tests();
|
||||||
let (server, mut cli) = serve_handler(handler).await;
|
let (server, mut cli) = serve_handler(handler).await;
|
||||||
@ -445,6 +455,8 @@ mod server_upstream_media_pairing {
|
|||||||
|
|
||||||
server.abort();
|
server.abort();
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -140,6 +140,10 @@ mod server_upstream_media_pairing {
|
|||||||
with_var("LESAVKA_CAPTURE_POWER_UNIT", Some("none"), || {
|
with_var("LESAVKA_CAPTURE_POWER_UNIT", Some("none"), || {
|
||||||
with_var("LESAVKA_DISABLE_UVC", None::<&str>, || {
|
with_var("LESAVKA_DISABLE_UVC", None::<&str>, || {
|
||||||
with_var("LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS", Some("80"), || {
|
with_var("LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS", Some("80"), || {
|
||||||
|
with_var(
|
||||||
|
"LESAVKA_UPSTREAM_CAMERA_STARTUP_GRACE_US",
|
||||||
|
Some("0"),
|
||||||
|
|| {
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
let (_dir, handler) = build_handler_for_tests();
|
let (_dir, handler) = build_handler_for_tests();
|
||||||
let (server, mut cli) = serve_handler(handler).await;
|
let (server, mut cli) = serve_handler(handler).await;
|
||||||
@ -178,17 +182,8 @@ mod server_upstream_media_pairing {
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.expect("send unmatched video packet");
|
.expect("send unmatched video packet");
|
||||||
drop(audio_tx);
|
|
||||||
drop(video_tx);
|
drop(video_tx);
|
||||||
|
|
||||||
let audio_ack = tokio::time::timeout(
|
|
||||||
std::time::Duration::from_secs(1),
|
|
||||||
audio_response.message(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("microphone ack timeout")
|
|
||||||
.expect("microphone ack grpc")
|
|
||||||
.expect("microphone ack item");
|
|
||||||
let video_ack = tokio::time::timeout(
|
let video_ack = tokio::time::timeout(
|
||||||
std::time::Duration::from_secs(1),
|
std::time::Duration::from_secs(1),
|
||||||
video_response.message(),
|
video_response.message(),
|
||||||
@ -197,11 +192,22 @@ mod server_upstream_media_pairing {
|
|||||||
.expect("camera ack timeout")
|
.expect("camera ack timeout")
|
||||||
.expect("camera ack grpc")
|
.expect("camera ack grpc")
|
||||||
.expect("camera ack item");
|
.expect("camera ack item");
|
||||||
assert_eq!(audio_ack, Empty {});
|
|
||||||
assert_eq!(video_ack, Empty {});
|
assert_eq!(video_ack, Empty {});
|
||||||
|
drop(audio_tx);
|
||||||
|
let audio_ack = tokio::time::timeout(
|
||||||
|
std::time::Duration::from_secs(1),
|
||||||
|
audio_response.message(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("microphone ack timeout")
|
||||||
|
.expect("microphone ack grpc")
|
||||||
|
.expect("microphone ack item");
|
||||||
|
assert_eq!(audio_ack, Empty {});
|
||||||
|
|
||||||
server.abort();
|
server.abort();
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user