use std::collections::BTreeSet;
use super::{Stats, StreamStats};
#[derive(Default)]
pub struct Log {
previous: Option<Stats>,
quiet: BTreeSet<u16>,
}
impl Log {
pub const INTERVAL: std::time::Duration = std::time::Duration::from_secs(1);
pub fn sample(&mut self, latest: Stats) {
self.sync(&latest);
for (pid, stream) in &latest.streams {
let previous = self.previous.as_ref().and_then(|previous| previous.streams.get(pid));
if previous.is_none_or(|previous| previous.units != stream.units) {
self.quiet.remove(pid);
} else if self.quiet.insert(*pid) {
tracing::info!(
pid = *pid,
track = stream.track,
units = stream.units,
quiet = ?stream.quiet,
"elementary stream stopped delivering access units"
);
}
}
self.previous = Some(latest);
}
pub fn finish(&self, latest: &Stats) {
self.sync(latest);
}
fn sync(&self, latest: &Stats) {
if self.previous.as_ref().map_or(0, |previous| previous.crc_error) != latest.crc_error {
tracing::info!(
crc_error = latest.crc_error,
"PAT or PMT sections dropped for a bad CRC"
);
}
let lost = |stream: &StreamStats| (stream.resyncs, stream.discarded, stream.unconfirmed);
for (pid, stream) in &latest.streams {
let previous = self.previous.as_ref().and_then(|previous| previous.streams.get(pid));
if previous.map_or((0, 0, 0), lost) == lost(stream) {
continue;
}
tracing::info!(
pid = *pid,
track = stream.track,
resyncs = stream.resyncs,
discarded = stream.discarded,
unconfirmed = stream.unconfirmed,
"audio frame sync lost"
);
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
#[tracing_test::traced_test]
fn reports_a_stream_that_stopped() {
const VIDEO: u16 = 0x100;
const AUDIO: u16 = 0x101;
let sample = |video: u64, audio: u64| {
let mut stats = Stats::default();
for (pid, track, units) in [(VIDEO, ".avc3", video), (AUDIO, ".mp2", audio)] {
let stream = StreamStats {
track,
units,
quiet: Some(std::time::Duration::from_millis(40)),
..Default::default()
};
stats.streams.insert(pid, stream);
}
stats
};
let mut log = Log::default();
for (video, audio) in [(10, 10), (10, 20), (10, 30), (11, 40), (11, 50)] {
log.sample(sample(video, audio));
}
logs_assert(|lines: &[&str]| {
let stopped: Vec<_> = lines
.iter()
.filter(|line| line.contains("stopped delivering access units"))
.collect();
match stopped.as_slice() {
[first, second] if [first, second].iter().all(|line| line.contains("pid=256")) => Ok(()),
_ => Err(format!("expected two lines for the video PID, got {stopped:?}")),
}
});
assert!(!logs_contain("audio frame sync lost"), "nothing lost frame sync");
}
#[test]
#[tracing_test::traced_test]
fn reports_frame_sync_lost_at_the_end() {
let sample = |resyncs: u64, units: u64| {
let stream = StreamStats {
track: ".mp2",
units,
resyncs,
..Default::default()
};
let mut stats = Stats::default();
stats.streams.insert(0x101, stream);
stats
};
let mut log = Log::default();
log.sample(sample(0, 10));
log.sample(sample(0, 20));
log.finish(&sample(1, 21));
logs_assert(|lines: &[&str]| {
match lines
.iter()
.filter(|line| line.contains("audio frame sync lost"))
.count()
{
1 => Ok(()),
n => Err(format!("expected one frame-sync line, got {n}")),
}
});
assert!(!logs_contain("stopped delivering"), "the stream kept counting");
}
#[test]
#[tracing_test::traced_test]
fn reports_crc_errors_when_they_move() {
let sample = |crc_error: u64| Stats {
crc_error,
..Default::default()
};
let mut log = Log::default();
log.sample(sample(0));
log.sample(sample(2));
log.sample(sample(2));
log.finish(&sample(3));
logs_assert(|lines: &[&str]| {
let dropped: Vec<_> = lines.iter().filter(|line| line.contains("bad CRC")).collect();
match dropped.as_slice() {
[first, second] if first.contains("crc_error=2") && second.contains("crc_error=3") => Ok(()),
_ => Err(format!("expected one line per move, got {dropped:?}")),
}
});
}
}