use std::collections::HashMap;
use std::process::ExitCode;
use dvb_si::ts::{TsPacket, TS_PACKET_SIZE};
use crate::util::{for_each_packet, read_file};
const PCR_CLOCK_HZ: u64 = 27_000_000;
const BITS_PER_PACKET: u64 = (TS_PACKET_SIZE as u64) * 8;
#[derive(Clone)]
struct PidRow {
pid: u16,
packets: u64,
}
fn estimate_bitrate_mbps(first: Option<(u64, u64)>, last: Option<(u64, u64)>) -> Option<f64> {
match (first, last) {
(Some((first_idx, first_v)), Some((last_idx, last_v)))
if last_v > first_v && last_idx > first_idx =>
{
let packets_between = last_idx - first_idx;
let delta = last_v - first_v;
let bps =
(packets_between * BITS_PER_PACKET) as f64 * (PCR_CLOCK_HZ as f64) / (delta as f64);
Some(bps / 1_000_000.0)
}
_ => None,
}
}
pub fn run(args: &[String]) -> ExitCode {
let mut path: Option<String> = None;
for arg in args {
match arg.as_str() {
"-h" | "--help" => {
eprintln!("usage: dvb-tools pids <file.ts>");
return ExitCode::SUCCESS;
}
other if other.starts_with('-') => {
eprintln!("dvb-tools pids: unknown option {other}");
return ExitCode::FAILURE;
}
other => path = Some(other.to_string()),
}
}
let Some(path) = path else {
eprintln!("usage: dvb-tools pids <file.ts>");
return ExitCode::FAILURE;
};
let data = match read_file(&path, "dvb-tools pids") {
Ok(d) => d,
Err(code) => return code,
};
let mut counts: HashMap<u16, u64> = HashMap::new();
let mut total_packets: u64 = 0;
let mut first_pcr: Option<(u64, u64)> = None;
let mut last_pcr: Option<(u64, u64)> = None;
let mut pcr_pid: Option<u16> = None;
for (idx, packet) in for_each_packet(&data).enumerate() {
total_packets = idx as u64 + 1;
let Ok(parsed) = TsPacket::parse(&packet) else {
continue;
};
let pid = parsed.header.pid;
*counts.entry(pid).or_insert(0) += 1;
if let Some(Ok(af)) = parsed.adaptation_field() {
if let Some(pcr) = af.pcr {
let pcr_27 = pcr.as_27mhz();
if first_pcr.is_none() {
first_pcr = Some((total_packets, pcr_27));
pcr_pid = Some(pid);
}
last_pcr = Some((total_packets, pcr_27));
}
}
}
if total_packets == 0 {
eprintln!("dvb-tools pids: no packets found");
return ExitCode::SUCCESS;
}
let mut rows: Vec<PidRow> = counts
.into_iter()
.map(|(pid, packets)| PidRow { pid, packets })
.collect();
rows.sort_by(|a, b| b.packets.cmp(&a.packets).then(a.pid.cmp(&b.pid)));
for row in &rows {
let pct = (row.packets as f64) * 100.0 / (total_packets as f64);
println!(
"pid=0x{:04X} packets={} {:.2}%",
row.pid, row.packets, pct
);
}
let bitrate_mbps = estimate_bitrate_mbps(first_pcr, last_pcr);
let pcr_label = match (bitrate_mbps, pcr_pid) {
(Some(mbps), Some(pid)) => format!("{mbps:.2} Mbit/s (PCR from pid 0x{pid:04X})"),
_ => "n/a (<=1 PCR seen)".to_string(),
};
eprintln!("-- total_packets={total_packets} bitrate={pcr_label}");
ExitCode::SUCCESS
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bitrate_from_two_pcrs() {
let first = Some((10, 0));
let last = Some((10_010, PCR_CLOCK_HZ));
let mbps = estimate_bitrate_mbps(first, last).expect("two valid PCRs");
assert!(
(mbps - 15.04).abs() < 1e-9,
"expected 15.04 Mbit/s, got {mbps}"
);
}
#[test]
fn bitrate_none_with_fewer_than_two_pcrs() {
assert_eq!(estimate_bitrate_mbps(None, None), None);
assert_eq!(estimate_bitrate_mbps(Some((5, 100)), Some((5, 100))), None);
}
#[test]
fn bitrate_none_when_pcr_does_not_advance() {
assert_eq!(
estimate_bitrate_mbps(Some((10, 500)), Some((20, 400))),
None
);
assert_eq!(
estimate_bitrate_mbps(Some((10, 500)), Some((20, 500))),
None
);
}
}