fix: preserve UVC format compatibility

This commit is contained in:
Brad Stein 2026-05-02 23:45:49 -03:00
parent 4dea0407b8
commit ffd6a08749
9 changed files with 61 additions and 12 deletions

View File

@ -674,3 +674,16 @@ downstream appsrc dropping.
- [x] Stop default downstream appsrc leaking on the UAC speech path; shredded chunks are worse than modest added latency for calls. - [x] Stop default downstream appsrc leaking on the UAC speech path; shredded chunks are worse than modest added latency for calls.
- [ ] Reinstall/restart Theia services so `/etc/lesavka/uvc.env` is refreshed from `640x480 @ 20fps` to `1280x720 @ 30fps`. - [ ] Reinstall/restart Theia services so `/etc/lesavka/uvc.env` is refreshed from `640x480 @ 20fps` to `1280x720 @ 30fps`.
- [ ] Re-run manual Google Meet before trusting mirrored probe calibration; verify speech is intelligible and video cadence is stable by eye. - [ ] Re-run manual Google Meet before trusting mirrored probe calibration; verify speech is intelligible and video cadence is stable by eye.
## 0.17.37 UVC Format Compatibility Checklist
Context: after 0.17.36, Google Meet showed `Video Format Not Supported`. The client correctly
captured the UI-selected `720p@30` profile, but it emitted those frames into a server UVC gadget still
advertising `640x480 @ 20fps`. USB camera consumers require advertised caps and frame payloads to
match; otherwise the feed is rejected before we can evaluate smoothness or sync.
- [x] Preserve UI-selected capture quality as the source capture profile.
- [x] Restore safe default UVC emission to the negotiated server gadget profile so browsers see frames matching the camera format they negotiated.
- [x] Keep `LESAVKA_CAM_EMIT_UI_PROFILE=1` as an explicit lab-only opt-in until the server can reconfigure the UVC gadget from the UI/session profile.
- [x] Keep `LESAVKA_CAM_LOCK_TO_SERVER_PROFILE=1` as a safety override that wins over experimental UI-profile emission.
- [ ] Add a real server-side UVC profile reconfigure path before making UI-selected quality drive the gadget-advertised output format.

6
Cargo.lock generated
View File

@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]] [[package]]
name = "lesavka_client" name = "lesavka_client"
version = "0.17.36" version = "0.17.37"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-stream", "async-stream",
@ -1686,7 +1686,7 @@ dependencies = [
[[package]] [[package]]
name = "lesavka_common" name = "lesavka_common"
version = "0.17.36" version = "0.17.37"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
@ -1698,7 +1698,7 @@ dependencies = [
[[package]] [[package]]
name = "lesavka_server" name = "lesavka_server"
version = "0.17.36" version = "0.17.37"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",

View File

@ -4,7 +4,7 @@ path = "src/main.rs"
[package] [package]
name = "lesavka_client" name = "lesavka_client"
version = "0.17.36" version = "0.17.37"
edition = "2024" edition = "2024"
[dependencies] [dependencies]

View File

@ -237,7 +237,7 @@ impl LesavkaClientApp {
width = cfg.width, width = cfg.width,
height = cfg.height, height = cfg.height,
fps = cfg.fps, fps = cfg.fps,
"📸 using server camera caps as codec/fallback; launcher camera quality remains authoritative" "📸 using negotiated server UVC caps for emitted format; launcher quality still controls local capture"
); );
} }
let ep = vid_ep.clone(); let ep = vid_ep.clone();

View File

@ -83,6 +83,7 @@ mod tests {
("LESAVKA_CAM_HEIGHT", Some("720")), ("LESAVKA_CAM_HEIGHT", Some("720")),
("LESAVKA_CAM_FPS", Some("30")), ("LESAVKA_CAM_FPS", Some("30")),
("LESAVKA_CAM_LOCK_TO_SERVER_PROFILE", None), ("LESAVKA_CAM_LOCK_TO_SERVER_PROFILE", None),
("LESAVKA_CAM_EMIT_UI_PROFILE", None),
], ],
|| assert_eq!(resolved_capture_profile(Some(cfg)), (1280, 720, 30)), || assert_eq!(resolved_capture_profile(Some(cfg)), (1280, 720, 30)),
); );
@ -90,8 +91,8 @@ mod tests {
#[test] #[test]
#[serial] #[serial]
/// UI-selected launcher quality is the source of truth for the camera uplink. /// UVC output must match the gadget profile that browsers negotiate.
fn negotiated_output_profile_follows_launcher_quality_by_default() { fn negotiated_output_profile_matches_server_uvc_contract_by_default() {
let cfg = CameraConfig { let cfg = CameraConfig {
codec: CameraCodec::Mjpeg, codec: CameraCodec::Mjpeg,
width: 640, width: 640,
@ -104,6 +105,36 @@ mod tests {
("LESAVKA_CAM_HEIGHT", Some("720")), ("LESAVKA_CAM_HEIGHT", Some("720")),
("LESAVKA_CAM_FPS", Some("30")), ("LESAVKA_CAM_FPS", Some("30")),
("LESAVKA_CAM_LOCK_TO_SERVER_PROFILE", None), ("LESAVKA_CAM_LOCK_TO_SERVER_PROFILE", None),
("LESAVKA_CAM_EMIT_UI_PROFILE", None),
],
|| {
let capture_profile = resolved_capture_profile(Some(cfg));
assert_eq!(capture_profile, (1280, 720, 30));
assert_eq!(
resolved_output_profile(Some(cfg), capture_profile),
(640, 480, 20)
);
},
);
}
#[test]
#[serial]
/// Keeps UI-profile emission explicit until the server can reconfigure UVC.
fn explicit_ui_profile_emission_keeps_lab_mode_available() {
let cfg = CameraConfig {
codec: CameraCodec::Mjpeg,
width: 640,
height: 480,
fps: 20,
};
temp_env::with_vars(
[
("LESAVKA_CAM_WIDTH", Some("1280")),
("LESAVKA_CAM_HEIGHT", Some("720")),
("LESAVKA_CAM_FPS", Some("30")),
("LESAVKA_CAM_LOCK_TO_SERVER_PROFILE", None),
("LESAVKA_CAM_EMIT_UI_PROFILE", Some("1")),
], ],
|| { || {
let capture_profile = resolved_capture_profile(Some(cfg)); let capture_profile = resolved_capture_profile(Some(cfg));
@ -118,8 +149,8 @@ mod tests {
#[test] #[test]
#[serial] #[serial]
/// Keeps the explicit lab lock available for controlled gadget debugging. /// The safety lock wins if both experimental flags are set.
fn explicit_server_profile_lock_keeps_lab_mode_available() { fn explicit_server_profile_lock_wins_over_ui_emission() {
let cfg = CameraConfig { let cfg = CameraConfig {
codec: CameraCodec::Mjpeg, codec: CameraCodec::Mjpeg,
width: 640, width: 640,
@ -132,6 +163,7 @@ mod tests {
("LESAVKA_CAM_HEIGHT", Some("720")), ("LESAVKA_CAM_HEIGHT", Some("720")),
("LESAVKA_CAM_FPS", Some("30")), ("LESAVKA_CAM_FPS", Some("30")),
("LESAVKA_CAM_LOCK_TO_SERVER_PROFILE", Some("1")), ("LESAVKA_CAM_LOCK_TO_SERVER_PROFILE", Some("1")),
("LESAVKA_CAM_EMIT_UI_PROFILE", Some("1")),
], ],
|| { || {
let capture_profile = resolved_capture_profile(Some(cfg)); let capture_profile = resolved_capture_profile(Some(cfg));

View File

@ -291,7 +291,10 @@ fn resolved_output_profile(
capture_profile: (u32, u32, u32), capture_profile: (u32, u32, u32),
) -> (u32, u32, u32) { ) -> (u32, u32, u32) {
match cfg { match cfg {
Some(cfg) if env_flag_enabled("LESAVKA_CAM_LOCK_TO_SERVER_PROFILE") => { Some(cfg)
if env_flag_enabled("LESAVKA_CAM_LOCK_TO_SERVER_PROFILE")
|| !env_flag_enabled("LESAVKA_CAM_EMIT_UI_PROFILE") =>
{
(cfg.width, cfg.height, cfg.fps.max(1)) (cfg.width, cfg.height, cfg.fps.max(1))
} }
_ => capture_profile, _ => capture_profile,

View File

@ -1,6 +1,6 @@
[package] [package]
name = "lesavka_common" name = "lesavka_common"
version = "0.17.36" version = "0.17.37"
edition = "2024" edition = "2024"
build = "build.rs" build = "build.rs"

View File

@ -10,7 +10,7 @@ bench = false
[package] [package]
name = "lesavka_server" name = "lesavka_server"
version = "0.17.36" version = "0.17.37"
edition = "2024" edition = "2024"
autobins = false autobins = false

View File

@ -171,6 +171,7 @@ fn launcher_webcam_quality_selection_reaches_preview_and_relay_env() {
assert!(DEVICE_TEST_SRC.contains("capsfilter caps=\\\"video/x-raw")); assert!(DEVICE_TEST_SRC.contains("capsfilter caps=\\\"video/x-raw"));
assert!(CAMERA_SRC.contains("fn resolved_capture_profile")); assert!(CAMERA_SRC.contains("fn resolved_capture_profile"));
assert!(CAMERA_SRC.contains("LESAVKA_CAM_LOCK_TO_SERVER_PROFILE")); assert!(CAMERA_SRC.contains("LESAVKA_CAM_LOCK_TO_SERVER_PROFILE"));
assert!(CAMERA_SRC.contains("LESAVKA_CAM_EMIT_UI_PROFILE"));
assert!(LAUNCHER_MOD_SRC.contains("\"LESAVKA_CAM_WIDTH\"")); assert!(LAUNCHER_MOD_SRC.contains("\"LESAVKA_CAM_WIDTH\""));
assert!(LAUNCHER_MOD_SRC.contains("\"LESAVKA_CAM_H264_KBIT\"")); assert!(LAUNCHER_MOD_SRC.contains("\"LESAVKA_CAM_H264_KBIT\""));
} }