launcher(ui): tighten recovery rail, chip health, and capture fidelity
This commit is contained in:
parent
21ad7c6ee9
commit
9401f2b7cd
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -1642,7 +1642,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.15.2"
|
version = "0.15.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
@ -1676,7 +1676,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.15.2"
|
version = "0.15.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
@ -1688,7 +1688,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.15.2"
|
version = "0.15.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.15.2"
|
version = "0.15.3"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -179,18 +179,16 @@ fn build_preview_pipeline(
|
|||||||
profile: PreviewProfile,
|
profile: PreviewProfile,
|
||||||
decoder_name: &str,
|
decoder_name: &str,
|
||||||
) -> Result<(gst::Pipeline, gst_app::AppSrc, gst_app::AppSink, String)> {
|
) -> Result<(gst::Pipeline, gst_app::AppSrc, gst_app::AppSink, String)> {
|
||||||
let source_mode = eye_source_mode_for_request(
|
let _display_bounds = (profile.display_width, profile.display_height);
|
||||||
|
let _source_mode = eye_source_mode_for_request(
|
||||||
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) =
|
|
||||||
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 ! \
|
||||||
h264parse name=preview_parse disable-passthrough=true ! {} name=decoder ! videoconvert ! \
|
h264parse name=preview_parse disable-passthrough=true ! {} name=decoder ! videoconvert ! \
|
||||||
videoscale add-borders=false ! \
|
video/x-raw,format=RGBA,pixel-aspect-ratio=1/1 ! \
|
||||||
video/x-raw,format=RGBA,width=(int){render_width},height=(int){render_height},pixel-aspect-ratio=1/1 ! \
|
|
||||||
appsink name=sink emit-signals=false sync=false max-buffers=1 drop=true",
|
appsink name=sink emit-signals=false sync=false max-buffers=1 drop=true",
|
||||||
decoder_name,
|
decoder_name,
|
||||||
);
|
);
|
||||||
@ -219,8 +217,6 @@ fn build_preview_pipeline(
|
|||||||
appsink.set_caps(Some(
|
appsink.set_caps(Some(
|
||||||
&gst::Caps::builder("video/x-raw")
|
&gst::Caps::builder("video/x-raw")
|
||||||
.field("format", "RGBA")
|
.field("format", "RGBA")
|
||||||
.field("width", render_width)
|
|
||||||
.field("height", render_height)
|
|
||||||
.field("pixel-aspect-ratio", gst::Fraction::new(1, 1))
|
.field("pixel-aspect-ratio", gst::Fraction::new(1, 1))
|
||||||
.build(),
|
.build(),
|
||||||
));
|
));
|
||||||
@ -228,27 +224,6 @@ fn build_preview_pipeline(
|
|||||||
Ok((pipeline, appsrc, appsink, decoder_name.to_string()))
|
Ok((pipeline, appsrc, appsink, decoder_name.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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).clamp(0.01, 1.0);
|
|
||||||
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 preview_decoder_candidates() -> Vec<String> {
|
fn preview_decoder_candidates() -> Vec<String> {
|
||||||
let mut candidates = Vec::new();
|
let mut candidates = Vec::new();
|
||||||
|
|||||||
@ -2,7 +2,7 @@ use super::{
|
|||||||
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,
|
||||||
preview_render_size, sanitize_preview_request,
|
sanitize_preview_request,
|
||||||
};
|
};
|
||||||
use crate::launcher::state::{CaptureSizePreset, LauncherState};
|
use crate::launcher::state::{CaptureSizePreset, LauncherState};
|
||||||
use futures::stream;
|
use futures::stream;
|
||||||
@ -155,18 +155,6 @@ fn breakout_preview_profile_defaults_to_higher_quality() {
|
|||||||
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);
|
||||||
|
|||||||
@ -377,7 +377,7 @@ fn breakout_size_changes_resize_the_open_popout_window() {
|
|||||||
fn server_chip_state_tracks_connection_not_just_reachability() {
|
fn server_chip_state_tracks_connection_not_just_reachability() {
|
||||||
let mut state = LauncherState::new();
|
let mut state = LauncherState::new();
|
||||||
assert_eq!(server_light_state(&state, false), StatusLightState::Idle);
|
assert_eq!(server_light_state(&state, false), StatusLightState::Idle);
|
||||||
assert_eq!(server_version_label(&state), "-");
|
assert_eq!(server_version_label(&state), "???");
|
||||||
|
|
||||||
state.set_server_available(true);
|
state.set_server_available(true);
|
||||||
state.set_server_version(Some(crate::VERSION.to_string()));
|
state.set_server_version(Some(crate::VERSION.to_string()));
|
||||||
@ -396,7 +396,7 @@ fn server_chip_state_tracks_connection_not_just_reachability() {
|
|||||||
|
|
||||||
state.set_server_version(Some(" ".to_string()));
|
state.set_server_version(Some(" ".to_string()));
|
||||||
assert_eq!(server_light_state(&state, false), StatusLightState::Idle);
|
assert_eq!(server_light_state(&state, false), StatusLightState::Idle);
|
||||||
assert_eq!(server_version_label(&state), "-");
|
assert_eq!(server_version_label(&state), "???");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@ -8,6 +8,7 @@ use {
|
|||||||
super::diagnostics::PerformanceSample,
|
super::diagnostics::PerformanceSample,
|
||||||
super::launcher_clipboard_control_path,
|
super::launcher_clipboard_control_path,
|
||||||
super::launcher_focus_signal_path,
|
super::launcher_focus_signal_path,
|
||||||
|
super::preview::{LauncherPreview, PreviewSurface},
|
||||||
super::power::{fetch_capture_power, reset_usb_gadget, set_capture_power_mode},
|
super::power::{fetch_capture_power, reset_usb_gadget, set_capture_power_mode},
|
||||||
super::state::{
|
super::state::{
|
||||||
BreakoutSizePreset, CapturePowerStatus, CaptureSizePreset, DisplaySurface,
|
BreakoutSizePreset, CapturePowerStatus, CaptureSizePreset, DisplaySurface,
|
||||||
|
|||||||
@ -157,6 +157,8 @@
|
|||||||
let mut state = state.borrow_mut();
|
let mut state = state.borrow_mut();
|
||||||
state.set_server_available(false);
|
state.set_server_available(false);
|
||||||
state.set_server_version(None);
|
state.set_server_version(None);
|
||||||
|
state.set_server_media_caps(None, None, None, None);
|
||||||
|
state.set_capture_power(CapturePowerStatus::default());
|
||||||
}
|
}
|
||||||
if let Some(preview) = preview.as_ref() {
|
if let Some(preview) = preview.as_ref() {
|
||||||
preview.set_server_addr(server_addr.clone());
|
preview.set_server_addr(server_addr.clone());
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
const EYE_RECORD_FPS: u32 = 20;
|
const DEFAULT_EYE_RECORD_FPS: u32 = 30;
|
||||||
const EYE_RECORD_FRAME_INTERVAL_MS: u64 = 1000 / EYE_RECORD_FPS as u64;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct EyeRecordState {
|
struct EyeRecordState {
|
||||||
@ -10,6 +9,8 @@
|
|||||||
output_path: Option<PathBuf>,
|
output_path: Option<PathBuf>,
|
||||||
next_frame_index: u32,
|
next_frame_index: u32,
|
||||||
captured_frames: u32,
|
captured_frames: u32,
|
||||||
|
encode_fps: u32,
|
||||||
|
encode_bitrate_kbit: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn eye_slug(title: &str) -> &'static str {
|
fn eye_slug(title: &str) -> &'static str {
|
||||||
@ -97,6 +98,35 @@
|
|||||||
.map_err(|err| format!("could not write {}: {err}", output_path.display()))
|
.map_err(|err| format!("could not write {}: {err}", output_path.display()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn recording_interval_ms(record_fps: u32) -> u64 {
|
||||||
|
let fps = record_fps.max(1);
|
||||||
|
(1000_u64 / fps as u64).max(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn best_effort_recording_profile(
|
||||||
|
state: &LauncherState,
|
||||||
|
preview: Option<&LauncherPreview>,
|
||||||
|
monitor_id: usize,
|
||||||
|
) -> (u32, u32) {
|
||||||
|
let choice = state
|
||||||
|
.display_capture_size_choice(monitor_id)
|
||||||
|
.unwrap_or_else(|| state.capture_size_choice(monitor_id));
|
||||||
|
let mut fps = if choice.fps == 0 {
|
||||||
|
DEFAULT_EYE_RECORD_FPS
|
||||||
|
} else {
|
||||||
|
choice.fps.max(1)
|
||||||
|
};
|
||||||
|
if let Some(snapshot) =
|
||||||
|
preview.and_then(|feed| feed.snapshot_metrics(monitor_id, PreviewSurface::Inline))
|
||||||
|
&& snapshot.server_fps.is_finite()
|
||||||
|
&& snapshot.server_fps >= 1.0
|
||||||
|
{
|
||||||
|
fps = snapshot.server_fps.round().clamp(1.0, 120.0) as u32;
|
||||||
|
}
|
||||||
|
let bitrate_kbit = choice.max_bitrate_kbit.max(800);
|
||||||
|
(fps, bitrate_kbit)
|
||||||
|
}
|
||||||
|
|
||||||
fn write_record_frame(state: &mut EyeRecordState, picture: >k::Picture) -> Result<(), String> {
|
fn write_record_frame(state: &mut EyeRecordState, picture: >k::Picture) -> Result<(), String> {
|
||||||
let frame_dir = state
|
let frame_dir = state
|
||||||
.frame_dir
|
.frame_dir
|
||||||
@ -121,8 +151,12 @@
|
|||||||
.take()
|
.take()
|
||||||
.ok_or_else(|| "recording output path was not initialized".to_string())?;
|
.ok_or_else(|| "recording output path was not initialized".to_string())?;
|
||||||
let captured_frames = state.captured_frames;
|
let captured_frames = state.captured_frames;
|
||||||
|
let encode_fps = state.encode_fps.max(1);
|
||||||
|
let encode_bitrate_kbit = state.encode_bitrate_kbit.max(800);
|
||||||
state.captured_frames = 0;
|
state.captured_frames = 0;
|
||||||
state.next_frame_index = 0;
|
state.next_frame_index = 0;
|
||||||
|
state.encode_fps = 0;
|
||||||
|
state.encode_bitrate_kbit = 0;
|
||||||
|
|
||||||
if captured_frames < 2 {
|
if captured_frames < 2 {
|
||||||
let _ = std::fs::remove_dir_all(&frame_dir);
|
let _ = std::fs::remove_dir_all(&frame_dir);
|
||||||
@ -130,6 +164,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let frame_pattern = frame_dir.join("frame-%06d.png");
|
let frame_pattern = frame_dir.join("frame-%06d.png");
|
||||||
|
let bitrate_arg = format!("{encode_bitrate_kbit}k");
|
||||||
let encode = Command::new("ffmpeg")
|
let encode = Command::new("ffmpeg")
|
||||||
.args([
|
.args([
|
||||||
"-hide_banner",
|
"-hide_banner",
|
||||||
@ -137,13 +172,17 @@
|
|||||||
"error",
|
"error",
|
||||||
"-y",
|
"-y",
|
||||||
"-framerate",
|
"-framerate",
|
||||||
&EYE_RECORD_FPS.to_string(),
|
&encode_fps.to_string(),
|
||||||
"-i",
|
"-i",
|
||||||
&frame_pattern.to_string_lossy(),
|
&frame_pattern.to_string_lossy(),
|
||||||
"-c:v",
|
"-c:v",
|
||||||
"libx264",
|
"libx264",
|
||||||
"-pix_fmt",
|
"-pix_fmt",
|
||||||
"yuv420p",
|
"yuv420p",
|
||||||
|
"-r",
|
||||||
|
&encode_fps.to_string(),
|
||||||
|
"-b:v",
|
||||||
|
&bitrate_arg,
|
||||||
&output_path.to_string_lossy(),
|
&output_path.to_string_lossy(),
|
||||||
])
|
])
|
||||||
.status()
|
.status()
|
||||||
@ -300,6 +339,8 @@
|
|||||||
let pane = pane.clone();
|
let pane = pane.clone();
|
||||||
let widgets = widgets_for_ui.clone();
|
let widgets = widgets_for_ui.clone();
|
||||||
let save_state = Rc::clone(&save_state);
|
let save_state = Rc::clone(&save_state);
|
||||||
|
let state = Rc::clone(&state);
|
||||||
|
let preview = preview.clone();
|
||||||
let record_button = pane.record_button.clone();
|
let record_button = pane.record_button.clone();
|
||||||
record_button.connect_clicked(move |button| {
|
record_button.connect_clicked(move |button| {
|
||||||
if save_state.borrow().timer.is_some() {
|
if save_state.borrow().timer.is_some() {
|
||||||
@ -330,6 +371,10 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let (record_fps, record_bitrate_kbit) = {
|
||||||
|
let state = state.borrow();
|
||||||
|
best_effort_recording_profile(&state, preview.as_deref(), monitor_id)
|
||||||
|
};
|
||||||
let root = {
|
let root = {
|
||||||
let borrowed = save_state.borrow();
|
let borrowed = save_state.borrow();
|
||||||
match ensure_eye_capture_root(borrowed.save_dir_override.as_ref()) {
|
match ensure_eye_capture_root(borrowed.save_dir_override.as_ref()) {
|
||||||
@ -361,13 +406,15 @@
|
|||||||
state.output_path = Some(output_path.clone());
|
state.output_path = Some(output_path.clone());
|
||||||
state.next_frame_index = 0;
|
state.next_frame_index = 0;
|
||||||
state.captured_frames = 0;
|
state.captured_frames = 0;
|
||||||
|
state.encode_fps = record_fps;
|
||||||
|
state.encode_bitrate_kbit = record_bitrate_kbit;
|
||||||
}
|
}
|
||||||
|
|
||||||
let pane_for_tick = pane.clone();
|
let pane_for_tick = pane.clone();
|
||||||
let widgets_for_tick = widgets.clone();
|
let widgets_for_tick = widgets.clone();
|
||||||
let save_state_for_tick = Rc::clone(&save_state);
|
let save_state_for_tick = Rc::clone(&save_state);
|
||||||
let timer = glib::timeout_add_local(
|
let timer = glib::timeout_add_local(
|
||||||
Duration::from_millis(EYE_RECORD_FRAME_INTERVAL_MS),
|
Duration::from_millis(recording_interval_ms(record_fps)),
|
||||||
move || {
|
move || {
|
||||||
let mut state = save_state_for_tick.borrow_mut();
|
let mut state = save_state_for_tick.borrow_mut();
|
||||||
if state.frame_dir.is_none() {
|
if state.frame_dir.is_none() {
|
||||||
@ -385,8 +432,8 @@
|
|||||||
save_state.borrow_mut().timer = Some(timer);
|
save_state.borrow_mut().timer = Some(timer);
|
||||||
button.set_label("Stop");
|
button.set_label("Stop");
|
||||||
widgets.status_label.set_text(&format!(
|
widgets.status_label.set_text(&format!(
|
||||||
"Recording {}... press Stop to finish.",
|
"Recording {} at {} fps (~{} kbit)... press Stop to finish.",
|
||||||
pane.title
|
pane.title, record_fps, record_bitrate_kbit
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -400,7 +447,7 @@
|
|||||||
widgets.usb_recover_button.connect_clicked(move |_| {
|
widgets.usb_recover_button.connect_clicked(move |_| {
|
||||||
let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref());
|
let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref());
|
||||||
widgets_for_click.status_label.set_text(
|
widgets_for_click.status_label.set_text(
|
||||||
"Requesting a forced USB gadget re-enumeration on the relay host...",
|
"Recover USB 1/3: sending gadget reset request to relay host...",
|
||||||
);
|
);
|
||||||
let (tx, rx) = std::sync::mpsc::channel();
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
@ -411,20 +458,20 @@
|
|||||||
glib::timeout_add_local(Duration::from_millis(100), move || match rx.try_recv() {
|
glib::timeout_add_local(Duration::from_millis(100), move || match rx.try_recv() {
|
||||||
Ok(Ok(())) => {
|
Ok(Ok(())) => {
|
||||||
widgets.status_label.set_text(
|
widgets.status_label.set_text(
|
||||||
"USB gadget recovery requested. Give the host a few seconds to re-enumerate keyboard, mouse, webcam, and audio.",
|
"Recover USB 2/3: relay acknowledged reset. Recover USB 3/3: waiting for USB/UAC/UVC chips to settle.",
|
||||||
);
|
);
|
||||||
glib::ControlFlow::Break
|
glib::ControlFlow::Break
|
||||||
}
|
}
|
||||||
Ok(Err(err)) => {
|
Ok(Err(err)) => {
|
||||||
widgets
|
widgets
|
||||||
.status_label
|
.status_label
|
||||||
.set_text(&format!("USB gadget recovery failed: {err}"));
|
.set_text(&format!("Recover USB failed: {err}"));
|
||||||
glib::ControlFlow::Break
|
glib::ControlFlow::Break
|
||||||
}
|
}
|
||||||
Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue,
|
Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue,
|
||||||
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
|
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
|
||||||
widgets.status_label.set_text(
|
widgets.status_label.set_text(
|
||||||
"USB gadget recovery ended unexpectedly before the relay answered.",
|
"Recover USB failed: relay stopped responding before completion.",
|
||||||
);
|
);
|
||||||
glib::ControlFlow::Break
|
glib::ControlFlow::Break
|
||||||
}
|
}
|
||||||
@ -441,7 +488,7 @@
|
|||||||
let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref());
|
let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref());
|
||||||
widgets_for_click
|
widgets_for_click
|
||||||
.status_label
|
.status_label
|
||||||
.set_text("Requesting UAC recovery (USB gadget rebuild) on the relay host...");
|
.set_text("Recover UAC 1/3: sending gadget reset request to relay host...");
|
||||||
let (tx, rx) = std::sync::mpsc::channel();
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let result = reset_usb_gadget(&server_addr).map_err(|err| format!("{err:#}"));
|
let result = reset_usb_gadget(&server_addr).map_err(|err| format!("{err:#}"));
|
||||||
@ -451,20 +498,20 @@
|
|||||||
glib::timeout_add_local(Duration::from_millis(100), move || match rx.try_recv() {
|
glib::timeout_add_local(Duration::from_millis(100), move || match rx.try_recv() {
|
||||||
Ok(Ok(())) => {
|
Ok(Ok(())) => {
|
||||||
widgets.status_label.set_text(
|
widgets.status_label.set_text(
|
||||||
"UAC recovery requested via USB gadget rebuild. Give the host a few seconds to re-enumerate audio.",
|
"Recover UAC 2/3: relay acknowledged reset. Recover UAC 3/3: waiting for UAC chip to settle.",
|
||||||
);
|
);
|
||||||
glib::ControlFlow::Break
|
glib::ControlFlow::Break
|
||||||
}
|
}
|
||||||
Ok(Err(err)) => {
|
Ok(Err(err)) => {
|
||||||
widgets
|
widgets
|
||||||
.status_label
|
.status_label
|
||||||
.set_text(&format!("UAC recovery failed: {err}"));
|
.set_text(&format!("Recover UAC failed: {err}"));
|
||||||
glib::ControlFlow::Break
|
glib::ControlFlow::Break
|
||||||
}
|
}
|
||||||
Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue,
|
Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue,
|
||||||
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
|
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
|
||||||
widgets.status_label.set_text(
|
widgets.status_label.set_text(
|
||||||
"UAC recovery ended unexpectedly before the relay answered.",
|
"Recover UAC failed: relay stopped responding before completion.",
|
||||||
);
|
);
|
||||||
glib::ControlFlow::Break
|
glib::ControlFlow::Break
|
||||||
}
|
}
|
||||||
@ -481,7 +528,7 @@
|
|||||||
let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref());
|
let server_addr = selected_server_addr(&server_entry, server_addr_fallback.as_ref());
|
||||||
widgets_for_click
|
widgets_for_click
|
||||||
.status_label
|
.status_label
|
||||||
.set_text("Requesting UVC recovery (USB gadget rebuild) on the relay host...");
|
.set_text("Recover UVC 1/3: sending gadget reset request to relay host...");
|
||||||
let (tx, rx) = std::sync::mpsc::channel();
|
let (tx, rx) = std::sync::mpsc::channel();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let result = reset_usb_gadget(&server_addr).map_err(|err| format!("{err:#}"));
|
let result = reset_usb_gadget(&server_addr).map_err(|err| format!("{err:#}"));
|
||||||
@ -491,20 +538,20 @@
|
|||||||
glib::timeout_add_local(Duration::from_millis(100), move || match rx.try_recv() {
|
glib::timeout_add_local(Duration::from_millis(100), move || match rx.try_recv() {
|
||||||
Ok(Ok(())) => {
|
Ok(Ok(())) => {
|
||||||
widgets.status_label.set_text(
|
widgets.status_label.set_text(
|
||||||
"UVC recovery requested via USB gadget rebuild. Give the host a few seconds to re-enumerate webcam video.",
|
"Recover UVC 2/3: relay acknowledged reset. Recover UVC 3/3: waiting for UVC chip to settle.",
|
||||||
);
|
);
|
||||||
glib::ControlFlow::Break
|
glib::ControlFlow::Break
|
||||||
}
|
}
|
||||||
Ok(Err(err)) => {
|
Ok(Err(err)) => {
|
||||||
widgets
|
widgets
|
||||||
.status_label
|
.status_label
|
||||||
.set_text(&format!("UVC recovery failed: {err}"));
|
.set_text(&format!("Recover UVC failed: {err}"));
|
||||||
glib::ControlFlow::Break
|
glib::ControlFlow::Break
|
||||||
}
|
}
|
||||||
Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue,
|
Err(std::sync::mpsc::TryRecvError::Empty) => glib::ControlFlow::Continue,
|
||||||
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
|
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
|
||||||
widgets.status_label.set_text(
|
widgets.status_label.set_text(
|
||||||
"UVC recovery ended unexpectedly before the relay answered.",
|
"Recover UVC failed: relay stopped responding before completion.",
|
||||||
);
|
);
|
||||||
glib::ControlFlow::Break
|
glib::ControlFlow::Break
|
||||||
}
|
}
|
||||||
|
|||||||
@ -72,6 +72,7 @@ pub fn build_launcher_view(
|
|||||||
camera_preview_stack,
|
camera_preview_stack,
|
||||||
camera_preview_frame,
|
camera_preview_frame,
|
||||||
camera_preview,
|
camera_preview,
|
||||||
|
webcam_transport_combo,
|
||||||
camera_mirror_button,
|
camera_mirror_button,
|
||||||
camera_mirror_revealer,
|
camera_mirror_revealer,
|
||||||
camera_status,
|
camera_status,
|
||||||
|
|||||||
@ -149,6 +149,7 @@
|
|||||||
swap_key_button: swap_key_button.clone(),
|
swap_key_button: swap_key_button.clone(),
|
||||||
camera_test_button: camera_test_button.clone(),
|
camera_test_button: camera_test_button.clone(),
|
||||||
camera_preview_stack: camera_preview_stack.clone(),
|
camera_preview_stack: camera_preview_stack.clone(),
|
||||||
|
webcam_transport_combo: webcam_transport_combo.clone(),
|
||||||
camera_mirror_button: camera_mirror_button.clone(),
|
camera_mirror_button: camera_mirror_button.clone(),
|
||||||
camera_mirror_revealer: camera_mirror_revealer.clone(),
|
camera_mirror_revealer: camera_mirror_revealer.clone(),
|
||||||
microphone_test_button: microphone_test_button.clone(),
|
microphone_test_button: microphone_test_button.clone(),
|
||||||
@ -184,6 +185,7 @@
|
|||||||
camera_preview_stack,
|
camera_preview_stack,
|
||||||
camera_preview_frame,
|
camera_preview_frame,
|
||||||
camera_preview,
|
camera_preview,
|
||||||
|
webcam_transport_combo,
|
||||||
camera_mirror_button,
|
camera_mirror_button,
|
||||||
camera_status,
|
camera_status,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -42,6 +42,7 @@ struct DeviceControlsContext {
|
|||||||
camera_preview_stack: gtk::Stack,
|
camera_preview_stack: gtk::Stack,
|
||||||
camera_preview_frame: gtk::AspectFrame,
|
camera_preview_frame: gtk::AspectFrame,
|
||||||
camera_preview: gtk::Picture,
|
camera_preview: gtk::Picture,
|
||||||
|
webcam_transport_combo: gtk::ComboBoxText,
|
||||||
camera_mirror_button: gtk::ToggleButton,
|
camera_mirror_button: gtk::ToggleButton,
|
||||||
camera_mirror_revealer: gtk::Revealer,
|
camera_mirror_revealer: gtk::Revealer,
|
||||||
camera_status: gtk::Label,
|
camera_status: gtk::Label,
|
||||||
|
|||||||
@ -328,7 +328,19 @@
|
|||||||
camera_preview_stack.add_named(&camera_preview_overlay, Some("live"));
|
camera_preview_stack.add_named(&camera_preview_overlay, Some("live"));
|
||||||
camera_preview_stack.set_visible_child_name("idle");
|
camera_preview_stack.set_visible_child_name("idle");
|
||||||
camera_preview_shell.append(&camera_preview_stack);
|
camera_preview_shell.append(&camera_preview_stack);
|
||||||
let webcam_group = build_subgroup("Webcam Preview");
|
let webcam_transport_combo = gtk::ComboBoxText::new();
|
||||||
|
webcam_transport_combo.add_css_class("compact-combo");
|
||||||
|
webcam_transport_combo.append(Some("mjpeg"), "MJPEG");
|
||||||
|
webcam_transport_combo.append(Some("yuy2"), "YUY2");
|
||||||
|
webcam_transport_combo.append(Some("h264"), "H.264");
|
||||||
|
webcam_transport_combo.set_active_id(Some("mjpeg"));
|
||||||
|
webcam_transport_combo.set_sensitive(false);
|
||||||
|
webcam_transport_combo.set_size_request(112, -1);
|
||||||
|
webcam_transport_combo.set_tooltip_text(Some(
|
||||||
|
"Upstream transport format. MJPEG is pinned while sync hardening is in progress.",
|
||||||
|
));
|
||||||
|
let webcam_group =
|
||||||
|
build_subgroup_with_action("Webcam Preview", Some(webcam_transport_combo.upcast_ref()));
|
||||||
webcam_group.set_hexpand(true);
|
webcam_group.set_hexpand(true);
|
||||||
webcam_group.set_vexpand(true);
|
webcam_group.set_vexpand(true);
|
||||||
webcam_group.set_valign(gtk::Align::Fill);
|
webcam_group.set_valign(gtk::Align::Fill);
|
||||||
@ -383,6 +395,7 @@
|
|||||||
camera_preview_stack,
|
camera_preview_stack,
|
||||||
camera_preview_frame,
|
camera_preview_frame,
|
||||||
camera_preview,
|
camera_preview,
|
||||||
|
webcam_transport_combo,
|
||||||
camera_mirror_button,
|
camera_mirror_button,
|
||||||
camera_mirror_revealer,
|
camera_mirror_revealer,
|
||||||
camera_status,
|
camera_status,
|
||||||
|
|||||||
@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
connection_body.append(&relay_grid);
|
connection_body.append(&relay_grid);
|
||||||
|
|
||||||
let recovery_heading = gtk::Label::new(Some("Recovery"));
|
let recovery_heading = gtk::Label::new(Some("Recover"));
|
||||||
recovery_heading.add_css_class("subgroup-title");
|
recovery_heading.add_css_class("subgroup-title");
|
||||||
recovery_heading.set_halign(gtk::Align::Start);
|
recovery_heading.set_halign(gtk::Align::Start);
|
||||||
let recovery_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
let recovery_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||||
@ -29,14 +29,15 @@
|
|||||||
let recovery_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
let recovery_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||||
recovery_buttons.set_hexpand(true);
|
recovery_buttons.set_hexpand(true);
|
||||||
recovery_buttons.set_homogeneous(true);
|
recovery_buttons.set_homogeneous(true);
|
||||||
let usb_recover_button = rail_button("Recover USB", "Re-enumerate remote USB gadget.");
|
let usb_recover_button = rail_button("USB", "Re-enumerate remote USB gadget.");
|
||||||
let uac_recover_button = rail_button("Recover UAC", "Rebuild remote USB audio function.");
|
let uac_recover_button = rail_button("UAC", "Rebuild remote USB audio function.");
|
||||||
let uvc_recover_button = rail_button("Recover UVC", "Rebuild remote USB webcam function.");
|
let uvc_recover_button = rail_button("UVC", "Rebuild remote USB webcam function.");
|
||||||
recovery_buttons.append(&usb_recover_button);
|
recovery_buttons.append(&usb_recover_button);
|
||||||
recovery_buttons.append(&uac_recover_button);
|
recovery_buttons.append(&uac_recover_button);
|
||||||
recovery_buttons.append(&uvc_recover_button);
|
recovery_buttons.append(&uvc_recover_button);
|
||||||
recovery_row.append(&recovery_buttons);
|
recovery_row.append(&recovery_buttons);
|
||||||
connection_body.append(&recovery_row);
|
connection_body.append(&recovery_row);
|
||||||
|
connection_body.append(>k::Separator::new(gtk::Orientation::Horizontal));
|
||||||
|
|
||||||
let tools_heading = gtk::Label::new(Some("Tools"));
|
let tools_heading = gtk::Label::new(Some("Tools"));
|
||||||
tools_heading.add_css_class("subgroup-title");
|
tools_heading.add_css_class("subgroup-title");
|
||||||
|
|||||||
@ -17,21 +17,25 @@ fn build_display_pane(title: &str, capture_path: &str) -> DisplayPaneWidgets {
|
|||||||
stream_status.set_max_width_chars(18);
|
stream_status.set_max_width_chars(18);
|
||||||
stream_status.set_tooltip_text(Some("Eye stream status."));
|
stream_status.set_tooltip_text(Some("Eye stream status."));
|
||||||
|
|
||||||
let header = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
let header_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||||
header.set_hexpand(true);
|
header_row.set_hexpand(true);
|
||||||
let title_label = gtk::Label::new(Some(title));
|
let title_label = gtk::Label::new(Some(title));
|
||||||
title_label.add_css_class("title-4");
|
title_label.add_css_class("title-4");
|
||||||
title_label.set_halign(gtk::Align::Start);
|
title_label.set_halign(gtk::Align::Start);
|
||||||
title_label.set_hexpand(false);
|
title_label.set_hexpand(true);
|
||||||
let capture_label = gtk::Label::new(Some(capture_path));
|
let capture_label = gtk::Label::new(Some(capture_path));
|
||||||
capture_label.add_css_class("dim-label");
|
capture_label.add_css_class("dim-label");
|
||||||
capture_label.set_halign(gtk::Align::End);
|
capture_label.set_halign(gtk::Align::End);
|
||||||
capture_label.set_hexpand(false);
|
capture_label.set_hexpand(false);
|
||||||
capture_label.set_ellipsize(pango::EllipsizeMode::Start);
|
capture_label.set_ellipsize(pango::EllipsizeMode::Start);
|
||||||
header.append(&title_label);
|
header_row.append(&title_label);
|
||||||
header.append(&stream_status);
|
header_row.append(&capture_label);
|
||||||
header.append(&capture_label);
|
let header_overlay = gtk::Overlay::new();
|
||||||
root.append(&header);
|
header_overlay.set_hexpand(true);
|
||||||
|
header_overlay.set_child(Some(&header_row));
|
||||||
|
header_overlay.add_overlay(&stream_status);
|
||||||
|
header_overlay.set_clip_overlay(&stream_status, true);
|
||||||
|
root.append(&header_overlay);
|
||||||
|
|
||||||
let picture = gtk::Picture::new();
|
let picture = gtk::Picture::new();
|
||||||
picture.add_css_class("eye-preview-surface");
|
picture.add_css_class("eye-preview-surface");
|
||||||
|
|||||||
@ -25,12 +25,23 @@ fn build_panel_with_action(title: &str, action: Option<>k::Widget>) -> (gtk::B
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn build_subgroup(title: &str) -> gtk::Box {
|
fn build_subgroup(title: &str) -> gtk::Box {
|
||||||
|
build_subgroup_with_action(title, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_subgroup_with_action(title: &str, action: Option<>k::Widget>) -> gtk::Box {
|
||||||
let group = gtk::Box::new(gtk::Orientation::Vertical, 8);
|
let group = gtk::Box::new(gtk::Orientation::Vertical, 8);
|
||||||
group.add_css_class("subgroup");
|
group.add_css_class("subgroup");
|
||||||
|
let heading_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);
|
||||||
|
heading_row.set_hexpand(true);
|
||||||
let heading = gtk::Label::new(Some(title));
|
let heading = gtk::Label::new(Some(title));
|
||||||
heading.add_css_class("subgroup-title");
|
heading.add_css_class("subgroup-title");
|
||||||
heading.set_halign(gtk::Align::Start);
|
heading.set_halign(gtk::Align::Start);
|
||||||
group.append(&heading);
|
heading.set_hexpand(true);
|
||||||
|
heading_row.append(&heading);
|
||||||
|
if let Some(action) = action {
|
||||||
|
heading_row.append(action);
|
||||||
|
}
|
||||||
|
group.append(&heading_row);
|
||||||
group
|
group
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,6 +49,7 @@ fn build_status_chip(label: &str, value: &str) -> (gtk::Box, gtk::Label) {
|
|||||||
let chip = gtk::Box::new(gtk::Orientation::Vertical, 4);
|
let chip = gtk::Box::new(gtk::Orientation::Vertical, 4);
|
||||||
chip.add_css_class("status-chip");
|
chip.add_css_class("status-chip");
|
||||||
chip.set_hexpand(false);
|
chip.set_hexpand(false);
|
||||||
|
chip.set_size_request(96, -1);
|
||||||
|
|
||||||
let label_widget = gtk::Label::new(Some(label));
|
let label_widget = gtk::Label::new(Some(label));
|
||||||
label_widget.add_css_class("status-chip-label");
|
label_widget.add_css_class("status-chip-label");
|
||||||
@ -47,6 +59,9 @@ fn build_status_chip(label: &str, value: &str) -> (gtk::Box, gtk::Label) {
|
|||||||
value_widget.add_css_class("status-chip-value");
|
value_widget.add_css_class("status-chip-value");
|
||||||
value_widget.set_halign(gtk::Align::Center);
|
value_widget.set_halign(gtk::Align::Center);
|
||||||
value_widget.set_xalign(0.5);
|
value_widget.set_xalign(0.5);
|
||||||
|
value_widget.set_ellipsize(pango::EllipsizeMode::End);
|
||||||
|
value_widget.set_single_line_mode(true);
|
||||||
|
value_widget.set_width_chars(9);
|
||||||
chip.append(&label_widget);
|
chip.append(&label_widget);
|
||||||
chip.append(&value_widget);
|
chip.append(&value_widget);
|
||||||
(chip, value_widget)
|
(chip, value_widget)
|
||||||
@ -56,6 +71,7 @@ fn build_status_chip_with_light(label: &str, value: &str) -> (gtk::Box, gtk::Box
|
|||||||
let chip = gtk::Box::new(gtk::Orientation::Vertical, 4);
|
let chip = gtk::Box::new(gtk::Orientation::Vertical, 4);
|
||||||
chip.add_css_class("status-chip");
|
chip.add_css_class("status-chip");
|
||||||
chip.set_hexpand(false);
|
chip.set_hexpand(false);
|
||||||
|
chip.set_size_request(96, -1);
|
||||||
|
|
||||||
let meta = gtk::Box::new(gtk::Orientation::Horizontal, 6);
|
let meta = gtk::Box::new(gtk::Orientation::Horizontal, 6);
|
||||||
meta.add_css_class("status-chip-meta");
|
meta.add_css_class("status-chip-meta");
|
||||||
@ -73,6 +89,9 @@ fn build_status_chip_with_light(label: &str, value: &str) -> (gtk::Box, gtk::Box
|
|||||||
value_widget.add_css_class("status-chip-value");
|
value_widget.add_css_class("status-chip-value");
|
||||||
value_widget.set_halign(gtk::Align::Center);
|
value_widget.set_halign(gtk::Align::Center);
|
||||||
value_widget.set_xalign(0.5);
|
value_widget.set_xalign(0.5);
|
||||||
|
value_widget.set_ellipsize(pango::EllipsizeMode::End);
|
||||||
|
value_widget.set_single_line_mode(true);
|
||||||
|
value_widget.set_width_chars(9);
|
||||||
chip.append(&meta);
|
chip.append(&meta);
|
||||||
chip.append(&value_widget);
|
chip.append(&value_widget);
|
||||||
(chip, light, value_widget)
|
(chip, light, value_widget)
|
||||||
|
|||||||
@ -156,6 +156,7 @@ pub struct LauncherWidgets {
|
|||||||
pub swap_key_button: gtk::Button,
|
pub swap_key_button: gtk::Button,
|
||||||
pub camera_test_button: gtk::Button,
|
pub camera_test_button: gtk::Button,
|
||||||
pub camera_preview_stack: gtk::Stack,
|
pub camera_preview_stack: gtk::Stack,
|
||||||
|
pub webcam_transport_combo: gtk::ComboBoxText,
|
||||||
pub camera_mirror_button: gtk::ToggleButton,
|
pub camera_mirror_button: gtk::ToggleButton,
|
||||||
pub camera_mirror_revealer: gtk::Revealer,
|
pub camera_mirror_revealer: gtk::Revealer,
|
||||||
pub microphone_test_button: gtk::Button,
|
pub microphone_test_button: gtk::Button,
|
||||||
@ -178,6 +179,7 @@ pub struct DeviceStageWidgets {
|
|||||||
pub camera_preview_stack: gtk::Stack,
|
pub camera_preview_stack: gtk::Stack,
|
||||||
pub camera_preview_frame: gtk::AspectFrame,
|
pub camera_preview_frame: gtk::AspectFrame,
|
||||||
pub camera_preview: gtk::Picture,
|
pub camera_preview: gtk::Picture,
|
||||||
|
pub webcam_transport_combo: gtk::ComboBoxText,
|
||||||
pub camera_mirror_button: gtk::ToggleButton,
|
pub camera_mirror_button: gtk::ToggleButton,
|
||||||
pub camera_status: gtk::Label,
|
pub camera_status: gtk::Label,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -103,7 +103,7 @@ fn normalize_version(version: &str) -> &str {
|
|||||||
|
|
||||||
fn server_version_label(state: &LauncherState) -> String {
|
fn server_version_label(state: &LauncherState) -> String {
|
||||||
if !state.server_available {
|
if !state.server_available {
|
||||||
return "-".to_string();
|
return "???".to_string();
|
||||||
}
|
}
|
||||||
let version = state
|
let version = state
|
||||||
.server_version
|
.server_version
|
||||||
@ -113,7 +113,7 @@ fn server_version_label(state: &LauncherState) -> String {
|
|||||||
match version {
|
match version {
|
||||||
Some(version) if version.starts_with('v') => version.to_string(),
|
Some(version) if version.starts_with('v') => version.to_string(),
|
||||||
Some(version) => format!("v{version}"),
|
Some(version) => format!("v{version}"),
|
||||||
None => "-".to_string(),
|
None => "???".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -44,14 +44,16 @@ pub type RelayChild = Child;
|
|||||||
|
|
||||||
pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, child_running: bool) {
|
pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, child_running: bool) {
|
||||||
let relay_live = child_running || state.remote_active;
|
let relay_live = child_running || state.remote_active;
|
||||||
|
let server_label = server_version_label(state);
|
||||||
set_status_light(
|
set_status_light(
|
||||||
&widgets.summary.relay_light,
|
&widgets.summary.relay_light,
|
||||||
server_light_state(state, relay_live),
|
server_light_state(state, relay_live),
|
||||||
);
|
);
|
||||||
|
widgets.summary.relay_value.set_text(&server_label);
|
||||||
widgets
|
widgets
|
||||||
.summary
|
.summary
|
||||||
.relay_value
|
.relay_value
|
||||||
.set_text(&server_version_label(state));
|
.set_tooltip_text(Some(&server_label));
|
||||||
set_status_light(
|
set_status_light(
|
||||||
&widgets.summary.routing_light,
|
&widgets.summary.routing_light,
|
||||||
StatusLightState::from_active(matches!(state.routing, InputRouting::Remote)),
|
StatusLightState::from_active(matches!(state.routing, InputRouting::Remote)),
|
||||||
@ -60,14 +62,21 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi
|
|||||||
.summary
|
.summary
|
||||||
.routing_value
|
.routing_value
|
||||||
.set_text(&capitalize(routing_name(state.routing)));
|
.set_text(&capitalize(routing_name(state.routing)));
|
||||||
|
let gpio_label = if state.server_available {
|
||||||
|
gpio_power_label(&state.capture_power)
|
||||||
|
} else {
|
||||||
|
"Offline".to_string()
|
||||||
|
};
|
||||||
set_status_light(
|
set_status_light(
|
||||||
&widgets.summary.gpio_light,
|
&widgets.summary.gpio_light,
|
||||||
gpio_light_state(&state.capture_power),
|
if state.server_available {
|
||||||
|
gpio_light_state(&state.capture_power)
|
||||||
|
} else {
|
||||||
|
StatusLightState::Idle
|
||||||
|
},
|
||||||
);
|
);
|
||||||
widgets
|
widgets.summary.gpio_value.set_text(&gpio_label);
|
||||||
.summary
|
widgets.summary.gpio_value.set_tooltip_text(Some(&gpio_label));
|
||||||
.gpio_value
|
|
||||||
.set_text(&gpio_power_label(&state.capture_power));
|
|
||||||
widgets
|
widgets
|
||||||
.summary
|
.summary
|
||||||
.shortcut_value
|
.shortcut_value
|
||||||
@ -76,16 +85,22 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi
|
|||||||
let (usb_state, usb_value) = recovery_usb_health(state);
|
let (usb_state, usb_value) = recovery_usb_health(state);
|
||||||
set_status_light(&widgets.summary.usb_light, usb_state);
|
set_status_light(&widgets.summary.usb_light, usb_state);
|
||||||
widgets.summary.usb_value.set_text(&usb_value);
|
widgets.summary.usb_value.set_text(&usb_value);
|
||||||
|
widgets.summary.usb_value.set_tooltip_text(Some(&usb_value));
|
||||||
let (uac_state, uac_value) = recovery_uac_health(state);
|
let (uac_state, uac_value) = recovery_uac_health(state);
|
||||||
set_status_light(&widgets.summary.uac_light, uac_state);
|
set_status_light(&widgets.summary.uac_light, uac_state);
|
||||||
widgets.summary.uac_value.set_text(&uac_value);
|
widgets.summary.uac_value.set_text(&uac_value);
|
||||||
|
widgets.summary.uac_value.set_tooltip_text(Some(&uac_value));
|
||||||
let (uvc_state, uvc_value) = recovery_uvc_health(state);
|
let (uvc_state, uvc_value) = recovery_uvc_health(state);
|
||||||
set_status_light(&widgets.summary.uvc_light, uvc_state);
|
set_status_light(&widgets.summary.uvc_light, uvc_state);
|
||||||
widgets.summary.uvc_value.set_text(&uvc_value);
|
widgets.summary.uvc_value.set_text(&uvc_value);
|
||||||
|
widgets.summary.uvc_value.set_tooltip_text(Some(&uvc_value));
|
||||||
|
|
||||||
widgets
|
let power_detail = if state.server_available {
|
||||||
.power_detail
|
capture_power_detail(&state.capture_power)
|
||||||
.set_text(&capture_power_detail(&state.capture_power));
|
} else {
|
||||||
|
"relay host is offline; GPIO power state is unavailable".to_string()
|
||||||
|
};
|
||||||
|
widgets.power_detail.set_text(&power_detail);
|
||||||
if (widgets.audio_gain_scale.value() - state.audio_gain_percent as f64).abs() > f64::EPSILON {
|
if (widgets.audio_gain_scale.value() - state.audio_gain_percent as f64).abs() > f64::EPSILON {
|
||||||
widgets
|
widgets
|
||||||
.audio_gain_scale
|
.audio_gain_scale
|
||||||
@ -177,6 +192,7 @@ pub fn refresh_launcher_ui(widgets: &LauncherWidgets, state: &LauncherState, chi
|
|||||||
widgets
|
widgets
|
||||||
.speaker_test_button
|
.speaker_test_button
|
||||||
.set_sensitive(!relay_live && state.channels.audio);
|
.set_sensitive(!relay_live && state.channels.audio);
|
||||||
|
widgets.webcam_transport_combo.set_sensitive(false);
|
||||||
widgets.input_toggle_button.set_label(match state.routing {
|
widgets.input_toggle_button.set_label(match state.routing {
|
||||||
InputRouting::Remote => "Route Local",
|
InputRouting::Remote => "Route Local",
|
||||||
InputRouting::Local => "Route Remote",
|
InputRouting::Local => "Route Remote",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.15.2"
|
version = "0.15.3"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ bench = false
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.15.2"
|
version = "0.15.3"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
autobins = false
|
autobins = false
|
||||||
|
|
||||||
|
|||||||
@ -88,13 +88,13 @@ fn eye_panes_keep_the_docked_preview_footprint_without_forcing_maximized_width()
|
|||||||
assert!(UI_LAYOUT_SRC.contains("capture_label.set_ellipsize(pango::EllipsizeMode::Start);"));
|
assert!(UI_LAYOUT_SRC.contains("capture_label.set_ellipsize(pango::EllipsizeMode::Start);"));
|
||||||
assert!(!UI_LAYOUT_SRC.contains("root.append(&stream_status);"));
|
assert!(!UI_LAYOUT_SRC.contains("root.append(&stream_status);"));
|
||||||
assert!(UI_LAYOUT_SRC.contains("stream_status.add_css_class(\"eye-inline-status\");"));
|
assert!(UI_LAYOUT_SRC.contains("stream_status.add_css_class(\"eye-inline-status\");"));
|
||||||
|
assert!(UI_LAYOUT_SRC.contains("let header_overlay = gtk::Overlay::new();"));
|
||||||
|
assert!(UI_LAYOUT_SRC.contains("header_row.append(&title_label);"));
|
||||||
|
assert!(UI_LAYOUT_SRC.contains("header_row.append(&capture_label);"));
|
||||||
|
assert!(UI_LAYOUT_SRC.contains("header_overlay.add_overlay(&stream_status);"));
|
||||||
assert!(
|
assert!(
|
||||||
source_index("header.append(&title_label);")
|
source_index("header_row.append(&title_label);")
|
||||||
< source_index("header.append(&stream_status);")
|
< source_index("header_row.append(&capture_label);")
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
source_index("header.append(&stream_status);")
|
|
||||||
< source_index("header.append(&capture_label);")
|
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
source_index("controls_grid.attach(&breakout_row, 0, 1, 1, 1);")
|
source_index("controls_grid.attach(&breakout_row, 0, 1, 1, 1);")
|
||||||
@ -281,7 +281,7 @@ fn relay_controls_keep_connect_inline_with_server_entry() {
|
|||||||
assert!(UI_LAYOUT_SRC.contains("let start_button = rail_button(\"Connect\""));
|
assert!(UI_LAYOUT_SRC.contains("let start_button = rail_button(\"Connect\""));
|
||||||
assert!(UI_LAYOUT_SRC.contains("pub(crate) fn set_rail_button_label("));
|
assert!(UI_LAYOUT_SRC.contains("pub(crate) fn set_rail_button_label("));
|
||||||
assert!(UI_LAYOUT_SRC.contains("relay_grid.attach(&start_button, 2, 0, 1, 1);"));
|
assert!(UI_LAYOUT_SRC.contains("relay_grid.attach(&start_button, 2, 0, 1, 1);"));
|
||||||
assert!(UI_LAYOUT_SRC.contains("let recovery_heading = gtk::Label::new(Some(\"Recovery\"));"));
|
assert!(UI_LAYOUT_SRC.contains("let recovery_heading = gtk::Label::new(Some(\"Recover\"));"));
|
||||||
assert!(UI_LAYOUT_SRC.contains("let recovery_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);"));
|
assert!(UI_LAYOUT_SRC.contains("let recovery_row = gtk::Box::new(gtk::Orientation::Horizontal, 8);"));
|
||||||
assert!(UI_LAYOUT_SRC.contains("let recovery_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8);"));
|
assert!(UI_LAYOUT_SRC.contains("let recovery_buttons = gtk::Box::new(gtk::Orientation::Horizontal, 8);"));
|
||||||
assert!(UI_LAYOUT_SRC.contains("recovery_buttons.set_homogeneous(true);"));
|
assert!(UI_LAYOUT_SRC.contains("recovery_buttons.set_homogeneous(true);"));
|
||||||
@ -292,9 +292,9 @@ fn relay_controls_keep_connect_inline_with_server_entry() {
|
|||||||
assert!(UI_LAYOUT_SRC.contains("tools_buttons.set_homogeneous(true);"));
|
assert!(UI_LAYOUT_SRC.contains("tools_buttons.set_homogeneous(true);"));
|
||||||
assert!(UI_LAYOUT_SRC.contains("tools_heading.set_width_chars(10);"));
|
assert!(UI_LAYOUT_SRC.contains("tools_heading.set_width_chars(10);"));
|
||||||
assert!(UI_LAYOUT_SRC.contains("let clipboard_button = rail_button(\"Clipboard\""));
|
assert!(UI_LAYOUT_SRC.contains("let clipboard_button = rail_button(\"Clipboard\""));
|
||||||
assert!(UI_LAYOUT_SRC.contains("let usb_recover_button = rail_button(\"Recover USB\""));
|
assert!(UI_LAYOUT_SRC.contains("let usb_recover_button = rail_button(\"USB\""));
|
||||||
assert!(UI_LAYOUT_SRC.contains("let uac_recover_button = rail_button(\"Recover UAC\""));
|
assert!(UI_LAYOUT_SRC.contains("let uac_recover_button = rail_button(\"UAC\""));
|
||||||
assert!(UI_LAYOUT_SRC.contains("let uvc_recover_button = rail_button(\"Recover UVC\""));
|
assert!(UI_LAYOUT_SRC.contains("let uvc_recover_button = rail_button(\"UVC\""));
|
||||||
assert!(UI_LAYOUT_SRC.contains("recovery_buttons.append(&usb_recover_button);"));
|
assert!(UI_LAYOUT_SRC.contains("recovery_buttons.append(&usb_recover_button);"));
|
||||||
assert!(UI_LAYOUT_SRC.contains("recovery_buttons.append(&uac_recover_button);"));
|
assert!(UI_LAYOUT_SRC.contains("recovery_buttons.append(&uac_recover_button);"));
|
||||||
assert!(UI_LAYOUT_SRC.contains("recovery_buttons.append(&uvc_recover_button);"));
|
assert!(UI_LAYOUT_SRC.contains("recovery_buttons.append(&uvc_recover_button);"));
|
||||||
@ -373,8 +373,8 @@ fn media_controls_own_stream_toggles_and_inline_gain_controls() {
|
|||||||
UI_LAYOUT_SRC
|
UI_LAYOUT_SRC
|
||||||
.matches("connection_body.append(>k::Separator::new(gtk::Orientation::Horizontal));")
|
.matches("connection_body.append(>k::Separator::new(gtk::Orientation::Horizontal));")
|
||||||
.count(),
|
.count(),
|
||||||
2,
|
3,
|
||||||
"the operations rail should not gain extra vertical sections that stretch the lower layout"
|
"recover/tools/gpio/inputs sections should remain visually separated"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
source_index("let power_heading = gtk::Label::new(Some(\"GPIO Power\"));")
|
source_index("let power_heading = gtk::Label::new(Some(\"GPIO Power\"));")
|
||||||
|
|||||||
@ -174,10 +174,10 @@ fn launcher_utility_buttons_still_bind_to_live_actions() {
|
|||||||
assert!(UI_SRC.contains("clip saved to"));
|
assert!(UI_SRC.contains("clip saved to"));
|
||||||
assert!(UI_SRC.contains("record_button.connect_clicked"));
|
assert!(UI_SRC.contains("record_button.connect_clicked"));
|
||||||
assert!(UI_SRC.contains("recording saved to"));
|
assert!(UI_SRC.contains("recording saved to"));
|
||||||
assert!(UI_SRC.contains("Recording {}... press Stop to finish."));
|
assert!(UI_SRC.contains("press Stop to finish."));
|
||||||
assert!(UI_SRC.contains("widgets.usb_recover_button.connect_clicked"));
|
assert!(UI_SRC.contains("widgets.usb_recover_button.connect_clicked"));
|
||||||
assert!(UI_SRC.contains("reset_usb_gadget(&server_addr)"));
|
assert!(UI_SRC.contains("reset_usb_gadget(&server_addr)"));
|
||||||
assert!(UI_SRC.contains("USB gadget recovery requested."));
|
assert!(UI_SRC.contains("Recover USB 2/3: relay acknowledged reset."));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -188,7 +188,7 @@ fn server_chip_distinguishes_reachable_from_connected() {
|
|||||||
assert!(UI_RUNTIME_SRC.contains("} else if relay_live {"));
|
assert!(UI_RUNTIME_SRC.contains("} else if relay_live {"));
|
||||||
assert!(UI_RUNTIME_SRC.contains("StatusLightState::Caution"));
|
assert!(UI_RUNTIME_SRC.contains("StatusLightState::Caution"));
|
||||||
assert!(UI_RUNTIME_SRC.contains("fn server_version_label("));
|
assert!(UI_RUNTIME_SRC.contains("fn server_version_label("));
|
||||||
assert!(UI_RUNTIME_SRC.contains("return \"-\".to_string();"));
|
assert!(UI_RUNTIME_SRC.contains("return \"???\".to_string();"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user