//! 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: 8, max_age: Duration::from_millis(350), }; #[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).saturating_mul(3)] } #[cfg(any(not(coverage), test))] fn build_regular_probe_frame(width: usize, height: usize) -> Vec { let mut frame = build_dark_probe_frame(width, height); let x0 = width / 4; let x1 = width.saturating_sub(x0); let y0 = height / 4; let y1 = height.saturating_sub(y0); fill_rect(&mut frame, width, x0, y0, x1, y1, 255); frame } #[cfg(any(not(coverage), test))] fn build_marker_probe_frame(width: usize, height: usize) -> Vec { let mut frame = build_dark_probe_frame(width, height); let x0 = width / 5; let x1 = width.saturating_sub(x0); let y0 = height / 5; let y1 = height.saturating_sub(y0); fill_rect(&mut frame, width, x0, y0, x1, y1, 255); 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), y0, (cx + cross_half_w).min(width), y1, 255, ); fill_rect( &mut frame, width, x0, cy.saturating_sub(cross_half_h), x1, (cy + cross_half_h).min(height), 255, ); 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.saturating_mul(3); 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) * 3; frame[offset] = value; frame[offset + 1] = value; frame[offset + 2] = 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 }