150 lines
5.7 KiB
Rust
150 lines
5.7 KiB
Rust
//! Static contract for the v2 bundled upstream media handoff.
|
|
//!
|
|
//! Scope: guard `StreamWebcamMedia` against reintroducing timed sink sleeps in
|
|
//! the gRPC receive loop.
|
|
//! Targets: `server/src/main/relay_service.rs` and
|
|
//! `server/src/main/relay_service/upstream_media_rpc.rs`.
|
|
//! Why: client->server freshness depends on draining bundled media as it
|
|
//! arrives; presentation timing belongs in bounded handoff workers.
|
|
|
|
const RELAY_SERVICE: &str = include_str!("../../server/src/main/relay_service.rs");
|
|
const WEBCAM_RPC: &str = include_str!("../../server/src/main/relay_service/upstream_media_rpc.rs");
|
|
const WEBCAM_SINK: &str = include_str!("../../server/src/video_sinks/webcam_sink.rs");
|
|
const MJPEG_SPOOL: &str = include_str!("../../server/src/video_sinks/mjpeg_spool.rs");
|
|
const PROFILE_OFFSETS: &str = include_str!("../../server/src/calibration/profile_offsets.rs");
|
|
const MATRIX_SCRIPT: &str = include_str!("../../scripts/manual/run_server_to_rc_mode_matrix.sh");
|
|
|
|
#[test]
|
|
fn bundled_receive_loop_enqueues_instead_of_sleeping_for_handoff() {
|
|
for expected in [
|
|
"tokio::sync::mpsc::channel::<MediaV2ScheduledAudio>(32)",
|
|
"tokio::sync::mpsc::channel::<MediaV2ScheduledVideo>(32)",
|
|
"tokio::spawn(run_media_v2_audio_handoff",
|
|
"tokio::spawn(run_media_v2_video_handoff",
|
|
"let bundle_epoch = bundle_arrived_at + schedule.common_delay;",
|
|
"let bundle_base_remote_pts_us = facts.capture_start_us;",
|
|
"prepare_media_v2_audio(",
|
|
"prepare_media_v2_video(",
|
|
"bundle_base_remote_pts_us",
|
|
"bundle_epoch",
|
|
".send(scheduled_audio)",
|
|
".send(scheduled_video)",
|
|
] {
|
|
assert!(
|
|
WEBCAM_RPC.contains(expected),
|
|
"v2 bundled RPC should enqueue handoff work with marker {expected}"
|
|
);
|
|
}
|
|
|
|
for forbidden in [
|
|
"push_media_v2_audio(",
|
|
"feed_media_v2_video(",
|
|
"sleep_until_media_v2(",
|
|
"MediaV2Clock",
|
|
] {
|
|
assert!(
|
|
!WEBCAM_RPC.contains(forbidden),
|
|
"v2 bundled RPC receive loop must not perform timed sink handoff: {forbidden}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn handoff_workers_own_timing_and_presentation_telemetry() {
|
|
for expected in [
|
|
"async fn run_media_v2_audio_handoff",
|
|
"async fn run_media_v2_video_handoff",
|
|
"sleep_until_media_v2(item.due_at).await;",
|
|
"sink.push(&audio);",
|
|
"sink.finish();",
|
|
"relay.feed(item.packet);",
|
|
"mark_audio_presented(pts, item.due_at)",
|
|
"mark_video_presented(presented_pts, item.due_at)",
|
|
"Why: sleeping in the receive loop created HTTP/2 backlog",
|
|
] {
|
|
assert!(
|
|
RELAY_SERVICE.contains(expected),
|
|
"handoff workers should preserve timing/telemetry marker {expected}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn stream_shutdown_drains_handoff_workers_before_releasing_leases() {
|
|
let audio_drop = WEBCAM_RPC.find("drop(audio_handoff_tx);").unwrap();
|
|
let video_drop = WEBCAM_RPC.find("drop(video_handoff_tx);").unwrap();
|
|
let audio_join = WEBCAM_RPC.find("audio_worker.await").unwrap();
|
|
let video_join = WEBCAM_RPC.find("video_worker.await").unwrap();
|
|
let close_camera = WEBCAM_RPC
|
|
.rfind("upstream_media_rt.close_camera(camera_lease.generation);")
|
|
.unwrap();
|
|
let close_microphone = WEBCAM_RPC
|
|
.rfind("upstream_media_rt.close_microphone(microphone_lease.generation);")
|
|
.unwrap();
|
|
|
|
assert!(audio_drop < audio_join);
|
|
assert!(video_drop < video_join);
|
|
assert!(audio_join < close_camera);
|
|
assert!(video_join < close_microphone);
|
|
}
|
|
|
|
#[test]
|
|
fn hevc_ingress_decodes_to_existing_mjpeg_uvc_path() {
|
|
for expected in [
|
|
"let use_hevc = matches!(cfg.codec, CameraCodec::Hevc);",
|
|
"video/x-h265",
|
|
"h265parse",
|
|
"pick_hevc_decoder()",
|
|
"jpegenc",
|
|
"LESAVKA_UVC_HEVC_JPEG_QUALITY",
|
|
"LESAVKA_UVC_HEVC_SPOOL_PULL_TIMEOUT_MS",
|
|
"image/jpeg",
|
|
"hevc_mjpeg_spool_sink",
|
|
".property(\"sync\", clock_align_enabled)",
|
|
"failed to spool decoded HEVC frame for UVC helper",
|
|
"HEVC camera uplink will be decoded and emitted as MJPEG/UVC",
|
|
] {
|
|
assert!(
|
|
WEBCAM_SINK.contains(expected) || MJPEG_SPOOL.contains(expected),
|
|
"HEVC UVC sink should preserve decode-to-MJPEG marker {expected}"
|
|
);
|
|
}
|
|
assert!(
|
|
WEBCAM_SINK.find("video/x-h265").unwrap() < WEBCAM_SINK.find("jpegenc").unwrap(),
|
|
"HEVC should be decoded before MJPEG encoding for the existing UVC output path"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn mjpeg_ingress_remains_passthrough_and_profile_calibrated() {
|
|
for expected in [
|
|
"let use_mjpeg = matches!(cfg.codec, CameraCodec::Mjpeg);",
|
|
"image/jpeg",
|
|
"src.set_caps(Some(&caps_mjpeg));",
|
|
"MjpegSpoolTiming::mjpeg_passthrough(pkt.pts)",
|
|
"spool_mjpeg_frame_with_timing(path, &pkt.data, Some(timing))",
|
|
"FACTORY_MJPEG_VIDEO_MODE_OFFSETS_US",
|
|
"FACTORY_HEVC_VIDEO_MODE_OFFSETS_US",
|
|
"LESAVKA_UPSTREAM_MJPEG_VIDEO_PLAYOUT_MODE_OFFSETS_US",
|
|
"LESAVKA_UPSTREAM_HEVC_VIDEO_PLAYOUT_MODE_OFFSETS_US",
|
|
"LESAVKA_SERVER_RC_PROFILE=${LESAVKA_SERVER_RC_PROFILE:-mjpeg}",
|
|
"LESAVKA_SERVER_RC_NORMALIZED_PROFILE=hevc",
|
|
] {
|
|
assert!(
|
|
WEBCAM_SINK.contains(expected)
|
|
|| PROFILE_OFFSETS.contains(expected)
|
|
|| MATRIX_SCRIPT.contains(expected),
|
|
"MJPEG/HEVC profile separation should contain marker {expected}"
|
|
);
|
|
}
|
|
assert!(
|
|
PROFILE_OFFSETS
|
|
.find("FACTORY_MJPEG_VIDEO_MODE_OFFSETS_US")
|
|
.unwrap()
|
|
< PROFILE_OFFSETS
|
|
.find("FACTORY_HEVC_VIDEO_MODE_OFFSETS_US")
|
|
.unwrap(),
|
|
"MJPEG factory map should stay separate from the additive HEVC map"
|
|
);
|
|
}
|