From ffd6a0874996724e1f9e45f8f7ecb23a7fcd8581 Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Sat, 2 May 2026 23:45:49 -0300 Subject: [PATCH] fix: preserve UVC format compatibility --- AGENTS.md | 13 ++++++ Cargo.lock | 6 +-- client/Cargo.toml | 2 +- client/src/app/session_lifecycle.rs | 2 +- client/src/input/camera.rs | 40 +++++++++++++++++-- client/src/input/camera/capture_pipeline.rs | 5 ++- common/Cargo.toml | 2 +- server/Cargo.toml | 2 +- .../tests/client_launcher_runtime_contract.rs | 1 + 9 files changed, 61 insertions(+), 12 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 61faec1..d2d115f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. - [ ] 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. + +## 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. diff --git a/Cargo.lock b/Cargo.lock index 412d9a4..6ccc464 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lesavka_client" -version = "0.17.36" +version = "0.17.37" dependencies = [ "anyhow", "async-stream", @@ -1686,7 +1686,7 @@ dependencies = [ [[package]] name = "lesavka_common" -version = "0.17.36" +version = "0.17.37" dependencies = [ "anyhow", "base64", @@ -1698,7 +1698,7 @@ dependencies = [ [[package]] name = "lesavka_server" -version = "0.17.36" +version = "0.17.37" dependencies = [ "anyhow", "base64", diff --git a/client/Cargo.toml b/client/Cargo.toml index 37d8b2b..6400ebd 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,7 +4,7 @@ path = "src/main.rs" [package] name = "lesavka_client" -version = "0.17.36" +version = "0.17.37" edition = "2024" [dependencies] diff --git a/client/src/app/session_lifecycle.rs b/client/src/app/session_lifecycle.rs index 2cc89cb..422043b 100644 --- a/client/src/app/session_lifecycle.rs +++ b/client/src/app/session_lifecycle.rs @@ -237,7 +237,7 @@ impl LesavkaClientApp { width = cfg.width, height = cfg.height, 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(); diff --git a/client/src/input/camera.rs b/client/src/input/camera.rs index adba9fd..611c30c 100644 --- a/client/src/input/camera.rs +++ b/client/src/input/camera.rs @@ -83,6 +83,7 @@ mod tests { ("LESAVKA_CAM_HEIGHT", Some("720")), ("LESAVKA_CAM_FPS", Some("30")), ("LESAVKA_CAM_LOCK_TO_SERVER_PROFILE", None), + ("LESAVKA_CAM_EMIT_UI_PROFILE", None), ], || assert_eq!(resolved_capture_profile(Some(cfg)), (1280, 720, 30)), ); @@ -90,8 +91,8 @@ mod tests { #[test] #[serial] - /// UI-selected launcher quality is the source of truth for the camera uplink. - fn negotiated_output_profile_follows_launcher_quality_by_default() { + /// UVC output must match the gadget profile that browsers negotiate. + fn negotiated_output_profile_matches_server_uvc_contract_by_default() { let cfg = CameraConfig { codec: CameraCodec::Mjpeg, width: 640, @@ -104,6 +105,36 @@ mod tests { ("LESAVKA_CAM_HEIGHT", Some("720")), ("LESAVKA_CAM_FPS", Some("30")), ("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)); @@ -118,8 +149,8 @@ mod tests { #[test] #[serial] - /// Keeps the explicit lab lock available for controlled gadget debugging. - fn explicit_server_profile_lock_keeps_lab_mode_available() { + /// The safety lock wins if both experimental flags are set. + fn explicit_server_profile_lock_wins_over_ui_emission() { let cfg = CameraConfig { codec: CameraCodec::Mjpeg, width: 640, @@ -132,6 +163,7 @@ mod tests { ("LESAVKA_CAM_HEIGHT", Some("720")), ("LESAVKA_CAM_FPS", Some("30")), ("LESAVKA_CAM_LOCK_TO_SERVER_PROFILE", Some("1")), + ("LESAVKA_CAM_EMIT_UI_PROFILE", Some("1")), ], || { let capture_profile = resolved_capture_profile(Some(cfg)); diff --git a/client/src/input/camera/capture_pipeline.rs b/client/src/input/camera/capture_pipeline.rs index d96b725..770f798 100644 --- a/client/src/input/camera/capture_pipeline.rs +++ b/client/src/input/camera/capture_pipeline.rs @@ -291,7 +291,10 @@ fn resolved_output_profile( capture_profile: (u32, u32, u32), ) -> (u32, u32, u32) { 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)) } _ => capture_profile, diff --git a/common/Cargo.toml b/common/Cargo.toml index a1b3fe2..8bd7bc8 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lesavka_common" -version = "0.17.36" +version = "0.17.37" edition = "2024" build = "build.rs" diff --git a/server/Cargo.toml b/server/Cargo.toml index a728539..558d588 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,7 +10,7 @@ bench = false [package] name = "lesavka_server" -version = "0.17.36" +version = "0.17.37" edition = "2024" autobins = false diff --git a/testing/tests/client_launcher_runtime_contract.rs b/testing/tests/client_launcher_runtime_contract.rs index 6a232b1..6e61b50 100644 --- a/testing/tests/client_launcher_runtime_contract.rs +++ b/testing/tests/client_launcher_runtime_contract.rs @@ -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!(CAMERA_SRC.contains("fn resolved_capture_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_H264_KBIT\"")); }