167 lines
4.7 KiB
Rust

//! 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<u8> {
vec![16u8; width.saturating_mul(height).saturating_mul(3)]
}
#[cfg(any(not(coverage), test))]
fn build_regular_probe_frame(width: usize, height: usize) -> Vec<u8> {
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<u8> {
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<u8> {
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::<i16>());
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(&amplitude.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
}