test(video): cover downstream eye feed
This commit is contained in:
parent
a2c317c4aa
commit
21c3ecfac6
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.11.37"
|
||||
version = "0.11.38"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -113,9 +113,9 @@ const LAUNCHER_DEFAULT_HEIGHT: i32 = 860;
|
||||
const OPERATIONS_RAIL_WIDTH: i32 = 288;
|
||||
const CAMERA_PREVIEW_VIEWPORT_HEIGHT: i32 = 72;
|
||||
const CAMERA_PREVIEW_VIEWPORT_WIDTH: i32 = 128;
|
||||
const DEVICE_PANEL_HEIGHT: i32 = 228;
|
||||
const DEVICE_TEST_GROUP_HEIGHT: i32 = 188;
|
||||
const SIDE_LOG_HEIGHT: i32 = 124;
|
||||
const DEVICE_PANEL_HEIGHT: i32 = 204;
|
||||
const DEVICE_TEST_GROUP_HEIGHT: i32 = 156;
|
||||
const SIDE_LOG_HEIGHT: i32 = 104;
|
||||
|
||||
pub fn build_launcher_view(
|
||||
app: >k::Application,
|
||||
@ -853,7 +853,7 @@ pub fn install_css(window: >k::ApplicationWindow) {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
progressbar.audio-check-meter.vertical trough {
|
||||
min-height: 96px;
|
||||
min-height: 72px;
|
||||
}
|
||||
progressbar.audio-check-meter progress {
|
||||
border-radius: 999px;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.11.37"
|
||||
version = "0.11.38"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.11.37"
|
||||
version = "0.11.38"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
165
testing/tests/video_downstream_feed_contract.rs
Normal file
165
testing/tests/video_downstream_feed_contract.rs
Normal file
@ -0,0 +1,165 @@
|
||||
//! Downstream eye-video feed contracts.
|
||||
//!
|
||||
//! Scope: exercise the server eye stream with a deterministic GStreamer test
|
||||
//! source and lock down the native source-mode policy used by the launcher and
|
||||
//! diagnostics.
|
||||
//! Targets: `server/src/video.rs`, `server/src/video_support.rs`,
|
||||
//! `common/src/eye_source.rs`.
|
||||
//! Why: the live capture cards are hardware-dependent, but this catches the
|
||||
//! packet, timing, keyframe, and mode-selection regressions that break the
|
||||
//! client previews before hardware enters the loop.
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use lesavka_common::eye_source::{
|
||||
EyeSourceMode, default_eye_source_mode, display_size_for_source_mode,
|
||||
eye_source_mode_for_request, native_eye_source_modes,
|
||||
};
|
||||
use lesavka_server::{video, video_support::contains_idr};
|
||||
use serial_test::serial;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn native_downstream_eye_modes_stay_widescreen_and_square_pixel() {
|
||||
assert_eq!(
|
||||
native_eye_source_modes(),
|
||||
&[
|
||||
EyeSourceMode {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
fps: 60,
|
||||
},
|
||||
EyeSourceMode {
|
||||
width: 1280,
|
||||
height: 720,
|
||||
fps: 60,
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
default_eye_source_mode(),
|
||||
EyeSourceMode {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
fps: 60,
|
||||
}
|
||||
);
|
||||
for mode in native_eye_source_modes() {
|
||||
assert_eq!(
|
||||
display_size_for_source_mode(*mode),
|
||||
(mode.width, mode.height)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn downstream_eye_request_chooses_best_native_capture_mode() {
|
||||
assert_eq!(
|
||||
eye_source_mode_for_request(0, 0),
|
||||
EyeSourceMode {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
fps: 60,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
eye_source_mode_for_request(1920, 1080),
|
||||
EyeSourceMode {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
fps: 60,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
eye_source_mode_for_request(1600, 900),
|
||||
EyeSourceMode {
|
||||
width: 1280,
|
||||
height: 720,
|
||||
fps: 60,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
eye_source_mode_for_request(960, 540),
|
||||
EyeSourceMode {
|
||||
width: 1280,
|
||||
height: 720,
|
||||
fps: 60,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn testsrc_downstream_feed_emits_h264_for_both_eyes() {
|
||||
with_fast_video_env(|| {
|
||||
let runtime = tokio::runtime::Runtime::new().expect("runtime");
|
||||
runtime.block_on(async {
|
||||
assert_testsrc_downstream_eye(0, 1920, 1080, 60).await;
|
||||
assert_testsrc_downstream_eye(1, 1280, 720, 60).await;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async fn assert_testsrc_downstream_eye(eye_id: u32, width: u32, height: u32, fps: u32) {
|
||||
let setup = tokio::time::timeout(
|
||||
Duration::from_secs(2),
|
||||
video::eye_ball_with_request("testsrc", eye_id, 1_200, width, height, fps),
|
||||
)
|
||||
.await;
|
||||
let mut stream = match setup {
|
||||
Ok(Ok(stream)) => stream,
|
||||
Ok(Err(err)) => panic!("testsrc eye-{eye_id} setup failed: {err:#}"),
|
||||
Err(_) => panic!("testsrc eye-{eye_id} setup timed out"),
|
||||
};
|
||||
|
||||
let mut last_pts = None;
|
||||
let mut saw_annex_b = false;
|
||||
let mut saw_idr = false;
|
||||
for packet_index in 0..8 {
|
||||
let packet = tokio::time::timeout(Duration::from_secs(2), stream.next())
|
||||
.await
|
||||
.unwrap_or_else(|_| panic!("eye-{eye_id} packet {packet_index} timed out"))
|
||||
.expect("stream should stay open")
|
||||
.unwrap_or_else(|err| panic!("eye-{eye_id} packet {packet_index} failed: {err}"));
|
||||
|
||||
assert_eq!(packet.id, eye_id, "logical eye id must be preserved");
|
||||
assert!(
|
||||
!packet.data.is_empty(),
|
||||
"eye-{eye_id} packet {packet_index} must carry H.264 bytes"
|
||||
);
|
||||
if let Some(previous) = last_pts {
|
||||
assert!(
|
||||
packet.pts >= previous,
|
||||
"eye-{eye_id} PTS must not move backward"
|
||||
);
|
||||
}
|
||||
last_pts = Some(packet.pts);
|
||||
saw_annex_b |= contains_annex_b_start_code(&packet.data);
|
||||
saw_idr |= contains_idr(&packet.data);
|
||||
}
|
||||
|
||||
assert!(saw_annex_b, "eye-{eye_id} should emit Annex-B H.264");
|
||||
assert!(
|
||||
saw_idr,
|
||||
"eye-{eye_id} should emit an IDR keyframe for decoder recovery"
|
||||
);
|
||||
drop(stream);
|
||||
}
|
||||
|
||||
fn with_fast_video_env(run: impl FnOnce()) {
|
||||
temp_env::with_vars(
|
||||
[
|
||||
("LESAVKA_EYE_ADAPTIVE", Some("0")),
|
||||
("LESAVKA_EYE_QUEUE_BUFFERS", Some("8")),
|
||||
("LESAVKA_EYE_APPSINK_BUFFERS", Some("8")),
|
||||
("LESAVKA_EYE_CHAN_CAPACITY", Some("32")),
|
||||
("LESAVKA_EYE_TESTSRC_KBIT", Some("1200")),
|
||||
("LESAVKA_EYE_KEYFRAME_INTERVAL", Some("1")),
|
||||
],
|
||||
run,
|
||||
);
|
||||
}
|
||||
|
||||
fn contains_annex_b_start_code(bytes: &[u8]) -> bool {
|
||||
bytes.windows(3).any(|window| window == [0, 0, 1])
|
||||
|| bytes.windows(4).any(|window| window == [0, 0, 0, 1])
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user