use std::collections::BTreeMap;
use super::disc::{ClipSummary, PlaylistSummary};
const SHORTFALL_FRACTION: f64 = 0.02;
const SHORTFALL_SECONDS: f64 = 1.0;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
pub struct ShortStreamFile {
file: String,
declared_seconds: f64,
measured_seconds: f64,
}
impl ShortStreamFile {
#[must_use]
pub fn file(&self) -> &str {
&self.file
}
#[must_use]
pub const fn declared_seconds(&self) -> f64 {
self.declared_seconds
}
#[must_use]
pub const fn measured_seconds(&self) -> f64 {
self.measured_seconds
}
#[must_use]
pub const fn missing_seconds(&self) -> f64 {
self.declared_seconds - self.measured_seconds
}
#[must_use]
pub fn notice(&self) -> String {
format!(
"{} is shorter than declared: measured {:.1} s of {:.1} s ({:.1} s missing)",
self.file(),
self.measured_seconds(),
self.declared_seconds(),
self.missing_seconds()
)
}
}
pub(crate) fn short_stream_files(playlists: &[PlaylistSummary]) -> Vec<ShortStreamFile> {
let mut worst: BTreeMap<String, ShortStreamFile> = BTreeMap::new();
for playlist in playlists {
for clip in &playlist.clips {
let Some(short) = clip_shortfall(clip) else {
continue;
};
if worst
.get(&short.file)
.is_none_or(|kept| short.missing_seconds() > kept.missing_seconds())
{
worst.insert(short.file.clone(), short);
}
}
}
worst.into_values().collect()
}
fn clip_shortfall(clip: &ClipSummary) -> Option<ShortStreamFile> {
if !clip.measured {
return None;
}
let missing = clip.length - clip.packet_seconds;
(missing > SHORTFALL_SECONDS.max(clip.length * SHORTFALL_FRACTION)).then(|| ShortStreamFile {
file: clip.name.clone(),
declared_seconds: clip.length,
measured_seconds: clip.packet_seconds,
})
}
#[cfg(test)]
mod tests {
use super::super::disc::{ClipStreamTally, ClipSummary, PlaylistSummary, fixtures};
use super::{ShortStreamFile, short_stream_files};
use crate::primitives::Pid;
use crate::stream::TsStreamType;
fn measured_clip(name: &str, declared: f64, measured: f64) -> ClipSummary {
ClipSummary {
streams: vec![ClipStreamTally {
pid: Pid::new(0x1011),
stream_type: TsStreamType::AvcVideo,
codec_short_name: "AVC".to_owned(),
payload_bytes: 1024,
packet_count: 8,
}],
..empty_clip(name, declared, measured)
}
}
fn empty_clip(name: &str, declared: f64, measured: f64) -> ClipSummary {
ClipSummary { measured: true, packet_seconds: measured, ..fixtures::clip(name, declared) }
}
fn disc_of(clips: Vec<ClipSummary>) -> Vec<PlaylistSummary> {
vec![fixtures::playlist("00000.MPLS", 100.0, clips)]
}
fn reported(playlists: &[PlaylistSummary]) -> Vec<(String, f64, f64)> {
short_stream_files(playlists)
.iter()
.map(|short| {
(short.file().to_owned(), short.declared_seconds(), short.measured_seconds())
})
.collect()
}
#[test]
fn a_file_measured_to_its_declared_span_is_not_reported() {
assert!(reported(&disc_of(vec![measured_clip("00000.M2TS", 100.0, 100.0)])).is_empty());
assert!(reported(&disc_of(vec![measured_clip("00000.M2TS", 100.0, 100.5)])).is_empty());
}
#[test]
fn a_file_the_demux_ran_out_of_is_reported_with_both_spans() {
let clips = vec![measured_clip("00011.M2TS", 1640.0, 820.0)];
assert_eq!(reported(&disc_of(clips)), vec![("00011.M2TS".to_owned(), 1640.0, 820.0)]);
}
#[test]
fn both_thresholds_have_to_be_passed() {
assert!(reported(&disc_of(vec![measured_clip("00000.M2TS", 100.0, 99.5)])).is_empty());
assert!(reported(&disc_of(vec![measured_clip("00000.M2TS", 1000.0, 985.0)])).is_empty());
assert!(reported(&disc_of(vec![measured_clip("00000.M2TS", 1000.0, 980.0)])).is_empty());
assert_eq!(reported(&disc_of(vec![measured_clip("00000.M2TS", 1000.0, 979.0)])).len(), 1);
assert!(reported(&disc_of(vec![measured_clip("00000.M2TS", 10.0, 9.1)])).is_empty());
assert_eq!(reported(&disc_of(vec![measured_clip("00000.M2TS", 10.0, 8.9)])).len(), 1);
}
#[test]
fn an_unmeasured_clip_is_never_reported() {
let unmeasured = fixtures::clip("00000.M2TS", 1640.0);
assert!(!unmeasured.measured);
assert!(reported(&disc_of(vec![unmeasured])).is_empty());
}
#[test]
fn a_file_that_demuxed_to_nothing_is_reported_as_the_whole_span_missing() {
let destroyed = empty_clip("00000.M2TS", 1640.0, 0.0);
assert!(destroyed.streams.is_empty());
assert_eq!(
reported(&disc_of(vec![destroyed])),
vec![("00000.M2TS".to_owned(), 1640.0, 0.0)]
);
}
#[test]
fn a_zero_length_clip_is_never_reported() {
assert!(reported(&disc_of(vec![measured_clip("00000.M2TS", 0.0, 0.0)])).is_empty());
}
#[test]
fn a_file_played_by_several_playlists_reports_its_worst_window() {
let big = measured_clip("00011.M2TS", 1640.0, 820.0);
let small = measured_clip("00011.M2TS", 100.0, 40.0);
let two = |first: &ClipSummary, second: &ClipSummary| {
vec![
fixtures::playlist("00000.MPLS", 100.0, vec![first.clone()]),
fixtures::playlist("00001.MPLS", 100.0, vec![second.clone()]),
]
};
let worst = vec![("00011.M2TS".to_owned(), 1640.0, 820.0)];
assert_eq!(reported(&two(&big, &small)), worst);
assert_eq!(reported(&two(&small, &big)), worst);
let early = measured_clip("00011.M2TS", 100.0, 40.0);
let late = measured_clip("00011.M2TS", 200.0, 140.0);
assert_eq!(reported(&two(&early, &late)), vec![("00011.M2TS".to_owned(), 100.0, 40.0)]);
}
#[test]
fn the_files_come_back_once_each_sorted_by_name() {
let clips = vec![
measured_clip("00033.M2TS", 100.0, 10.0),
measured_clip("00011.M2TS", 100.0, 20.0),
measured_clip("00033.M2TS", 100.0, 30.0),
];
let names: Vec<String> =
reported(&disc_of(clips)).into_iter().map(|(file, _, _)| file).collect();
assert_eq!(names, vec!["00011.M2TS".to_owned(), "00033.M2TS".to_owned()]);
}
#[test]
fn the_notice_spells_the_sentence_every_surface_raises() {
let short: Vec<ShortStreamFile> =
short_stream_files(&disc_of(vec![measured_clip("00011.M2TS", 1640.0, 500.0)]));
let first = short.first().expect("the short file is reported");
assert_eq!(
first.notice(),
"00011.M2TS is shorter than declared: measured 500.0 s of 1640.0 s (1140.0 s missing)"
);
}
#[test]
fn the_missing_span_is_the_difference_of_the_two() {
let short: Vec<ShortStreamFile> =
short_stream_files(&disc_of(vec![measured_clip("00000.M2TS", 100.0, 40.0)]));
let first = short.first().expect("the short file is reported");
assert_eq!(first.missing_seconds().to_bits(), 60.0_f64.to_bits());
}
mod prop {
use proptest::prelude::{prop_assert, prop_assert_eq, proptest};
use super::super::{SHORTFALL_FRACTION, SHORTFALL_SECONDS, short_stream_files};
use super::{disc_of, measured_clip};
proptest! {
#[test]
fn a_demux_that_reached_the_declared_span_is_never_reported(
declared in 0.0_f64..100_000.0,
over in 0.0_f64..1_000.0,
) {
let clips = vec![measured_clip("00000.M2TS", declared, declared + over)];
prop_assert!(short_stream_files(&disc_of(clips)).is_empty());
}
#[test]
fn every_reported_file_passed_both_thresholds(
declared in 0.0_f64..100_000.0,
measured in -1_000.0_f64..100_000.0,
) {
let clips = vec![measured_clip("00000.M2TS", declared, measured)];
for short in short_stream_files(&disc_of(clips)) {
prop_assert_eq!(
short.missing_seconds().to_bits(),
(declared - measured).to_bits()
);
prop_assert!(short.missing_seconds() > SHORTFALL_SECONDS);
prop_assert!(short.missing_seconds() > declared * SHORTFALL_FRACTION);
}
}
#[test]
fn a_non_finite_span_is_never_reported(measured in -1_000.0_f64..1_000.0) {
for declared in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
let clips = vec![measured_clip("00000.M2TS", declared, measured)];
prop_assert!(short_stream_files(&disc_of(clips)).is_empty());
}
}
}
}
}