use crate::helper::lock;
use crate::shm::Frame;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
const PAUSE: Duration = Duration::from_millis(250);
const STUTTER: Duration = Duration::from_millis(34);
const QUEUE: usize = 3;
#[derive(Debug, Clone)]
pub struct TapFrame {
pub at: Instant,
pub frame: Frame,
}
#[derive(Debug, Clone, Default)]
pub struct TapHealth {
pub received: u64,
pub sampled: u64,
pub skipped_by_pipeline: u64,
pub skipped_by_tap: u64,
pub max_gap_ms: f32,
pub gaps_over_34ms: u32,
pub last_arrival: Option<Instant>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TapRecv {
Timeout,
Closed,
}
struct State {
health: TapHealth,
next_due: Option<Instant>,
prev_seq: Option<u64>,
}
struct Shared {
state: Mutex<State>,
in_flight: AtomicUsize,
}
pub struct FrameTap {
rx: Receiver<TapFrame>,
shared: Arc<Shared>,
}
#[doc(hidden)]
pub struct TapFeeder {
tx: Sender<TapFrame>,
shared: Arc<Shared>,
interval: Duration,
}
impl FrameTap {
#[doc(hidden)]
pub fn manual(fps: u32) -> (FrameTap, TapFeeder) {
let (tx, rx) = mpsc::channel();
let shared = Arc::new(Shared {
state: Mutex::new(State {
health: TapHealth::default(),
next_due: None,
prev_seq: None,
}),
in_flight: AtomicUsize::new(0),
});
let interval = Duration::from_secs(1) / fps.clamp(1, 60);
let feeder = TapFeeder {
tx,
shared: shared.clone(),
interval,
};
(FrameTap { rx, shared }, feeder)
}
pub fn recv_timeout(&self, wait: Duration) -> Result<TapFrame, TapRecv> {
match self.rx.recv_timeout(wait) {
Ok(f) => {
self.shared.in_flight.fetch_sub(1, Ordering::SeqCst);
Ok(f)
}
Err(RecvTimeoutError::Timeout) => Err(TapRecv::Timeout),
Err(RecvTimeoutError::Disconnected) => Err(TapRecv::Closed),
}
}
pub fn health(&self) -> TapHealth {
lock(&self.shared.state).health.clone()
}
}
impl TapFeeder {
pub fn offer(&self, at: Instant, frame: &Frame) -> bool {
if Arc::strong_count(&self.shared) < 2 {
return false;
}
{
let mut s = lock(&self.shared.state);
let State {
health: h,
next_due,
prev_seq,
} = &mut *s;
h.received += 1;
if let Some(prev) = h.last_arrival {
let gap = at.saturating_duration_since(prev);
if gap < PAUSE {
h.max_gap_ms = h.max_gap_ms.max(gap.as_secs_f32() * 1000.0);
if gap > STUTTER {
h.gaps_over_34ms += 1;
}
}
}
h.last_arrival = Some(at);
if let Some(prev) = *prev_seq
&& frame.seq > prev
{
h.skipped_by_pipeline += ((frame.seq - prev) / 2).saturating_sub(1);
}
*prev_seq = Some(frame.seq);
let tol = self.interval / 5;
if next_due.is_some_and(|due| at + tol < due) {
return true;
}
let after_this = at + self.interval - tol;
*next_due = Some(match *next_due {
Some(due) => (due + self.interval).max(after_this),
None => after_this,
});
if self.shared.in_flight.load(Ordering::SeqCst) >= QUEUE {
h.skipped_by_tap += 1;
return true;
}
self.shared.in_flight.fetch_add(1, Ordering::SeqCst);
h.sampled += 1;
}
let sent = TapFrame {
at,
frame: frame.clone(),
};
self.tx.send(sent).is_ok()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{Duration, Instant};
const W: Duration = Duration::from_secs(5);
fn frame(seq: u64) -> Frame {
Frame {
width: 1,
height: 1,
source_width: 1,
source_height: 1,
seq,
data: vec![seq as u8; 4],
}
}
fn ms(n: f64) -> Duration {
Duration::from_secs_f64(n / 1000.0)
}
fn feed_steady(fps: u32, n: u64, every_ms: f64) -> TapHealth {
let (tap, feeder) = FrameTap::manual(fps);
let t0 = Instant::now();
for i in 0..n {
assert!(feeder.offer(t0 + ms(every_ms * i as f64), &frame(2 + 2 * i)));
while tap.recv_timeout(Duration::ZERO).is_ok() {}
}
tap.health()
}
#[test]
fn samples_60fps_input_at_about_30() {
let h = feed_steady(30, 120, 16.7);
assert_eq!(h.received, 120);
assert!((58..=62).contains(&h.sampled), "sampled {}", h.sampled);
assert_eq!((h.skipped_by_pipeline, h.skipped_by_tap), (0, 0));
}
#[test]
fn fps_25_samples_about_25() {
let h = feed_steady(25, 150, 16.7);
assert!((60..=65).contains(&h.sampled), "sampled {}", h.sampled);
}
#[test]
fn no_burst_after_a_pause() {
let (tap, feeder) = FrameTap::manual(30);
let t0 = Instant::now();
let mut sampled = Vec::new();
let mut offer = |at_ms: f64, seq: u64| {
feeder.offer(t0 + ms(at_ms), &frame(seq));
while let Ok(t) = tap.recv_timeout(Duration::ZERO) {
sampled.push(t.frame.seq);
}
};
for i in 0..30u64 {
offer(16.7 * i as f64, 2 + 2 * i);
}
let resume = 16.7 * 29.0 + 1000.0;
for i in 0..6u64 {
offer(resume + 16.7 * i as f64, 62 + 2 * i);
}
assert!(
sampled.contains(&62),
"first frame after the pause: {sampled:?}"
);
assert!(
!sampled.contains(&64),
"no catch-up sample right after it: {sampled:?}"
);
assert!(sampled.contains(&66), "cadence resumes: {sampled:?}");
}
#[test]
fn fps_is_clamped() {
assert_eq!(feed_steady(0, 100, 10.0).sampled, 2);
let h = feed_steady(1000, 120, 1000.0 / 120.0);
assert!((58..=62).contains(&h.sampled), "sampled {}", h.sampled);
}
#[test]
fn seq_gaps_count_pipeline_skips() {
let (tap, feeder) = FrameTap::manual(30);
let t0 = Instant::now();
for (i, seq) in [2, 4, 10].into_iter().enumerate() {
feeder.offer(t0 + ms(40.0 * i as f64), &frame(seq));
}
assert_eq!(tap.health().skipped_by_pipeline, 2);
}
#[test]
fn seq_reset_is_not_a_gap() {
let (tap, feeder) = FrameTap::manual(30);
let t0 = Instant::now();
for (i, seq) in [10, 12, 2, 4].into_iter().enumerate() {
feeder.offer(t0 + ms(40.0 * i as f64), &frame(seq));
}
assert_eq!(tap.health().skipped_by_pipeline, 0);
}
#[test]
fn full_queue_skips_without_blocking() {
let (tap, feeder) = FrameTap::manual(30);
let t0 = Instant::now();
let start = Instant::now();
for i in 0..10u64 {
assert!(feeder.offer(t0 + ms(40.0 * i as f64), &frame(2 + 2 * i)));
}
let took = start.elapsed();
assert!(took < Duration::from_millis(50), "10 offers took {took:?}");
let h = tap.health();
assert_eq!((h.received, h.sampled, h.skipped_by_tap), (10, 3, 7));
let seqs: Vec<u64> = (0..3)
.map(|_| tap.recv_timeout(W).unwrap().frame.seq)
.collect();
assert_eq!(seqs, [2, 4, 6]);
assert!(feeder.offer(t0 + ms(400.0), &frame(22)));
assert_eq!(tap.recv_timeout(W).unwrap().frame.seq, 22);
}
#[test]
fn pauses_are_not_stutters() {
let (tap, feeder) = FrameTap::manual(30);
let mut at = Instant::now();
feeder.offer(at, &frame(2));
for (i, gap) in [16.0, 40.0, 600.0].into_iter().enumerate() {
at += ms(gap);
feeder.offer(at, &frame(4 + 2 * i as u64));
}
let h = tap.health();
assert!(
(h.max_gap_ms - 40.0).abs() < 0.01,
"max gap {}",
h.max_gap_ms
);
assert_eq!(h.gaps_over_34ms, 1);
assert_eq!(h.last_arrival, Some(at));
}
#[test]
fn offer_returns_false_after_tap_drop() {
let (tap, feeder) = FrameTap::manual(30);
let t0 = Instant::now();
assert!(feeder.offer(t0, &frame(2)));
drop(tap);
assert!(!feeder.offer(t0 + ms(40.0), &frame(4)));
assert!(
!feeder.offer(t0 + ms(41.0), &frame(6)),
"not sampled, still false"
);
}
#[test]
fn recv_reports_closed_when_feeders_gone() {
let (tap, feeder) = FrameTap::manual(30);
assert_eq!(
tap.recv_timeout(Duration::from_millis(10)).unwrap_err(),
TapRecv::Timeout
);
feeder.offer(Instant::now(), &frame(2));
drop(feeder);
assert_eq!(tap.recv_timeout(W).unwrap().frame.seq, 2);
let start = Instant::now();
assert_eq!(tap.recv_timeout(W).unwrap_err(), TapRecv::Closed);
assert!(
start.elapsed() < Duration::from_secs(1),
"Closed is immediate"
);
}
#[test]
fn two_taps_both_receive() {
let (a, fa) = FrameTap::manual(30);
let (b, fb) = FrameTap::manual(30);
let t0 = Instant::now();
for i in 0..3u64 {
let (at, f) = (t0 + ms(40.0 * i as f64), frame(2 + 2 * i));
assert!(fa.offer(at, &f));
assert!(fb.offer(at, &f));
}
for tap in [&a, &b] {
let got: Vec<(Instant, u64)> = (0..3)
.map(|_| {
let t = tap.recv_timeout(W).unwrap();
(t.at, t.frame.seq)
})
.collect();
assert_eq!(got, [(t0, 2), (t0 + ms(40.0), 4), (t0 + ms(80.0), 6)]);
}
}
}