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::sync::{Arc, Mutex};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
const CAMERA_PREVIEW_WIDTH: i32 = 360;
|
const CAMERA_PREVIEW_WIDTH: i32 = 300;
|
||||||
const CAMERA_PREVIEW_HEIGHT: i32 = 202;
|
const CAMERA_PREVIEW_HEIGHT: i32 = 168;
|
||||||
const CAMERA_PREVIEW_IDLE: &str = "Select a webcam and click Start Preview.";
|
const CAMERA_PREVIEW_IDLE: &str = "Select a webcam and click Start Preview.";
|
||||||
const MIC_MONITOR_RATE: i32 = 16_000;
|
const MIC_MONITOR_RATE: i32 = 16_000;
|
||||||
const MIC_MONITOR_CHANNELS: i32 = 1;
|
const MIC_MONITOR_CHANNELS: i32 = 1;
|
||||||
|
|||||||
@ -13,7 +13,7 @@ use gtk::prelude::WidgetExt;
|
|||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
use gtk::{gdk, glib};
|
use gtk::{gdk, glib};
|
||||||
use lesavka_common::{
|
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},
|
lesavka::{MonitorRequest, VideoPacket, relay_client::RelayClient},
|
||||||
};
|
};
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
@ -1416,7 +1416,8 @@ fn build_preview_pipeline(
|
|||||||
profile.requested_width.max(2) as u32,
|
profile.requested_width.max(2) as u32,
|
||||||
profile.requested_height.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!(
|
let desc = format!(
|
||||||
"appsrc name=src is-live=true format=time do-timestamp=true block=false ! \
|
"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 ! \
|
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))
|
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))]
|
#[cfg(not(coverage))]
|
||||||
fn push_preview_packet(appsrc: &gst_app::AppSrc, pkt: VideoPacket) {
|
fn push_preview_packet(appsrc: &gst_app::AppSrc, pkt: VideoPacket) {
|
||||||
let mut buf = gst::Buffer::from_slice(pkt.data);
|
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,
|
DEFAULT_EYE_SOURCE_HEIGHT, DEFAULT_EYE_SOURCE_WIDTH, INLINE_PREVIEW_MAX_KBIT,
|
||||||
INLINE_PREVIEW_REQUEST_FPS, INLINE_PREVIEW_REQUEST_HEIGHT, INLINE_PREVIEW_REQUEST_WIDTH,
|
INLINE_PREVIEW_REQUEST_FPS, INLINE_PREVIEW_REQUEST_HEIGHT, INLINE_PREVIEW_REQUEST_WIDTH,
|
||||||
LauncherPreview, PREVIEW_HEIGHT, PREVIEW_WIDTH, PreviewSurface, PreviewTelemetry,
|
LauncherPreview, PREVIEW_HEIGHT, PREVIEW_WIDTH, PreviewSurface, PreviewTelemetry,
|
||||||
sanitize_preview_request,
|
preview_render_size, sanitize_preview_request,
|
||||||
};
|
};
|
||||||
use crate::launcher::state::{CaptureSizePreset, LauncherState};
|
use crate::launcher::state::{CaptureSizePreset, LauncherState};
|
||||||
use futures::stream;
|
use futures::stream;
|
||||||
@ -1798,6 +1820,18 @@ mod tests {
|
|||||||
assert_eq!(profile.max_bitrate_kbit, 18_000);
|
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]
|
#[test]
|
||||||
fn preview_request_sanitizer_keeps_requested_source_geometry() {
|
fn preview_request_sanitizer_keeps_requested_source_geometry() {
|
||||||
let adapted = sanitize_preview_request(1920, 1080, 60, 18_000);
|
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)));
|
Rc::new(Cell::new(Instant::now() + Duration::from_millis(250)));
|
||||||
let next_diagnostics_sample =
|
let next_diagnostics_sample =
|
||||||
Rc::new(Cell::new(Instant::now() + Duration::from_secs(1)));
|
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 (clipboard_tx, clipboard_rx) = std::sync::mpsc::channel::<ClipboardMessage>();
|
||||||
let (log_tx, log_rx) = std::sync::mpsc::channel::<String>();
|
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 diagnostics_process = Rc::clone(&diagnostics_process);
|
||||||
let next_diagnostics_probe = Rc::clone(&next_diagnostics_probe);
|
let next_diagnostics_probe = Rc::clone(&next_diagnostics_probe);
|
||||||
let next_diagnostics_sample = Rc::clone(&next_diagnostics_sample);
|
let next_diagnostics_sample = Rc::clone(&next_diagnostics_sample);
|
||||||
|
let preview_session_active = Rc::clone(&preview_session_active);
|
||||||
let log_tx = log_tx.clone();
|
let log_tx = log_tx.clone();
|
||||||
glib::timeout_add_local(Duration::from_millis(180), move || {
|
glib::timeout_add_local(Duration::from_millis(180), move || {
|
||||||
let child_running = reap_exited_child(&child_proc);
|
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 {
|
if !child_running && state.borrow().remote_active {
|
||||||
let power_mode = {
|
let power_mode = {
|
||||||
let mut state = state.borrow_mut();
|
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_WIDTH: i32 = 1380;
|
||||||
const LAUNCHER_DEFAULT_HEIGHT: i32 = 860;
|
const LAUNCHER_DEFAULT_HEIGHT: i32 = 860;
|
||||||
const OPERATIONS_RAIL_WIDTH: i32 = 288;
|
const OPERATIONS_RAIL_WIDTH: i32 = 288;
|
||||||
const CAMERA_PREVIEW_VIEWPORT_HEIGHT: i32 = 202;
|
const CAMERA_PREVIEW_VIEWPORT_HEIGHT: i32 = 168;
|
||||||
const CAMERA_PREVIEW_VIEWPORT_WIDTH: i32 = 360;
|
const CAMERA_PREVIEW_VIEWPORT_WIDTH: i32 = 300;
|
||||||
|
|
||||||
pub fn build_launcher_view(
|
pub fn build_launcher_view(
|
||||||
app: >k::Application,
|
app: >k::Application,
|
||||||
@ -326,7 +326,7 @@ pub fn build_launcher_view(
|
|||||||
let camera_preview = gtk::Picture::new();
|
let camera_preview = gtk::Picture::new();
|
||||||
camera_preview.set_can_shrink(false);
|
camera_preview.set_can_shrink(false);
|
||||||
camera_preview.set_hexpand(true);
|
camera_preview.set_hexpand(true);
|
||||||
camera_preview.set_vexpand(true);
|
camera_preview.set_vexpand(false);
|
||||||
camera_preview.set_size_request(
|
camera_preview.set_size_request(
|
||||||
CAMERA_PREVIEW_VIEWPORT_WIDTH,
|
CAMERA_PREVIEW_VIEWPORT_WIDTH,
|
||||||
CAMERA_PREVIEW_VIEWPORT_HEIGHT,
|
CAMERA_PREVIEW_VIEWPORT_HEIGHT,
|
||||||
@ -337,7 +337,7 @@ pub fn build_launcher_view(
|
|||||||
camera_status.add_css_class("dim-label");
|
camera_status.add_css_class("dim-label");
|
||||||
camera_status.set_wrap(true);
|
camera_status.set_wrap(true);
|
||||||
camera_status.set_xalign(0.0);
|
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);
|
let camera_preview_shell = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||||
camera_preview_shell.set_hexpand(true);
|
camera_preview_shell.set_hexpand(true);
|
||||||
camera_preview_shell.set_vexpand(false);
|
camera_preview_shell.set_vexpand(false);
|
||||||
@ -356,17 +356,15 @@ pub fn build_launcher_view(
|
|||||||
let playback_group = build_subgroup("Mic Playback");
|
let playback_group = build_subgroup("Mic Playback");
|
||||||
let playback_body = gtk::Box::new(gtk::Orientation::Vertical, 8);
|
let playback_body = gtk::Box::new(gtk::Orientation::Vertical, 8);
|
||||||
let playback_row = gtk::Box::new(gtk::Orientation::Horizontal, 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");
|
let microphone_replay_button = gtk::Button::with_label("Replay Last 3s");
|
||||||
stabilize_button(µphone_replay_button, 124);
|
stabilize_button(µphone_replay_button, 124);
|
||||||
let audio_preview_heading = gtk::Label::new(Some("Local Playback / Activity"));
|
audio_check_meter.set_hexpand(true);
|
||||||
audio_preview_heading.add_css_class("subgroup-title");
|
audio_check_meter.set_show_text(true);
|
||||||
audio_preview_heading.set_hexpand(true);
|
audio_check_meter.set_text(Some("Idle"));
|
||||||
audio_preview_heading.set_halign(gtk::Align::Start);
|
|
||||||
playback_row.append(µphone_replay_button);
|
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(&playback_row);
|
||||||
playback_body.append(&audio_check_meter);
|
|
||||||
audio_check_detail.set_visible(false);
|
audio_check_detail.set_visible(false);
|
||||||
playback_body.append(&audio_check_detail);
|
playback_body.append(&audio_check_detail);
|
||||||
playback_group.append(&playback_body);
|
playback_group.append(&playback_body);
|
||||||
@ -1195,12 +1193,14 @@ fn build_display_pane(title: &str, capture_path: &str) -> DisplayPaneWidgets {
|
|||||||
root.append(&stack);
|
root.append(&stack);
|
||||||
|
|
||||||
let stream_status = gtk::Label::new(Some("Connect relay to preview."));
|
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_halign(gtk::Align::Start);
|
||||||
stream_status.set_hexpand(true);
|
stream_status.set_hexpand(true);
|
||||||
stream_status.set_ellipsize(pango::EllipsizeMode::End);
|
stream_status.set_ellipsize(pango::EllipsizeMode::End);
|
||||||
stream_status.set_single_line_mode(true);
|
stream_status.set_single_line_mode(true);
|
||||||
stream_status.set_max_width_chars(24);
|
stream_status.set_max_width_chars(24);
|
||||||
stream_status.set_tooltip_text(Some("Connect relay to preview."));
|
stream_status.set_tooltip_text(Some("Connect relay to preview."));
|
||||||
|
root.append(&stream_status);
|
||||||
let feed_source_combo = gtk::ComboBoxText::new();
|
let feed_source_combo = gtk::ComboBoxText::new();
|
||||||
feed_source_combo.set_tooltip_text(Some(
|
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.",
|
"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,
|
microphone_replay_running,
|
||||||
));
|
));
|
||||||
if microphone_running {
|
if microphone_running {
|
||||||
|
let level = tests.microphone_level_fraction().clamp(0.0, 1.0);
|
||||||
|
widgets.audio_check_meter.set_fraction(level);
|
||||||
widgets
|
widgets
|
||||||
.audio_check_meter
|
.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 {
|
} else if speaker_running || microphone_replay_running {
|
||||||
|
widgets.audio_check_meter.set_text(Some("Playback"));
|
||||||
widgets.audio_check_meter.pulse();
|
widgets.audio_check_meter.pulse();
|
||||||
} else {
|
} else {
|
||||||
widgets.audio_check_meter.set_fraction(0.0);
|
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 {
|
} else {
|
||||||
format!(
|
format!(
|
||||||
"v4l2src name=cam_{eye} device=\"{dev}\" io-mode=mmap do-timestamp=true ! \
|
"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 ! \
|
queue max-size-buffers={queue_buffers} max-size-time=0 max-size-bytes=0 leaky=downstream ! \
|
||||||
h264parse disable-passthrough=true config-interval=-1 ! \
|
h264parse disable-passthrough=true config-interval=-1 ! \
|
||||||
video/x-h264,stream-format=byte-stream,alignment=au ! \
|
video/x-h264,stream-format=byte-stream,alignment=au ! \
|
||||||
appsink name=sink emit-signals=true max-buffers={appsink_buffers} drop=true",
|
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