use std::collections::{BTreeMap, HashMap, HashSet};
use anyhow::Context;
use mpeg2ts::es::StreamType;
use mpeg2ts::ts::{Pid, TsPacket};
use super::adts;
use super::catalog;
use super::psi::{self, PesStart, Pmt};
use crate::catalog::hang::CatalogExt;
use crate::codec::{aac, ac3, eac3, h264, h265, legacy, mp2, opus};
use moq_net::Timestamp;
pub struct Import<E: catalog::Catalog = ()> {
broadcast: moq_net::broadcast::Producer,
catalog: crate::catalog::Producer<E>,
container: hang::catalog::Container,
initial_reservation: Option<crate::catalog::Reserved<E>>,
pat: PatReader,
pmt_sections: HashMap<Pid, SectionReassembler>,
crc_errors: BTreeMap<u16, u64>,
streams: HashMap<Pid, Stream<E>>,
retired_stats: BTreeMap<u16, StreamStats>,
liveness: Liveness,
pending: HashMap<Pid, Pending>,
continuity: HashMap<Pid, Continuity>,
pcr_pid: Option<Pid>,
mux_rate: super::mux_rate::Meter,
published: bool,
initialized: bool,
scratch: Vec<u8>,
framer: Framer,
sections: HashMap<u16, SectionStream<E>>,
supports_mpegts: bool,
es_descriptors: HashMap<u16, Vec<catalog::Descriptor>>,
recorded_media: HashSet<Pid>,
program_recorded: bool,
si_sections: HashMap<u16, SectionReassembler>,
si: super::si::Capture<E>,
identity_recorded: bool,
last_pts: Option<Timestamp>,
media_unwrap: PtsUnwrap,
anchor: Option<crate::clock::Anchor>,
program: Option<u16>,
}
impl<E: catalog::Catalog> Import<E> {
pub fn new(broadcast: moq_net::broadcast::Producer, reserved: crate::catalog::Reserved<E>) -> Self {
let container = hang::catalog::Container::default();
let catalog = reserved.producer();
let mut snapshot = catalog.snapshot();
let supports_mpegts = snapshot.ext.mpegts_mut().is_some();
let si = super::si::Capture::new(broadcast.clone(), catalog.clone());
Self {
broadcast,
catalog,
container,
initial_reservation: Some(reserved),
pat: PatReader::default(),
pmt_sections: HashMap::new(),
crc_errors: BTreeMap::new(),
streams: HashMap::new(),
retired_stats: BTreeMap::new(),
liveness: Liveness::default(),
pending: HashMap::new(),
continuity: HashMap::new(),
pcr_pid: None,
mux_rate: Default::default(),
published: false,
initialized: false,
scratch: Vec::new(),
framer: Framer::default(),
sections: HashMap::new(),
supports_mpegts,
es_descriptors: HashMap::new(),
recorded_media: HashSet::new(),
program_recorded: false,
si_sections: HashMap::new(),
si,
identity_recorded: false,
last_pts: None,
media_unwrap: PtsUnwrap::default(),
anchor: None,
program: None,
}
}
pub fn with_program(mut self, program: u16) -> Self {
self.program = Some(program);
self.si.select(program);
self
}
pub fn live(mut self) -> Self {
self.anchor = Some(crate::clock::Anchor::new(self.catalog.clock()));
self
}
pub fn with_container(mut self, container: hang::catalog::Container) -> Self {
self.container = container;
self
}
fn reserve(&self) -> crate::catalog::Reserved<E> {
self.catalog.reserve()
}
fn video_hint(&self) -> crate::catalog::VideoHint {
crate::catalog::VideoHint {
container: self.container.clone(),
..Default::default()
}
}
pub fn decode(&mut self, data: &[u8]) -> anyhow::Result<()> {
self.scratch.extend_from_slice(data);
let mut off = 0;
while let Some(at) = self.framer.next(&self.scratch, &mut off) {
let pkt: [u8; TsPacket::SIZE] = self.scratch[at..at + TsPacket::SIZE].try_into().unwrap();
let pid = (((pkt[1] & 0x1f) as u16) << 8) | pkt[2] as u16;
if self.supports_mpegts {
self.mux_rate.packet();
}
let continuation = Pid::new(pid)
.ok()
.filter(|pid| self.streams.contains_key(pid) || self.pcr_pid == Some(*pid))
.map(|pid| self.continuity.entry(pid).or_default().observe(&pkt));
if matches!(continuation, Some(Continuation::Duplicate)) {
continue;
}
if let Ok(pid) = Pid::new(pid)
&& (self.streams.contains_key(&pid) || self.pcr_pid == Some(pid))
&& pkt[1] & 0x80 == 0
&& !adaptation_valid(&pkt)
{
if matches!(continuation, Some(Continuation::Broken)) {
self.salvage(pid)?;
}
self.damage(pid, &anyhow::anyhow!("malformed TS adaptation field"))?;
continue;
}
if self.pcr_pid.is_some_and(|p| p.as_u16() == pid) && pkt[1] & 0x80 == 0 {
if discontinuity_indicator(&pkt) {
self.timebase_break()?;
} else if let Some(pcr) = pcr(&pkt) {
self.liveness.pcr(pcr);
if self.supports_mpegts && self.mux_rate.pcr(pcr) {
self.record_mux_rate()?;
}
}
}
if let Some(section) = self.sections.get_mut(&pid) {
let units = section.packet(&pkt, self.last_pts)?;
self.published |= units > 0;
self.liveness.delivered(pid, units);
continue;
}
if self.supports_mpegts && catalog::SI_PIDS.contains(&pid) {
self.si_section(pid, &pkt)?;
continue;
}
if let Ok(pid) = Pid::new(pid)
&& self.streams.contains_key(&pid)
{
match continuation.expect("media PID continuity was classified") {
Continuation::Duplicate => continue,
Continuation::Corrupt => {
self.damage(pid, &anyhow::anyhow!("transport error indicator"))?;
continue;
}
Continuation::Broken => {
self.broken(pid)?;
}
Continuation::Contiguous => {}
}
}
let Ok(pid) = Pid::new(pid) else {
continue;
};
if pid.as_u16() == Pid::PAT {
self.pat_packet(&pkt)?;
} else if self.pmt_sections.contains_key(&pid) {
self.pmt_packet(pid, &pkt)?;
} else {
match self.streams.get(&pid) {
None | Some(Stream::Ignored) => {}
Some(_) => self.pes_packet(pid, &pkt)?,
}
}
}
self.scratch.drain(..off);
self.si.flush(self.last_pts.unwrap_or(Timestamp::ZERO), false)?;
for stream in self.streams.values_mut() {
stream.tick()?;
}
Ok(())
}
fn pat_packet(&mut self, pkt: &[u8; TsPacket::SIZE]) -> anyhow::Result<()> {
let crc_error = self.crc_errors.entry(Pid::PAT).or_default();
if let Some(pat) = self.pat.push(pkt, crc_error) {
self.handle_pat(&pat)?;
}
Ok(())
}
fn pmt_packet(&mut self, pid: Pid, pkt: &[u8; TsPacket::SIZE]) -> anyhow::Result<()> {
let mut sections = Vec::new();
self.pmt_sections.entry(pid).or_default().push(pkt, &mut sections);
for section in sections {
if section.first() != Some(&psi::PMT_TABLE_ID) {
continue;
}
if !psi::crc_ok(§ion) {
self.crc_error(pid);
continue;
}
if let Some(pmt) = Pmt::parse(§ion) {
self.handle_pmt(pmt)?;
}
}
Ok(())
}
fn crc_error(&mut self, pid: Pid) {
*self.crc_errors.entry(pid.as_u16()).or_default() += 1;
tracing::warn!(pid = pid.as_u16(), "dropped a PSI section with a bad CRC");
}
fn handle_pmt(&mut self, pmt: Pmt) -> anyhow::Result<()> {
if self.program.is_some_and(|program| program != pmt.program_number) {
return Ok(());
}
if self.pcr_pid != pmt.pcr_pid {
self.pcr_pid = pmt.pcr_pid;
self.liveness.discontinuity();
if self.mux_rate.discontinuity() {
self.record_mux_rate()?;
}
}
let cuei = pmt
.program_info
.iter()
.any(|d| d.tag == 0x05 && d.data.len() >= 4 && &d.data[0..4] == b"CUEI");
if self.supports_mpegts && !self.program_recorded && !pmt.program_info.is_empty() {
if let Some(mpegts) = self.catalog.modify()?.ext.mpegts_mut() {
mpegts.program_descriptors = pmt.program_info.clone();
}
self.program_recorded = true;
}
for es in &pmt.streams {
if self.supports_mpegts {
self.es_descriptors.insert(es.pid.as_u16(), es.descriptors.clone());
}
if es.stream_type == 0x05 || (cuei && es.stream_type == StreamType::Dts8ChannelLosslessAudio as u8) {
self.ensure_section(es.pid, es.stream_type, &es.descriptors)?;
} else {
self.ensure_stream(es.pid, es.stream_type, &es.descriptors)?;
}
}
self.initial_reservation = None;
Ok(())
}
fn pes_packet(&mut self, pid: Pid, pkt: &[u8; TsPacket::SIZE]) -> anyhow::Result<()> {
let payload = match payload(pkt) {
Payload::None => return Ok(()),
Payload::Bytes(payload) => payload,
Payload::Malformed => {
return self.damage(pid, &anyhow::anyhow!("malformed TS adaptation field"));
}
};
if pkt[1] & 0x40 != 0 {
match PesStart::parse(payload) {
Ok(pes) => self.handle_pes_start(pid, pes),
Err(err) => {
self.flush(pid)?;
self.damage(pid, &err)
}
}
} else {
self.handle_pes_continuation(pid, payload)
}
}
fn ensure_stream(&mut self, pid: Pid, stream_type: u8, descriptors: &[catalog::Descriptor]) -> anyhow::Result<()> {
if let Some(mut section) = self.sections.remove(&pid.as_u16()) {
section.finish()?;
self.pending.remove(&pid);
}
if self.streams.contains_key(&pid) {
return Ok(());
}
let stream = match StreamType::from_u8(stream_type).ok() {
Some(StreamType::H264) => {
let track = self
.broadcast
.unique_track(".avc3", self.catalog.track_info(hang::catalog::PRIORITY.video))?;
Stream::H264 {
split: h264::Split::new(),
import: Box::new(h264::Import::new(track, self.reserve(), self.video_hint())?),
unwrap: PtsUnwrap::default(),
reanchor: Reanchor::default(),
}
}
Some(StreamType::H265) => {
let track = self
.broadcast
.unique_track(".hev1", self.catalog.track_info(hang::catalog::PRIORITY.video))?;
Stream::H265 {
split: h265::Split::new(),
import: Box::new(h265::Import::new(track, self.reserve(), self.video_hint())?),
unwrap: PtsUnwrap::default(),
reanchor: Reanchor::default(),
}
}
Some(StreamType::AdtsAac) => Stream::Aac(Box::new(AacStream {
import: None,
broadcast: self.broadcast.clone(),
reserved: Some(self.reserve()),
container: self.container.clone(),
unwrap: PtsUnwrap::default(),
reanchor: Reanchor::default(),
tail: Vec::new(),
tail_pts: None,
resync: Resync::new(pid.as_u16(), ".aac"),
burst: std::time::Duration::ZERO,
})),
Some(StreamType::Mpeg1Audio | StreamType::Mpeg2HalvedSampleRateAudio) => {
self.legacy_stream(pid, &mp2::DESCRIPTOR)
}
Some(StreamType::DolbyDigitalUpToSixChannelAudio) => self.legacy_stream(pid, &ac3::DESCRIPTOR),
Some(StreamType::DolbyDigitalPlusUpTo16ChannelAudioForAtsc) => self.legacy_stream(pid, &eac3::DESCRIPTOR),
Some(StreamType::Mpeg2PacketizedData) if registration_format(descriptors) == Some(*b"Opus") => {
let config = opus_config(descriptors)?;
let track = self
.broadcast
.unique_track(".opus", self.catalog.track_info(hang::catalog::PRIORITY.audio))?;
let mut config: hang::catalog::AudioConfig = config.into();
config.container = self.container.clone();
Stream::Opus(Box::new(OpusStream {
import: opus::Import::new(track, self.reserve(), config)?,
unwrap: PtsUnwrap::default(),
reanchor: Reanchor::default(),
}))
}
Some(StreamType::Mpeg1Video | StreamType::Mpeg2Video) => Stream::Clock,
_ => {
if self.supports_mpegts && stream_type != StreamType::Dts8ChannelLosslessAudio as u8 {
let descriptors = descriptors.to_vec();
match VerbatimStream::new(
self.broadcast.clone(),
self.catalog.clone(),
pid.as_u16(),
stream_type,
descriptors,
) {
Ok(stream) => Stream::Verbatim(Box::new(stream)),
Err(err) => {
tracing::warn!(?err, pid = pid.as_u16(), "failed to create verbatim stream, dropping");
Stream::Ignored
}
}
} else {
tracing::warn!(stream_type, pid = pid.as_u16(), "unsupported TS stream type, dropping");
Stream::Ignored
}
}
};
if !matches!(stream, Stream::Ignored | Stream::Clock) {
self.initialized = true;
}
if !matches!(stream, Stream::Ignored) {
self.liveness.register(pid.as_u16());
}
self.streams.insert(pid, stream);
self.continuity.entry(pid).or_default();
Ok(())
}
fn legacy_stream(&self, pid: Pid, descriptor: &'static legacy::Descriptor) -> Stream<E> {
Stream::Legacy(Box::new(LegacyStream {
descriptor,
import: None,
broadcast: self.broadcast.clone(),
reserved: Some(self.reserve()),
container: self.container.clone(),
unwrap: PtsUnwrap::default(),
tail: Vec::new(),
tail_pts: None,
resync: Resync::new(pid.as_u16(), descriptor.track_suffix),
reanchor: Reanchor::default(),
}))
}
fn ensure_section(&mut self, pid: Pid, stream_type: u8, descriptors: &[catalog::Descriptor]) -> anyhow::Result<()> {
if self.sections.contains_key(&pid.as_u16()) {
return Ok(());
}
self.pending.remove(&pid);
self.continuity.remove(&pid);
if !self.supports_mpegts {
let previous = self.retire_stream(pid);
self.streams.insert(pid, Stream::Ignored);
if !matches!(previous, Some(Stream::Ignored)) {
tracing::warn!(
pid = pid.as_u16(),
"private section stream detected without `mpegts` catalog support; dropping"
);
}
return Ok(());
}
self.retire_stream(pid);
let descriptors = descriptors.to_vec();
let stream = SectionStream::new(
self.broadcast.clone(),
self.catalog.clone(),
pid.as_u16(),
stream_type,
descriptors,
)?;
self.sections.insert(pid.as_u16(), stream);
self.liveness.register(pid.as_u16());
self.initialized = true;
tracing::debug!(
pid = pid.as_u16(),
stream_type,
"private section stream detected; reassembling its sections"
);
Ok(())
}
fn retire_stream(&mut self, pid: Pid) -> Option<Stream<E>> {
let stream = self.streams.remove(&pid)?;
if let Some(current) = stream.stats() {
self.retired_stats
.entry(pid.as_u16())
.and_modify(|retired| retired.merge(¤t))
.or_insert(current);
}
Some(stream)
}
fn handle_pes_start(&mut self, pid: Pid, pes: PesStart) -> anyhow::Result<()> {
if self.pending.contains_key(&pid) {
self.flush(pid)?;
}
let Some(stream) = self.streams.get(&pid) else {
return Ok(());
};
let is_video = matches!(stream, Stream::H264 { .. } | Stream::H265 { .. } | Stream::Clock);
let is_clock = matches!(stream, Stream::Clock);
if is_video {
for stream in self.streams.values_mut() {
if let Stream::Aac(audio) = stream {
audio.burst = std::time::Duration::ZERO;
}
}
if pes.pts.is_some() {
let pts = unwrap_pts(&mut self.media_unwrap, pes.pts, self.anchor.as_mut())?;
let video = match self.streams.get(&pid) {
Some(Stream::H264 { reanchor, import, .. }) => {
Some((reanchor, import.floor(false), import.floor(true)))
}
Some(Stream::H265 { reanchor, import, .. }) => {
Some((reanchor, import.floor(false), import.floor(true)))
}
_ => None,
};
self.last_pts = match (pts, video) {
(Some(pts), Some((reanchor, edge, floor))) => {
let pts = reanchor.shifted(pts)?;
match floor {
Some(floor) if edge.is_some_and(|edge| pts.as_micros() < edge.as_micros()) => Some(floor),
_ => Some(pts),
}
}
(pts, _) => pts,
};
}
}
if is_clock {
self.liveness.delivered(pid.as_u16(), 1);
return Ok(());
}
let mut pending = Pending {
pts: pes.pts,
dts: pes.dts,
stream_id: pes.stream_id,
data: Vec::with_capacity(pes.data.len()),
data_len: pes.data_len,
};
pending.data.extend_from_slice(pes.data);
let complete = matches!(pes.data_len, Some(len) if pending.data.len() >= len);
self.pending.insert(pid, pending);
if complete {
self.flush(pid)?;
}
Ok(())
}
fn handle_pes_continuation(&mut self, pid: Pid, data: &[u8]) -> anyhow::Result<()> {
let Some(pending) = self.pending.get_mut(&pid) else {
return Ok(());
};
pending.data.extend_from_slice(data);
if matches!(pending.data_len, Some(len) if pending.data.len() >= len) {
self.flush(pid)?;
}
Ok(())
}
fn damage(&mut self, pid: Pid, err: &anyhow::Error) -> anyhow::Result<()> {
self.pending.remove(&pid);
match self.streams.get_mut(&pid) {
Some(Stream::Ignored) => return Ok(()),
Some(stream) => stream.lost()?,
None => {}
}
tracing::warn!(pid = pid.as_u16(), error = %err, "dropped a damaged TS unit");
Ok(())
}
fn flush(&mut self, pid: Pid) -> anyhow::Result<()> {
let Some(pending) = self.pending.remove(&pid) else {
return Ok(());
};
let batched = self
.streams
.values()
.any(|stream| matches!(stream, Stream::H264 { .. } | Stream::H265 { .. } | Stream::Clock));
let Some(stream) = self.streams.get_mut(&pid) else {
return Ok(());
};
let units = match stream.write(pending, batched, self.anchor.as_mut()) {
Ok(units) => units,
Err(err) if err.is::<Damaged>() => return self.damage(pid, &err),
Err(err) => return Err(err),
};
self.published |= units > 0;
self.liveness.delivered(pid.as_u16(), units);
self.record_media_track(pid)
}
fn broken(&mut self, pid: Pid) -> anyhow::Result<()> {
self.salvage(pid)?;
if let Some(stream) = self.streams.get_mut(&pid) {
stream.lost()?;
}
Ok(())
}
fn salvage(&mut self, pid: Pid) -> anyhow::Result<()> {
if self.streams.get(&pid).is_some_and(Stream::salvages_partial_pes) {
self.flush(pid)
} else {
self.pending.remove(&pid);
Ok(())
}
}
fn timebase_break(&mut self) -> anyhow::Result<()> {
for pid in self.streams.keys().copied().collect::<Vec<_>>() {
self.salvage(pid)?;
}
for stream in self.streams.values_mut() {
stream.desync();
stream.discontinuity(self.published)?;
}
for section in self.sections.values_mut() {
section.discontinuity(self.published)?;
}
self.media_unwrap.discontinuity();
self.last_pts = None;
self.published = false;
self.liveness.discontinuity();
if self.mux_rate.discontinuity() {
self.record_mux_rate()?;
}
tracing::debug!("MPEG-TS system time-base discontinuity");
Ok(())
}
fn record_mux_rate(&mut self) -> anyhow::Result<()> {
let rate = self.mux_rate.published();
if let Some(mpegts) = self.catalog.modify()?.ext.mpegts_mut() {
mpegts.mux_rate = rate;
}
Ok(())
}
fn record_media_track(&mut self, pid: Pid) -> anyhow::Result<()> {
if !self.supports_mpegts || self.recorded_media.contains(&pid) {
return Ok(());
}
let (name, descriptors) = {
let Some(name) = self.streams.get(&pid).and_then(|s| s.media_track_name()) else {
return Ok(());
};
(
name,
self.es_descriptors.get(&pid.as_u16()).cloned().unwrap_or_default(),
)
};
if let Some(mpegts) = self.catalog.modify()?.ext.mpegts_mut() {
let entry = mpegts
.tracks
.entry(name)
.or_insert_with(|| catalog::Track::new(pid.as_u16()));
entry.pid = pid.as_u16();
entry.descriptors = descriptors;
}
self.recorded_media.insert(pid);
Ok(())
}
pub(super) fn handle_pat(&mut self, pat: &psi::Pat) -> anyhow::Result<()> {
let programs: Vec<_> = pat.programs.iter().filter(|entry| entry.program_number != 0).collect();
let numbers = || programs.iter().map(|entry| entry.program_number).collect::<Vec<_>>();
let entry = match self.program {
Some(program) => match programs.iter().find(|entry| entry.program_number == program) {
Some(entry) => entry,
None => anyhow::bail!(
"transport stream has no program {program}; its PAT lists {}",
list_programs(&numbers())
),
},
None => match programs.as_slice() {
[] => return Ok(()),
[entry] => entry,
_ => return Err(MultipleProgramsError { programs: numbers() }.into()),
},
};
self.pmt_sections.entry(entry.pmt_pid).or_default();
self.record_program_identity(pat.transport_stream_id, entry)
}
fn record_program_identity(&mut self, transport_stream_id: u16, entry: &psi::Association) -> anyhow::Result<()> {
if !self.supports_mpegts || self.identity_recorded {
return Ok(());
}
if let Some(mpegts) = self.catalog.modify()?.ext.mpegts_mut() {
let program = mpegts.program.get_or_insert_with(Default::default);
program.transport_stream_id = transport_stream_id;
program.program_number = entry.program_number;
program.pmt_pid = entry.pmt_pid.as_u16();
}
self.identity_recorded = true;
Ok(())
}
fn si_section(&mut self, pid: u16, pkt: &[u8]) -> anyhow::Result<()> {
let mut sections = Vec::new();
self.si_sections.entry(pid).or_default().push(pkt, &mut sections);
for section in sections {
self.si.section(pid, section)?;
}
Ok(())
}
pub fn seek(&mut self, sequence: u64) -> anyhow::Result<()> {
for stream in self.streams.values_mut() {
stream.seek(sequence)?;
}
for section in self.sections.values_mut() {
section.seek(sequence)?;
}
Ok(())
}
pub fn finish(&mut self) -> anyhow::Result<()> {
let pids: Vec<Pid> = self.pending.keys().copied().collect();
for pid in pids {
self.flush(pid)?;
}
for (pid, stream) in &mut self.streams {
let units = stream.finish()?;
self.liveness.delivered(pid.as_u16(), units);
}
for section in self.sections.values_mut() {
section.finish()?;
}
self.si.finish(self.last_pts.unwrap_or(Timestamp::ZERO))?;
Ok(())
}
pub fn stats(&self) -> Stats {
let mut streams = self.retired_stats.clone();
let routes = self
.streams
.iter()
.filter_map(|(pid, stream)| Some((pid.as_u16(), stream.stats()?)))
.chain(self.sections.keys().map(|&pid| (pid, StreamStats::new(".ts"))));
for (pid, current) in routes {
streams
.entry(pid)
.and_modify(|retired| retired.merge(¤t))
.or_insert(current);
}
for (pid, stats) in &mut streams {
(stats.units, stats.quiet) = self.liveness.stream(*pid);
}
Stats {
streams,
crc_error: self.crc_errors.values().sum(),
}
}
pub(super) fn crc_errors(&self) -> &BTreeMap<u16, u64> {
&self.crc_errors
}
pub fn abort(mut self, err: moq_net::Error) {
for stream in std::mem::take(&mut self.streams).into_values() {
stream.abort(err.clone());
}
for section in std::mem::take(&mut self.sections).into_values() {
section.abort(err.clone());
}
let si = std::mem::replace(
&mut self.si,
super::si::Capture::new(self.broadcast.clone(), self.catalog.clone()),
);
si.abort(err);
}
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("transport stream carries {} programs ({})", .programs.len(), list_programs(.programs))]
pub struct MultipleProgramsError {
pub programs: Vec<u16>,
}
fn list_programs(programs: &[u16]) -> String {
programs.iter().map(u16::to_string).collect::<Vec<_>>().join(", ")
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct Stats {
pub streams: BTreeMap<u16, StreamStats>,
pub crc_error: u64,
}
impl Stats {
pub fn is_empty(&self) -> bool {
self.streams.is_empty() && self.crc_error == 0
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct StreamStats {
pub track: &'static str,
pub units: u64,
pub quiet: Option<std::time::Duration>,
pub resyncs: u64,
pub discarded: u64,
pub unconfirmed: u64,
}
impl StreamStats {
fn new(track: &'static str) -> Self {
Self {
track,
..Default::default()
}
}
fn merge(&mut self, current: &Self) {
self.track = current.track;
self.resyncs += current.resyncs;
self.discarded += current.discarded;
self.unconfirmed += current.unconfirmed;
}
}
struct Pending {
pts: Option<u64>,
dts: Option<u64>,
stream_id: u8,
data: Vec<u8>,
data_len: Option<usize>,
}
impl Pending {
fn empty() -> Self {
Self {
pts: None,
dts: None,
stream_id: 0,
data: Vec::new(),
data_len: None,
}
}
}
fn register_verbatim<E: catalog::Catalog>(
broadcast: &mut moq_net::broadcast::Producer,
catalog: &mut crate::catalog::Producer<E>,
pid: u16,
stream_type: u8,
framing: catalog::Framing,
descriptors: Vec<catalog::Descriptor>,
) -> anyhow::Result<crate::container::Producer<crate::catalog::hang::Container>> {
let track = broadcast.unique_track(".ts", catalog.track_info(hang::catalog::PRIORITY.text))?;
let name = track.name().to_string();
let media = catalog.media_raw(
track,
crate::catalog::hang::Container::Legacy(crate::container::Kind::Data),
)?;
let mut guard = catalog.modify()?;
let Some(mpegts) = guard.ext.mpegts_mut() else {
anyhow::bail!("catalog extension no longer carries an mpegts section");
};
mpegts.tracks.insert(
name,
catalog::Track {
pid,
descriptors,
verbatim: Some(catalog::Verbatim::new(stream_type, framing)),
},
);
drop(guard);
Ok(media)
}
fn unregister_verbatim<E: catalog::Catalog>(catalog: &mut crate::catalog::Producer<E>, name: &str) {
let Ok(mut catalog) = catalog.modify() else {
return;
};
if let Some(mpegts) = catalog.ext.mpegts_mut() {
mpegts.tracks.remove(name);
}
}
struct VerbatimEntry<E: catalog::Catalog> {
catalog: crate::catalog::Producer<E>,
name: String,
}
impl<E: catalog::Catalog> Drop for VerbatimEntry<E> {
fn drop(&mut self) {
unregister_verbatim(&mut self.catalog, &self.name);
}
}
struct SectionStream<E: catalog::Catalog> {
track: crate::container::Producer<crate::catalog::hang::Container>,
_entry: VerbatimEntry<E>,
reassembler: SectionReassembler,
}
impl<E: catalog::Catalog> SectionStream<E> {
fn new(
mut broadcast: moq_net::broadcast::Producer,
mut catalog: crate::catalog::Producer<E>,
pid: u16,
stream_type: u8,
descriptors: Vec<catalog::Descriptor>,
) -> anyhow::Result<Self> {
let track = register_verbatim(
&mut broadcast,
&mut catalog,
pid,
stream_type,
catalog::Framing::Section,
descriptors,
)?;
let entry = VerbatimEntry {
name: track.name().to_string(),
catalog,
};
Ok(Self {
track,
_entry: entry,
reassembler: SectionReassembler::default(),
})
}
fn packet(&mut self, pkt: &[u8], pts: Option<Timestamp>) -> anyhow::Result<u64> {
let mut sections = Vec::new();
self.reassembler.push(pkt, &mut sections);
let published = sections.len() as u64;
for section in sections {
self.emit(section, pts)?;
}
Ok(published)
}
fn emit(&mut self, section: Vec<u8>, pts: Option<Timestamp>) -> anyhow::Result<()> {
let timestamp = pts.max(self.track.live_edge()).unwrap_or(Timestamp::ZERO);
let frame = crate::container::Frame {
timestamp,
duration: None,
payload: bytes::Bytes::from(section),
keyframe: true,
};
self.track.write(frame)?;
self.track.cut(None)?;
Ok(())
}
fn discontinuity(&mut self, published: bool) -> anyhow::Result<()> {
self.reassembler = SectionReassembler::default();
if published {
self.track.discontinuity()?;
}
Ok(())
}
fn seek(&mut self, sequence: u64) -> anyhow::Result<()> {
self.track.seek(sequence)?;
Ok(())
}
fn finish(&mut self) -> anyhow::Result<()> {
self.track.finish()?;
Ok(())
}
fn abort(self, err: moq_net::Error) {
self.track.abort(err);
}
}
struct VerbatimStream<E: catalog::Catalog> {
track: crate::container::Producer<crate::catalog::hang::Container>,
entry: VerbatimEntry<E>,
unwrap: PtsUnwrap,
reanchor: Reanchor,
stream_id_recorded: bool,
}
impl<E: catalog::Catalog> VerbatimStream<E> {
fn new(
mut broadcast: moq_net::broadcast::Producer,
mut catalog: crate::catalog::Producer<E>,
pid: u16,
stream_type: u8,
descriptors: Vec<catalog::Descriptor>,
) -> anyhow::Result<Self> {
let track = register_verbatim(
&mut broadcast,
&mut catalog,
pid,
stream_type,
catalog::Framing::Pes,
descriptors,
)?;
let entry = VerbatimEntry {
name: track.name().to_string(),
catalog,
};
Ok(Self {
track,
entry,
unwrap: PtsUnwrap::default(),
reanchor: Reanchor::default(),
stream_id_recorded: false,
})
}
fn write(&mut self, pending: Pending, anchor: Option<&mut crate::clock::Anchor>) -> anyhow::Result<u64> {
if !self.stream_id_recorded {
let name = self.track.name().to_string();
if let Some(mpegts) = self.entry.catalog.modify()?.ext.mpegts_mut()
&& let Some(verbatim) = mpegts.tracks.get_mut(&name).and_then(|t| t.verbatim.as_mut())
{
verbatim.stream_id = Some(pending.stream_id);
}
self.stream_id_recorded = true;
}
let edge = self.track.live_edge();
let pts = match unwrap_pts(&mut self.unwrap, pending.pts, anchor)? {
Some(pts) => self.reanchor.apply(pts, edge)?,
None => edge.unwrap_or(Timestamp::ZERO),
};
let frame = crate::container::Frame {
timestamp: pts,
duration: None,
payload: bytes::Bytes::from(pending.data),
keyframe: true,
};
self.track.write(frame)?;
self.track.cut(None)?;
Ok(1)
}
fn discontinuity(&mut self, published: bool) -> anyhow::Result<()> {
self.unwrap.discontinuity();
self.reanchor.discontinuity();
if published {
self.track.discontinuity()?;
}
Ok(())
}
fn seek(&mut self, sequence: u64) -> anyhow::Result<()> {
self.track.seek(sequence)?;
Ok(())
}
fn finish(&mut self) -> anyhow::Result<()> {
self.track.finish()?;
Ok(())
}
fn abort(self, err: moq_net::Error) {
self.track.abort(err);
}
}
#[derive(Default)]
struct Continuity {
last_cc: Option<u8>,
last_pkt: Option<[u8; 188]>,
}
enum Continuation {
Duplicate,
Contiguous,
Broken,
Corrupt,
}
fn discontinuity_indicator(pkt: &[u8; 188]) -> bool {
pkt[3] & 0x20 != 0 && pkt[4] > 0 && pkt[5] & 0x80 != 0
}
fn pcr(pkt: &[u8; 188]) -> Option<u64> {
if pkt[3] & 0x20 == 0 || pkt[4] < 7 || pkt[5] & 0x10 == 0 {
return None;
}
let base = (u64::from(pkt[6]) << 25)
| (u64::from(pkt[7]) << 17)
| (u64::from(pkt[8]) << 9)
| (u64::from(pkt[9]) << 1)
| (u64::from(pkt[10]) >> 7);
let ext = (u64::from(pkt[10] & 0x01) << 8) | u64::from(pkt[11]);
Some(base * 300 + ext)
}
fn is_duplicate(last: &[u8; 188], pkt: &[u8; 188]) -> bool {
if last == pkt {
return true;
}
let afc = (last[3] >> 4) & 0x3;
if afc & 0x2 == 0 || last[..6] != pkt[..6] {
return false;
}
let af_len = last[4] as usize;
let flags = last[5];
let clock_len = usize::from(flags & 0x10 != 0) * 6 + usize::from(flags & 0x08 != 0) * 6;
let clock_end = 6 + clock_len;
let adaptation_end = 5 + af_len;
if clock_len == 0 || clock_end > adaptation_end || adaptation_end > last.len() {
return false;
}
last[clock_end..] == pkt[clock_end..]
}
impl Continuity {
fn observe(&mut self, pkt: &[u8; 188]) -> Continuation {
if pkt[1] & 0x80 != 0 {
self.last_cc = None;
self.last_pkt = None;
return Continuation::Corrupt;
}
let afc = (pkt[3] >> 4) & 0x3;
let has_payload = afc & 0x1 != 0;
let discontinuity = discontinuity_indicator(pkt);
if !has_payload {
if discontinuity {
self.last_cc = None;
self.last_pkt = None;
return Continuation::Broken;
}
return Continuation::Contiguous;
}
if self.last_pkt.as_ref().is_some_and(|last| is_duplicate(last, pkt)) {
return Continuation::Duplicate;
}
self.last_pkt = Some(*pkt);
let cc = pkt[3] & 0x0f;
let cc_gap = matches!(self.last_cc, Some(last) if cc != (last + 1) & 0x0f);
self.last_cc = Some(cc);
if discontinuity || cc_gap {
Continuation::Broken
} else {
Continuation::Contiguous
}
}
}
#[derive(Default)]
pub(super) struct Liveness {
pcr: Option<u64>,
elapsed: Option<u64>,
streams: HashMap<u16, (u64, u64)>,
}
impl Liveness {
pub(super) fn pcr(&mut self, pcr: u64) {
self.step(pcr, super::mux_rate::MAX_INTERVAL);
}
pub(super) fn written_pcr(&mut self, pcr: u64) {
self.step(pcr, super::mux_rate::PCR_WRAP);
}
fn step(&mut self, pcr: u64, max: u64) {
let elapsed = self.elapsed.get_or_insert(0);
if let Some(last) = self.pcr.replace(pcr) {
let step = (pcr + super::mux_rate::PCR_WRAP - last) % super::mux_rate::PCR_WRAP;
if step <= max {
*elapsed += step;
}
}
}
pub(super) fn discontinuity(&mut self) {
self.pcr = None;
}
pub(super) fn register(&mut self, pid: u16) {
let now = self.elapsed.unwrap_or(0);
self.streams.entry(pid).or_insert((0, now));
}
pub(super) fn delivered(&mut self, pid: u16, units: u64) {
if units == 0 {
return;
}
let now = self.elapsed.unwrap_or(0);
if let Some((count, last)) = self.streams.get_mut(&pid) {
*count += units;
*last = now;
}
}
pub(super) fn stream(&self, pid: u16) -> (u64, Option<std::time::Duration>) {
let Some(&(units, last)) = self.streams.get(&pid) else {
return (0, None);
};
let quiet = self
.elapsed
.map(|now| std::time::Duration::from_nanos((now - last) * 1_000 / 27));
(units, quiet)
}
}
#[derive(Default)]
pub(super) struct Framer {
synced: bool,
}
impl Framer {
pub(super) fn next(&mut self, buf: &[u8], off: &mut usize) -> Option<usize> {
while *off + TsPacket::SIZE <= buf.len() {
if self.synced && buf[*off] == 0x47 {
let at = *off;
*off += TsPacket::SIZE;
return Some(at);
}
self.synced = false;
loop {
let Some(rel) = memchr::memchr(0x47, &buf[*off..]) else {
*off = buf.len();
break;
};
*off += rel;
match buf.get(*off + TsPacket::SIZE) {
Some(&0x47) => {
self.synced = true;
break;
}
Some(_) => *off += 1,
None => break,
}
}
if !self.synced {
return None;
}
}
None
}
}
#[derive(Default)]
pub(super) struct PatReader {
sections: SectionReassembler,
table: psi::PatAssembler,
}
impl PatReader {
pub(super) fn push(&mut self, pkt: &[u8; TsPacket::SIZE], crc_error: &mut u64) -> Option<psi::Pat> {
let mut sections = Vec::new();
self.sections.push(pkt, &mut sections);
let mut pat = None;
for section in sections {
if section.first() != Some(&psi::PAT_TABLE_ID) {
continue;
}
if !psi::crc_ok(§ion) {
*crc_error += 1;
tracing::warn!(pid = Pid::PAT, "dropped a PSI section with a bad CRC");
continue;
}
pat = self.table.section(§ion).or(pat);
}
pat
}
}
fn adaptation_valid(pkt: &[u8; TsPacket::SIZE]) -> bool {
if pkt[3] & 0x20 == 0 {
return true;
}
let Some(field) = pkt.get(5..5 + usize::from(pkt[4])) else {
return false;
};
let Some(&flags) = field.first() else {
return true;
};
let mut off =
1 + usize::from(flags & 0x10 != 0) * 6 + usize::from(flags & 0x08 != 0) * 6 + usize::from(flags & 0x04 != 0);
for flag in [0x02, 0x01] {
if flags & flag != 0 {
let Some(&len) = field.get(off) else {
return false;
};
off += 1 + usize::from(len);
}
}
off <= field.len()
}
enum Payload<'a> {
None,
Bytes(&'a [u8]),
Malformed,
}
fn payload(pkt: &[u8; TsPacket::SIZE]) -> Payload<'_> {
let afc = (pkt[3] >> 4) & 0x3;
if afc & 0x1 == 0 {
return Payload::None;
}
let off = if afc & 0x2 != 0 { 5 + pkt[4] as usize } else { 4 };
match pkt.get(off..) {
Some([]) => Payload::None,
Some(payload) => Payload::Bytes(payload),
None => Payload::Malformed,
}
}
#[derive(Default)]
struct SectionReassembler {
acc: Vec<u8>,
continuity: Continuity,
}
impl SectionReassembler {
fn push(&mut self, pkt: &[u8], out: &mut Vec<Vec<u8>>) {
let pkt: &[u8; 188] = pkt.try_into().expect("section packet must be 188 bytes");
match self.continuity.observe(pkt) {
Continuation::Duplicate => return,
Continuation::Corrupt => {
self.acc.clear();
return;
}
Continuation::Broken => self.acc.clear(),
Continuation::Contiguous => {}
}
let pusi = pkt[1] & 0x40 != 0;
let payload = match payload(pkt) {
Payload::None => return,
Payload::Bytes(payload) => payload,
Payload::Malformed => {
self.acc.clear();
return;
}
};
if pusi {
let ptr = payload[0] as usize;
if 1 + ptr > payload.len() {
self.acc.clear();
return;
}
if !self.acc.is_empty() {
self.acc.extend_from_slice(&payload[1..1 + ptr]);
self.drain(out);
}
self.acc.clear();
self.acc.extend_from_slice(&payload[1 + ptr..]);
self.drain(out);
} else if !self.acc.is_empty() {
self.acc.extend_from_slice(payload);
self.drain(out);
}
}
fn drain(&mut self, out: &mut Vec<Vec<u8>>) {
loop {
match self.acc.first() {
None => return,
Some(&0xff) => {
self.acc.clear();
return;
}
_ => {}
}
if self.acc.len() < 3 {
return;
}
let section_length = (((self.acc[1] & 0x0f) as usize) << 8) | self.acc[2] as usize;
if section_length > 4093 {
self.acc.clear();
return;
}
let full = 3 + section_length;
if self.acc.len() < full {
return;
}
out.push(self.acc.drain(..full).collect());
}
}
}
#[derive(Debug, thiserror::Error)]
#[error("{0}")]
struct Damaged(anyhow::Error);
fn unit_error(err: crate::Error) -> anyhow::Error {
let damaged = matches!(
&err,
crate::Error::Annexb(_)
| crate::Error::H264(
h264::Error::NalTooShort
| h264::Error::ForbiddenZeroBit
| h264::Error::SpsTooShort
| h264::Error::SpsParse
| h264::Error::NotInitialized
) | crate::Error::Aac(
aac::Error::ProgramConfigMissing | aac::Error::ProgramConfigTruncated | aac::Error::ProgramConfigEmpty
) | crate::Error::H265(
h265::Error::NalTooShort
| h265::Error::ForbiddenZeroBit
| h265::Error::SpsParse
| h265::Error::MissingLevelIdc
| h265::Error::NotInitialized
| h265::Error::MissingSps
)
);
if damaged {
Damaged(err.into()).into()
} else {
err.into()
}
}
enum Stream<E: catalog::Catalog = ()> {
H264 {
split: h264::Split,
import: Box<h264::Import>,
unwrap: PtsUnwrap,
reanchor: Reanchor,
},
H265 {
split: h265::Split,
import: Box<h265::Import>,
unwrap: PtsUnwrap,
reanchor: Reanchor,
},
Aac(Box<AacStream<E>>),
Opus(Box<OpusStream>),
Legacy(Box<LegacyStream<E>>),
Verbatim(Box<VerbatimStream<E>>),
Clock,
Ignored,
}
impl<E: catalog::Catalog> Stream<E> {
fn write(
&mut self,
pending: Pending,
batched: bool,
anchor: Option<&mut crate::clock::Anchor>,
) -> anyhow::Result<u64> {
match self {
Stream::H264 {
split,
import,
unwrap,
reanchor,
} => {
let reorder = reorder_delay(pending.pts, pending.dts);
let pts = unwrap_pts(unwrap, pending.pts, anchor)?;
let params = split.params();
let published = (|| {
let mut frames = split.decode(&pending.data, pts).map_err(unit_error)?;
frames.extend(split.flush(pts).map_err(unit_error)?);
let mut published = 0;
for mut frame in frames {
frame.timestamp = reanchor.apply(frame.timestamp, import.floor(frame.keyframe))?;
published += u64::from(skip_missing_keyframe(import.decode([frame]))?);
}
anyhow::Ok(published)
})();
if published.as_ref().is_err_and(|err| err.is::<Damaged>()) {
split.restore(params);
}
let published = published?;
if let Some(reorder) = reorder {
import.observe_reorder(reorder)?;
}
Ok(published)
}
Stream::H265 {
split,
import,
unwrap,
reanchor,
} => {
let reorder = reorder_delay(pending.pts, pending.dts);
let pts = unwrap_pts(unwrap, pending.pts, anchor)?;
let params = split.params();
let published = (|| {
let mut frames = split.decode(&pending.data, pts).map_err(unit_error)?;
frames.extend(split.flush(pts).map_err(unit_error)?);
let mut published = 0;
for mut frame in frames {
frame.timestamp = reanchor.apply(frame.timestamp, import.floor(frame.keyframe))?;
published += u64::from(skip_missing_keyframe(import.decode([frame]))?);
}
anyhow::Ok(published)
})();
if published.as_ref().is_err_and(|err| err.is::<Damaged>()) {
split.restore(params);
}
let published = published?;
if let Some(reorder) = reorder {
import.observe_reorder(reorder)?;
}
Ok(published)
}
Stream::Aac(stream) => stream.write(pending, batched, anchor),
Stream::Opus(stream) => stream.write(pending, anchor),
Stream::Legacy(stream) => stream.write(pending, anchor),
Stream::Verbatim(stream) => stream.write(pending, anchor),
Stream::Clock | Stream::Ignored => Ok(0),
}
}
fn tick(&mut self) -> anyhow::Result<()> {
match self {
Stream::H264 { import, .. } => import.tick()?,
Stream::H265 { import, .. } => import.tick()?,
Stream::Aac(_)
| Stream::Opus(_)
| Stream::Legacy(_)
| Stream::Verbatim(_)
| Stream::Clock
| Stream::Ignored => {}
}
Ok(())
}
fn salvages_partial_pes(&self) -> bool {
match self {
Stream::Aac(_) | Stream::Legacy(_) => true,
Stream::Opus(_) => false,
Stream::H264 { .. } | Stream::H265 { .. } | Stream::Verbatim(_) => false,
Stream::Clock | Stream::Ignored => false,
}
}
fn desync(&mut self) {
match self {
Stream::H264 { split, .. } => split.reset(),
Stream::H265 { split, .. } => split.reset(),
Stream::Aac(stream) => stream.desync(),
Stream::Legacy(stream) => stream.desync(),
Stream::Opus(_) | Stream::Verbatim(_) | Stream::Clock | Stream::Ignored => {}
}
}
fn lost(&mut self) -> anyhow::Result<()> {
self.desync();
match self {
Stream::H264 { import, .. } => import.cut(None)?,
Stream::H265 { import, .. } => import.cut(None)?,
_ => {}
}
Ok(())
}
fn discontinuity(&mut self, published: bool) -> anyhow::Result<()> {
match self {
Stream::H264 {
import,
unwrap,
reanchor,
..
} => {
unwrap.discontinuity();
reanchor.discontinuity();
if published {
import.discontinuity()?;
}
Ok(())
}
Stream::H265 {
import,
unwrap,
reanchor,
..
} => {
unwrap.discontinuity();
reanchor.discontinuity();
if published {
import.discontinuity()?;
}
Ok(())
}
Stream::Aac(stream) => stream.discontinuity(published),
Stream::Opus(stream) => stream.discontinuity(published),
Stream::Legacy(stream) => stream.discontinuity(published),
Stream::Verbatim(stream) => stream.discontinuity(published),
Stream::Clock | Stream::Ignored => Ok(()),
}
}
fn seek(&mut self, sequence: u64) -> anyhow::Result<()> {
match self {
Stream::H264 { split, import, .. } => {
split.reset();
Ok(import.seek(sequence)?)
}
Stream::H265 { split, import, .. } => {
split.reset();
Ok(import.seek(sequence)?)
}
Stream::Aac(stream) => stream.seek(sequence),
Stream::Opus(stream) => stream.seek(sequence),
Stream::Legacy(stream) => stream.seek(sequence),
Stream::Verbatim(stream) => stream.seek(sequence),
Stream::Clock | Stream::Ignored => Ok(()),
}
}
fn finish(&mut self) -> anyhow::Result<u64> {
match self {
Stream::Aac(stream) => return stream.finish(),
Stream::Legacy(stream) => return stream.finish(),
Stream::H264 { import, .. } => import.finish()?,
Stream::H265 { import, .. } => import.finish()?,
Stream::Opus(stream) => stream.finish()?,
Stream::Verbatim(stream) => stream.finish()?,
Stream::Clock | Stream::Ignored => {}
}
Ok(0)
}
fn abort(self, err: moq_net::Error) {
match self {
Stream::H264 { import, .. } => import.abort(err),
Stream::H265 { import, .. } => import.abort(err),
Stream::Aac(stream) => stream.abort(err),
Stream::Opus(stream) => stream.abort(err),
Stream::Legacy(stream) => stream.abort(err),
Stream::Verbatim(stream) => stream.abort(err),
Stream::Clock | Stream::Ignored => {}
}
}
fn stats(&self) -> Option<StreamStats> {
Some(match self {
Stream::Aac(stream) => stream.resync.stats(),
Stream::Legacy(stream) => stream.resync.stats(),
Stream::H264 { .. } => StreamStats::new(".avc3"),
Stream::H265 { .. } => StreamStats::new(".hev1"),
Stream::Opus(_) => StreamStats::new(".opus"),
Stream::Verbatim(_) => StreamStats::new(".ts"),
Stream::Clock => StreamStats::new(""),
Stream::Ignored => return None,
})
}
fn media_track_name(&self) -> Option<String> {
match self {
Stream::H264 { import, .. } => Some(import.name().to_string()),
Stream::H265 { import, .. } => Some(import.name().to_string()),
Stream::Aac(stream) => stream.import.as_ref().map(|i| i.name().to_string()),
Stream::Opus(stream) => Some(stream.import.name().to_string()),
Stream::Legacy(stream) => stream.import.as_ref().map(|i| i.name().to_string()),
Stream::Verbatim(_) | Stream::Clock | Stream::Ignored => None,
}
}
}
struct Resync {
pid: u16,
discarded: usize,
unconfirmed: bool,
draining: bool,
stats: StreamStats,
}
impl Resync {
fn new(pid: u16, track: &'static str) -> Self {
Self {
pid,
discarded: 0,
unconfirmed: true,
draining: false,
stats: StreamStats::new(track),
}
}
const BUDGET: usize = 64 * 1024;
fn recover(&mut self, data: &[u8], offset: usize, codec: &SyncWord) -> Recover {
self.unconfirmed = true;
if let Some(rel) = memchr::memchr(codec.sync_byte, &data[offset + 1..]) {
let found = offset + 1 + rel;
self.discarded += found - offset;
return Recover::At(found);
}
let keep = data.len().saturating_sub(codec.min_header_len - 1).max(offset);
self.discarded += keep - offset;
Recover::Carry(keep)
}
fn exhausted(&self) -> bool {
self.discarded > Self::BUDGET
}
fn unconfirmed(&self) -> bool {
self.unconfirmed
}
fn needs_confirmation(&self, in_tail: bool) -> bool {
!self.draining && (self.unconfirmed || in_tail)
}
fn published(&mut self, unvouched: bool) {
if self.discarded > 0 {
self.stats.discarded += self.discarded as u64;
if !unvouched {
self.stats.resyncs += 1;
tracing::warn!(
pid = self.pid,
track = self.stats.track,
discarded = self.discarded,
resyncs = self.stats.resyncs,
"audio stream lost frame sync and resynced"
);
}
}
self.stats.unconfirmed += u64::from(unvouched);
self.discarded = 0;
self.unconfirmed = false;
}
fn stats(&self) -> StreamStats {
let mut stats = self.stats.clone();
stats.discarded += self.discarded as u64;
stats
}
fn refund(&mut self, discarded: usize) {
self.discarded = discarded;
}
fn desynced(&mut self) {
self.stats.discarded += self.discarded as u64;
self.unconfirmed = true;
self.discarded = 0;
}
fn drain(&mut self) {
self.unconfirmed = false;
self.draining = true;
}
}
enum Recover {
At(usize),
Carry(usize),
}
struct Fallback {
offset: usize,
discarded: usize,
pts: Option<Timestamp>,
in_tail: bool,
}
struct SyncWord {
min_header_len: usize,
sync_byte: u8,
}
impl SyncWord {
const ADTS: Self = Self {
min_header_len: adts::MIN_HEADER_LEN,
sync_byte: 0xFF,
};
}
impl From<&legacy::Descriptor> for SyncWord {
fn from(descriptor: &legacy::Descriptor) -> Self {
Self {
min_header_len: descriptor.min_header_len,
sync_byte: descriptor.sync_byte,
}
}
}
struct AacStream<E: CatalogExt = ()> {
import: Option<aac::Import>,
broadcast: moq_net::broadcast::Producer,
reserved: Option<crate::catalog::Reserved<E>>,
container: hang::catalog::Container,
unwrap: PtsUnwrap,
reanchor: Reanchor,
tail: Vec<u8>,
tail_pts: Option<Timestamp>,
resync: Resync,
burst: std::time::Duration,
}
impl<E: CatalogExt> AacStream<E> {
fn write(
&mut self,
pending: Pending,
batched: bool,
anchor: Option<&mut crate::clock::Anchor>,
) -> anyhow::Result<u64> {
let pes_base = unwrap_pts(&mut self.unwrap, pending.pts, anchor)?;
let carried = self.tail.len();
let joined;
let data: &[u8] = if carried == 0 {
&pending.data
} else {
let mut j = std::mem::take(&mut self.tail);
j.extend_from_slice(&pending.data);
joined = j;
&joined
};
let mut pts = if carried > 0 {
self.tail_pts.take()
} else {
self.reanchor(pes_base)?
};
let mut in_tail = carried > 0;
let mut burst = std::time::Duration::ZERO;
let mut published = 0;
let mut offset = 0;
let mut fallback = None;
while offset + adts::MIN_HEADER_LEN <= data.len() {
if in_tail && offset >= carried {
pts = self.reanchor(pes_base)?;
in_tail = false;
}
let confirm = self.resync.needs_confirmation(in_tail);
let unvouched = !confirm && (self.resync.unconfirmed() || in_tail);
let parsed: Result<_, Option<anyhow::Error>> = match adts::Header::parse(&data[offset..]) {
Ok(header) => {
let end = offset + header.frame_len;
if end > data.len() {
if !self.resync.unconfirmed() {
break;
}
fallback.get_or_insert(Fallback {
offset,
discarded: self.resync.discarded,
pts,
in_tail,
});
Err(None)
} else if !confirm {
Ok((header, end))
} else if end + adts::MIN_HEADER_LEN > data.len() {
break;
} else {
adts::Header::parse(&data[end..]).map(|_| (header, end)).map_err(Some)
}
}
Err(err) => Err(Some(err)),
};
let (header, end) = match parsed {
Ok(found) => found,
Err(err) => {
if self.resync.exhausted() {
let context = format!("AAC stream never regained sync after {} bytes", self.resync.discarded);
return Err(match err {
Some(err) => err.context(context),
None => anyhow::Error::msg(context),
});
}
match self.resync.recover(data, offset, &SyncWord::ADTS) {
Recover::At(next) => {
if in_tail {
pts = self.reanchor(pes_base)?;
in_tail = false;
}
offset = next;
continue;
}
Recover::Carry(next) => {
match fallback {
Some(found) => {
self.resync.refund(found.discarded);
offset = found.offset;
pts = found.pts;
in_tail = found.in_tail;
}
None => {
if in_tail {
pts = self.reanchor(pes_base)?;
in_tail = false;
}
offset = next;
}
}
break;
}
}
}
};
let mut block = &data[offset + header.header_len..end];
let import = match &mut self.import {
Some(import) => import,
None => {
let asc = aac::in_band_config(
header.object_type,
header.sample_rate,
header.channel_config,
&mut block,
)
.map_err(|err| unit_error(err.into()))?;
let mut config = aac::config(&asc)?;
config.container = self.container.clone();
let reserved = self.reserved.take().expect("aac reservation already consumed");
let track = self
.broadcast
.unique_track(".aac", reserved.track_info(hang::catalog::PRIORITY.audio))?;
let aac = aac::Import::new(track, reserved, config)?;
self.import.insert(aac)
}
};
import.decode(block, pts)?;
burst += std::time::Duration::from_nanos((1024_u64 * 1_000_000_000).div_ceil(header.sample_rate as u64));
import.cut(None)?;
self.resync.published(unvouched);
published += 1;
fallback = None;
pts = advance_pts(pts, 1024, header.sample_rate)?;
offset = end;
}
if let Some(import) = &mut self.import {
self.burst = if batched { self.burst + burst } else { burst };
import.burst(self.burst)?;
}
if offset < data.len() {
if in_tail && offset >= carried {
pts = self.reanchor(pes_base)?;
}
self.tail = data[offset..].to_vec();
self.tail_pts = pts;
}
Ok(published)
}
fn reanchor(&mut self, pts: Option<Timestamp>) -> anyhow::Result<Option<Timestamp>> {
let edge = self.import.as_ref().and_then(aac::Import::live_edge);
pts.map(|pts| self.reanchor.apply(pts, edge)).transpose()
}
fn discontinuity(&mut self, published: bool) -> anyhow::Result<()> {
self.desync();
self.unwrap.discontinuity();
self.reanchor.discontinuity();
if published && let Some(import) = &mut self.import {
import.discontinuity()?;
}
Ok(())
}
fn seek(&mut self, sequence: u64) -> anyhow::Result<()> {
self.desync();
if let Some(import) = &mut self.import {
import.seek(sequence)?;
}
Ok(())
}
fn desync(&mut self) {
self.burst = std::time::Duration::ZERO;
self.tail.clear();
self.tail_pts = None;
self.resync.desynced();
}
fn finish(&mut self) -> anyhow::Result<u64> {
let mut drained = 0;
if !self.tail.is_empty() && self.import.is_some() {
self.resync.drain();
drained = self.write(Pending::empty(), true, None)?;
}
if !self.tail.is_empty() {
tracing::debug!(bytes = self.tail.len(), "dropping partial ADTS frame at end of stream");
}
if self.import.is_none() {
self.reserved.take();
}
if let Some(import) = &mut self.import {
import.finish()?;
}
Ok(drained)
}
fn abort(mut self, err: moq_net::Error) {
if let Some(import) = self.import.take() {
import.abort(err);
}
}
}
struct OpusStream {
import: opus::Import,
unwrap: PtsUnwrap,
reanchor: Reanchor,
}
impl OpusStream {
fn write(&mut self, pending: Pending, anchor: Option<&mut crate::clock::Anchor>) -> anyhow::Result<u64> {
let base = unwrap_pts(&mut self.unwrap, pending.pts, anchor)?;
let edge = self.import.live_edge();
let base = base.map(|base| self.reanchor.apply(base, edge)).transpose()?;
let packets = opus_packets(&pending.data).map_err(Damaged)?;
let mut published = 0;
let mut elapsed: u64 = 0;
for packet in packets {
let pts = match base {
Some(base) if elapsed > 0 => {
let advance = Timestamp::from_scale(elapsed, 48_000)?;
Some(base.checked_add(advance.convert(base.scale())?)?)
}
other => other,
};
self.import.decode(packet, pts)?;
self.import.cut(None)?;
elapsed += opus::packet_samples(packet).unwrap_or(960) as u64;
published += 1;
}
Ok(published)
}
fn discontinuity(&mut self, published: bool) -> anyhow::Result<()> {
self.unwrap.discontinuity();
self.reanchor.discontinuity();
if published {
self.import.discontinuity()?;
}
Ok(())
}
fn seek(&mut self, sequence: u64) -> anyhow::Result<()> {
Ok(self.import.seek(sequence)?)
}
fn finish(&mut self) -> anyhow::Result<()> {
Ok(self.import.finish()?)
}
fn abort(self, err: moq_net::Error) {
self.import.abort(err);
}
}
fn registration_format(descriptors: &[catalog::Descriptor]) -> Option<[u8; 4]> {
descriptors
.iter()
.find(|d| d.tag == 0x05)
.and_then(|d| d.data.get(..4))
.and_then(|s| s.try_into().ok())
}
fn opus_config(descriptors: &[catalog::Descriptor]) -> crate::Result<opus::Config> {
let channels = descriptors
.iter()
.find(|d| d.tag == 0x7f && d.data.first() == Some(&0x80))
.and_then(|d| d.data.get(1))
.and_then(|&cc| match cc {
0 => Some(2),
1..=8 => Some(cc as u32),
_ => None,
})
.unwrap_or(2);
let mut config = opus::Config::new(48_000, channels);
if channels > 2 {
let mapping = opus::Mapping::vorbis(channels as u8)?;
config.mapping_family = mapping.family();
config.mapping = Some(mapping);
}
Ok(config)
}
fn opus_packets(mut data: &[u8]) -> anyhow::Result<Vec<&[u8]>> {
let mut packets = Vec::new();
while !data.is_empty() {
let (header_len, size) = parse_opus_control_header(data)?;
let end = header_len + size;
let packet = data
.get(header_len..end)
.context("Opus access unit exceeds PES payload")?;
packets.push(packet);
data = &data[end..];
}
Ok(packets)
}
fn parse_opus_control_header(data: &[u8]) -> anyhow::Result<(usize, usize)> {
anyhow::ensure!(data.len() >= 2, "Opus control header truncated");
anyhow::ensure!(
data[0] == 0x7f && (data[1] & 0xe0) == 0xe0,
"invalid Opus control header sync (0x{:02x}{:02x})",
data[0],
data[1]
);
let start_trim = (data[1] & 0x10) != 0;
let end_trim = (data[1] & 0x08) != 0;
let control_ext = (data[1] & 0x04) != 0;
let mut pos = 2;
let mut size = 0usize;
loop {
let b = *data.get(pos).context("Opus au_size truncated")?;
pos += 1;
size += b as usize;
if b != 0xff {
break;
}
}
if start_trim {
pos += 2;
}
if end_trim {
pos += 2;
}
if control_ext {
let len = *data.get(pos).context("Opus control extension truncated")? as usize;
pos += 1 + len;
}
anyhow::ensure!(pos <= data.len(), "Opus control header exceeds payload");
Ok((pos, size))
}
struct LegacyStream<E: CatalogExt = ()> {
descriptor: &'static legacy::Descriptor,
import: Option<legacy::Import>,
broadcast: moq_net::broadcast::Producer,
reserved: Option<crate::catalog::Reserved<E>>,
container: hang::catalog::Container,
unwrap: PtsUnwrap,
tail: Vec<u8>,
tail_pts: Option<Timestamp>,
resync: Resync,
reanchor: Reanchor,
}
impl<E: CatalogExt> LegacyStream<E> {
fn write(&mut self, pending: Pending, anchor: Option<&mut crate::clock::Anchor>) -> anyhow::Result<u64> {
let mut published = 0;
let pes_base = unwrap_pts(&mut self.unwrap, pending.pts, anchor)?;
let carried = self.tail.len();
let joined;
let data: &[u8] = if carried == 0 {
&pending.data
} else {
let mut j = std::mem::take(&mut self.tail);
j.extend_from_slice(&pending.data);
joined = j;
&joined
};
let mut pts = if carried > 0 {
self.tail_pts.take()
} else {
self.reanchor(pes_base)?
};
let mut in_tail = carried > 0;
let mut offset = 0;
let mut fallback = None;
while offset + self.descriptor.min_header_len <= data.len() {
if in_tail && offset >= carried {
pts = self.reanchor(pes_base)?;
in_tail = false;
}
let confirm = self.resync.needs_confirmation(in_tail);
let unvouched = !confirm && (self.resync.unconfirmed() || in_tail);
let parsed: Result<_, Option<legacy::Error>> = match (self.descriptor.parse)(&data[offset..]) {
Ok(header) => {
let end = offset + header.len;
if end > data.len() {
if !self.resync.unconfirmed() {
break;
}
fallback.get_or_insert(Fallback {
offset,
discarded: self.resync.discarded,
pts,
in_tail,
});
Err(None)
} else if !confirm {
Ok((header, end))
} else if end + self.descriptor.min_header_len > data.len() {
break;
} else {
(self.descriptor.parse)(&data[end..])
.map(|_| (header, end))
.map_err(Some)
}
}
Err(err) => Err(Some(err)),
};
let (header, end) = match parsed {
Ok(found) => found,
Err(err) => {
if self.resync.exhausted() {
let context = format!(
"{} stream never regained sync after {} bytes",
self.descriptor.track_suffix, self.resync.discarded
);
return Err(match err {
Some(err) => anyhow::Error::new(err).context(context),
None => anyhow::Error::msg(context),
});
}
match self.resync.recover(data, offset, &self.descriptor.into()) {
Recover::At(next) => {
if in_tail {
pts = self.reanchor(pes_base)?;
in_tail = false;
}
offset = next;
continue;
}
Recover::Carry(next) => {
match fallback {
Some(found) => {
self.resync.refund(found.discarded);
offset = found.offset;
pts = found.pts;
in_tail = found.in_tail;
}
None => {
if in_tail {
pts = self.reanchor(pes_base)?;
in_tail = false;
}
offset = next;
}
}
break;
}
}
}
};
let import = match &mut self.import {
Some(import) => import,
None => {
let config = legacy::Config {
sample_rate: header.sample_rate,
channel_count: header.channel_count,
container: self.container.clone(),
};
let reserved = self.reserved.take().expect("legacy reservation already consumed");
let track = self.broadcast.unique_track(
self.descriptor.track_suffix,
reserved.track_info(hang::catalog::PRIORITY.audio),
)?;
let legacy = legacy::Import::new(self.descriptor, track, reserved, config)?;
self.import.insert(legacy)
}
};
import.decode(&data[offset..end], pts)?;
import.cut(None)?;
self.resync.published(unvouched);
published += 1;
fallback = None;
pts = advance_pts(pts, header.samples, header.sample_rate)?;
offset = end;
}
if offset < data.len() {
if in_tail && offset >= carried {
pts = self.reanchor(pes_base)?;
}
self.tail = data[offset..].to_vec();
self.tail_pts = pts;
}
Ok(published)
}
fn reanchor(&mut self, pts: Option<Timestamp>) -> anyhow::Result<Option<Timestamp>> {
let edge = self.import.as_ref().and_then(legacy::Import::live_edge);
pts.map(|pts| self.reanchor.apply(pts, edge)).transpose()
}
fn discontinuity(&mut self, published: bool) -> anyhow::Result<()> {
self.desync();
self.unwrap.discontinuity();
self.reanchor.discontinuity();
if published && let Some(import) = &mut self.import {
import.discontinuity()?;
}
Ok(())
}
fn seek(&mut self, sequence: u64) -> anyhow::Result<()> {
self.desync();
if let Some(import) = &mut self.import {
import.seek(sequence)?;
}
Ok(())
}
fn desync(&mut self) {
self.tail.clear();
self.tail_pts = None;
self.resync.desynced();
}
fn finish(&mut self) -> anyhow::Result<u64> {
let mut drained = 0;
if !self.tail.is_empty() && self.import.is_some() {
self.resync.drain();
drained = self.write(Pending::empty(), None)?;
}
if !self.tail.is_empty() {
tracing::debug!(
suffix = self.descriptor.track_suffix,
bytes = self.tail.len(),
"dropping partial frame at end of stream"
);
}
if self.import.is_none() {
self.reserved.take();
}
if let Some(import) = &mut self.import {
import.finish()?;
}
Ok(drained)
}
fn abort(mut self, err: moq_net::Error) {
if let Some(import) = self.import.take() {
import.abort(err);
}
}
}
fn skip_missing_keyframe(result: crate::Result<()>) -> anyhow::Result<bool> {
match result {
Ok(()) => Ok(true),
Err(crate::Error::MissingKeyframe(_)) => Ok(false),
Err(e) => Err(unit_error(e)),
}
}
fn advance_pts(pts: Option<Timestamp>, samples: u64, sample_rate: u32) -> anyhow::Result<Option<Timestamp>> {
let Some(pts) = pts else {
return Ok(None);
};
let advance = Timestamp::from_scale(samples, sample_rate as u64)?;
Ok(Some(pts.checked_add(advance.convert(pts.scale())?)?))
}
fn unwrap_pts(
unwrap: &mut PtsUnwrap,
pts: Option<u64>,
anchor: Option<&mut crate::clock::Anchor>,
) -> anyhow::Result<Option<Timestamp>> {
let Some(raw) = pts else {
return Ok(None);
};
let extended = Timestamp::from_scale(unwrap.unwrap(raw), 90_000)?;
Ok(Some(match anchor {
Some(anchor) => anchor.translate(&mut unwrap.lane, extended)?,
None => extended,
}))
}
fn reorder_delay(pts: Option<u64>, dts: Option<u64>) -> Option<Timestamp> {
const FIELD: u64 = 1 << 33;
const MAX_REORDER_TICKS: u64 = 90_000 * 2; let (pts, dts) = (pts?, dts?);
let delay = pts.wrapping_sub(dts) & (FIELD - 1);
if delay == 0 || delay > MAX_REORDER_TICKS {
return None;
}
Timestamp::from_scale(delay, 90_000).ok()
}
#[derive(Default)]
struct PtsUnwrap {
last: Option<u64>,
offset: u64,
lane: crate::clock::Lane,
}
impl PtsUnwrap {
fn unwrap(&mut self, raw: u64) -> u64 {
const WRAP: u64 = 1 << 33;
const HALF: i64 = (WRAP / 2) as i64;
if let Some(last) = self.last {
let diff = raw as i64 - last as i64;
if diff < -HALF {
self.offset += WRAP;
} else if diff > HALF && self.offset >= WRAP {
self.offset -= WRAP;
}
}
self.last = Some(raw);
self.offset + raw
}
fn discontinuity(&mut self) {
self.last = None;
self.lane.restart();
}
}
#[derive(Default)]
struct Reanchor {
shift: Option<Timestamp>,
}
impl Reanchor {
fn shifted(&self, pts: Timestamp) -> anyhow::Result<Timestamp> {
Ok(match self.shift {
Some(shift) => pts.checked_add(shift.convert(pts.scale())?)?,
None => pts,
})
}
fn apply(&mut self, pts: Timestamp, edge: Option<Timestamp>) -> anyhow::Result<Timestamp> {
let scale = pts.scale();
let pts = self.shifted(pts)?;
let Some(edge) = edge.filter(|edge| pts.as_micros() < edge.as_micros()) else {
return Ok(pts);
};
let mut target = Timestamp::new(u64::try_from(edge.as_scale(scale))?, scale)?;
if target.as_micros() < edge.as_micros() {
target = Timestamp::new(target.value() + 1, scale)?;
}
let grow = target.checked_sub(pts)?;
self.shift = Some(match self.shift {
Some(shift) => shift.convert(scale)?.checked_add(grow)?,
None => grow,
});
Ok(target)
}
fn discontinuity(&mut self) {
self.shift = None;
}
}
#[cfg(test)]
pub(super) mod test {
use std::collections::BTreeMap;
use std::time::Duration;
use moq_net::Timestamp;
use mpeg2ts::es::StreamType;
use super::{Continuation, Continuity, SectionReassembler, Stream};
use mpeg2ts::ts::{Pid, TsPacket};
const RECORDING_MAX_AGE: std::time::Duration = Duration::from_secs(30);
const CUE: [u8; 30] = [
0xfc, 0x30, 0x1b, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0xf0, 0x0a, 0x05, 0x00, 0x00, 0x2b, 0xb4,
0x7f, 0xdf, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xad, 0x25, 0xe8, 0x39,
];
#[test]
fn resync_stats_include_an_in_progress_scan() {
let mut resync = super::Resync::new(0x61, ".mp2");
let codec = super::SyncWord {
min_header_len: 4,
sync_byte: 0xff,
};
assert!(matches!(resync.recover(&[0; 10], 0, &codec), super::Recover::Carry(7)));
let stats = resync.stats();
assert_eq!(stats.discarded, 7, "the active scan must be visible before recovery");
assert_eq!(stats.resyncs, 0, "the stream has not regained sync yet");
}
#[test]
fn liveness_steps_over_a_corrupt_pcr_and_the_wrap() {
const MS: u64 = 27_000;
let mut liveness = super::Liveness::default();
liveness.register(0x100);
let mut pcr = super::super::mux_rate::PCR_WRAP - 100 * MS;
for _ in 0..10 {
liveness.pcr(pcr);
pcr = (pcr + 40 * MS) % super::super::mux_rate::PCR_WRAP;
}
assert_eq!(liveness.stream(0x100), (0, Some(Duration::from_millis(360))));
liveness.pcr(pcr + 23_861_000 * MS);
liveness.pcr(pcr);
assert_eq!(liveness.stream(0x100).1, Some(Duration::from_millis(360)));
liveness.pcr(pcr + 40 * MS);
liveness.delivered(0x100, 3);
liveness.pcr(pcr + 80 * MS);
assert_eq!(liveness.stream(0x100), (3, Some(Duration::from_millis(40))));
}
#[test]
fn resync_does_not_count_an_unvouched_eof_publication() {
let mut resync = super::Resync::new(0x61, ".mp2");
let codec = super::SyncWord {
min_header_len: 4,
sync_byte: 0xff,
};
resync.recover(&[0; 10], 0, &codec);
resync.drain();
resync.published(true);
let stats = resync.stats();
assert_eq!(stats.discarded, 7, "the bytes skipped before EOF were discarded");
assert_eq!(
stats.resyncs, 0,
"an unconfirmed frame does not prove sync was regained"
);
assert_eq!(stats.unconfirmed, 1, "the unvouched publication is counted separately");
}
#[test]
fn remapping_a_pid_keeps_its_retired_resync_stats() {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let pid = mpeg2ts::ts::Pid::new(0x61).unwrap();
let mut stream = import.legacy_stream(pid, &super::mp2::DESCRIPTOR);
let super::Stream::Legacy(legacy) = &mut stream else {
unreachable!();
};
let codec = super::SyncWord::from(&super::mp2::DESCRIPTOR);
legacy.resync.recover(&[0; 10], 0, &codec);
legacy.resync.published(false);
import.streams.insert(pid, stream);
let before = import.stats();
import
.ensure_section(pid, StreamType::Dts8ChannelLosslessAudio as u8, &[])
.unwrap();
assert!(matches!(import.streams.get(&pid), Some(super::Stream::Ignored)));
assert_eq!(import.stats(), before, "a PMT remap must not reset lifetime counters");
}
fn packet(pusi: bool, cc: u8, pointer: u8, body: &[u8]) -> Vec<u8> {
let mut p = vec![0x47, 0x00, 0x21, 0x10 | (cc & 0x0f)];
if pusi {
p[1] |= 0x40;
p.push(pointer);
}
p.extend_from_slice(body);
assert!(p.len() <= 188, "test packet body overflows 188 bytes");
p.resize(188, 0xff);
p
}
fn discontinuity_packet(cc: u8, body: &[u8]) -> Vec<u8> {
let mut p = vec![0x47, 0x00, 0x21, 0x30 | (cc & 0x0f), 0x01, 0x80];
p.extend_from_slice(body);
assert!(p.len() <= 188, "test packet body overflows 188 bytes");
p.resize(188, 0xff);
p
}
fn clock_packet(cc: u8, pcr: Option<[u8; 6]>, opcr: Option<[u8; 6]>, body: &[u8]) -> Vec<u8> {
let flags = if pcr.is_some() { 0x10 } else { 0 } | if opcr.is_some() { 0x08 } else { 0 };
let length = 1 + usize::from(pcr.is_some()) * 6 + usize::from(opcr.is_some()) * 6;
let mut p = vec![0x47, 0x00, 0x21, 0x30 | (cc & 0x0f), length as u8, flags];
if let Some(pcr) = pcr {
p.extend_from_slice(&pcr);
}
if let Some(opcr) = opcr {
p.extend_from_slice(&opcr);
}
p.extend_from_slice(body);
assert!(p.len() <= 188, "test packet body overflows 188 bytes");
p.resize(188, 0xff);
p
}
fn fake_section(table_id: u8, body_len: usize) -> Vec<u8> {
let mut s = vec![table_id, ((body_len >> 8) & 0x0f) as u8, (body_len & 0xff) as u8];
s.resize(3 + body_len, 0x00);
s
}
fn run(pkts: &[Vec<u8>]) -> Vec<Vec<u8>> {
let mut r = SectionReassembler::default();
let mut out = Vec::new();
for p in pkts {
r.push(p, &mut out);
}
out
}
#[test]
fn single_section() {
assert_eq!(run(&[packet(true, 0, 0, &CUE)]), vec![CUE.to_vec()]);
}
#[test]
fn carries_all_sections_verbatim() {
let other = fake_section(0x00, 5);
let mut body = other.clone();
body.extend_from_slice(&CUE);
assert_eq!(run(&[packet(true, 0, 0, &body)]), vec![other, CUE.to_vec()]);
}
#[test]
fn stuffing_only() {
assert!(run(&[packet(true, 0, 0, &[])]).is_empty());
}
#[test]
fn payload_split_across_packets() {
let section = fake_section(0xfc, 247);
let p1 = packet(true, 0, 0, §ion[..183]);
let p2 = packet(false, 1, 0, §ion[183..]);
assert_eq!(run(&[p1, p2]), vec![section]);
}
#[test]
fn header_split_across_packets() {
let a = fake_section(0xfc, 178);
let mut body = a.clone();
body.extend_from_slice(&CUE[..2]);
let p1 = packet(true, 0, 0, &body);
let p2 = packet(false, 1, 0, &CUE[2..]);
assert_eq!(run(&[p1, p2]), vec![a, CUE.to_vec()]);
}
#[test]
fn continuity_gap_drops_partial() {
let section = fake_section(0xfc, 247);
let p1 = packet(true, 0, 0, §ion[..183]);
let p2 = packet(false, 3, 0, §ion[183..]);
assert!(run(&[p1, p2]).is_empty());
}
#[test]
fn discontinuity_drops_partial() {
let section = fake_section(0xfc, 247);
let p1 = packet(true, 0, 0, §ion[..183]);
let p2 = discontinuity_packet(1, §ion[183..]);
assert!(run(&[p1, p2]).is_empty());
}
#[test]
fn gap_then_unaligned_payload_is_not_emitted() {
let section = fake_section(0xfc, 247);
let p1 = packet(true, 0, 0, §ion[..183]);
let p2 = packet(false, 2, 0, &CUE); assert!(run(&[p1, p2]).is_empty());
}
#[test]
fn corrupt_pointer_field_is_dropped() {
assert!(run(&[packet(true, 0, 200, &CUE)]).is_empty());
}
#[test]
fn stays_desynced_until_next_pusi() {
let section = fake_section(0xfc, 247);
let p1 = packet(true, 0, 0, §ion[..183]);
let p2 = packet(false, 2, 0, §ion[183..]); let p3 = packet(false, 3, 0, &CUE); let p4 = packet(true, 4, 0, &CUE); assert_eq!(run(&[p1, p2, p3, p4]), vec![CUE.to_vec()]);
}
#[test]
fn orphan_tail_before_section_is_skipped() {
let mut body = CUE.to_vec(); body.extend_from_slice(&CUE); let pkt = packet(true, 0, CUE.len() as u8, &body);
assert_eq!(run(&[pkt]), vec![CUE.to_vec()]);
}
fn synth_pmt(es: &[(StreamType, u16)], cuei: bool) -> Vec<u8> {
synth_programs(&[(1, 0x0100, es)], cuei)
}
type SynthProgram<'a> = (u16, u16, &'a [(StreamType, u16)]);
fn synth_programs(programs: &[SynthProgram], cuei: bool) -> Vec<u8> {
use mpeg2ts::ts::payload::{Pat, Pmt};
use mpeg2ts::ts::{
ContinuityCounter, Descriptor, EsInfo, Pid, ProgramAssociation, TransportScramblingControl, TsHeader,
TsPacket, TsPacketWriter, TsPayload, VersionNumber, WriteTsPacket,
};
let pat = Pat {
transport_stream_id: 1,
version_number: VersionNumber::default(),
table: programs
.iter()
.map(|&(program_num, pmt_pid, _)| ProgramAssociation {
program_num,
program_map_pid: Pid::new(pmt_pid).unwrap(),
})
.collect(),
};
let pmt = |program_num: u16, es: &[(StreamType, u16)]| Pmt {
program_num,
pcr_pid: es.first().map(|&(_, pid)| Pid::new(pid).unwrap()),
version_number: VersionNumber::default(),
program_info: if cuei {
vec![Descriptor {
tag: 0x05,
data: b"CUEI".to_vec(),
}]
} else {
Vec::new()
},
es_info: es
.iter()
.map(|&(stream_type, pid)| EsInfo {
stream_type,
elementary_pid: Pid::new(pid).unwrap(),
descriptors: Vec::new(),
})
.collect(),
};
let write = |out: &mut Vec<u8>, pid: u16, payload: TsPayload| {
let packet = TsPacket {
header: TsHeader {
transport_error_indicator: false,
transport_priority: false,
pid: Pid::new(pid).unwrap(),
transport_scrambling_control: TransportScramblingControl::NotScrambled,
continuity_counter: ContinuityCounter::default(),
},
adaptation_field: None,
payload: Some(payload),
};
TsPacketWriter::new(out).write_ts_packet(&packet).unwrap();
};
let mut out = Vec::new();
write(&mut out, Pid::PAT, TsPayload::Pat(pat));
for &(program_num, pmt_pid, es) in programs {
write(&mut out, pmt_pid, TsPayload::Pmt(pmt(program_num, es)));
}
out
}
#[test]
fn scte35_extension_catalogs_the_cue_track() {
use crate::catalog::hang::Catalog;
use crate::container::ts::catalog::Ext;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = crate::catalog::Producer::new(
&mut broadcast,
crate::catalog::Config::default().with_catalog(Catalog::<Ext>::default()),
)
.unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let mut bytes = bytes::BytesMut::new();
bytes.extend_from_slice(&synth_pmt(&[(StreamType::Dts8ChannelLosslessAudio, 0x21)], true));
bytes.extend_from_slice(&packet(true, 0, 0, &CUE));
import.decode(&bytes).unwrap();
import.finish().unwrap();
assert_eq!(
catalog.snapshot().ext.mpegts.tracks.len(),
1,
"expected one scte35 rendition"
);
}
#[tokio::test(start_paused = true)]
async fn base_catalog_routes_cue_pid_to_ignored() {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut updates = catalog.consume().unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let mut bytes = bytes::BytesMut::new();
bytes.extend_from_slice(&synth_pmt(&[(StreamType::Dts8ChannelLosslessAudio, 0x21)], true));
bytes.extend_from_slice(&packet(true, 0, 0, &CUE));
import.decode(&bytes).unwrap();
assert!(
import.sections.is_empty(),
"no cue stream is created for a base catalog"
);
assert!(
matches!(
import.streams.get(&mpeg2ts::ts::Pid::new(0x21).unwrap()),
Some(super::Stream::Ignored)
),
"the CUEI PID routes to Ignored"
);
import.finish().unwrap();
assert!(
tokio::time::timeout(Duration::from_millis(10), updates.next())
.await
.is_err(),
"SCTE detection must not publish the base catalog"
);
}
#[tokio::test(start_paused = true)]
async fn pmt_without_cuei_then_with_cuei_upgrades() {
use crate::catalog::hang::{Catalog, Container};
use crate::container::Consumer;
use crate::container::ts::catalog::Ext;
const SECTION_PID: u16 = 0x0021;
let pid = mpeg2ts::ts::Pid::new(SECTION_PID).unwrap();
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(
&mut broadcast,
crate::catalog::Config::default().with_catalog(Catalog::<Ext>::default()),
)
.unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let mut bytes = bytes::BytesMut::new();
bytes.extend_from_slice(&synth_pmt(
&[(StreamType::Dts8ChannelLosslessAudio, SECTION_PID)],
false,
));
import.decode(&bytes).unwrap();
assert!(
matches!(import.streams.get(&pid), Some(super::Stream::Ignored)),
"pre-CUEI PMT routes the PID to Ignored"
);
let mut bytes = bytes::BytesMut::new();
bytes.extend_from_slice(&synth_pmt(&[(StreamType::Dts8ChannelLosslessAudio, SECTION_PID)], true));
bytes.extend_from_slice(&packet(true, 0, 0, &CUE));
import.decode(&bytes).unwrap();
assert!(
!import.streams.contains_key(&pid),
"upgrade drops the stale Ignored route"
);
assert_eq!(
catalog.snapshot().ext.mpegts.tracks.len(),
1,
"upgrade advertises the cue track"
);
let name = catalog.snapshot().ext.mpegts.tracks.keys().next().unwrap().clone();
import.finish().unwrap();
let track = consumer
.track(&name)
.unwrap()
.subscribe(moq_net::track::Subscription::default().with_max_age(RECORDING_MAX_AGE))
.await
.unwrap();
let mut reader = Consumer::new(track, Container::Legacy(crate::container::Kind::Data));
let frame = tokio::time::timeout(Duration::from_secs(1), reader.read())
.await
.expect("cue read timed out")
.unwrap()
.expect("a published cue frame");
assert_eq!(
&frame.payload[..],
&CUE[..],
"verbatim splice_info_section after upgrade"
);
}
fn pes_packet(pid: u16, pts: u64) -> Vec<u8> {
let pts_field = [
0x21 | (((pts >> 30) & 0x07) << 1) as u8,
((pts >> 22) & 0xff) as u8,
0x01 | (((pts >> 15) & 0x7f) << 1) as u8,
((pts >> 7) & 0xff) as u8,
0x01 | ((pts & 0x7f) << 1) as u8,
];
let mut pes = vec![0x00, 0x00, 0x01, 0xe0]; let pes_len = 3 + 5 + 1; pes.push((pes_len >> 8) as u8);
pes.push((pes_len & 0xff) as u8);
pes.push(0x80); pes.push(0x80); pes.push(0x05); pes.extend_from_slice(&pts_field);
pes.push(0xff);
let mut p = vec![0x47, 0x40 | ((pid >> 8) as u8 & 0x1f), (pid & 0xff) as u8, 0x10];
p.extend_from_slice(&pes);
assert!(p.len() <= 188, "PES packet overflows 188 bytes");
p.resize(188, 0xff);
p
}
#[test]
fn media_clock_follows_video_not_private_pes() {
const VIDEO_PID: u16 = 0x0050;
const PRIVATE_PID: u16 = 0x0051;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let mut bytes = bytes::BytesMut::new();
bytes.extend_from_slice(&synth_pmt(
&[
(StreamType::Mpeg2Video, VIDEO_PID),
(StreamType::Mpeg2PacketizedData, PRIVATE_PID),
],
true,
));
import.decode(&bytes).unwrap();
import.decode(pes_packet(PRIVATE_PID, 1_000).as_slice()).unwrap();
assert!(import.last_pts.is_none(), "a private PES must not start the clock");
import.decode(pes_packet(VIDEO_PID, 90_000).as_slice()).unwrap();
let after_video = import.last_pts;
assert!(after_video.is_some(), "MPEG-2 video PTS must set the clock");
import.decode(pes_packet(PRIVATE_PID, 270_000).as_slice()).unwrap();
assert_eq!(
import.last_pts, after_video,
"a later private PES must not overwrite the clock"
);
}
fn audio_pes_packet(pid: u16, cc: u8, pts: u64, payload: &[u8]) -> Vec<u8> {
let pts_field = [
0x21 | (((pts >> 30) & 0x07) << 1) as u8,
((pts >> 22) & 0xff) as u8,
0x01 | (((pts >> 15) & 0x7f) << 1) as u8,
((pts >> 7) & 0xff) as u8,
0x01 | ((pts & 0x7f) << 1) as u8,
];
let mut pes = vec![0x00, 0x00, 0x01, 0xc0];
let pes_len = 3 + 5 + payload.len();
pes.push((pes_len >> 8) as u8);
pes.push((pes_len & 0xff) as u8);
pes.extend_from_slice(&[0x80, 0x80, 0x05]); pes.extend_from_slice(&pts_field);
pes.extend_from_slice(payload);
let af_len = 184 - 1 - pes.len();
let mut p = vec![
0x47,
0x40 | ((pid >> 8) as u8 & 0x1f),
(pid & 0xff) as u8,
0x30 | (cc & 0x0f),
];
p.push(af_len as u8);
if af_len > 0 {
p.push(0x00); p.extend(std::iter::repeat_n(0xff, af_len - 1));
}
p.extend_from_slice(&pes);
assert_eq!(p.len(), 188, "audio PES packet must fill exactly one TS packet");
p
}
#[test]
fn aac_jitter_accumulates_adjacent_pes_per_pid() {
const AUDIO: u16 = 0x60;
const VIDEO: u16 = 0x61;
const OTHER: u16 = 0x62;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
import
.decode(&synth_pmt(
&[
(StreamType::AdtsAac, AUDIO),
(StreamType::Mpeg2Video, VIDEO),
(StreamType::AdtsAac, OTHER),
],
false,
))
.unwrap();
let mut frame = super::adts::write_header(2, 44_100, 2, 8).unwrap().to_vec();
frame.extend_from_slice(&[0; 8]);
for i in 0..4 {
import
.decode(&audio_pes_packet(
AUDIO,
i,
90_000 + u64::from(i) * 4180,
&frame.repeat(2),
))
.unwrap();
}
let name = catalog.snapshot().audio.renditions.keys().next().unwrap().clone();
let jitter = catalog.snapshot().audio.renditions[&name].jitter.unwrap();
import
.decode(&audio_pes_packet(OTHER, 0, 900_000, &frame.repeat(2)))
.unwrap();
let snapshot = catalog.snapshot();
let other = snapshot
.audio
.renditions
.iter()
.find(|(key, _)| *key != &name)
.unwrap()
.1;
assert_eq!(other.jitter.unwrap().as_nanos().div_ceil(1_000_000), 47);
assert_eq!(jitter.as_nanos().div_ceil(1_000_000), 186);
import.decode(&pes_packet(VIDEO, 110_000)).unwrap();
import
.decode(&audio_pes_packet(AUDIO, 4, 110_000, &frame.repeat(2)))
.unwrap();
assert_eq!(catalog.snapshot().audio.renditions[&name].jitter, Some(jitter));
}
#[test]
fn aac_pes_jitter_survives_bitrate_updates() {
const PID: u16 = 0x60;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
import.decode(&synth_pmt(&[(StreamType::AdtsAac, PID)], false)).unwrap();
let mut frame = super::adts::write_header(2, 44_100, 2, 8).unwrap().to_vec();
frame.extend_from_slice(&[0; 8]);
import
.decode(&audio_pes_packet(PID, 0, 90_000, &frame.repeat(7)))
.unwrap();
let initial = catalog
.snapshot()
.audio
.renditions
.values()
.next()
.unwrap()
.jitter
.unwrap();
assert_eq!(
initial.as_nanos().div_ceil(1_000_000),
163,
"a seven-frame PES is one burst"
);
for i in 0..60 {
let pts = 90_000 + (7 + i) * 1024 * 90_000 / 44_100;
import
.decode(&audio_pes_packet(PID, ((i + 1) % 16) as u8, pts, &frame))
.unwrap();
}
import.finish().unwrap();
let snapshot = catalog.snapshot();
let config = snapshot.audio.renditions.values().next().unwrap();
assert!(config.bitrate.is_some(), "exercise a bitrate refinement");
assert_eq!(config.jitter, Some(initial));
}
#[test]
fn aac_pes_jitter_counts_completed_split_frames() {
const PID: u16 = 0x60;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
import.decode(&synth_pmt(&[(StreamType::AdtsAac, PID)], false)).unwrap();
let mut frame = super::adts::write_header(2, 44_100, 2, 8).unwrap().to_vec();
frame.extend_from_slice(&[0; 8]);
let mut first = frame.repeat(2);
first.extend_from_slice(&frame[..10]);
import.decode(&audio_pes_packet(PID, 0, 90_000, &first)).unwrap();
let mut second = frame[10..].to_vec();
second.extend_from_slice(&frame.repeat(6));
import.decode(&audio_pes_packet(PID, 1, 900_000, &second)).unwrap();
let snapshot = catalog.snapshot();
let jitter = snapshot.audio.renditions.values().next().unwrap().jitter.unwrap();
assert_eq!(jitter.as_nanos().div_ceil(1_000_000), 163);
}
fn audio_pes_open(pid: u16, cc: u8, pts: u64, declared: usize, payload: &[u8]) -> Vec<u8> {
assert!(payload.len() < declared, "the test PES must remain open");
let pts_field = [
0x21 | (((pts >> 30) & 0x07) << 1) as u8,
((pts >> 22) & 0xff) as u8,
0x01 | (((pts >> 15) & 0x7f) << 1) as u8,
((pts >> 7) & 0xff) as u8,
0x01 | ((pts & 0x7f) << 1) as u8,
];
let mut pes = vec![0x00, 0x00, 0x01, 0xc0];
let pes_len = 3 + 5 + declared;
pes.push((pes_len >> 8) as u8);
pes.push((pes_len & 0xff) as u8);
pes.extend_from_slice(&[0x80, 0x80, 0x05]);
pes.extend_from_slice(&pts_field);
pes.extend_from_slice(payload);
let af_len = 184 - 1 - pes.len();
let mut p = vec![
0x47,
0x40 | ((pid >> 8) as u8 & 0x1f),
(pid & 0xff) as u8,
0x30 | (cc & 0x0f),
];
p.push(af_len as u8);
if af_len > 0 {
p.push(0x00);
p.extend(std::iter::repeat_n(0xff, af_len - 1));
}
p.extend_from_slice(&pes);
assert_eq!(p.len(), 188, "open audio PES packet must fill exactly one TS packet");
p
}
fn ts_continuation(pid: u16, cc: u8, payload: &[u8]) -> Vec<u8> {
let af_len = 184 - 1 - payload.len();
let mut p = vec![0x47, ((pid >> 8) as u8 & 0x1f), (pid & 0xff) as u8, 0x30 | (cc & 0x0f)];
p.push(af_len as u8);
if af_len > 0 {
p.push(0x00);
p.extend(std::iter::repeat_n(0xff, af_len - 1));
}
p.extend_from_slice(payload);
assert_eq!(p.len(), 188, "continuation packet must fill exactly one TS packet");
p
}
#[test]
fn aac_jitter_is_the_pes_burst_not_one_frame() {
const AAC_PID: u16 = 0x0060;
const FRAME: u64 = 1024 * 90_000 / 48_000;
const PER_PES: u64 = 7;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let mut bytes = bytes::BytesMut::new();
bytes.extend_from_slice(&synth_pmt(&[(StreamType::AdtsAac, AAC_PID)], false));
for pes in 0..10u64 {
let mut payload = Vec::new();
for frame in 0..PER_PES {
payload.extend_from_slice(&adts_frame(17, 0xA0 | frame as u8));
}
bytes.extend_from_slice(&audio_pes_packet(
AAC_PID,
pes as u8 & 0x0f,
pes * PER_PES * FRAME,
&payload,
));
}
import.decode(&bytes).unwrap();
import.finish().unwrap();
let snap = catalog.snapshot();
let aac = snap.audio.renditions.values().next().expect("an AAC rendition");
assert!(aac.bitrate.is_some(), "the bitrate window closed at least once");
let jitter = aac.jitter.expect("AAC publishes a jitter");
assert!(
jitter >= std::time::Duration::from_millis(120),
"jitter describes one frame, not the burst: {jitter:?}"
);
assert!(
jitter <= std::time::Duration::from_millis(160),
"jitter grew past one burst: {jitter:?}"
);
}
#[test]
fn verbatim_audio_does_not_anchor_aac_jitter() {
const AAC_PID: u16 = 0x0060;
const MP2_PID: u16 = 0x0061;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let mut bytes = bytes::BytesMut::new();
bytes.extend_from_slice(&synth_pmt(
&[(StreamType::AdtsAac, AAC_PID), (StreamType::Mpeg1Audio, MP2_PID)],
false,
));
let mut mp2 = vec![0xFF, 0xFD, 0x14, 0x00];
mp2.resize(96, 0xAA);
bytes.extend_from_slice(&audio_pes_packet(MP2_PID, 0, 90_000, &mp2));
bytes.extend_from_slice(&audio_pes_packet(MP2_PID, 1, 92_160, &mp2));
let mut aac = Vec::new();
for _ in 0..2 {
aac.extend_from_slice(&super::adts::write_header(2, 48_000, 2, 8).unwrap());
aac.extend_from_slice(&[0u8; 8]);
}
bytes.extend_from_slice(&audio_pes_packet(AAC_PID, 0, 270_000, &aac));
import.decode(&bytes).unwrap();
import.finish().unwrap();
let snap = catalog.snapshot();
assert_eq!(snap.audio.renditions.len(), 2, "AAC and MP2 renditions");
let aac_rendition = snap
.audio
.renditions
.values()
.find(|a| a.codec.to_string().starts_with("mp4a"))
.expect("AAC rendition");
let jitter = aac_rendition.jitter.expect("AAC publishes a jitter");
assert!(
jitter <= Duration::from_millis(100),
"AAC jitter anchored on a foreign PID: {jitter:?}"
);
}
async fn read_audio_frames(
consumer: &moq_net::broadcast::Consumer,
catalog: &crate::catalog::Producer,
) -> Vec<crate::container::Frame> {
let name = catalog
.snapshot()
.audio
.renditions
.keys()
.next()
.expect("an audio track")
.clone();
let track = consumer
.track(&name)
.unwrap()
.subscribe(moq_net::track::Subscription::default().with_max_age(RECORDING_MAX_AGE))
.await
.unwrap();
let mut reader = crate::container::Consumer::new(
track,
crate::catalog::hang::Container::Legacy(crate::container::Kind::Audio),
);
let mut frames = Vec::new();
while let Ok(Ok(Some(frame))) = tokio::time::timeout(Duration::from_millis(50), reader.read()).await {
frames.push(frame);
}
frames
}
#[tokio::test(start_paused = true)]
async fn legacy_survives_a_looping_file_wrap() {
let data = include_bytes!("test_data/ac3.ts");
let cut = (data.len() / 2) / 188 * 188;
let mut looped = data[..cut].to_vec();
looped.extend_from_slice(data);
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
import
.decode(&bytes::BytesMut::from(&looped[..]))
.expect("a loop wrap must not end the session");
import.finish().unwrap();
let looped_stats = import.stats();
let frames = read_audio_frames(&consumer, &catalog).await;
assert!(
frames.iter().all(|f| f.payload[0] == 0x0B && f.payload[1] == 0x77),
"resync published a frame that doesn't start at an AC-3 sync word"
);
let pristine = {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
import.decode(&bytes::BytesMut::from(&data[..])).unwrap();
import.finish().unwrap();
read_audio_frames(&consumer, &catalog).await.len()
};
assert!(
frames.len() > pristine,
"the wrap swallowed frames: {} looped vs {pristine} pristine",
frames.len()
);
assert!(
looped_stats
.streams
.values()
.all(|s| s.resyncs == 0 && s.discarded == 0 && s.unconfirmed == 0),
"a wrap the continuity check absorbs should cost no frame sync: {looped_stats:?}"
);
}
async fn read_video_frames(
consumer: &moq_net::broadcast::Consumer,
catalog: &crate::catalog::Producer,
) -> Vec<crate::container::Frame> {
let name = catalog
.snapshot()
.video
.renditions
.keys()
.next()
.expect("a video track")
.clone();
let track = consumer.track(&name).unwrap().subscribe(None).await.unwrap();
let mut reader = crate::container::Consumer::new(
track,
crate::catalog::hang::Container::Legacy(crate::container::Kind::Data),
);
let mut frames = Vec::new();
while let Ok(Ok(Some(frame))) = tokio::time::timeout(Duration::from_millis(50), reader.read()).await {
frames.push(frame);
}
frames
}
fn annexb_au(keyframe: bool) -> Vec<u8> {
use crate::container::test_util::{IDR, PPS, SPS};
let nals: &[&[u8]] = if keyframe {
&[SPS, PPS, IDR]
} else {
&[&[0x41, 0x9a, 0x00, 0x01]]
};
let mut out = Vec::new();
for nal in nals {
out.extend_from_slice(&[0, 0, 0, 1]);
out.extend_from_slice(nal);
}
out
}
#[tokio::test(start_paused = true)]
async fn video_drops_a_partial_access_unit_across_a_continuity_break() {
const VIDEO_PID: u16 = 0x0050;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let pmt = synth_pmt(&[(StreamType::H264, VIDEO_PID)], false);
import.decode(&bytes::BytesMut::from(&pmt[..])).unwrap();
let whole = annexb_au(true);
import
.decode(audio_pes_open(VIDEO_PID, 0, 90_000, whole.len() + 32, &whole[..28]).as_slice())
.unwrap();
import
.decode(ts_continuation(VIDEO_PID, 12, &whole[28..]).as_slice())
.unwrap();
import
.decode(audio_pes_packet(VIDEO_PID, 13, 270_000, &whole).as_slice())
.unwrap();
import
.decode(audio_pes_packet(VIDEO_PID, 14, 450_000, &annexb_au(false)).as_slice())
.unwrap();
import.finish().unwrap();
let frames = read_video_frames(&consumer, &catalog).await;
assert!(
frames.iter().all(|f| f.payload.len() >= whole.len() || !f.keyframe),
"a keyframe with missing slices reached the track: {:?}",
frames.iter().map(|f| (f.keyframe, f.payload.len())).collect::<Vec<_>>()
);
assert_eq!(
frames.first().map(|f| f.payload.to_vec()),
Some(whole.clone()),
"the first published picture is not the whole keyframe"
);
}
fn adts_frame(raw_len: usize, fill: u8) -> Vec<u8> {
let mut f = super::adts::write_header(2, 48_000, 2, raw_len).unwrap().to_vec();
f.resize(raw_len + 7, fill);
f
}
#[tokio::test(start_paused = true)]
async fn aac_drops_a_pes_completed_across_a_continuity_break() {
const AAC_PID: u16 = 0x0060;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let pmt = synth_pmt(&[(StreamType::AdtsAac, AAC_PID)], false);
import.decode(&bytes::BytesMut::from(&pmt[..])).unwrap();
let mut opening = adts_frame(40, 0xAA);
opening.extend_from_slice(&adts_frame(40, 0xBB)[..25]);
import
.decode(audio_pes_open(AAC_PID, 0, 90_000, 94, &opening).as_slice())
.unwrap();
import
.decode(ts_continuation(AAC_PID, 12, &adts_frame(40, 0xCC)[..32]).as_slice())
.unwrap();
let mut normal = adts_frame(40, 0xDD);
normal.extend_from_slice(&adts_frame(40, 0xEE));
import
.decode(audio_pes_packet(AAC_PID, 13, 270_000, &normal).as_slice())
.unwrap();
import.finish().unwrap();
let frames = read_audio_frames(&consumer, &catalog).await;
assert_eq!(
frames.iter().map(|f| f.payload.to_vec()).collect::<Vec<_>>(),
[0xAA, 0xDD, 0xEE]
.map(|fill| adts_frame(40, fill)[7..].to_vec())
.to_vec(),
"the wrap was spliced onto the frame the cut left open"
);
}
#[tokio::test(start_paused = true)]
async fn aac_frame_split_across_pes_reassembles() {
const AAC_PID: u16 = 0x0060;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let pmt = synth_pmt(&[(StreamType::AdtsAac, AAC_PID)], false);
import.decode(&bytes::BytesMut::from(&pmt[..])).unwrap();
let frame = adts_frame(40, 0x5A);
import
.decode(audio_pes_packet(AAC_PID, 0, 90_000, &frame[..25]).as_slice())
.expect("a split ADTS frame is not fatal");
let mut rest = frame[25..].to_vec();
rest.extend_from_slice(&adts_frame(40, 0x77));
import
.decode(audio_pes_packet(AAC_PID, 1, 270_000, &rest).as_slice())
.unwrap();
import.finish().unwrap();
let frames = read_audio_frames(&consumer, &catalog).await;
assert_eq!(frames.len(), 2, "the split frame is reassembled, not dropped");
assert_eq!(frames[0].payload.as_ref(), &frame[7..], "reassembled byte-exact");
assert_eq!(frames[0].timestamp, Timestamp::from_micros(1_000_000).unwrap());
}
#[tokio::test(start_paused = true)]
async fn aac_resyncs_past_damaged_header() {
const AAC_PID: u16 = 0x0060;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let pmt = synth_pmt(&[(StreamType::AdtsAac, AAC_PID)], false);
import.decode(&bytes::BytesMut::from(&pmt[..])).unwrap();
let mut payload = adts_frame(40, 0xAA);
payload.extend_from_slice(&adts_frame(40, 0xBB));
import
.decode(audio_pes_packet(AAC_PID, 0, 90_000, &payload).as_slice())
.unwrap();
let mut damaged = adts_frame(40, 0x11);
damaged[0] = 0xFE;
let mut hit = damaged.clone();
hit.extend_from_slice(&adts_frame(40, 0xCC));
import
.decode(audio_pes_packet(AAC_PID, 1, 270_000, &hit).as_slice())
.expect("a damaged ADTS header is not fatal");
import
.decode(audio_pes_packet(AAC_PID, 2, 450_000, &adts_frame(40, 0xDD)).as_slice())
.unwrap();
import.finish().unwrap();
let frames = read_audio_frames(&consumer, &catalog).await;
assert_eq!(
frames.iter().map(|f| f.payload.to_vec()).collect::<Vec<_>>(),
[0xAA, 0xBB, 0xCC, 0xDD]
.map(|fill| adts_frame(40, fill)[7..].to_vec())
.to_vec(),
"the undamaged frames either side survive, and only the damaged one is dropped"
);
assert_eq!(
import.stats().streams,
BTreeMap::from([(
AAC_PID,
super::StreamStats {
track: ".aac",
units: 4,
quiet: None,
resyncs: 1,
discarded: 47,
unconfirmed: 0,
}
)]),
"the resync left no trace an operator could alarm on"
);
}
#[tokio::test(start_paused = true)]
async fn aac_confirms_a_frame_joined_out_of_a_carried_tail() {
const AAC_PID: u16 = 0x0060;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let pmt = synth_pmt(&[(StreamType::AdtsAac, AAC_PID)], false);
import.decode(&bytes::BytesMut::from(&pmt[..])).unwrap();
let mut payload = adts_frame(40, 0xAA);
payload.extend_from_slice(&adts_frame(40, 0xBB)[..25]);
import
.decode(audio_pes_packet(AAC_PID, 0, 90_000, &payload).as_slice())
.unwrap();
let mut wrapped = adts_frame(40, 0xCC);
wrapped.extend_from_slice(&adts_frame(40, 0xDD));
import
.decode(audio_pes_packet(AAC_PID, 1, 270_000, &wrapped).as_slice())
.expect("a splice is not fatal");
import
.decode(audio_pes_packet(AAC_PID, 2, 450_000, &adts_frame(40, 0xEE)).as_slice())
.unwrap();
import.finish().unwrap();
let frames = read_audio_frames(&consumer, &catalog).await;
assert_eq!(
frames.iter().map(|f| f.payload.to_vec()).collect::<Vec<_>>(),
[0xAA, 0xCC, 0xDD, 0xEE]
.map(|fill| adts_frame(40, fill)[7..].to_vec())
.to_vec(),
"the splice cost more than the frame it interrupted"
);
assert_eq!(
frames[1].timestamp,
Timestamp::from_micros(3_000_000).unwrap(),
"not re-anchored on the new PES"
);
}
#[tokio::test(start_paused = true)]
async fn aac_drains_a_joined_frame_at_end_of_stream() {
const AAC_PID: u16 = 0x0060;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let pmt = synth_pmt(&[(StreamType::AdtsAac, AAC_PID)], false);
import.decode(&bytes::BytesMut::from(&pmt[..])).unwrap();
let split = adts_frame(40, 0xBB);
let mut payload = adts_frame(40, 0xAA);
payload.extend_from_slice(&split[..25]);
import
.decode(audio_pes_packet(AAC_PID, 0, 90_000, &payload).as_slice())
.unwrap();
import
.decode(audio_pes_packet(AAC_PID, 1, 270_000, &split[25..]).as_slice())
.unwrap();
import.finish().unwrap();
let frames = read_audio_frames(&consumer, &catalog).await;
assert_eq!(
frames.iter().map(|f| f.payload.to_vec()).collect::<Vec<_>>(),
[0xAA, 0xBB].map(|fill| adts_frame(40, fill)[7..].to_vec()).to_vec(),
"the last frame was held for a confirmation that could never arrive"
);
}
fn mp2_frame(fill: u8) -> Vec<u8> {
let mut f = vec![0xFF, 0xF5, 0x18, 0xC0];
f.resize(72, fill);
f
}
#[tokio::test(start_paused = true)]
async fn legacy_drops_a_pes_completed_across_a_continuity_break() {
const MP2_PID: u16 = 0x0061;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let pmt = synth_pmt(&[(StreamType::Mpeg1Audio, MP2_PID)], false);
import.decode(&bytes::BytesMut::from(&pmt[..])).unwrap();
let mut opening = mp2_frame(0xAA);
opening.extend_from_slice(&mp2_frame(0xBB)[..40]);
import
.decode(audio_pes_open(MP2_PID, 0, 90_000, 144, &opening).as_slice())
.unwrap();
import
.decode(ts_continuation(MP2_PID, 12, &mp2_frame(0xCC)[..32]).as_slice())
.unwrap();
let mut normal = mp2_frame(0xDD);
normal.extend_from_slice(&mp2_frame(0xEE));
import
.decode(audio_pes_packet(MP2_PID, 13, 270_000, &normal).as_slice())
.unwrap();
import.finish().unwrap();
let frames = read_audio_frames(&consumer, &catalog).await;
assert_eq!(
frames.iter().map(|f| f.payload.clone()).collect::<Vec<_>>(),
vec![mp2_frame(0xAA), mp2_frame(0xDD), mp2_frame(0xEE)],
"the wrap was spliced onto the frame the cut left open"
);
}
#[tokio::test(start_paused = true)]
async fn legacy_resyncs_past_damaged_header() {
const MP2_PID: u16 = 0x0061;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let pmt = synth_pmt(&[(StreamType::Mpeg1Audio, MP2_PID)], false);
import.decode(&bytes::BytesMut::from(&pmt[..])).unwrap();
let mut payload = mp2_frame(0xAA);
payload.extend_from_slice(&mp2_frame(0xBB));
import
.decode(audio_pes_packet(MP2_PID, 0, 90_000, &payload).as_slice())
.unwrap();
let mut damaged = mp2_frame(0x11);
damaged[0] = 0xFE;
let mut hit = damaged.clone();
hit.extend_from_slice(&mp2_frame(0xCC));
import
.decode(audio_pes_packet(MP2_PID, 1, 270_000, &hit).as_slice())
.expect("a damaged frame header is not fatal");
import
.decode(audio_pes_packet(MP2_PID, 2, 450_000, &mp2_frame(0xDD)).as_slice())
.unwrap();
import.finish().unwrap();
let frames = read_audio_frames(&consumer, &catalog).await;
assert_eq!(
frames.iter().map(|f| f.payload.clone()).collect::<Vec<_>>(),
vec![mp2_frame(0xAA), mp2_frame(0xBB), mp2_frame(0xCC), mp2_frame(0xDD)],
"the undamaged frames either side survive, and only the damaged one is dropped"
);
assert_eq!(
import.stats().streams,
BTreeMap::from([(
MP2_PID,
super::StreamStats {
track: ".mp2",
units: 4,
quiet: None,
resyncs: 1,
discarded: 72,
unconfirmed: 0,
}
)]),
"the resync left no trace an operator could alarm on"
);
}
#[tokio::test(start_paused = true)]
async fn legacy_resyncs_past_stale_tail_at_a_splice() {
const MP2_PID: u16 = 0x0061;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let pmt = synth_pmt(&[(StreamType::Mpeg1Audio, MP2_PID)], false);
import.decode(&bytes::BytesMut::from(&pmt[..])).unwrap();
let mut cut = mp2_frame(0xAA);
cut.truncate(40);
import
.decode(audio_pes_packet(MP2_PID, 0, 90_000, &cut).as_slice())
.unwrap();
let mut wrapped = mp2_frame(0xCC);
wrapped.extend_from_slice(&mp2_frame(0xDD));
import
.decode(audio_pes_packet(MP2_PID, 1, 270_000, &wrapped).as_slice())
.expect("a splice is not fatal");
import
.decode(audio_pes_packet(MP2_PID, 2, 450_000, &mp2_frame(0xEE)).as_slice())
.unwrap();
import.finish().unwrap();
let frames = read_audio_frames(&consumer, &catalog).await;
assert_eq!(
frames.iter().map(|f| f.payload.clone()).collect::<Vec<_>>(),
vec![mp2_frame(0xCC), mp2_frame(0xDD), mp2_frame(0xEE)],
"the splice was published as audio instead of scanned past"
);
assert_eq!(
frames[0].timestamp,
Timestamp::from_micros(3_000_000).unwrap(),
"not re-anchored on the new PES"
);
}
#[tokio::test(start_paused = true)]
async fn legacy_confirms_a_frame_joined_out_of_a_carried_tail() {
const MP2_PID: u16 = 0x0061;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let pmt = synth_pmt(&[(StreamType::Mpeg1Audio, MP2_PID)], false);
import.decode(&bytes::BytesMut::from(&pmt[..])).unwrap();
let mut payload = mp2_frame(0xAA);
payload.extend_from_slice(&mp2_frame(0xBB)[..40]);
import
.decode(audio_pes_packet(MP2_PID, 0, 90_000, &payload).as_slice())
.unwrap();
let mut wrapped = mp2_frame(0xCC);
wrapped.extend_from_slice(&mp2_frame(0xDD));
import
.decode(audio_pes_packet(MP2_PID, 1, 270_000, &wrapped).as_slice())
.expect("a splice is not fatal");
import
.decode(audio_pes_packet(MP2_PID, 2, 450_000, &mp2_frame(0xEE)).as_slice())
.unwrap();
import.finish().unwrap();
let frames = read_audio_frames(&consumer, &catalog).await;
assert_eq!(
frames.iter().map(|f| f.payload.clone()).collect::<Vec<_>>(),
vec![mp2_frame(0xAA), mp2_frame(0xCC), mp2_frame(0xDD), mp2_frame(0xEE)],
"the splice cost more than the frame it interrupted"
);
assert_eq!(
frames[1].timestamp,
Timestamp::from_micros(3_000_000).unwrap(),
"not re-anchored on the new PES"
);
}
#[tokio::test(start_paused = true)]
async fn legacy_resync_below_the_live_edge_publishes_forward() {
const MP2_PID: u16 = 0x0061;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let pmt = synth_pmt(&[(StreamType::Mpeg1Audio, MP2_PID)], false);
import.decode(&bytes::BytesMut::from(&pmt[..])).unwrap();
let mut first = mp2_frame(0xAA);
first.extend_from_slice(&mp2_frame(0xBB));
import
.decode(audio_pes_packet(MP2_PID, 0, 90_000, &first).as_slice())
.unwrap();
let mut join = mp2_frame(0xDD);
join.extend_from_slice(&mp2_frame(0xEE));
import
.decode(audio_pes_packet(MP2_PID, 1, 89_910, &join).as_slice())
.expect("a PES PTS below the live edge must not abort the import");
import.finish().unwrap();
let frames = read_audio_frames(&consumer, &catalog).await;
assert!(
frames.len() >= 3,
"expected published frames after the join, got {}",
frames.len()
);
let mut last = 0;
for frame in &frames {
assert!(
frame.timestamp.as_micros() >= last,
"published a rewind: {last} then {}",
frame.timestamp.as_micros()
);
last = frame.timestamp.as_micros();
}
}
#[tokio::test(start_paused = true)]
async fn legacy_drains_a_joined_frame_at_end_of_stream() {
const MP2_PID: u16 = 0x0061;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let pmt = synth_pmt(&[(StreamType::Mpeg1Audio, MP2_PID)], false);
import.decode(&bytes::BytesMut::from(&pmt[..])).unwrap();
let mut payload = mp2_frame(0xAA);
payload.extend_from_slice(&mp2_frame(0xBB)[..40]);
import
.decode(audio_pes_packet(MP2_PID, 0, 90_000, &payload).as_slice())
.unwrap();
import
.decode(audio_pes_packet(MP2_PID, 1, 270_000, &mp2_frame(0xBB)[40..]).as_slice())
.unwrap();
import.finish().unwrap();
let frames = read_audio_frames(&consumer, &catalog).await;
assert_eq!(
frames.iter().map(|f| f.payload.clone()).collect::<Vec<_>>(),
vec![mp2_frame(0xAA), mp2_frame(0xBB)],
"the last frame was held for a confirmation that could never arrive"
);
assert_eq!(
import.stats().streams,
BTreeMap::from([(
MP2_PID,
super::StreamStats {
track: ".mp2",
units: 2,
quiet: None,
resyncs: 0,
discarded: 0,
unconfirmed: 1,
}
)]),
"the drained frame was published without a trace"
);
}
#[tokio::test(start_paused = true)]
async fn legacy_rejects_an_unconfirmed_sync_candidate() {
const MP2_PID: u16 = 0x0061;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let pmt = synth_pmt(&[(StreamType::Mpeg1Audio, MP2_PID)], false);
import.decode(&bytes::BytesMut::from(&pmt[..])).unwrap();
let mut payload = mp2_frame(0xAA);
payload.extend_from_slice(&mp2_frame(0xBB));
import
.decode(audio_pes_packet(MP2_PID, 0, 90_000, &payload).as_slice())
.unwrap();
let mut damaged = mp2_frame(0x11);
damaged[0] = 0xFE;
damaged[8..12].copy_from_slice(&mp2_frame(0)[..4]);
let mut hit = damaged.clone();
hit.extend_from_slice(&mp2_frame(0xCC));
import
.decode(audio_pes_packet(MP2_PID, 1, 270_000, &hit).as_slice())
.unwrap();
import
.decode(audio_pes_packet(MP2_PID, 2, 450_000, &mp2_frame(0xDD)).as_slice())
.unwrap();
import.finish().unwrap();
let frames = read_audio_frames(&consumer, &catalog).await;
assert_eq!(
frames.iter().map(|f| f.payload.clone()).collect::<Vec<_>>(),
vec![mp2_frame(0xAA), mp2_frame(0xBB), mp2_frame(0xCC), mp2_frame(0xDD)],
"a false sync inside the payload was published as audio"
);
}
#[tokio::test(start_paused = true)]
async fn legacy_confirms_the_first_frame_of_a_stream() {
const MP2_PID: u16 = 0x0061;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let pmt = synth_pmt(&[(StreamType::Mpeg1Audio, MP2_PID)], false);
import.decode(&bytes::BytesMut::from(&pmt[..])).unwrap();
let mut joined = mp2_frame(0)[..4].to_vec();
joined.resize(40, 0x11);
import
.decode(audio_pes_packet(MP2_PID, 0, 90_000, &joined).as_slice())
.unwrap();
let mut real = mp2_frame(0xCC);
real.extend_from_slice(&mp2_frame(0xDD));
import
.decode(audio_pes_packet(MP2_PID, 1, 270_000, &real).as_slice())
.unwrap();
import.finish().unwrap();
let frames = read_audio_frames(&consumer, &catalog).await;
assert!(
!frames.iter().any(|f| f.payload.ends_with(&[0x11; 8])),
"published the unvouched-for bytes the capture joined in the middle of"
);
assert_eq!(
frames.iter().map(|f| f.payload.clone()).collect::<Vec<_>>(),
vec![mp2_frame(0xCC), mp2_frame(0xDD)],
"the real stream is picked up once two frames chain"
);
}
#[tokio::test(start_paused = true)]
async fn legacy_confirms_the_first_frame_after_a_seek() {
const MP2_PID: u16 = 0x0061;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let pmt = synth_pmt(&[(StreamType::Mpeg1Audio, MP2_PID)], false);
import.decode(&bytes::BytesMut::from(&pmt[..])).unwrap();
let mut opening = mp2_frame(0xAA);
opening.extend_from_slice(&mp2_frame(0xBB));
import
.decode(audio_pes_packet(MP2_PID, 0, 90_000, &opening).as_slice())
.unwrap();
import.seek(10).unwrap();
let mut landed = mp2_frame(0)[..4].to_vec();
landed.resize(40, 0x11);
import
.decode(audio_pes_packet(MP2_PID, 1, 270_000, &landed).as_slice())
.unwrap();
let mut real = mp2_frame(0xCC);
real.extend_from_slice(&mp2_frame(0xDD));
import
.decode(audio_pes_packet(MP2_PID, 2, 450_000, &real).as_slice())
.unwrap();
import.finish().unwrap();
let frames = read_audio_frames(&consumer, &catalog).await;
assert_eq!(
frames.iter().map(|f| f.payload.clone()).collect::<Vec<_>>(),
vec![mp2_frame(0xAA), mp2_frame(0xBB), mp2_frame(0xCC), mp2_frame(0xDD)],
"published the unvouched-for bytes the seek landed in the middle of"
);
}
#[tokio::test(start_paused = true)]
async fn legacy_does_not_bill_a_frame_that_arrives_slowly() {
const MP2_PID: u16 = 0x0061;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let pmt = synth_pmt(&[(StreamType::Mpeg1Audio, MP2_PID)], false);
import.decode(&bytes::BytesMut::from(&pmt[..])).unwrap();
let mut frame = vec![0xFF, 0xFD, 0xE8, 0x00];
frame.resize(1728, 0xAA);
for (i, chunk) in frame.chunks(4).enumerate() {
import
.decode(audio_pes_packet(MP2_PID, (i % 16) as u8, 90_000, chunk).as_slice())
.expect("a slowly arriving frame must not exhaust the resync budget");
}
import
.decode(audio_pes_packet(MP2_PID, 0, 270_000, &frame[..4]).as_slice())
.unwrap();
import.finish().unwrap();
let frames = read_audio_frames(&consumer, &catalog).await;
assert_eq!(
frames.iter().map(|f| f.payload.clone()).collect::<Vec<_>>(),
vec![frame.clone()],
"the reassembled frame keeps its bytes"
);
assert_eq!(frames[0].timestamp, Timestamp::from_micros(1_000_000).unwrap());
}
#[tokio::test(start_paused = true)]
async fn a_pid_that_never_publishes_releases_the_catalog() {
const MP2_PID: u16 = 0x0061;
const AAC_PID: u16 = 0x0060;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let pmt = synth_pmt(
&[(StreamType::AdtsAac, AAC_PID), (StreamType::Mpeg1Audio, MP2_PID)],
false,
);
import.decode(&bytes::BytesMut::from(&pmt[..])).unwrap();
import
.decode(audio_pes_packet(MP2_PID, 0, 90_000, &mp2_frame(0xAA)).as_slice())
.unwrap();
let mut aac = adts_frame(40, 0xCC);
aac.extend_from_slice(&adts_frame(40, 0xDD));
import
.decode(audio_pes_packet(AAC_PID, 0, 90_000, &aac).as_slice())
.unwrap();
import.finish().unwrap();
let track = consumer
.track(hang::catalog::Catalog::DEFAULT_NAME)
.unwrap()
.subscribe(moq_net::track::Subscription::default().with_max_age(RECORDING_MAX_AGE))
.await
.unwrap();
let mut reader = crate::container::Consumer::new(
track,
crate::catalog::hang::Container::Legacy(crate::container::Kind::Data),
);
let published = tokio::time::timeout(Duration::from_millis(50), reader.read()).await;
assert!(
matches!(published, Ok(Ok(Some(_)))),
"a PID that never published held the catalog shut: {published:?}"
);
drop(import);
}
#[test]
fn legacy_gives_up_when_nothing_ever_parses() {
const MP2_PID: u16 = 0x0061;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let pmt = synth_pmt(&[(StreamType::Mpeg1Audio, MP2_PID)], false);
import.decode(&bytes::BytesMut::from(&pmt[..])).unwrap();
let header = mp2_frame(0);
let junk: Vec<u8> = (0..150)
.map(|i: usize| match i % 16 {
n @ 0..=3 => header[n],
_ => 0xBB,
})
.collect();
let err = (0..1000)
.map(|i| import.decode(audio_pes_packet(MP2_PID, (i % 16) as u8, 90_000, &junk).as_slice()))
.find_map(Result::err)
.expect("an unparseable stream must still fail");
assert!(
err.to_string().contains("never regained sync"),
"gave up with the wrong error: {err}"
);
}
#[tokio::test(start_paused = true)]
async fn legacy_seek_resets_the_resync_budget() {
const MP2_PID: u16 = 0x0061;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let pmt = synth_pmt(&[(StreamType::Mpeg1Audio, MP2_PID)], false);
import.decode(&bytes::BytesMut::from(&pmt[..])).unwrap();
let junk = vec![0x00u8; 150];
for i in 0..436 {
import
.decode(audio_pes_packet(MP2_PID, (i % 16) as u8, 90_000, &junk).as_slice())
.expect("still under the budget");
}
import.seek(10).unwrap();
for i in 0..2 {
import
.decode(audio_pes_packet(MP2_PID, i, 270_000, &junk).as_slice())
.expect("a seek must not inherit the pre-seek budget");
}
let mut good = mp2_frame(0xAA);
good.extend_from_slice(&mp2_frame(0xBB));
import
.decode(audio_pes_packet(MP2_PID, 2, 450_000, &good).as_slice())
.expect("a clean stream after a seek must not inherit the pre-seek budget");
import.finish().unwrap();
let frames = read_audio_frames(&consumer, &catalog).await;
assert_eq!(
frames.iter().map(|f| f.payload.clone()).collect::<Vec<_>>(),
vec![mp2_frame(0xAA), mp2_frame(0xBB)],
"the post-seek stream should publish normally"
);
}
#[test]
fn aac_gives_up_when_nothing_ever_parses() {
const AAC_PID: u16 = 0x0060;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let pmt = synth_pmt(&[(StreamType::AdtsAac, AAC_PID)], false);
import.decode(&bytes::BytesMut::from(&pmt[..])).unwrap();
let header = adts_frame(40, 0);
let junk: Vec<u8> = (0..150)
.map(|i: usize| match i % 16 {
n if n < super::adts::MIN_HEADER_LEN => header[n],
_ => 0xBB,
})
.collect();
let err = (0..1000)
.map(|i| import.decode(audio_pes_packet(AAC_PID, (i % 16) as u8, 90_000, &junk).as_slice()))
.find_map(Result::err)
.expect("an unparseable stream must still fail");
assert!(
err.to_string().contains("never regained sync"),
"gave up with the wrong error: {err}"
);
}
#[tokio::test(start_paused = true)]
async fn legacy_frame_split_across_pes_reassembles() {
const MP2_PID: u16 = 0x0061;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let pmt = synth_pmt(&[(StreamType::Mpeg1Audio, MP2_PID)], false);
import.decode(&bytes::BytesMut::from(&pmt[..])).unwrap();
let mut frame_a = vec![0xFF, 0xFD, 0x14, 0x00];
frame_a.extend((4..96).map(|i| i as u8));
let mut frame_b = vec![0xFF, 0xFD, 0x14, 0x00];
frame_b.extend((4..96).rev().map(|i| i as u8));
let mut second = frame_a[50..].to_vec();
second.extend_from_slice(&frame_b);
import
.decode(audio_pes_packet(MP2_PID, 0, 90_000, &frame_a[..50]).as_slice())
.unwrap();
import
.decode(audio_pes_packet(MP2_PID, 1, 270_000, &second).as_slice())
.unwrap();
import.finish().unwrap();
let frames = read_audio_frames(&consumer, &catalog).await;
assert_eq!(frames.len(), 2, "both frames must survive the split");
assert_eq!(
frames[0].payload.as_ref(),
&frame_a[..],
"frame A reassembled byte-exact"
);
assert_eq!(frames[1].payload.as_ref(), &frame_b[..], "frame B intact");
assert_eq!(frames[0].timestamp, Timestamp::from_micros(1_000_000).unwrap());
assert_eq!(frames[1].timestamp, Timestamp::from_micros(3_000_000).unwrap());
}
#[tokio::test(start_paused = true)]
async fn legacy_header_split_keeps_origin_pts() {
const MP2_PID: u16 = 0x0061;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let pmt = synth_pmt(&[(StreamType::Mpeg1Audio, MP2_PID)], false);
import.decode(&bytes::BytesMut::from(&pmt[..])).unwrap();
let mut frame_a = vec![0xFF, 0xFD, 0x14, 0x00];
frame_a.resize(96, 0x55);
let mut frame_b = vec![0xFF, 0xFD, 0x14, 0x00];
frame_b.resize(96, 0x66);
let mut first = frame_a.clone();
first.extend_from_slice(&frame_b[..2]);
import
.decode(audio_pes_packet(MP2_PID, 0, 90_000, &first).as_slice())
.unwrap();
import
.decode(audio_pes_packet(MP2_PID, 1, 900_000, &frame_b[2..]).as_slice())
.unwrap();
import.finish().unwrap();
let frames = read_audio_frames(&consumer, &catalog).await;
assert_eq!(frames.len(), 2, "both frames must survive the header split");
assert_eq!(
frames[1].payload.as_ref(),
&frame_b[..],
"frame B reassembled byte-exact"
);
assert_eq!(frames[1].timestamp, Timestamp::from_micros(1_024_000).unwrap());
}
#[tokio::test(start_paused = true)]
async fn scte35_cue_stamped_with_video_pts() {
use crate::catalog::hang::{Catalog, Container};
use crate::container::Consumer;
use crate::container::ts::catalog::Ext;
use moq_net::Timestamp;
const VIDEO_PID: u16 = 0x0050;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(
&mut broadcast,
crate::catalog::Config::default().with_catalog(Catalog::<Ext>::default()),
)
.unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let mut bytes = bytes::BytesMut::new();
bytes.extend_from_slice(&synth_pmt(
&[
(StreamType::Mpeg2Video, VIDEO_PID),
(StreamType::Dts8ChannelLosslessAudio, 0x21),
],
true,
));
bytes.extend_from_slice(&pes_packet(VIDEO_PID, 90_000)); bytes.extend_from_slice(&packet(true, 0, 0, &CUE)); import.decode(&bytes).unwrap();
let clock = import.last_pts.expect("video set the media clock");
import.finish().unwrap();
let name = catalog.snapshot().ext.mpegts.tracks.keys().next().unwrap().clone();
let track = consumer
.track(&name)
.unwrap()
.subscribe(moq_net::track::Subscription::default().with_max_age(RECORDING_MAX_AGE))
.await
.unwrap();
let mut reader = Consumer::new(track, Container::Legacy(crate::container::Kind::Data));
let frame = tokio::time::timeout(Duration::from_secs(1), reader.read())
.await
.expect("cue read timed out")
.unwrap()
.expect("a published cue frame");
assert_eq!(&frame.payload[..], &CUE[..], "verbatim splice_info_section");
assert_ne!(frame.timestamp, Timestamp::ZERO, "cue must not stamp zero");
assert_eq!(
Duration::from(frame.timestamp),
Duration::from(clock),
"cue stamped with the video media clock"
);
}
#[test]
fn section_pid_without_cuei_is_dropped_not_cataloged() {
use crate::catalog::hang::Catalog;
use crate::container::ts::catalog::Ext;
const VIDEO_PID: u16 = 0x0050;
const SECTION_PID: u16 = 0x0021;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = crate::catalog::Producer::new(
&mut broadcast,
crate::catalog::Config::default().with_catalog(Catalog::<Ext>::default()),
)
.unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let mut bytes = bytes::BytesMut::new();
bytes.extend_from_slice(&synth_pmt(
&[
(StreamType::Mpeg2Video, VIDEO_PID),
(StreamType::Dts8ChannelLosslessAudio, SECTION_PID),
],
false,
));
bytes.extend_from_slice(&packet(true, 0, 0, &CUE)); bytes.extend_from_slice(&pes_packet(VIDEO_PID, 90_000)); import.decode(&bytes).unwrap();
assert!(
import.last_pts.is_some(),
"video kept importing past the dropped section PID"
);
assert!(
catalog.snapshot().ext.mpegts.tracks.is_empty(),
"a 0x86 PID without CUEI must not be cataloged"
);
}
#[test]
fn tei_pusi_section_is_dropped() {
let mut corrupt = packet(true, 0, 0, &CUE);
corrupt[1] |= 0x80; let clean = packet(true, 1, 0, &CUE);
assert_eq!(
run(&[corrupt, clean]),
vec![CUE.to_vec()],
"a section was emitted from a packet flagged corrupt"
);
}
#[test]
fn duplicate_mid_section_packet_is_skipped() {
let section = fake_section(0xfc, 400); let p1 = packet(true, 0, 0, §ion[..183]);
let p2 = packet(false, 1, 0, §ion[183..367]);
let p3 = packet(false, 2, 0, §ion[367..]);
assert_eq!(run(&[p1, p2.clone(), p2, p3]), vec![section]);
}
#[test]
fn duplicate_with_refreshed_clock_is_skipped() {
let old = [0x00, 0x00, 0x00, 0x00, 0x7e, 0x00];
let refreshed = [0x00, 0x00, 0x00, 0x01, 0x7e, 0x00];
for (name, pcr, opcr) in [("PCR", Some(old), None), ("OPCR", None, Some(old))] {
let section = fake_section(0xfc, 390);
let p1 = packet(true, 0, 0, §ion[..183]);
let p2 = clock_packet(1, pcr, opcr, §ion[183..359]);
let duplicate = clock_packet(1, pcr.map(|_| refreshed), opcr.map(|_| refreshed), §ion[183..359]);
let p3 = packet(false, 2, 0, §ion[359..]);
assert_eq!(run(&[p1, p2, duplicate, p3]), vec![section], "refreshed {name}");
}
}
#[test]
fn repeated_counter_after_fifteen_lost_packets_is_broken() {
let mut continuity = Continuity::default();
let previous: [u8; 188] = packet(false, 5, 0, &[0x11; 184]).try_into().unwrap();
let after_loss: [u8; 188] = packet(false, 5, 0, &[0x22; 184]).try_into().unwrap();
assert!(matches!(continuity.observe(&previous), Continuation::Contiguous));
assert!(matches!(continuity.observe(&after_loss), Continuation::Broken));
}
#[test]
fn duplicate_pusi_packet_emits_once() {
let p = packet(true, 0, 0, &CUE);
assert_eq!(run(&[p.clone(), p]), vec![CUE.to_vec()]);
}
#[test]
fn tei_continuation_drops_partial_and_resyncs() {
let section = fake_section(0xfc, 247);
let p1 = packet(true, 0, 0, §ion[..183]);
let mut p2 = packet(false, 1, 0, §ion[183..]);
p2[1] |= 0x80; let p3 = packet(false, 2, 0, &CUE); let p4 = packet(true, 3, 0, &CUE); assert_eq!(run(&[p1, p2, p3, p4]), vec![CUE.to_vec()]);
}
#[tokio::test(start_paused = true)]
async fn private_pes_carried_verbatim() {
use crate::catalog::hang::{Catalog, Container};
use crate::container::Consumer;
use crate::container::ts::catalog::{Ext, Framing};
const VIDEO_PID: u16 = 0x0050;
const DATA_PID: u16 = 0x0052;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(
&mut broadcast,
crate::catalog::Config::default().with_catalog(Catalog::<Ext>::default()),
)
.unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let mut bytes = bytes::BytesMut::new();
bytes.extend_from_slice(&synth_pmt(
&[
(StreamType::Mpeg2Video, VIDEO_PID),
(StreamType::Mpeg2PacketizedData, DATA_PID),
],
false,
));
bytes.extend_from_slice(&pes_packet(VIDEO_PID, 90_000)); let payload = [0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02];
bytes.extend_from_slice(&audio_pes_packet(DATA_PID, 0, 90_000, &payload));
import.decode(&bytes).unwrap();
import.finish().unwrap();
let snap = catalog.snapshot();
assert_eq!(
snap.ext.mpegts.tracks.len(),
1,
"the private PES PID is carried verbatim"
);
let (name, track) = snap.ext.mpegts.tracks.iter().next().unwrap();
let verbatim = track.verbatim.as_ref().expect("a verbatim carriage record");
assert_eq!(verbatim.stream_type, 0x06, "recorded the PMT stream_type");
assert_eq!(verbatim.framing, Framing::Pes, "private PES is PES-framed");
assert_eq!(verbatim.stream_id, Some(0xC0), "recorded the PES stream_id");
assert_eq!(track.pid, DATA_PID, "recorded the original PID");
let track = consumer
.track(name.as_str())
.unwrap()
.subscribe(moq_net::track::Subscription::default().with_max_age(RECORDING_MAX_AGE))
.await
.unwrap();
let mut reader = Consumer::new(track, Container::Legacy(crate::container::Kind::Data));
let frame = tokio::time::timeout(Duration::from_secs(1), reader.read())
.await
.expect("verbatim read timed out")
.unwrap()
.expect("a published verbatim frame");
assert_eq!(&frame.payload[..], &payload[..], "verbatim PES payload round-trips");
}
fn clock_break_packet(pid: u16) -> Vec<u8> {
let mut p = vec![
0x47,
(pid >> 8) as u8 & 0x1f,
(pid & 0xff) as u8,
0x20,
183, 0x80,
];
p.resize(188, 0xff);
p
}
fn flag_discontinuity(mut pkt: Vec<u8>) -> Vec<u8> {
assert!(
pkt[3] & 0x20 != 0 && pkt[4] > 0,
"packet has no adaptation field to flag"
);
pkt[5] |= 0x80;
pkt
}
fn mp2_pes(pid: u16, cc: u8, pts: u64, fills: [u8; 2]) -> Vec<u8> {
let mut payload = mp2_frame(fills[0]);
payload.extend_from_slice(&mp2_frame(fills[1]));
audio_pes_packet(pid, cc, pts, &payload)
}
async fn read_breaks(consumer: &moq_net::broadcast::Consumer, name: &str) -> (Vec<crate::container::Frame>, u64) {
let subscription = moq_net::track::Subscription::default().with_max_age(std::time::Duration::from_secs(3600));
let track = consumer.track(name).unwrap().subscribe(subscription).await.unwrap();
let mut reader = crate::container::Consumer::new(
track,
crate::catalog::hang::Container::Legacy(crate::container::Kind::Audio),
);
let mut frames = Vec::new();
while let Ok(Ok(Some(frame))) = tokio::time::timeout(std::time::Duration::from_millis(50), reader.read()).await
{
frames.push(frame);
}
(frames, reader.discontinuity())
}
async fn read_all_breaks(
consumer: &moq_net::broadcast::Consumer,
catalog: &crate::catalog::Producer,
) -> Vec<(Vec<crate::container::Frame>, u64)> {
let names: Vec<String> = catalog.snapshot().audio.renditions.keys().cloned().collect();
let mut out = Vec::new();
for name in names {
out.push(read_breaks(consumer, &name).await);
}
assert_eq!(out.len(), 2, "both renditions must exist");
out
}
pub(in crate::container::ts) fn two_programs() -> Vec<u8> {
let mut data = synth_programs(
&[
(1, 0x0100, &[(StreamType::Mpeg1Audio, 0x0061)]),
(2, 0x0200, &[(StreamType::Mpeg1Audio, 0x0071)]),
],
false,
);
data.extend(mp2_pes(0x0061, 0, 90_000, [0xAA, 0xBB]));
data.extend(mp2_pes(0x0071, 0, 3_601 * 90_000, [0xCC, 0xDD]));
data
}
#[test]
fn a_multi_program_input_is_refused_before_publishing() {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let err = import.decode(&two_programs()).unwrap_err();
assert_eq!(
err.downcast_ref::<super::MultipleProgramsError>(),
Some(&super::MultipleProgramsError { programs: vec![1, 2] }),
"{err:#}"
);
assert!(catalog.snapshot().audio.renditions.is_empty(), "nothing was published");
}
#[test]
fn a_program_added_mid_stream_ends_the_import() {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
import
.decode(&synth_pmt(&[(StreamType::Mpeg1Audio, 0x0061)], false))
.unwrap();
import.decode(&mp2_pes(0x0061, 0, 90_000, [0xAA, 0xBB])).unwrap();
let err = import.decode(&two_programs()).unwrap_err();
assert!(err.downcast_ref::<super::MultipleProgramsError>().is_some(), "{err:#}");
assert_eq!(
catalog.snapshot().audio.renditions.len(),
1,
"program 1 stays published"
);
}
#[tokio::test(start_paused = true)]
async fn a_selected_program_publishes_only_its_streams_on_its_clock() {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve()).with_program(2);
import.decode(&two_programs()).unwrap();
import.finish().unwrap();
let snapshot = catalog.snapshot();
let names: Vec<_> = snapshot.audio.renditions.keys().collect();
assert_eq!(names.len(), 1, "only program 2's stream: {names:?}");
let (frames, _) = read_breaks(&consumer, names[0]).await;
assert!(!frames.is_empty(), "program 2 published");
assert!(
frames
.iter()
.all(|frame| frame.payload[4] == 0xCC || frame.payload[4] == 0xDD),
"no program 1 frame leaks in"
);
assert_eq!(frames[0].timestamp.as_micros(), 3_601_000_000, "program 2's own PTS");
}
#[test]
fn a_selected_program_the_pat_does_not_list_is_refused() {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve()).with_program(3);
let err = import.decode(&two_programs()).unwrap_err().to_string();
assert!(err.contains("no program 3") && err.contains("1, 2"), "{err}");
}
pub(in crate::container::ts) fn psi_packets(pid: u16, cc: &mut u8, tail: &[u8], section: &[u8]) -> Vec<u8> {
let mut payload = vec![tail.len() as u8];
payload.extend_from_slice(tail);
payload.extend_from_slice(section);
let mut out = Vec::new();
for (i, chunk) in payload.chunks(184).enumerate() {
let pusi = if i == 0 { 0x40 } else { 0 };
out.extend_from_slice(&[0x47, pusi | (pid >> 8) as u8, pid as u8, 0x10 | *cc]);
out.extend_from_slice(chunk);
out.resize(out.len().next_multiple_of(188), 0xff);
*cc = (*cc + 1) & 0x0f;
}
out
}
pub(in crate::container::ts) fn pat_section(programs: &[(u16, u16)]) -> Vec<u8> {
let body: Vec<u8> = programs
.iter()
.flat_map(|&(number, pid)| [(number >> 8) as u8, number as u8, 0xe0 | (pid >> 8) as u8, pid as u8])
.collect();
super::psi::section(0x00, 1, 0, 0, 0, &body)
}
fn pmt_section(program: u16, version: u8, streams: &[(u8, u16, &[u8])]) -> Vec<u8> {
let pcr = streams.first().map_or(0x1fff, |&(_, pid, _)| pid);
let mut body = vec![0xe0 | (pcr >> 8) as u8, pcr as u8, 0xf0, 0x00];
for &(stream_type, pid, descriptors) in streams {
body.extend_from_slice(&[
stream_type,
0xe0 | (pid >> 8) as u8,
pid as u8,
0xf0,
descriptors.len() as u8,
]);
body.extend_from_slice(descriptors);
}
super::psi::section(0x02, program, version, 0, 0, &body)
}
pub(in crate::container::ts) fn corrupt(mut section: Vec<u8>) -> Vec<u8> {
*section.last_mut().unwrap() ^= 0x01;
section
}
const MP2: u8 = StreamType::Mpeg1Audio as u8;
pub(in crate::container::ts) fn fifty_programs() -> Vec<u8> {
let programs: Vec<_> = (1..=50).map(|n| (n, 0x0100 + n)).collect();
let pat = pat_section(&programs);
let mut data = psi_packets(0, &mut 0, &[], &pat);
assert_eq!(data.len(), 2 * 188, "the PAT spans two packets");
data.extend(psi_packets(
0x0132,
&mut 0,
&[],
&pmt_section(50, 0, &[(MP2, 0x0061, &[])]),
));
data.extend(mp2_pes(0x0061, 0, 90_000, [0xAA, 0xBB]));
data
}
pub(in crate::container::ts) fn two_section_pat() -> Vec<u8> {
let mut cc = 0;
let mut data = Vec::new();
for (number, program) in [(0, 1u16), (1, 2)] {
let entry = [0x00, program as u8, 0xe0 | program as u8, 0x00];
let section = super::psi::section(0x00, 1, 0, number, 1, &entry);
data.extend(psi_packets(0, &mut cc, &[], §ion));
}
for (program, pid, fills) in [(1u16, 0x0061, [0xAA, 0xBB]), (2, 0x0071, [0xCC, 0xDD])] {
let pmt = pmt_section(program, 0, &[(MP2, pid, &[])]);
data.extend(psi_packets(program << 8, &mut 0, &[], &pmt));
data.extend(mp2_pes(pid, 0, 90_000, fills));
}
data
}
fn psi_import() -> (crate::catalog::Producer, super::Import) {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let import = super::Import::new(broadcast, catalog.reserve());
(catalog, import)
}
#[test]
fn a_pmt_spanning_two_packets_is_read_whole() {
let (catalog, mut import) = psi_import();
let mut descriptor = vec![0x80, 200];
descriptor.resize(202, 0x00);
let pmt = pmt_section(1, 0, &[(MP2, 0x0061, &descriptor), (MP2, 0x0062, &[])]);
let mut data = psi_packets(0, &mut 0, &[], &pat_section(&[(1, 0x0100)]));
data.extend(psi_packets(0x0100, &mut 0, &[], &pmt));
assert_eq!(data.len(), 3 * 188, "the PMT spans two packets");
data.extend(mp2_pes(0x0061, 0, 90_000, [0xAA, 0xBB]));
data.extend(mp2_pes(0x0062, 0, 90_000, [0xCC, 0xDD]));
import.decode(&data).unwrap();
import.finish().unwrap();
assert_eq!(catalog.snapshot().audio.renditions.len(), 2, "both streams publish");
assert_eq!(import.stats().crc_error, 0);
}
#[test]
fn a_pat_behind_a_nonzero_pointer_field_is_read() {
let (catalog, mut import) = psi_import();
let mut data = psi_packets(0, &mut 0, &[0xab; 7], &pat_section(&[(1, 0x0100)]));
data.extend(psi_packets(
0x0100,
&mut 0,
&[],
&pmt_section(1, 0, &[(MP2, 0x0061, &[])]),
));
data.extend(mp2_pes(0x0061, 0, 90_000, [0xAA, 0xBB]));
import.decode(&data).unwrap();
import.finish().unwrap();
assert_eq!(catalog.snapshot().audio.renditions.len(), 1);
}
#[test]
fn a_program_listed_in_a_pats_second_packet_is_selected() {
let (catalog, import) = psi_import();
let mut import = import.with_program(50);
import.decode(&fifty_programs()).unwrap();
import.finish().unwrap();
assert_eq!(catalog.snapshot().audio.renditions.len(), 1, "program 50 publishes");
let (_, mut import) = psi_import();
let err = import.decode(&fifty_programs()).unwrap_err();
let err = err.downcast_ref::<super::MultipleProgramsError>().unwrap();
assert_eq!(err.programs, (1..=50).collect::<Vec<_>>());
}
#[test]
fn corrupt_psi_between_good_repetitions_is_dropped_and_counted() {
let (catalog, mut import) = psi_import();
let (mut pat_cc, mut pmt_cc) = (0, 0);
let pat = pat_section(&[(1, 0x0100)]);
let pmt = pmt_section(1, 0, &[(MP2, 0x0061, &[])]);
let two = [(MP2, 0x0061, &[][..]), (MP2, 0x0062, &[][..])];
let mut data = psi_packets(0, &mut pat_cc, &[], &pat);
data.extend(psi_packets(0x0100, &mut pmt_cc, &[], &pmt));
data.extend(mp2_pes(0x0061, 0, 90_000, [0xAA, 0xBB]));
import.decode(&data).unwrap();
let units = import.stats().streams[&0x0061].units;
let mut data = psi_packets(0, &mut pat_cc, &[], &corrupt(pat_section(&[(1, 0x0100), (2, 0x0200)])));
data.extend(psi_packets(0x0100, &mut pmt_cc, &[], &corrupt(pmt_section(1, 1, &two))));
data.extend(psi_packets(0, &mut pat_cc, &[], &pat));
data.extend(psi_packets(0x0100, &mut pmt_cc, &[], &pmt));
data.extend(mp2_pes(0x0061, 1, 180_000, [0xAA, 0xBB]));
import.decode(&data).unwrap();
assert_eq!(
catalog.snapshot().audio.renditions.len(),
1,
"the corrupt PMT added nothing"
);
assert!(
import.stats().streams[&0x0061].units > units,
"the stream kept delivering"
);
assert_eq!(import.crc_errors(), &BTreeMap::from([(0x0000, 1), (0x0100, 1)]));
assert_eq!(import.stats().crc_error, 2);
let mut data = psi_packets(0x0100, &mut pmt_cc, &[], &pmt_section(1, 2, &two));
data.extend(mp2_pes(0x0062, 0, 180_000, [0xCC, 0xDD]));
import.decode(&data).unwrap();
import.finish().unwrap();
assert_eq!(
catalog.snapshot().audio.renditions.len(),
2,
"the good revision applied"
);
assert_eq!(import.stats().crc_error, 2, "good sections count nothing");
}
#[test]
fn a_feed_whose_only_pat_is_corrupt_publishes_nothing() {
let (catalog, mut import) = psi_import();
let mut data = psi_packets(0, &mut 0, &[], &corrupt(pat_section(&[(1, 0x0100)])));
data.extend(psi_packets(
0x0100,
&mut 0,
&[],
&pmt_section(1, 0, &[(MP2, 0x0061, &[])]),
));
data.extend(mp2_pes(0x0061, 0, 90_000, [0xAA, 0xBB]));
import.decode(&data).unwrap();
import.finish().unwrap();
assert!(catalog.snapshot().audio.renditions.is_empty());
assert_eq!(import.stats().crc_error, 1);
assert!(!import.stats().is_empty(), "a dropped section is something to report");
}
#[test]
fn adaptation_field_past_the_packet_drops_partial() {
let section = fake_section(0xfc, 300);
let mut malformed = packet(false, 1, 0, §ion[183..]);
malformed[3] = 0x31;
malformed[4] = 200;
let rest = packet(false, 2, 0, §ion[183..]);
assert!(run(&[packet(true, 0, 0, §ion[..183]), malformed, rest]).is_empty());
}
const PCR_PID: u16 = 0x0061;
const PEER_PID: u16 = 0x0062;
fn two_stream_import() -> (moq_net::broadcast::Consumer, crate::catalog::Producer, super::Import) {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let pmt = synth_pmt(
&[(StreamType::Mpeg1Audio, PCR_PID), (StreamType::Mpeg1Audio, PEER_PID)],
false,
);
import.decode(&bytes::BytesMut::from(&pmt[..])).unwrap();
(consumer, catalog, import)
}
#[test]
fn timebase_reset_clears_unpublished_pending() {
let (_, _, mut import) = two_stream_import();
let payload = mp2_frame(0xAA);
import
.decode(audio_pes_open(PEER_PID, 0, 90_000, payload.len() * 2, &payload[..20]).as_slice())
.unwrap();
assert!(!import.published);
assert!(!import.pending.is_empty());
import.decode(clock_break_packet(PCR_PID).as_slice()).unwrap();
assert!(import.pending.is_empty(), "old PES survived the timebase reset");
}
#[test]
fn timebase_reset_clears_section_clock() {
let (_, _, mut import) = two_stream_import();
import.last_pts = Some(Timestamp::from_micros(45_000_000).unwrap());
import.published = true;
import.decode(clock_break_packet(PCR_PID).as_slice()).unwrap();
assert!(import.last_pts.is_none(), "section clock belongs to the old timebase");
}
#[tokio::test(start_paused = true)]
async fn timebase_reset_after_section_only_publication() {
use crate::catalog::hang::Catalog;
use crate::container::ts::catalog::Ext;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(
&mut broadcast,
crate::catalog::Config::default().with_catalog(Catalog::<Ext>::default()),
)
.unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
import
.decode(
synth_pmt(
&[
(StreamType::Mpeg2Video, PCR_PID),
(StreamType::Dts8ChannelLosslessAudio, 0x21),
],
true,
)
.as_slice(),
)
.unwrap();
import.decode(packet(true, 0, 0, &CUE).as_slice()).unwrap();
import.decode(clock_break_packet(PCR_PID).as_slice()).unwrap();
import.decode(packet(true, 1, 0, &CUE).as_slice()).unwrap();
import.decode(clock_break_packet(PCR_PID).as_slice()).unwrap();
import.decode(packet(true, 2, 0, &CUE).as_slice()).unwrap();
import.finish().unwrap();
let name = catalog.snapshot().ext.mpegts.tracks.keys().next().unwrap().clone();
let (frames, breaks) = read_breaks(&consumer, &name).await;
assert_eq!(frames.len(), 3);
assert_eq!(
breaks, 0,
"a data sequence hole whose timestamps do not jump is not a playhead event"
);
}
#[tokio::test(start_paused = true)]
async fn duplicate_payload_clock_packet_declares_one_break() {
let (consumer, catalog, mut import) = two_stream_import();
for pid in [PCR_PID, PEER_PID] {
import.decode(mp2_pes(pid, 0, 90_000, [0xAA, 0xBB]).as_slice()).unwrap();
}
let flagged = flag_discontinuity(mp2_pes(PCR_PID, 1, 180_000, [0xCC, 0xDD]));
import.decode(flagged.as_slice()).unwrap();
import.decode(flagged.as_slice()).unwrap();
for pid in [PCR_PID, PEER_PID] {
let cc = if pid == PCR_PID { 2 } else { 1 };
import
.decode(mp2_pes(pid, cc, 270_000, [0xEE, 0xFF]).as_slice())
.unwrap();
}
import.finish().unwrap();
for (_, breaks) in read_all_breaks(&consumer, &catalog).await {
assert_eq!(breaks, 1, "a retransmitted packet declared another break");
}
}
#[tokio::test(start_paused = true)]
async fn incomplete_media_does_not_declare_another_break() {
let (consumer, catalog, mut import) = two_stream_import();
for pid in [PCR_PID, PEER_PID] {
import.decode(mp2_pes(pid, 0, 90_000, [0xAA, 0xBB]).as_slice()).unwrap();
}
import.decode(clock_break_packet(PCR_PID).as_slice()).unwrap();
import
.decode(audio_pes_packet(PEER_PID, 1, 180_000, &mp2_frame(0xCC)[..20]).as_slice())
.unwrap();
import.decode(clock_break_packet(PCR_PID).as_slice()).unwrap();
for pid in [PCR_PID, PEER_PID] {
let cc = if pid == PCR_PID { 1 } else { 2 };
import
.decode(mp2_pes(pid, cc, 270_000, [0xEE, 0xFF]).as_slice())
.unwrap();
}
import.finish().unwrap();
for (_, breaks) in read_all_breaks(&consumer, &catalog).await {
assert_eq!(breaks, 1, "an incomplete frame was counted as publication");
}
}
#[tokio::test(start_paused = true)]
async fn pcr_discontinuity_breaks_every_track() {
let (consumer, catalog, mut import) = two_stream_import();
for pid in [PCR_PID, PEER_PID] {
import.decode(mp2_pes(pid, 0, 90_000, [0xAA, 0xBB]).as_slice()).unwrap();
}
import.decode(clock_break_packet(PCR_PID).as_slice()).unwrap();
for pid in [PCR_PID, PEER_PID] {
import
.decode(mp2_pes(pid, 1, 90_000 + 20 * 90_000, [0xCC, 0xDD]).as_slice())
.unwrap();
}
import.finish().unwrap();
for (frames, breaks) in read_all_breaks(&consumer, &catalog).await {
assert_eq!(breaks, 1, "the timebase break did not reach this track");
let fills: Vec<u8> = frames.iter().map(|f| f.payload[4]).collect();
assert_eq!(fills, [0xAA, 0xBB, 0xCC, 0xDD], "media either side of the break");
assert_eq!(frames[2].timestamp.as_micros(), 21_000_000, "the new timeline");
}
}
#[tokio::test(start_paused = true)]
async fn a_backwards_restart_keeps_its_own_timestamps() {
let (consumer, catalog, mut import) = two_stream_import();
for pid in [PCR_PID, PEER_PID] {
import
.decode(mp2_pes(pid, 0, 45 * 90_000, [0xAA, 0xBB]).as_slice())
.unwrap();
}
import.decode(clock_break_packet(PCR_PID).as_slice()).unwrap();
for pid in [PCR_PID, PEER_PID] {
import.decode(mp2_pes(pid, 1, 90_000, [0xCC, 0xDD]).as_slice()).unwrap();
}
import.finish().unwrap();
for (frames, breaks) in read_all_breaks(&consumer, &catalog).await {
assert!(breaks >= 1, "the timebase break did not reach this track");
let stamps: Vec<u128> = frames.iter().map(|f| f.timestamp.as_micros()).collect();
assert_eq!(stamps[0], 45_000_000, "the old timeline");
assert!(
stamps.last().is_some_and(|&last| last >= 45_000_000),
"the restarted clock rewound instead of continuing forward: {stamps:?}"
);
}
}
fn pes_timestamp(prefix: u8, t: u64) -> [u8; 5] {
[
(prefix << 4) | (((t >> 30) & 0x07) << 1) as u8 | 0x01,
((t >> 22) & 0xff) as u8,
0x01 | (((t >> 15) & 0x7f) << 1) as u8,
((t >> 7) & 0xff) as u8,
0x01 | ((t & 0x7f) << 1) as u8,
]
}
fn video_pes(pid: u16, cc: u8, pts: u64, dts: Option<u64>, payload: &[u8]) -> Vec<u8> {
let mut header = Vec::new();
match dts {
Some(dts) => {
header.extend_from_slice(&[0x80, 0xC0, 10]);
header.extend_from_slice(&pes_timestamp(0b0011, pts));
header.extend_from_slice(&pes_timestamp(0b0001, dts));
}
None => {
header.extend_from_slice(&[0x80, 0x80, 5]);
header.extend_from_slice(&pes_timestamp(0b0010, pts));
}
}
let mut pes = vec![0x00, 0x00, 0x01, 0xe0];
let pes_len = header.len() + payload.len();
pes.push((pes_len >> 8) as u8);
pes.push((pes_len & 0xff) as u8);
pes.extend_from_slice(&header);
pes.extend_from_slice(payload);
let af_len = 184 - 1 - pes.len();
let mut p = vec![
0x47,
0x40 | ((pid >> 8) as u8 & 0x1f),
(pid & 0xff) as u8,
0x30 | (cc & 0x0f),
];
p.push(af_len as u8);
if af_len > 0 {
p.push(0x00);
p.extend(std::iter::repeat_n(0xff, af_len - 1));
}
p.extend_from_slice(&pes);
assert_eq!(p.len(), 188, "video PES packet must fill exactly one TS packet");
p
}
const FRAME: u64 = 3_000;
#[derive(Default)]
struct Mux {
out: Vec<u8>,
cc: std::collections::HashMap<u16, u8>,
}
impl Mux {
fn cc(&mut self, pid: u16) -> u8 {
let cc = self.cc.entry(pid).or_default();
let current = *cc;
*cc = (*cc + 1) & 0x0f;
current
}
fn gop(&mut self, pid: u16, base: u64) {
let frames = [
(true, base + FRAME, Some(base)),
(false, base + 4 * FRAME, Some(base + FRAME)),
(false, base + 2 * FRAME, None),
(false, base + 3 * FRAME, None),
];
for (keyframe, pts, dts) in frames {
let cc = self.cc(pid);
self.out
.extend_from_slice(&video_pes(pid, cc, pts, dts, &annexb_au(keyframe)));
}
}
fn gops(&mut self, pid: u16, base: u64, count: u64) {
for i in 0..count {
self.gop(pid, base + i * 4 * FRAME);
}
}
}
async fn read_track(
consumer: &moq_net::broadcast::Consumer,
name: &str,
kind: crate::container::Kind,
) -> Vec<crate::container::Frame> {
let subscription = moq_net::track::Subscription::default().with_max_age(Duration::from_secs(3600));
let track = consumer.track(name).unwrap().subscribe(subscription).await.unwrap();
let mut reader = crate::container::Consumer::new(track, crate::catalog::hang::Container::Legacy(kind));
let mut frames = Vec::new();
while let Ok(Ok(Some(frame))) = tokio::time::timeout(Duration::from_millis(50), reader.read()).await {
if !frame.payload.is_empty() {
frames.push(frame);
}
}
frames
}
#[allow(clippy::type_complexity)]
fn import_all(
data: &[u8],
) -> anyhow::Result<(
moq_net::broadcast::Consumer,
crate::catalog::Producer<Ext>,
super::Import<Ext>,
)> {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(
&mut broadcast,
crate::catalog::Config::default().with_catalog(crate::catalog::hang::Catalog::<Ext>::default()),
)
.unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
import.decode(data)?;
import.finish()?;
Ok((consumer, catalog, import))
}
use crate::container::ts::catalog::Ext;
fn assert_forward(frames: &[crate::container::Frame]) {
let (mut edge, mut open) = (0, 0);
for frame in frames {
let ts = frame.timestamp.as_micros();
if frame.keyframe {
edge = edge.max(open);
}
assert!(ts >= edge, "rewound to {ts} below the edge {edge}");
open = open.max(ts);
}
}
async fn video_frames(data: &[u8]) -> Vec<crate::container::Frame> {
let (consumer, catalog, _import) = import_all(data).expect("the import must survive the step back");
let name = catalog.snapshot().video.renditions.keys().next().unwrap().clone();
read_track(&consumer, &name, crate::container::Kind::Video).await
}
const VIDEO: u16 = 0x0050;
async fn damaged_video_recovers(damage: &str) {
let peer = VIDEO + 1;
let mut mux = Mux {
out: synth_pmt(&[(StreamType::H264, VIDEO), (StreamType::H264, peer)], false),
..Default::default()
};
mux.gop(VIDEO, 90_000);
mux.gop(peer, 90_000);
let damaged_at = mux.out.len();
mux.gop(VIDEO, 90_000 + 4 * FRAME);
mux.gop(peer, 90_000 + 4 * FRAME);
let packet = &mut mux.out[damaged_at..damaged_at + TsPacket::SIZE];
let payload_at = 5 + usize::from(packet[4]);
match damage {
"pes" => packet[payload_at + 6..payload_at + 19].fill(0),
"nal" => {
let nal = packet
.windows(5)
.position(|bytes| bytes[..4] == [0, 0, 0, 1] && bytes[4] & 0x1f == 5)
.unwrap();
packet[nal + 4] |= 0x80;
}
"late-nal" => {
let mut au = annexb_au(true);
au.extend_from_slice(&[0, 0, 0, 1, 0xe1, 0x80]);
packet.copy_from_slice(&video_pes(VIDEO, 4, 90_000 + 5 * FRAME, Some(90_000 + 4 * FRAME), &au));
}
"queued" => {
let mut au = annexb_au(true);
au.extend_from_slice(&[0, 0, 0, 1, 0x09, 0xf0, 0, 0, 0, 1, 0xe1, 0x80, 0, 0, 0, 1, 0x41, 0x9a]);
packet.copy_from_slice(&video_pes(VIDEO, 4, 90_000 + 5 * FRAME, Some(90_000 + 4 * FRAME), &au));
}
"adaptation" => packet[4] = 255,
"adaptation-clock" => {
packet[4] = 1;
packet[5] = 0x90;
}
"tei" => packet[1] |= 0x80,
"clean" => {}
_ => unreachable!(),
}
assert_eq!(
packet[1] & 0x80 != 0,
damage == "tei",
"only flagged corruption sets TEI"
);
let resume_at = mux.out.len();
mux.gop(VIDEO, 90_000 + 8 * FRAME);
mux.gop(peer, 90_000 + 8 * FRAME);
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
import
.decode(&mux.out[..resume_at])
.expect("damage must stay local to the unit");
assert!(import.scratch.is_empty(), "every damaged packet must be consumed");
assert!(
!import.pending.contains_key(&Pid::new(VIDEO).unwrap()),
"no damaged PES remains"
);
import
.decode(&mux.out[resume_at..])
.expect("next keyframe must recover");
import.finish().unwrap();
for (pid, expected) in [(VIDEO, if damage == "clean" { 12 } else { 8 }), (peer, 12)] {
let Stream::H264 { import: video, .. } = &import.streams[&Pid::new(pid).unwrap()] else {
panic!("video route");
};
let frames = read_track(&consumer, video.name(), crate::container::Kind::Video).await;
assert_eq!(frames.len(), expected, "published pictures on PID {pid}");
if pid == VIDEO && damage != "clean" {
assert!(frames[4].keyframe, "recovery starts at a keyframe");
assert_eq!(
frames[4].timestamp.as_micros(),
u128::from(90_000 + 9 * FRAME) * 1_000_000 / 90_000
);
}
}
}
#[tokio::test(start_paused = true)]
async fn damaged_pes_header_recovers() {
damaged_video_recovers("pes").await;
}
#[tokio::test(start_paused = true)]
async fn damaged_h264_nal_recovers() {
damaged_video_recovers("nal").await;
}
#[tokio::test(start_paused = true)]
async fn damaged_trailing_nal_refuses_the_whole_access_unit() {
damaged_video_recovers("late-nal").await;
}
#[tokio::test(start_paused = true)]
async fn damaged_unit_drops_a_picture_the_splitter_queued() {
damaged_video_recovers("queued").await;
}
#[tokio::test(start_paused = true)]
async fn damaged_adaptation_recovers() {
damaged_video_recovers("adaptation").await;
}
#[tokio::test(start_paused = true)]
async fn damaged_adaptation_cannot_reset_the_program_clock() {
damaged_video_recovers("adaptation-clock").await;
}
#[tokio::test(start_paused = true)]
async fn damaged_tei_recovers() {
damaged_video_recovers("tei").await;
}
#[tokio::test(start_paused = true)]
async fn clean_video_has_no_damage() {
damaged_video_recovers("clean").await;
}
async fn damaged_sps_keeps_the_last_good_parameter_sets(stream_type: StreamType) {
let (params, idr, delta): (&[&[u8]], &[u8], &[u8]) = match stream_type {
StreamType::H264 => {
use crate::container::test_util::{IDR, PPS, SPS};
(&[SPS, PPS], IDR, &[0x41, 0x9a, 0x00, 0x01])
}
StreamType::H265 => {
use crate::codec::h265::fixtures::{PPS, SPS, VPS};
(&[VPS, SPS, PPS], &[0x26, 0x01, 0x80, 0xaa], &[0x02, 0x01, 0x80, 0x33])
}
_ => unreachable!(),
};
let good_sps = params[params.len() - 2];
let truncated = &good_sps[..2];
let au = |nals: &[&[u8]]| {
let mut out = Vec::new();
for nal in nals {
out.extend_from_slice(&[0, 0, 0, 1]);
out.extend_from_slice(nal);
}
out
};
let mut keyframe = params.to_vec();
keyframe.push(idr);
let mut damaged = params.to_vec();
damaged[params.len() - 2] = truncated;
damaged.push(idr);
let units = [
au(&keyframe),
au(&[delta]),
au(&damaged),
au(&[delta]),
au(&[idr]),
au(&[delta]),
];
let mut data = synth_pmt(&[(stream_type, VIDEO)], false);
for (cc, unit) in units.iter().enumerate() {
let pts = 90_000 + cc as u64 * FRAME;
data.extend(video_pes(VIDEO, cc as u8, pts, None, unit));
}
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
import.decode(&data).expect("a damaged SPS stays local to its unit");
import.finish().unwrap();
let name = catalog.snapshot().video.renditions.keys().next().unwrap().clone();
let frames = read_track(&consumer, &name, crate::container::Kind::Video).await;
assert_eq!(frames.len(), 4, "the bare keyframe after the damage recovers");
assert!(frames[2].keyframe);
assert!(
frames[2].payload.windows(good_sps.len()).any(|bytes| bytes == good_sps),
"the recovery keyframe carries the last good SPS"
);
}
#[tokio::test(start_paused = true)]
async fn damaged_h264_sps_keeps_the_last_good_parameter_sets() {
damaged_sps_keeps_the_last_good_parameter_sets(StreamType::H264).await;
}
#[tokio::test(start_paused = true)]
async fn damaged_h265_sps_keeps_the_last_good_parameter_sets() {
damaged_sps_keeps_the_last_good_parameter_sets(StreamType::H265).await;
}
#[test]
fn damaged_adaptation_on_a_dedicated_pcr_pid_keeps_the_clock() {
const PCR: u16 = 0x0200;
let mut mux = Mux {
out: synth_pmt(&[(StreamType::H264, VIDEO)], false),
..Default::default()
};
mux.gop(VIDEO, 90_000);
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
import.decode(&mux.out).unwrap();
import.pcr_pid = Some(Pid::new(PCR).unwrap());
assert!(import.last_pts.is_some());
let mut packet = clock_break_packet(PCR);
packet[4] = 1;
packet[5] = 0x90;
import.decode(&packet).unwrap();
assert!(import.last_pts.is_some(), "a malformed field reset the program clock");
}
#[test]
fn damaged_pes_start_flushes_the_unit_before_it() {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let unbounded = |cc, pts, keyframe| {
let mut packet = video_pes(VIDEO, cc, pts, None, &annexb_au(keyframe));
let start = 5 + usize::from(packet[4]);
packet[start + 4..start + 6].fill(0);
packet
};
let mut data = synth_pmt(&[(StreamType::H264, VIDEO)], false);
data.extend(unbounded(0, 90_000, true));
let mut damaged = unbounded(1, 90_000 + FRAME, false);
let start = 5 + usize::from(damaged[4]);
damaged[start + 6..start + 19].fill(0);
data.extend(damaged);
import.decode(&data).unwrap();
import.finish().unwrap();
assert_eq!(
import.stats().streams[&VIDEO].units,
1,
"the keyframe before the damage"
);
}
#[test]
fn damaged_final_access_unit_is_refused() {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
let mut au = annexb_au(true);
let nal = au
.windows(5)
.position(|bytes| bytes[..4] == [0, 0, 0, 1] && bytes[4] & 0x1f == 5)
.unwrap();
au[nal + 4] |= 0x80;
let mut packet = video_pes(VIDEO, 0, 90_000, None, &au);
let start = 5 + usize::from(packet[4]);
packet[start + 4..start + 6].fill(0);
let mut data = synth_pmt(&[(StreamType::H264, VIDEO)], false);
data.extend(packet);
import.decode(&data).unwrap();
import.finish().unwrap();
assert_eq!(import.stats().streams[&VIDEO].units, 0);
assert!(catalog.snapshot().video.renditions.is_empty());
}
#[tokio::test(start_paused = true)]
async fn damaged_opus_pes_is_not_half_published() {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
import
.ensure_stream(
Pid::new(VIDEO).unwrap(),
0x06,
&[super::catalog::Descriptor {
tag: 0x05,
data: bytes::Bytes::from_static(b"Opus"),
}],
)
.unwrap();
let good = [0x7f, 0xe0, 3, 0xf8, 0xff, 0xfe];
let mut bad = good.to_vec();
bad.push(0x7f);
let mut data = Vec::new();
for (cc, payload) in [(0, good.as_slice()), (1, bad.as_slice()), (2, good.as_slice())] {
data.extend(video_pes(VIDEO, cc, 90_000 + u64::from(cc) * FRAME, None, payload));
}
import.decode(&data).unwrap();
import.finish().unwrap();
assert_eq!(import.stats().streams[&VIDEO].units, 2);
let name = catalog.snapshot().audio.renditions.keys().next().unwrap().clone();
let frames = read_track(&consumer, &name, crate::container::Kind::Audio).await;
assert_eq!(frames.len(), 2, "no packet from the damaged PES is published");
}
#[test]
fn damaged_recovery_keeps_publishing_errors_fatal() {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let track = broadcast
.create_track("video", hang::container::track_info(hang::catalog::PRIORITY.video))
.unwrap();
let video = crate::codec::h264::Import::new(track.clone(), catalog.reserve(), Default::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
import.streams.insert(
Pid::new(VIDEO).unwrap(),
Stream::H264 {
split: crate::codec::h264::Split::new(),
import: Box::new(video),
unwrap: Default::default(),
reanchor: Default::default(),
},
);
track.abort(moq_net::Error::Closed).unwrap();
let mut data = Vec::new();
for cc in 0..3 {
data.extend(video_pes(
VIDEO,
cc,
90_000 + u64::from(cc) * FRAME,
None,
&annexb_au(true),
));
}
let err = import.decode(&data).expect_err("closed producer must end ingest");
assert!(
matches!(err.downcast_ref::<crate::Error>(), Some(crate::Error::Moq(_))),
"{err:?}"
);
assert!(
!super::unit_error(crate::Error::H264(crate::codec::h264::Error::MissingTimestamp)).is::<super::Damaged>()
);
}
#[tokio::test(start_paused = true)]
async fn damaged_video_closes_its_group_at_the_break() {
use mpeg2ts::ts::{ReadTsPacket, TsPacketReader, TsPayload};
const MS: u64 = 90;
let at = |ms: u64| 90_000 + ms * MS;
let mut mux = Mux {
out: synth_pmt(&[(StreamType::H264, VIDEO)], false),
..Default::default()
};
let pictures = [
(true, at(0), None),
(false, at(40), None),
(false, at(80), None),
(true, at(1200), Some(at(1000))),
(false, at(1040), None),
(false, at(1280), Some(at(1080))),
];
let mut damaged_at = 0;
for (i, (keyframe, pts, dts)) in pictures.into_iter().enumerate() {
if i == 2 {
damaged_at = mux.out.len();
}
let cc = mux.cc(VIDEO);
mux.out
.extend_from_slice(&video_pes(VIDEO, cc, pts, dts, &annexb_au(keyframe)));
}
mux.out[damaged_at + 1] |= 0x80;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
import.decode(&mux.out).unwrap();
import.finish().unwrap();
let name = catalog.snapshot().video.renditions.keys().next().unwrap().clone();
let subscription = moq_net::track::Subscription::default().with_max_age(Duration::from_secs(3600));
let mut track = consumer.track(&name).unwrap().subscribe(subscription).await.unwrap();
let mut groups = Vec::new();
while let Some(mut group) = track.recv_group().await.unwrap() {
let mut frames = Vec::new();
while let Some(mut frame) = group.next_frame().await.unwrap() {
let frame = hang::container::Frame::decode(frame.read_all().await.unwrap()).unwrap();
frames.push((frame.timestamp.as_micros(), frame.payload.is_empty()));
}
groups.push(frames);
}
let us = |ms: u64| u128::from(at(ms)) * 1_000_000 / 90_000;
assert_eq!(
groups,
[
vec![(us(0), false), (us(40), false), (us(80), true)],
vec![(us(1200), false), (us(1040), false), (us(1280), false)],
],
"the first group ends at the break, one frame after its last picture"
);
let mut exporter = super::super::Export::new(crate::source::announced(&consumer))
.await
.unwrap()
.with_max_age(Duration::from_secs(30));
let mut ts = Vec::new();
while let Ok(res) = tokio::time::timeout(Duration::from_secs(1), exporter.next()).await {
let Some(frame) = res.expect("the exporter must take the open GOP after the break") else {
break;
};
ts.extend_from_slice(&frame.payload);
}
let mut reader = TsPacketReader::new(std::io::Cursor::new(ts));
let mut pictures = 0;
while let Some(packet) = reader.read_ts_packet().unwrap() {
pictures += usize::from(matches!(packet.payload, Some(TsPayload::PesStart(_))));
}
assert_eq!(pictures, 5, "every picture either side of the break is exported");
}
#[tokio::test(start_paused = true)]
async fn h264_join_below_a_p_frame_publishes_forward() {
let mut mux = Mux {
out: synth_pmt(&[(StreamType::H264, VIDEO)], false),
..Default::default()
};
mux.gops(VIDEO, 90_000, 2);
mux.gops(VIDEO, 90_000 + 6 * FRAME, 2);
let frames = video_frames(&mux.out).await;
assert_eq!(frames.len(), 16, "every picture either side of the join");
assert_forward(&frames);
}
#[tokio::test(start_paused = true)]
async fn h264_flagged_backward_restart_publishes_forward() {
let mut mux = Mux {
out: synth_pmt(&[(StreamType::H264, VIDEO)], false),
..Default::default()
};
mux.gops(VIDEO, 45 * 90_000, 2);
mux.out.extend_from_slice(&clock_break_packet(VIDEO));
mux.gops(VIDEO, 90_000, 2);
let frames = video_frames(&mux.out).await;
assert_eq!(frames.len(), 16);
assert_forward(&frames);
}
#[tokio::test(start_paused = true)]
async fn h264_unflagged_backward_restart_publishes_forward() {
let mut mux = Mux {
out: synth_pmt(&[(StreamType::H264, VIDEO)], false),
..Default::default()
};
mux.gops(VIDEO, 45 * 90_000, 2);
mux.gops(VIDEO, 90_000, 2);
let frames = video_frames(&mux.out).await;
assert_eq!(frames.len(), 16);
assert_forward(&frames);
}
#[tokio::test(start_paused = true)]
async fn h264_survives_two_loop_wraps() {
let mut mux = Mux {
out: synth_pmt(&[(StreamType::H264, VIDEO)], false),
..Default::default()
};
for _ in 0..3 {
mux.gops(VIDEO, 90_000, 2);
}
let frames = video_frames(&mux.out).await;
assert_eq!(frames.len(), 24, "every pass publishes");
assert_forward(&frames);
let stamps: Vec<u128> = frames
.iter()
.filter(|f| f.keyframe)
.map(|f| f.timestamp.as_micros())
.collect();
let gop = 4 * 1_000_000 * FRAME as u128 / 90_000;
let steps: Vec<u128> = stamps.windows(2).map(|pair| pair[1] - pair[0]).collect();
let near = |a: u128, b: u128| a.abs_diff(b) <= 1;
assert!(
[steps[0], steps[2], steps[4]].iter().all(|&step| near(step, gop)),
"a pass was respaced: {stamps:?}"
);
assert!(near(steps[1], steps[3]), "the wraps landed differently: {stamps:?}");
}
#[tokio::test(start_paused = true)]
async fn legacy_survives_two_loop_wraps() {
const MP2_PID: u16 = 0x0061;
let mut mux = Mux {
out: synth_pmt(&[(StreamType::Mpeg1Audio, MP2_PID)], false),
..Default::default()
};
const PES_TICKS: u64 = 2 * 72 * 90;
for _ in 0..3 {
for i in 0..4 {
let cc = mux.cc(MP2_PID);
mux.out
.extend_from_slice(&mp2_pes(MP2_PID, cc, 90_000 + i * PES_TICKS, [0xAA, 0xBB]));
}
}
let (consumer, catalog, _import) = import_all(&mux.out).expect("a second wrap must not end the import");
let name = catalog.snapshot().audio.renditions.keys().next().unwrap().clone();
let frames = read_track(&consumer, &name, crate::container::Kind::Audio).await;
assert!(frames.len() >= 20, "every pass publishes: {}", frames.len());
assert_forward(&frames);
}
#[tokio::test(start_paused = true)]
async fn aac_loop_overlapping_the_edge_publishes_forward() {
const AAC_PID: u16 = 0x0060;
const AAC_FRAME: u64 = 1024 * 90_000 / 48_000;
let mut mux = Mux {
out: synth_pmt(&[(StreamType::AdtsAac, AAC_PID)], false),
..Default::default()
};
for pass in 0..3u64 {
for i in 0..4u64 {
let mut payload = adts_frame(17, 0xA0 | i as u8);
payload.extend_from_slice(&adts_frame(17, 0xB0 | i as u8));
let cc = mux.cc(AAC_PID);
let pts = 90_000 + pass * 6 * AAC_FRAME + i * 2 * AAC_FRAME;
mux.out.extend_from_slice(&audio_pes_packet(AAC_PID, cc, pts, &payload));
}
}
let (consumer, catalog, _import) = import_all(&mux.out).expect("an overlapping wrap must not end the import");
let name = catalog.snapshot().audio.renditions.keys().next().unwrap().clone();
let frames = read_track(&consumer, &name, crate::container::Kind::Audio).await;
assert!(frames.len() >= 20, "every pass publishes: {}", frames.len());
assert_forward(&frames);
}
#[tokio::test(start_paused = true)]
async fn verbatim_pes_below_the_edge_publishes_forward() {
const DATA_PID: u16 = 0x0052;
let mut mux = Mux {
out: synth_pmt(&[(StreamType::Mpeg2PacketizedData, DATA_PID)], false),
..Default::default()
};
for pts in [45 * 90_000, 46 * 90_000, 90_000, 2 * 90_000, 90_000] {
let cc = mux.cc(DATA_PID);
mux.out
.extend_from_slice(&audio_pes_packet(DATA_PID, cc, pts, &[0xDE, 0xAD]));
}
let (consumer, catalog, _import) =
import_all(&mux.out).expect("a verbatim PES below the edge must not end the import");
let name = catalog.snapshot().ext.mpegts.tracks.keys().next().unwrap().clone();
let frames = read_track(&consumer, &name, crate::container::Kind::Data).await;
assert_eq!(frames.len(), 5);
assert_forward(&frames);
}
#[tokio::test(start_paused = true)]
async fn section_after_a_b_frame_publishes_forward() {
const CUE_PID: u16 = 0x0021;
let mut mux = Mux {
out: synth_pmt(
&[
(StreamType::H264, VIDEO),
(StreamType::Dts8ChannelLosslessAudio, CUE_PID),
],
true,
),
..Default::default()
};
for _ in 0..2 {
for (keyframe, pts, dts) in [
(true, 90_000 + FRAME, Some(90_000)),
(false, 90_000 + 4 * FRAME, Some(90_000 + FRAME)),
(false, 90_000 + 2 * FRAME, None),
] {
let cc = mux.cc(VIDEO);
mux.out
.extend_from_slice(&video_pes(VIDEO, cc, pts, dts, &annexb_au(keyframe)));
let cc = mux.cc(CUE_PID);
mux.out.extend_from_slice(&packet(true, cc, 0, &CUE));
}
}
let (consumer, catalog, _import) = import_all(&mux.out).expect("a cue after a B-frame must not end the import");
let name = catalog.snapshot().ext.mpegts.tracks.keys().next().unwrap().clone();
let frames = read_track(&consumer, &name, crate::container::Kind::Data).await;
assert_eq!(frames.len(), 6, "every cue publishes");
assert_forward(&frames);
assert!(
frames[4].timestamp.as_micros() > frames[2].timestamp.as_micros(),
"the cues after the wrap did not move with the video: {:?}",
frames.iter().map(|f| f.timestamp.as_micros()).collect::<Vec<_>>()
);
}
#[tokio::test(start_paused = true)]
async fn section_with_a_wrap_keyframe_follows_it() {
const CUE_PID: u16 = 0x0021;
let mut mux = Mux {
out: synth_pmt(
&[
(StreamType::H264, VIDEO),
(StreamType::Dts8ChannelLosslessAudio, CUE_PID),
],
true,
),
..Default::default()
};
for _ in 0..2 {
let cc = mux.cc(VIDEO);
let idr = video_pes(VIDEO, cc, 90_000 + FRAME, Some(90_000), &annexb_au(true));
mux.out.extend_from_slice(&idr);
let cc = mux.cc(CUE_PID);
mux.out.extend_from_slice(&packet(true, cc, 0, &CUE));
for (pts, dts) in [
(90_000 + 4 * FRAME, Some(90_000 + FRAME)),
(90_000 + 2 * FRAME, None),
(90_000 + 3 * FRAME, None),
] {
let cc = mux.cc(VIDEO);
mux.out
.extend_from_slice(&video_pes(VIDEO, cc, pts, dts, &annexb_au(false)));
}
mux.gops(VIDEO, 90_000 + 4 * FRAME, 1);
}
let (consumer, catalog, _import) = import_all(&mux.out).expect("the import must survive the wrap");
let snapshot = catalog.snapshot();
let video = snapshot.video.renditions.keys().next().unwrap().clone();
let cues = snapshot.ext.mpegts.tracks.keys().find(|name| **name != video).unwrap();
let cues = read_track(&consumer, cues, crate::container::Kind::Data).await;
let video = read_track(&consumer, &video, crate::container::Kind::Video).await;
assert_eq!(cues.len(), 2);
assert_eq!(
cues[1].timestamp, video[8].timestamp,
"the cue left its keyframe behind"
);
}
fn pcr_packet(pid: u16, ticks: u64) -> Vec<u8> {
let (base, ext) = (ticks / 300, ticks % 300);
let mut p = vec![
0x47,
(pid >> 8) as u8 & 0x1f,
(pid & 0xff) as u8,
0x20,
183,
0x10,
(base >> 25) as u8,
(base >> 17) as u8,
(base >> 9) as u8,
(base >> 1) as u8,
((base as u8 & 1) << 7) | 0x7e | (ext >> 8) as u8,
ext as u8,
];
p.resize(188, 0xff);
p
}
#[test]
fn mux_rate_follows_the_clock() {
use crate::catalog::hang::Catalog;
use crate::container::ts::catalog::Ext;
let mut broadcast = moq_net::broadcast::Info::new().produce();
let catalog = crate::catalog::Producer::new(
&mut broadcast,
crate::catalog::Config::default().with_catalog(Catalog::<Ext>::default()),
)
.unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
import
.decode(&synth_pmt(&[(StreamType::Mpeg1Audio, PCR_PID)], false))
.unwrap();
const INTERVAL: u64 = 27_000_000 / 25;
let mut ticks = 0;
let mut feed = |import: &mut super::Import<Ext>, seconds: u64, stuffing: &dyn Fn(u64) -> usize| {
for i in 0..seconds * 25 {
let mut bytes = pcr_packet(PCR_PID, ticks);
for _ in 0..stuffing(i) {
bytes.extend_from_slice(&super::super::export::NULL_PACKET);
}
import.decode(&bytes).unwrap();
ticks += INTERVAL;
}
};
let rate = || catalog.snapshot().ext.mpegts.mux_rate;
feed(&mut import, 3, &|_| 99);
assert_eq!(rate(), Some(3_760_000), "a stable window records the rate");
feed(&mut import, 4, &|i| if (i / 5) % 2 == 0 { 10 } else { 90 });
assert_eq!(rate(), None, "an unstable window clears the record");
feed(&mut import, 4, &|_| 99);
assert_eq!(rate(), Some(3_760_000), "a fresh stable window records it again");
import.decode(&clock_break_packet(PCR_PID)).unwrap();
assert_eq!(rate(), None, "a time-base break clears the record at once");
}
#[tokio::test(start_paused = true)]
async fn elementary_discontinuity_is_not_a_timebase_break() {
let (consumer, catalog, mut import) = two_stream_import();
for pid in [PCR_PID, PEER_PID] {
import.decode(mp2_pes(pid, 0, 90_000, [0xAA, 0xBB]).as_slice()).unwrap();
}
import
.decode(flag_discontinuity(mp2_pes(PEER_PID, 5, 180_000, [0xCC, 0xDD])).as_slice())
.unwrap();
import
.decode(mp2_pes(PCR_PID, 1, 180_000, [0xCC, 0xDD]).as_slice())
.unwrap();
import.finish().unwrap();
for (_, breaks) in read_all_breaks(&consumer, &catalog).await {
assert_eq!(breaks, 0, "a counter jump is not a program timebase reset");
}
}
#[tokio::test(start_paused = true)]
async fn a_counter_gap_is_not_a_timebase_break() {
let (consumer, catalog, mut import) = two_stream_import();
for pid in [PCR_PID, PEER_PID] {
import.decode(mp2_pes(pid, 0, 90_000, [0xAA, 0xBB]).as_slice()).unwrap();
}
import
.decode(mp2_pes(PCR_PID, 7, 180_000, [0xCC, 0xDD]).as_slice())
.unwrap();
import.finish().unwrap();
for (_, breaks) in read_all_breaks(&consumer, &catalog).await {
assert_eq!(breaks, 0, "lost packets are not a signalled clock reset");
}
}
#[tokio::test(start_paused = true)]
async fn a_timestamp_rollover_is_not_a_timebase_break() {
let (consumer, catalog, mut import) = two_stream_import();
const FIELD: u64 = 1 << 33;
for pid in [PCR_PID, PEER_PID] {
import
.decode(mp2_pes(pid, 0, FIELD - 90_000, [0xAA, 0xBB]).as_slice())
.unwrap();
import.decode(mp2_pes(pid, 1, 90_000, [0xCC, 0xDD]).as_slice()).unwrap();
}
import.finish().unwrap();
for (frames, breaks) in read_all_breaks(&consumer, &catalog).await {
assert_eq!(breaks, 0, "a rollover is not a break");
let stamps: Vec<u128> = frames.iter().map(|f| f.timestamp.as_micros()).collect();
assert!(
stamps.windows(2).all(|pair| pair[0] < pair[1]),
"the rollover broke the timeline: {stamps:?}"
);
}
}
#[tokio::test(start_paused = true)]
async fn repeated_flags_declare_one_break() {
let (consumer, catalog, mut import) = two_stream_import();
for pid in [PCR_PID, PEER_PID] {
import.decode(mp2_pes(pid, 0, 90_000, [0xAA, 0xBB]).as_slice()).unwrap();
}
for _ in 0..4 {
import.decode(clock_break_packet(PCR_PID).as_slice()).unwrap();
}
for pid in [PCR_PID, PEER_PID] {
import
.decode(mp2_pes(pid, 1, 180_000, [0xCC, 0xDD]).as_slice())
.unwrap();
}
import.finish().unwrap();
for (_, breaks) in read_all_breaks(&consumer, &catalog).await {
assert_eq!(breaks, 1, "each flagged packet declared its own break");
}
}
#[tokio::test(start_paused = true)]
async fn a_corrupt_packet_declares_no_break() {
let (consumer, catalog, mut import) = two_stream_import();
for pid in [PCR_PID, PEER_PID] {
import.decode(mp2_pes(pid, 0, 90_000, [0xAA, 0xBB]).as_slice()).unwrap();
}
let mut corrupt = clock_break_packet(PCR_PID);
corrupt[1] |= 0x80;
import.decode(corrupt.as_slice()).unwrap();
for pid in [PCR_PID, PEER_PID] {
import
.decode(mp2_pes(pid, 1, 180_000, [0xCC, 0xDD]).as_slice())
.unwrap();
}
import.finish().unwrap();
for (_, breaks) in read_all_breaks(&consumer, &catalog).await {
assert_eq!(breaks, 0, "a corrupt packet's adaptation field was trusted");
}
}
#[tokio::test(start_paused = true)]
async fn a_break_before_any_media_is_ignored() {
let (consumer, catalog, mut import) = two_stream_import();
import.decode(clock_break_packet(PCR_PID).as_slice()).unwrap();
for pid in [PCR_PID, PEER_PID] {
import.decode(mp2_pes(pid, 0, 90_000, [0xAA, 0xBB]).as_slice()).unwrap();
}
import.finish().unwrap();
for (frames, breaks) in read_all_breaks(&consumer, &catalog).await {
assert_eq!(breaks, 0, "nothing had been published to break from");
assert!(!frames.is_empty(), "the media after the flag still publishes");
}
}
async fn export_discontinuities(data: &[u8]) -> usize {
let mut broadcast = moq_net::broadcast::Info::new().produce();
let consumer = broadcast.consume();
let catalog = crate::catalog::Producer::new(&mut broadcast, crate::catalog::Config::default()).unwrap();
let mut import = super::Import::new(broadcast, catalog.reserve());
import.decode(&bytes::BytesMut::from(data)).unwrap();
import.finish().unwrap();
let mut exporter = crate::container::ts::Export::new(crate::source::announced(&consumer))
.await
.unwrap()
.with_max_age(std::time::Duration::from_secs(3600));
let mut flagged = 0;
while let Ok(Ok(Some(frame))) =
tokio::time::timeout(std::time::Duration::from_millis(100), exporter.next()).await
{
flagged += frame
.payload
.as_chunks::<188>()
.0
.iter()
.filter(|p| p[3] & 0x20 != 0 && p[4] > 0 && p[5] & 0x80 != 0)
.count();
}
flagged
}
#[tokio::test(start_paused = true)]
async fn a_signalled_reset_reaches_the_exported_clock() {
const PID: u16 = 0x0061;
const PES_TICKS: u64 = 2 * 72 * 90;
let media = |ts: &mut Vec<u8>, cc: &mut u8, base: u64| {
for i in 0..8 {
ts.extend_from_slice(&mp2_pes(PID, *cc, base + i * PES_TICKS, [0xAA, 0xBB]));
*cc = (*cc + 1) & 0x0f;
}
};
let mut cc = 0;
let mut signalled = synth_pmt(&[(StreamType::Mpeg1Audio, PID)], false);
media(&mut signalled, &mut cc, 90_000);
let mut unsignalled = signalled.clone();
signalled.extend_from_slice(&clock_break_packet(PID));
let mut peer_cc = cc;
media(&mut signalled, &mut cc, 31 * 90_000);
media(&mut unsignalled, &mut peer_cc, 31 * 90_000);
assert_eq!(
export_discontinuities(&signalled).await,
1,
"the declared reset never reached the exported clock"
);
assert_eq!(
export_discontinuities(&unsignalled).await,
0,
"a leap the source did not declare must not be flagged"
);
}
#[tokio::test(start_paused = true)]
async fn a_shared_forward_boundary_resets_once_per_rendition() {
const PES_TICKS: u64 = 2 * 72 * 90;
let mut stimulus = synth_pmt(
&[(StreamType::Mpeg1Audio, PCR_PID), (StreamType::Mpeg1Audio, PEER_PID)],
false,
);
let media = |ts: &mut Vec<u8>, cc: &mut u8, base: u64| {
for i in 0..8 {
for pid in [PCR_PID, PEER_PID] {
ts.extend_from_slice(&mp2_pes(pid, *cc, base + i * PES_TICKS, [0xAA, 0xBB]));
}
*cc = (*cc + 1) & 0x0f;
}
};
let mut cc = 0;
media(&mut stimulus, &mut cc, 90_000);
stimulus.extend_from_slice(&clock_break_packet(PCR_PID));
media(&mut stimulus, &mut cc, 31 * 90_000);
assert_eq!(export_discontinuities(&stimulus).await, 1, "one flag per program break");
}
}