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)> {
|
fn build_camera_preview_pipeline(device: &str) -> Result<(gst::Pipeline, gst_app::AppSink)> {
|
||||||
let device = gst_quote(device);
|
let desc = camera_preview_pipeline_desc(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 pipeline = gst::parse::launch(&desc)?
|
let pipeline = gst::parse::launch(&desc)?
|
||||||
.downcast::<gst::Pipeline>()
|
.downcast::<gst::Pipeline>()
|
||||||
.expect("camera preview pipeline");
|
.expect("camera preview pipeline");
|
||||||
@ -363,6 +356,16 @@ fn build_camera_preview_pipeline(device: &str) -> Result<(gst::Pipeline, gst_app
|
|||||||
Ok((pipeline, appsink))
|
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> {
|
fn sample_to_frame(sample: &gst::Sample) -> Option<PreviewFrame> {
|
||||||
let caps = sample.caps()?;
|
let caps = sample.caps()?;
|
||||||
let structure = caps.structure(0)?;
|
let structure = caps.structure(0)?;
|
||||||
@ -422,7 +425,7 @@ fn quote(value: impl Into<String>) -> String {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{normalize_camera_selection, resolve_camera_device};
|
use super::{camera_preview_pipeline_desc, normalize_camera_selection, resolve_camera_device};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_camera_device_accepts_explicit_paths_and_catalog_names() {
|
fn resolve_camera_device_accepts_explicit_paths_and_catalog_names() {
|
||||||
@ -443,4 +446,12 @@ mod tests {
|
|||||||
Some("usb-Logitech_C920-video-index0".to_string())
|
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);
|
root.append(&content);
|
||||||
|
|
||||||
let sidebar = gtk::Box::new(gtk::Orientation::Vertical, 12);
|
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);
|
sidebar.set_valign(gtk::Align::Fill);
|
||||||
content.append(&sidebar);
|
content.append(&sidebar);
|
||||||
|
|
||||||
@ -145,7 +145,6 @@ pub fn build_launcher_view(
|
|||||||
content.append(&stage);
|
content.append(&stage);
|
||||||
|
|
||||||
let (connection_panel, connection_body) = build_panel("Session");
|
let (connection_panel, connection_body) = build_panel("Session");
|
||||||
let server_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
|
||||||
let server_entry = gtk::Entry::new();
|
let server_entry = gtk::Entry::new();
|
||||||
server_entry.add_css_class("server-entry");
|
server_entry.add_css_class("server-entry");
|
||||||
server_entry.set_hexpand(true);
|
server_entry.set_hexpand(true);
|
||||||
@ -153,20 +152,39 @@ pub fn build_launcher_view(
|
|||||||
server_entry.set_tooltip_text(Some(
|
server_entry.set_tooltip_text(Some(
|
||||||
"Relay host address for previews, power control, and the live session.",
|
"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");
|
let start_button = gtk::Button::with_label("Start Relay");
|
||||||
start_button.add_css_class("suggested-action");
|
start_button.add_css_class("suggested-action");
|
||||||
|
start_button.set_hexpand(true);
|
||||||
start_button.set_tooltip_text(Some(
|
start_button.set_tooltip_text(Some(
|
||||||
"Launch the relay using the staged devices and current input routing.",
|
"Launch the relay using the staged devices and current input routing.",
|
||||||
));
|
));
|
||||||
let stop_button = gtk::Button::with_label("Stop Relay");
|
let stop_button = gtk::Button::with_label("Stop Relay");
|
||||||
stop_button.add_css_class("destructive-action");
|
stop_button.add_css_class("destructive-action");
|
||||||
|
stop_button.set_hexpand(true);
|
||||||
stop_button.set_tooltip_text(Some(
|
stop_button.set_tooltip_text(Some(
|
||||||
"Stop the live relay session. Local staging and previews stay available.",
|
"Stop the live relay session. Local staging and previews stay available.",
|
||||||
));
|
));
|
||||||
server_row.append(&server_entry);
|
relay_actions_row.append(&start_button);
|
||||||
server_row.append(&start_button);
|
relay_actions_row.append(&stop_button);
|
||||||
server_row.append(&stop_button);
|
connection_body.append(&relay_actions_row);
|
||||||
connection_body.append(&server_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(
|
let power_intro = gtk::Label::new(Some(
|
||||||
"Capture power can stay automatic or be forced on/off while you stage a session.",
|
"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,
|
&speaker_test_button,
|
||||||
);
|
);
|
||||||
|
|
||||||
let preview_shell = gtk::Box::new(gtk::Orientation::Vertical, 8);
|
sidebar.append(&devices_panel);
|
||||||
preview_shell.add_css_class("camera-preview-shell");
|
|
||||||
let preview_heading = gtk::Label::new(Some("Selected Camera Preview"));
|
let stage_header = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||||
preview_heading.add_css_class("panel-title");
|
let stage_title = gtk::Label::new(Some("Remote Eye Feeds"));
|
||||||
preview_heading.set_halign(gtk::Align::Start);
|
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(
|
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.add_css_class("dim-label");
|
||||||
preview_note.set_wrap(true);
|
preview_note.set_wrap(true);
|
||||||
@ -317,18 +364,22 @@ pub fn build_launcher_view(
|
|||||||
let camera_preview = gtk::Picture::new();
|
let camera_preview = gtk::Picture::new();
|
||||||
camera_preview.set_can_shrink(true);
|
camera_preview.set_can_shrink(true);
|
||||||
camera_preview.set_hexpand(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");
|
camera_preview.add_css_class("camera-preview-frame");
|
||||||
let camera_status = gtk::Label::new(Some("Select a camera and click Start Preview."));
|
let camera_status = gtk::Label::new(Some("Select a camera and click Start Preview."));
|
||||||
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);
|
||||||
preview_shell.append(&preview_heading);
|
preview_body.append(&preview_note);
|
||||||
preview_shell.append(&preview_note);
|
preview_body.append(&camera_preview);
|
||||||
preview_shell.append(&camera_preview);
|
preview_body.append(&camera_status);
|
||||||
preview_shell.append(&camera_status);
|
workspace_row.append(&preview_panel);
|
||||||
devices_body.append(&preview_shell);
|
|
||||||
sidebar.append(&devices_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 (plan_panel, plan_body) = build_panel("Launch Plan");
|
||||||
let launch_plan_title = gtk::Label::new(Some("Stage locally, then start the relay."));
|
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(&launch_plan_summary);
|
||||||
plan_body.append(&local_test_detail);
|
plan_body.append(&local_test_detail);
|
||||||
plan_body.append(&launch_plan_detail);
|
plan_body.append(&launch_plan_detail);
|
||||||
sidebar.append(&plan_panel);
|
operations_column.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);
|
|
||||||
|
|
||||||
let status_label = gtk::Label::new(Some("Launcher ready."));
|
let status_label = gtk::Label::new(Some("Launcher ready."));
|
||||||
status_label.add_css_class("status-line");
|
status_label.add_css_class("status-line");
|
||||||
@ -520,12 +533,6 @@ pub fn install_css(window: >k::ApplicationWindow) {
|
|||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 24px;
|
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 {
|
picture.camera-preview-frame {
|
||||||
background: rgba(0, 0, 0, 0.28);
|
background: rgba(0, 0, 0, 0.28);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.10);
|
border: 1px solid rgba(255, 255, 255, 0.10);
|
||||||
@ -544,6 +551,9 @@ pub fn install_css(window: >k::ApplicationWindow) {
|
|||||||
entry.server-entry {
|
entry.server-entry {
|
||||||
min-height: 38px;
|
min-height: 38px;
|
||||||
}
|
}
|
||||||
|
picture {
|
||||||
|
content-fit: contain;
|
||||||
|
}
|
||||||
button.pill-toggle {
|
button.pill-toggle {
|
||||||
min-height: 36px;
|
min-height: 36px;
|
||||||
padding: 0 14px;
|
padding: 0 14px;
|
||||||
@ -628,7 +638,7 @@ fn build_display_pane(title: &str, capture_path: &str) -> DisplayPaneWidgets {
|
|||||||
picture.set_hexpand(true);
|
picture.set_hexpand(true);
|
||||||
picture.set_vexpand(true);
|
picture.set_vexpand(true);
|
||||||
picture.set_can_shrink(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);
|
let preview_box = gtk::Box::new(gtk::Orientation::Vertical, 0);
|
||||||
preview_box.append(&picture);
|
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.add_css_class("display-placeholder");
|
||||||
placeholder_box.set_hexpand(true);
|
placeholder_box.set_hexpand(true);
|
||||||
placeholder_box.set_vexpand(true);
|
placeholder_box.set_vexpand(true);
|
||||||
placeholder_box.set_size_request(540, 304);
|
placeholder_box.set_size_request(540, 240);
|
||||||
placeholder_box.append(&placeholder);
|
placeholder_box.append(&placeholder);
|
||||||
|
|
||||||
let stack = gtk::Stack::new();
|
let stack = gtk::Stack::new();
|
||||||
|
|||||||
@ -58,7 +58,7 @@
|
|||||||
"client/src/launcher/device_test.rs": {
|
"client/src/launcher/device_test.rs": {
|
||||||
"clippy_warnings": 22,
|
"clippy_warnings": 22,
|
||||||
"doc_debt": 20,
|
"doc_debt": 20,
|
||||||
"loc": 446
|
"loc": 457
|
||||||
},
|
},
|
||||||
"client/src/launcher/devices.rs": {
|
"client/src/launcher/devices.rs": {
|
||||||
"clippy_warnings": 6,
|
"clippy_warnings": 6,
|
||||||
@ -98,7 +98,7 @@
|
|||||||
"client/src/launcher/ui_components.rs": {
|
"client/src/launcher/ui_components.rs": {
|
||||||
"clippy_warnings": 8,
|
"clippy_warnings": 8,
|
||||||
"doc_debt": 4,
|
"doc_debt": 4,
|
||||||
"loc": 679
|
"loc": 689
|
||||||
},
|
},
|
||||||
"client/src/launcher/ui_runtime.rs": {
|
"client/src/launcher/ui_runtime.rs": {
|
||||||
"clippy_warnings": 10,
|
"clippy_warnings": 10,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user