lesavka: fix sd source preview geometry

This commit is contained in:
Brad Stein 2026-04-19 15:07:24 -03:00
parent 6234ee872c
commit 8273b83017
10 changed files with 113 additions and 8 deletions

View File

@ -4,7 +4,7 @@ path = "src/main.rs"
[package] [package]
name = "lesavka_client" name = "lesavka_client"
version = "0.11.16" version = "0.11.17"
edition = "2024" edition = "2024"
[dependencies] [dependencies]

View File

@ -12,7 +12,10 @@ use gstreamer_app as gst_app;
use gtk::prelude::WidgetExt; use gtk::prelude::WidgetExt;
#[cfg(not(coverage))] #[cfg(not(coverage))]
use gtk::{gdk, glib}; use gtk::{gdk, glib};
use lesavka_common::lesavka::{MonitorRequest, VideoPacket, relay_client::RelayClient}; use lesavka_common::{
eye_source::{display_size_for_source_mode, eye_source_mode_for_request},
lesavka::{MonitorRequest, VideoPacket, relay_client::RelayClient},
};
#[cfg(not(coverage))] #[cfg(not(coverage))]
use std::collections::VecDeque; use std::collections::VecDeque;
#[cfg(not(coverage))] #[cfg(not(coverage))]
@ -1406,14 +1409,20 @@ fn looks_like_preview_problem(status: &str) -> bool {
#[cfg(not(coverage))] #[cfg(not(coverage))]
fn build_preview_pipeline( fn build_preview_pipeline(
_profile: PreviewProfile, profile: PreviewProfile,
) -> Result<(gst::Pipeline, gst_app::AppSrc, gst_app::AppSink, String)> { ) -> Result<(gst::Pipeline, gst_app::AppSrc, gst_app::AppSink, String)> {
let decoder_name = pick_h264_decoder(); let decoder_name = pick_h264_decoder();
let source_mode = eye_source_mode_for_request(
profile.requested_width.max(2) as u32,
profile.requested_height.max(2) as u32,
);
let (render_width, render_height) = display_size_for_source_mode(source_mode);
let 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 ! {decoder_name} name=decoder ! videoconvert ! \ h264parse name=preview_parse disable-passthrough=true ! {decoder_name} name=decoder ! videoconvert ! \
video/x-raw,format=RGBA,pixel-aspect-ratio=1/1 ! \ videoscale add-borders=false ! \
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",
); );
let pipeline = gst::parse::launch(&desc)? let pipeline = gst::parse::launch(&desc)?
@ -1441,6 +1450,8 @@ 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 as i32))
.field("height", &(render_height as i32))
.field("pixel-aspect-ratio", &gst::Fraction::new(1, 1)) .field("pixel-aspect-ratio", &gst::Fraction::new(1, 1))
.build(), .build(),
)); ));

View File

@ -1,7 +1,9 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::devices::DeviceCatalog; use super::devices::DeviceCatalog;
use lesavka_common::eye_source::{EyeSourceMode, default_eye_source_mode, native_eye_source_modes}; use lesavka_common::eye_source::{
EyeSourceMode, default_eye_source_mode, display_size_for_source_mode, native_eye_source_modes,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum InputRouting { pub enum InputRouting {
@ -206,6 +208,15 @@ impl CaptureSizePreset {
_ => Self::P1080, _ => Self::P1080,
} }
} }
pub fn display_size(self) -> (u32, u32) {
display_size_for_source_mode(self.source_mode())
}
pub fn display_aspect_ratio(self) -> f32 {
let (width, height) = self.display_size();
width.max(1) as f32 / height.max(1) as f32
}
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]

View File

@ -239,9 +239,24 @@ fn refresh_eye_feed_controls(
state.breakout_size_options(monitor_id), state.breakout_size_options(monitor_id),
state.breakout_size_preset(monitor_id), state.breakout_size_preset(monitor_id),
); );
refresh_preview_frame_ratio(widgets, monitor_id, state);
} }
} }
#[cfg(not(coverage))]
fn refresh_preview_frame_ratio(
widgets: &super::ui_components::LauncherWidgets,
monitor_id: usize,
state: &LauncherState,
) {
let capture = state
.display_capture_size_choice(monitor_id)
.unwrap_or_else(|| state.capture_size_choice(monitor_id));
widgets.display_panes[monitor_id]
.preview_frame
.set_ratio(capture.preset.display_aspect_ratio());
}
#[cfg(not(coverage))] #[cfg(not(coverage))]
fn eye_caps_changed(state: &LauncherState, caps: &crate::handshake::PeerCaps) -> bool { fn eye_caps_changed(state: &LauncherState, caps: &crate::handshake::PeerCaps) -> bool {
let next_width = caps.eye_width.unwrap_or(state.preview_source.width); let next_width = caps.eye_width.unwrap_or(state.preview_source.width);
@ -499,6 +514,12 @@ fn rebind_popout_preview(
) { ) {
handle.binding = binding; handle.binding = binding;
} }
let capture = state
.display_capture_size_choice(monitor_id)
.unwrap_or_else(|| state.capture_size_choice(monitor_id));
handle
.frame
.set_ratio(capture.preset.display_aspect_ratio());
} }
#[cfg(not(coverage))] #[cfg(not(coverage))]

View File

@ -28,6 +28,7 @@ pub struct SummaryWidgets {
pub struct DisplayPaneWidgets { pub struct DisplayPaneWidgets {
pub root: gtk::Box, pub root: gtk::Box,
pub stack: gtk::Stack, pub stack: gtk::Stack,
pub preview_frame: gtk::AspectFrame,
pub picture: gtk::Picture, pub picture: gtk::Picture,
pub stream_status: gtk::Label, pub stream_status: gtk::Label,
pub placeholder: gtk::Label, pub placeholder: gtk::Label,
@ -41,6 +42,7 @@ pub struct DisplayPaneWidgets {
pub struct PopoutWindowHandle { pub struct PopoutWindowHandle {
pub window: gtk::ApplicationWindow, pub window: gtk::ApplicationWindow,
pub frame: gtk::AspectFrame,
pub picture: gtk::Picture, pub picture: gtk::Picture,
pub status_label: gtk::Label, pub status_label: gtk::Label,
pub binding: PreviewBinding, pub binding: PreviewBinding,
@ -1133,6 +1135,7 @@ fn build_display_pane(title: &str, capture_path: &str) -> DisplayPaneWidgets {
DisplayPaneWidgets { DisplayPaneWidgets {
root, root,
stack, stack,
preview_frame,
picture, picture,
stream_status, stream_status,
placeholder, placeholder,

View File

@ -307,6 +307,7 @@ pub fn open_popout_window(
let mut popouts = popouts.borrow_mut(); let mut popouts = popouts.borrow_mut();
popouts[monitor_id] = Some(PopoutWindowHandle { popouts[monitor_id] = Some(PopoutWindowHandle {
window: window.clone(), window: window.clone(),
frame: frame.clone(),
picture: picture.clone(), picture: picture.clone(),
status_label: stream_status.clone(), status_label: stream_status.clone(),
binding, binding,
@ -1218,6 +1219,7 @@ mod tests {
.application(&app) .application(&app)
.title("Left") .title("Left")
.build(), .build(),
frame: gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false),
picture: gtk::Picture::new(), picture: gtk::Picture::new(),
status_label: gtk::Label::new(None), status_label: gtk::Label::new(None),
binding: left_binding, binding: left_binding,
@ -1227,6 +1229,7 @@ mod tests {
.application(&app) .application(&app)
.title("Right") .title("Right")
.build(), .build(),
frame: gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false),
picture: gtk::Picture::new(), picture: gtk::Picture::new(),
status_label: gtk::Label::new(None), status_label: gtk::Label::new(None),
binding: right_binding, binding: right_binding,
@ -1281,6 +1284,7 @@ mod tests {
let mut slot = popouts.borrow_mut(); let mut slot = popouts.borrow_mut();
slot[0] = Some(PopoutWindowHandle { slot[0] = Some(PopoutWindowHandle {
window, window,
frame: gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false),
picture: gtk::Picture::new(), picture: gtk::Picture::new(),
status_label: gtk::Label::new(None), status_label: gtk::Label::new(None),
binding: PreviewBinding::test_stub(), binding: PreviewBinding::test_stub(),
@ -1328,6 +1332,7 @@ mod tests {
.application(&app) .application(&app)
.title("Left") .title("Left")
.build(), .build(),
frame: gtk::AspectFrame::new(0.5, 0.5, 16.0 / 9.0, false),
picture: gtk::Picture::new(), picture: gtk::Picture::new(),
status_label: gtk::Label::new(None), status_label: gtk::Label::new(None),
binding: PreviewBinding::test_stub(), binding: PreviewBinding::test_stub(),

View File

@ -1,6 +1,6 @@
[package] [package]
name = "lesavka_common" name = "lesavka_common"
version = "0.11.16" version = "0.11.17"
edition = "2024" edition = "2024"
build = "build.rs" build = "build.rs"

View File

@ -17,6 +17,6 @@ mod tests {
#[test] #[test]
fn banner_includes_version() { fn banner_includes_version() {
assert_eq!(banner("0.11.16"), "lesavka-common CLI (v0.11.16)"); assert_eq!(banner("0.11.17"), "lesavka-common CLI (v0.11.17)");
} }
} }

View File

@ -41,6 +41,16 @@ pub fn default_eye_source_mode() -> EyeSourceMode {
GC311_H264_SOURCE_MODES[0] GC311_H264_SOURCE_MODES[0]
} }
pub fn display_size_for_source_mode(mode: EyeSourceMode) -> (u32, u32) {
match (mode.width, mode.height) {
// GC311 exposes SD widescreen source modes with non-square pixels. Render them
// into a square-pixel frame before the GTK preview consumes them.
(720, 576) => (1024, 576),
(720, 480) => (854, 480),
_ => (mode.width, mode.height),
}
}
pub fn eye_source_mode_for_request(requested_width: u32, requested_height: u32) -> EyeSourceMode { pub fn eye_source_mode_for_request(requested_width: u32, requested_height: u32) -> EyeSourceMode {
if requested_width == 0 || requested_height == 0 { if requested_width == 0 || requested_height == 0 {
return default_eye_source_mode(); return default_eye_source_mode();
@ -96,4 +106,48 @@ mod tests {
} }
); );
} }
#[test]
fn reports_square_pixel_display_size_for_each_native_mode() {
assert_eq!(
display_size_for_source_mode(EyeSourceMode {
width: 1920,
height: 1080,
fps: 60,
}),
(1920, 1080)
);
assert_eq!(
display_size_for_source_mode(EyeSourceMode {
width: 1280,
height: 720,
fps: 60,
}),
(1280, 720)
);
assert_eq!(
display_size_for_source_mode(EyeSourceMode {
width: 720,
height: 576,
fps: 50,
}),
(1024, 576)
);
assert_eq!(
display_size_for_source_mode(EyeSourceMode {
width: 720,
height: 480,
fps: 60,
}),
(854, 480)
);
assert_eq!(
display_size_for_source_mode(EyeSourceMode {
width: 640,
height: 480,
fps: 60,
}),
(640, 480)
);
}
} }

View File

@ -10,7 +10,7 @@ bench = false
[package] [package]
name = "lesavka_server" name = "lesavka_server"
version = "0.11.16" version = "0.11.17"
edition = "2024" edition = "2024"
autobins = false autobins = false