use std::collections::BTreeMap;
use anyhow::Context;
use mpeg2ts::ts::{Pid, TsPacket};
use super::import::{Framer, PatReader};
use super::{Ext, Import, Stats, psi};
use crate::catalog;
pub struct Programs {
origin: moq_net::origin::Producer,
name: moq_net::PathOwned,
config: catalog::Config,
live: bool,
pending: Vec<u8>,
scan: PatScan,
programs: Vec<Program>,
}
struct Program {
_broadcast: moq_net::broadcast::Producer,
import: Import<Ext>,
catalog: catalog::Producer<Ext>,
}
impl Programs {
pub fn new(origin: moq_net::origin::Producer, name: impl moq_net::AsPath, config: catalog::Config) -> Self {
Self {
origin,
name: name.as_path().to_owned(),
config,
live: false,
pending: Vec::new(),
scan: PatScan::default(),
programs: Vec::new(),
}
}
pub fn live(mut self) -> Self {
self.live = true;
self
}
pub fn decode(&mut self, data: &[u8]) -> anyhow::Result<()> {
if !self.programs.is_empty() {
return self.feed(data);
}
self.pending.extend_from_slice(data);
let Some(pat) = self.scan.scan(&mut self.pending) else {
return Ok(());
};
for program in pat.program_numbers() {
let name = program_broadcast(self.name.as_str(), program);
let mut broadcast = self
.origin
.create_broadcast(&name)
.with_context(|| format!("failed to create broadcast {name}"))?;
let config = self
.config
.clone()
.with_catalog(catalog::hang::Catalog::<Ext>::default());
let catalog = catalog::Producer::new(&mut broadcast, config)?;
let mut import = Import::new(broadcast.clone(), catalog.reserve()).with_program(program);
if self.live {
import = import.live();
}
import.handle_pat(&pat)?;
broadcast
.announce(Default::default())
.with_context(|| format!("failed to announce broadcast {name}"))?;
self.programs.push(Program {
_broadcast: broadcast,
import,
catalog,
});
}
let pending = std::mem::take(&mut self.pending);
self.feed(&pending)
}
fn feed(&mut self, data: &[u8]) -> anyhow::Result<()> {
for program in &mut self.programs {
program.import.decode(data)?;
}
Ok(())
}
pub fn stats(&self) -> Stats {
let mut stats = Stats::default();
let mut crc_errors = BTreeMap::<u16, u64>::new();
for program in &self.programs {
stats.streams.extend(program.import.stats().streams);
for (&pid, &count) in program.import.crc_errors() {
let merged = crc_errors.entry(pid).or_default();
*merged = (*merged).max(count);
}
}
*crc_errors.entry(Pid::PAT).or_default() += self.scan.crc_error;
stats.crc_error = crc_errors.values().sum();
stats
}
pub fn finish(&mut self) -> anyhow::Result<()> {
for program in &mut self.programs {
program.import.finish()?;
program.catalog.finish()?;
}
Ok(())
}
pub fn abort(self, err: moq_net::Error) {
for program in self.programs {
program.import.abort(err.clone());
}
}
}
fn program_broadcast(name: &str, program: u16) -> String {
match catalog::CatalogFormat::detect(name) {
Some(format) => {
let stem = &name[..name.len() - format.extension().len()];
format!("{stem}/{program}{}", format.extension())
}
None => format!("{name}/{program}"),
}
}
#[derive(Default)]
struct PatScan {
framer: Framer,
pat: PatReader,
crc_error: u64,
}
impl PatScan {
fn scan(&mut self, pending: &mut Vec<u8>) -> Option<psi::Pat> {
let mut off = 0;
let mut found = None;
while let Some(at) = self.framer.next(pending, &mut off) {
let pkt: &[u8; TsPacket::SIZE] = pending[at..at + TsPacket::SIZE].try_into().unwrap();
if (u16::from(pkt[1] & 0x1f) << 8 | u16::from(pkt[2])) != Pid::PAT {
continue;
}
if let Some(pat) = self.pat.push(pkt, &mut self.crc_error)
&& !pat.program_numbers().is_empty()
{
found = Some(pat);
break;
}
}
pending.drain(..off);
found
}
}
#[cfg(test)]
mod test {
use std::time::Duration;
use super::*;
use crate::catalog::hang::Container;
use crate::container::Consumer;
use crate::container::ts::import::test::{
corrupt, fifty_programs, pat_section, psi_packets, two_programs, two_section_pat,
};
#[test]
fn program_broadcasts_keep_the_catalog_suffix_last() {
assert_eq!(program_broadcast("event.hang", 2), "event/2.hang");
assert_eq!(program_broadcast("demo/event.msf", 7), "demo/event/7.msf");
assert_eq!(program_broadcast("event", 1), "event/1");
}
fn pat_programs(data: &[u8]) -> Option<Vec<u16>> {
Some(PatScan::default().scan(&mut data.to_vec())?.program_numbers())
}
#[test]
fn pat_programs_reads_the_first_whole_pat() {
let data = two_programs();
assert_eq!(pat_programs(&data), Some(vec![1, 2]));
let mut shifted = vec![0x47, 0x40, 0x00, 0x10];
shifted.extend_from_slice(&data);
assert_eq!(pat_programs(&shifted), Some(vec![1, 2]));
assert_eq!(pat_programs(&data[..100]), None);
}
#[tokio::test]
async fn a_pat_spanning_two_packets_starts_every_program() {
let origin = crate::source::produce_origin();
let mut programs = Programs::new(origin.clone(), "event.hang", catalog::Config::default());
let input = fifty_programs();
for chunk in input.chunks(100) {
programs.decode(chunk).unwrap();
}
programs.finish().unwrap();
assert_eq!(programs.programs.len(), 50);
let consumer = crate::Source::new(origin.consume(), "event/50.hang")
.broadcast()
.await
.unwrap();
let catalog = hang::catalog::Catalog::<()>::subscribe(&consumer)
.await
.unwrap()
.next()
.await
.unwrap()
.expect("a catalog");
assert_eq!(catalog.audio.renditions.len(), 1, "program 50 publishes its stream");
}
#[test]
fn a_corrupt_pat_starts_no_program() {
let origin = crate::source::produce_origin();
let mut programs = Programs::new(origin, "event.hang", catalog::Config::default());
let mut null = vec![0x47, 0x1f, 0xff, 0x10];
null.resize(188, 0xff);
let mut cc = 0;
let mut data = psi_packets(0, &mut cc, &[], &corrupt(pat_section(&[(1, 0x0100)])));
data.extend_from_slice(&null);
programs.decode(&data).unwrap();
assert!(programs.programs.is_empty());
assert_eq!(programs.stats().crc_error, 1);
let mut data = psi_packets(0, &mut cc, &[], &pat_section(&[(1, 0x0100)]));
data.extend_from_slice(&null);
programs.decode(&data).unwrap();
assert_eq!(programs.programs.len(), 1);
assert_eq!(programs.stats().crc_error, 1);
}
async fn renditions(origin: &moq_net::origin::Producer, path: &str) -> usize {
let consumer = crate::Source::new(origin.consume(), path).broadcast().await.unwrap();
let catalog = hang::catalog::Catalog::<()>::subscribe(&consumer)
.await
.unwrap()
.next()
.await
.unwrap()
.expect("a catalog");
catalog.audio.renditions.len()
}
#[tokio::test]
async fn a_pat_in_two_sections_starts_every_program() {
let origin = crate::source::produce_origin();
let mut programs = Programs::new(origin.clone(), "event.hang", catalog::Config::default());
programs.decode(&two_section_pat()).unwrap();
programs.finish().unwrap();
assert_eq!(programs.programs.len(), 2);
assert_eq!(renditions(&origin, "event/1.hang").await, 1);
assert_eq!(renditions(&origin, "event/2.hang").await, 1);
}
#[test]
fn a_partial_pat_does_not_hold_the_multiplex() {
let origin = crate::source::produce_origin();
let mut programs = Programs::new(origin, "event.hang", catalog::Config::default());
programs.decode(&fifty_programs()[..188]).unwrap();
let mut null = vec![0x47, 0x1f, 0xff, 0x10];
null.resize(188, 0xff);
for _ in 0..1000 {
programs.decode(&null).unwrap();
}
assert!(programs.programs.is_empty());
assert!(
programs.pending.len() < 2 * 188,
"held {} bytes",
programs.pending.len()
);
}
#[test]
fn a_corrupt_pat_beside_a_good_one_counts_once() {
let origin = crate::source::produce_origin();
let mut programs = Programs::new(origin, "event.hang", catalog::Config::default());
let mut sections = corrupt(pat_section(&[(1, 0x0100)]));
sections.extend(pat_section(&[(1, 0x0100)]));
let mut data = psi_packets(0, &mut 0, &[], §ions);
let mut null = vec![0x47, 0x1f, 0xff, 0x10];
null.resize(188, 0xff);
data.extend_from_slice(&null);
programs.decode(&data).unwrap();
assert_eq!(programs.programs.len(), 1);
assert_eq!(programs.stats().crc_error, 1);
}
#[tokio::test]
async fn every_program_publishes_its_own_broadcast() {
let origin = crate::source::produce_origin();
let ago = Duration::from_secs(60);
let clock = crate::Clock::at(std::time::Instant::now() - ago, std::time::SystemTime::now() - ago).unwrap();
let config = catalog::Config::default().with_clock(clock);
let mut programs = Programs::new(origin.clone(), "event.hang", config).live();
let input = two_programs();
let before = clock.now();
programs.decode(&input[..100]).unwrap();
assert!(
programs.programs.is_empty(),
"no program is known before the PAT is whole"
);
programs.decode(&input[100..]).unwrap();
programs.finish().unwrap();
let after = clock.now();
for (path, fills) in [("event/1.hang", [0xAA, 0xBB]), ("event/2.hang", [0xCC, 0xDD])] {
let consumer = crate::Source::new(origin.consume(), path).broadcast().await.unwrap();
let catalog = hang::catalog::Catalog::<()>::subscribe(&consumer)
.await
.unwrap()
.next()
.await
.unwrap()
.expect("a catalog");
let renditions: Vec<_> = catalog.audio.renditions.iter().collect();
assert_eq!(renditions.len(), 1, "{path} carries its own program's one stream");
let (name, config) = renditions[0];
let track = consumer.track(name).unwrap().subscribe(None).await.unwrap();
let container = Container::try_from(config).unwrap();
let frame = Consumer::new(track, container).read().await.unwrap().expect("a frame");
assert!(fills.contains(&frame.payload[4]), "{path} carries only its own program");
let skew = Duration::from_secs(2).as_micros();
assert!(
before.as_micros() - skew <= frame.timestamp.as_micros()
&& frame.timestamp.as_micros() <= after.as_micros() + skew,
"{path} is live on arrival: {:?} not in {before:?}..={after:?}",
frame.timestamp
);
}
}
}