/// Generate a server-local A/V signature and feed the physical UVC/UAC sinks. /// /// Inputs: the active camera relay, active UAC voice sink, camera profile, and /// probe request timing. /// Outputs: a small count summary after the last generated packet. /// Why: this probe intentionally bypasses client capture/uplink but uses the /// same final server output handoff calls as received client media, so the /// measured skew/freshness is the server final-handoff-to-RCT path. #[cfg(not(coverage))] pub async fn run_server_output_delay_probe( relay: Arc, sink: &mut Voice, camera: &CameraConfig, request: &OutputDelayProbeRequest, ) -> Result { let config = ProbeConfig::from_request(request)?; if config.event_count() == 0 { bail!("probe duration must extend beyond warmup"); } let frame_step = Duration::from_nanos(1_000_000_000u64 / u64::from(camera.fps.max(1))); let audio_chunk = Duration::from_millis(AUDIO_CHUNK_MS); let samples_per_chunk = ((u64::from(AUDIO_SAMPLE_RATE) * AUDIO_CHUNK_MS) / 1_000) as usize; let frames = EncodedProbeFrames::new(camera, &config, frame_step)?; let server_start_unix_ns = unix_ns_now(); let start = tokio::time::Instant::now(); let mut timeline = OutputDelayProbeTimeline::new(&config, camera, server_start_unix_ns); let mut frame_index = 0u64; let mut audio_index = 0u64; let mut video_frames = 0u64; let mut audio_packets = 0u64; loop { let next_frame_pts = duration_mul(frame_step, frame_index); let next_audio_pts = duration_mul(audio_chunk, audio_index); let frame_active = next_frame_pts <= config.duration; let audio_active = next_audio_pts <= config.duration; if !frame_active && !audio_active { break; } let next_frame_due = if frame_active { next_frame_pts.saturating_add(config.video_delay) } else { Duration::MAX }; let next_audio_due = if audio_active { next_audio_pts.saturating_add(config.audio_delay) } else { Duration::MAX }; tokio::time::sleep_until(start + next_frame_due.min(next_audio_due)).await; if audio_active && next_audio_due <= next_frame_due { let pts_us = duration_us(next_audio_pts); let event_slot = config.event_slot_at(next_audio_pts); let data = render_audio_chunk(&config, next_audio_pts, samples_per_chunk); let seq = audio_index.saturating_add(1); sink.push(&AudioPacket { id: 0, pts: pts_us, data, seq, client_capture_pts_us: pts_us, client_send_pts_us: pts_us, client_queue_depth: 0, client_queue_age_ms: 0, encoding: lesavka_common::lesavka::AudioEncoding::PcmS16le as i32, sample_rate: AUDIO_SAMPLE_RATE, channels: AUDIO_CHANNELS as u32, frame_duration_us: AUDIO_CHUNK_MS.saturating_mul(1_000) as u32, }); if let Some(slot) = event_slot { let monotonic_us = monotonic_us_since(start); timeline.mark_audio( slot, seq, monotonic_us, unix_ns_from_start(server_start_unix_ns, monotonic_us), ); } audio_packets = audio_packets.saturating_add(1); audio_index = audio_index.saturating_add(1); } if frame_active && next_frame_due <= next_audio_due { let pts_us = duration_us(next_frame_pts); let event_slot = config.event_slot_at(next_frame_pts); let seq = frame_index.saturating_add(1); relay.feed(VideoPacket { id: 0, pts: pts_us, data: frames.packet_for_frame(frame_index)?.to_vec(), seq, effective_fps: camera.fps, client_capture_pts_us: pts_us, client_send_pts_us: pts_us, client_queue_depth: 0, client_queue_age_ms: 0, ..Default::default() }); if let Some(slot) = event_slot { let monotonic_us = monotonic_us_since(start); timeline.mark_video( slot, seq, monotonic_us, unix_ns_from_start(server_start_unix_ns, monotonic_us), ); } video_frames = video_frames.saturating_add(1); frame_index = frame_index.saturating_add(1); } } sink.finish(); Ok(OutputDelayProbeSummary { video_frames, audio_packets, event_count: config.event_count(), timeline_json: serde_json::to_string(&timeline) .context("serializing output-delay server timeline")?, }) } #[cfg(coverage)] pub async fn run_server_output_delay_probe( _relay: Arc, _sink: &mut Voice, camera: &CameraConfig, request: &OutputDelayProbeRequest, ) -> Result { coverage_output_delay_summary(camera, request) } #[cfg(coverage)] fn coverage_output_delay_summary( camera: &CameraConfig, request: &OutputDelayProbeRequest, ) -> Result { let config = ProbeConfig::from_request(request)?; Ok(OutputDelayProbeSummary { video_frames: 1, audio_packets: 1, event_count: config.event_count(), timeline_json: serde_json::to_string(&OutputDelayProbeTimeline::new( &config, camera, unix_ns_now(), )) .unwrap_or_else(|_| "{}".to_string()), }) }