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]
name = "lesavka_client"
version = "0.11.16"
version = "0.11.17"
edition = "2024"
[dependencies]

View File

@ -12,7 +12,10 @@ use gstreamer_app as gst_app;
use gtk::prelude::WidgetExt;
#[cfg(not(coverage))]
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))]
use std::collections::VecDeque;
#[cfg(not(coverage))]
@ -1406,14 +1409,20 @@ fn looks_like_preview_problem(status: &str) -> bool {
#[cfg(not(coverage))]
fn build_preview_pipeline(
_profile: PreviewProfile,
profile: PreviewProfile,
) -> Result<(gst::Pipeline, gst_app::AppSrc, gst_app::AppSink, String)> {
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!(
"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 ! \
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",
);
let pipeline = gst::parse::launch(&desc)?
@ -1441,6 +1450,8 @@ fn build_preview_pipeline(
appsink.set_caps(Some(
&gst::Caps::builder("video/x-raw")
.field("format", &"RGBA")
.field("width", &(render_width as i32))
.field("height", &(render_height as i32))
.field("pixel-aspect-ratio", &gst::Fraction::new(1, 1))
.build(),
));

View File

@ -1,7 +1,9 @@
use serde::{Deserialize, Serialize};
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)]
pub enum InputRouting {
@ -206,6 +208,15 @@ impl CaptureSizePreset {
_ => 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)]

View File

@ -239,9 +239,24 @@ fn refresh_eye_feed_controls(
state.breakout_size_options(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))]
fn eye_caps_changed(state: &LauncherState, caps: &crate::handshake::PeerCaps) -> bool {
let next_width = caps.eye_width.unwrap_or(state.preview_source.width);
@ -499,6 +514,12 @@ fn rebind_popout_preview(
) {
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))]

View File

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

View File

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

View File

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

View File

@ -17,6 +17,6 @@ mod tests {
#[test]
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]
}
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 {
if requested_width == 0 || requested_height == 0 {
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]
name = "lesavka_server"
version = "0.11.16"
version = "0.11.17"
edition = "2024"
autobins = false