fix(launcher): rebalance layout and local preview
This commit is contained in:
parent
e7dcfd2fd5
commit
308ea1bf85
@ -337,14 +337,7 @@ fn run_camera_preview_feed(
|
||||
}
|
||||
|
||||
fn build_camera_preview_pipeline(device: &str) -> Result<(gst::Pipeline, gst_app::AppSink)> {
|
||||
let device = gst_quote(device);
|
||||
let desc = format!(
|
||||
"v4l2src device=\"{device}\" do-timestamp=true ! \
|
||||
video/x-raw,width={CAMERA_PREVIEW_WIDTH},height={CAMERA_PREVIEW_HEIGHT},framerate=30/1 ! \
|
||||
videoconvert ! videoscale ! \
|
||||
video/x-raw,format=RGBA,width={CAMERA_PREVIEW_WIDTH},height={CAMERA_PREVIEW_HEIGHT},pixel-aspect-ratio=1/1 ! \
|
||||
appsink name=sink emit-signals=false sync=false max-buffers=1 drop=true"
|
||||
);
|
||||
let desc = camera_preview_pipeline_desc(device);
|
||||
let pipeline = gst::parse::launch(&desc)?
|
||||
.downcast::<gst::Pipeline>()
|
||||
.expect("camera preview pipeline");
|
||||
@ -363,6 +356,16 @@ fn build_camera_preview_pipeline(device: &str) -> Result<(gst::Pipeline, gst_app
|
||||
Ok((pipeline, appsink))
|
||||
}
|
||||
|
||||
fn camera_preview_pipeline_desc(device: &str) -> String {
|
||||
let device = gst_quote(device);
|
||||
format!(
|
||||
"v4l2src device=\"{device}\" do-timestamp=true ! \
|
||||
videoconvert ! videoscale ! videorate ! \
|
||||
video/x-raw,format=RGBA,width={CAMERA_PREVIEW_WIDTH},height={CAMERA_PREVIEW_HEIGHT},framerate=30/1,pixel-aspect-ratio=1/1 ! \
|
||||
appsink name=sink emit-signals=false sync=false max-buffers=1 drop=true"
|
||||
)
|
||||
}
|
||||
|
||||
fn sample_to_frame(sample: &gst::Sample) -> Option<PreviewFrame> {
|
||||
let caps = sample.caps()?;
|
||||
let structure = caps.structure(0)?;
|
||||
@ -422,7 +425,7 @@ fn quote(value: impl Into<String>) -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{normalize_camera_selection, resolve_camera_device};
|
||||
use super::{camera_preview_pipeline_desc, normalize_camera_selection, resolve_camera_device};
|
||||
|
||||
#[test]
|
||||
fn resolve_camera_device_accepts_explicit_paths_and_catalog_names() {
|
||||
@ -443,4 +446,12 @@ mod tests {
|
||||
Some("usb-Logitech_C920-video-index0".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn camera_preview_pipeline_scales_after_source_instead_of_pinning_raw_source_caps() {
|
||||
let desc = camera_preview_pipeline_desc("/dev/video0");
|
||||
assert!(desc.contains("v4l2src device=\"/dev/video0\""));
|
||||
assert!(desc.contains("videoconvert ! videoscale ! videorate !"));
|
||||
assert!(!desc.contains("v4l2src device=\"/dev/video0\" do-timestamp=true ! video/x-raw,"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -135,7 +135,7 @@ pub fn build_launcher_view(
|
||||
root.append(&content);
|
||||
|
||||
let sidebar = gtk::Box::new(gtk::Orientation::Vertical, 12);
|
||||
sidebar.set_size_request(410, -1);
|
||||
sidebar.set_size_request(420, -1);
|
||||
sidebar.set_valign(gtk::Align::Fill);
|
||||
content.append(&sidebar);
|
||||
|
||||
@ -145,7 +145,6 @@ pub fn build_launcher_view(
|
||||
content.append(&stage);
|
||||
|
||||
let (connection_panel, connection_body) = build_panel("Session");
|
||||
let server_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
let server_entry = gtk::Entry::new();
|
||||
server_entry.add_css_class("server-entry");
|
||||
server_entry.set_hexpand(true);
|
||||
@ -153,20 +152,39 @@ pub fn build_launcher_view(
|
||||
server_entry.set_tooltip_text(Some(
|
||||
"Relay host address for previews, power control, and the live session.",
|
||||
));
|
||||
connection_body.append(&server_entry);
|
||||
|
||||
let relay_actions_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
let start_button = gtk::Button::with_label("Start Relay");
|
||||
start_button.add_css_class("suggested-action");
|
||||
start_button.set_hexpand(true);
|
||||
start_button.set_tooltip_text(Some(
|
||||
"Launch the relay using the staged devices and current input routing.",
|
||||
));
|
||||
let stop_button = gtk::Button::with_label("Stop Relay");
|
||||
stop_button.add_css_class("destructive-action");
|
||||
stop_button.set_hexpand(true);
|
||||
stop_button.set_tooltip_text(Some(
|
||||
"Stop the live relay session. Local staging and previews stay available.",
|
||||
));
|
||||
server_row.append(&server_entry);
|
||||
server_row.append(&start_button);
|
||||
server_row.append(&stop_button);
|
||||
connection_body.append(&server_row);
|
||||
relay_actions_row.append(&start_button);
|
||||
relay_actions_row.append(&stop_button);
|
||||
connection_body.append(&relay_actions_row);
|
||||
|
||||
let live_actions_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
let clipboard_button = gtk::Button::with_label("Send Clipboard");
|
||||
clipboard_button.set_hexpand(true);
|
||||
clipboard_button.set_tooltip_text(Some(
|
||||
"Type the current local clipboard into the remote target. This stays launcher-only.",
|
||||
));
|
||||
let probe_button = gtk::Button::with_label("Copy Gate Probe");
|
||||
probe_button.set_hexpand(true);
|
||||
probe_button.set_tooltip_text(Some(
|
||||
"Copy the hygiene/quality probe command into the local clipboard.",
|
||||
));
|
||||
live_actions_row.append(&clipboard_button);
|
||||
live_actions_row.append(&probe_button);
|
||||
connection_body.append(&live_actions_row);
|
||||
|
||||
let power_intro = gtk::Label::new(Some(
|
||||
"Capture power can stay automatic or be forced on/off while you stage a session.",
|
||||
@ -303,13 +321,42 @@ pub fn build_launcher_view(
|
||||
&speaker_test_button,
|
||||
);
|
||||
|
||||
let preview_shell = gtk::Box::new(gtk::Orientation::Vertical, 8);
|
||||
preview_shell.add_css_class("camera-preview-shell");
|
||||
let preview_heading = gtk::Label::new(Some("Selected Camera Preview"));
|
||||
preview_heading.add_css_class("panel-title");
|
||||
preview_heading.set_halign(gtk::Align::Start);
|
||||
sidebar.append(&devices_panel);
|
||||
|
||||
let stage_header = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
let stage_title = gtk::Label::new(Some("Remote Eye Feeds"));
|
||||
stage_title.add_css_class("title-4");
|
||||
stage_title.set_halign(gtk::Align::Start);
|
||||
stage_header.append(&stage_title);
|
||||
let stage_note = gtk::Label::new(Some(
|
||||
"Live server-side eye feeds. In Auto mode, open previews and active relay sessions count as capture demand.",
|
||||
));
|
||||
stage_note.add_css_class("dim-label");
|
||||
stage_note.set_wrap(true);
|
||||
stage_note.set_xalign(0.0);
|
||||
stage.append(&stage_header);
|
||||
stage.append(&stage_note);
|
||||
|
||||
let display_row = gtk::Box::new(gtk::Orientation::Horizontal, 16);
|
||||
display_row.set_hexpand(true);
|
||||
display_row.set_vexpand(true);
|
||||
display_row.set_homogeneous(true);
|
||||
let left_pane = build_display_pane("Left Eye", "/dev/lesavka_l_eye");
|
||||
let right_pane = build_display_pane("Right Eye", "/dev/lesavka_r_eye");
|
||||
display_row.append(&left_pane.root);
|
||||
display_row.append(&right_pane.root);
|
||||
stage.append(&display_row);
|
||||
|
||||
let workspace_row = gtk::Box::new(gtk::Orientation::Horizontal, 16);
|
||||
workspace_row.set_hexpand(true);
|
||||
workspace_row.set_vexpand(true);
|
||||
stage.append(&workspace_row);
|
||||
|
||||
let (preview_panel, preview_body) = build_panel("Selected Camera Preview");
|
||||
preview_panel.set_hexpand(true);
|
||||
preview_panel.set_vexpand(true);
|
||||
let preview_note = gtk::Label::new(Some(
|
||||
"Use this to verify the chosen webcam in-place. Audio device tests still stay local.",
|
||||
"Verify the chosen webcam here before you launch. Audio device tests still stay local.",
|
||||
));
|
||||
preview_note.add_css_class("dim-label");
|
||||
preview_note.set_wrap(true);
|
||||
@ -317,18 +364,22 @@ pub fn build_launcher_view(
|
||||
let camera_preview = gtk::Picture::new();
|
||||
camera_preview.set_can_shrink(true);
|
||||
camera_preview.set_hexpand(true);
|
||||
camera_preview.set_size_request(360, 202);
|
||||
camera_preview.set_vexpand(true);
|
||||
camera_preview.set_size_request(420, 210);
|
||||
camera_preview.set_keep_aspect_ratio(true);
|
||||
camera_preview.add_css_class("camera-preview-frame");
|
||||
let camera_status = gtk::Label::new(Some("Select a camera and click Start Preview."));
|
||||
camera_status.add_css_class("dim-label");
|
||||
camera_status.set_wrap(true);
|
||||
camera_status.set_xalign(0.0);
|
||||
preview_shell.append(&preview_heading);
|
||||
preview_shell.append(&preview_note);
|
||||
preview_shell.append(&camera_preview);
|
||||
preview_shell.append(&camera_status);
|
||||
devices_body.append(&preview_shell);
|
||||
sidebar.append(&devices_panel);
|
||||
preview_body.append(&preview_note);
|
||||
preview_body.append(&camera_preview);
|
||||
preview_body.append(&camera_status);
|
||||
workspace_row.append(&preview_panel);
|
||||
|
||||
let operations_column = gtk::Box::new(gtk::Orientation::Vertical, 12);
|
||||
operations_column.set_size_request(340, -1);
|
||||
workspace_row.append(&operations_column);
|
||||
|
||||
let (plan_panel, plan_body) = build_panel("Launch Plan");
|
||||
let launch_plan_title = gtk::Label::new(Some("Stage locally, then start the relay."));
|
||||
@ -359,45 +410,7 @@ pub fn build_launcher_view(
|
||||
plan_body.append(&launch_plan_summary);
|
||||
plan_body.append(&local_test_detail);
|
||||
plan_body.append(&launch_plan_detail);
|
||||
sidebar.append(&plan_panel);
|
||||
|
||||
let (actions_panel, actions_body) = build_panel("Remote Actions");
|
||||
let actions_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
let clipboard_button = gtk::Button::with_label("Send Clipboard");
|
||||
clipboard_button.set_tooltip_text(Some(
|
||||
"Type the current local clipboard into the remote target. This stays launcher-only.",
|
||||
));
|
||||
let probe_button = gtk::Button::with_label("Copy Gate Probe");
|
||||
probe_button.set_tooltip_text(Some(
|
||||
"Copy the hygiene/quality probe command into the local clipboard.",
|
||||
));
|
||||
actions_row.append(&clipboard_button);
|
||||
actions_row.append(&probe_button);
|
||||
actions_body.append(&actions_row);
|
||||
sidebar.append(&actions_panel);
|
||||
|
||||
let stage_header = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||
let stage_title = gtk::Label::new(Some("Remote Eye Feeds"));
|
||||
stage_title.add_css_class("title-4");
|
||||
stage_title.set_halign(gtk::Align::Start);
|
||||
stage_header.append(&stage_title);
|
||||
let stage_note = gtk::Label::new(Some(
|
||||
"These are the live server-side eye feeds. In Auto mode, open eye previews and active relay sessions count as capture demand.",
|
||||
));
|
||||
stage_note.add_css_class("dim-label");
|
||||
stage_note.set_wrap(true);
|
||||
stage_note.set_xalign(0.0);
|
||||
stage.append(&stage_header);
|
||||
stage.append(&stage_note);
|
||||
|
||||
let display_row = gtk::Box::new(gtk::Orientation::Horizontal, 16);
|
||||
display_row.set_hexpand(true);
|
||||
display_row.set_vexpand(true);
|
||||
let left_pane = build_display_pane("Left Eye", "/dev/lesavka_l_eye");
|
||||
let right_pane = build_display_pane("Right Eye", "/dev/lesavka_r_eye");
|
||||
display_row.append(&left_pane.root);
|
||||
display_row.append(&right_pane.root);
|
||||
stage.append(&display_row);
|
||||
operations_column.append(&plan_panel);
|
||||
|
||||
let status_label = gtk::Label::new(Some("Launcher ready."));
|
||||
status_label.add_css_class("status-line");
|
||||
@ -520,12 +533,6 @@ pub fn install_css(window: >k::ApplicationWindow) {
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
}
|
||||
box.camera-preview-shell {
|
||||
background: rgba(255, 255, 255, 0.025);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 16px;
|
||||
padding: 14px;
|
||||
}
|
||||
picture.camera-preview-frame {
|
||||
background: rgba(0, 0, 0, 0.28);
|
||||
border: 1px solid rgba(255, 255, 255, 0.10);
|
||||
@ -544,6 +551,9 @@ pub fn install_css(window: >k::ApplicationWindow) {
|
||||
entry.server-entry {
|
||||
min-height: 38px;
|
||||
}
|
||||
picture {
|
||||
content-fit: contain;
|
||||
}
|
||||
button.pill-toggle {
|
||||
min-height: 36px;
|
||||
padding: 0 14px;
|
||||
@ -628,7 +638,7 @@ fn build_display_pane(title: &str, capture_path: &str) -> DisplayPaneWidgets {
|
||||
picture.set_hexpand(true);
|
||||
picture.set_vexpand(true);
|
||||
picture.set_can_shrink(true);
|
||||
picture.set_size_request(540, 304);
|
||||
picture.set_size_request(540, 240);
|
||||
|
||||
let preview_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||
preview_box.append(&picture);
|
||||
@ -645,7 +655,7 @@ fn build_display_pane(title: &str, capture_path: &str) -> DisplayPaneWidgets {
|
||||
placeholder_box.add_css_class("display-placeholder");
|
||||
placeholder_box.set_hexpand(true);
|
||||
placeholder_box.set_vexpand(true);
|
||||
placeholder_box.set_size_request(540, 304);
|
||||
placeholder_box.set_size_request(540, 240);
|
||||
placeholder_box.append(&placeholder);
|
||||
|
||||
let stack = gtk::Stack::new();
|
||||
|
||||
@ -58,7 +58,7 @@
|
||||
"client/src/launcher/device_test.rs": {
|
||||
"clippy_warnings": 22,
|
||||
"doc_debt": 20,
|
||||
"loc": 446
|
||||
"loc": 457
|
||||
},
|
||||
"client/src/launcher/devices.rs": {
|
||||
"clippy_warnings": 6,
|
||||
@ -98,7 +98,7 @@
|
||||
"client/src/launcher/ui_components.rs": {
|
||||
"clippy_warnings": 8,
|
||||
"doc_debt": 4,
|
||||
"loc": 679
|
||||
"loc": 689
|
||||
},
|
||||
"client/src/launcher/ui_runtime.rs": {
|
||||
"clippy_warnings": 10,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user