fix(sync): honor configurable uvc codec

This commit is contained in:
Brad Stein 2026-04-27 13:50:48 -03:00
parent f477332834
commit ab00babf99
8 changed files with 92 additions and 9 deletions

6
Cargo.lock generated
View File

@ -1642,7 +1642,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]] [[package]]
name = "lesavka_client" name = "lesavka_client"
version = "0.14.19" version = "0.14.20"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-stream", "async-stream",
@ -1676,7 +1676,7 @@ dependencies = [
[[package]] [[package]]
name = "lesavka_common" name = "lesavka_common"
version = "0.14.19" version = "0.14.20"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
@ -1688,7 +1688,7 @@ dependencies = [
[[package]] [[package]]
name = "lesavka_server" name = "lesavka_server"
version = "0.14.19" version = "0.14.20"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",

View File

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

View File

@ -7,6 +7,12 @@ use crate::sync_probe::schedule::PulseSchedule;
use lesavka_common::lesavka::{AudioPacket, VideoPacket}; use lesavka_common::lesavka::{AudioPacket, VideoPacket};
use std::time::Duration; use std::time::Duration;
use std::time::Instant; use std::time::Instant;
#[cfg(not(coverage))]
use gstreamer as gst;
#[cfg(not(coverage))]
use gstreamer::prelude::*;
#[cfg(not(coverage))]
use gstreamer_app as gst_app;
fn stub_camera() -> CameraConfig { fn stub_camera() -> CameraConfig {
CameraConfig { CameraConfig {
@ -141,6 +147,49 @@ fn probe_video_frames_render_distinct_idle_regular_and_marker_patterns() {
assert_ne!(regular, marker); assert_ne!(regular, marker);
} }
#[cfg(not(coverage))]
fn decode_mjpeg_packet_mean_luma(packet: &VideoPacket) -> u8 {
gst::init().expect("gst init");
let pipeline = gst::parse::launch(
"appsrc name=src is-live=false format=time do-timestamp=false \
caps=image/jpeg,parsed=true ! jpegdec ! videoconvert ! \
video/x-raw,format=GRAY8 ! \
appsink name=sink emit-signals=false sync=false max-buffers=1 drop=false",
)
.expect("decode pipeline")
.downcast::<gst::Pipeline>()
.expect("pipeline");
let src = pipeline
.by_name("src")
.expect("src")
.downcast::<gst_app::AppSrc>()
.expect("appsrc");
let sink = pipeline
.by_name("sink")
.expect("sink")
.downcast::<gst_app::AppSink>()
.expect("appsink");
pipeline
.set_state(gst::State::Playing)
.expect("pipeline playing");
let mut buffer = gst::Buffer::from_slice(packet.data.clone());
if let Some(meta) = buffer.get_mut() {
meta.set_pts(Some(gst::ClockTime::from_useconds(packet.pts)));
}
src.push_buffer(buffer).expect("push buffer");
src.end_of_stream().expect("end of stream");
let sample = sink.pull_sample().expect("decoded sample");
pipeline
.set_state(gst::State::Null)
.expect("pipeline null");
let buffer = sample.buffer().expect("sample buffer");
let map = buffer.map_readable().expect("buffer readable");
let bytes = map.as_slice();
let mean = bytes.iter().map(|value| u64::from(*value)).sum::<u64>() / bytes.len().max(1) as u64;
mean.min(u64::from(u8::MAX)) as u8
}
#[test] #[test]
fn probe_video_pts_are_lag_capped_like_audio() { fn probe_video_pts_are_lag_capped_like_audio() {
let rebaser = crate::live_capture_clock::DurationPacedSourcePtsRebaser::default(); let rebaser = crate::live_capture_clock::DurationPacedSourcePtsRebaser::default();
@ -415,4 +464,11 @@ async fn runtime_probe_video_packets_change_across_a_pulse_boundary() {
assert_ne!(dark_packet.data, pulse_packet.data); assert_ne!(dark_packet.data, pulse_packet.data);
assert!(!dark_packet.data.is_empty()); assert!(!dark_packet.data.is_empty());
assert!(!pulse_packet.data.is_empty()); assert!(!pulse_packet.data.is_empty());
let dark_mean = decode_mjpeg_packet_mean_luma(&dark_packet);
let pulse_mean = decode_mjpeg_packet_mean_luma(&pulse_packet);
assert!(
pulse_mean > dark_mean.saturating_add(100),
"expected decoded pulse frame to be much brighter than decoded dark frame, got dark={dark_mean} pulse={pulse_mean}"
);
} }

View File

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

View File

@ -499,6 +499,7 @@ fi
printf 'LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=%s\n' "${LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US:-0}" printf 'LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US=%s\n' "${LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US:-0}"
printf 'LESAVKA_UPSTREAM_PAIR_SLACK_US=%s\n' "${LESAVKA_UPSTREAM_PAIR_SLACK_US:-20000}" printf 'LESAVKA_UPSTREAM_PAIR_SLACK_US=%s\n' "${LESAVKA_UPSTREAM_PAIR_SLACK_US:-20000}"
printf 'LESAVKA_UPSTREAM_STALE_DROP_MS=%s\n' "${LESAVKA_UPSTREAM_STALE_DROP_MS:-80}" printf 'LESAVKA_UPSTREAM_STALE_DROP_MS=%s\n' "${LESAVKA_UPSTREAM_STALE_DROP_MS:-80}"
printf 'LESAVKA_UVC_CODEC=%s\n' "${LESAVKA_UVC_CODEC:-mjpeg}"
} | sudo tee /etc/lesavka/server.env >/dev/null } | sudo tee /etc/lesavka/server.env >/dev/null
echo "==> 6a. Systemd units - lesavka-core" echo "==> 6a. Systemd units - lesavka-core"
@ -514,6 +515,7 @@ ExecStart=/usr/local/bin/lesavka-core.sh
RemainAfterExit=yes RemainAfterExit=yes
Environment=LESAVKA_UVC_FALLBACK=0 Environment=LESAVKA_UVC_FALLBACK=0
Environment=LESAVKA_UVC_CODEC=mjpeg Environment=LESAVKA_UVC_CODEC=mjpeg
EnvironmentFile=-/etc/lesavka/server.env
CapabilityBoundingSet=CAP_SYS_ADMIN CAP_SYS_MODULE CapabilityBoundingSet=CAP_SYS_ADMIN CAP_SYS_MODULE
AmbientCapabilities=CAP_SYS_MODULE AmbientCapabilities=CAP_SYS_MODULE
MountFlags=slave MountFlags=slave
@ -572,7 +574,7 @@ if [[ -n ${LESAVKA_ALLOW_GADGET_RESET:-} ]] || ! is_attached_state "$UDC_STATE";
LESAVKA_DETACH_CLEAR_UDC=1 \ LESAVKA_DETACH_CLEAR_UDC=1 \
LESAVKA_RELOAD_UVCVIDEO=1 \ LESAVKA_RELOAD_UVCVIDEO=1 \
LESAVKA_UVC_FALLBACK=1 \ LESAVKA_UVC_FALLBACK=1 \
LESAVKA_UVC_CODEC=mjpeg \ LESAVKA_UVC_CODEC="${LESAVKA_UVC_CODEC:-mjpeg}" \
/usr/local/bin/lesavka-core.sh /usr/local/bin/lesavka-core.sh
sudo systemctl restart lesavka-core sudo systemctl restart lesavka-core
echo "✅ lesavka-core installed and restarted..." echo "✅ lesavka-core installed and restarted..."

View File

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

View File

@ -90,6 +90,17 @@ fn select_hdmi_codec(hw_decode: bool) -> CameraCodec {
}) })
} }
fn select_uvc_codec(uvc_env: Option<&HashMap<String, String>>) -> CameraCodec {
std::env::var("LESAVKA_UVC_CODEC")
.ok()
.or_else(|| std::env::var("LESAVKA_CAM_CODEC").ok())
.or_else(|| uvc_env.and_then(|env| env.get("LESAVKA_UVC_CODEC").cloned()))
.or_else(|| uvc_env.and_then(|env| env.get("LESAVKA_CAM_CODEC").cloned()))
.as_deref()
.and_then(parse_camera_codec)
.unwrap_or(CameraCodec::Mjpeg)
}
fn select_hdmi_config(hdmi: Option<HdmiConnector>) -> CameraConfig { fn select_hdmi_config(hdmi: Option<HdmiConnector>) -> CameraConfig {
let hw_decode = has_hw_h264_decode(); let hw_decode = has_hw_h264_decode();
let (default_width, default_height) = if hw_decode { (1920, 1080) } else { (1280, 720) }; let (default_width, default_height) = if hw_decode { (1920, 1080) } else { (1280, 720) };
@ -144,10 +155,11 @@ fn select_uvc_config() -> CameraConfig {
}) })
}) })
.unwrap_or(25); .unwrap_or(25);
let codec = select_uvc_codec(None);
CameraConfig { CameraConfig {
output: CameraOutput::Uvc, output: CameraOutput::Uvc,
codec: CameraCodec::Mjpeg, codec,
width, width,
height, height,
fps, fps,
@ -182,10 +194,11 @@ fn select_uvc_config() -> CameraConfig {
}) })
}) })
.unwrap_or(25); .unwrap_or(25);
let codec = select_uvc_codec(Some(&uvc_env));
CameraConfig { CameraConfig {
output: CameraOutput::Uvc, output: CameraOutput::Uvc,
codec: CameraCodec::Mjpeg, codec,
width, width,
height, height,
fps, fps,

View File

@ -32,6 +32,18 @@ fn camera_config_env_override_prefers_uvc_values() {
}); });
} }
#[test]
#[serial]
fn camera_config_env_override_honors_uvc_codec() {
with_var("LESAVKA_CAM_OUTPUT", Some("uvc"), || {
with_var("LESAVKA_UVC_CODEC", Some("h264"), || {
let cfg = update_camera_config();
assert_eq!(cfg.output, CameraOutput::Uvc);
assert_eq!(cfg.codec, CameraCodec::H264);
});
});
}
#[test] #[test]
#[serial] #[serial]
fn hdmi_camera_profile_honors_installed_1080p_override() { fn hdmi_camera_profile_honors_installed_1080p_override() {