//! Shared-clock synthetic A/V source for the upstream sync probe. #[cfg(any(not(coverage), test))] use anyhow::{Context, Result, bail}; #[cfg(any(not(coverage), test))] use gst::prelude::*; #[cfg(any(not(coverage), test))] use gstreamer as gst; #[cfg(any(not(coverage), test))] use gstreamer_app as gst_app; #[cfg(any(not(coverage), test))] use lesavka_common::lesavka::{AudioPacket, VideoPacket}; #[cfg(any(not(coverage), test))] use std::sync::{ Arc, atomic::{AtomicBool, Ordering}, }; #[cfg(any(not(coverage), test))] use std::thread::{self, JoinHandle}; #[cfg(any(not(coverage), test))] use std::time::{Duration, Instant}; #[cfg(any(not(coverage), test))] use std::{f64::consts::TAU, mem::size_of}; #[cfg(any(not(coverage), test))] use crate::input::camera::{CameraCodec, CameraConfig}; #[cfg(any(not(coverage), test))] use crate::sync_probe::schedule::PulseSchedule; #[cfg(any(not(coverage), test))] use crate::uplink_fresh_queue::{FreshPacketQueue, FreshQueueConfig}; #[cfg(coverage)] mod coverage_stub; #[cfg(not(coverage))] mod runtime; #[cfg(test)] mod tests; #[cfg(coverage)] pub use coverage_stub::SyncProbeCapture; #[cfg(not(coverage))] pub use runtime::SyncProbeCapture; #[cfg(any(not(coverage), test))] const PROBE_VIDEO_QUEUE: FreshQueueConfig = FreshQueueConfig { capacity: 32, max_age: Duration::from_secs(1), }; #[cfg(any(not(coverage), test))] const PROBE_AUDIO_QUEUE: FreshQueueConfig = FreshQueueConfig { capacity: 32, max_age: Duration::from_millis(400), }; #[cfg(any(not(coverage), test))] const AUDIO_SAMPLE_RATE: i32 = 48_000; #[cfg(any(not(coverage), test))] const AUDIO_CHANNELS: usize = 2; #[cfg(any(not(coverage), test))] const AUDIO_CHUNK_MS: u64 = 10; #[cfg(any(not(coverage), test))] const AUDIO_PULSE_FREQUENCY_HZ: f64 = 1_800.0; #[cfg(any(not(coverage), test))] const AUDIO_PULSE_AMPLITUDE: f64 = 24_000.0; #[cfg(any(not(coverage), test))] fn build_dark_probe_frame(width: usize, height: usize) -> Vec { vec![16u8; width.saturating_mul(height)] } #[cfg(any(not(coverage), test))] fn build_regular_probe_frame(width: usize, height: usize) -> Vec { vec![240u8; width.saturating_mul(height)] } #[cfg(any(not(coverage), test))] fn build_marker_probe_frame(width: usize, height: usize) -> Vec { let mut frame = build_regular_probe_frame(width, height); let cross_half_w = (width / 48).max(6); let cross_half_h = (height / 48).max(6); let cx = width / 2; let cy = height / 2; fill_rect( &mut frame, width, cx.saturating_sub(cross_half_w), 0, (cx + cross_half_w).min(width), height, 16, ); fill_rect( &mut frame, width, 0, cy.saturating_sub(cross_half_h), width, (cy + cross_half_h).min(height), 16, ); frame } #[cfg(any(not(coverage), test))] fn fill_rect( frame: &mut [u8], width: usize, x0: usize, y0: usize, x1: usize, y1: usize, value: u8, ) { let height = frame.len() / width.max(1); let x1 = x1.min(width); let y1 = y1.min(height); for y in y0.min(height)..y1 { for x in x0.min(width)..x1 { let offset = y * width + x; frame[offset] = value; } } } #[cfg(any(not(coverage), test))] fn render_audio_chunk( schedule: &PulseSchedule, chunk_pts: Duration, samples_per_chunk: usize, ) -> Vec { let sample_step = Duration::from_nanos(1_000_000_000u64 / AUDIO_SAMPLE_RATE as u64); let mut pcm = Vec::with_capacity(samples_per_chunk * AUDIO_CHANNELS * size_of::()); for sample_index in 0..samples_per_chunk { let sample_pts = chunk_pts + sample_step.saturating_mul(sample_index as u32); let amplitude = if schedule.flash_active(sample_pts) { let phase = TAU * AUDIO_PULSE_FREQUENCY_HZ * sample_pts.as_secs_f64(); (phase.sin() * AUDIO_PULSE_AMPLITUDE) as i16 } else { 0 }; for _ in 0..AUDIO_CHANNELS { pcm.extend_from_slice(&litude.to_le_bytes()); } } pcm } #[cfg(test)] fn probe_pts_exceeds_duration(pts_usecs: u64, duration: std::time::Duration) -> bool { pts_usecs > duration.as_micros() as u64 }