tests: protect server RC calibration defaults
This commit is contained in:
parent
1b3b8c2cbb
commit
1f6d34b6fa
6
Cargo.lock
generated
6
Cargo.lock
generated
@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.19.29"
|
version = "0.19.30"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-stream",
|
"async-stream",
|
||||||
@ -1686,7 +1686,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.19.29"
|
version = "0.19.30"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
@ -1698,7 +1698,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.19.29"
|
version = "0.19.30"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
|
|||||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_client"
|
name = "lesavka_client"
|
||||||
version = "0.19.29"
|
version = "0.19.30"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
impl LesavkaClientApp {
|
impl LesavkaClientApp {
|
||||||
/*──────────────── bundled webcam + mic stream ─────────────────*/
|
/*──────────────── bundled webcam + mic stream ─────────────────*/
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn webcam_media_loop(
|
async fn webcam_media_loop(
|
||||||
ep: Channel,
|
ep: Channel,
|
||||||
initial_camera_source: Option<String>,
|
initial_camera_source: Option<String>,
|
||||||
@ -985,6 +986,7 @@ fn retain_newest_pending_audio(pending_audio: &mut Vec<AudioPacket>) -> usize {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn emit_bundled_media(
|
fn emit_bundled_media(
|
||||||
session_id: u64,
|
session_id: u64,
|
||||||
bundle_seq: &mut u64,
|
bundle_seq: &mut u64,
|
||||||
@ -1297,9 +1299,18 @@ mod uplink_timing_tests {
|
|||||||
packet.client_send_pts_us >= packet.client_capture_pts_us,
|
packet.client_send_pts_us >= packet.client_capture_pts_us,
|
||||||
"enqueue/send stamp must be on or after the shared-clock capture estimate"
|
"enqueue/send stamp must be on or after the shared-clock capture estimate"
|
||||||
);
|
);
|
||||||
|
let capture_to_enqueue = Duration::from_micros(
|
||||||
|
packet
|
||||||
|
.client_send_pts_us
|
||||||
|
.saturating_sub(packet.client_capture_pts_us),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
capture_to_enqueue, enqueue_age,
|
||||||
|
"enqueue timing metadata should stay anchored to the pre-pop stamp"
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
packet.client_send_pts_us - packet.client_capture_pts_us <= 3_000,
|
packet.client_queue_age_ms >= duration_ms_u32(enqueue_age).saturating_add(4),
|
||||||
"capture-to-enqueue age, not async pop delay, should define the timing window"
|
"queue age should include the simulated async pop delay without mutating send timing"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1331,9 +1342,18 @@ mod uplink_timing_tests {
|
|||||||
packet.client_send_pts_us >= packet.client_capture_pts_us,
|
packet.client_send_pts_us >= packet.client_capture_pts_us,
|
||||||
"enqueue/send stamp must be on or after the shared-clock capture estimate"
|
"enqueue/send stamp must be on or after the shared-clock capture estimate"
|
||||||
);
|
);
|
||||||
|
let capture_to_enqueue = Duration::from_micros(
|
||||||
|
packet
|
||||||
|
.client_send_pts_us
|
||||||
|
.saturating_sub(packet.client_capture_pts_us),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
capture_to_enqueue, enqueue_age,
|
||||||
|
"enqueue timing metadata should stay anchored to the pre-pop stamp"
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
packet.client_send_pts_us - packet.client_capture_pts_us <= 4_000,
|
packet.client_queue_age_ms >= duration_ms_u32(enqueue_age).saturating_add(4),
|
||||||
"capture-to-enqueue age, not async pop delay, should define the timing window"
|
"queue age should include the simulated async pop delay without mutating send timing"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -279,7 +279,7 @@ fn adaptive_gray_roi_mask(frames: &[&[u8]], pixel_count: usize) -> Option<Vec<bo
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let mut scores = vec![0.0; pixel_count];
|
let mut scores = vec![0.0; pixel_count];
|
||||||
for pixel_index in 0..pixel_count {
|
for (pixel_index, score) in scores.iter_mut().enumerate() {
|
||||||
let mut min = u8::MAX;
|
let mut min = u8::MAX;
|
||||||
let mut max = u8::MIN;
|
let mut max = u8::MIN;
|
||||||
for frame in frames {
|
for frame in frames {
|
||||||
@ -287,7 +287,7 @@ fn adaptive_gray_roi_mask(frames: &[&[u8]], pixel_count: usize) -> Option<Vec<bo
|
|||||||
min = min.min(value);
|
min = min.min(value);
|
||||||
max = max.max(value);
|
max = max.max(value);
|
||||||
}
|
}
|
||||||
scores[pixel_index] = f64::from(max.saturating_sub(min)) * dark_roi_factor(min);
|
*score = f64::from(max.saturating_sub(min)) * dark_roi_factor(min);
|
||||||
}
|
}
|
||||||
adaptive_roi_mask_from_scores(&scores, MIN_GRAY_ROI_SCORE)
|
adaptive_roi_mask_from_scores(&scores, MIN_GRAY_ROI_SCORE)
|
||||||
}
|
}
|
||||||
@ -297,7 +297,7 @@ fn adaptive_rgb_roi_mask(frames: &[&[u8]], pixel_count: usize) -> Option<Vec<boo
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let mut scores = vec![0.0; pixel_count];
|
let mut scores = vec![0.0; pixel_count];
|
||||||
for pixel_index in 0..pixel_count {
|
for (pixel_index, score) in scores.iter_mut().enumerate() {
|
||||||
let mut min_r = u8::MAX;
|
let mut min_r = u8::MAX;
|
||||||
let mut min_g = u8::MAX;
|
let mut min_g = u8::MAX;
|
||||||
let mut min_b = u8::MAX;
|
let mut min_b = u8::MAX;
|
||||||
@ -329,7 +329,7 @@ fn adaptive_rgb_roi_mask(frames: &[&[u8]], pixel_count: usize) -> Option<Vec<boo
|
|||||||
+ f64::from(max_g.saturating_sub(min_g))
|
+ f64::from(max_g.saturating_sub(min_g))
|
||||||
+ f64::from(max_b.saturating_sub(min_b));
|
+ f64::from(max_b.saturating_sub(min_b));
|
||||||
let luma_span = f64::from(max_luma.saturating_sub(min_luma));
|
let luma_span = f64::from(max_luma.saturating_sub(min_luma));
|
||||||
scores[pixel_index] =
|
*score =
|
||||||
(rgb_span + (2.0 * luma_span)) * (1.0 + best_palette_score) * dark_roi_factor(min_luma);
|
(rgb_span + (2.0 * luma_span)) * (1.0 + best_palette_score) * dark_roi_factor(min_luma);
|
||||||
}
|
}
|
||||||
adaptive_roi_mask_from_scores(&scores, MIN_RGB_ROI_SCORE)
|
adaptive_roi_mask_from_scores(&scores, MIN_RGB_ROI_SCORE)
|
||||||
@ -354,13 +354,11 @@ fn adaptive_roi_mask_from_scores(scores: &[f64], min_score: f64) -> Option<Vec<b
|
|||||||
.min(scores.len());
|
.min(scores.len());
|
||||||
let score_floor = (max_score * ADAPTIVE_ROI_SCORE_FRACTION).max(min_score);
|
let score_floor = (max_score * ADAPTIVE_ROI_SCORE_FRACTION).max(min_score);
|
||||||
let mut mask = vec![false; scores.len()];
|
let mut mask = vec![false; scores.len()];
|
||||||
let mut selected = 0usize;
|
for (selected, (index, score)) in ranked.into_iter().take(max_selected).enumerate() {
|
||||||
for (index, score) in ranked.into_iter().take(max_selected) {
|
|
||||||
if score < score_floor && selected >= MIN_ADAPTIVE_ROI_PIXELS {
|
if score < score_floor && selected >= MIN_ADAPTIVE_ROI_PIXELS {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
mask[index] = true;
|
mask[index] = true;
|
||||||
selected += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mask = retain_largest_connected_roi(mask);
|
let mask = retain_largest_connected_roi(mask);
|
||||||
|
|||||||
@ -728,6 +728,7 @@ pub(super) fn window_segment(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn push_audio_segment(
|
fn push_audio_segment(
|
||||||
segments: &mut Vec<PulseSegment>,
|
segments: &mut Vec<PulseSegment>,
|
||||||
samples: &[i16],
|
samples: &[i16],
|
||||||
|
|||||||
@ -206,7 +206,7 @@ pub(crate) fn correlate_coded_segments(
|
|||||||
if event_width_codes.is_empty() {
|
if event_width_codes.is_empty() {
|
||||||
bail!("event width code sequence must not be empty");
|
bail!("event width code sequence must not be empty");
|
||||||
}
|
}
|
||||||
if event_width_codes.iter().any(|code| *code == 0) {
|
if event_width_codes.contains(&0) {
|
||||||
bail!("event width codes must stay positive");
|
bail!("event width codes must stay positive");
|
||||||
}
|
}
|
||||||
if pulse_period_s <= 0.0 {
|
if pulse_period_s <= 0.0 {
|
||||||
|
|||||||
@ -191,7 +191,7 @@ async fn collect_probe_audio_grace(
|
|||||||
pending_audio: &mut Vec<AudioPacket>,
|
pending_audio: &mut Vec<AudioPacket>,
|
||||||
audio_done: &mut bool,
|
audio_done: &mut bool,
|
||||||
audio_seq: &mut u64,
|
audio_seq: &mut u64,
|
||||||
mut audio_dump: Option<&mut File>,
|
audio_dump: Option<&mut File>,
|
||||||
) {
|
) {
|
||||||
if *audio_done || !pending_audio.is_empty() {
|
if *audio_done || !pending_audio.is_empty() {
|
||||||
return;
|
return;
|
||||||
@ -202,7 +202,7 @@ async fn collect_probe_audio_grace(
|
|||||||
};
|
};
|
||||||
if let Some(mut packet) = next.packet {
|
if let Some(mut packet) = next.packet {
|
||||||
stamp_probe_audio_packet(&mut packet, audio_seq, next.queue_depth);
|
stamp_probe_audio_packet(&mut packet, audio_seq, next.queue_depth);
|
||||||
write_probe_audio_dump(audio_dump.as_mut().map(|file| &mut **file), &packet);
|
write_probe_audio_dump(audio_dump, &packet);
|
||||||
pending_audio.push(packet);
|
pending_audio.push(packet);
|
||||||
retain_newest_probe_audio(pending_audio);
|
retain_newest_probe_audio(pending_audio);
|
||||||
} else if next.closed {
|
} else if next.closed {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lesavka_common"
|
name = "lesavka_common"
|
||||||
version = "0.19.29"
|
version = "0.19.30"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
|
|||||||
@ -10,7 +10,7 @@ bench = false
|
|||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "lesavka_server"
|
name = "lesavka_server"
|
||||||
version = "0.19.29"
|
version = "0.19.30"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
autobins = false
|
autobins = false
|
||||||
|
|
||||||
|
|||||||
@ -477,8 +477,14 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
#[test]
|
const BLESSED_SERVER_TO_RCT_VIDEO_OFFSETS: &[(&str, i64)] = &[
|
||||||
fn default_snapshot_uses_factory_mjpeg_calibration() {
|
("1280x720@20", 162_659),
|
||||||
|
("1280x720@30", 135_090),
|
||||||
|
("1920x1080@20", 160_045),
|
||||||
|
("1920x1080@30", 127_952),
|
||||||
|
];
|
||||||
|
|
||||||
|
fn with_clean_offset_env(test: impl FnOnce()) {
|
||||||
temp_env::with_vars(
|
temp_env::with_vars(
|
||||||
[
|
[
|
||||||
("LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US", None::<&str>),
|
("LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US", None::<&str>),
|
||||||
@ -491,18 +497,70 @@ mod tests {
|
|||||||
"LESAVKA_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US",
|
"LESAVKA_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US",
|
||||||
None::<&str>,
|
None::<&str>,
|
||||||
),
|
),
|
||||||
|
("UVC_MODE", None::<&str>),
|
||||||
|
("LESAVKA_UVC_MODE", None::<&str>),
|
||||||
("LESAVKA_UVC_WIDTH", None::<&str>),
|
("LESAVKA_UVC_WIDTH", None::<&str>),
|
||||||
("LESAVKA_UVC_HEIGHT", None::<&str>),
|
("LESAVKA_UVC_HEIGHT", None::<&str>),
|
||||||
("LESAVKA_UVC_FPS", None::<&str>),
|
("LESAVKA_UVC_FPS", None::<&str>),
|
||||||
("LESAVKA_UVC_INTERVAL", None::<&str>),
|
("LESAVKA_UVC_INTERVAL", None::<&str>),
|
||||||
|
("LESAVKA_CAM_WIDTH", None::<&str>),
|
||||||
|
("LESAVKA_CAM_HEIGHT", None::<&str>),
|
||||||
|
("LESAVKA_CAM_FPS", None::<&str>),
|
||||||
],
|
],
|
||||||
|| {
|
test,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn blessed_server_to_rct_offsets_are_release_defaults() {
|
||||||
|
assert_eq!(
|
||||||
|
FACTORY_MJPEG_VIDEO_OFFSET_US, FACTORY_MJPEG_VIDEO_OFFSET_1280X720_30_US,
|
||||||
|
"720p30 is the blessed default profile until a new lab matrix replaces it"
|
||||||
|
);
|
||||||
|
assert_eq!(FACTORY_MJPEG_VIDEO_OFFSET_1280X720_20_US, 162_659);
|
||||||
|
assert_eq!(FACTORY_MJPEG_VIDEO_OFFSET_1280X720_30_US, 135_090);
|
||||||
|
assert_eq!(FACTORY_MJPEG_VIDEO_OFFSET_1920X1080_20_US, 160_045);
|
||||||
|
assert_eq!(FACTORY_MJPEG_VIDEO_OFFSET_1920X1080_30_US, 127_952);
|
||||||
|
assert_eq!(
|
||||||
|
FACTORY_MJPEG_AUDIO_MODE_OFFSETS_US,
|
||||||
|
"1280x720@20=0,1280x720@30=0,1920x1080@20=0,1920x1080@30=0"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
FACTORY_MJPEG_VIDEO_MODE_OFFSETS_US,
|
||||||
|
"1280x720@20=162659,1280x720@30=135090,1920x1080@20=160045,1920x1080@30=127952"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_supported_uvc_mode_loads_tailored_factory_offset() {
|
||||||
|
for (mode, expected_offset_us) in BLESSED_SERVER_TO_RCT_VIDEO_OFFSETS {
|
||||||
|
with_clean_offset_env(|| {
|
||||||
|
temp_env::with_var("UVC_MODE", Some(*mode), || {
|
||||||
|
let state = snapshot_from_env();
|
||||||
|
assert_eq!(
|
||||||
|
state.factory_video_offset_us, *expected_offset_us,
|
||||||
|
"{mode} should use its baked server-to-RCT factory offset"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
state.default_video_offset_us, *expected_offset_us,
|
||||||
|
"{mode} should default to its baked server-to-RCT offset"
|
||||||
|
);
|
||||||
|
assert_eq!(state.default_audio_offset_us, 0);
|
||||||
|
assert_eq!(state.source, "factory");
|
||||||
|
assert_eq!(state.confidence, FACTORY_CONFIDENCE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_snapshot_uses_factory_mjpeg_calibration() {
|
||||||
|
with_clean_offset_env(|| {
|
||||||
let state = snapshot_from_env();
|
let state = snapshot_from_env();
|
||||||
assert_eq!(state.default_audio_offset_us, 0);
|
assert_eq!(state.default_audio_offset_us, 0);
|
||||||
assert_eq!(state.active_video_offset_us, FACTORY_MJPEG_VIDEO_OFFSET_US);
|
assert_eq!(state.active_video_offset_us, FACTORY_MJPEG_VIDEO_OFFSET_US);
|
||||||
assert_eq!(state.source, "factory");
|
assert_eq!(state.source, "factory");
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@ -174,6 +174,7 @@ async fn push_media_v2_audio(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(coverage))]
|
#[cfg(not(coverage))]
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn feed_media_v2_video(
|
async fn feed_media_v2_video(
|
||||||
video: Option<VideoPacket>,
|
video: Option<VideoPacket>,
|
||||||
clock: &mut MediaV2Clock,
|
clock: &mut MediaV2Clock,
|
||||||
|
|||||||
@ -130,20 +130,15 @@ impl ScalarWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
enum UpstreamSyncPhase {
|
enum UpstreamSyncPhase {
|
||||||
|
#[default]
|
||||||
Acquiring,
|
Acquiring,
|
||||||
Syncing,
|
Syncing,
|
||||||
Live,
|
Live,
|
||||||
Healing,
|
Healing,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for UpstreamSyncPhase {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Acquiring
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UpstreamSyncPhase {
|
impl UpstreamSyncPhase {
|
||||||
fn as_str(self) -> &'static str {
|
fn as_str(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
@ -803,3 +798,80 @@ fn apply_offset(instant: Instant, offset_us: i64) -> Instant {
|
|||||||
.unwrap_or(instant)
|
.unwrap_or(instant)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn with_clean_offset_env(test: impl FnOnce()) {
|
||||||
|
temp_env::with_vars(
|
||||||
|
[
|
||||||
|
("LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US", None::<&str>),
|
||||||
|
("LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US", None::<&str>),
|
||||||
|
(
|
||||||
|
"LESAVKA_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US",
|
||||||
|
None::<&str>,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"LESAVKA_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US",
|
||||||
|
None::<&str>,
|
||||||
|
),
|
||||||
|
("LESAVKA_UVC_WIDTH", None::<&str>),
|
||||||
|
("LESAVKA_UVC_HEIGHT", None::<&str>),
|
||||||
|
("LESAVKA_UVC_FPS", None::<&str>),
|
||||||
|
("LESAVKA_UVC_INTERVAL", None::<&str>),
|
||||||
|
],
|
||||||
|
test,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runtime_uses_baked_mode_offsets_before_calibration_store_loads() {
|
||||||
|
for (width, height, fps, expected_video_offset_us) in [
|
||||||
|
("1280", "720", "20", 162_659),
|
||||||
|
("1280", "720", "30", 135_090),
|
||||||
|
("1920", "1080", "20", 160_045),
|
||||||
|
("1920", "1080", "30", 127_952),
|
||||||
|
] {
|
||||||
|
with_clean_offset_env(|| {
|
||||||
|
temp_env::with_vars(
|
||||||
|
[
|
||||||
|
("LESAVKA_UVC_WIDTH", Some(width)),
|
||||||
|
("LESAVKA_UVC_HEIGHT", Some(height)),
|
||||||
|
("LESAVKA_UVC_FPS", Some(fps)),
|
||||||
|
],
|
||||||
|
|| {
|
||||||
|
let runtime = UpstreamMediaRuntime::new();
|
||||||
|
assert_eq!(
|
||||||
|
runtime.playout_offsets(),
|
||||||
|
(expected_video_offset_us, 0),
|
||||||
|
"{width}x{height}@{fps} should use its baked startup offset"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runtime_prefers_mode_offset_map_over_scalar_fallback() {
|
||||||
|
with_clean_offset_env(|| {
|
||||||
|
temp_env::with_vars(
|
||||||
|
[
|
||||||
|
("LESAVKA_UVC_WIDTH", Some("1280")),
|
||||||
|
("LESAVKA_UVC_HEIGHT", Some("720")),
|
||||||
|
("LESAVKA_UVC_FPS", Some("30")),
|
||||||
|
("LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US", Some("999999")),
|
||||||
|
(
|
||||||
|
"LESAVKA_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US",
|
||||||
|
Some("1280x720@30=135090"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|| {
|
||||||
|
let runtime = UpstreamMediaRuntime::new();
|
||||||
|
assert_eq!(runtime.playout_offsets(), (135_090, 0));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -26,15 +26,17 @@ mod inputs_contract {
|
|||||||
use evdev::uinput::VirtualDevice;
|
use evdev::uinput::VirtualDevice;
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use temp_env::with_var;
|
use temp_env::{with_var, with_vars};
|
||||||
|
|
||||||
fn open_virtual_device(vdev: &mut VirtualDevice) -> Option<evdev::Device> {
|
fn open_virtual_device_with_path(
|
||||||
|
vdev: &mut VirtualDevice,
|
||||||
|
) -> Option<(std::path::PathBuf, evdev::Device)> {
|
||||||
for _ in 0..40 {
|
for _ in 0..40 {
|
||||||
if let Ok(mut nodes) = vdev.enumerate_dev_nodes_blocking() {
|
if let Ok(mut nodes) = vdev.enumerate_dev_nodes_blocking() {
|
||||||
if let Some(Ok(path)) = nodes.next() {
|
if let Some(Ok(path)) = nodes.next() {
|
||||||
if let Ok(dev) = evdev::Device::open(path) {
|
if let Ok(dev) = evdev::Device::open(&path) {
|
||||||
let _ = dev.set_nonblocking(true);
|
let _ = dev.set_nonblocking(true);
|
||||||
return Some(dev);
|
return Some((path.to_path_buf(), dev));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -43,6 +45,10 @@ mod inputs_contract {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn open_virtual_device(vdev: &mut VirtualDevice) -> Option<evdev::Device> {
|
||||||
|
open_virtual_device_with_path(vdev).map(|(_, dev)| dev)
|
||||||
|
}
|
||||||
|
|
||||||
fn build_keyboard() -> Option<evdev::Device> {
|
fn build_keyboard() -> Option<evdev::Device> {
|
||||||
let mut keys = AttributeSet::<evdev::KeyCode>::new();
|
let mut keys = AttributeSet::<evdev::KeyCode>::new();
|
||||||
keys.insert(evdev::KeyCode::KEY_A);
|
keys.insert(evdev::KeyCode::KEY_A);
|
||||||
@ -138,12 +144,13 @@ mod inputs_contract {
|
|||||||
|
|
||||||
fn build_keyboard_pair(name: &str) -> Option<(VirtualDevice, evdev::Device)> {
|
fn build_keyboard_pair(name: &str) -> Option<(VirtualDevice, evdev::Device)> {
|
||||||
build_keyboard_pair_with_keys(name, &[evdev::KeyCode::KEY_A, evdev::KeyCode::KEY_ENTER])
|
build_keyboard_pair_with_keys(name, &[evdev::KeyCode::KEY_A, evdev::KeyCode::KEY_ENTER])
|
||||||
|
.map(|(vdev, _, dev)| (vdev, dev))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_keyboard_pair_with_keys(
|
fn build_keyboard_pair_with_keys(
|
||||||
name: &str,
|
name: &str,
|
||||||
supported_keys: &[evdev::KeyCode],
|
supported_keys: &[evdev::KeyCode],
|
||||||
) -> Option<(VirtualDevice, evdev::Device)> {
|
) -> Option<(VirtualDevice, std::path::PathBuf, evdev::Device)> {
|
||||||
let mut keys = AttributeSet::<evdev::KeyCode>::new();
|
let mut keys = AttributeSet::<evdev::KeyCode>::new();
|
||||||
for key in supported_keys {
|
for key in supported_keys {
|
||||||
keys.insert(*key);
|
keys.insert(*key);
|
||||||
@ -157,11 +164,17 @@ mod inputs_contract {
|
|||||||
.build()
|
.build()
|
||||||
.ok()?;
|
.ok()?;
|
||||||
|
|
||||||
let dev = open_virtual_device(&mut vdev)?;
|
let (path, dev) = open_virtual_device_with_path(&mut vdev)?;
|
||||||
Some((vdev, dev))
|
Some((vdev, path, dev))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_mouse_pair(name: &str) -> Option<(VirtualDevice, evdev::Device)> {
|
fn build_mouse_pair(name: &str) -> Option<(VirtualDevice, evdev::Device)> {
|
||||||
|
build_mouse_pair_with_path(name).map(|(vdev, _, dev)| (vdev, dev))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_mouse_pair_with_path(
|
||||||
|
name: &str,
|
||||||
|
) -> Option<(VirtualDevice, std::path::PathBuf, evdev::Device)> {
|
||||||
let mut keys = AttributeSet::<evdev::KeyCode>::new();
|
let mut keys = AttributeSet::<evdev::KeyCode>::new();
|
||||||
keys.insert(evdev::KeyCode::BTN_LEFT);
|
keys.insert(evdev::KeyCode::BTN_LEFT);
|
||||||
let mut rel = AttributeSet::<evdev::RelativeAxisCode>::new();
|
let mut rel = AttributeSet::<evdev::RelativeAxisCode>::new();
|
||||||
@ -178,8 +191,8 @@ mod inputs_contract {
|
|||||||
.build()
|
.build()
|
||||||
.ok()?;
|
.ok()?;
|
||||||
|
|
||||||
let dev = open_virtual_device(&mut vdev)?;
|
let (path, dev) = open_virtual_device_with_path(&mut vdev)?;
|
||||||
Some((vdev, dev))
|
Some((vdev, path, dev))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_aggregator() -> InputAggregator {
|
fn new_aggregator() -> InputAggregator {
|
||||||
@ -287,22 +300,37 @@ mod inputs_contract {
|
|||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
#[serial]
|
||||||
fn init_grabs_virtual_keyboard_and_mouse_when_available() {
|
fn init_grabs_virtual_keyboard_and_mouse_when_available() {
|
||||||
let Some((_kbd_vdev, _kbd_dev)) = build_keyboard_pair("lesavka-input-init-kbd") else {
|
let Some((_kbd_vdev, kbd_path, _kbd_dev)) = build_keyboard_pair_with_keys(
|
||||||
|
"lesavka-input-init-kbd",
|
||||||
|
&[evdev::KeyCode::KEY_A, evdev::KeyCode::KEY_ENTER],
|
||||||
|
) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some((_mouse_vdev, _mouse_dev)) = build_mouse_pair("lesavka-input-init-mouse") else {
|
let Some((_mouse_vdev, mouse_path, _mouse_dev)) =
|
||||||
|
build_mouse_pair_with_path("lesavka-input-init-mouse")
|
||||||
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let kbd_path = kbd_path.to_string_lossy().into_owned();
|
||||||
|
let mouse_path = mouse_path.to_string_lossy().into_owned();
|
||||||
|
with_vars(
|
||||||
|
[
|
||||||
|
("LESAVKA_KEYBOARD_DEVICE", Some(kbd_path.as_str())),
|
||||||
|
("LESAVKA_MOUSE_DEVICE", Some(mouse_path.as_str())),
|
||||||
|
],
|
||||||
|
|| {
|
||||||
let mut agg = new_aggregator();
|
let mut agg = new_aggregator();
|
||||||
let result = agg.init();
|
let result = agg.init();
|
||||||
assert!(
|
assert!(
|
||||||
result.is_ok(),
|
result.is_ok(),
|
||||||
"init should succeed with virtual input devices"
|
"init should succeed with selected virtual input devices"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!agg.keyboards.is_empty() || !agg.mice.is_empty(),
|
!agg.keyboards.is_empty() || !agg.mice.is_empty(),
|
||||||
"init should discover at least one virtual input device"
|
"init should discover at least one selected virtual input device"
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user