From da7a49bc8c5c94b2bba33835da36a0dd6b54114e Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Sat, 9 May 2026 23:26:28 -0300 Subject: [PATCH] launcher: polish relay controls --- Cargo.lock | 6 +- client/Cargo.toml | 2 +- client/src/app_support.rs | 5 ++ client/src/launcher/state/selection_models.rs | 9 +-- client/src/launcher/tests/ui_runtime.rs | 45 +++++++++++ client/src/launcher/ui/runtime_poll.rs | 40 +--------- .../ui/runtime_poll/runtime_monitor_tick.rs | 40 ++++++++++ .../ui_components/build_operations_rail.rs | 3 +- common/Cargo.toml | 2 +- docs/operational-env.md | 18 ++++- scripts/ci/hygiene_gate_baseline.json | 79 +++++++++++-------- server/Cargo.toml | 2 +- server/src/main/relay_service.rs | 5 ++ server/src/main/relay_service_tests.rs | 2 +- server/src/video_sinks.rs | 4 +- testing/tests/client_app_include_contract.rs | 4 +- 16 files changed, 176 insertions(+), 90 deletions(-) create mode 100644 client/src/launcher/ui/runtime_poll/runtime_monitor_tick.rs diff --git a/Cargo.lock b/Cargo.lock index 4387f49..43d867f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lesavka_client" -version = "0.21.16" +version = "0.21.17" dependencies = [ "anyhow", "async-stream", @@ -1686,7 +1686,7 @@ dependencies = [ [[package]] name = "lesavka_common" -version = "0.21.16" +version = "0.21.17" dependencies = [ "anyhow", "base64", @@ -1698,7 +1698,7 @@ dependencies = [ [[package]] name = "lesavka_server" -version = "0.21.16" +version = "0.21.17" dependencies = [ "anyhow", "base64", diff --git a/client/Cargo.toml b/client/Cargo.toml index c2ddcaf..b7bf6fe 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,7 +4,7 @@ path = "src/main.rs" [package] name = "lesavka_client" -version = "0.21.16" +version = "0.21.17" edition = "2024" [dependencies] diff --git a/client/src/app_support.rs b/client/src/app_support.rs index 75dc69d..7296df1 100644 --- a/client/src/app_support.rs +++ b/client/src/app_support.rs @@ -49,6 +49,11 @@ pub fn next_delay(current: Duration) -> Duration { } } +/// Parse the camera codec string used by launcher and server negotiation. +/// +/// Inputs: operator/env codec text. Output: the supported transport codec when +/// recognized. Why: the client must not silently fall back to a differently +/// calibrated upstream path when the UI asks for HEVC or MJPEG. fn parse_camera_codec(raw: &str) -> Option { match raw.trim().to_ascii_lowercase().as_str() { "mjpeg" | "mjpg" | "jpeg" => Some(CameraCodec::Mjpeg), diff --git a/client/src/launcher/state/selection_models.rs b/client/src/launcher/state/selection_models.rs index a3309a5..9073566 100644 --- a/client/src/launcher/state/selection_models.rs +++ b/client/src/launcher/state/selection_models.rs @@ -27,8 +27,9 @@ impl InputRouting { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum WebcamTransport { + #[default] Hevc, Mjpeg, } @@ -81,12 +82,6 @@ impl WebcamTransport { } } -impl Default for WebcamTransport { - fn default() -> Self { - Self::Hevc - } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum ViewMode { Unified, diff --git a/client/src/launcher/tests/ui_runtime.rs b/client/src/launcher/tests/ui_runtime.rs index d5d7d55..c3363b1 100644 --- a/client/src/launcher/tests/ui_runtime.rs +++ b/client/src/launcher/tests/ui_runtime.rs @@ -235,6 +235,51 @@ fn populated_launcher_runtime_widgets_stay_compact() { ); } +#[gtk::test] +#[serial] +fn relay_connect_button_aligns_with_recovery_video_column() { + if gtk::gdk::Display::default().is_none() { + return; + } + + let app = gtk::Application::builder() + .application_id("dev.lesavka.test-relay-row-alignment") + .build(); + let _ = app.register(None::<>k::gio::Cancellable>); + + let view = build_launcher_view( + &app, + "https://38.28.125.112:50051", + &realistic_device_catalog(), + &LauncherState::new(), + ); + present_and_settle(&view.window); + + let connect_bounds = view + .widgets + .start_button + .compute_bounds(&view.window) + .expect("connect button bounds"); + let video_bounds = view + .widgets + .uvc_recover_button + .compute_bounds(&view.window) + .expect("video recovery button bounds"); + + assert!( + (connect_bounds.x() - video_bounds.x()).abs() <= 8.0, + "Connect should visually align with the Video recovery column: connect_x={} video_x={}", + connect_bounds.x(), + video_bounds.x() + ); + assert!( + connect_bounds.width() >= video_bounds.width() - 2.0, + "Connect should be at least as stable as the Video recovery button: connect_w={} video_w={}", + connect_bounds.width(), + video_bounds.width() + ); +} + #[gtk::test] #[serial] fn launcher_shell_installs_native_window_chrome() { diff --git a/client/src/launcher/ui/runtime_poll.rs b/client/src/launcher/ui/runtime_poll.rs index ef3ed1d..04843a7 100644 --- a/client/src/launcher/ui/runtime_poll.rs +++ b/client/src/launcher/ui/runtime_poll.rs @@ -458,45 +458,7 @@ next_diagnostics_probe.set(now + Duration::from_secs(2)); } - if now >= next_diagnostics_sample.get() { - let network = diagnostics_network.borrow_mut().snapshot(); - let uplink = - crate::uplink_telemetry::load_uplink_telemetry(&uplink_telemetry_path); - let client_process_cpu_pct = diagnostics_process - .borrow_mut() - .sample_percent() - .unwrap_or(0.0); - record_diagnostics_sample( - &widgets, - &state.borrow(), - preview.as_ref().map(|preview| preview.as_ref()), - uplink.as_ref(), - network, - client_process_cpu_pct, - ); - next_diagnostics_sample.set(now + Duration::from_secs(1)); - } - - let (camera_probe_active, camera_label, mic_probe_active) = { - let state = state.borrow(); - ( - state.channels.camera && state.devices.camera.is_some(), - state.devices.camera.clone(), - state.channels.microphone && state.devices.microphone.is_some(), - ) - }; - if let Err(err) = tests.borrow_mut().sync_relay_uplink_probe( - child_running, - camera_probe_active, - camera_label.as_deref(), - &camera_preview_path, - mic_probe_active, - &mic_level_path, - ) { - widgets - .status_label - .set_text(&format!("Local uplink monitor could not start: {err}")); - } + include!("runtime_poll/runtime_monitor_tick.rs"); refresh_launcher_ui(&widgets, &state.borrow(), child_running); if child_running diff --git a/client/src/launcher/ui/runtime_poll/runtime_monitor_tick.rs b/client/src/launcher/ui/runtime_poll/runtime_monitor_tick.rs new file mode 100644 index 0000000..92a0fa9 --- /dev/null +++ b/client/src/launcher/ui/runtime_poll/runtime_monitor_tick.rs @@ -0,0 +1,40 @@ +{ + if now >= next_diagnostics_sample.get() { + let network = diagnostics_network.borrow_mut().snapshot(); + let uplink = crate::uplink_telemetry::load_uplink_telemetry(&uplink_telemetry_path); + let client_process_cpu_pct = diagnostics_process + .borrow_mut() + .sample_percent() + .unwrap_or(0.0); + record_diagnostics_sample( + &widgets, + &state.borrow(), + preview.as_ref().map(|preview| preview.as_ref()), + uplink.as_ref(), + network, + client_process_cpu_pct, + ); + next_diagnostics_sample.set(now + Duration::from_secs(1)); + } + + let (camera_probe_active, camera_label, mic_probe_active) = { + let state = state.borrow(); + ( + state.channels.camera && state.devices.camera.is_some(), + state.devices.camera.clone(), + state.channels.microphone && state.devices.microphone.is_some(), + ) + }; + if let Err(err) = tests.borrow_mut().sync_relay_uplink_probe( + child_running, + camera_probe_active, + camera_label.as_deref(), + &camera_preview_path, + mic_probe_active, + &mic_level_path, + ) { + widgets + .status_label + .set_text(&format!("Local uplink monitor could not start: {err}")); + } +} diff --git a/client/src/launcher/ui_components/build_operations_rail.rs b/client/src/launcher/ui_components/build_operations_rail.rs index 003c397..a12cb80 100644 --- a/client/src/launcher/ui_components/build_operations_rail.rs +++ b/client/src/launcher/ui_components/build_operations_rail.rs @@ -10,7 +10,7 @@ )); let relay_grid = gtk::Grid::new(); relay_grid.set_column_homogeneous(false); - relay_grid.set_column_spacing(6); + relay_grid.set_column_spacing(8); relay_grid.set_hexpand(true); relay_grid.set_row_spacing(8); relay_grid.attach(&server_entry, 0, 0, 1, 1); @@ -23,7 +23,6 @@ let start_button = rail_button("Connect", "Start or stop relay."); start_button.add_css_class("suggested-action"); start_button.set_hexpand(false); - stabilize_button(&start_button, 76); relay_grid.attach(&start_button, 2, 0, 1, 1); connection_body.append(&relay_grid); diff --git a/common/Cargo.toml b/common/Cargo.toml index abc73f5..3b4970e 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lesavka_common" -version = "0.21.16" +version = "0.21.17" edition = "2024" build = "build.rs" diff --git a/docs/operational-env.md b/docs/operational-env.md index bdd2084..f5a25f1 100644 --- a/docs/operational-env.md +++ b/docs/operational-env.md @@ -44,6 +44,7 @@ from `LESAVKA_CLIENT_PKI_SSH_SOURCE` over SSH. Runtime clients require the insta | `LESAVKA_CAM_FPS` | client media capture/playback override | | `LESAVKA_CAM_H264_KBIT` | client media capture/playback override | | `LESAVKA_CAM_HEIGHT` | client media capture/playback override | +| `LESAVKA_CAM_HEVC_KEYFRAME_INTERVAL` | client HEVC upstream keyframe cadence in frames; defaults to `1` so freshness-first drops freeze/stutter instead of producing predictive-frame corruption | | `LESAVKA_CAM_JPEG_QUALITY` | client media capture/playback override | | `LESAVKA_CAM_KEYFRAME_INTERVAL` | client media capture/playback override | | `LESAVKA_CAM_MJPG` | client media capture/playback override | @@ -312,6 +313,7 @@ from `LESAVKA_CLIENT_PKI_SSH_SOURCE` over SSH. Runtime clients require the insta | `LESAVKA_UVC_FRAME_META_LOG_PATH` | UVC helper diagnostic override; when set with `LESAVKA_UVC_FRAME_META=1`, append every MJPEG spool timing record as JSONL for full-probe HEVC/RCT correlation; summarize with `scripts/manual/summarize_uvc_frame_meta_log.py` | | `LESAVKA_UVC_FRAME_META_PATH` | UVC helper diagnostic override; explicit path for the optional MJPEG spool metadata sidecar | | `LESAVKA_UVC_FRAME_MAX_AGE_MS` | UVC helper freshness override; stale spooled MJPEG frames older than this are not replayed, defaults to `1000`; `0` disables TTL | +| `LESAVKA_UVC_FRAME_MAX_BYTES` | UVC helper MJPEG frame-size guard; explicit maximum accepted frame bytes, where `0` disables the guard and otherwise oversized frames are frozen out | | `LESAVKA_UVC_FRAME_SIZE` | server hardware/device override | | `LESAVKA_UVC_HEIGHT` | server hardware/device override | | `LESAVKA_UVC_HEVC_SPOOL_PULL_TIMEOUT_MS` | server HEVC decode-to-MJPEG freshness override; appsink pull wait for decoded MJPEG handoff before publishing newest frame to the UVC helper, defaults to `5` and is capped at `50` | @@ -322,6 +324,7 @@ from `LESAVKA_CLIENT_PKI_SSH_SOURCE` over SSH. Runtime clients require the insta | `LESAVKA_UVC_MAXPACKET` | server hardware/device override | | `LESAVKA_UVC_MAXPAYLOAD_LIMIT` | 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 | | `LESAVKA_UVC_SKIP_UDEV` | server hardware/device override | | `LESAVKA_UVC_STREAMING_INTERVAL` | server hardware/device override | | `LESAVKA_UVC_STREAM_INTF` | server hardware/device override | @@ -399,6 +402,7 @@ These entries are intentionally concise because most are manual lab or CI harnes | `LESAVKA_HEVC_REENTRY_WAIT_INTERVAL_SECONDS` | manual HEVC re-entry helper retry interval while waiting for SSH after a lab host outage, defaults to `15` | | `LESAVKA_HEVC_REENTRY_WAIT_SECONDS` | manual HEVC re-entry helper reachability wait budget; when greater than `0`, polls SSH before status/build/deploy/reconfigure instead of failing immediately | | `LESAVKA_INSTALL_CAM_CODEC` | server installer camera ingress codec default; persists `LESAVKA_CAM_CODEC` for installed services, defaults to `hevc` | +| `LESAVKA_INSTALL_SOURCE` | install script source selector; use `ref` to fetch the requested git ref instead of building the existing local checkout | | `LESAVKA_INSTALL_UVC_FRAME_META` | server installer diagnostic toggle; persists `LESAVKA_UVC_FRAME_META`, defaults to `0` so spool metadata is opt-in | | `LESAVKA_INSTALL_UVC_FRAME_META_LOG_PATH` | server installer diagnostic path; persists `LESAVKA_UVC_FRAME_META_LOG_PATH`, defaults to `/tmp/lesavka-uvc-frame-meta.jsonl` for optional client-to-RCT spool-boundary fetches | | `LESAVKA_INSTALL_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US` | installer default override; seeds server calibration env files with known lab-measured output-path offsets | @@ -418,6 +422,13 @@ These entries are intentionally concise because most are manual lab or CI harnes | `LESAVKA_MIC_PULSE_LATENCY_TIME_US` | client microphone capture override; tunes Pulse/PipeWire packet sizing, buffering, or selected source behavior | | `LESAVKA_MODE` | manual probe/server connection override used to resolve the target Lesavka server and mode under test | | `LESAVKA_OPEN_MANUAL_REVIEW_DOLPHIN` | manual browser-stimulus probe override; controls local review browser behavior for mirrored upstream A/V tests | +| `LESAVKA_GOOGLE_MEET_URL` | manual Google Meet observer probe URL; recorded in artifacts and optionally opened locally without storing credentials | +| `LESAVKA_MEET_ANALYSIS_REQUIRED` | manual Google Meet observer probe guard; when true, fail if no observer capture is supplied for analysis | +| `LESAVKA_MEET_LOCAL_REVIEW` | manual Google Meet observer probe toggle for local post-run review helpers | +| `LESAVKA_MEET_OBSERVER_CAPTURE` | manual Google Meet observer recording path copied and analyzed after the synthetic upstream stimulus run | +| `LESAVKA_MEET_OPEN_URL` | manual Google Meet observer probe toggle; when true, opens `LESAVKA_GOOGLE_MEET_URL` in the local browser | +| `LESAVKA_MEET_SKIP_PROMPTS` | manual Google Meet observer probe toggle for non-interactive lab retries after setup is already complete | +| `LESAVKA_MEET_START_DELAY_SECONDS` | manual Google Meet observer probe delay before synthetic media starts, giving the operator/browser time to settle | | `LESAVKA_OUTPUT_DELAY_CONFIRMING` | manual direct UVC/UAC output-delay probe override; controls server-generated output calibration, confirmation, freshness, or reporting | | `LESAVKA_OUTPUT_DELAY_PROBE_AUDIO_DELAY_US` | manual direct UVC/UAC output-delay probe override; controls server-generated output calibration, confirmation, freshness, or reporting | | `LESAVKA_OUTPUT_DELAY_PROBE_VIDEO_DELAY_US` | manual direct UVC/UAC output-delay probe override; controls server-generated output calibration, confirmation, freshness, or reporting | @@ -557,7 +568,12 @@ These entries are intentionally concise because most are manual lab or CI harnes | `LESAVKA_UPSTREAM_BLIND_HEAL_MAX_STEP_US` | server upstream media blind-healer tuning knob; adjusts cautious runtime offset correction when telemetry indicates persistent skew | | `LESAVKA_UPSTREAM_BLIND_HEAL_MIN_SAMPLES` | server upstream media blind-healer tuning knob; adjusts cautious runtime offset correction when telemetry indicates persistent skew | | `LESAVKA_UPSTREAM_BLIND_HEAL_TARGET` | server upstream media blind-healer tuning knob; adjusts cautious runtime offset correction when telemetry indicates persistent skew | +| `LESAVKA_UPSTREAM_AUTO_HEAL` | client live bundled-upstream startup heal toggle; defaults on to retire a stale first UAC epoch after connect | +| `LESAVKA_UPSTREAM_AUTO_HEAL_AFTER_MS` | client live bundled-upstream startup heal delay; defaults to `3000`ms before issuing the safe audio-epoch recovery | | `LESAVKA_UPSTREAM_SOURCE_LEAD_CAP_MS` | server upstream media timing override; bounds live source lead or playout behavior while tuning client-to-server transport | | `LESAVKA_UVC_CONFIGFS_BASE` | server UVC gadget mode/configfs override used by runtime reconfiguration and hardware-in-the-loop probes | -| `LESAVKA_UVC_HEVC_JPEG_QUALITY` | server HEVC-to-MJPEG UVC bridge JPEG quality; defaults to `90` to keep RCT output compatible while limiting encode cost | +| `LESAVKA_UVC_HEVC_FREEZE_ON_SIZE_DROP` | server HEVC-to-MJPEG corruption guard toggle; defaults on so suspicious decoded frame collapses freeze the last good MJPEG frame | +| `LESAVKA_UVC_HEVC_JPEG_QUALITY` | server HEVC-to-MJPEG UVC bridge JPEG quality; defaults to `72` to lower UVC payload pressure while keeping RCT output compatible | +| `LESAVKA_UVC_HEVC_MIN_REFERENCE_BYTES` | server HEVC-to-MJPEG corruption guard baseline; decoded MJPEG frames smaller than this do not become freeze references | +| `LESAVKA_UVC_HEVC_SIZE_DROP_PCT` | server HEVC-to-MJPEG corruption guard threshold; frames below this percentage of the last good reference are frozen out | | `LESAVKA_UVC_MODE` | server UVC gadget mode/configfs override used by runtime reconfiguration and hardware-in-the-loop probes | diff --git a/scripts/ci/hygiene_gate_baseline.json b/scripts/ci/hygiene_gate_baseline.json index ead5e06..b5990cc 100644 --- a/scripts/ci/hygiene_gate_baseline.json +++ b/scripts/ci/hygiene_gate_baseline.json @@ -28,7 +28,7 @@ "client/src/app/uplink_media.rs": { "clippy_warnings": 0, "doc_debt": 0, - "loc": 17 + "loc": 19 }, "client/src/app/uplink_media/bundled_media_queue.rs": { "clippy_warnings": 0, @@ -55,6 +55,11 @@ "doc_debt": 0, "loc": 212 }, + "client/src/app/uplink_media/video_keyframes.rs": { + "clippy_warnings": 0, + "doc_debt": 0, + "loc": 103 + }, "client/src/app/uplink_media/voice_loop.rs": { "clippy_warnings": 0, "doc_debt": 0, @@ -63,17 +68,17 @@ "client/src/app/uplink_media/webcam_media_loop.rs": { "clippy_warnings": 0, "doc_debt": 0, - "loc": 295 + "loc": 421 }, "client/src/app_support.rs": { "clippy_warnings": 0, "doc_debt": 3, - "loc": 138 + "loc": 174 }, "client/src/bin/lesavka-relayctl.rs": { "clippy_warnings": 0, "doc_debt": 0, - "loc": 423 + "loc": 425 }, "client/src/bin/lesavka-sync-analyze.rs": { "clippy_warnings": 0, @@ -113,7 +118,7 @@ "client/src/input/camera.rs": { "clippy_warnings": 0, "doc_debt": 0, - "loc": 206 + "loc": 232 }, "client/src/input/camera/bus_and_encoder.rs": { "clippy_warnings": 0, @@ -123,7 +128,7 @@ "client/src/input/camera/capture_pipeline.rs": { "clippy_warnings": 0, "doc_debt": 0, - "loc": 404 + "loc": 420 }, "client/src/input/camera/device_selection.rs": { "clippy_warnings": 0, @@ -268,7 +273,7 @@ "client/src/launcher/diagnostics/diagnostics_models.rs": { "clippy_warnings": 0, "doc_debt": 1, - "loc": 199 + "loc": 200 }, "client/src/launcher/diagnostics/recommendations.rs": { "clippy_warnings": 0, @@ -278,17 +283,17 @@ "client/src/launcher/diagnostics/snapshot_report.rs": { "clippy_warnings": 0, "doc_debt": 2, - "loc": 303 + "loc": 304 }, "client/src/launcher/diagnostics/snapshot_report_text.rs": { "clippy_warnings": 0, "doc_debt": 2, - "loc": 329 + "loc": 330 }, "client/src/launcher/mod.rs": { "clippy_warnings": 0, "doc_debt": 5, - "loc": 240 + "loc": 244 }, "client/src/launcher/power.rs": { "clippy_warnings": 0, @@ -333,12 +338,12 @@ "client/src/launcher/state/launcher_state_impl.rs": { "clippy_warnings": 0, "doc_debt": 17, - "loc": 459 + "loc": 463 }, "client/src/launcher/state/launcher_status_line.rs": { "clippy_warnings": 0, "doc_debt": 1, - "loc": 48 + "loc": 49 }, "client/src/launcher/state/profile_helpers.rs": { "clippy_warnings": 0, @@ -348,27 +353,27 @@ "client/src/launcher/state/selection_models.rs": { "clippy_warnings": 0, "doc_debt": 0, - "loc": 325 + "loc": 380 }, "client/src/launcher/state/selection_models/sync_and_state_status.rs": { "clippy_warnings": 0, "doc_debt": 0, - "loc": 329 + "loc": 331 }, "client/src/launcher/ui.rs": { "clippy_warnings": 0, "doc_debt": 1, - "loc": 201 + "loc": 202 }, "client/src/launcher/ui/activation_context.rs": { "clippy_warnings": 0, "doc_debt": 0, - "loc": 45 + "loc": 46 }, "client/src/launcher/ui/activation_setup.rs": { "clippy_warnings": 0, "doc_debt": 0, - "loc": 187 + "loc": 189 }, "client/src/launcher/ui/control_requests.rs": { "clippy_warnings": 0, @@ -408,7 +413,7 @@ "client/src/launcher/ui/message_and_network_state.rs": { "clippy_warnings": 0, "doc_debt": 3, - "loc": 141 + "loc": 157 }, "client/src/launcher/ui/power_display_key_bindings.rs": { "clippy_warnings": 0, @@ -423,12 +428,17 @@ "client/src/launcher/ui/relay_input_bindings.rs": { "clippy_warnings": 0, "doc_debt": 0, - "loc": 190 + "loc": 201 }, "client/src/launcher/ui/runtime_poll.rs": { "clippy_warnings": 0, "doc_debt": 0, - "loc": 497 + "loc": 476 + }, + "client/src/launcher/ui/runtime_poll/runtime_monitor_tick.rs": { + "clippy_warnings": 0, + "doc_debt": 0, + "loc": 40 }, "client/src/launcher/ui/session_preview_coverage.rs": { "clippy_warnings": 0, @@ -438,7 +448,7 @@ "client/src/launcher/ui/stage_device_bindings.rs": { "clippy_warnings": 0, "doc_debt": 0, - "loc": 190 + "loc": 219 }, "client/src/launcher/ui/startup_window_guard.rs": { "clippy_warnings": 0, @@ -548,12 +558,12 @@ "client/src/launcher/ui_runtime/status_details.rs": { "clippy_warnings": 0, "doc_debt": 12, - "loc": 427 + "loc": 423 }, "client/src/launcher/ui_runtime/status_refresh.rs": { "clippy_warnings": 0, "doc_debt": 3, - "loc": 350 + "loc": 366 }, "client/src/layout.rs": { "clippy_warnings": 0, @@ -893,12 +903,12 @@ "server/src/bin/lesavka_uvc/coverage_model.rs": { "clippy_warnings": 0, "doc_debt": 0, - "loc": 137 + "loc": 141 }, "server/src/bin/lesavka_uvc/coverage_startup.rs": { "clippy_warnings": 0, "doc_debt": 5, - "loc": 203 + "loc": 229 }, "server/src/bin/lesavka_uvc/payload_limits.rs": { "clippy_warnings": 0, @@ -998,7 +1008,7 @@ "server/src/main.rs": { "clippy_warnings": 0, "doc_debt": 1, - "loc": 109 + "loc": 112 }, "server/src/main/entrypoint.rs": { "clippy_warnings": 0, @@ -1023,7 +1033,7 @@ "server/src/main/relay_service.rs": { "clippy_warnings": 0, "doc_debt": 0, - "loc": 414 + "loc": 494 }, "server/src/main/relay_service/camera_stream_rpc.rs": { "clippy_warnings": 0, @@ -1053,7 +1063,7 @@ "server/src/main/relay_service/upstream_media_rpc.rs": { "clippy_warnings": 0, "doc_debt": 0, - "loc": 251 + "loc": 285 }, "server/src/main/relay_service_coverage.rs": { "clippy_warnings": 0, @@ -1073,7 +1083,7 @@ "server/src/main/relay_service_tests.rs": { "clippy_warnings": 0, "doc_debt": 0, - "loc": 220 + "loc": 308 }, "server/src/main/relay_stream_lifecycle.rs": { "clippy_warnings": 0, @@ -1198,7 +1208,7 @@ "server/src/video_sinks.rs": { "clippy_warnings": 0, "doc_debt": 0, - "loc": 4 + "loc": 7 }, "server/src/video_sinks/camera_relay.rs": { "clippy_warnings": 0, @@ -1210,6 +1220,11 @@ "doc_debt": 7, "loc": 466 }, + "server/src/video_sinks/hevc_mjpeg_guard.rs": { + "clippy_warnings": 0, + "doc_debt": 2, + "loc": 120 + }, "server/src/video_sinks/mjpeg_spool.rs": { "clippy_warnings": 0, "doc_debt": 0, @@ -1218,12 +1233,12 @@ "server/src/video_sinks/webcam_sink.rs": { "clippy_warnings": 0, "doc_debt": 6, - "loc": 479 + "loc": 494 }, "server/src/video_support.rs": { "clippy_warnings": 0, "doc_debt": 1, - "loc": 301 + "loc": 340 }, "testing/src/lib.rs": { "clippy_warnings": 0, diff --git a/server/Cargo.toml b/server/Cargo.toml index 2ffc55d..d7de53f 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,7 +10,7 @@ bench = false [package] name = "lesavka_server" -version = "0.21.16" +version = "0.21.17" edition = "2024" autobins = false diff --git a/server/src/main/relay_service.rs b/server/src/main/relay_service.rs index c3ecd65..911a614 100644 --- a/server/src/main/relay_service.rs +++ b/server/src/main/relay_service.rs @@ -160,6 +160,11 @@ fn media_v2_handoff_schedule( } #[cfg(not(coverage))] +/// Convert a negotiated video FPS into a microsecond frame step. +/// +/// Inputs: frames per second from the camera config. Output: at least one +/// microsecond per frame. Why: recovery keyframe cadence and audio/video +/// bundle planning both need a bounded step even when a bad mode reports zero. fn media_v2_frame_step_us(fps: u32) -> u64 { if fps == 0 { return 1; diff --git a/server/src/main/relay_service_tests.rs b/server/src/main/relay_service_tests.rs index 94d58fe..8f717ca 100644 --- a/server/src/main/relay_service_tests.rs +++ b/server/src/main/relay_service_tests.rs @@ -8,8 +8,8 @@ mod tests { prepare_media_v2_video, retain_freshest_audio_packet, retain_freshest_video_packet, summarize_media_v2_bundle, }; - use crate::camera::CameraCodec; use lesavka_common::lesavka::{AudioPacket, UpstreamMediaBundle, VideoPacket}; + use lesavka_server::camera::CameraCodec; use lesavka_server::upstream_media_runtime::{ UpstreamClientTiming, UpstreamMediaKind, UpstreamMediaRuntime, }; diff --git a/server/src/video_sinks.rs b/server/src/video_sinks.rs index 46dd26d..4685b18 100644 --- a/server/src/video_sinks.rs +++ b/server/src/video_sinks.rs @@ -1,5 +1,7 @@ // Camera sink pipelines for UVC webcam output and HDMI capture adapters. -mod hevc_mjpeg_guard; +mod hevc_mjpeg_guard { + include!("video_sinks/hevc_mjpeg_guard.rs"); +} include!("video_sinks/webcam_sink.rs"); include!("video_sinks/hdmi_sink.rs"); include!("video_sinks/camera_relay.rs"); diff --git a/testing/tests/client_app_include_contract.rs b/testing/tests/client_app_include_contract.rs index 97aaeee..5051a97 100644 --- a/testing/tests/client_app_include_contract.rs +++ b/testing/tests/client_app_include_contract.rs @@ -51,6 +51,8 @@ mod app_support { #[derive(Clone, Copy, Debug)] pub enum CameraCodec { H264, + Hevc, + Mjpeg, } #[derive(Clone, Copy, Debug)] @@ -96,7 +98,7 @@ mod relay_transport { mod input { pub mod camera { - pub use crate::app_support::CameraConfig; + pub use crate::app_support::{CameraCodec, CameraConfig}; use lesavka_common::lesavka::VideoPacket; pub struct CameraCapture;