launcher: polish relay controls

This commit is contained in:
Brad Stein 2026-05-09 23:26:28 -03:00
parent fb466abdfd
commit da7a49bc8c
16 changed files with 176 additions and 90 deletions

6
Cargo.lock generated
View File

@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]] [[package]]
name = "lesavka_client" name = "lesavka_client"
version = "0.21.16" version = "0.21.17"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-stream", "async-stream",
@ -1686,7 +1686,7 @@ dependencies = [
[[package]] [[package]]
name = "lesavka_common" name = "lesavka_common"
version = "0.21.16" version = "0.21.17"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
@ -1698,7 +1698,7 @@ dependencies = [
[[package]] [[package]]
name = "lesavka_server" name = "lesavka_server"
version = "0.21.16" version = "0.21.17"
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.21.16" version = "0.21.17"
edition = "2024" edition = "2024"
[dependencies] [dependencies]

View File

@ -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<CameraCodec> { fn parse_camera_codec(raw: &str) -> Option<CameraCodec> {
match raw.trim().to_ascii_lowercase().as_str() { match raw.trim().to_ascii_lowercase().as_str() {
"mjpeg" | "mjpg" | "jpeg" => Some(CameraCodec::Mjpeg), "mjpeg" | "mjpg" | "jpeg" => Some(CameraCodec::Mjpeg),

View File

@ -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 { pub enum WebcamTransport {
#[default]
Hevc, Hevc,
Mjpeg, Mjpeg,
} }
@ -81,12 +82,6 @@ impl WebcamTransport {
} }
} }
impl Default for WebcamTransport {
fn default() -> Self {
Self::Hevc
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ViewMode { pub enum ViewMode {
Unified, Unified,

View File

@ -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::<&gtk::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] #[gtk::test]
#[serial] #[serial]
fn launcher_shell_installs_native_window_chrome() { fn launcher_shell_installs_native_window_chrome() {

View File

@ -458,45 +458,7 @@
next_diagnostics_probe.set(now + Duration::from_secs(2)); next_diagnostics_probe.set(now + Duration::from_secs(2));
} }
if now >= next_diagnostics_sample.get() { include!("runtime_poll/runtime_monitor_tick.rs");
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}"));
}
refresh_launcher_ui(&widgets, &state.borrow(), child_running); refresh_launcher_ui(&widgets, &state.borrow(), child_running);
if child_running if child_running

View File

@ -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}"));
}
}

View File

@ -10,7 +10,7 @@
)); ));
let relay_grid = gtk::Grid::new(); let relay_grid = gtk::Grid::new();
relay_grid.set_column_homogeneous(false); 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_hexpand(true);
relay_grid.set_row_spacing(8); relay_grid.set_row_spacing(8);
relay_grid.attach(&server_entry, 0, 0, 1, 1); relay_grid.attach(&server_entry, 0, 0, 1, 1);
@ -23,7 +23,6 @@
let start_button = rail_button("Connect", "Start or stop relay."); let start_button = rail_button("Connect", "Start or stop relay.");
start_button.add_css_class("suggested-action"); start_button.add_css_class("suggested-action");
start_button.set_hexpand(false); start_button.set_hexpand(false);
stabilize_button(&start_button, 76);
relay_grid.attach(&start_button, 2, 0, 1, 1); relay_grid.attach(&start_button, 2, 0, 1, 1);
connection_body.append(&relay_grid); connection_body.append(&relay_grid);

View File

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

View File

@ -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_FPS` | client media capture/playback override |
| `LESAVKA_CAM_H264_KBIT` | client media capture/playback override | | `LESAVKA_CAM_H264_KBIT` | client media capture/playback override |
| `LESAVKA_CAM_HEIGHT` | 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_JPEG_QUALITY` | client media capture/playback override |
| `LESAVKA_CAM_KEYFRAME_INTERVAL` | client media capture/playback override | | `LESAVKA_CAM_KEYFRAME_INTERVAL` | client media capture/playback override |
| `LESAVKA_CAM_MJPG` | 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_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_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_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_FRAME_SIZE` | server hardware/device override |
| `LESAVKA_UVC_HEIGHT` | 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` | | `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_MAXPACKET` | server hardware/device override |
| `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 |
| `LESAVKA_UVC_SKIP_UDEV` | server hardware/device override | | `LESAVKA_UVC_SKIP_UDEV` | server hardware/device override |
| `LESAVKA_UVC_STREAMING_INTERVAL` | server hardware/device override | | `LESAVKA_UVC_STREAMING_INTERVAL` | server hardware/device override |
| `LESAVKA_UVC_STREAM_INTF` | 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_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_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_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` | 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_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 | | `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_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_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_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_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_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 | | `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_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_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_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_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_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 | | `LESAVKA_UVC_MODE` | server UVC gadget mode/configfs override used by runtime reconfiguration and hardware-in-the-loop probes |

View File

@ -28,7 +28,7 @@
"client/src/app/uplink_media.rs": { "client/src/app/uplink_media.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 0, "doc_debt": 0,
"loc": 17 "loc": 19
}, },
"client/src/app/uplink_media/bundled_media_queue.rs": { "client/src/app/uplink_media/bundled_media_queue.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
@ -55,6 +55,11 @@
"doc_debt": 0, "doc_debt": 0,
"loc": 212 "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": { "client/src/app/uplink_media/voice_loop.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 0, "doc_debt": 0,
@ -63,17 +68,17 @@
"client/src/app/uplink_media/webcam_media_loop.rs": { "client/src/app/uplink_media/webcam_media_loop.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 0, "doc_debt": 0,
"loc": 295 "loc": 421
}, },
"client/src/app_support.rs": { "client/src/app_support.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 3, "doc_debt": 3,
"loc": 138 "loc": 174
}, },
"client/src/bin/lesavka-relayctl.rs": { "client/src/bin/lesavka-relayctl.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 0, "doc_debt": 0,
"loc": 423 "loc": 425
}, },
"client/src/bin/lesavka-sync-analyze.rs": { "client/src/bin/lesavka-sync-analyze.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
@ -113,7 +118,7 @@
"client/src/input/camera.rs": { "client/src/input/camera.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 0, "doc_debt": 0,
"loc": 206 "loc": 232
}, },
"client/src/input/camera/bus_and_encoder.rs": { "client/src/input/camera/bus_and_encoder.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
@ -123,7 +128,7 @@
"client/src/input/camera/capture_pipeline.rs": { "client/src/input/camera/capture_pipeline.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 0, "doc_debt": 0,
"loc": 404 "loc": 420
}, },
"client/src/input/camera/device_selection.rs": { "client/src/input/camera/device_selection.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
@ -268,7 +273,7 @@
"client/src/launcher/diagnostics/diagnostics_models.rs": { "client/src/launcher/diagnostics/diagnostics_models.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 1, "doc_debt": 1,
"loc": 199 "loc": 200
}, },
"client/src/launcher/diagnostics/recommendations.rs": { "client/src/launcher/diagnostics/recommendations.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
@ -278,17 +283,17 @@
"client/src/launcher/diagnostics/snapshot_report.rs": { "client/src/launcher/diagnostics/snapshot_report.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 2, "doc_debt": 2,
"loc": 303 "loc": 304
}, },
"client/src/launcher/diagnostics/snapshot_report_text.rs": { "client/src/launcher/diagnostics/snapshot_report_text.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 2, "doc_debt": 2,
"loc": 329 "loc": 330
}, },
"client/src/launcher/mod.rs": { "client/src/launcher/mod.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 5, "doc_debt": 5,
"loc": 240 "loc": 244
}, },
"client/src/launcher/power.rs": { "client/src/launcher/power.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
@ -333,12 +338,12 @@
"client/src/launcher/state/launcher_state_impl.rs": { "client/src/launcher/state/launcher_state_impl.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 17, "doc_debt": 17,
"loc": 459 "loc": 463
}, },
"client/src/launcher/state/launcher_status_line.rs": { "client/src/launcher/state/launcher_status_line.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 1, "doc_debt": 1,
"loc": 48 "loc": 49
}, },
"client/src/launcher/state/profile_helpers.rs": { "client/src/launcher/state/profile_helpers.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
@ -348,27 +353,27 @@
"client/src/launcher/state/selection_models.rs": { "client/src/launcher/state/selection_models.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 0, "doc_debt": 0,
"loc": 325 "loc": 380
}, },
"client/src/launcher/state/selection_models/sync_and_state_status.rs": { "client/src/launcher/state/selection_models/sync_and_state_status.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 0, "doc_debt": 0,
"loc": 329 "loc": 331
}, },
"client/src/launcher/ui.rs": { "client/src/launcher/ui.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 1, "doc_debt": 1,
"loc": 201 "loc": 202
}, },
"client/src/launcher/ui/activation_context.rs": { "client/src/launcher/ui/activation_context.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 0, "doc_debt": 0,
"loc": 45 "loc": 46
}, },
"client/src/launcher/ui/activation_setup.rs": { "client/src/launcher/ui/activation_setup.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 0, "doc_debt": 0,
"loc": 187 "loc": 189
}, },
"client/src/launcher/ui/control_requests.rs": { "client/src/launcher/ui/control_requests.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
@ -408,7 +413,7 @@
"client/src/launcher/ui/message_and_network_state.rs": { "client/src/launcher/ui/message_and_network_state.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 3, "doc_debt": 3,
"loc": 141 "loc": 157
}, },
"client/src/launcher/ui/power_display_key_bindings.rs": { "client/src/launcher/ui/power_display_key_bindings.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
@ -423,12 +428,17 @@
"client/src/launcher/ui/relay_input_bindings.rs": { "client/src/launcher/ui/relay_input_bindings.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 0, "doc_debt": 0,
"loc": 190 "loc": 201
}, },
"client/src/launcher/ui/runtime_poll.rs": { "client/src/launcher/ui/runtime_poll.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 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": { "client/src/launcher/ui/session_preview_coverage.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
@ -438,7 +448,7 @@
"client/src/launcher/ui/stage_device_bindings.rs": { "client/src/launcher/ui/stage_device_bindings.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 0, "doc_debt": 0,
"loc": 190 "loc": 219
}, },
"client/src/launcher/ui/startup_window_guard.rs": { "client/src/launcher/ui/startup_window_guard.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
@ -548,12 +558,12 @@
"client/src/launcher/ui_runtime/status_details.rs": { "client/src/launcher/ui_runtime/status_details.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 12, "doc_debt": 12,
"loc": 427 "loc": 423
}, },
"client/src/launcher/ui_runtime/status_refresh.rs": { "client/src/launcher/ui_runtime/status_refresh.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 3, "doc_debt": 3,
"loc": 350 "loc": 366
}, },
"client/src/layout.rs": { "client/src/layout.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
@ -893,12 +903,12 @@
"server/src/bin/lesavka_uvc/coverage_model.rs": { "server/src/bin/lesavka_uvc/coverage_model.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 0, "doc_debt": 0,
"loc": 137 "loc": 141
}, },
"server/src/bin/lesavka_uvc/coverage_startup.rs": { "server/src/bin/lesavka_uvc/coverage_startup.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 5, "doc_debt": 5,
"loc": 203 "loc": 229
}, },
"server/src/bin/lesavka_uvc/payload_limits.rs": { "server/src/bin/lesavka_uvc/payload_limits.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
@ -998,7 +1008,7 @@
"server/src/main.rs": { "server/src/main.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 1, "doc_debt": 1,
"loc": 109 "loc": 112
}, },
"server/src/main/entrypoint.rs": { "server/src/main/entrypoint.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
@ -1023,7 +1033,7 @@
"server/src/main/relay_service.rs": { "server/src/main/relay_service.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 0, "doc_debt": 0,
"loc": 414 "loc": 494
}, },
"server/src/main/relay_service/camera_stream_rpc.rs": { "server/src/main/relay_service/camera_stream_rpc.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
@ -1053,7 +1063,7 @@
"server/src/main/relay_service/upstream_media_rpc.rs": { "server/src/main/relay_service/upstream_media_rpc.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 0, "doc_debt": 0,
"loc": 251 "loc": 285
}, },
"server/src/main/relay_service_coverage.rs": { "server/src/main/relay_service_coverage.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
@ -1073,7 +1083,7 @@
"server/src/main/relay_service_tests.rs": { "server/src/main/relay_service_tests.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 0, "doc_debt": 0,
"loc": 220 "loc": 308
}, },
"server/src/main/relay_stream_lifecycle.rs": { "server/src/main/relay_stream_lifecycle.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
@ -1198,7 +1208,7 @@
"server/src/video_sinks.rs": { "server/src/video_sinks.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 0, "doc_debt": 0,
"loc": 4 "loc": 7
}, },
"server/src/video_sinks/camera_relay.rs": { "server/src/video_sinks/camera_relay.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
@ -1210,6 +1220,11 @@
"doc_debt": 7, "doc_debt": 7,
"loc": 466 "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": { "server/src/video_sinks/mjpeg_spool.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 0, "doc_debt": 0,
@ -1218,12 +1233,12 @@
"server/src/video_sinks/webcam_sink.rs": { "server/src/video_sinks/webcam_sink.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 6, "doc_debt": 6,
"loc": 479 "loc": 494
}, },
"server/src/video_support.rs": { "server/src/video_support.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,
"doc_debt": 1, "doc_debt": 1,
"loc": 301 "loc": 340
}, },
"testing/src/lib.rs": { "testing/src/lib.rs": {
"clippy_warnings": 0, "clippy_warnings": 0,

View File

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

View File

@ -160,6 +160,11 @@ fn media_v2_handoff_schedule(
} }
#[cfg(not(coverage))] #[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 { fn media_v2_frame_step_us(fps: u32) -> u64 {
if fps == 0 { if fps == 0 {
return 1; return 1;

View File

@ -8,8 +8,8 @@ mod tests {
prepare_media_v2_video, retain_freshest_audio_packet, retain_freshest_video_packet, prepare_media_v2_video, retain_freshest_audio_packet, retain_freshest_video_packet,
summarize_media_v2_bundle, summarize_media_v2_bundle,
}; };
use crate::camera::CameraCodec;
use lesavka_common::lesavka::{AudioPacket, UpstreamMediaBundle, VideoPacket}; use lesavka_common::lesavka::{AudioPacket, UpstreamMediaBundle, VideoPacket};
use lesavka_server::camera::CameraCodec;
use lesavka_server::upstream_media_runtime::{ use lesavka_server::upstream_media_runtime::{
UpstreamClientTiming, UpstreamMediaKind, UpstreamMediaRuntime, UpstreamClientTiming, UpstreamMediaKind, UpstreamMediaRuntime,
}; };

View File

@ -1,5 +1,7 @@
// Camera sink pipelines for UVC webcam output and HDMI capture adapters. // 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/webcam_sink.rs");
include!("video_sinks/hdmi_sink.rs"); include!("video_sinks/hdmi_sink.rs");
include!("video_sinks/camera_relay.rs"); include!("video_sinks/camera_relay.rs");

View File

@ -51,6 +51,8 @@ mod app_support {
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub enum CameraCodec { pub enum CameraCodec {
H264, H264,
Hevc,
Mjpeg,
} }
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
@ -96,7 +98,7 @@ mod relay_transport {
mod input { mod input {
pub mod camera { pub mod camera {
pub use crate::app_support::CameraConfig; pub use crate::app_support::{CameraCodec, CameraConfig};
use lesavka_common::lesavka::VideoPacket; use lesavka_common::lesavka::VideoPacket;
pub struct CameraCapture; pub struct CameraCapture;