fix(launcher): recover preview feed and tighten staging/testing panel UX

This commit is contained in:
Brad Stein 2026-04-21 01:13:12 -03:00
parent 8dcdbb7770
commit 22a92c9fe7
6 changed files with 69 additions and 19 deletions

View File

@ -11,8 +11,8 @@ use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
const CAMERA_PREVIEW_WIDTH: i32 = 360;
const CAMERA_PREVIEW_HEIGHT: i32 = 202;
const CAMERA_PREVIEW_WIDTH: i32 = 300;
const CAMERA_PREVIEW_HEIGHT: i32 = 168;
const CAMERA_PREVIEW_IDLE: &str = "Select a webcam and click Start Preview.";
const MIC_MONITOR_RATE: i32 = 16_000;
const MIC_MONITOR_CHANNELS: i32 = 1;

View File

@ -13,7 +13,7 @@ use gtk::prelude::WidgetExt;
#[cfg(not(coverage))]
use gtk::{gdk, glib};
use lesavka_common::{
eye_source::{display_size_for_source_mode, eye_source_mode_for_request},
eye_source::eye_source_mode_for_request,
lesavka::{MonitorRequest, VideoPacket, relay_client::RelayClient},
};
#[cfg(not(coverage))]
@ -1416,7 +1416,8 @@ fn build_preview_pipeline(
profile.requested_width.max(2) as u32,
profile.requested_height.max(2) as u32,
);
let (render_width, render_height) = display_size_for_source_mode(source_mode);
let (render_width, render_height) =
preview_render_size(profile, source_mode.width, source_mode.height);
let desc = format!(
"appsrc name=src is-live=true format=time do-timestamp=true block=false ! \
queue max-size-buffers=6 max-size-time=0 max-size-bytes=0 leaky=downstream ! \
@ -1459,6 +1460,27 @@ fn build_preview_pipeline(
Ok((pipeline, appsrc, appsink, decoder_name))
}
#[cfg(not(coverage))]
fn preview_render_size(
profile: PreviewProfile,
source_width: u32,
source_height: u32,
) -> (i32, i32) {
fn round_down_even(value: i32) -> i32 {
let clamped = value.max(2);
clamped - (clamped % 2)
}
let source_w = source_width.max(2) as f32;
let source_h = source_height.max(2) as f32;
let max_w = profile.display_width.max(2) as f32;
let max_h = profile.display_height.max(2) as f32;
let scale = (max_w / source_w).min(max_h / source_h).min(1.0).max(0.01);
let render_w = round_down_even((source_w * scale).round() as i32);
let render_h = round_down_even((source_h * scale).round() as i32);
(render_w.max(2), render_h.max(2))
}
#[cfg(not(coverage))]
fn push_preview_packet(appsrc: &gst_app::AppSrc, pkt: VideoPacket) {
let mut buf = gst::Buffer::from_slice(pkt.data);
@ -1641,7 +1663,7 @@ mod tests {
DEFAULT_EYE_SOURCE_HEIGHT, DEFAULT_EYE_SOURCE_WIDTH, INLINE_PREVIEW_MAX_KBIT,
INLINE_PREVIEW_REQUEST_FPS, INLINE_PREVIEW_REQUEST_HEIGHT, INLINE_PREVIEW_REQUEST_WIDTH,
LauncherPreview, PREVIEW_HEIGHT, PREVIEW_WIDTH, PreviewSurface, PreviewTelemetry,
sanitize_preview_request,
preview_render_size, sanitize_preview_request,
};
use crate::launcher::state::{CaptureSizePreset, LauncherState};
use futures::stream;
@ -1798,6 +1820,18 @@ mod tests {
assert_eq!(profile.max_bitrate_kbit, 18_000);
}
#[test]
fn preview_render_size_fits_source_into_display_budget() {
let profile = PreviewSurface::Inline.profile();
assert_eq!(preview_render_size(profile, 1920, 1080), (960, 540));
}
#[test]
fn preview_render_size_never_upscales_beyond_source_geometry() {
let profile = PreviewSurface::Window.profile();
assert_eq!(preview_render_size(profile, 1280, 720), (1280, 720));
}
#[test]
fn preview_request_sanitizer_keeps_requested_source_geometry() {
let adapted = sanitize_preview_request(1920, 1080, 60, 18_000);

View File

@ -774,6 +774,7 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
Rc::new(Cell::new(Instant::now() + Duration::from_millis(250)));
let next_diagnostics_sample =
Rc::new(Cell::new(Instant::now() + Duration::from_secs(1)));
let preview_session_active = Rc::new(Cell::new(false));
let (clipboard_tx, clipboard_rx) = std::sync::mpsc::channel::<ClipboardMessage>();
let (log_tx, log_rx) = std::sync::mpsc::channel::<String>();
@ -1822,9 +1823,20 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
let diagnostics_process = Rc::clone(&diagnostics_process);
let next_diagnostics_probe = Rc::clone(&next_diagnostics_probe);
let next_diagnostics_sample = Rc::clone(&next_diagnostics_sample);
let preview_session_active = Rc::clone(&preview_session_active);
let log_tx = log_tx.clone();
glib::timeout_add_local(Duration::from_millis(180), move || {
let child_running = reap_exited_child(&child_proc);
if let Some(preview) = preview.as_ref() {
let desired_preview_active = {
let state_snapshot = state.borrow();
session_preview_active(&state_snapshot, child_running)
};
if preview_session_active.get() != desired_preview_active {
preview.set_session_active(desired_preview_active);
preview_session_active.set(desired_preview_active);
}
}
if !child_running && state.borrow().remote_active {
let power_mode = {
let mut state = state.borrow_mut();

View File

@ -111,8 +111,8 @@ const LESAVKA_ICON_SEARCH_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/ass
const LAUNCHER_DEFAULT_WIDTH: i32 = 1380;
const LAUNCHER_DEFAULT_HEIGHT: i32 = 860;
const OPERATIONS_RAIL_WIDTH: i32 = 288;
const CAMERA_PREVIEW_VIEWPORT_HEIGHT: i32 = 202;
const CAMERA_PREVIEW_VIEWPORT_WIDTH: i32 = 360;
const CAMERA_PREVIEW_VIEWPORT_HEIGHT: i32 = 168;
const CAMERA_PREVIEW_VIEWPORT_WIDTH: i32 = 300;
pub fn build_launcher_view(
app: &gtk::Application,
@ -326,7 +326,7 @@ pub fn build_launcher_view(
let camera_preview = gtk::Picture::new();
camera_preview.set_can_shrink(false);
camera_preview.set_hexpand(true);
camera_preview.set_vexpand(true);
camera_preview.set_vexpand(false);
camera_preview.set_size_request(
CAMERA_PREVIEW_VIEWPORT_WIDTH,
CAMERA_PREVIEW_VIEWPORT_HEIGHT,
@ -337,7 +337,7 @@ pub fn build_launcher_view(
camera_status.add_css_class("dim-label");
camera_status.set_wrap(true);
camera_status.set_xalign(0.0);
camera_status.set_visible(false);
camera_status.set_visible(true);
let camera_preview_shell = gtk::Box::new(gtk::Orientation::Vertical, 0);
camera_preview_shell.set_hexpand(true);
camera_preview_shell.set_vexpand(false);
@ -356,17 +356,15 @@ pub fn build_launcher_view(
let playback_group = build_subgroup("Mic Playback");
let playback_body = gtk::Box::new(gtk::Orientation::Vertical, 8);
let playback_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
playback_row.set_homogeneous(true);
playback_row.set_homogeneous(false);
let microphone_replay_button = gtk::Button::with_label("Replay Last 3s");
stabilize_button(&microphone_replay_button, 124);
let audio_preview_heading = gtk::Label::new(Some("Local Playback / Activity"));
audio_preview_heading.add_css_class("subgroup-title");
audio_preview_heading.set_hexpand(true);
audio_preview_heading.set_halign(gtk::Align::Start);
audio_check_meter.set_hexpand(true);
audio_check_meter.set_show_text(true);
audio_check_meter.set_text(Some("Idle"));
playback_row.append(&microphone_replay_button);
playback_row.append(&audio_preview_heading);
playback_row.append(&audio_check_meter);
playback_body.append(&playback_row);
playback_body.append(&audio_check_meter);
audio_check_detail.set_visible(false);
playback_body.append(&audio_check_detail);
playback_group.append(&playback_body);
@ -1195,12 +1193,14 @@ fn build_display_pane(title: &str, capture_path: &str) -> DisplayPaneWidgets {
root.append(&stack);
let stream_status = gtk::Label::new(Some("Connect relay to preview."));
stream_status.add_css_class("status-line");
stream_status.set_halign(gtk::Align::Start);
stream_status.set_hexpand(true);
stream_status.set_ellipsize(pango::EllipsizeMode::End);
stream_status.set_single_line_mode(true);
stream_status.set_max_width_chars(24);
stream_status.set_tooltip_text(Some("Connect relay to preview."));
root.append(&stream_status);
let feed_source_combo = gtk::ComboBoxText::new();
feed_source_combo.set_tooltip_text(Some(
"Choose which physical eye feed appears in this pane. Off disables the pane; the opposite-eye option mirrors the other physical feed while preserving a separate stream load for realistic validation.",

View File

@ -171,13 +171,17 @@ pub fn refresh_test_buttons(widgets: &LauncherWidgets, tests: &mut DeviceTestCon
microphone_replay_running,
));
if microphone_running {
let level = tests.microphone_level_fraction().clamp(0.0, 1.0);
widgets.audio_check_meter.set_fraction(level);
widgets
.audio_check_meter
.set_fraction(tests.microphone_level_fraction());
.set_text(Some(&format!("Mic {:>3}%", (level * 100.0).round() as u32)));
} else if speaker_running || microphone_replay_running {
widgets.audio_check_meter.set_text(Some("Playback"));
widgets.audio_check_meter.pulse();
} else {
widgets.audio_check_meter.set_fraction(0.0);
widgets.audio_check_meter.set_text(Some("Idle"));
}
}

View File

@ -444,12 +444,12 @@ pub async fn eye_ball_with_request(
} else {
format!(
"v4l2src name=cam_{eye} device=\"{dev}\" io-mode=mmap do-timestamp=true ! \
video/x-h264,width={},height={},framerate={}/1 ! \
video/x-h264,width={},height={} ! \
queue max-size-buffers={queue_buffers} max-size-time=0 max-size-bytes=0 leaky=downstream ! \
h264parse disable-passthrough=true config-interval=-1 ! \
video/x-h264,stream-format=byte-stream,alignment=au ! \
appsink name=sink emit-signals=true max-buffers={appsink_buffers} drop=true",
request.width, request.height, request.fps,
request.width, request.height,
)
};