lesavka: clean up stream profiles for 0.7.1
This commit is contained in:
parent
d4cc469173
commit
b7803dc432
15
README.md
15
README.md
@ -19,10 +19,10 @@ Lesavka is a remote-control and remote-presence client/server pair built to make
|
||||
## Current Capabilities
|
||||
- KDE launcher integration for the local client install
|
||||
- Session console with copy and breakout support
|
||||
- Adjustable capture and breakout sizing for each eye feed
|
||||
- Adjustable capture and breakout sizing for each eye feed with standard-size profiles
|
||||
- Automatic redocking of broken-out eye windows when the relay disconnects
|
||||
- Modifier-aware keyboard relay that now supports `Shift+a -> A`
|
||||
- Server and client build identity visible in the launcher
|
||||
- Client and server semver visible in the launcher
|
||||
|
||||
## Install / Update
|
||||
|
||||
@ -40,6 +40,17 @@ ssh theia 'cd /var/src/lesavka && git pull --ff-only && sudo LESAVKA_REF=master
|
||||
|
||||
These install scripts are intended to be the trusted, repeatable delivery path. They pull the requested ref, ensure the environment is ready, build the correct binaries, install them into sensible system paths, and refresh the launched application or service.
|
||||
|
||||
## Capture / Display Profiles
|
||||
- `Source` capture keeps the HDMI device's own H.264 stream and asks the server to pace it. That is the lowest-overhead path, but its keyframe cadence comes from the capture hardware.
|
||||
- Standard capture profiles such as `360p`, `540p`, `720p`, `900p`, and `1080p` force the server to re-encode the eye feed at a known resolution, fps, and bitrate tier.
|
||||
- Breakout display profiles use standard client-side sizes plus `Source Size` and `Display Size`, so the popout window size is explicit instead of implied.
|
||||
|
||||
## Versioning
|
||||
- Lesavka uses semver: `<major>.<minor>.<patch>`.
|
||||
- Bump `patch` for bug fixes, stability work, profile tuning, and install-script fixes that should not change the operator workflow.
|
||||
- Bump `minor` for new user-visible features, diagnostics, launcher controls, or protocol additions that remain backward-compatible when client and server are updated together.
|
||||
- Bump `major` for breaking changes to protocol, install behavior, or operator workflows that require a deliberate upgrade step.
|
||||
|
||||
## Operator Workflow
|
||||
1. Install or update the client and server through the install scripts.
|
||||
2. Launch `Lesavka` from the KDE application launcher or run `lesavka`.
|
||||
|
||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -80,27 +80,6 @@ impl CameraCapture {
|
||||
.unwrap_or(false)
|
||||
});
|
||||
let jpeg_quality = env_u32("LESAVKA_CAM_JPEG_QUALITY", 85).clamp(1, 100);
|
||||
let (enc, kf_prop, kf_val) = if use_mjpg_source && !output_mjpeg {
|
||||
("x264enc", "key-int-max", "30")
|
||||
} else {
|
||||
Self::choose_encoder()
|
||||
};
|
||||
if use_mjpg_source && !output_mjpeg {
|
||||
tracing::info!("📸 using MJPG source with software encode");
|
||||
}
|
||||
let _enc_opts = if enc == "x264enc" {
|
||||
let bitrate_kbit = env_u32("LESAVKA_CAM_H264_KBIT", 4500);
|
||||
format!(
|
||||
"{enc} tune=zerolatency speed-preset=faster bitrate={bitrate_kbit} {kf_prop}={kf_val}"
|
||||
)
|
||||
} else {
|
||||
format!("{enc} {kf_prop}={kf_val}")
|
||||
};
|
||||
if output_mjpeg {
|
||||
tracing::info!("📸 outputting MJPEG frames for UVC (quality={jpeg_quality})");
|
||||
} else {
|
||||
tracing::info!("📸 using encoder element: {enc}");
|
||||
}
|
||||
let width = cfg
|
||||
.map(|cfg| cfg.width)
|
||||
.unwrap_or_else(|| env_u32("LESAVKA_CAM_WIDTH", 1280));
|
||||
@ -111,6 +90,28 @@ impl CameraCapture {
|
||||
.map(|cfg| cfg.fps)
|
||||
.unwrap_or_else(|| env_u32("LESAVKA_CAM_FPS", 25))
|
||||
.max(1);
|
||||
let keyframe_interval = env_u32("LESAVKA_CAM_KEYFRAME_INTERVAL", fps.min(15)).clamp(1, fps);
|
||||
let (enc, kf_prop) = if use_mjpg_source && !output_mjpeg {
|
||||
("x264enc", "key-int-max")
|
||||
} else {
|
||||
Self::choose_encoder()
|
||||
};
|
||||
if use_mjpg_source && !output_mjpeg {
|
||||
tracing::info!("📸 using MJPG source with software encode");
|
||||
}
|
||||
let _enc_opts = if enc == "x264enc" {
|
||||
let bitrate_kbit = env_u32("LESAVKA_CAM_H264_KBIT", 4500);
|
||||
format!(
|
||||
"{enc} tune=zerolatency speed-preset=faster bitrate={bitrate_kbit} {kf_prop}={keyframe_interval}"
|
||||
)
|
||||
} else {
|
||||
format!("{enc} {kf_prop}={keyframe_interval}")
|
||||
};
|
||||
if output_mjpeg {
|
||||
tracing::info!("📸 outputting MJPEG frames for UVC (quality={jpeg_quality})");
|
||||
} else {
|
||||
tracing::info!("📸 using encoder element: {enc}");
|
||||
}
|
||||
#[cfg(not(coverage))]
|
||||
let have_nvvidconv = gst::ElementFactory::find("nvvidconv").is_some();
|
||||
let (src_caps, preenc) = match enc {
|
||||
@ -336,32 +337,28 @@ impl CameraCapture {
|
||||
}
|
||||
|
||||
#[cfg(not(coverage))]
|
||||
fn choose_encoder() -> (&'static str, &'static str, &'static str) {
|
||||
fn choose_encoder() -> (&'static str, &'static str) {
|
||||
match () {
|
||||
_ if gst::ElementFactory::find("nvh264enc").is_some() => {
|
||||
("nvh264enc", "gop-size", "30")
|
||||
}
|
||||
_ if gst::ElementFactory::find("nvh264enc").is_some() => ("nvh264enc", "gop-size"),
|
||||
_ if gst::ElementFactory::find("vaapih264enc").is_some() => {
|
||||
("vaapih264enc", "keyframe-period", "30")
|
||||
("vaapih264enc", "keyframe-period")
|
||||
}
|
||||
_ if gst::ElementFactory::find("v4l2h264enc").is_some() => {
|
||||
("v4l2h264enc", "idrcount", "30")
|
||||
}
|
||||
_ => ("x264enc", "key-int-max", "30"),
|
||||
_ if gst::ElementFactory::find("v4l2h264enc").is_some() => ("v4l2h264enc", "idrcount"),
|
||||
_ => ("x264enc", "key-int-max"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(coverage)]
|
||||
fn choose_encoder() -> (&'static str, &'static str, &'static str) {
|
||||
fn choose_encoder() -> (&'static str, &'static str) {
|
||||
match std::env::var("LESAVKA_CAM_TEST_ENCODER")
|
||||
.ok()
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
{
|
||||
Some("nvh264enc") => ("nvh264enc", "gop-size", "30"),
|
||||
Some("vaapih264enc") => ("vaapih264enc", "keyframe-period", "30"),
|
||||
Some("v4l2h264enc") => ("v4l2h264enc", "idrcount", "30"),
|
||||
_ => ("x264enc", "key-int-max", "30"),
|
||||
Some("nvh264enc") => ("nvh264enc", "gop-size"),
|
||||
Some("vaapih264enc") => ("vaapih264enc", "keyframe-period"),
|
||||
Some("v4l2h264enc") => ("v4l2h264enc", "idrcount"),
|
||||
_ => ("x264enc", "key-int-max"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,9 +73,11 @@ pub struct SnapshotReport {
|
||||
pub breakout_display: String,
|
||||
pub left_surface: String,
|
||||
pub left_capture_profile: String,
|
||||
pub left_capture_transport: String,
|
||||
pub left_breakout_profile: String,
|
||||
pub right_surface: String,
|
||||
pub right_capture_profile: String,
|
||||
pub right_capture_transport: String,
|
||||
pub right_breakout_profile: String,
|
||||
pub selected_camera: Option<String>,
|
||||
pub selected_microphone: Option<String>,
|
||||
@ -119,30 +121,32 @@ impl SnapshotReport {
|
||||
left_surface: state.display_surface(0).label().to_string(),
|
||||
left_capture_profile: format!(
|
||||
"{} | {}x{} | {} fps | {} kbit",
|
||||
left_capture.preset.as_id(),
|
||||
left_capture.preset.label(),
|
||||
left_capture.width,
|
||||
left_capture.height,
|
||||
left_capture.fps,
|
||||
left_capture.max_bitrate_kbit
|
||||
),
|
||||
left_capture_transport: left_capture.preset.transport_label().to_string(),
|
||||
left_breakout_profile: format!(
|
||||
"{} | {}x{}",
|
||||
left_breakout.preset.as_id(),
|
||||
left_breakout.preset.label(),
|
||||
left_breakout.width,
|
||||
left_breakout.height
|
||||
),
|
||||
right_surface: state.display_surface(1).label().to_string(),
|
||||
right_capture_profile: format!(
|
||||
"{} | {}x{} | {} fps | {} kbit",
|
||||
right_capture.preset.as_id(),
|
||||
right_capture.preset.label(),
|
||||
right_capture.width,
|
||||
right_capture.height,
|
||||
right_capture.fps,
|
||||
right_capture.max_bitrate_kbit
|
||||
),
|
||||
right_capture_transport: right_capture.preset.transport_label().to_string(),
|
||||
right_breakout_profile: format!(
|
||||
"{} | {}x{}",
|
||||
right_breakout.preset.as_id(),
|
||||
right_breakout.preset.label(),
|
||||
right_breakout.width,
|
||||
right_breakout.height
|
||||
),
|
||||
@ -185,10 +189,12 @@ impl SnapshotReport {
|
||||
let _ = writeln!(text, "left eye");
|
||||
let _ = writeln!(text, " surface: {}", self.left_surface);
|
||||
let _ = writeln!(text, " capture: {}", self.left_capture_profile);
|
||||
let _ = writeln!(text, " transport: {}", self.left_capture_transport);
|
||||
let _ = writeln!(text, " breakout: {}", self.left_breakout_profile);
|
||||
let _ = writeln!(text, "right eye");
|
||||
let _ = writeln!(text, " surface: {}", self.right_surface);
|
||||
let _ = writeln!(text, " capture: {}", self.right_capture_profile);
|
||||
let _ = writeln!(text, " transport: {}", self.right_capture_transport);
|
||||
let _ = writeln!(text, " breakout: {}", self.right_breakout_profile);
|
||||
let _ = writeln!(text);
|
||||
let _ = writeln!(text, "device staging");
|
||||
@ -327,6 +333,27 @@ fn recommendations_for(state: &LauncherState, log: &DiagnosticsLog) -> Vec<Strin
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
let source_passthrough = state
|
||||
.capture_sizes
|
||||
.iter()
|
||||
.any(|preset| matches!(preset, super::state::CaptureSizePreset::Source));
|
||||
if source_passthrough {
|
||||
items.push(
|
||||
"Source capture uses the HDMI device's own H.264 stream. If motion damage lingers for seconds, switch that eye to 1080p, 900p, or 720p so the server re-encodes with a tighter keyframe cadence."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
if let Some(sample) = log.latest()
|
||||
&& sample.video_loss_pct < 0.5
|
||||
&& sample.dropped_frames == 0
|
||||
&& ((sample.left_server_fps - sample.left_receive_fps) > 6.0
|
||||
|| (sample.right_server_fps - sample.right_receive_fps) > 6.0)
|
||||
{
|
||||
items.push(
|
||||
"Receive fps is well below the target without packet loss. That usually points at source cadence or local decode pressure more than WAN loss, so compare Source against 1080p/900p and watch which side stays steadier."
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
if state.breakout_count() == 2 {
|
||||
items.push(
|
||||
"Both eye feeds are broken out right now. If the client starts struggling, compare in-launcher preview smoothness against full-window decode."
|
||||
@ -415,6 +442,7 @@ mod tests {
|
||||
assert!(report.status.contains("mode=remote"));
|
||||
assert!(report.client_version.starts_with("0."));
|
||||
assert!(report.left_capture_profile.contains("fps"));
|
||||
assert_eq!(report.left_capture_transport, "source pass-through");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -441,6 +469,7 @@ mod tests {
|
||||
assert!(text.contains("Lesavka Diagnostics"));
|
||||
assert!(text.contains("client: v"));
|
||||
assert!(text.contains("left eye"));
|
||||
assert!(text.contains("transport:"));
|
||||
assert!(text.contains("recommendations"));
|
||||
}
|
||||
|
||||
|
||||
@ -49,6 +49,7 @@ impl DisplaySurface {
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum BreakoutSizePreset {
|
||||
P360,
|
||||
P540,
|
||||
P720,
|
||||
P900,
|
||||
@ -61,6 +62,7 @@ pub enum BreakoutSizePreset {
|
||||
impl BreakoutSizePreset {
|
||||
pub fn as_id(self) -> &'static str {
|
||||
match self {
|
||||
Self::P360 => "360p",
|
||||
Self::P540 => "540p",
|
||||
Self::P720 => "720p",
|
||||
Self::P900 => "900p",
|
||||
@ -73,6 +75,7 @@ impl BreakoutSizePreset {
|
||||
|
||||
pub fn from_id(raw: &str) -> Option<Self> {
|
||||
match raw {
|
||||
"360p" => Some(Self::P360),
|
||||
"540p" => Some(Self::P540),
|
||||
"720p" => Some(Self::P720),
|
||||
"900p" => Some(Self::P900),
|
||||
@ -83,10 +86,24 @@ impl BreakoutSizePreset {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::P360 => "360p",
|
||||
Self::P540 => "540p",
|
||||
Self::P720 => "720p",
|
||||
Self::P900 => "900p",
|
||||
Self::P1080 => "1080p",
|
||||
Self::P1440 => "1440p",
|
||||
Self::Source => "Source",
|
||||
Self::FillDisplay => "Display",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum CaptureSizePreset {
|
||||
P360,
|
||||
P540,
|
||||
P720,
|
||||
P900,
|
||||
@ -98,6 +115,7 @@ pub enum CaptureSizePreset {
|
||||
impl CaptureSizePreset {
|
||||
pub fn as_id(self) -> &'static str {
|
||||
match self {
|
||||
Self::P360 => "360p",
|
||||
Self::P540 => "540p",
|
||||
Self::P720 => "720p",
|
||||
Self::P900 => "900p",
|
||||
@ -109,6 +127,7 @@ impl CaptureSizePreset {
|
||||
|
||||
pub fn from_id(raw: &str) -> Option<Self> {
|
||||
match raw {
|
||||
"360p" => Some(Self::P360),
|
||||
"540p" => Some(Self::P540),
|
||||
"720p" => Some(Self::P720),
|
||||
"900p" => Some(Self::P900),
|
||||
@ -118,6 +137,25 @@ impl CaptureSizePreset {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::P360 => "360p",
|
||||
Self::P540 => "540p",
|
||||
Self::P720 => "720p",
|
||||
Self::P900 => "900p",
|
||||
Self::P1080 => "1080p",
|
||||
Self::P1440 => "1440p",
|
||||
Self::Source => "Source",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transport_label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Source => "source pass-through",
|
||||
_ => "server re-encode",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@ -514,6 +552,9 @@ fn breakout_size_choice(
|
||||
let display_width = display_fill.width.max(1) as i32;
|
||||
let display_height = display_fill.height.max(1) as i32;
|
||||
let (width, height) = match preset {
|
||||
BreakoutSizePreset::P360 => {
|
||||
fit_standard_dimensions(physical_width, physical_height, 640, 360)
|
||||
}
|
||||
BreakoutSizePreset::P540 => {
|
||||
fit_standard_dimensions(physical_width, physical_height, 960, 540)
|
||||
}
|
||||
@ -552,6 +593,7 @@ fn breakout_size_options(
|
||||
let mut options = Vec::new();
|
||||
for preset in [
|
||||
BreakoutSizePreset::Source,
|
||||
BreakoutSizePreset::P360,
|
||||
BreakoutSizePreset::P540,
|
||||
BreakoutSizePreset::P720,
|
||||
BreakoutSizePreset::P900,
|
||||
@ -581,10 +623,14 @@ fn capture_size_choice(source: PreviewSourceSize, preset: CaptureSizePreset) ->
|
||||
let source_height = source.height.max(1) as i32;
|
||||
let source_fps = source.fps.max(1);
|
||||
let (width, height, fps, max_bitrate_kbit) = match preset {
|
||||
CaptureSizePreset::P540 => {
|
||||
CaptureSizePreset::P360 => {
|
||||
let (width, height) = fit_standard_dimensions(source_width, source_height, 640, 360);
|
||||
(width, height, source_fps.min(15), 2_500)
|
||||
}
|
||||
CaptureSizePreset::P540 => {
|
||||
let (width, height) = fit_standard_dimensions(source_width, source_height, 960, 540);
|
||||
(width, height, source_fps.min(20), 4_000)
|
||||
}
|
||||
CaptureSizePreset::P720 => {
|
||||
let (width, height) = fit_standard_dimensions(source_width, source_height, 1280, 720);
|
||||
(width, height, source_fps.min(24), 6_000)
|
||||
@ -631,6 +677,7 @@ fn capture_size_options(source: PreviewSourceSize) -> Vec<CaptureSizeChoice> {
|
||||
let mut options = Vec::new();
|
||||
for preset in [
|
||||
CaptureSizePreset::Source,
|
||||
CaptureSizePreset::P360,
|
||||
CaptureSizePreset::P540,
|
||||
CaptureSizePreset::P720,
|
||||
CaptureSizePreset::P900,
|
||||
@ -860,15 +907,20 @@ mod tests {
|
||||
|
||||
state.set_capture_size_preset(0, CaptureSizePreset::P540);
|
||||
let compact_capture = state.capture_size_choice(0);
|
||||
assert_eq!(compact_capture.width, 640);
|
||||
assert_eq!(compact_capture.height, 360);
|
||||
assert_eq!(compact_capture.fps, 15);
|
||||
assert_eq!(compact_capture.max_bitrate_kbit, 2_500);
|
||||
assert_eq!(compact_capture.width, 960);
|
||||
assert_eq!(compact_capture.height, 540);
|
||||
assert_eq!(compact_capture.fps, 20);
|
||||
assert_eq!(compact_capture.max_bitrate_kbit, 4_000);
|
||||
|
||||
let display = state.breakout_size_choice(0);
|
||||
assert_eq!(display.width, 1920);
|
||||
assert_eq!(display.height, 1080);
|
||||
|
||||
state.set_breakout_size_preset(0, BreakoutSizePreset::P360);
|
||||
let smaller = state.breakout_size_choice(0);
|
||||
assert_eq!(smaller.width, 640);
|
||||
assert_eq!(smaller.height, 360);
|
||||
|
||||
state.set_breakout_size_preset(0, BreakoutSizePreset::P540);
|
||||
let compact = state.breakout_size_choice(0);
|
||||
assert_eq!(compact.width, 960);
|
||||
@ -959,6 +1011,10 @@ mod tests {
|
||||
|
||||
state.set_capture_size_preset(0, CaptureSizePreset::P540);
|
||||
let compact = state.capture_size_choice(0);
|
||||
assert_eq!(compact.fps, 15);
|
||||
assert_eq!(compact.fps, 20);
|
||||
|
||||
state.set_capture_size_preset(0, CaptureSizePreset::P360);
|
||||
let small = state.capture_size_choice(0);
|
||||
assert_eq!(small.fps, 15);
|
||||
}
|
||||
}
|
||||
|
||||
@ -831,12 +831,20 @@ pub fn sync_capture_size_combo(
|
||||
for option in options {
|
||||
let label = match option.preset {
|
||||
CaptureSizePreset::Source => format!(
|
||||
"{}x{} @ {} fps • {} kbit (Source Size)",
|
||||
option.width, option.height, option.fps, option.max_bitrate_kbit
|
||||
"{} • {}x{} @ {} fps • {} kbit (Pass-through)",
|
||||
option.preset.label(),
|
||||
option.width,
|
||||
option.height,
|
||||
option.fps,
|
||||
option.max_bitrate_kbit
|
||||
),
|
||||
_ => format!(
|
||||
"{}x{} @ {} fps • {} kbit",
|
||||
option.width, option.height, option.fps, option.max_bitrate_kbit
|
||||
"{} • {}x{} @ {} fps • {} kbit",
|
||||
option.preset.label(),
|
||||
option.width,
|
||||
option.height,
|
||||
option.fps,
|
||||
option.max_bitrate_kbit
|
||||
),
|
||||
};
|
||||
combo.append(Some(option.preset.as_id()), &label);
|
||||
@ -853,12 +861,27 @@ pub fn sync_breakout_size_combo(
|
||||
for option in options {
|
||||
let label = match option.preset {
|
||||
BreakoutSizePreset::Source => {
|
||||
format!("{}x{} (Source Size)", option.width, option.height)
|
||||
format!(
|
||||
"{} • {}x{} (Source Size)",
|
||||
option.preset.label(),
|
||||
option.width,
|
||||
option.height
|
||||
)
|
||||
}
|
||||
BreakoutSizePreset::FillDisplay => {
|
||||
format!("{}x{} (Display Size)", option.width, option.height)
|
||||
format!(
|
||||
"{} • {}x{} (Display Size)",
|
||||
option.preset.label(),
|
||||
option.width,
|
||||
option.height
|
||||
)
|
||||
}
|
||||
_ => format!("{}x{}", option.width, option.height),
|
||||
_ => format!(
|
||||
"{} • {}x{}",
|
||||
option.preset.label(),
|
||||
option.width,
|
||||
option.height
|
||||
),
|
||||
};
|
||||
combo.append(Some(option.preset.as_id()), &label);
|
||||
}
|
||||
@ -992,14 +1015,14 @@ fn build_display_pane(title: &str, capture_path: &str) -> DisplayPaneWidgets {
|
||||
stream_status.set_tooltip_text(Some("Connect relay to preview."));
|
||||
let capture_combo = gtk::ComboBoxText::new();
|
||||
capture_combo.set_tooltip_text(Some(
|
||||
"Choose the server-side capture profile for this eye feed: resolution, target fps, and bitrate.",
|
||||
"Choose the server-side capture profile for this eye feed. Source keeps the HDMI device's own H.264 stream; the standard sizes force a server re-encode at a known resolution, fps, and bitrate.",
|
||||
));
|
||||
capture_combo.set_size_request(272, -1);
|
||||
capture_combo.set_size_request(360, -1);
|
||||
let breakout_combo = gtk::ComboBoxText::new();
|
||||
breakout_combo.set_tooltip_text(Some(
|
||||
"Choose the client-side breakout window size for this eye feed.",
|
||||
"Choose the client-side breakout window size for this eye feed. Source Size preserves the feed's own dimensions; Display Size fills the effective monitor size.",
|
||||
));
|
||||
breakout_combo.set_size_request(180, -1);
|
||||
breakout_combo.set_size_request(260, -1);
|
||||
let action_button = gtk::Button::with_label("Break Out");
|
||||
stabilize_button(&action_button, 104);
|
||||
action_button.set_halign(gtk::Align::End);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@ -17,6 +17,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn banner_includes_version() {
|
||||
assert_eq!(banner("0.7.0"), "lesavka-common CLI (v0.7.0)");
|
||||
assert_eq!(banner("0.7.1"), "lesavka-common CLI (v0.7.1)");
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
@ -391,6 +391,11 @@ pub async fn eye_ball_with_request(
|
||||
|
||||
let queue_buffers = env_u32("LESAVKA_EYE_QUEUE_BUFFERS", 8).max(1);
|
||||
let appsink_buffers = env_u32("LESAVKA_EYE_APPSINK_BUFFERS", 8).max(1);
|
||||
let keyframe_interval = env_u32(
|
||||
"LESAVKA_EYE_KEYFRAME_INTERVAL",
|
||||
request.requested_fps.max(1).min(15),
|
||||
)
|
||||
.clamp(1, request.requested_fps.max(1));
|
||||
let use_test_src =
|
||||
dev.eq_ignore_ascii_case("testsrc") || dev.eq_ignore_ascii_case("videotestsrc");
|
||||
if !use_test_src {
|
||||
@ -402,7 +407,7 @@ pub async fn eye_ball_with_request(
|
||||
"videotestsrc name=cam_{eye} is-live=true pattern=smpte ! \
|
||||
video/x-raw,width={},height={},framerate={}/1 ! \
|
||||
queue max-size-buffers={queue_buffers} max-size-time=0 max-size-bytes=0 leaky=downstream ! \
|
||||
x264enc tune=zerolatency speed-preset=veryfast bitrate={test_bitrate} key-int-max=30 ! \
|
||||
x264enc tune=zerolatency speed-preset=veryfast bitrate={test_bitrate} key-int-max={keyframe_interval} ! \
|
||||
h264parse disable-passthrough=true config-interval=-1 ! \
|
||||
video/x-h264,stream-format=byte-stream,alignment=au ! \
|
||||
appsink name=sink emit-signals=true max-buffers={appsink_buffers} drop=true",
|
||||
@ -422,7 +427,7 @@ pub async fn eye_ball_with_request(
|
||||
request.requested_height,
|
||||
request.requested_fps,
|
||||
request.max_bitrate_kbit,
|
||||
request.requested_fps.max(1),
|
||||
keyframe_interval,
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user