167 lines
4.7 KiB
Rust
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(&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
|
|
}
|