fix(launcher): recover preview feed and tighten staging/testing panel UX
This commit is contained in:
parent
8dcdbb7770
commit
22a92c9fe7
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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: >k::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(µphone_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(µphone_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.",
|
||||
|
||||
@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user