fix(sync): honor configurable uvc codec
This commit is contained in:
parent
f477332834
commit
ab00babf99
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -1642,7 +1642,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_client"
|
||||
version = "0.14.19"
|
||||
version = "0.14.20"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-stream",
|
||||
@ -1676,7 +1676,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_common"
|
||||
version = "0.14.19"
|
||||
version = "0.14.20"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
@ -1688,7 +1688,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lesavka_server"
|
||||
version = "0.14.19"
|
||||
version = "0.14.20"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
|
||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.14.19"
|
||||
version = "0.14.20"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -7,6 +7,12 @@ use crate::sync_probe::schedule::PulseSchedule;
|
||||
use lesavka_common::lesavka::{AudioPacket, VideoPacket};
|
||||
use std::time::Duration;
|
||||
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 {
|
||||
CameraConfig {
|
||||
@ -141,6 +147,49 @@ fn probe_video_frames_render_distinct_idle_regular_and_marker_patterns() {
|
||||
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]
|
||||
fn probe_video_pts_are_lag_capped_like_audio() {
|
||||
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!(!dark_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}"
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.14.19"
|
||||
version = "0.14.20"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@ -499,6 +499,7 @@ fi
|
||||
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_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
|
||||
|
||||
echo "==> 6a. Systemd units - lesavka-core"
|
||||
@ -514,6 +515,7 @@ ExecStart=/usr/local/bin/lesavka-core.sh
|
||||
RemainAfterExit=yes
|
||||
Environment=LESAVKA_UVC_FALLBACK=0
|
||||
Environment=LESAVKA_UVC_CODEC=mjpeg
|
||||
EnvironmentFile=-/etc/lesavka/server.env
|
||||
CapabilityBoundingSet=CAP_SYS_ADMIN CAP_SYS_MODULE
|
||||
AmbientCapabilities=CAP_SYS_MODULE
|
||||
MountFlags=slave
|
||||
@ -572,7 +574,7 @@ if [[ -n ${LESAVKA_ALLOW_GADGET_RESET:-} ]] || ! is_attached_state "$UDC_STATE";
|
||||
LESAVKA_DETACH_CLEAR_UDC=1 \
|
||||
LESAVKA_RELOAD_UVCVIDEO=1 \
|
||||
LESAVKA_UVC_FALLBACK=1 \
|
||||
LESAVKA_UVC_CODEC=mjpeg \
|
||||
LESAVKA_UVC_CODEC="${LESAVKA_UVC_CODEC:-mjpeg}" \
|
||||
/usr/local/bin/lesavka-core.sh
|
||||
sudo systemctl restart lesavka-core
|
||||
echo "✅ lesavka-core installed and restarted..."
|
||||
|
||||
@ -10,7 +10,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.14.19"
|
||||
version = "0.14.20"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
@ -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 {
|
||||
let hw_decode = has_hw_h264_decode();
|
||||
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);
|
||||
let codec = select_uvc_codec(None);
|
||||
|
||||
CameraConfig {
|
||||
output: CameraOutput::Uvc,
|
||||
codec: CameraCodec::Mjpeg,
|
||||
codec,
|
||||
width,
|
||||
height,
|
||||
fps,
|
||||
@ -182,10 +194,11 @@ fn select_uvc_config() -> CameraConfig {
|
||||
})
|
||||
})
|
||||
.unwrap_or(25);
|
||||
let codec = select_uvc_codec(Some(&uvc_env));
|
||||
|
||||
CameraConfig {
|
||||
output: CameraOutput::Uvc,
|
||||
codec: CameraCodec::Mjpeg,
|
||||
codec,
|
||||
width,
|
||||
height,
|
||||
fps,
|
||||
|
||||
@ -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]
|
||||
#[serial]
|
||||
fn hdmi_camera_profile_honors_installed_1080p_override() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user