use alloc::format;
use broadcast_common::Unpackage;
use transmux::{CodecConfig, Media, TsDemux};
use crate::Diagnostic;
use crate::Report;
use crate::report::{Finding, Location, Severity};
const HEVC_NAL_SPS: u8 = 33;
const FPS_TOLERANCE_FRACTION: f32 = 0.10;
const MIN_SAMPLES_FOR_CADENCE: usize = 4;
#[derive(Debug, Clone, Copy)]
pub struct FpsCadenceCheck;
impl Diagnostic for FpsCadenceCheck {
fn run(&self, ts: &[u8], report: &mut Report) {
let Ok(media) = TsDemux::new().unpackage(ts) else {
return;
};
check_media(&media, report);
}
}
fn check_media(media: &Media, report: &mut Report) {
for track in &media.tracks {
let pid = track.spec.source_pid.unwrap_or(0);
let declared_fps = match &track.spec.config {
CodecConfig::Avc { config, .. } => config.config.sps.first().and_then(|s| {
let info = s.decode().ok()?;
if !info.frame_mbs_only {
return None;
}
info.fps
}),
CodecConfig::Hevc { config, .. } => config
.config
.arrays
.iter()
.find(|a| a.nal_unit_type == HEVC_NAL_SPS)
.and_then(|a| a.nalus.first())
.and_then(|n| n.decode_sps().ok().flatten()?.fps),
_ => None,
};
let Some(declared_fps) = declared_fps else {
continue;
};
if declared_fps <= 0.0 {
continue;
}
if track.samples.len() < MIN_SAMPLES_FOR_CADENCE {
continue;
}
let total_duration: u64 = track
.samples
.iter()
.map(|s| s.duration.unwrap_or(0) as u64)
.sum();
if total_duration == 0 {
continue;
}
let mean_duration = total_duration as f64 / track.samples.len() as f64;
let timescale = track.timescale();
if timescale == 0 {
continue;
}
let measured_fps = timescale as f64 / mean_duration;
let relative_diff =
(measured_fps - declared_fps as f64).abs() / declared_fps.max(1.0) as f64;
if relative_diff > FPS_TOLERANCE_FRACTION as f64 {
report.push(Finding::new(
Severity::Warning,
Location::new(0, pid),
"fps-cadence-mismatch",
format!(
"Track (PID 0x{pid:04X}) VUI-declared frame rate {declared_fps:.3} fps \
disagrees with the measured sample cadence {measured_fps:.3} fps \
(mean sample duration {mean_duration:.1} ticks @ {timescale} Hz) — \
ITU-T H.264 §E.2.1 / ITU-T H.265 §E.2.1 VUI timing vs. \
ISO/IEC 13818-1 §2.4.3.7 PES-timestamp cadence"
),
));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_media_no_findings() {
let media = Media::new(alloc::vec::Vec::new(), 90_000);
let mut report = Report::new();
check_media(&media, &mut report);
assert!(report.is_empty());
}
}