lesavka: force reencode when requested
This commit is contained in:
parent
bb4921e7e9
commit
7cb0a4d655
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.11.1"
|
||||
version = "0.11.2"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.11.1"
|
||||
version = "0.11.2"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)");
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.11.1"
|
||||
version = "0.11.2"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
@ -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:#}")))?;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user