// Integration coverage for the MJPEG spool used by UVC output. // // Scope: write real frame and metadata files through the same spool helper // used by direct MJPEG ingress and decoded HEVC ingress. // Targets: server/src/video_sinks/mjpeg_spool.rs. // Why: both profiles share the UVC helper handoff path, so metadata must keep // them separated without changing the atomic frame publication behavior. use std::fs; use serial_test::serial; #[allow(dead_code, clippy::items_after_test_module)] mod spool { include!(concat!( env!("CARGO_MANIFEST_DIR"), "/server/src/video_sinks/mjpeg_spool.rs" )); pub fn write_hevc( path: &std::path::Path, data: &[u8], source_pts_us: u64, decoded_pts_us: Option, ) -> anyhow::Result<()> { spool_mjpeg_frame_with_timing( path, data, Some(MjpegSpoolTiming::hevc_decoded_mjpeg( source_pts_us, decoded_pts_us, )), ) } pub fn write_mjpeg( path: &std::path::Path, data: &[u8], source_pts_us: u64, ) -> anyhow::Result<()> { spool_mjpeg_frame_with_timing( path, data, Some(MjpegSpoolTiming::mjpeg_passthrough(source_pts_us)), ) } } #[test] #[serial] fn hevc_decoded_mjpeg_spool_writes_frame_and_timing_metadata() { let dir = tempfile::tempdir().expect("tempdir"); let frame_path = dir.path().join("uvc-frame.mjpg"); let log_path = dir.path().join("uvc-frame.jsonl"); let log_path_text = log_path.to_string_lossy().to_string(); let frame = [0xff, 0xd8, 0x11, 0x22, 0xff, 0xd9]; temp_env::with_vars( [ ("LESAVKA_UVC_FRAME_META", Some("1")), ( "LESAVKA_UVC_FRAME_META_LOG_PATH", Some(log_path_text.as_str()), ), ("LESAVKA_UVC_FRAME_META_PATH", None), ], || { spool::write_hevc(&frame_path, &frame, 42_000, Some(44_000)) .expect("spool decoded HEVC frame"); }, ); assert_eq!(fs::read(&frame_path).expect("frame bytes"), frame); let sidecar = frame_path.with_extension("mjpg.meta.json"); let sidecar_text = fs::read_to_string(sidecar).expect("sidecar metadata"); let log_text = fs::read_to_string(log_path).expect("metadata log"); for text in [sidecar_text.as_str(), log_text.as_str()] { assert!(text.contains("\"profile\":\"hevc-decoded-mjpeg\"")); assert!(text.contains("\"bytes\":6")); assert!(text.contains("\"source_pts_us\":42000")); assert!(text.contains("\"decoded_pts_us\":44000")); } } #[test] #[serial] fn mjpeg_passthrough_spool_keeps_decode_timing_null() { let dir = tempfile::tempdir().expect("tempdir"); let frame_path = dir.path().join("uvc-frame.mjpg"); let frame = [0xff, 0xd8, 0x33, 0x44, 0xff, 0xd9]; temp_env::with_var("LESAVKA_UVC_FRAME_META", Some("1"), || { spool::write_mjpeg(&frame_path, &frame, 55_000).expect("spool MJPEG frame"); }); assert_eq!(fs::read(&frame_path).expect("frame bytes"), frame); let sidecar_text = fs::read_to_string(frame_path.with_extension("mjpg.meta.json")).expect("sidecar metadata"); assert!(sidecar_text.contains("\"profile\":\"mjpeg-passthrough\"")); assert!(sidecar_text.contains("\"source_pts_us\":55000")); assert!(sidecar_text.contains("\"decoded_pts_us\":null")); }