lesavka: force reencode when requested

This commit is contained in:
Brad Stein 2026-04-17 11:51:19 -03:00
parent bb4921e7e9
commit 7cb0a4d655
14 changed files with 89 additions and 15 deletions

View File

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

View File

@ -425,6 +425,7 @@ impl LesavkaClientApp {
requested_width: 0,
requested_height: 0,
requested_fps: 0,
prefer_reencode: false,
};
match cli.capture_video(Request::new(req)).await {
Ok(mut stream) => {
@ -468,6 +469,7 @@ impl LesavkaClientApp {
requested_width: 0,
requested_height: 0,
requested_fps: 0,
prefer_reencode: false,
};
match cli.capture_audio(Request::new(req)).await {
Ok(mut stream) => {

View File

@ -93,6 +93,7 @@ struct PreviewProfile {
requested_height: i32,
requested_fps: u32,
max_bitrate_kbit: u32,
prefer_reencode: bool,
}
#[cfg(not(coverage))]
@ -112,6 +113,7 @@ impl PreviewSurface {
),
requested_fps: preview_bitrate("LESAVKA_PREVIEW_REQUEST_FPS", 30),
max_bitrate_kbit: preview_bitrate("LESAVKA_PREVIEW_MAX_KBIT", 12_000),
prefer_reencode: true,
},
Self::Window => PreviewProfile {
display_width: preview_dimension("LESAVKA_BREAKOUT_PREVIEW_WIDTH", 1280),
@ -126,6 +128,7 @@ impl PreviewSurface {
),
requested_fps: preview_bitrate("LESAVKA_BREAKOUT_REQUEST_FPS", 30),
max_bitrate_kbit: preview_bitrate("LESAVKA_BREAKOUT_PREVIEW_MAX_KBIT", 12_000),
prefer_reencode: true,
},
}
}
@ -262,6 +265,7 @@ impl LauncherPreview {
requested_height: i32,
requested_fps: u32,
max_bitrate_kbit: u32,
prefer_reencode: bool,
) {
self.rebuild_feed(
&self.inline_feeds,
@ -271,6 +275,7 @@ impl LauncherPreview {
requested_height,
requested_fps,
max_bitrate_kbit,
prefer_reencode,
)),
None,
);
@ -282,6 +287,7 @@ impl LauncherPreview {
requested_height,
requested_fps,
max_bitrate_kbit,
prefer_reencode,
)),
None,
);
@ -295,7 +301,7 @@ impl LauncherPreview {
&self,
feeds: &Arc<Mutex<[PreviewFeed; 2]>>,
monitor_id: usize,
requested: Option<(i32, i32, u32, u32)>,
requested: Option<(i32, i32, u32, u32, bool)>,
display: Option<(i32, i32)>,
) {
let Ok(mut feeds) = feeds.lock() else {
@ -306,13 +312,19 @@ impl LauncherPreview {
};
let was_active = existing.is_active();
let mut profile = existing.profile();
if let Some((requested_width, requested_height, requested_fps, max_bitrate_kbit)) =
requested
if let Some((
requested_width,
requested_height,
requested_fps,
max_bitrate_kbit,
prefer_reencode,
)) = requested
{
profile.requested_width = requested_width.max(2);
profile.requested_height = requested_height.max(2);
profile.requested_fps = requested_fps.max(1);
profile.max_bitrate_kbit = max_bitrate_kbit.max(800);
profile.prefer_reencode = prefer_reencode;
}
if let Some((display_width, display_height)) = display {
profile.display_width = display_width.max(2);
@ -913,6 +925,7 @@ fn run_preview_feed(
requested_width: profile.requested_width.max(0) as u32,
requested_height: profile.requested_height.max(0) as u32,
requested_fps: profile.requested_fps,
prefer_reencode: profile.prefer_reencode,
};
match cli.capture_video(Request::new(req)).await {
Ok(mut stream) => {

View File

@ -790,6 +790,7 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
choice.height,
choice.fps,
choice.max_bitrate_kbit,
choice.preset != CaptureSizePreset::Source,
);
rebind_inline_preview(preview, &widgets, monitor_id);
rebind_popout_preview(preview, &popouts, monitor_id);
@ -830,6 +831,7 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
choice.height,
choice.fps,
choice.max_bitrate_kbit,
choice.preset != CaptureSizePreset::Source,
);
rebind_inline_preview(preview, &widgets, monitor_id);
rebind_popout_preview(preview, &popouts, monitor_id);
@ -871,6 +873,7 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
choice.height,
choice.fps,
choice.max_bitrate_kbit,
choice.preset != CaptureSizePreset::Source,
);
rebind_inline_preview(preview, &widgets, monitor_id);
rebind_popout_preview(preview, &popouts, monitor_id);
@ -1811,6 +1814,7 @@ pub fn run_gui_launcher(server_addr: String) -> Result<()> {
capture.height,
capture.fps,
capture.max_bitrate_kbit,
capture.preset != CaptureSizePreset::Source,
);
preview.set_breakout_profile(
monitor_id,

View File

@ -1055,6 +1055,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_keep_aspect_ratio(true);
picture.set_size_request(220, 124);
let preview_box = gtk::Box::new(gtk::Orientation::Vertical, 0);

View File

@ -237,7 +237,7 @@ pub fn open_popout_window(
let picture = gtk::Picture::new();
picture.set_hexpand(true);
picture.set_vexpand(true);
picture.set_can_shrink(false);
picture.set_can_shrink(true);
picture.set_keep_aspect_ratio(true);
picture.set_size_request(breakout_size.width, breakout_size.height);
let root = gtk::Box::new(gtk::Orientation::Vertical, 0);

View File

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

View File

@ -11,6 +11,7 @@ message MonitorRequest {
uint32 requested_width = 3;
uint32 requested_height = 4;
uint32 requested_fps = 5;
bool prefer_reencode = 6;
}
message VideoPacket {
uint32 id = 1;

View File

@ -17,6 +17,6 @@ mod tests {
#[test]
fn banner_includes_version() {
assert_eq!(banner("0.11.1"), "lesavka-common CLI (v0.11.1)");
assert_eq!(banner("0.11.2"), "lesavka-common CLI (v0.11.2)");
}
}

View File

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

View File

@ -114,6 +114,7 @@ impl Handler {
requested_width = req.requested_width,
requested_height = req.requested_height,
requested_fps = req.requested_fps,
prefer_reencode = req.prefer_reencode,
"🎥 capture_video opened"
);
debug!(rpc_id, "🎥 streaming {dev}");
@ -127,6 +128,7 @@ impl Handler {
req.requested_width,
req.requested_height,
req.requested_fps,
req.prefer_reencode,
)
.await
.map_err(|e| Status::internal(format!("{e:#}")))?;

View File

@ -233,7 +233,7 @@ struct EyeCaptureRequest {
requested_height: u32,
requested_fps: u32,
max_bitrate_kbit: u32,
downscale: bool,
reencode: bool,
}
fn normalize_eye_capture_request(
@ -241,6 +241,7 @@ fn normalize_eye_capture_request(
requested_height: u32,
requested_fps: u32,
max_bitrate_kbit: u32,
prefer_reencode: bool,
) -> EyeCaptureRequest {
let (source_width, source_height, source_fps) = eye_source_profile();
let requested_width = if requested_width == 0 {
@ -258,14 +259,21 @@ fn normalize_eye_capture_request(
} else {
requested_fps.max(1).min(source_fps.max(1))
};
let max_bitrate_kbit = max_bitrate_kbit.max(800);
let downscale = requested_width < source_width || requested_height < source_height;
let baseline_source_bitrate_kbit = 12_000;
let reencode = prefer_reencode
|| downscale
|| requested_fps != source_fps.max(1)
|| max_bitrate_kbit != baseline_source_bitrate_kbit;
EyeCaptureRequest {
source_width,
source_height,
requested_width,
requested_height,
requested_fps,
max_bitrate_kbit: max_bitrate_kbit.max(800),
downscale: requested_width < source_width || requested_height < source_height,
max_bitrate_kbit,
reencode,
}
}
@ -329,7 +337,7 @@ async fn wait_for_eye_device(dev: &str, eye: &str) -> anyhow::Result<()> {
/// frames before they build up in gRPC queues and destabilize downstream playback.
#[cfg(coverage)]
pub async fn eye_ball(dev: &str, id: u32, _max_bitrate_kbit: u32) -> anyhow::Result<VideoStream> {
eye_ball_with_request(dev, id, _max_bitrate_kbit, 0, 0, 0).await
eye_ball_with_request(dev, id, _max_bitrate_kbit, 0, 0, 0, false).await
}
#[cfg(coverage)]
@ -340,6 +348,7 @@ pub async fn eye_ball_with_request(
_requested_width: u32,
_requested_height: u32,
_requested_fps: u32,
_prefer_reencode: bool,
) -> anyhow::Result<VideoStream> {
let _ = EYE_ID[id as usize];
if dev.contains('"') {
@ -375,7 +384,7 @@ pub async fn eye_ball_with_request(
#[cfg(not(coverage))]
pub async fn eye_ball(dev: &str, id: u32, max_bitrate_kbit: u32) -> anyhow::Result<VideoStream> {
eye_ball_with_request(dev, id, max_bitrate_kbit, 0, 0, 0).await
eye_ball_with_request(dev, id, max_bitrate_kbit, 0, 0, 0, false).await
}
#[cfg(not(coverage))]
@ -386,6 +395,7 @@ pub async fn eye_ball_with_request(
requested_width: u32,
requested_height: u32,
requested_fps: u32,
prefer_reencode: bool,
) -> anyhow::Result<VideoStream> {
let eye = EYE_ID[id as usize];
gst::init().context("gst init")?;
@ -395,6 +405,7 @@ pub async fn eye_ball_with_request(
requested_height,
requested_fps,
max_bitrate_kbit,
prefer_reencode,
);
let target_fps = if requested_fps > 0 {
request.requested_fps
@ -445,7 +456,7 @@ pub async fn eye_ball_with_request(
dev.eq_ignore_ascii_case("testsrc") || dev.eq_ignore_ascii_case("videotestsrc");
let server_encoder_label = if use_test_src {
"x264enc(testsrc)".to_string()
} else if request.downscale {
} else if request.reencode {
"x264enc".to_string()
} else {
"source-pass-through".to_string()
@ -466,7 +477,7 @@ pub async fn eye_ball_with_request(
appsink name=sink emit-signals=true max-buffers={appsink_buffers} drop=true",
request.requested_width, request.requested_height, request.requested_fps,
)
} else if request.downscale {
} else if request.reencode {
format!(
"v4l2src name=cam_{eye} device=\"{dev}\" io-mode=mmap do-timestamp=true ! \
queue max-size-buffers={queue_buffers} max-size-time=0 max-size-bytes=0 leaky=downstream ! \
@ -693,3 +704,37 @@ pub async fn eye_ball_with_request(
inner: ReceiverStream::new(rx),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn source_profile_stays_pass_through_without_explicit_reencode_request() {
let request = normalize_eye_capture_request(1920, 1080, 30, 12_000, false);
assert_eq!(request.requested_width, 1920);
assert_eq!(request.requested_height, 1080);
assert_eq!(request.requested_fps, 30);
assert!(!request.reencode);
}
#[test]
fn explicit_reencode_preference_forces_same_size_reencode() {
let request = normalize_eye_capture_request(1920, 1080, 30, 12_000, true);
assert_eq!(request.requested_width, 1920);
assert_eq!(request.requested_height, 1080);
assert_eq!(request.requested_fps, 30);
assert!(request.reencode);
}
#[test]
fn bitrate_or_fps_change_forces_reencode_even_at_source_size() {
let bitrate_request = normalize_eye_capture_request(1920, 1080, 30, 2_500, false);
let fps_request = normalize_eye_capture_request(1920, 1080, 24, 12_000, false);
assert!(bitrate_request.reencode);
assert!(fps_request.reencode);
}
}

View File

@ -152,6 +152,7 @@ mod server_main_binary {
requested_width: 0,
requested_height: 0,
requested_fps: 0,
prefer_reencode: false,
}))
.await
});
@ -205,6 +206,7 @@ mod server_main_binary {
requested_width: 0,
requested_height: 0,
requested_fps: 0,
prefer_reencode: false,
};
let rt = tokio::runtime::Runtime::new().expect("runtime");

View File

@ -81,6 +81,7 @@ mod server_main_rpc {
requested_width: 0,
requested_height: 0,
requested_fps: 0,
prefer_reencode: false,
}))
.await
});
@ -104,6 +105,7 @@ mod server_main_rpc {
requested_width: 0,
requested_height: 0,
requested_fps: 0,
prefer_reencode: false,
}))
.await
});
@ -133,6 +135,7 @@ mod server_main_rpc {
requested_width: 0,
requested_height: 0,
requested_fps: 0,
prefer_reencode: false,
}))
.await
})
@ -206,6 +209,7 @@ mod server_main_rpc {
requested_width: 0,
requested_height: 0,
requested_fps: 0,
prefer_reencode: false,
};
let rt = tokio::runtime::Runtime::new().expect("runtime");