use crate::display_track::{ActivationOutcome, SpsTracker, TrackCodec};
use crate::ir::{AfdPersistence, DisplayChangeSet, DisplayInfo};
use crate::mpeg2_display::{Mpeg2DisplayTracker, Mpeg2Outcome};
use crate::{ProgramNumber, TrackId};
use alloc::collections::btree_map::Entry;
use alloc::collections::{BTreeMap, BTreeSet, VecDeque};
use alloc::sync::Arc;
use alloc::vec::Vec;
use broadcast_common::error::{BufferTooShort, InvalidValue};
use broadcast_common::len::need_in;
use core::marker::PhantomData;
use mpeg_ts::pid::Pid;
use broadcast_common::timescale::{Rounding, rescale};
use broadcast_common::{Demand, Serialize, Stage, Timestamp, Unpackage};
use mpeg_pes::{PesAssembler, PesPacket};
use mpeg_ts::resync::TsResync;
use mpeg_ts::ts::{SectionReassembler, TS_PACKET_SIZE, TsPacket};
use crate::aac_asc::{
ADTS_HEADER_SIZE, AdtsHeader, AudioSpecificConfig, SamplingFrequencyIndex, parse_adts_header,
};
use crate::ac3::{
AC3_SAMPLES_PER_SYNCFRAME, AC3_SYNCWORD, Ac3SyncframeInfo, EAC3_STRMTYP_DEPENDENT,
Ec3SpecificBox, Ec3SyncframeInfo,
};
use crate::annexb::{annexb_to_length_prefixed, iter_annexb_nals};
use crate::avc_config::{AVCConfigurationBox, AVCDecoderConfigurationRecord};
use crate::dts::{DTS_CORE_SYNC_WORD, DtsCoreFrameInfo};
use crate::error::{Error, Result};
use crate::hdr::{HEVC_PREFIX_SEI_NUT, HdrStaticMetadata};
use crate::hevc_config::{HEVCConfigurationBox, HEVCDecoderConfigurationRecord};
use crate::media::{Media, PcrSample, Track};
use crate::mp4esds::{
DecoderConfigDescriptor, DecoderSpecificInfo, ESDescriptor, EsdsBox, SLConfigDescriptor,
};
use crate::mpeg_legacy::{Mpeg2SeqHeader, MpegAudioFrameHeader};
use crate::mpegh::{MHADecoderConfigurationRecord, find_mpegh3da_config};
use crate::nal::{
NalCodec, ParamKind, access_unit_is_rap, is_keyframe_nal, nal_unit_type, parameter_set_is_valid,
};
use crate::nalu_types::{AvcPps, AvcSps, HevcNalArray, HevcNalUnit};
use crate::pipeline::{CodecConfig, DataCarriage, Provenance, Sample, SampleFlags, TrackSpec};
const PAT_PID: Pid = Pid::new(0x0000);
pub(crate) const TABLE_ID_PAT: u8 = 0x00;
pub(crate) const TABLE_ID_PMT: u8 = 0x02;
const SECTION_HEADER_LEN: usize = 8;
const VERSION_NUMBER_MASK: u8 = 0x1F;
const CURRENT_NEXT_INDICATOR_BIT: u8 = 0x01;
const CRC32_LEN: usize = 4;
const SECTION_SYNTAX_INDICATOR_BIT: u8 = 0x80;
const SECTION_LENGTH_HI_MASK: u8 = 0x0F;
const PID_HI_MASK: u8 = 0x1F;
const PAT_ENTRY_LEN: usize = 4;
const INFO_LENGTH_HI_MASK: u8 = 0x0F;
const NETWORK_PROGRAM_NUMBER: u16 = 0x0000;
const NULL_PACKET_PID: Pid = Pid::new(0x1FFF);
const MAX_UNATTRIBUTED_BYTES: usize = 4 * 1024 * 1024;
const TS_MAX_PAYLOAD_BYTES: usize = TS_PACKET_SIZE - 4;
const PES_LENGTH_FIELD_END: usize = 6;
const MAX_PES_BUFFER_BYTES: usize = 4 * 1024 * 1024;
const MAX_PROBE_BACKLOG_BYTES: usize = 4 * 1024 * 1024;
pub(crate) const STREAM_TYPE_MPEG2_VIDEO: u8 = 0x02;
const STREAM_TYPE_MPEG1_AUDIO: u8 = 0x03;
const STREAM_TYPE_MPEG2_AUDIO: u8 = 0x04;
const STREAM_TYPE_PES_PRIVATE: u8 = 0x06;
const STREAM_TYPE_AAC_ADTS: u8 = 0x0F;
const STREAM_TYPE_METADATA_PES: u8 = 0x15;
const STREAM_TYPE_AVC: u8 = 0x1B;
const STREAM_TYPE_HEVC: u8 = 0x24;
const STREAM_TYPE_MPEGH: u8 = 0x2D;
const STREAM_TYPE_AC3: u8 = 0x81;
const STREAM_TYPE_DTS_82: u8 = 0x82;
const STREAM_TYPE_DTS_85: u8 = 0x85;
const STREAM_TYPE_EAC3: u8 = 0x87;
const STREAM_TYPE_DTS_8A: u8 = 0x8A;
const DESC_TAG_AC3: u8 = 0x6A;
const DESC_TAG_ENHANCED_AC3: u8 = 0x7A;
const DESC_TAG_DTS: u8 = 0x7B;
const STREAM_TYPE_PRIVATE_SECTIONS: u8 = 0x05;
const STREAM_TYPE_DSMCC_TYPE_A: u8 = 0x0A;
const STREAM_TYPE_DSMCC_TYPE_B: u8 = 0x0B;
const STREAM_TYPE_DSMCC_TYPE_C: u8 = 0x0C;
const STREAM_TYPE_DSMCC_TYPE_D: u8 = 0x0D;
const STREAM_TYPE_DSMCC_SYNC_DOWNLOAD: u8 = 0x14;
const STREAM_TYPE_SCTE35: u8 = 0x86;
const NAL_LENGTH_SIZE_MINUS_ONE: u8 = 3;
const H264_NAL_SPS: u8 = 7;
const H264_NAL_PPS: u8 = 8;
const H264_NAL_TYPE_MASK: u8 = 0x1F;
const H265_NAL_VPS: u8 = 32;
const H265_NAL_SPS: u8 = 33;
const H265_NAL_PPS: u8 = 34;
const HVCC_CONFIGURATION_VERSION: u8 = 1;
const HVCC_CONSTANT_FRAME_RATE_UNSPEC: u8 = 0;
const HVCC_NUM_TEMPORAL_LAYERS: u8 = 1;
const HVCC_PARALLELISM_TYPE_UNKNOWN: u8 = 0;
const HVCC_AVG_FRAME_RATE_UNSPEC: u16 = 0;
const HVCC_MIN_SPATIAL_SEGMENTATION_UNSPEC: u16 = 0;
const OTI_MPEG4_AUDIO: u8 = 0x40;
pub(crate) const OTI_MPEG2_VIDEO_MAIN: u8 = 0x61;
const OTI_MPEG2_AUDIO: u8 = 0x69;
const OTI_MPEG1_AUDIO: u8 = 0x6B;
const STREAM_TYPE_AUDIO: u8 = 0x05;
pub(crate) const STREAM_TYPE_VISUAL: u8 = 0x04;
const ESDS_ES_ID: u16 = 1;
pub(crate) const ESDS_VIDEO_ES_ID: u16 = 2;
const AUDIO_SAMPLE_SIZE_BITS: u16 = 16;
const MPEGH_REFERENCE_CHANNEL_LAYOUT_UNSPECIFIED: u8 = 0;
const MPEGH_CHANNEL_COUNT_UNSPECIFIED: u16 = 0;
const MPEGH_SAMPLE_RATE_UNSPECIFIED: u32 = 0;
const MPEGH_CONFIG_LEN_MAX: usize = u16::MAX as usize;
const VIDEO_TIMESCALE: u32 = 90_000;
const AAC_SAMPLES_PER_FRAME: u32 = 1024;
pub(crate) const MPEG2_PICTURE_START_CODE: u8 = 0x00;
pub(crate) const MPEG2_PICTURE_CODING_TYPE_I: u8 = 0x01;
const TS_WRAP: u64 = broadcast_common::clock33::WRAP_33BIT;
const TS_WRAP_MASK: u64 = TS_WRAP - 1;
const MAX_FRAME_PERIOD_TICKS: u64 = VIDEO_TIMESCALE as u64;
const FRAME_PERIOD_WINDOW: usize = 5;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Codec {
H264,
Hevc,
Mpeg2Video,
MpegAudio(bool),
Aac,
Ac3,
Eac3,
Dts,
MpegH,
Data(u8),
}
impl Codec {
fn from_stream_type(stream_type: u8) -> Self {
match stream_type {
STREAM_TYPE_MPEG2_VIDEO => Codec::Mpeg2Video,
STREAM_TYPE_MPEG1_AUDIO => Codec::MpegAudio(false),
STREAM_TYPE_MPEG2_AUDIO => Codec::MpegAudio(true),
STREAM_TYPE_AVC => Codec::H264,
STREAM_TYPE_HEVC => Codec::Hevc,
STREAM_TYPE_AAC_ADTS => Codec::Aac,
STREAM_TYPE_AC3 => Codec::Ac3,
STREAM_TYPE_EAC3 => Codec::Eac3,
STREAM_TYPE_DTS_82 | STREAM_TYPE_DTS_85 | STREAM_TYPE_DTS_8A => Codec::Dts,
STREAM_TYPE_MPEGH => Codec::MpegH,
_ => Codec::Data(stream_type),
}
}
fn refine_with_descriptors(self, stream_type: u8, descriptors: &[u8]) -> Self {
if !matches!(
stream_type,
STREAM_TYPE_PES_PRIVATE | STREAM_TYPE_METADATA_PES
) {
return self;
}
if stream_type == STREAM_TYPE_PES_PRIVATE
&& crate::dovi::has_registration_descriptor(descriptors)
&& let Some(d) = crate::dovi::find_video_stream_descriptor(descriptors)
&& d.record.bl_present
&& d.record.is_hevc_profile()
{
return Codec::Hevc;
}
let mut off = 0usize;
while off + 2 <= descriptors.len() {
let tag = descriptors[off];
let len = descriptors[off + 1] as usize;
match tag {
DESC_TAG_AC3 => return Codec::Ac3,
DESC_TAG_ENHANCED_AC3 => return Codec::Eac3,
DESC_TAG_DTS => return Codec::Dts,
_ => {}
}
off += 2 + len;
}
self
}
}
fn data_carriage(stream_type: u8) -> DataCarriage {
match stream_type {
STREAM_TYPE_PRIVATE_SECTIONS
| STREAM_TYPE_DSMCC_TYPE_A
| STREAM_TYPE_DSMCC_TYPE_B
| STREAM_TYPE_DSMCC_TYPE_C
| STREAM_TYPE_DSMCC_TYPE_D
| STREAM_TYPE_DSMCC_SYNC_DOWNLOAD
| STREAM_TYPE_SCTE35 => DataCarriage::Sections,
_ => DataCarriage::Pes,
}
}
fn unwrap_ts(prev_unwrapped: i128, prev_raw: u64, raw: u64) -> i128 {
broadcast_common::clock33::unwrap_delta(prev_unwrapped, prev_raw, raw)
}
fn rescale_to_track(anchor_90k: i128, timescale: u32) -> i64 {
to_ticks(rescale(
anchor_90k,
VIDEO_TIMESCALE,
timescale,
Rounding::Floor,
))
}
pub(crate) fn mpeg2_is_sync(au: &[u8]) -> bool {
let mut i = 0usize;
while i + 4 <= au.len() {
if au[i] == 0x00 && au[i + 1] == 0x00 && au[i + 2] == 0x01 {
let code = au[i + 3];
if code == crate::mpeg_legacy::SEQUENCE_HEADER_CODE[3] {
return true;
}
if code == MPEG2_PICTURE_START_CODE && i + 6 <= au.len() {
let pct = (au[i + 5] >> 3) & 0x07;
return pct == MPEG2_PICTURE_CODING_TYPE_I;
}
}
i += 1;
}
false
}
#[cfg(test)]
mod probe_counter {
thread_local! {
static SYNC_PROBES: core::cell::Cell<u64> = const { core::cell::Cell::new(0) };
}
pub(super) fn record() {
SYNC_PROBES.with(|c| c.set(c.get().wrapping_add(1)));
}
pub(super) fn read() -> u64 {
SYNC_PROBES.with(|c| c.get())
}
}
#[cfg(test)]
fn record_sync_probe() {
probe_counter::record();
}
#[cfg(test)]
fn record_probe_pass() {
probe_counter::record();
}
#[cfg(not(test))]
#[inline(always)]
fn record_probe_pass() {}
#[cfg(not(test))]
#[inline(always)]
fn record_sync_probe() {}
#[cfg(test)]
fn sync_probes() -> u64 {
probe_counter::read()
}
fn find_mpeg_audio_sync(data: &[u8]) -> Option<(usize, MpegAudioFrameHeader)> {
let mut off = 0usize;
while off + 4 <= data.len() {
record_sync_probe();
if let Ok(hdr) = MpegAudioFrameHeader::parse(&data[off..]) {
return Some((off, hdr));
}
off += 1;
}
None
}
const AUDIO_CARRY_MAX_BYTES: usize = 16_384;
enum SyncParse<H> {
Frame(H, usize),
NeedMore,
NotSync,
}
trait FrameSync {
type Hdr: Copy;
const MIN_FRAME_LEN: usize;
fn parse(data: &[u8]) -> SyncParse<Self::Hdr>;
fn is_partial_sync(prefix: &[u8]) -> bool;
fn folds_into_previous(_hdr: &Self::Hdr) -> bool {
false
}
fn tail_starts_new_unit(_tail: &[u8]) -> bool {
true
}
fn payload<'a>(
_hdr: &Self::Hdr,
frame: &'a [u8],
header_len: usize,
) -> Option<alloc::borrow::Cow<'a, [u8]>> {
(frame.len() > header_len).then(|| frame[header_len..].into())
}
}
fn is_sync_prefix(prefix: &[u8], sync: &[u8]) -> bool {
prefix.iter().zip(sync).all(|(a, b)| a == b)
}
struct MpegAudioSync;
impl FrameSync for MpegAudioSync {
type Hdr = MpegAudioFrameHeader;
const MIN_FRAME_LEN: usize = 4;
fn parse(data: &[u8]) -> SyncParse<Self::Hdr> {
if data.len() < Self::MIN_FRAME_LEN {
return SyncParse::NeedMore;
}
match MpegAudioFrameHeader::parse(data) {
Ok(hdr) => SyncParse::Frame(hdr, hdr.frame_length),
Err(_) => SyncParse::NotSync,
}
}
fn is_partial_sync(prefix: &[u8]) -> bool {
const SYNC_HI_BYTE: u8 = 0xFF;
const SYNC_LO_MASK: u8 = 0xE0;
prefix.first() == Some(&SYNC_HI_BYTE)
&& prefix
.get(1)
.is_none_or(|b| b & SYNC_LO_MASK == SYNC_LO_MASK)
}
}
struct AdtsSync;
impl FrameSync for AdtsSync {
type Hdr = AdtsHeader;
const MIN_FRAME_LEN: usize = ADTS_HEADER_SIZE;
fn payload<'a>(
hdr: &Self::Hdr,
frame: &'a [u8],
_header_len: usize,
) -> Option<alloc::borrow::Cow<'a, [u8]>> {
adts_frame_payload(hdr, frame)
}
fn parse(data: &[u8]) -> SyncParse<Self::Hdr> {
if data.len() < ADTS_HEADER_SIZE {
return SyncParse::NeedMore;
}
match parse_adts_header(data) {
Ok(hdr) => SyncParse::Frame(hdr, hdr.frame_length as usize),
Err(_) => SyncParse::NotSync,
}
}
fn is_partial_sync(prefix: &[u8]) -> bool {
const SYNC_HI_BYTE: u8 = 0xFF;
const SYNC_LO_MASK: u8 = 0xF0;
prefix.first() == Some(&SYNC_HI_BYTE)
&& prefix
.get(1)
.is_none_or(|b| b & SYNC_LO_MASK == SYNC_LO_MASK)
}
}
fn sync_from_parse<H>(r: Result<H>, frame_len: impl FnOnce(&H) -> Option<usize>) -> SyncParse<H> {
match r {
Ok(h) => match frame_len(&h) {
Some(len) if len != 0 => SyncParse::Frame(h, len),
_ => SyncParse::NotSync,
},
Err(Error::BufferTooShort(..)) => SyncParse::NeedMore,
Err(_) => SyncParse::NotSync,
}
}
const AC3_SYNC_BYTES: [u8; 2] = AC3_SYNCWORD.to_be_bytes();
struct Ac3Sync;
impl FrameSync for Ac3Sync {
type Hdr = Ac3SyncframeInfo;
const MIN_FRAME_LEN: usize = 8;
fn parse(data: &[u8]) -> SyncParse<Self::Hdr> {
if data.len() < AC3_SYNC_BYTES.len() {
return SyncParse::NeedMore;
}
if data[..AC3_SYNC_BYTES.len()] != AC3_SYNC_BYTES {
return SyncParse::NotSync;
}
sync_from_parse(Ac3SyncframeInfo::parse_at(data, 0), |i| i.frame_len_bytes())
}
fn is_partial_sync(prefix: &[u8]) -> bool {
is_sync_prefix(prefix, &AC3_SYNC_BYTES)
}
}
struct Eac3Sync;
impl FrameSync for Eac3Sync {
type Hdr = Ec3SyncframeInfo;
const MIN_FRAME_LEN: usize = 8;
fn parse(data: &[u8]) -> SyncParse<Self::Hdr> {
if data.len() < AC3_SYNC_BYTES.len() {
return SyncParse::NeedMore;
}
if data[..AC3_SYNC_BYTES.len()] != AC3_SYNC_BYTES {
return SyncParse::NotSync;
}
sync_from_parse(Ec3SyncframeInfo::parse_at(data, 0), |i| {
Some((i.frmsiz as usize + 1) * 2)
})
}
fn is_partial_sync(prefix: &[u8]) -> bool {
is_sync_prefix(prefix, &AC3_SYNC_BYTES)
}
fn folds_into_previous(hdr: &Self::Hdr) -> bool {
hdr.strmtyp == EAC3_STRMTYP_DEPENDENT
}
fn tail_starts_new_unit(tail: &[u8]) -> bool {
const BSID_BYTE: usize = 5;
const AC3_MAX_BSID: u8 = 8;
if tail.len() > BSID_BYTE && tail[BSID_BYTE] >> 3 <= AC3_MAX_BSID {
return true;
}
tail.len() > AC3_SYNC_BYTES.len()
&& (tail[AC3_SYNC_BYTES.len()] >> 6) != EAC3_STRMTYP_DEPENDENT
}
}
const DTS_SYNC_BYTES: [u8; 4] = DTS_CORE_SYNC_WORD.to_be_bytes();
struct DtsSync;
impl FrameSync for DtsSync {
type Hdr = DtsCoreFrameInfo;
const MIN_FRAME_LEN: usize = 8;
fn parse(data: &[u8]) -> SyncParse<Self::Hdr> {
let n = data.len().min(DTS_SYNC_BYTES.len());
if !is_sync_prefix(&data[..n], &DTS_SYNC_BYTES) {
return SyncParse::NotSync;
}
if n < DTS_SYNC_BYTES.len() {
return SyncParse::NeedMore;
}
sync_from_parse(DtsCoreFrameInfo::parse_at(data, 0), |i| Some(i.frame_size))
}
fn is_partial_sync(prefix: &[u8]) -> bool {
is_sync_prefix(prefix, &DTS_SYNC_BYTES)
}
}
struct FrameSplit<'a, H> {
frames: Vec<(usize, &'a [u8], H)>,
tail_start: usize,
}
fn split_frames_with_tail<F: FrameSync>(buf: &[u8]) -> FrameSplit<'_, F::Hdr> {
let mut frames: Vec<(usize, &[u8], F::Hdr)> = Vec::new();
let mut at = 0usize;
let mut tail_start = buf.len();
while at < buf.len() {
record_sync_probe();
match F::parse(&buf[at..]) {
SyncParse::Frame(hdr, flen) => {
if flen < F::MIN_FRAME_LEN {
break;
}
if at + flen > buf.len() {
tail_start = at;
break;
}
match frames.last_mut() {
Some(prev) if F::folds_into_previous(&hdr) && prev.0 + prev.1.len() == at => {
prev.1 = &buf[prev.0..at + flen];
}
_ if F::folds_into_previous(&hdr) => {}
_ => frames.push((at, &buf[at..at + flen], hdr)),
}
at += flen;
}
SyncParse::NeedMore if F::is_partial_sync(&buf[at..]) => {
tail_start = at;
break;
}
SyncParse::NeedMore | SyncParse::NotSync => at += 1,
}
}
FrameSplit { frames, tail_start }
}
fn find_adts_sync(data: &[u8]) -> Option<(usize, AdtsHeader)> {
let mut off = 0usize;
while off + ADTS_HEADER_SIZE <= data.len() {
record_sync_probe();
if let Ok(hdr) = parse_adts_header(&data[off..]) {
return Some((off, hdr));
}
off += 1;
}
None
}
#[cfg(test)]
fn split_adts_frames(payload: &[u8]) -> Vec<(&[u8], AdtsHeader)> {
split_frames_with_tail::<AdtsSync>(payload)
.frames
.into_iter()
.map(|(_, f, h)| (f, h))
.collect()
}
const ADTS_CRC_SIZE: usize = 2;
const ADTS_BLOCK_POSITION_SIZE: usize = 2;
fn adts_header_len(hdr: &AdtsHeader) -> usize {
if hdr.protection_absent {
ADTS_HEADER_SIZE
} else {
ADTS_HEADER_SIZE
+ ADTS_BLOCK_POSITION_SIZE * hdr.num_raw_data_blocks as usize
+ ADTS_CRC_SIZE
}
}
fn adts_frame_payload<'a>(
hdr: &AdtsHeader,
frame: &'a [u8],
) -> Option<alloc::borrow::Cow<'a, [u8]>> {
let header_len = adts_header_len(hdr);
if frame.len() <= header_len {
return None;
}
let n = hdr.num_raw_data_blocks as usize;
let plain = &frame[header_len..];
if hdr.protection_absent || n == 0 {
return Some(plain.into());
}
let positions = &frame[ADTS_HEADER_SIZE..ADTS_HEADER_SIZE + ADTS_BLOCK_POSITION_SIZE * n];
let mut starts = alloc::vec![0usize];
starts.extend(
positions
.chunks_exact(ADTS_BLOCK_POSITION_SIZE)
.map(|c| u16::from_be_bytes([c[0], c[1]]) as usize),
);
let ends = starts[1..].iter().copied().chain([plain.len()]);
let mut out = Vec::with_capacity(plain.len());
let mut prev = 0usize;
for (i, end) in ends.enumerate() {
let start = starts[i];
let ok = start >= prev && end >= start + ADTS_CRC_SIZE && end <= plain.len();
if !ok {
return Some(plain.into());
}
out.extend_from_slice(&plain[start..end - ADTS_CRC_SIZE]);
prev = end;
}
Some(out.into())
}
fn parse_pat(section: &[u8]) -> Result<Vec<(u16, Pid)>> {
if section.first().copied() != Some(TABLE_ID_PAT) {
return Ok(Vec::new());
}
let body = section_body(section, "PAT")?;
let mut programs = Vec::new();
let mut off = 0usize;
while off + PAT_ENTRY_LEN <= body.len() {
let program_number = u16::from_be_bytes([body[off], body[off + 1]]);
let pid = Pid::new((((body[off + 2] & PID_HI_MASK) as u16) << 8) | body[off + 3] as u16);
if program_number != NETWORK_PROGRAM_NUMBER {
programs.push((program_number, pid));
}
off += PAT_ENTRY_LEN;
}
Ok(programs)
}
struct PmtSectionHeader {
program_number: u16,
version: u8,
current_next: bool,
section_number: u8,
last_section_number: u8,
pcr_pid: Pid,
}
fn parse_pmt_section_header(section: &[u8]) -> Result<PmtSectionHeader> {
if section.first().copied() != Some(TABLE_ID_PMT) {
return Err(Error::InvalidValue(InvalidValue {
field: "table_id",
value: section.first().copied().unwrap_or(0) as u64,
reason: "not a PMT section",
}));
}
need_in(section, SECTION_HEADER_LEN, "PMT section header")?;
Ok(PmtSectionHeader {
program_number: u16::from_be_bytes([section[3], section[4]]),
version: (section[5] >> 1) & VERSION_NUMBER_MASK,
current_next: section[5] & CURRENT_NEXT_INDICATOR_BIT != 0,
section_number: section[6],
last_section_number: section[7],
pcr_pid: Pid::new((((section[8] & PID_HI_MASK) as u16) << 8) | section[9] as u16),
})
}
fn parse_pmt(section: &[u8]) -> Result<Vec<(Pid, Codec, Vec<u8>)>> {
if section.first().copied() != Some(TABLE_ID_PMT) {
return Ok(Vec::new());
}
let body = section_body(section, "PMT")?;
need_in(body, 4, "PMT program-info prefix")?;
let program_info_length = (((body[2] & INFO_LENGTH_HI_MASK) as usize) << 8) | body[3] as usize;
let mut off = 4 + program_info_length;
let mut out = Vec::new();
while off + 5 <= body.len() {
let stream_type = body[off];
let es_pid = Pid::new((((body[off + 1] & PID_HI_MASK) as u16) << 8) | body[off + 2] as u16);
let es_info_length =
(((body[off + 3] & INFO_LENGTH_HI_MASK) as usize) << 8) | body[off + 4] as usize;
let desc_start = off + 5;
let desc_end = (desc_start + es_info_length).min(body.len());
let descriptors = body[desc_start..desc_end].to_vec();
let codec =
Codec::from_stream_type(stream_type).refine_with_descriptors(stream_type, &descriptors);
out.push((es_pid, codec, descriptors));
off += 5 + es_info_length;
}
Ok(out)
}
fn section_body<'a>(section: &'a [u8], what: &'static str) -> Result<&'a [u8]> {
if section.len() < SECTION_HEADER_LEN + CRC32_LEN {
return Err(Error::BufferTooShort(BufferTooShort {
need: SECTION_HEADER_LEN + CRC32_LEN,
have: section.len(),
what,
}));
}
let section_length =
(((section[1] & SECTION_LENGTH_HI_MASK) as usize) << 8) | section[2] as usize;
let total = 3 + section_length;
let end = total.min(section.len());
if end < SECTION_HEADER_LEN + CRC32_LEN {
return Err(Error::BufferTooShort(BufferTooShort {
need: SECTION_HEADER_LEN + CRC32_LEN,
have: end,
what,
}));
}
Ok(§ion[SECTION_HEADER_LEN..end - CRC32_LEN])
}
fn psi_section_crc_ok(section: &[u8]) -> bool {
if section.len() < SECTION_HEADER_LEN + CRC32_LEN {
return false;
}
if section[1] & SECTION_SYNTAX_INDICATOR_BIT == 0 {
return false;
}
let section_length =
(((section[1] & SECTION_LENGTH_HI_MASK) as usize) << 8) | section[2] as usize;
let total = 3 + section_length;
if total > section.len() || total < SECTION_HEADER_LEN + CRC32_LEN {
return false;
}
let (covered, trailer) = section[..total].split_at(total - CRC32_LEN);
let declared = u32::from_be_bytes([trailer[0], trailer[1], trailer[2], trailer[3]]);
broadcast_common::crc32_mpeg2::compute(covered) == declared
}
fn section_current_next(section: &[u8]) -> bool {
section
.get(5)
.is_some_and(|b| b & CURRENT_NEXT_INDICATOR_BIT != 0)
}
struct BufferedAu {
data: Vec<u8>,
pts_uw: i128,
dts_uw: i128,
}
enum ConfigProbe {
H264 {
sps: Option<Vec<u8>>,
pps: Option<Vec<u8>>,
},
Hevc {
vps: Option<Vec<u8>>,
sps: Option<Vec<u8>>,
pps: Option<Vec<u8>>,
hdr_sei: Vec<Vec<u8>>,
},
Mpeg2Video,
MpegAudio {
is_mpeg2: bool,
},
Aac,
Ac3,
Eac3,
Dts,
MpegH,
Data,
}
fn initial_probe(codec: Codec) -> ConfigProbe {
match codec {
Codec::H264 => ConfigProbe::H264 {
sps: None,
pps: None,
},
Codec::Hevc => ConfigProbe::Hevc {
vps: None,
sps: None,
pps: None,
hdr_sei: Vec::new(),
},
Codec::Mpeg2Video => ConfigProbe::Mpeg2Video,
Codec::MpegAudio(is_mpeg2) => ConfigProbe::MpegAudio { is_mpeg2 },
Codec::Aac => ConfigProbe::Aac,
Codec::Ac3 => ConfigProbe::Ac3,
Codec::Eac3 => ConfigProbe::Eac3,
Codec::Dts => ConfigProbe::Dts,
Codec::MpegH => ConfigProbe::MpegH,
Codec::Data(_) => ConfigProbe::Data,
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum VideoCodec {
H264,
Hevc,
Mpeg2,
}
enum AudioKind {
Aac,
Ac3,
Eac3,
Dts,
MpegAudio { samples_per_frame: u32 },
}
#[derive(Default)]
struct AudioAnchor {
seed: Option<AudioAnchorSeed>,
duration_scale: u32,
carry: Vec<u8>,
hold_final_au: bool,
carry_provenance: Option<Provenance>,
}
#[derive(Clone, Copy)]
struct AudioAnchorSeed {
next_dts: i64,
next_pts: i64,
anchor_dts_uw: i128,
anchor_pts_uw: i128,
ticks_since_anchor: i64,
}
const AUDIO_REANCHOR_THRESHOLD_MS: i128 = 20;
pub(crate) fn audio_discontinuity_threshold_90k(sample_rate: u32) -> i128 {
let one_sample_period = (VIDEO_TIMESCALE as u128).div_ceil(sample_rate.max(1) as u128) as i128;
let ms_bound = (VIDEO_TIMESCALE as i128 * AUDIO_REANCHOR_THRESHOLD_MS) / 1000;
ms_bound.max(one_sample_period * 2)
}
struct PendingOneBehind {
data: Vec<u8>,
is_sync: bool,
pts_uw: i128,
dts_uw: i128,
display: Option<Arc<DisplayInfo>>,
pending_events: PendingDisplayEvents,
}
#[derive(Default)]
struct PendingDisplayEvents {
config: Option<TrackSpec>,
display: Option<(Arc<DisplayInfo>, DisplayChangeSet)>,
}
#[derive(Default)]
struct PaffPairing {
params: crate::slice_header::AvcParamSets,
first_field: Option<crate::slice_header::AvcFieldInfo>,
}
const MAX_FIELD_PAIR_DTS_TICKS: i128 = MAX_FRAME_PERIOD_TICKS as i128;
fn to_ticks(uw: i128) -> i64 {
uw.clamp(i64::MIN as i128, i64::MAX as i128) as i64
}
fn ts_provenance(dts: i64, pts: i64) -> Provenance {
Provenance {
wire_dts: Some((dts as u64) % TS_WRAP),
wire_pts: Some((pts as u64) % TS_WRAP),
}
}
#[allow(clippy::large_enum_variant)]
enum LiveKind {
Video {
pending: Option<PendingOneBehind>,
last_duration: u32,
codec: VideoCodec,
paff: PaffPairing,
},
Audio {
sample_rate: u32,
kind: AudioKind,
anchor: AudioAnchor,
},
Data {
pending: Option<PendingOneBehind>,
last_duration: u32,
},
MpegH {
pending: Option<PendingOneBehind>,
last_duration: u32,
},
Section,
}
struct LiveTrack {
track_id: TrackId,
kind: LiveKind,
config: CodecConfig,
timescale: u32,
parameter_sets: ParameterSets,
config_header: Option<Vec<u8>>,
sps_tracker: Option<SpsTracker>,
mpeg2_tracker: Option<Mpeg2DisplayTracker>,
pending_config: Option<TrackSpec>,
display: Option<(Arc<DisplayInfo>, DisplayChangeSet)>,
suppress_next_display_change: bool,
damaged_promotion: bool,
skip_next_reprobe: bool,
hdr10plus: bool,
}
impl LiveTrack {
fn display(&mut self, au: &[u8]) -> Option<Arc<DisplayInfo>> {
if let Some(tracker) = self.mpeg2_tracker.as_mut() {
if let Mpeg2Outcome::Changed {
display, changed, ..
} = tracker.observe(au)
{
self.display = Some((display, changed));
}
return tracker.state().cloned();
}
let tracker = self.sps_tracker.as_mut()?;
if let ActivationOutcome::Changed {
display, changed, ..
} = tracker.observe(au)
{
self.display = Some((display, changed));
}
tracker.state().cloned()
}
}
#[allow(clippy::large_enum_variant)]
enum TrackState {
Probing {
probe: ConfigProbe,
backlog: Vec<BufferedAu>,
},
Parked {
config: CodecConfig,
timescale: u32,
kind: LiveKind,
backlog: Vec<BufferedAu>,
},
Live(LiveTrack),
Abandoned,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DiscontinuityVerdict {
Forward,
Lifted,
}
#[derive(Default)]
struct WrapState {
initialized: bool,
dts_seen_real: bool,
pts_seen_real: bool,
prev_dts_raw: u64,
prev_dts_uw: i128,
prev_pts_raw: u64,
prev_pts_uw: i128,
discontinuity_offset: i128,
}
impl WrapState {
fn push(&mut self, raw_pts: u64, raw_dts: u64) -> (i128, i128) {
if !self.initialized {
self.initialized = true;
self.dts_seen_real = raw_dts != 0;
self.pts_seen_real = raw_pts != 0;
self.prev_dts_raw = raw_dts;
self.prev_dts_uw = raw_dts as i128;
self.prev_pts_raw = raw_pts;
self.prev_pts_uw = raw_pts as i128;
return (self.prev_pts_uw, self.prev_dts_uw);
}
let dts_uw = if self.dts_seen_real {
unwrap_ts(self.prev_dts_uw, self.prev_dts_raw, raw_dts)
} else {
self.dts_seen_real = raw_dts != 0;
raw_dts as i128
};
let pts_uw = if self.pts_seen_real {
unwrap_ts(self.prev_pts_uw, self.prev_pts_raw, raw_pts)
} else {
self.pts_seen_real = raw_pts != 0;
raw_pts as i128
};
self.prev_dts_raw = raw_dts;
self.prev_dts_uw = dts_uw;
self.prev_pts_raw = raw_pts;
self.prev_pts_uw = pts_uw;
(
pts_uw.saturating_add(self.discontinuity_offset),
dts_uw.saturating_add(self.discontinuity_offset),
)
}
fn rebase_at_discontinuity(&mut self, raw_dts: u64, step: i128) -> DiscontinuityVerdict {
if !self.initialized {
return DiscontinuityVerdict::Forward;
}
let next_uw = if self.dts_seen_real {
unwrap_ts(self.prev_dts_uw, self.prev_dts_raw, raw_dts)
} else {
raw_dts as i128
};
let last_emitted = self.prev_dts_uw.saturating_add(self.discontinuity_offset);
let next_emitted = next_uw.saturating_add(self.discontinuity_offset);
let slack = step.max(1);
if next_emitted + slack > last_emitted {
return DiscontinuityVerdict::Forward;
}
let lift = last_emitted
.saturating_sub(next_emitted)
.saturating_add(slack);
self.discontinuity_offset = self.discontinuity_offset.saturating_add(lift);
self.prev_dts_raw = raw_dts;
self.prev_dts_uw = next_uw;
self.prev_pts_raw = raw_dts;
self.prev_pts_uw = next_uw;
self.dts_seen_real = true;
self.pts_seen_real = true;
DiscontinuityVerdict::Lifted
}
}
enum Carrier {
Pes(PesAssembler),
Section(SectionReassembler),
}
fn initial_carrier(codec: Codec) -> Carrier {
match codec {
Codec::Data(stream_type) if data_carriage(stream_type) == DataCarriage::Sections => {
Carrier::Section(SectionReassembler::default())
}
_ => Carrier::Pes(PesAssembler::new()),
}
}
struct StreamState {
codec: Codec,
descriptors: Vec<u8>,
carrier: Carrier,
pes_bytes: usize,
fallback: (u64, u64),
frame_period: Option<u64>,
last_stamped_dts: Option<u64>,
units_since_stamped: u64,
has_any: bool,
wrap: WrapState,
track: Option<TrackState>,
last_frame_period: i128,
recent_steps: Vec<u64>,
pending_rebase: bool,
current_pes_intact: bool,
pes_declared_len: u16,
pes_received: usize,
backlog_bytes: usize,
probe_tail: Vec<u8>,
probe_aus: u32,
probe_walks: u32,
probe_walked_len: usize,
payload_packets: u32,
damaged_promotion: bool,
declared_at_packet: u64,
unit_released: bool,
}
impl StreamState {
fn restart_timeline(&mut self) {
self.carrier = initial_carrier(self.codec);
self.pes_bytes = 0;
self.fallback = (0, 0);
self.frame_period = None;
self.last_stamped_dts = None;
self.units_since_stamped = 0;
self.has_any = false;
self.wrap = WrapState::default();
self.last_frame_period = 0;
self.recent_steps.clear();
self.pending_rebase = false;
self.current_pes_intact = true;
self.pes_declared_len = 0;
self.pes_received = 0;
match &mut self.track {
Some(TrackState::Probing { backlog, .. }) => {
backlog.clear();
self.backlog_bytes = 0;
self.probe_tail.clear();
}
Some(TrackState::Live(live)) => match &mut live.kind {
LiveKind::Audio { anchor, .. } => {
anchor.seed = None;
anchor.carry.clear();
anchor.carry_provenance = None;
}
LiveKind::Video { paff, .. } => paff.first_field = None,
LiveKind::Data { .. } | LiveKind::MpegH { .. } | LiveKind::Section => {}
},
_ => {}
}
}
}
#[allow(clippy::too_many_arguments)]
fn advance_one_behind(
pending: &mut Option<PendingOneBehind>,
last_duration: &mut u32,
data: Vec<u8>,
is_sync: bool,
pts_uw: i128,
dts_uw: i128,
duration_from_pts: bool,
track_id: TrackId,
provenance: EventProvenance,
display: Option<Arc<DisplayInfo>>,
pending_events: PendingDisplayEvents,
events: &mut VecDeque<DemuxEvent>,
) {
if let Some(prev) = pending.take() {
let duration = if duration_from_pts {
(pts_uw - prev.pts_uw).max(0) as u32
} else {
(dts_uw - prev.dts_uw).max(0) as u32
};
*last_duration = duration;
let dts = to_ticks(prev.dts_uw);
let pts = to_ticks(prev.pts_uw);
if let Some(spec) = prev.pending_events.config {
events.push_back(DemuxEvent::TrackUpdated(spec));
}
if let Some((display, changed)) = prev.pending_events.display {
events.push_back(DemuxEvent::DisplayChanged {
track_id,
display,
changed,
dts: Some(dts),
pts: Some(pts),
random_access: prev.is_sync,
provenance,
});
}
events.push_back(DemuxEvent::Sample {
track_id,
sample: Sample {
data: prev.data.into(),
dts: Some(dts),
pts: Some(pts),
duration: Some(duration),
flags: SampleFlags::new(prev.is_sync),
provenance: Some(ts_provenance(dts, pts)),
display: prev.display,
},
});
}
*pending = Some(PendingOneBehind {
data,
is_sync,
pts_uw,
dts_uw,
display,
pending_events: PendingDisplayEvents::default(),
});
if let Some(spec) = pending_events.config {
events.push_back(DemuxEvent::TrackUpdated(spec));
}
let pending_events = PendingDisplayEvents {
config: None,
display: pending_events.display,
};
if let Some((display, changed)) = pending_events.display {
events.push_back(DemuxEvent::DisplayChanged {
track_id,
display,
changed,
dts: Some(to_ticks(dts_uw)),
pts: Some(to_ticks(pts_uw)),
random_access: is_sync,
provenance,
});
}
}
fn flush_one_behind(
pending: &mut Option<PendingOneBehind>,
last_duration: u32,
track_id: TrackId,
provenance: EventProvenance,
events: &mut VecDeque<DemuxEvent>,
) {
if let Some(p) = pending.take() {
let dts = to_ticks(p.dts_uw);
let pts = to_ticks(p.pts_uw);
if let Some(spec) = p.pending_events.config {
events.push_back(DemuxEvent::TrackUpdated(spec));
}
if let Some((display, changed)) = p.pending_events.display {
events.push_back(DemuxEvent::DisplayChanged {
track_id,
display,
changed,
dts: Some(dts),
pts: Some(pts),
random_access: p.is_sync,
provenance,
});
}
events.push_back(DemuxEvent::Sample {
track_id,
sample: Sample {
data: p.data.into(),
dts: Some(dts),
pts: Some(pts),
duration: Some(last_duration),
flags: SampleFlags::new(p.is_sync),
provenance: Some(ts_provenance(dts, pts)),
display: p.display,
},
});
}
}
fn annexb_to_length_prefixed_no_aud(annexb: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(annexb.len());
for nal in iter_annexb_nals(annexb) {
if nal_unit_type(NalCodec::Avc, nal) == Some(crate::slice_header::NAL_AUD) {
continue;
}
out.extend_from_slice(&(nal.len() as u32).to_be_bytes());
out.extend_from_slice(nal);
}
out
}
fn video_sample_bytes(codec: VideoCodec, au_data: &[u8]) -> (Vec<u8>, bool) {
match codec {
VideoCodec::H264 => {
let is_rap = access_unit_is_rap(NalCodec::Avc, au_data, false);
(annexb_to_length_prefixed(au_data), is_rap)
}
VideoCodec::Hevc => {
let mut irap = false;
for nal in iter_annexb_nals(au_data) {
if is_keyframe_nal(NalCodec::Hevc, nal) {
irap = true;
}
}
(annexb_to_length_prefixed(au_data), irap)
}
VideoCodec::Mpeg2 => (au_data.to_vec(), mpeg2_is_sync(au_data)),
}
}
fn scale_plan_durations(plan: &mut Option<AudioPlan<'_>>, scale: u32) {
if scale > 1
&& let Some(p) = plan
{
for f in &mut p.frames {
f.duration = f.duration.saturating_mul(scale);
}
}
}
struct PlannedFrame<'a> {
data: Option<alloc::borrow::Cow<'a, [u8]>>,
duration: u32,
from_carry: bool,
start: usize,
end: usize,
}
struct AudioPlan<'a> {
frames: Vec<PlannedFrame<'a>>,
tail: &'a [u8],
commences: bool,
carry_ok: bool,
}
fn plan_with<'a, F: FrameSync>(
data: &'a [u8],
carry_len: usize,
shape: impl Fn(&F::Hdr) -> (usize, u32),
) -> AudioPlan<'a> {
let split = split_frames_with_tail::<F>(data);
let carry_ok = carry_len == 0
|| match split.frames.first() {
Some(&(0, f, _)) => {
data.len() == f.len() || !matches!(F::parse(&data[f.len()..]), SyncParse::NotSync)
}
Some(_) => false,
None => split.tail_start == 0,
};
let frames: Vec<PlannedFrame<'a>> = split
.frames
.iter()
.map(|&(start, f, ref h)| {
let (header_len, duration) = shape(h);
PlannedFrame {
data: F::payload(h, f, header_len),
duration,
from_carry: start < carry_len,
start,
end: start + f.len(),
}
})
.collect();
let tail = &data[split.tail_start..];
let commences = frames.iter().any(|f| !f.from_carry)
|| (!tail.is_empty() && split.tail_start >= carry_len && F::tail_starts_new_unit(tail));
AudioPlan {
frames,
tail,
commences,
carry_ok,
}
}
fn plan_audio_frames<'a>(
kind: &AudioKind,
data: &'a [u8],
carry_len: usize,
) -> Option<AudioPlan<'a>> {
match kind {
AudioKind::Aac => Some(plan_with::<AdtsSync>(data, carry_len, |h| {
(
adts_header_len(h),
AAC_SAMPLES_PER_FRAME * (h.num_raw_data_blocks as u32 + 1),
)
})),
AudioKind::MpegAudio { samples_per_frame } => {
let spf = *samples_per_frame;
Some(plan_with::<MpegAudioSync>(data, carry_len, move |_| {
(0, spf)
}))
}
AudioKind::Ac3 => Some(plan_with::<Ac3Sync>(data, carry_len, |_| {
(0, AC3_SAMPLES_PER_SYNCFRAME)
})),
AudioKind::Eac3 => Some(plan_with::<Eac3Sync>(data, carry_len, |h| {
(0, h.samples_per_frame())
})),
AudioKind::Dts => Some(plan_with::<DtsSync>(data, carry_len, |h| {
(0, h.samples_per_frame)
})),
}
}
fn carried_frame_remainder(live: &LiveTrack, data: &[u8]) -> usize {
let LiveKind::Audio {
kind: AudioKind::Aac,
anchor,
..
} = &live.kind
else {
return 0;
};
if anchor.carry.is_empty() {
return 0;
}
let mut head = [0u8; ADTS_HEADER_SIZE];
let have = anchor.carry.len().min(ADTS_HEADER_SIZE);
head[..have].copy_from_slice(&anchor.carry[..have]);
let need = ADTS_HEADER_SIZE - have;
if need > data.len() {
return data.len();
}
head[have..].copy_from_slice(&data[..need]);
match AdtsSync::parse(&head) {
SyncParse::Frame(_, flen) => flen.saturating_sub(anchor.carry.len()).min(data.len()),
_ => 0,
}
}
fn clear_audio_carry(stream: &mut StreamState, events: &mut VecDeque<DemuxEvent>) {
if let Some(TrackState::Live(LiveTrack {
track_id,
kind: LiveKind::Audio { kind, anchor, .. },
..
})) = stream.track.as_mut()
{
if anchor.hold_final_au {
flush_audio_carry(kind, anchor, *track_id, events);
}
anchor.carry.clear();
}
}
#[allow(clippy::too_many_arguments)]
fn emit_audio_au(
kind: &AudioKind,
sample_rate: u32,
anchor: &mut AudioAnchor,
au_data: &[u8],
pts_uw: i128,
dts_uw: i128,
track_id: TrackId,
events: &mut VecDeque<DemuxEvent>,
) {
let carry = core::mem::take(&mut anchor.carry);
let joined: Vec<u8>;
let (data, carry_len): (&[u8], usize) = if carry.is_empty() {
(au_data, 0)
} else {
joined = [carry.as_slice(), au_data].concat();
(joined.as_slice(), carry.len())
};
let mut plan = plan_audio_frames(kind, data, carry_len);
if carry_len > 0 && plan.as_ref().is_none_or(|p| !p.carry_ok) {
plan = plan_audio_frames(kind, au_data, 0);
}
scale_plan_durations(&mut plan, anchor.duration_scale);
let lead_ticks: i64 = plan.as_ref().map_or(0, |p| {
p.frames
.iter()
.filter(|f| f.from_carry)
.map(|f| f.duration as i64)
.sum()
});
let seed = anchor.seed;
let mut fresh_anchor = match &seed {
None => true,
Some(seed) => {
let expected_dts_uw = seed.anchor_dts_uw
+ ((seed.ticks_since_anchor + lead_ticks) as i128 * VIDEO_TIMESCALE as i128)
/ sample_rate.max(1) as i128;
(dts_uw - expected_dts_uw).abs() > audio_discontinuity_threshold_90k(sample_rate)
}
};
if seed.is_some() && plan.as_ref().is_some_and(|p| !p.commences) {
fresh_anchor = false;
}
let mut held_done = 0usize;
let mut held_ticks = 0i64;
if fresh_anchor
&& carry_len > 0
&& let (Some(seed), Some(p)) = (&seed, &plan)
{
for f in p
.frames
.iter()
.take_while(|f| f.from_carry && f.end <= carry_len)
{
if let Some(payload) = &f.data {
let mut sample = Sample::from_raw(
payload.to_vec(),
Some(seed.next_dts + held_ticks),
Some(seed.next_pts + held_ticks),
Some(f.duration),
);
if let Some(prov) = anchor.carry_provenance {
sample = sample.with_provenance(prov);
}
events.push_back(DemuxEvent::Sample { track_id, sample });
}
held_ticks += f.duration as i64;
held_done += 1;
}
}
let lead_ticks = lead_ticks - held_ticks;
if fresh_anchor && seed.is_some() {
events.push_back(DemuxEvent::Discontinuity {
track: Some(track_id),
kind: DiscontinuityKind::TimelineReanchored,
provenance: EventProvenance::default(),
});
}
let (dts0, pts0) = match (fresh_anchor, &seed) {
(false, Some(seed)) => (seed.next_dts, seed.next_pts),
_ => (
rescale_to_track(dts_uw, sample_rate) - lead_ticks,
rescale_to_track(pts_uw, sample_rate) - lead_ticks,
),
};
if matches!(kind, AudioKind::Eac3)
&& !anchor.hold_final_au
&& let Some(p) = &plan
&& p.frames.iter().any(|f| {
matches!(
Eac3Sync::parse(&data[f.start..f.end]),
SyncParse::Frame(_, first_len) if first_len < f.end - f.start
)
})
{
anchor.hold_final_au = true;
}
let mut elapsed = 0u64;
let held_from: Option<usize> = plan.as_ref().and_then(|p| {
let last = p.frames.last()?;
let tail_start = data.len() - p.tail.len();
(matches!(kind, AudioKind::Eac3)
&& anchor.hold_final_au
&& last.data.is_some()
&& last.end == tail_start
&& data.len() - last.start <= AUDIO_CARRY_MAX_BYTES)
.then_some(last.start)
});
let au_provenance = ts_provenance(to_ticks(dts_uw), to_ticks(pts_uw));
let audio_sample = |data: Vec<u8>, duration: u32, elapsed: u64| -> Sample {
let dts = dts0 + elapsed as i64;
let pts = pts0 + elapsed as i64;
Sample::from_raw(data, Some(dts), Some(pts), Some(duration)).with_provenance(au_provenance)
};
match kind {
AudioKind::Aac
| AudioKind::MpegAudio { .. }
| AudioKind::Ac3
| AudioKind::Eac3
| AudioKind::Dts => {
if let Some(plan) = &plan {
for (i, f) in plan.frames.iter().enumerate().skip(held_done) {
if held_from.is_some() && i + 1 == plan.frames.len() {
break;
}
if let Some(payload) = &f.data {
events.push_back(DemuxEvent::Sample {
track_id,
sample: audio_sample(payload.to_vec(), f.duration, elapsed),
});
}
elapsed += f.duration as u64;
}
}
}
}
let (anchor_dts_uw, anchor_pts_uw, ticks_since_anchor) = match (fresh_anchor, &seed) {
(false, Some(seed)) => (
seed.anchor_dts_uw,
seed.anchor_pts_uw,
seed.ticks_since_anchor,
),
_ => (dts_uw, pts_uw, -lead_ticks),
};
anchor.seed = Some(AudioAnchorSeed {
next_dts: dts0 + elapsed as i64,
next_pts: pts0 + elapsed as i64,
anchor_dts_uw,
anchor_pts_uw,
ticks_since_anchor: ticks_since_anchor + elapsed as i64,
});
if let Some(from) = held_from {
anchor.carry = data[from..].to_vec();
anchor.carry_provenance = Some(au_provenance);
} else if let Some(plan) = &plan
&& plan.tail.len() <= AUDIO_CARRY_MAX_BYTES
{
anchor.carry = plan.tail.to_vec();
anchor.carry_provenance = Some(au_provenance);
}
}
fn flush_audio_carry(
kind: &AudioKind,
anchor: &mut AudioAnchor,
track_id: TrackId,
events: &mut VecDeque<DemuxEvent>,
) {
let carry = core::mem::take(&mut anchor.carry);
let (Some(seed), false) = (anchor.seed, carry.is_empty()) else {
return;
};
let mut plan = plan_audio_frames(kind, &carry, 0);
scale_plan_durations(&mut plan, anchor.duration_scale);
let Some(plan) = plan else {
return;
};
let mut elapsed = 0i64;
for f in &plan.frames {
if let Some(payload) = &f.data {
let mut sample = Sample::from_raw(
payload.to_vec(),
Some(seed.next_dts + elapsed),
Some(seed.next_pts + elapsed),
Some(f.duration),
);
if let Some(p) = anchor.carry_provenance {
sample = sample.with_provenance(p);
}
events.push_back(DemuxEvent::Sample { track_id, sample });
}
elapsed += f.duration as i64;
}
anchor.seed = Some(AudioAnchorSeed {
next_dts: seed.next_dts + elapsed,
next_pts: seed.next_pts + elapsed,
ticks_since_anchor: seed.ticks_since_anchor + elapsed,
..seed
});
}
fn eac3_has_dependent_substreams(config: &CodecConfig) -> bool {
matches!(config, CodecConfig::Eac3 { config, .. }
if config.substreams.iter().any(|s| s.num_dep_sub > 0 || s.chan_loc.is_some()))
}
fn push_live_au(
live: &mut LiveTrack,
data: &[u8],
pts_uw: i128,
dts_uw: i128,
provenance: EventProvenance,
display: Option<Arc<DisplayInfo>>,
events: &mut VecDeque<DemuxEvent>,
) {
let track_id = live.track_id;
match &mut live.kind {
LiveKind::Video {
pending,
last_duration,
codec,
paff,
} => {
use crate::slice_header::AvcPicture;
let raised = PendingDisplayEvents {
config: live.pending_config.take(),
display: live.display.take(),
};
let (bytes, is_sync) = video_sample_bytes(*codec, data);
let mut this_field = None;
if *codec == VideoCodec::H264
&& let AvcPicture::Field(second) = paff.params.classify_au(data)
{
if let (Some(first), Some(p)) = (paff.first_field, pending.as_mut())
&& first.pairs_with(&second)
&& (0..=MAX_FIELD_PAIR_DTS_TICKS).contains(&(dts_uw - p.dts_uw))
{
let raised = PendingDisplayEvents {
config: p.pending_events.config.take().or(raised.config),
display: p.pending_events.display.take().or(raised.display),
};
p.data
.extend_from_slice(&annexb_to_length_prefixed_no_aud(data));
p.pending_events = raised;
paff.first_field = None;
return;
}
this_field = Some(second);
}
paff.first_field = this_field;
advance_one_behind(
pending,
last_duration,
bytes,
is_sync,
pts_uw,
dts_uw,
false,
track_id,
provenance,
display,
raised,
events,
);
}
LiveKind::Data {
pending,
last_duration,
} => {
advance_one_behind(
pending,
last_duration,
data.to_vec(),
true,
pts_uw,
dts_uw,
true,
track_id,
provenance,
display,
PendingDisplayEvents::default(),
events,
);
}
LiveKind::Audio {
sample_rate,
kind,
anchor,
} => {
anchor.hold_final_au |= eac3_has_dependent_substreams(&live.config);
emit_audio_au(
kind,
*sample_rate,
anchor,
data,
pts_uw,
dts_uw,
track_id,
events,
);
}
LiveKind::MpegH {
pending,
last_duration,
} => {
let is_sync = find_mpegh3da_config(data).is_some();
advance_one_behind(
pending,
last_duration,
data.to_vec(),
is_sync,
pts_uw,
dts_uw,
true,
track_id,
provenance,
display,
PendingDisplayEvents::default(),
events,
);
}
LiveKind::Section => {
events.push_back(DemuxEvent::Sample {
track_id,
sample: Sample::from_raw(data.to_vec(), None, None, None),
});
}
}
}
fn reprobe_if_config_changed(
stream: &mut StreamState,
live: &mut LiveTrack,
data: &[u8],
) -> Option<(CodecConfig, u32)> {
let changed = match stream.codec {
Codec::H264 | Codec::Hevc => {
observe_parameter_sets(stream.codec, data, &mut live.parameter_sets)
}
Codec::Aac => {
let (_, hdr) = find_adts_sync(data)?;
let asc = AudioSpecificConfig::from_adts_header(&hdr)
.try_to_bytes()
.ok()?;
if live.config_header.as_deref() == Some(asc.as_slice()) {
false
} else {
live.config_header = Some(asc);
true
}
}
_ => return None,
};
if !changed {
return None;
}
let descriptors = stream.descriptors.clone();
let mut probe = initial_probe(stream.codec);
seed_probe_parameter_sets(&mut probe, &live.parameter_sets);
let recovered = finalize_probe(stream.codec, &descriptors, &mut probe, data);
let (config, timescale, _kind) = recovered?;
Some((config, timescale))
}
fn nal_codec_of(codec: Codec) -> NalCodec {
match codec {
Codec::Hevc => NalCodec::Hevc,
_ => NalCodec::Avc,
}
}
fn count_invalid_parameter_sets(codec: Codec, data: &[u8]) -> usize {
let mut n = 0;
match codec {
Codec::H264 => {
for nal in iter_annexb_nals(data) {
let kind = match nal[0] & H264_NAL_TYPE_MASK {
H264_NAL_SPS => ParamKind::Sps,
H264_NAL_PPS => ParamKind::Pps,
_ => continue,
};
n += usize::from(!parameter_set_is_valid(nal_codec_of(codec), kind, nal));
}
}
Codec::Hevc => {
for nal in iter_annexb_nals(data) {
let kind = match nal_unit_type(NalCodec::Hevc, nal) {
Some(H265_NAL_VPS) => ParamKind::Vps,
Some(H265_NAL_SPS) => ParamKind::Sps,
Some(H265_NAL_PPS) => ParamKind::Pps,
_ => continue,
};
n += usize::from(!parameter_set_is_valid(nal_codec_of(codec), kind, nal));
}
}
_ => {}
}
n
}
#[derive(Default, Clone)]
struct ParameterSets {
vps: ParamSlot,
sps: ParamSlot,
pps: ParamSlot,
}
const MAX_SEEN_PARAMETER_SETS: usize = 64;
#[derive(Default, Clone)]
struct ParamSlot {
current: Option<Vec<u8>>,
by_id: Vec<(u64, Vec<u8>)>,
}
impl ParamSlot {
fn record_current(&mut self, bytes: &[u8]) -> bool {
if self.current.as_deref() == Some(bytes) {
return false;
}
self.current = Some(bytes.to_vec());
true
}
fn record_pps(&mut self, id: u64, bytes: &[u8]) -> bool {
match self.by_id.iter_mut().find(|(i, _)| *i == id) {
Some((_, known)) if known.as_slice() == bytes => return false,
Some((_, known)) => *known = bytes.to_vec(),
None => {
if self.by_id.len() >= MAX_SEEN_PARAMETER_SETS {
self.by_id.remove(0);
}
self.by_id.push((id, bytes.to_vec()));
}
}
self.current = Some(bytes.to_vec());
true
}
}
fn pps_id(codec: Codec, nal: &[u8]) -> u64 {
let header_len = match codec {
Codec::Hevc => 2,
_ => 1,
};
nal.get(header_len..)
.and_then(|body| crate::bitreader::BitReader::with_unescape(body, "pps id").ok())
.and_then(|mut r| r.read_ue("pic_parameter_set_id").ok())
.unwrap_or(u64::MAX)
}
impl ParameterSets {
fn observe(&mut self, codec: Codec, data: &[u8]) -> bool {
let mut changed = false;
match codec {
Codec::H264 => {
for nal in iter_annexb_nals(data) {
match nal[0] & H264_NAL_TYPE_MASK {
H264_NAL_SPS
if parameter_set_is_valid(nal_codec_of(codec), ParamKind::Sps, nal) =>
{
changed |= self.sps.record_current(nal)
}
H264_NAL_PPS
if parameter_set_is_valid(nal_codec_of(codec), ParamKind::Pps, nal) =>
{
changed |= self.pps.record_pps(pps_id(codec, nal), nal)
}
_ => {}
}
}
}
Codec::Hevc => {
for nal in iter_annexb_nals(data) {
match nal_unit_type(NalCodec::Hevc, nal) {
Some(H265_NAL_VPS)
if parameter_set_is_valid(nal_codec_of(codec), ParamKind::Vps, nal) =>
{
changed |= self.vps.record_current(nal)
}
Some(H265_NAL_SPS)
if parameter_set_is_valid(nal_codec_of(codec), ParamKind::Sps, nal) =>
{
changed |= self.sps.record_current(nal)
}
Some(H265_NAL_PPS)
if parameter_set_is_valid(nal_codec_of(codec), ParamKind::Pps, nal) =>
{
changed |= self.pps.record_pps(pps_id(codec, nal), nal)
}
_ => {}
}
}
}
_ => {}
}
changed
}
}
fn observe_parameter_sets(codec: Codec, data: &[u8], sets: &mut ParameterSets) -> bool {
sets.observe(codec, data)
}
fn harvest_damaged_au(
stream: &mut StreamState,
pid: Pid,
data: &[u8],
events: &mut VecDeque<DemuxEvent>,
) {
if !matches!(stream.codec, Codec::H264 | Codec::Hevc) {
return;
}
if stream.probe_aus > 0 || stream.backlog_bytes > 0 {
return;
}
let Ok(pes) = PesPacket::parse(data) else {
return;
};
if pes.payload.is_empty() {
return;
}
let data = pes.payload;
let track_id = match &stream.track {
Some(TrackState::Live(live)) => Some(live.track_id),
_ => None,
};
let _ = track_id;
for _ in 0..count_invalid_parameter_sets(stream.codec, data) {
events.push_back(DemuxEvent::InputDegraded {
track: track_id,
kind: InputDegradation::InvalidParameterSet,
provenance: EventProvenance {
pid: Some(pid),
packet_index: None,
program_number: None,
},
});
}
if !matches!(stream.track, Some(TrackState::Probing { .. })) {
return;
}
let descriptors = stream.descriptors.clone();
let Some(track) = stream.track.take() else {
stream.track = None;
return;
};
let new_track = match track {
TrackState::Probing { mut probe, backlog } => {
let resolved = finalize_probe(stream.codec, &descriptors, &mut probe, data);
match resolved {
Some((config, timescale, kind)) => {
stream.damaged_promotion = true;
TrackState::Parked {
config,
timescale,
kind,
backlog,
}
}
None => TrackState::Probing { probe, backlog },
}
}
other => other,
};
stream.track = Some(new_track);
}
fn seed_probe_parameter_sets(probe: &mut ConfigProbe, sets: &ParameterSets) {
match probe {
ConfigProbe::H264 { sps, pps } => {
sps.clone_from(&sets.sps.current);
pps.clone_from(&sets.pps.current);
}
ConfigProbe::Hevc { vps, sps, pps, .. } => {
vps.clone_from(&sets.vps.current);
sps.clone_from(&sets.sps.current);
pps.clone_from(&sets.pps.current);
}
_ => {}
}
}
fn finalize_probe(
codec: Codec,
descriptors: &[u8],
probe: &mut ConfigProbe,
latest: &[u8],
) -> Option<(CodecConfig, u32, LiveKind)> {
record_probe_pass();
match probe {
ConfigProbe::Data => {
let Codec::Data(stream_type) = codec else {
return None;
};
let carriage = data_carriage(stream_type);
let kind = match carriage {
DataCarriage::Pes => LiveKind::Data {
pending: None,
last_duration: 0,
},
DataCarriage::Sections => LiveKind::Section,
};
Some((
CodecConfig::Data {
stream_type,
descriptors: descriptors.to_vec(),
carriage,
},
VIDEO_TIMESCALE,
kind,
))
}
ConfigProbe::H264 { sps, pps } => {
for nal in iter_annexb_nals(latest) {
match nal[0] & H264_NAL_TYPE_MASK {
H264_NAL_SPS
if sps.is_none()
&& parameter_set_is_valid(nal_codec_of(codec), ParamKind::Sps, nal) =>
{
*sps = Some(nal.to_vec())
}
H264_NAL_PPS
if pps.is_none()
&& parameter_set_is_valid(nal_codec_of(codec), ParamKind::Pps, nal) =>
{
*pps = Some(nal.to_vec())
}
_ => {}
}
}
let (sps_bytes, pps_bytes) = (sps.as_ref()?, pps.as_ref()?);
if sps_bytes.len() < 4 {
return None;
}
let info = crate::sps::decode_avc_sps(sps_bytes).ok();
let (width, height) = info
.as_ref()
.map(|i| {
(
i.width.min(u16::MAX as u32) as u16,
i.height.min(u16::MAX as u32) as u16,
)
})
.unwrap_or((0, 0));
let ext = info
.as_ref()
.filter(|i| crate::sps::is_high_profile(i.profile_idc));
let record = AVCDecoderConfigurationRecord {
configuration_version: 1,
profile_indication: sps_bytes[1],
profile_compatibility: sps_bytes[2],
level_indication: sps_bytes[3],
length_size_minus_one: NAL_LENGTH_SIZE_MINUS_ONE,
sps: alloc::vec![AvcSps(sps_bytes.clone())],
pps: alloc::vec![AvcPps(pps_bytes.clone())],
chroma_format: ext.map(|i| i.chroma_format_idc),
bit_depth_luma_minus8: ext.map(|i| i.bit_depth_luma.saturating_sub(8)),
bit_depth_chroma_minus8: ext.map(|i| i.bit_depth_chroma.saturating_sub(8)),
sps_ext: alloc::vec![],
};
Some((
CodecConfig::Avc {
config: AVCConfigurationBox::new(record),
width,
height,
},
VIDEO_TIMESCALE,
LiveKind::Video {
pending: None,
last_duration: 0,
codec: VideoCodec::H264,
paff: PaffPairing::default(),
},
))
}
ConfigProbe::Hevc {
vps,
sps,
pps,
hdr_sei,
} => {
for nal in iter_annexb_nals(latest) {
match nal_unit_type(NalCodec::Hevc, nal) {
Some(H265_NAL_VPS)
if vps.is_none()
&& parameter_set_is_valid(nal_codec_of(codec), ParamKind::Vps, nal) =>
{
*vps = Some(nal.to_vec())
}
Some(H265_NAL_SPS)
if sps.is_none()
&& parameter_set_is_valid(nal_codec_of(codec), ParamKind::Sps, nal) =>
{
*sps = Some(nal.to_vec())
}
Some(H265_NAL_PPS)
if pps.is_none()
&& parameter_set_is_valid(nal_codec_of(codec), ParamKind::Pps, nal) =>
{
*pps = Some(nal.to_vec())
}
Some(HEVC_PREFIX_SEI_NUT) => {
let mut have = HdrStaticMetadata::default();
for kept in hdr_sei.iter() {
have.absorb_hevc_sei_nal(kept);
}
let mut with = have;
with.absorb_hevc_sei_nal(nal);
if with != have
&& let Ok(Some(declarative)) = with.to_hevc_declarative_sei_nal()
{
hdr_sei.clear();
hdr_sei.push(declarative);
}
}
_ => {}
}
}
let sps_bytes = sps.as_ref()?;
let info = crate::sps::decode_hevc_sps(sps_bytes).ok()?;
let width = info.width.min(u16::MAX as u32) as u16;
let height = info.height.min(u16::MAX as u32) as u16;
let mut arrays: Vec<HevcNalArray> = Vec::new();
if let Some(vps_nal) = vps.clone() {
arrays.push(HevcNalArray::new(
true,
H265_NAL_VPS,
alloc::vec![HevcNalUnit::new(vps_nal)],
));
}
arrays.push(HevcNalArray::new(
true,
H265_NAL_SPS,
alloc::vec![HevcNalUnit::new(sps_bytes.clone())],
));
if let Some(pps_nal) = pps.clone() {
arrays.push(HevcNalArray::new(
true,
H265_NAL_PPS,
alloc::vec![HevcNalUnit::new(pps_nal)],
));
}
if !hdr_sei.is_empty() {
arrays.push(HevcNalArray::new(
false,
HEVC_PREFIX_SEI_NUT,
hdr_sei.iter().cloned().map(HevcNalUnit::new).collect(),
));
}
let record = HEVCDecoderConfigurationRecord {
configuration_version: HVCC_CONFIGURATION_VERSION,
general_profile_space: info.general_profile_space,
general_tier_flag: info.general_tier_flag,
general_profile_idc: info.general_profile_idc,
general_profile_compatibility_flags: info.general_profile_compatibility_flags,
general_constraint_indicator_flags: info.general_constraint_indicator_flags,
general_level_idc: info.general_level_idc,
min_spatial_segmentation_idc: HVCC_MIN_SPATIAL_SEGMENTATION_UNSPEC,
parallelism_type: HVCC_PARALLELISM_TYPE_UNKNOWN,
chroma_format_idc: info.chroma_format_idc,
bit_depth_luma_minus8: info.bit_depth_luma.saturating_sub(8),
bit_depth_chroma_minus8: info.bit_depth_chroma.saturating_sub(8),
avg_frame_rate: HVCC_AVG_FRAME_RATE_UNSPEC,
constant_frame_rate: HVCC_CONSTANT_FRAME_RATE_UNSPEC,
num_temporal_layers: HVCC_NUM_TEMPORAL_LAYERS,
temporal_id_nested: false,
length_size_minus_one: NAL_LENGTH_SIZE_MINUS_ONE,
arrays,
};
Some((
CodecConfig::Hevc {
config: HEVCConfigurationBox::new(record),
width,
height,
},
VIDEO_TIMESCALE,
LiveKind::Video {
pending: None,
last_duration: 0,
codec: VideoCodec::Hevc,
paff: PaffPairing::default(),
},
))
}
ConfigProbe::Mpeg2Video => {
let seq = Mpeg2SeqHeader::find(latest).ok()?;
let esds = EsdsBox::new(ESDescriptor::new(
ESDS_VIDEO_ES_ID,
0,
Some(DecoderConfigDescriptor::new(
OTI_MPEG2_VIDEO_MAIN,
STREAM_TYPE_VISUAL,
false,
0,
0,
0,
None,
)),
Some(SLConfigDescriptor::predefined_two()),
));
Some((
CodecConfig::Mpeg2Video {
esds,
width: seq.width,
height: seq.height,
},
VIDEO_TIMESCALE,
LiveKind::Video {
pending: None,
last_duration: 0,
codec: VideoCodec::Mpeg2,
paff: PaffPairing::default(),
},
))
}
ConfigProbe::MpegAudio { is_mpeg2 } => {
let (_, first) = find_mpeg_audio_sync(latest)?;
let sample_rate = first.sample_rate;
let channel_count = first.channels;
let samples_per_frame = first.samples_per_frame;
let oti = if *is_mpeg2 {
OTI_MPEG2_AUDIO
} else {
OTI_MPEG1_AUDIO
};
let esds = EsdsBox::new(ESDescriptor::new(
ESDS_ES_ID,
0,
Some(DecoderConfigDescriptor::new(
oti,
STREAM_TYPE_AUDIO,
false,
0,
first.bitrate_bps,
first.bitrate_bps,
None,
)),
Some(SLConfigDescriptor::predefined_two()),
));
Some((
CodecConfig::MpegAudio {
esds,
layer: first.layer,
channel_count,
sample_rate,
sample_size: AUDIO_SAMPLE_SIZE_BITS,
},
sample_rate,
LiveKind::Audio {
sample_rate,
kind: AudioKind::MpegAudio { samples_per_frame },
anchor: AudioAnchor::default(),
},
))
}
ConfigProbe::Aac => {
let (_, first_hdr) = find_adts_sync(latest)?;
let asc = AudioSpecificConfig::from_adts_header(&first_hdr);
let sample_rate =
SamplingFrequencyIndex::from(first_hdr.sampling_frequency_index).table_hz()?;
let channel_count = crate::flv::aac_channel_count(&asc);
let esds = EsdsBox::new(ESDescriptor::new(
ESDS_ES_ID,
0,
Some(DecoderConfigDescriptor::new(
OTI_MPEG4_AUDIO,
STREAM_TYPE_AUDIO,
false,
0,
0,
0,
Some(DecoderSpecificInfo::new(asc.try_to_bytes().ok()?)),
)),
Some(SLConfigDescriptor::predefined_two()),
));
let config = CodecConfig::Aac {
esds,
channel_count,
sample_rate,
sample_size: AUDIO_SAMPLE_SIZE_BITS,
};
let config = match crate::AudioMetadata::from_es_info(descriptors).he_aac {
Some(profile) => config
.clone()
.with_explicit_he_aac(profile)
.unwrap_or(config),
None => config,
};
let timescale = config.audio_sample_rate().unwrap_or(sample_rate);
Some((
config,
timescale,
LiveKind::Audio {
sample_rate: timescale,
kind: AudioKind::Aac,
anchor: AudioAnchor {
duration_scale: timescale / sample_rate.max(1),
..AudioAnchor::default()
},
},
))
}
ConfigProbe::Ac3 => {
let start = first_sync_offset::<Ac3Sync>(latest)?;
let info = Ac3SyncframeInfo::from_es(&latest[start..]).ok()?;
let sample_rate = info.sample_rate;
let channel_count = info.channel_count() as u16;
let config = info.into_dac3();
Some((
CodecConfig::Ac3 {
config,
channel_count,
sample_rate,
sample_size: AUDIO_SAMPLE_SIZE_BITS,
},
sample_rate,
LiveKind::Audio {
sample_rate,
kind: AudioKind::Ac3,
anchor: AudioAnchor::default(),
},
))
}
ConfigProbe::Eac3 => eac3_probe_result(latest, Eac3Accept::ChainVerified),
ConfigProbe::Dts => {
let info = DtsCoreFrameInfo::from_es(latest).ok()?;
let sample_rate = info.sample_rate;
let channel_count = info.channels as u16;
let config = info.into_ddts();
Some((
CodecConfig::Dts {
config,
codec_fourcc: crate::dts::DTSC_FOURCC,
channel_count,
sample_rate,
sample_size: AUDIO_SAMPLE_SIZE_BITS,
},
sample_rate,
LiveKind::Audio {
sample_rate,
kind: AudioKind::Dts,
anchor: AudioAnchor::default(),
},
))
}
ConfigProbe::MpegH => {
let config_bytes = find_mpegh3da_config(latest)?;
if config_bytes.len() > MPEGH_CONFIG_LEN_MAX {
return None;
}
let profile_level_indication = *config_bytes.first()?;
let config = MHADecoderConfigurationRecord::new(
profile_level_indication,
MPEGH_REFERENCE_CHANNEL_LAYOUT_UNSPECIFIED,
config_bytes.to_vec(),
);
Some((
CodecConfig::MpegH {
config,
channel_count: MPEGH_CHANNEL_COUNT_UNSPECIFIED,
sample_rate: MPEGH_SAMPLE_RATE_UNSPECIFIED,
sample_size: AUDIO_SAMPLE_SIZE_BITS,
},
VIDEO_TIMESCALE,
LiveKind::MpegH {
pending: None,
last_duration: 0,
},
))
}
}
}
fn abandon_backlog(
stream: &mut StreamState,
pid: Pid,
events: &mut VecDeque<DemuxEvent>,
) -> TrackState {
stream.backlog_bytes = 0;
events.push_back(DemuxEvent::TrackAbandoned {
track_id: None,
reason: AbandonReason::BudgetExceeded,
provenance: EventProvenance {
pid: Some(pid),
packet_index: None,
program_number: None,
},
});
TrackState::Abandoned
}
fn advance_track(
stream: &mut StreamState,
pid: Pid,
data: &[u8],
pts_uw: i128,
dts_uw: i128,
events: &mut VecDeque<DemuxEvent>,
) {
let Some(track) = stream.track.take() else {
return;
};
let track_id = match &track {
TrackState::Live(live) => Some(live.track_id),
_ => None,
};
for _ in 0..count_invalid_parameter_sets(stream.codec, data) {
events.push_back(DemuxEvent::InputDegraded {
track: track_id,
kind: InputDegradation::InvalidParameterSet,
provenance: EventProvenance {
pid: Some(pid),
packet_index: None,
program_number: None,
},
});
}
let new_track = match track {
TrackState::Live(mut live) => {
let skip = carried_frame_remainder(&live, data);
let new_config = if live.skip_next_reprobe {
let changed =
observe_parameter_sets(stream.codec, &data[skip..], &mut live.parameter_sets);
if changed || count_invalid_parameter_sets(stream.codec, &data[skip..]) > 0 {
live.skip_next_reprobe = false;
}
None
} else {
reprobe_if_config_changed(stream, &mut live, &data[skip..])
};
let display = live.display(data);
if live.suppress_next_display_change {
live.display = None;
live.suppress_next_display_change = false;
}
if live.damaged_promotion
&& let Some((_, changed)) = &live.display
&& changed.afd
&& !changed.aspect_ratio
&& !changed.output_rect
&& !changed.default_display_rect
&& !changed.video_signal
&& !changed.overscan
&& !changed.bar_data
&& !changed.initial
{
live.display = None;
}
let provenance = EventProvenance {
pid: Some(pid),
packet_index: None,
program_number: None,
};
if let Some((config, timescale)) = new_config {
live.config = config;
live.timescale = timescale;
let updated = TrackSpec::new(live.track_id, live.timescale, live.config.clone())
.with_source(pid, stream.descriptors.clone())
.with_hdr10plus(live.hdr10plus);
if matches!(live.kind, LiveKind::Video { .. }) {
live.pending_config = Some(updated);
push_live_au(&mut live, data, pts_uw, dts_uw, provenance, display, events);
} else {
events.push_back(DemuxEvent::TrackUpdated(updated));
push_live_au(&mut live, data, pts_uw, dts_uw, provenance, display, events);
}
} else {
push_live_au(&mut live, data, pts_uw, dts_uw, provenance, display, events);
}
TrackState::Live(live)
}
TrackState::Abandoned => TrackState::Abandoned,
TrackState::Parked {
config,
timescale,
kind,
mut backlog,
} => {
stream.backlog_bytes = stream.backlog_bytes.saturating_add(data.len());
backlog.push(BufferedAu {
data: data.to_vec(),
pts_uw,
dts_uw,
});
if stream.backlog_bytes > MAX_PROBE_BACKLOG_BYTES {
abandon_backlog(stream, pid, events)
} else {
TrackState::Parked {
config,
timescale,
kind,
backlog,
}
}
}
TrackState::Probing {
mut probe,
mut backlog,
} => {
stream.backlog_bytes = stream.backlog_bytes.saturating_add(data.len());
backlog.push(BufferedAu {
data: data.to_vec(),
pts_uw,
dts_uw,
});
let Some(latest) = backlog.last() else {
return;
};
let eac3_gated = stream.codec == Codec::Eac3
&& stream.probe_walks >= EAC3_PROBE_FULL_WALKS
&& !stream.probe_tail.is_empty()
&& stream.probe_tail.len() + latest.data.len()
< stream.probe_walked_len
+ Eac3Sync::MIN_FRAME_LEN.max(stream.probe_walked_len / 8);
let eac3_all: Option<Vec<u8>> = (stream.codec == Codec::Eac3 && !eac3_gated)
.then(|| [stream.probe_tail.as_slice(), latest.data.as_slice()].concat());
if let Some(all) = &eac3_all
&& !stream.probe_tail.is_empty()
{
stream.probe_walks = stream.probe_walks.saturating_add(1);
stream.probe_walked_len = all.len();
}
let eac3_window: Option<&[u8]> = eac3_all
.as_deref()
.filter(|_| !stream.probe_tail.is_empty());
let mut resolved = match eac3_window {
Some(w) => finalize_probe(stream.codec, &stream.descriptors, &mut probe, w),
None => None,
};
if resolved.is_none() {
resolved =
finalize_probe(stream.codec, &stream.descriptors, &mut probe, &latest.data);
}
if resolved.is_none()
&& stream.codec != Codec::Eac3
&& let Some(window) =
audio_probe_window(stream.codec, &stream.probe_tail, &latest.data)
{
resolved = finalize_probe(stream.codec, &stream.descriptors, &mut probe, &window);
}
stream.probe_aus = stream.probe_aus.saturating_add(1);
if resolved.is_none()
&& let Some(window) = eac3_all.as_deref()
&& window.len() >= Eac3Sync::MIN_FRAME_LEN
{
resolved = eac3_probe_result(window, Eac3Accept::BrokenChain);
if resolved.is_none() {
resolved = eac3_probe_result(window, Eac3Accept::UnverifiedBrokenChain);
}
if resolved.is_none()
&& let Some(start) = first_eac3_independent_offset(window, false)
&& Ec3SyncframeInfo::from_es_all(&window[start..]).len()
>= EAC3_PROBE_MAX_FRAMES
{
resolved = eac3_probe_result(window, Eac3Accept::Anything);
}
}
stream.probe_tail = if resolved.is_some() {
Vec::new()
} else if eac3_gated {
let mut tail = core::mem::take(&mut stream.probe_tail);
tail.extend_from_slice(&latest.data);
if tail.len() > EAC3_PROBE_TAIL_MAX {
tail.clear();
}
tail
} else if let Some(all) = eac3_all.as_deref() {
eac3_probe_tail(all)
} else {
audio_probe_next_tail(stream.codec, &stream.probe_tail, &latest.data)
};
stream.probe_walked_len = if eac3_gated && !stream.probe_tail.is_empty() {
stream.probe_walked_len
} else {
stream.probe_tail.len()
};
match resolved {
Some((config, timescale, kind)) => TrackState::Parked {
config,
timescale,
kind,
backlog,
},
None if stream.backlog_bytes > MAX_PROBE_BACKLOG_BYTES => {
stream.probe_tail = Vec::new();
stream.probe_walked_len = 0;
abandon_backlog(stream, pid, events)
}
None => TrackState::Probing { probe, backlog },
}
}
};
stream.track = Some(new_track);
}
const AUDIO_PROBE_BOUNDARY_BYTES: usize = 16;
const EAC3_PROBE_MAX_FRAMES: usize = 16;
const PROBE_HEAD_BLOCK_AUS: u32 = 512;
const SILENT_HEAD_PARKED_BYTES: usize = MAX_PROBE_BACKLOG_BYTES / 2;
const STUCK_HEAD_OWN_BYTES: usize = MAX_PROBE_BACKLOG_BYTES / 2;
const STUCK_HEAD_INPUT_PACKETS: u64 = 4 * 1024 * 1024 / TS_PACKET_SIZE as u64;
const EAC3_PROBE_FULL_WALKS: u32 = 4096;
const EAC3_PROBE_TAIL_MAX: usize = EAC3_PROBE_MAX_FRAMES * 2 * 2048 + AUDIO_PROBE_BOUNDARY_BYTES;
fn audio_probe_window(codec: Codec, tail: &[u8], latest: &[u8]) -> Option<Vec<u8>> {
if tail.is_empty() {
return None;
}
match codec {
Codec::MpegAudio(_) if first_sync_offset::<MpegAudioSync>(latest).is_some() => None,
Codec::Aac if first_sync_offset::<AdtsSync>(latest).is_some() => None,
Codec::Ac3 if first_sync_offset::<Ac3Sync>(latest).is_some() => None,
Codec::Dts if first_sync_offset::<DtsSync>(latest).is_some() => None,
Codec::MpegAudio(_) | Codec::Aac | Codec::Ac3 | Codec::Dts => {
let head = &latest[..latest.len().min(AUDIO_PROBE_BOUNDARY_BYTES)];
Some([tail, head].concat())
}
Codec::Eac3 => Some([tail, latest].concat()),
_ => None,
}
}
fn audio_probe_next_tail(codec: Codec, tail: &[u8], latest: &[u8]) -> Vec<u8> {
match codec {
Codec::MpegAudio(_) | Codec::Aac | Codec::Ac3 | Codec::Dts => {
let all = [tail, latest].concat();
all[all.len().saturating_sub(AUDIO_PROBE_BOUNDARY_BYTES)..].to_vec()
}
Codec::Eac3 => eac3_probe_tail(&[tail, latest].concat()),
_ => Vec::new(),
}
}
fn eac3_probe_tail(all: &[u8]) -> Vec<u8> {
let start = (0..all.len()).find(|&at| match Eac3Sync::parse(&all[at..]) {
SyncParse::Frame(hdr, _) => hdr.strmtyp != EAC3_STRMTYP_DEPENDENT,
SyncParse::NeedMore => Eac3Sync::is_partial_sync(&all[at..]),
SyncParse::NotSync => false,
});
match start {
Some(start) if all.len() - start <= EAC3_PROBE_TAIL_MAX => all[start..].to_vec(),
_ => Vec::new(),
}
}
fn first_eac3_independent_offset(data: &[u8], crc: bool) -> Option<usize> {
(0..data.len()).find(|&at| match Eac3Sync::parse(&data[at..]) {
SyncParse::Frame(hdr, len) => {
len >= Eac3Sync::MIN_FRAME_LEN
&& hdr.strmtyp != EAC3_STRMTYP_DEPENDENT
&& (!crc
|| data
.get(at..at + len)
.is_some_and(crate::ac3::eac3_frame_crc_ok))
}
_ => false,
})
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Eac3Accept {
ChainVerified,
BrokenChain,
UnverifiedBrokenChain,
Anything,
}
fn eac3_probe_result(latest: &[u8], accept: Eac3Accept) -> Option<(CodecConfig, u32, LiveKind)> {
let verified = matches!(accept, Eac3Accept::ChainVerified | Eac3Accept::BrokenChain);
let start = first_eac3_independent_offset(latest, verified)?;
let mut frames: alloc::vec::Vec<Ec3SyncframeInfo> = match accept {
Eac3Accept::Anything => Ec3SyncframeInfo::from_es_first_au(&latest[start..]),
Eac3Accept::ChainVerified => {
Ec3SyncframeInfo::from_es_first_au_crc_verified(&latest[start..], false)?
}
Eac3Accept::BrokenChain => {
Ec3SyncframeInfo::from_es_first_au_crc_verified(&latest[start..], true)?
}
Eac3Accept::UnverifiedBrokenChain => {
Ec3SyncframeInfo::from_es_first_au_or_broken_chain(&latest[start..])?
}
};
for f in frames
.iter_mut()
.filter(|f| f.strmtyp != EAC3_STRMTYP_DEPENDENT)
{
if let Some(bsmod) =
Ec3SyncframeInfo::first_crc_verified_bsmod(&latest[start..], f.substreamid)
{
f.bsmod = bsmod;
}
}
let info = *frames.first()?;
let sample_rate = info.sample_rate;
let channel_count = info.channel_count() as u16;
let config = Ec3SpecificBox::from_syncframes(&frames).ok()?;
Some((
CodecConfig::Eac3 {
config,
channel_count,
sample_rate,
sample_size: AUDIO_SAMPLE_SIZE_BITS,
},
sample_rate,
LiveKind::Audio {
sample_rate,
kind: AudioKind::Eac3,
anchor: AudioAnchor::default(),
},
))
}
fn first_sync_offset<F: FrameSync>(data: &[u8]) -> Option<usize> {
(0..data.len()).find(
|&at| matches!(F::parse(&data[at..]), SyncParse::Frame(_, len) if len >= F::MIN_FRAME_LEN),
)
}
fn feed_pes_bounded(
stream: &mut StreamState,
pid: Pid,
pusi: bool,
payload: &[u8],
events: &mut VecDeque<DemuxEvent>,
) -> Option<Vec<u8>> {
let Carrier::Pes(assembler) = &mut stream.carrier else {
return None;
};
if pusi {
stream.pes_received = payload.len();
stream.pes_declared_len = if payload.len() >= PES_LENGTH_FIELD_END {
u16::from_be_bytes([payload[4], payload[5]])
} else {
0
};
} else {
stream.pes_received = stream.pes_received.saturating_add(payload.len());
}
if pusi {
stream.pes_bytes = payload.len();
} else if stream.pes_bytes > 0 {
stream.pes_bytes = stream.pes_bytes.saturating_add(payload.len());
}
let completed = assembler.feed(pusi, payload);
if stream.pes_bytes > MAX_PES_BUFFER_BYTES {
let dropped_bytes = stream.pes_bytes as u64;
let _ = assembler.flush();
stream.pes_bytes = 0;
clear_audio_carry(stream, events);
let track = match stream.track.as_ref() {
Some(TrackState::Live(live)) => Some(live.track_id),
_ => None,
};
events.push_back(DemuxEvent::Discontinuity {
track,
kind: DiscontinuityKind::BudgetExceeded {
bytes: dropped_bytes,
},
provenance: EventProvenance {
pid: Some(pid),
packet_index: None,
program_number: None,
},
});
}
completed
}
fn on_completed_pes(
stream: &mut StreamState,
pid: Pid,
pes_bytes: &[u8],
events: &mut VecDeque<DemuxEvent>,
) {
let Ok(pes) = PesPacket::parse(pes_bytes) else {
return;
};
if pes.payload.is_empty() {
return;
}
let (pts, dts, stamped) = match pes.header.as_ref() {
Some(h) => {
let hp = h.pts.map(|p| p.0);
let hd = h.dts.map(|d| d.0);
match (hp, hd) {
(Some(p), Some(d)) => (p, d, true),
(Some(p), None) => (p, p, true),
(None, Some(d)) => (d, d, true),
(None, None) => (0, 0, false),
}
}
None => (0, 0, false),
};
let (pts, dts) = if stamped {
if let Some(prev) = stream.last_stamped_dts
&& !stream.pending_rebase
{
let span = dts.wrapping_sub(prev) & TS_WRAP_MASK;
let periods = stream.units_since_stamped.saturating_add(1);
let period = span / periods;
if period > 0 && period <= MAX_FRAME_PERIOD_TICKS {
if stream.recent_steps.len() == FRAME_PERIOD_WINDOW {
stream.recent_steps.remove(0);
}
stream.recent_steps.push(period);
let mut sorted = stream.recent_steps.clone();
sorted.sort_unstable();
let median = sorted[sorted.len() / 2];
stream.frame_period = Some(median);
stream.last_frame_period = median as i128;
}
}
stream.last_stamped_dts = Some(dts);
stream.units_since_stamped = 0;
stream.fallback = (pts, dts);
stream.has_any = true;
(pts, dts)
} else if stream.has_any {
let step = stream.frame_period.unwrap_or(0);
stream.units_since_stamped = stream.units_since_stamped.saturating_add(1);
let pts = stream.fallback.0.wrapping_add(step) & TS_WRAP_MASK;
let dts = stream.fallback.1.wrapping_add(step) & TS_WRAP_MASK;
stream.fallback = (pts, dts);
(pts, dts)
} else {
stream.units_since_stamped = stream.units_since_stamped.saturating_add(1);
(0, 0)
};
if stream.pending_rebase {
stream.pending_rebase = stream
.wrap
.rebase_at_discontinuity(dts, stream.last_frame_period)
== DiscontinuityVerdict::Forward;
}
let (pts_uw, dts_uw) = stream.wrap.push(pts, dts);
advance_track(stream, pid, pes.payload, pts_uw, dts_uw, events);
}
fn on_completed_section(
stream: &mut StreamState,
pid: Pid,
section: &[u8],
events: &mut VecDeque<DemuxEvent>,
) {
if section.is_empty() {
return;
}
advance_track(stream, pid, section, 0, 0, events);
}
pub use crate::ir::{
AbandonReason, DegradationRecord, DemuxEvent, DiscontinuityKind, EventProvenance,
InputDegradation,
};
const PCR_CLOCK_HZ: u32 = broadcast_common::clock33::PCR_HZ as u32;
#[derive(Debug, Clone)]
struct CcState {
initialized: bool,
last_cc: u8,
last_packet: Option<[u8; TS_PACKET_SIZE]>,
dup_used_for_last: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CcVerdict {
Deliver,
Duplicate,
Gap,
}
pub struct StreamingTsDemux {
resync: TsResync,
packet_index: u64,
degraded_cap: Option<usize>,
degraded_dropped: u64,
queue_scanned: usize,
degraded_queued: usize,
pat_reasm: SectionReassembler,
pmt_reasm: BTreeMap<Pid, PmtState>,
es_seen: BTreeSet<Pid>,
streams: BTreeMap<Pid, StreamState>,
unattributed: BTreeMap<Pid, VecDeque<(bool, Vec<u8>)>>,
removed_pids: BTreeSet<Pid>,
es_declarers: BTreeMap<Pid, BTreeSet<Pid>>,
unattributed_order: VecDeque<u16>,
unattributed_bytes: usize,
codec_order: Vec<Pid>,
data_order: Vec<Pid>,
resolved: BTreeSet<Pid>,
skipped: BTreeSet<Pid>,
next_track_id: TrackId,
events: VecDeque<DemuxEvent>,
generation: u32,
tracks_resolved_signalled_at: Option<u32>,
cc_states: BTreeMap<Pid, CcState>,
afd_persistence: AfdPersistence,
strict_pmt_order: bool,
}
struct PmtState {
reasm: SectionReassembler,
program_number: u16,
pcr_pid: Option<Pid>,
last_applied_version: Option<u8>,
applied_es: BTreeSet<Pid>,
}
impl Default for StreamingTsDemux {
fn default() -> Self {
Self::new()
}
}
impl StreamingTsDemux {
pub fn new() -> Self {
Self {
resync: TsResync::new(),
packet_index: 0,
degraded_cap: None,
degraded_dropped: 0,
queue_scanned: 0,
degraded_queued: 0,
pat_reasm: SectionReassembler::default(),
pmt_reasm: BTreeMap::new(),
es_seen: BTreeSet::new(),
streams: BTreeMap::new(),
unattributed: BTreeMap::new(),
removed_pids: BTreeSet::new(),
es_declarers: BTreeMap::new(),
unattributed_order: VecDeque::new(),
unattributed_bytes: 0,
codec_order: Vec::new(),
data_order: Vec::new(),
resolved: BTreeSet::new(),
skipped: BTreeSet::new(),
next_track_id: TrackId::new(1),
events: VecDeque::new(),
generation: 0,
tracks_resolved_signalled_at: None,
cc_states: BTreeMap::new(),
afd_persistence: AfdPersistence::UntilReplaced,
strict_pmt_order: false,
}
}
#[must_use]
pub fn with_strict_pmt_order(mut self) -> Self {
self.strict_pmt_order = true;
self
}
#[must_use]
pub fn with_max_degraded_events(mut self, cap: usize) -> Self {
self.degraded_cap = Some(cap);
self
}
#[must_use]
pub fn with_afd_persistence(mut self, policy: AfdPersistence) -> Self {
self.afd_persistence = policy;
self
}
pub fn degraded_events_dropped(&self) -> u64 {
self.degraded_dropped
}
fn enforce_degraded_cap(&mut self) {
let Some(cap) = self.degraded_cap else {
return;
};
let tail: Vec<DemuxEvent> = self.events.drain(self.queue_scanned..).collect();
for ev in tail {
if matches!(ev, DemuxEvent::InputDegraded { .. }) {
if self.degraded_queued >= cap {
self.degraded_dropped += 1;
continue;
}
self.degraded_queued += 1;
}
self.events.push_back(ev);
}
self.queue_scanned = self.events.len();
}
pub fn feed(&mut self, data: &[u8]) {
let packets = self.resync.feed(data);
let mut losses = self.resync.take_losses().into_iter().peekable();
for raw in &packets {
while let Some(loss) = losses.next_if(|l| l.packet_index <= self.packet_index) {
self.push_resync_loss(loss);
}
self.process_packet(raw);
}
for loss in losses {
self.push_resync_loss(loss);
}
self.enforce_degraded_cap();
}
fn push_resync_loss(&mut self, loss: mpeg_ts::resync::ResyncLoss) {
let kind = if loss.truncated_tail {
InputDegradation::TruncatedPacket {
offset: loss.offset,
dropped_bytes: loss.dropped_bytes,
}
} else {
InputDegradation::SyncLoss {
offset: loss.offset,
dropped_bytes: loss.dropped_bytes,
}
};
self.events.push_back(DemuxEvent::InputDegraded {
track: None,
kind,
provenance: EventProvenance {
pid: None,
packet_index: Some(loss.packet_index),
program_number: None,
},
});
}
fn live_track_id(&self, pid: Pid) -> Option<TrackId> {
match self.streams.get(&pid)?.track.as_ref()? {
TrackState::Live(live) => Some(live.track_id),
_ => None,
}
}
fn process_packet(&mut self, raw: &[u8; TS_PACKET_SIZE]) {
let idx = self.packet_index;
self.packet_index += 1;
let Ok(pkt) = TsPacket::parse(raw) else {
return;
};
if pkt.header.tei {
let provenance = EventProvenance {
pid: Some(pkt.header.pid),
packet_index: Some(idx),
program_number: None,
};
self.events.push_back(DemuxEvent::InputDegraded {
track: self.live_track_id(pkt.header.pid),
kind: InputDegradation::TransportError,
provenance,
});
}
let discontinuity_signalled = pkt.header.has_adaptation
&& raw
.get(4)
.is_some_and(|af_len| *af_len > 0 && raw.get(5).is_some_and(|b| b & 0x80 != 0));
if let Some(Ok(af)) = pkt.adaptation_field() {
let provenance = EventProvenance {
pid: Some(pkt.header.pid),
packet_index: Some(idx),
program_number: None,
};
if af.discontinuity_indicator {
let pid = pkt.header.pid;
let program_ok = self.pmt_reasm.values().any(|pmt| pmt.pcr_pid == Some(pid));
if program_ok {
let members: Vec<Pid> = self
.es_declarers
.iter()
.filter(|(_, pmt_pids)| {
pmt_pids.iter().any(|pmt_pid| {
self.pmt_reasm
.get(&Pid::new((pmt_pid).value()))
.is_some_and(|pmt| pmt.pcr_pid == Some(pid))
})
})
.map(|(&es_pid, _)| es_pid)
.collect();
for es_pid in members {
if let Some(stream) = self.streams.get_mut(&es_pid) {
stream.pending_rebase = true;
}
}
}
self.events.push_back(DemuxEvent::Discontinuity {
track: self.live_track_id(pid),
kind: DiscontinuityKind::Signalled,
provenance,
});
}
if let Some(pcr) = af.pcr {
let mut programs: Vec<u16> = self
.pmt_reasm
.values()
.filter(|pmt| pmt.pcr_pid == Some(pkt.header.pid))
.map(|pmt| pmt.program_number)
.collect();
programs.sort_unstable();
programs.dedup();
let pmts_pending = self.pmt_reasm.is_empty()
|| self.pmt_reasm.values().any(|pmt| pmt.pcr_pid.is_none());
let mut emit = |program_number: Option<u16>| {
self.events.push_back(DemuxEvent::ClockReference {
ticks: pcr.as_27mhz(),
clock_hz: PCR_CLOCK_HZ,
discontinuous: af.discontinuity_indicator,
provenance: EventProvenance {
program_number,
..provenance
},
});
};
if programs.is_empty() {
if pmts_pending {
emit(None);
}
} else {
for program in programs {
emit(Some(program));
}
}
}
}
let mut cc_verdict = CcVerdict::Deliver;
if pkt.header.has_payload && pkt.header.pid != NULL_PACKET_PID {
cc_verdict = self.check_cc(
pkt.header.pid,
pkt.header.continuity_counter,
raw,
discontinuity_signalled,
idx,
);
}
let pid = pkt.header.pid;
let pusi = pkt.header.pusi;
let Some(payload) = pkt.payload else {
return;
};
if cc_verdict == CcVerdict::Duplicate {
self.try_promote_ready();
return;
}
if pid == PAT_PID {
self.pat_reasm.feed(payload, pusi);
while let Some(section) = self.pat_reasm.pop_section() {
if !psi_section_crc_ok(§ion) {
continue;
}
if !section_current_next(§ion) {
continue;
}
if let Ok(programs) = parse_pat(§ion) {
for (program_number, pmt_pid) in programs {
self.learn_pmt_pid(pmt_pid, program_number);
}
}
}
return;
}
if let Some(pmt_state) = self.pmt_reasm.get_mut(&pid) {
pmt_state.reasm.feed(payload, pusi);
let mut sections: Vec<Vec<u8>> = Vec::new();
while let Some(section) = pmt_state.reasm.pop_section() {
sections.push(section.to_vec());
}
let program_number = pmt_state.program_number;
let mut to_apply: Option<Vec<(Pid, Codec, Vec<u8>)>> = None;
for section in §ions {
if !psi_section_crc_ok(section) {
continue;
}
let Ok(header) = parse_pmt_section_header(section) else {
continue;
};
if header.program_number != program_number {
continue;
}
if header.section_number != 0 || header.last_section_number != 0 {
continue;
}
if !header.current_next {
continue;
}
if pmt_state.last_applied_version == Some(header.version) {
continue;
}
pmt_state.last_applied_version = Some(header.version);
pmt_state.pcr_pid = Some(header.pcr_pid);
if let Ok(es_list) = parse_pmt(section) {
to_apply = Some(es_list);
}
}
if let Some(es_list) = to_apply {
let old_applied_es = pmt_state.applied_es.clone();
let new_applied_es: BTreeSet<Pid> = es_list.iter().map(|(p, _, _)| *p).collect();
self.apply_pmt_diff(pid, &old_applied_es, es_list);
if let Some(pmt_state) = self.pmt_reasm.get_mut(&pid) {
pmt_state.applied_es = new_applied_es;
}
}
self.try_promote_ready();
return;
}
if let Some(stream) = self.streams.get_mut(&pid) {
stream.payload_packets = stream.payload_packets.saturating_add(1);
let mut sections: Vec<Vec<u8>> = Vec::new();
let gap_here = cc_verdict == CcVerdict::Gap;
let short_of_declared = stream.pes_declared_len > 0
&& stream.pes_received < PES_LENGTH_FIELD_END + stream.pes_declared_len as usize;
let completed_intact = stream.current_pes_intact && !short_of_declared;
let mut completed_pes = None;
if gap_here
&& !pusi
&& matches!(stream.codec, Codec::H264 | Codec::Hevc)
&& matches!(stream.carrier, Carrier::Pes(_))
{
if let Carrier::Pes(assembler) = &mut stream.carrier
&& let Some(bytes) = assembler.flush()
{
harvest_damaged_au(stream, pid, &bytes, &mut self.events);
}
stream.current_pes_intact = false;
clear_audio_carry(stream, &mut self.events);
} else if matches!(stream.carrier, Carrier::Pes(_)) {
completed_pes = feed_pes_bounded(stream, pid, pusi, payload, &mut self.events);
} else if let Carrier::Section(reasm) = &mut stream.carrier {
reasm.feed(payload, pusi);
while let Some(s) = reasm.pop_section() {
sections.push(s.to_vec());
}
}
if completed_pes.is_some() && !completed_intact {
clear_audio_carry(stream, &mut self.events);
}
if completed_intact && let Some(completed) = completed_pes {
on_completed_pes(stream, pid, &completed, &mut self.events);
} else if let Some(completed) = completed_pes
&& matches!(stream.codec, Codec::H264 | Codec::Hevc)
{
harvest_damaged_au(stream, pid, &completed, &mut self.events);
}
if gap_here {
clear_audio_carry(stream, &mut self.events);
}
if matches!(stream.carrier, Carrier::Pes(_)) {
if pusi {
stream.current_pes_intact = true;
} else if gap_here {
stream.current_pes_intact = false;
if let Carrier::Pes(assembler) = &mut stream.carrier {
let _ = assembler.flush();
}
}
}
for s in sections {
on_completed_section(stream, pid, &s, &mut self.events);
}
} else if pid != NULL_PACKET_PID && !self.removed_pids.contains(&pid) {
self.unattributed
.entry(pid)
.or_default()
.push_back((pusi, payload.to_vec()));
self.unattributed_order.push_back(pid.value());
self.unattributed_bytes += payload.len();
self.evict_unattributed();
}
self.try_promote_ready();
}
fn check_cc(
&mut self,
pid: Pid,
cc: u8,
raw: &[u8; TS_PACKET_SIZE],
discontinuity_signalled: bool,
packet_index: u64,
) -> CcVerdict {
let track = self.live_track_id(pid);
match self.cc_states.entry(pid) {
Entry::Occupied(mut e) => {
let state = e.get_mut();
if !state.initialized {
state.initialized = true;
state.last_cc = cc;
state.last_packet = Some(*raw);
return CcVerdict::Deliver;
}
if cc == state.last_cc
&& let Some(prev) = state.last_packet
{
match broadcast_common::ts_dup::check_duplicate(
&prev,
raw,
state.dup_used_for_last,
) {
broadcast_common::ts_dup::DuplicateVerdict::Legal => {
state.dup_used_for_last = true;
state.last_packet = Some(*raw);
return CcVerdict::Duplicate;
}
broadcast_common::ts_dup::DuplicateVerdict::IllegalThirdRepeat => {
if !discontinuity_signalled {
self.events.push_back(DemuxEvent::InputDegraded {
track,
kind: InputDegradation::ContinuityGap {
expected: (state.last_cc + 1) & 0x0F,
got: cc,
},
provenance: EventProvenance {
pid: Some(pid),
packet_index: Some(packet_index),
program_number: None,
},
});
}
state.last_packet = Some(*raw);
return CcVerdict::Duplicate;
}
_ => {}
}
}
let expected = (state.last_cc + 1) & 0x0F;
let mut verdict = CcVerdict::Deliver;
if !discontinuity_signalled && cc != expected {
self.events.push_back(DemuxEvent::InputDegraded {
track,
kind: InputDegradation::ContinuityGap { expected, got: cc },
provenance: EventProvenance {
pid: Some(pid),
packet_index: Some(packet_index),
program_number: None,
},
});
verdict = CcVerdict::Gap;
}
state.last_cc = cc;
state.last_packet = Some(*raw);
state.dup_used_for_last = false;
verdict
}
Entry::Vacant(e) => {
e.insert(CcState {
initialized: true,
last_cc: cc,
last_packet: Some(*raw),
dup_used_for_last: false,
});
CcVerdict::Deliver
}
}
}
fn learn_pmt_pid(&mut self, pmt_pid: Pid, program_number: u16) {
match self.pmt_reasm.get_mut(&pmt_pid) {
Some(state) if state.program_number != program_number => {
state.program_number = program_number;
state.last_applied_version = None;
}
Some(_) => {}
None => {
self.pmt_reasm.insert(
pmt_pid,
PmtState {
reasm: SectionReassembler::default(),
program_number,
pcr_pid: None,
last_applied_version: None,
applied_es: BTreeSet::new(),
},
);
}
}
}
fn evict_unattributed(&mut self) {
while self.unattributed_bytes > MAX_UNATTRIBUTED_BYTES {
let Some(pid) = self.unattributed_order.pop_front() else {
break;
};
if let Some(buf) = self.unattributed.get_mut(&Pid::new(pid)) {
if let Some((_, payload)) = buf.pop_front() {
self.unattributed_bytes = self.unattributed_bytes.saturating_sub(payload.len());
}
if buf.is_empty() {
self.unattributed.remove(&Pid::new(pid));
}
}
}
}
fn register_new_es(&mut self, es_pid: Pid, codec: Codec, descriptors: Vec<u8>) {
self.register_new_es_at(es_pid, codec, descriptors, None);
}
fn register_new_es_at(
&mut self,
es_pid: Pid,
codec: Codec,
descriptors: Vec<u8>,
reinsert_at: Option<usize>,
) {
self.removed_pids.remove(&es_pid);
let order = if matches!(codec, Codec::Data(_)) {
&mut self.data_order
} else {
&mut self.codec_order
};
match reinsert_at {
Some(idx) if idx <= order.len() => order.insert(idx, es_pid),
_ => order.push(es_pid),
}
let mut stream = StreamState {
codec,
descriptors,
carrier: initial_carrier(codec),
last_frame_period: 0,
recent_steps: Vec::new(),
pending_rebase: false,
current_pes_intact: true,
pes_declared_len: 0,
pes_received: 0,
pes_bytes: 0,
fallback: (0, 0),
frame_period: None,
last_stamped_dts: None,
units_since_stamped: 0,
has_any: false,
wrap: WrapState::default(),
track: Some(TrackState::Probing {
probe: initial_probe(codec),
backlog: Vec::new(),
}),
backlog_bytes: 0,
probe_tail: Vec::new(),
probe_aus: 0,
probe_walks: 0,
probe_walked_len: 0,
payload_packets: 0,
damaged_promotion: false,
declared_at_packet: self.packet_index,
unit_released: false,
};
if let Some(buffered) = self.unattributed.remove(&es_pid) {
for (buf_pusi, buf_payload) in buffered {
stream.payload_packets = stream.payload_packets.saturating_add(1);
self.unattributed_bytes = self.unattributed_bytes.saturating_sub(buf_payload.len());
let mut sections: Vec<Vec<u8>> = Vec::new();
let completed_pes = if matches!(stream.carrier, Carrier::Pes(_)) {
feed_pes_bounded(
&mut stream,
es_pid,
buf_pusi,
&buf_payload,
&mut self.events,
)
} else if let Carrier::Section(reasm) = &mut stream.carrier {
reasm.feed(&buf_payload, buf_pusi);
while let Some(s) = reasm.pop_section() {
sections.push(s.to_vec());
}
None
} else {
None
};
if let Some(completed) = completed_pes {
on_completed_pes(&mut stream, es_pid, &completed, &mut self.events);
}
for s in sections {
on_completed_section(&mut stream, es_pid, &s, &mut self.events);
}
}
}
self.streams.insert(es_pid, stream);
}
fn remove_track(&mut self, pid: Pid) {
self.es_seen.remove(&pid);
self.codec_order.retain(|&p| p != pid);
self.data_order.retain(|&p| p != pid);
self.resolved.remove(&pid);
self.skipped.remove(&pid);
if let Some(buffered) = self.unattributed.remove(&pid) {
for (_, payload) in &buffered {
self.unattributed_bytes = self.unattributed_bytes.saturating_sub(payload.len());
}
}
self.removed_pids.insert(pid);
self.cc_states.remove(&pid);
if let Some(stream) = self.streams.remove(&pid)
&& let Some(TrackState::Live(mut live)) = stream.track
{
if let LiveKind::Audio { kind, anchor, .. } = &mut live.kind
&& anchor.hold_final_au
{
flush_audio_carry(kind, anchor, live.track_id, &mut self.events);
}
self.events.push_back(DemuxEvent::TrackRemoved {
track_id: live.track_id,
provenance: EventProvenance {
pid: Some(pid),
packet_index: None,
program_number: None,
},
});
}
}
fn apply_pmt_diff(
&mut self,
pmt_pid: Pid,
old_applied_es: &BTreeSet<Pid>,
es_list: Vec<(Pid, Codec, Vec<u8>)>,
) {
let new_pids: BTreeSet<Pid> = es_list.iter().map(|(p, _, _)| *p).collect();
let removed: Vec<Pid> = old_applied_es
.iter()
.copied()
.filter(|p| !new_pids.contains(p))
.collect();
for pid in removed {
let declarers = self.es_declarers.get_mut(&pid);
let still_declared = match declarers {
Some(declarers) => {
declarers.remove(&pmt_pid);
let empty = declarers.is_empty();
if empty {
self.es_declarers.remove(&pid);
}
!empty
}
None => false,
};
if !still_declared {
self.remove_track(pid);
}
}
for (es_pid, codec, descriptors) in es_list {
self.es_declarers.entry(es_pid).or_default().insert(pmt_pid);
if self.es_seen.insert(es_pid) {
self.register_new_es(es_pid, codec, descriptors);
continue;
}
let Some(existing) = self.streams.get(&es_pid) else {
continue;
};
let old_codec = existing.codec;
let codec_changed = old_codec != codec;
let descriptors_changed = existing.descriptors != descriptors;
if !codec_changed && !descriptors_changed {
continue;
}
if codec_changed {
let other_declarers_remain = self
.es_declarers
.get(&es_pid)
.is_some_and(|declarers| declarers.iter().any(|&p| p != pmt_pid));
if other_declarers_remain {
continue;
}
let old_is_data = matches!(old_codec, Codec::Data(_));
let new_is_data = matches!(codec, Codec::Data(_));
let order_slot = if old_is_data == new_is_data {
let order = if old_is_data {
&self.data_order
} else {
&self.codec_order
};
order.iter().position(|&p| p == es_pid)
} else {
None
};
self.remove_track(es_pid);
self.es_seen.insert(es_pid);
self.register_new_es_at(es_pid, codec, descriptors, order_slot);
continue;
}
let Some(stream) = self.streams.get_mut(&es_pid) else {
continue;
};
stream.descriptors = descriptors.clone();
if let Some(TrackState::Live(live)) = stream.track.as_ref() {
let spec = TrackSpec::new(live.track_id, live.timescale, live.config.clone())
.with_source(es_pid, descriptors)
.with_hdr10plus(live.hdr10plus);
self.events.push_back(DemuxEvent::TrackUpdated(spec));
}
}
self.generation = self.generation.wrapping_add(1);
}
fn try_promote_ready(&mut self) {
let pending: Vec<Pid> = self
.codec_order
.iter()
.chain(self.data_order.iter())
.copied()
.filter(|p| !self.resolved.contains(p))
.collect();
let mut held_programs: BTreeSet<Option<Pid>> = BTreeSet::new();
for next_pid in pending {
let program = self
.es_declarers
.get(&next_pid)
.and_then(|pmt_pids| pmt_pids.iter().next())
.copied();
if held_programs.contains(&program) {
continue;
}
let Some(stream) = self.streams.get_mut(&next_pid) else {
break;
};
let Some(track) = stream.track.take() else {
break;
};
match track {
TrackState::Parked {
config,
timescale,
kind,
backlog,
} => {
let track_id = self.next_track_id;
self.next_track_id = track_id.next();
let hdr10plus = matches!(config, CodecConfig::Hevc { .. })
&& backlog.first().is_some_and(|au| {
crate::hdr10plus::Hdr10PlusMetadata::annexb_access_unit_carries_hdr10plus(
&au.data,
)
});
let spec = TrackSpec::new(track_id, timescale, config.clone())
.with_source(next_pid, stream.descriptors.clone())
.with_hdr10plus(hdr10plus);
let program_number = self
.es_declarers
.get(&next_pid)
.and_then(|pmt_pids| pmt_pids.iter().next())
.and_then(|&pmt_pid| self.pmt_reasm.get(&pmt_pid))
.map(|state| ProgramNumber::new(state.program_number));
let spec = if let Some(pn) = program_number {
spec.with_program(pn)
} else {
spec
};
let sps_tracker = match stream.codec {
Codec::H264 => Some(
SpsTracker::new(TrackCodec::Avc)
.with_afd_persistence(self.afd_persistence),
),
Codec::Hevc => Some(
SpsTracker::new(TrackCodec::Hevc)
.with_afd_persistence(self.afd_persistence),
),
_ => None,
};
let mpeg2_tracker =
(stream.codec == Codec::Mpeg2Video).then(Mpeg2DisplayTracker::new);
let mut live = LiveTrack {
track_id,
kind,
config,
timescale,
parameter_sets: ParameterSets::default(),
config_header: None,
sps_tracker,
mpeg2_tracker,
pending_config: None,
display: None,
suppress_next_display_change: stream.damaged_promotion,
damaged_promotion: stream.damaged_promotion,
skip_next_reprobe: stream.damaged_promotion,
hdr10plus,
};
let config_geometry =
TrackSpec::new(track_id, timescale, live.config.clone()).display;
let geom_display = if live.mpeg2_tracker.is_some() {
let es: Vec<u8> = backlog
.iter()
.flat_map(|au| au.data.iter().copied())
.collect();
crate::ir::DisplayGeometry::from_mpeg2_es(&es).ok()
} else {
config_geometry
}
.and_then(|g| DisplayInfo::from_geometry(Some(g)));
if let Some(display) = &geom_display
&& let Some(tracker) = live.sps_tracker.as_mut()
{
tracker.seed_state(display.geometry.clone());
}
for au in &backlog {
if let Some(tracker) = live.sps_tracker.as_mut() {
let _ = tracker.observe(&au.data);
}
if let Some(tracker) = live.mpeg2_tracker.as_mut() {
let _ = tracker.observe(&au.data);
}
}
if let Some(tracker) = live.sps_tracker.as_mut() {
let af = tracker.state().and_then(|s| s.active_format.clone());
let geom = geom_display.as_ref().and_then(|d| d.geometry.clone());
tracker.set_state(Some(Arc::new(
DisplayInfo::new(geom).with_active_format(af),
)));
}
let initial_display = live
.sps_tracker
.as_ref()
.and_then(|t| t.state().cloned())
.or_else(|| live.mpeg2_tracker.as_ref().and_then(|t| t.state().cloned()))
.or_else(|| geom_display.clone().map(Arc::new));
self.events.push_back(DemuxEvent::TrackAdded(spec));
if let Some(display) = &initial_display {
self.events.push_back(DemuxEvent::DisplayChanged {
track_id,
display: display.clone(),
changed: DisplayChangeSet::initial(),
dts: None,
pts: None,
random_access: false,
provenance: EventProvenance {
pid: Some(next_pid),
packet_index: None,
program_number: None,
},
});
}
if let Some(display) = &initial_display
&& let Some(tracker) = live.sps_tracker.as_mut()
&& tracker.state().is_none()
{
tracker.seed_full(Some(display.clone()));
}
if let Some(tracker) = live.mpeg2_tracker.as_mut()
&& tracker.state().is_none()
{
tracker.seed_full(initial_display.clone());
}
for au in &backlog {
let _ = observe_parameter_sets(
stream.codec,
&au.data,
&mut live.parameter_sets,
);
}
if stream.codec == Codec::Aac
&& let Some(au) = backlog.last()
&& let Some((_, hdr)) = find_adts_sync(&au.data)
{
live.config_header = AudioSpecificConfig::from_adts_header(&hdr)
.try_to_bytes()
.ok();
}
let provenance = EventProvenance {
pid: Some(next_pid),
packet_index: None,
program_number: None,
};
for au in backlog {
let display = live.display(&au.data);
push_live_au(
&mut live,
&au.data,
au.pts_uw,
au.dts_uw,
provenance,
display,
&mut self.events,
);
}
stream.track = Some(TrackState::Live(live));
stream.backlog_bytes = 0;
self.resolved.insert(next_pid);
if self.skipped.remove(&next_pid) {
self.tracks_resolved_signalled_at = None;
}
}
other @ TrackState::Probing { .. } => {
stream.track = Some(other);
let never_sent = stream.payload_packets == 0;
let aus_stalled = stream.probe_aus >= PROBE_HEAD_BLOCK_AUS
&& !matches!(stream.codec, Codec::H264 | Codec::Hevc | Codec::Mpeg2Video);
let is_video =
matches!(stream.codec, Codec::H264 | Codec::Hevc | Codec::Mpeg2Video);
let no_access_unit = stream.probe_aus == 0
&& usize::try_from(stream.payload_packets)
.map_or(usize::MAX, |n| n.saturating_mul(TS_PACKET_SIZE))
>= STUCK_HEAD_OWN_BYTES;
let declared_at = stream.declared_at_packet;
let released = stream.unit_released;
let silent =
never_sent && self.parked_behind(next_pid) >= SILENT_HEAD_PARKED_BYTES;
let live_stuck = !self.strict_pmt_order
&& (no_access_unit
|| (!is_video
&& self.packet_index.saturating_sub(declared_at)
>= STUCK_HEAD_INPUT_PACKETS));
let stuck = released || live_stuck;
if silent || aus_stalled || stuck {
self.skipped.insert(next_pid);
continue;
}
if self.strict_pmt_order {
break;
}
held_programs.insert(program);
continue;
}
other @ TrackState::Live(_) => {
stream.track = Some(other);
self.resolved.insert(next_pid);
self.skipped.remove(&next_pid);
}
TrackState::Abandoned => {
stream.track = Some(TrackState::Abandoned);
self.resolved.insert(next_pid);
self.skipped.remove(&next_pid);
}
}
}
self.maybe_signal_tracks_resolved();
}
pub fn release_silent_heads(&mut self) {
for stream in self.streams.values_mut() {
if stream.payload_packets == 0
&& matches!(stream.track, Some(TrackState::Probing { .. }))
{
stream.unit_released = true;
}
}
self.try_promote_ready();
}
fn parked_behind(&self, head: Pid) -> usize {
self.codec_order
.iter()
.chain(self.data_order.iter())
.filter(|p| **p != head && !self.resolved.contains(*p))
.filter_map(|p| self.streams.get(p))
.fold(0usize, |sum, s| sum.saturating_add(s.backlog_bytes))
}
fn maybe_signal_tracks_resolved(&mut self) {
let known = self.codec_order.len() + self.data_order.len();
if known == 0 {
return;
}
if self.resolved.len() + self.skipped.len() == known
&& self.tracks_resolved_signalled_at != Some(self.generation)
{
self.tracks_resolved_signalled_at = Some(self.generation);
self.events.push_back(DemuxEvent::TracksResolved {
generation: self.generation,
});
}
}
pub fn poll_event(&mut self) -> Option<DemuxEvent> {
let ev = self.events.pop_front()?;
if self.queue_scanned > 0 {
self.queue_scanned -= 1;
if matches!(ev, DemuxEvent::InputDegraded { .. }) {
self.degraded_queued -= 1;
}
}
Some(ev)
}
pub fn finish(&mut self) {
self.flush_pes_tails();
self.resolve_eac3_probes_from_tail();
self.try_promote_ready();
while let Some(&next_pid) = self
.codec_order
.iter()
.chain(self.data_order.iter())
.find(|p| !self.resolved.contains(p))
{
match self.streams.get(&next_pid).and_then(|s| s.track.as_ref()) {
Some(TrackState::Probing { .. }) => {
self.resolved.insert(next_pid);
self.skipped.remove(&next_pid);
self.events.push_back(DemuxEvent::TrackAbandoned {
track_id: None,
reason: AbandonReason::ConfigUnrecoverable,
provenance: EventProvenance {
pid: Some(next_pid),
packet_index: None,
program_number: None,
},
});
self.try_promote_ready();
}
_ => break,
}
}
self.flush_live_tails();
self.enforce_degraded_cap();
}
pub fn restart_timeline(&mut self) {
self.flush_pes_tails();
for stream in self.streams.values_mut() {
if matches!(stream.track, Some(TrackState::Probing { .. })) {
stream.unit_released = true;
}
}
self.try_promote_ready();
self.flush_live_tails();
for pmt in self.pmt_reasm.values_mut() {
pmt.last_applied_version = None;
}
self.cc_states.clear();
for stream in self.streams.values_mut() {
stream.restart_timeline();
}
self.enforce_degraded_cap();
}
fn flush_pes_tails(&mut self) {
self.resync.finish();
for loss in self.resync.take_losses() {
self.push_resync_loss(loss);
}
for (&pid, stream) in self.streams.iter_mut() {
let completed = match &mut stream.carrier {
Carrier::Pes(assembler) => assembler.flush(),
Carrier::Section(_) => None,
};
if let Some(completed) = completed {
let short_of_declared = stream.pes_declared_len > 0
&& completed.len() < PES_LENGTH_FIELD_END + stream.pes_declared_len as usize;
if stream.current_pes_intact && !short_of_declared {
on_completed_pes(stream, pid, &completed, &mut self.events);
} else if matches!(stream.codec, Codec::H264 | Codec::Hevc) {
harvest_damaged_au(stream, pid, &completed, &mut self.events);
}
}
}
}
fn resolve_eac3_probes_from_tail(&mut self) {
for stream in self.streams.values_mut() {
if stream.codec != Codec::Eac3
|| stream.probe_tail.is_empty()
|| !matches!(stream.track, Some(TrackState::Probing { .. }))
{
continue;
}
let Some(TrackState::Probing { probe, backlog }) = stream.track.take() else {
continue;
};
let resolved = eac3_probe_result(&stream.probe_tail, Eac3Accept::Anything);
stream.probe_tail = Vec::new();
stream.track = Some(match resolved {
Some((config, timescale, kind)) => TrackState::Parked {
config,
timescale,
kind,
backlog,
},
None => TrackState::Probing { probe, backlog },
});
}
}
fn flush_live_tails(&mut self) {
for (&pid, stream) in self.streams.iter_mut() {
if let Some(TrackState::Live(live)) = &mut stream.track {
let provenance = EventProvenance {
pid: Some(pid),
packet_index: None,
program_number: None,
};
match &mut live.kind {
LiveKind::Video {
pending,
last_duration,
..
} => {
flush_one_behind(
pending,
*last_duration,
live.track_id,
provenance,
&mut self.events,
);
}
LiveKind::Data {
pending,
last_duration,
}
| LiveKind::MpegH {
pending,
last_duration,
} => {
flush_one_behind(
pending,
*last_duration,
live.track_id,
provenance,
&mut self.events,
);
}
LiveKind::Audio { kind, anchor, .. } => {
flush_audio_carry(kind, anchor, live.track_id, &mut self.events);
}
LiveKind::Section => {}
}
}
}
}
}
impl Stage for StreamingTsDemux {
type In<'a> = &'a [u8];
type Out = DemuxEvent;
type Error = core::convert::Infallible;
fn feed(&mut self, input: &[u8], _now: Timestamp) -> core::result::Result<(), Self::Error> {
self.feed(input);
Ok(())
}
fn poll(&mut self) -> Option<Self::Out> {
self.poll_event()
}
fn finish(&mut self) -> core::result::Result<(), Self::Error> {
self.finish();
Ok(())
}
fn demand(&self) -> Demand {
if self.unattributed_bytes.saturating_add(TS_MAX_PAYLOAD_BYTES) > MAX_UNATTRIBUTED_BYTES {
Demand::saturated()
} else {
Demand::new(TS_PACKET_SIZE)
}
}
}
#[derive(Debug, Default, Clone)]
pub struct TsDemux<'a> {
_marker: PhantomData<&'a [u8]>,
afd_persistence: AfdPersistence,
}
impl<'a> TsDemux<'a> {
pub fn new() -> Self {
Self {
_marker: PhantomData,
afd_persistence: AfdPersistence::UntilReplaced,
}
}
#[must_use]
pub fn with_afd_persistence(mut self, policy: AfdPersistence) -> Self {
self.afd_persistence = policy;
self
}
pub fn demux(&mut self, input: &'a [u8]) -> Result<Media> {
let mut demux = StreamingTsDemux::new()
.with_afd_persistence(self.afd_persistence)
.with_strict_pmt_order();
demux.feed(input);
demux.finish();
let mut tracks: Vec<Track> = Vec::new();
let mut index_by_id: BTreeMap<TrackId, usize> = BTreeMap::new();
let mut pcr: Vec<PcrSample> = Vec::new();
let mut degraded: Vec<InputDegradation> = Vec::new();
let mut degraded_at: Vec<DegradationRecord> = Vec::new();
while let Some(event) = demux.poll_event() {
match event {
DemuxEvent::TrackAdded(spec) => {
index_by_id.insert(spec.track_id, tracks.len());
tracks.push(Track::new(spec, Vec::new()));
}
DemuxEvent::TrackUpdated(spec) => {
if let Some(&i) = index_by_id.get(&spec.track_id) {
tracks[i].spec = spec;
}
}
DemuxEvent::TrackRemoved { .. } => {}
DemuxEvent::TrackAbandoned { .. } => {}
DemuxEvent::Sample { track_id, sample }
if let Some(&i) = index_by_id.get(&track_id) =>
{
let track = &mut tracks[i];
if track.samples.is_empty()
&& let Some(dts) = sample.dts
{
track.start_decode_time = dts as u64;
}
track.samples.push(sample);
}
DemuxEvent::Sample { .. } => {}
DemuxEvent::ClockReference {
ticks,
discontinuous,
provenance,
..
} => pcr.push(PcrSample {
pcr_27mhz: ticks,
pid: provenance.pid.unwrap_or(Pid::new(0)),
packet_index: provenance.packet_index.unwrap_or(0),
discontinuity: discontinuous,
}),
DemuxEvent::Discontinuity { .. } => {}
DemuxEvent::InputDegraded {
track,
kind,
provenance,
} => {
degraded.push(kind);
degraded_at.push(DegradationRecord::new(kind, track, provenance));
}
DemuxEvent::TracksResolved { .. } => {}
DemuxEvent::DisplayChanged { .. } => {}
}
}
for record in &mut degraded_at {
if record.track.is_none()
&& let Some(pid) = record.provenance.pid
&& let Some(t) = tracks.iter().find(|t| t.spec.source_pid == Some(pid))
{
record.track = Some(t.spec.track_id);
}
}
let mut media = Media::new(tracks, VIDEO_TIMESCALE).with_pcr(pcr);
media.degraded = degraded;
media.degraded_at = degraded_at;
Ok(media)
}
}
impl<'a> Unpackage for TsDemux<'a> {
type Input = &'a [u8];
type Media = Media;
type Error = Error;
fn unpackage(&mut self, input: &'a [u8]) -> Result<Media> {
self.demux(input)
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::ts_mux::TsMux;
use broadcast_common::{Package, Parse};
#[test]
fn data_carriage_classifies_every_known_stream_type() {
assert_eq!(data_carriage(0x06), DataCarriage::Pes, "PES private data");
assert_eq!(data_carriage(0x15), DataCarriage::Pes, "metadata in PES");
assert_eq!(data_carriage(0x7F), DataCarriage::Pes, "unrecognised → PES");
for &st in &[
STREAM_TYPE_PRIVATE_SECTIONS,
STREAM_TYPE_DSMCC_TYPE_A,
STREAM_TYPE_DSMCC_TYPE_B,
STREAM_TYPE_DSMCC_TYPE_C,
STREAM_TYPE_DSMCC_TYPE_D,
STREAM_TYPE_DSMCC_SYNC_DOWNLOAD,
STREAM_TYPE_SCTE35,
] {
assert_eq!(
data_carriage(st),
DataCarriage::Sections,
"stream_type {st:#04X} must be section-carried"
);
}
}
#[test]
fn from_stream_type_unknown_becomes_opaque_data() {
assert_eq!(Codec::from_stream_type(STREAM_TYPE_AVC), Codec::H264);
assert_eq!(Codec::from_stream_type(0x7F), Codec::Data(0x7F));
assert_eq!(
Codec::from_stream_type(STREAM_TYPE_SCTE35),
Codec::Data(STREAM_TYPE_SCTE35)
);
}
const PACKET_PAYLOAD_LEN: usize = TS_PACKET_SIZE - 4;
fn payload_only_packet(pid: Pid, cc: u8) -> [u8; TS_PACKET_SIZE] {
let mut p = [0xFFu8; TS_PACKET_SIZE];
p[0] = 0x47; p[1] = ((pid.value() >> 8) as u8) & PID_HI_MASK; p[2] = (pid.value() & 0xFF) as u8; p[3] = 0x10 | (cc & 0x0F); p
}
#[test]
fn unattributed_buffer_is_bounded_for_never_claimed_pid() {
let target_bytes = MAX_UNATTRIBUTED_BYTES * 3;
let packet_count = target_bytes / PACKET_PAYLOAD_LEN + 1;
let unclaimed_pid: Pid = Pid::new(0x0123);
let mut demux = StreamingTsDemux::new();
for i in 0..packet_count {
demux.feed(&payload_only_packet(unclaimed_pid, i as u8));
}
assert!(
demux.unattributed_bytes <= MAX_UNATTRIBUTED_BYTES,
"unattributed_bytes {} exceeded cap {}",
demux.unattributed_bytes,
MAX_UNATTRIBUTED_BYTES
);
assert!(
demux.unattributed_bytes > 0,
"expected the never-claimed PID's payload to be buffered"
);
let actual: usize = demux
.unattributed
.values()
.flat_map(|q| q.iter())
.map(|(_, payload)| payload.len())
.sum();
assert_eq!(
actual, demux.unattributed_bytes,
"unattributed_bytes drifted from the real retained size"
);
}
#[test]
fn runaway_pes_without_payload_unit_start_is_bounded_not_unbounded() {
use crate::TsMux;
use crate::media::{Media, Track};
use crate::pipeline::{CodecConfig, Sample, TrackSpec};
use crate::rtp_sdp::avc_config_from_sprop;
use broadcast_common::Package;
const ES_PID: Pid = Pid::new(0x0100);
let avc = avc_config_from_sprop("Z0IAKeKQFAe2AtwEBAaQeJEV,aM48gA==").unwrap();
let spec = TrackSpec::new(
TrackId::new(1),
VIDEO_TIMESCALE,
CodecConfig::Avc {
config: avc,
width: 0,
height: 0,
},
);
let frame_dur = VIDEO_TIMESCALE / 30;
let samples: Vec<Sample> = (0..3u32)
.map(|i| {
let nal = [0x65u8, 0xAA, i as u8];
let mut data = (nal.len() as u32).to_be_bytes().to_vec();
data.extend_from_slice(&nal);
let dts = i64::from(i) * i64::from(frame_dur);
Sample::new(data, Some(dts), Some(dts), Some(frame_dur), i == 0)
})
.collect();
let track = Track::new(spec, samples);
let media = Media::new(vec![track], VIDEO_TIMESCALE);
let ts_bytes = TsMux::default().package(&media).expect("mux to TS");
let mut demux = StreamingTsDemux::new();
demux.feed(&ts_bytes);
while demux.poll_event().is_some() {}
assert!(
demux.streams.contains_key(&ES_PID),
"expected the muxed video ES at PID {ES_PID:#06X} — TsMux's ES_PID_BASE convention"
);
let mut cc: u8 = 0;
let mut hit_cap = false;
for _ in 0..30_000u32 {
let mut pkt = [0xFFu8; TS_PACKET_SIZE];
pkt[0] = 0x47;
pkt[1] = ((ES_PID.value() >> 8) as u8) & PID_HI_MASK; pkt[2] = (ES_PID.value() & 0xFF) as u8;
pkt[3] = 0x10 | (cc & 0x0F);
cc = cc.wrapping_add(1);
demux.feed(&pkt);
if matches!(
demux.poll_event(),
Some(DemuxEvent::Discontinuity { provenance, .. }) if provenance.pid == Some(ES_PID)
) {
hit_cap = true;
break;
}
}
assert!(
hit_cap,
"expected MAX_PES_BUFFER_BYTES to trip well within 30000 continuation \
packets (never grow unbounded)"
);
assert_eq!(
demux.streams.get(&ES_PID).unwrap().pes_bytes,
0,
"pes_bytes must reset to 0 once the cap trips"
);
const PUSI_BIT: u8 = 0x40; let mut pkt = [0xFFu8; TS_PACKET_SIZE];
pkt[0] = 0x47;
pkt[1] = PUSI_BIT | (((ES_PID.value() >> 8) as u8) & PID_HI_MASK);
pkt[2] = (ES_PID.value() & 0xFF) as u8;
pkt[3] = 0x10 | (cc & 0x0F);
pkt[4] = 0; demux.feed(&pkt);
assert_eq!(
demux.streams.get(&ES_PID).unwrap().pes_bytes,
PACKET_PAYLOAD_LEN,
"a fresh payload_unit_start must be accepted and start a new count"
);
}
#[test]
fn probe_backlog_is_bounded_for_both_the_never_resolving_pid_and_its_collateral_pid() {
use crate::TsMux;
use crate::media::{Media, Track};
use crate::pipeline::{CodecConfig, DataCarriage, Sample, TrackSpec};
use crate::rtp_sdp::avc_config_from_sprop;
use broadcast_common::Package;
const SAMPLE_BYTES: usize = 4096;
const SAMPLE_COUNT: u32 = 1200;
let frame_dur = VIDEO_TIMESCALE / 30;
let avc = avc_config_from_sprop("Z0IAKeKQFAe2AtwEBAaQeJEV,aM48gA==").unwrap();
let video_spec = TrackSpec::new(
TrackId::new(1),
VIDEO_TIMESCALE,
CodecConfig::Avc {
config: avc,
width: 0,
height: 0,
},
);
let video_samples: Vec<Sample> = (0..SAMPLE_COUNT)
.map(|i| {
let mut nal = alloc::vec![0x41u8]; nal.resize(SAMPLE_BYTES, 0xAA);
let mut data = (nal.len() as u32).to_be_bytes().to_vec();
data.extend_from_slice(&nal);
let dts = i64::from(i) * i64::from(frame_dur);
Sample::new(data, Some(dts), Some(dts), Some(frame_dur), false)
})
.collect();
let video_track = Track::new(video_spec, video_samples);
let data_spec = TrackSpec::new(
TrackId::new(2),
VIDEO_TIMESCALE,
CodecConfig::Data {
stream_type: 0x7F,
descriptors: Vec::new(),
carriage: DataCarriage::Pes,
},
);
let data_samples: Vec<Sample> = (0..SAMPLE_COUNT)
.map(|i| {
let payload = alloc::vec![0xBBu8; SAMPLE_BYTES];
let dts = i64::from(i) * i64::from(frame_dur);
Sample::new(payload, Some(dts), Some(dts), Some(frame_dur), true)
})
.collect();
let data_track = Track::new(data_spec, data_samples);
let media = Media::new(vec![video_track, data_track], VIDEO_TIMESCALE);
let ts_bytes = TsMux::default().package(&media).expect("mux to TS");
let mut demux = StreamingTsDemux::new();
demux.feed(&ts_bytes);
let mut abandoned_pids: Vec<Pid> = Vec::new();
while let Some(ev) = demux.poll_event() {
if let DemuxEvent::TrackAbandoned {
reason: AbandonReason::BudgetExceeded,
provenance,
..
} = ev
&& let Some(pid) = provenance.pid
{
abandoned_pids.push(pid);
}
}
assert!(
!abandoned_pids.is_empty(),
"expected at least one TrackAbandoned{{BudgetExceeded}} from an abandoned probe backlog \
(issue #774: this replaced the mis-typed Discontinuity this path used to emit)"
);
let mut demux2 = StreamingTsDemux::new();
demux2.feed(&ts_bytes);
let saw_discontinuity_for_abandoned_pid = std::iter::from_fn(|| demux2.poll_event())
.any(|ev| matches!(ev, DemuxEvent::Discontinuity { provenance, .. } if provenance.pid == Some(PID_A)));
assert!(
!saw_discontinuity_for_abandoned_pid,
"a probe-backlog-budget abandonment (issue #774) must be a TrackAbandoned, \
never a Discontinuity"
);
for (&pid, stream) in demux.streams.iter() {
assert!(
stream.backlog_bytes <= MAX_PROBE_BACKLOG_BYTES,
"PID {pid:#06X} backlog_bytes {} exceeded cap {MAX_PROBE_BACKLOG_BYTES}",
stream.backlog_bytes
);
}
const PID_A: Pid = Pid::new(0x0100);
let abandoned = matches!(
demux.streams.get(&PID_A).and_then(|s| s.track.as_ref()),
Some(TrackState::Abandoned)
);
assert!(abandoned, "PID A must be Abandoned, never Live");
const PID_B: Pid = Pid::new(0x0101);
let pid_b_resolved = matches!(
demux.streams.get(&PID_B).and_then(|s| s.track.as_ref()),
Some(TrackState::Live(_)) | Some(TrackState::Abandoned)
);
assert!(
pid_b_resolved,
"PID B must reach a final disposition (Live or Abandoned), not stay wedged"
);
}
#[test]
fn track_abandoned_config_unrecoverable_fires_at_finish() {
use crate::TsMux;
use crate::media::{Media, Track};
use crate::pipeline::{CodecConfig, Sample, TrackSpec};
use crate::rtp_sdp::avc_config_from_sprop;
use broadcast_common::Package;
const PID_A: Pid = Pid::new(0x0100);
let frame_dur = VIDEO_TIMESCALE / 30;
let avc = avc_config_from_sprop("Z0IAKeKQFAe2AtwEBAaQeJEV,aM48gA==").unwrap();
let video_spec = TrackSpec::new(
TrackId::new(1),
VIDEO_TIMESCALE,
CodecConfig::Avc {
config: avc,
width: 0,
height: 0,
},
);
let video_samples: Vec<Sample> = (0..5u32)
.map(|i| {
let nal = alloc::vec![0x41u8, 0xAA, 0xBB]; let mut data = (nal.len() as u32).to_be_bytes().to_vec();
data.extend_from_slice(&nal);
let dts = i64::from(i) * i64::from(frame_dur);
Sample::new(data, Some(dts), Some(dts), Some(frame_dur), false)
})
.collect();
let video_track = Track::new(video_spec, video_samples);
let media = Media::new(vec![video_track], VIDEO_TIMESCALE);
let ts_bytes = TsMux::default().package(&media).expect("mux to TS");
let mut demux = StreamingTsDemux::new();
demux.feed(&ts_bytes);
assert!(
!matches!(
demux.streams.get(&PID_A).and_then(|s| s.track.as_ref()),
Some(TrackState::Abandoned)
),
"sanity: PID A must still be Probing before finish() — the byte cap must not \
have tripped (this test is about the end-of-input path, not the budget one)"
);
while demux.poll_event().is_some() {}
demux.finish();
let mut saw_config_unrecoverable = false;
while let Some(ev) = demux.poll_event() {
if let DemuxEvent::TrackAbandoned {
track_id,
reason: AbandonReason::ConfigUnrecoverable,
provenance,
} = ev
{
assert_eq!(
track_id, None,
"a track abandoned before ever resolving has no track_id to report"
);
assert_eq!(provenance.pid, Some(PID_A));
saw_config_unrecoverable = true;
}
}
assert!(
saw_config_unrecoverable,
"expected TrackAbandoned{{ConfigUnrecoverable}} once finish() concludes PID A's \
config will never resolve"
);
}
const SECTION_BODY_PREFIX_LEN: usize = 5;
pub(crate) const ES_STREAM_ID_VIDEO: u8 = 0xE0;
fn pes_packet_untimed(stream_id: u8, au: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&[0x00, 0x00, 0x01, stream_id]);
let payload_len = au.len() + 3;
out.extend_from_slice(&(payload_len as u16).to_be_bytes());
out.push(0x80); out.push(0x00); out.push(0x00); out.extend_from_slice(au);
out
}
const ES_STREAM_ID_PRIVATE_DATA: u8 = 0xBD;
fn data_au(seed: usize) -> Vec<u8> {
(0..300).map(|i| ((i + seed) & 0xFF) as u8).collect()
}
fn long_data_au() -> Vec<u8> {
data_au(0)
}
fn long_data_au_alt() -> Vec<u8> {
data_au(0x55)
}
fn unbounded_pes(stream_id: u8, au: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&[0x00, 0x00, 0x01, stream_id]);
out.extend_from_slice(&0u16.to_be_bytes()); out.push(0x80); out.push(0x00); out.push(0x00); out.extend_from_slice(au);
out
}
fn untimed_pes(stream_id: u8, au: &[u8]) -> Vec<u8> {
pes_packet_untimed(stream_id, au)
}
const ES_STREAM_ID_AUDIO: u8 = 0xBD;
fn encode_ts_field(prefix: u8, value: i64) -> [u8; 5] {
broadcast_common::clock33::pack_marked(value as u64, prefix)
}
pub(crate) fn pes_packet(stream_id: u8, pts: i64, dts: Option<i64>, au: &[u8]) -> Vec<u8> {
let header_data_len = if dts.is_some() { 10 } else { 5 };
let mut out = Vec::new();
out.extend_from_slice(&[0x00, 0x00, 0x01, stream_id]);
let payload_len = au.len() + 3 + header_data_len;
out.extend_from_slice(&u16::try_from(payload_len).unwrap_or(0).to_be_bytes());
out.push(0x80); out.push(if dts.is_some() { 0xC0 } else { 0x80 });
out.push(header_data_len as u8);
if let Some(d) = dts {
out.extend_from_slice(&encode_ts_field(0b0011, pts));
out.extend_from_slice(&encode_ts_field(0b0001, d));
} else {
out.extend_from_slice(&encode_ts_field(0b0010, pts));
}
out.extend_from_slice(au);
out
}
pub(crate) const PAT_PID_UNDER_TEST: Pid = Pid::new(0x0000);
pub(crate) const PMT_PID_UNDER_TEST: Pid = Pid::new(0x1000);
pub(crate) const ES_PID_UNDER_TEST: Pid = Pid::new(0x1100);
const PROGRAM_NUMBER_UNDER_TEST: u16 = 1;
pub(crate) fn pat_body() -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&PROGRAM_NUMBER_UNDER_TEST.to_be_bytes());
body.extend_from_slice(&(0xE000 | PMT_PID_UNDER_TEST.value()).to_be_bytes());
body
}
pub(crate) fn pmt_body(es_pid: Pid, stream_type: u8) -> Vec<u8> {
pmt_body_with_pcr_pid(es_pid, stream_type, Pid::new(0))
}
fn pmt_body_two_es_with_pcr_pid(
video_pid: Pid,
video_type: u8,
audio_pid: Pid,
audio_type: u8,
pcr_pid: Pid,
) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&(0xE000 | pcr_pid.value()).to_be_bytes());
body.extend_from_slice(&0xF000u16.to_be_bytes()); for (pid, stream_type) in [(video_pid, video_type), (audio_pid, audio_type)] {
body.push(stream_type);
body.extend_from_slice(&(0xE000 | pid.value()).to_be_bytes());
body.extend_from_slice(&0xF000u16.to_be_bytes()); }
body
}
fn pmt_body_with_pcr_pid(es_pid: Pid, stream_type: u8, pcr_pid: Pid) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&(0xE000 | pcr_pid.value()).to_be_bytes());
body.extend_from_slice(&0xF000u16.to_be_bytes()); body.push(stream_type);
body.extend_from_slice(&(0xE000 | es_pid.value()).to_be_bytes());
body.extend_from_slice(&0xF000u16.to_be_bytes()); body
}
pub(crate) fn psi_section_packets(pid: Pid, cc0: u8, table_id: u8, body: &[u8]) -> Vec<u8> {
psi_section_packets_for(pid, cc0, table_id, PROGRAM_NUMBER_UNDER_TEST, body)
}
fn psi_section_packets_for(
pid: Pid,
cc0: u8,
table_id: u8,
table_id_extension: u16,
body: &[u8],
) -> Vec<u8> {
psi_section_packets_versioned(pid, cc0, table_id, table_id_extension, 0, body)
}
fn psi_section_packets_versioned(
pid: Pid,
cc0: u8,
table_id: u8,
table_id_extension: u16,
version: u8,
body: &[u8],
) -> Vec<u8> {
let mut section = Vec::new();
let section_length = (SECTION_BODY_PREFIX_LEN + body.len() + CRC32_LEN) as u16;
section.push(table_id);
section.push(SECTION_SYNTAX_INDICATOR_BIT | (section_length >> 8) as u8);
section.push(section_length as u8);
section.extend_from_slice(&table_id_extension.to_be_bytes());
section.push(0xC1 | (version << 1)); section.push(0x00); section.push(0x00); section.extend_from_slice(body);
let crc = broadcast_common::crc32_mpeg2::compute(§ion);
section.extend_from_slice(&crc.to_be_bytes());
let mut packetised = Vec::with_capacity(section.len() + 1);
packetised.push(0x00);
packetised.extend_from_slice(§ion);
pes_packets(pid, cc0, &packetised)
}
fn pes_packets_with_discontinuity(pid: Pid, cc0: u8, payload: &[u8]) -> Vec<u8> {
const AF_LEN: usize = 8;
const PAYLOAD_START: usize = 4 + 1 + AF_LEN;
let first_take = (TS_PACKET_SIZE - PAYLOAD_START).min(payload.len());
let mut out = Vec::new();
let mut p = [0xFFu8; TS_PACKET_SIZE];
p[0] = 0x47;
p[1] = ((pid.value() >> 8) as u8 & PID_HI_MASK) | 0x40; p[2] = (pid.value() & 0xFF) as u8;
p[3] = 0x30 | (cc0 & 0x0F); p[4] = AF_LEN as u8;
p[5] = 0x80; p[6..PAYLOAD_START].fill(0xFF); p[PAYLOAD_START..PAYLOAD_START + first_take].copy_from_slice(&payload[..first_take]);
out.extend_from_slice(&p);
out.extend_from_slice(&continuation_packets(
pid,
cc0.wrapping_add(1),
&payload[first_take..],
));
out
}
fn continuation_packets(pid: Pid, cc0: u8, payload: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
let mut off = 0usize;
let mut cc = cc0;
while off < payload.len() {
let take = (payload.len() - off).min(TS_PACKET_SIZE - 4);
let mut p = [0xFFu8; TS_PACKET_SIZE];
p[0] = 0x47;
p[1] = (pid.value() >> 8) as u8 & PID_HI_MASK;
p[2] = (pid.value() & 0xFF) as u8;
p[3] = 0x10 | (cc & 0x0F);
p[4..4 + take].copy_from_slice(&payload[off..off + take]);
out.extend_from_slice(&p);
off += take;
cc = cc.wrapping_add(1);
}
out
}
pub(crate) fn pes_packets(pid: Pid, cc0: u8, payload: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
let mut off = 0usize;
let mut first = true;
let mut cc = cc0;
while off < payload.len() {
let take = (payload.len() - off).min(TS_PACKET_SIZE - 4);
let mut p = [0xFFu8; TS_PACKET_SIZE];
p[0] = 0x47;
p[1] = ((pid.value() >> 8) as u8 & PID_HI_MASK) | if first { 0x40 } else { 0x00 };
p[2] = (pid.value() & 0xFF) as u8;
p[3] = 0x10 | (cc & 0x0F);
p[4..4 + take].copy_from_slice(&payload[off..off + take]);
out.extend_from_slice(&p);
off += take;
first = false;
cc = cc.wrapping_add(1);
}
out
}
fn stalled_head_ts(
video_aus: u32,
data_pids: usize,
data_aus: u32,
audio_head: bool,
resolve_last: bool,
) -> Vec<u8> {
use crate::TsMux;
use crate::media::{Media, Track};
use crate::pipeline::{CodecConfig, DataCarriage, Sample, TrackSpec};
use crate::rtp_sdp::avc_config_from_sprop;
use broadcast_common::Package;
let frame_dur = VIDEO_TIMESCALE / 30;
let avc = avc_config_from_sprop("Z0IAKeKQFAe2AtwEBAaQeJEV,aM48gA==").unwrap();
let video_samples: Vec<Sample> = (0..video_aus)
.map(|i| {
let sync = resolve_last && i + 2 == video_aus;
let nal = if audio_head {
alloc::vec![0xBBu8; 3]
} else {
alloc::vec![0x41u8, 0xAA, 0xBB]
};
let mut data = if audio_head {
Vec::new()
} else {
(nal.len() as u32).to_be_bytes().to_vec()
};
data.extend_from_slice(&nal);
let dts = i64::from(i) * i64::from(frame_dur);
Sample::new(data, Some(dts), Some(dts), Some(frame_dur), sync)
})
.collect();
let head_config = if audio_head {
CodecConfig::Data {
stream_type: 0x04,
descriptors: Vec::new(),
carriage: DataCarriage::Pes,
}
} else {
CodecConfig::Avc {
config: avc,
width: 0,
height: 0,
}
};
let mut tracks = vec![Track::new(
TrackSpec::new(TrackId::new(1), VIDEO_TIMESCALE, head_config),
video_samples,
)];
for k in 0..data_pids {
let samples: Vec<Sample> = (0..data_aus)
.map(|i| {
let dts = i64::from(i) * i64::from(frame_dur);
Sample::new(
alloc::vec![0xBBu8; 32],
Some(dts),
Some(dts),
Some(frame_dur),
true,
)
})
.collect();
tracks.push(Track::new(
TrackSpec::new(
TrackId::new(2 + k as u32),
VIDEO_TIMESCALE,
CodecConfig::Data {
stream_type: 0x7F,
descriptors: Vec::new(),
carriage: DataCarriage::Pes,
},
),
samples,
));
}
TsMux::default()
.package(&Media::new(tracks, VIDEO_TIMESCALE))
.expect("mux to TS")
}
fn live_track_added_pids(ts: &[u8]) -> Vec<u16> {
let mut demux = StreamingTsDemux::new();
let mut pids = Vec::new();
for chunk in ts.chunks(TS_PACKET_SIZE * 16) {
demux.feed(chunk);
while let Some(ev) = demux.poll_event() {
if let DemuxEvent::TrackAdded(spec) = ev {
pids.push(spec.source_pid.map_or(0, |p| p.value()));
}
}
}
pids
}
fn eac3_pes_ts(payloads: &[Vec<u8>]) -> Vec<u8> {
single_es_pes_ts(STREAM_TYPE_EAC3, payloads)
}
fn single_es_pes_ts(stream_type: u8, payloads: &[Vec<u8>]) -> Vec<u8> {
let mut ts = psi_section_packets(PAT_PID_UNDER_TEST, 0, 0x00, &pat_body());
ts.extend(psi_section_packets(
PMT_PID_UNDER_TEST,
0,
0x02,
&pmt_body(ES_PID_UNDER_TEST, stream_type),
));
let mut cc = 0u8;
for (n, es) in payloads.iter().enumerate() {
let pts = 90_000u64 + 2_880 * n as u64;
let mut pes = alloc::vec![0, 0, 1, 0xBD];
pes.extend_from_slice(&((3 + 5 + es.len()) as u16).to_be_bytes());
pes.extend_from_slice(&[0x80, 0x80, 5]);
pes.push(0x21 | ((pts >> 29) as u8 & 0x0E));
pes.push((pts >> 22) as u8);
pes.push(0x01 | ((pts >> 14) as u8 & 0xFE));
pes.push((pts >> 7) as u8);
pes.push(0x01 | ((pts << 1) as u8 & 0xFE));
pes.extend_from_slice(es);
ts.extend(pes_packets(ES_PID_UNDER_TEST, cc, &pes));
cc = cc.wrapping_add(pes.len().div_ceil(TS_PACKET_SIZE - 4) as u8);
}
ts
}
fn live_eac3_tracks(ts: &[u8]) -> usize {
let mut demux = StreamingTsDemux::new();
demux.feed(ts);
let mut n = 0;
while let Some(ev) = demux.poll_event() {
if let DemuxEvent::TrackAdded(spec) = ev
&& matches!(spec.config, CodecConfig::Eac3 { .. })
{
n += 1;
}
}
n
}
fn live_eac3_bsmod(ts: &[u8]) -> Option<u8> {
let mut demux = StreamingTsDemux::new();
demux.feed(ts);
while let Some(ev) = demux.poll_event() {
if let DemuxEvent::TrackAdded(spec) = ev
&& let CodecConfig::Eac3 { config, .. } = spec.config
{
return Some(config.substreams[0].bsmod);
}
}
None
}
#[test]
fn eac3_probe_skips_a_damaged_frame_in_front_of_an_intact_one() {
use crate::ac3::tests::{damaged_for_tests, eac3_bsi};
let bad = damaged_for_tests(eac3_bsi(0, 0, 63, 2, false, 0, 3, None, Some(5)));
let good = eac3_bsi(0, 0, 63, 2, false, 0, 3, None, Some(0));
let pes = [bad, good.clone(), good].concat();
assert_eq!(live_eac3_bsmod(&eac3_pes_ts(&[pes.clone(), pes])), Some(0));
}
#[test]
fn eac3_bsmod_comes_from_the_first_intact_frame_that_carries_it() {
use crate::ac3::tests::eac3_bsi;
let none = eac3_bsi(0, 0, 63, 2, false, 0, 3, None, None);
let two = eac3_bsi(0, 0, 63, 2, false, 0, 3, None, Some(2));
let pes = [none.clone(), two, none].concat();
assert_eq!(live_eac3_bsmod(&eac3_pes_ts(&[pes.clone(), pes])), Some(2));
}
#[test]
fn eac3_probe_with_no_intact_frame_still_announces_the_track() {
use crate::ac3::tests::{damaged_for_tests, eac3_bsi};
let bad = damaged_for_tests(eac3_bsi(0, 0, 63, 2, false, 0, 3, None, Some(2)));
let pes = [bad.clone(), bad].concat();
assert_eq!(live_eac3_bsmod(&eac3_pes_ts(&[pes.clone(), pes])), Some(2));
}
#[test]
fn eac3_probe_that_never_completes_resolves_after_the_bound() {
use crate::ac3::Ec3SyncframeInfo;
use crate::ac3::tests::seven_one_eac3_es;
let au = seven_one_eac3_es();
let frames = Ec3SyncframeInfo::from_es_all(&au);
assert_eq!(frames.len(), 2, "independent + dependent");
let dep_at = (frames[0].frmsiz as usize + 1) * 2;
let dep_only = au[dep_at..].to_vec();
let build = |pes_count: usize| {
let mut v = alloc::vec![au.clone()];
v.extend((1..pes_count).map(|_| dep_only.clone()));
eac3_pes_ts(&v)
};
let bound = EAC3_PROBE_MAX_FRAMES;
assert_eq!(live_eac3_tracks(&build(bound - 1)), 0, "one frame short");
assert_eq!(live_eac3_tracks(&build(bound)), 1, "bound reached");
}
#[test]
fn eac3_probe_on_lossy_capture_settles_for_a_broken_chain() {
use crate::ac3::tests::seven_one_eac3_es;
let clean = seven_one_eac3_es();
let mut lossy = clean.clone();
lossy.extend_from_slice(&[0x43, 0x8B, 0x88, 0xBC, 0xA9, 0xB4, 0x00, 0x15]);
assert_eq!(
live_eac3_tracks(&eac3_pes_ts(&[lossy.clone(), lossy])),
1,
"broken chain"
);
assert_eq!(
live_eac3_tracks(&eac3_pes_ts(&[clean.clone(), clean])),
0,
"the access unit merely ends"
);
}
fn live_track_added_ids(ts: &[u8]) -> Vec<(u16, u32)> {
let mut demux = StreamingTsDemux::new();
let mut out = Vec::new();
for chunk in ts.chunks(TS_PACKET_SIZE * 16) {
demux.feed(chunk);
while let Some(ev) = demux.poll_event() {
if let DemuxEvent::TrackAdded(spec) = ev {
out.push((
spec.source_pid.map_or(0, |p| p.value()),
spec.track_id.value(),
));
}
}
}
out
}
#[test]
fn stalled_probe_does_not_block_later_pids_forever() {
let ts = stalled_head_ts(PROBE_HEAD_BLOCK_AUS + 20, 2, 4, true, false);
let pids = live_track_added_pids(&ts);
assert_eq!(
pids,
vec![0x0101, 0x0102],
"both data PIDs promoted, in PMT order, past the stalled head"
);
}
#[test]
fn slow_probe_still_blocks_later_pids_below_the_bound() {
let ts = stalled_head_ts(PROBE_HEAD_BLOCK_AUS - 20, 2, 4, true, false);
assert!(
live_track_added_pids(&ts).is_empty(),
"a probe below the bound keeps PMT-order promotion"
);
}
#[test]
fn slow_video_head_keeps_track_id_one() {
let unresolved = stalled_head_ts(PROBE_HEAD_BLOCK_AUS + 20, 2, 4, false, false);
assert!(
live_track_added_ids(&unresolved).is_empty(),
"a video head past the bound must still block later PIDs"
);
let resolved = stalled_head_ts(PROBE_HEAD_BLOCK_AUS + 20, 2, 4, false, true);
assert_eq!(
live_track_added_ids(&resolved),
vec![(0x0100, 1), (0x0101, 2), (0x0102, 3)],
"slow video must be track 1, later PIDs 2 and 3"
);
}
fn pmt_body_multi(es: &[(Pid, u8)]) -> Vec<u8> {
let mut body = Vec::new();
body.extend_from_slice(&(0xE000 | es[0].0.value()).to_be_bytes());
body.extend_from_slice(&0xF000u16.to_be_bytes());
for &(pid, stream_type) in es {
body.push(stream_type);
body.extend_from_slice(&(0xE000 | pid.value()).to_be_bytes());
body.extend_from_slice(&0xF000u16.to_be_bytes());
}
body
}
#[derive(Debug, PartialEq, Eq, Clone)]
enum Seen {
Added(u16, u32),
Resolved,
Sample(u32),
Removed(u32),
}
fn seen_events(ts: &[u8]) -> Vec<Seen> {
let mut demux = StreamingTsDemux::new();
let mut out = Vec::new();
for chunk in ts.chunks(TS_PACKET_SIZE * 16) {
demux.feed(chunk);
while let Some(ev) = demux.poll_event() {
match ev {
DemuxEvent::TrackAdded(spec) => out.push(Seen::Added(
spec.source_pid.map_or(0, |p| p.value()),
spec.track_id.value(),
)),
DemuxEvent::TracksResolved { .. } => out.push(Seen::Resolved),
DemuxEvent::Sample { track_id, .. } => out.push(Seen::Sample(track_id.value())),
DemuxEvent::TrackRemoved { track_id, .. } => {
out.push(Seen::Removed(track_id.value()));
}
_ => {}
}
}
}
out
}
fn added(seen: &[Seen]) -> Vec<(u16, u32)> {
seen.iter()
.filter_map(|e| match e {
Seen::Added(p, t) => Some((*p, *t)),
_ => None,
})
.collect()
}
#[test]
fn stalled_head_counts_toward_tracks_resolved() {
let ts = stalled_head_ts(PROBE_HEAD_BLOCK_AUS + 20, 2, 4, true, false);
let seen = seen_events(&ts);
assert_eq!(added(&seen), vec![(0x0101, 1), (0x0102, 2)]);
let resolved: Vec<usize> = seen
.iter()
.enumerate()
.filter(|(_, e)| **e == Seen::Resolved)
.map(|(i, _)| i)
.collect();
assert_eq!(resolved.len(), 1, "exactly one TracksResolved: {seen:?}");
let last_added = seen
.iter()
.rposition(|e| matches!(e, Seen::Added(..)))
.unwrap();
assert!(resolved[0] > last_added, "after every TrackAdded");
assert!(
seen.contains(&Seen::Sample(1)) && seen.contains(&Seen::Sample(2)),
"the promoted tracks' samples flow: {seen:?}"
);
}
fn silent_head_ts(
head_type: u8,
head_pes: &[Vec<u8>],
flood_type: u8,
flood_payload: impl Fn(usize) -> Vec<u8>,
flood_packets: usize,
) -> Vec<u8> {
let head = Pid::new(0x0100);
let flood = Pid::new(0x0101);
let mut ts = psi_section_packets(PAT_PID_UNDER_TEST, 0, 0x00, &pat_body());
ts.extend(psi_section_packets(
PMT_PID_UNDER_TEST,
0,
0x02,
&pmt_body_multi(&[(head, head_type), (flood, flood_type)]),
));
let mut cc = [0u8; 2];
let mut push = |ts: &mut Vec<u8>, which: usize, pes: &[u8]| {
let pid = if which == 0 { head } else { flood };
let packets = pes_packets(pid, cc[which], pes);
cc[which] = cc[which].wrapping_add((packets.len() / TS_PACKET_SIZE) as u8);
ts.extend_from_slice(&packets);
};
let start = ts.len();
let mut n = 0usize;
while (ts.len() - start) / TS_PACKET_SIZE < flood_packets {
let pes = pes_packet(
ES_STREAM_ID_MPEG_AUDIO,
CARRY_BASE_PTS_90K + n as i64 * 1920,
None,
&flood_payload(n),
);
push(&mut ts, 1, &pes);
n += 1;
}
for (i, h) in head_pes.iter().enumerate() {
let pes = pes_packet(
ES_STREAM_ID_MPEG_AUDIO,
CARRY_BASE_PTS_90K + i as i64 * 2160,
None,
h,
);
push(&mut ts, 0, &pes);
}
for k in 0..2 {
let pes = pes_packet(
ES_STREAM_ID_MPEG_AUDIO,
CARRY_BASE_PTS_90K + (n + k) as i64 * 1920,
None,
&flood_payload(n + k),
);
push(&mut ts, 1, &pes);
}
ts
}
#[test]
fn stepped_over_head_resolving_late_is_promoted_and_rearms_the_signal() {
let garbage: Vec<Vec<u8>> = (0..PROBE_HEAD_BLOCK_AUS as usize + 20)
.map(|_| alloc::vec![0xBBu8; 3])
.collect();
let real = mp2_frame(1);
let mut head = garbage;
head.extend((0..4).map(|_| real.clone()));
let ts = silent_head_ts(0x04, &head, 0x7F, |n| alloc::vec![n as u8; 32], 40);
let seen = seen_events(&ts);
assert_eq!(added(&seen), vec![(0x0101, 1), (0x0100, 2)], "{seen:?}");
let resolved: Vec<usize> = seen
.iter()
.enumerate()
.filter(|(_, e)| **e == Seen::Resolved)
.map(|(i, _)| i)
.collect();
let head_added = seen
.iter()
.position(|e| *e == Seen::Added(0x0100, 2))
.unwrap();
assert_eq!(
resolved.len(),
2,
"fires again after the late promotion: {seen:?}"
);
assert!(resolved[0] < head_added && resolved[1] > head_added);
assert!(
seen.contains(&Seen::Sample(2)),
"the late track's samples flow"
);
}
fn first_announce(ts: &[u8]) -> Option<(usize, usize)> {
let mut demux = StreamingTsDemux::new();
let mut prev = 0usize;
for (i, chunk) in ts.chunks(TS_PACKET_SIZE).enumerate() {
demux.feed(chunk);
while let Some(ev) = demux.poll_event() {
if matches!(ev, DemuxEvent::TrackAdded(_)) {
return Some((i, prev));
}
}
prev = demux
.streams
.get(&Pid::new(0x0101))
.map_or(0, |s| s.backlog_bytes);
}
None
}
const FLOOD_PAST_CRITERION: usize = 25_000;
#[test]
fn declared_but_absent_video_does_not_block_audio() {
let ts = silent_head_ts(
STREAM_TYPE_AVC,
&[],
STREAM_TYPE_AAC_ADTS,
adts_frame,
FLOOD_PAST_CRITERION,
);
let seen = seen_events(&ts);
assert_eq!(added(&seen), vec![(0x0101, 1)], "{seen:?}");
assert!(seen.contains(&Seen::Resolved), "TracksResolved fires");
assert!(seen.contains(&Seen::Sample(1)), "audio samples flow");
let (_, before) = first_announce(&ts).expect("audio announced");
assert!(
before < SILENT_HEAD_PARKED_BYTES && before + 512 >= SILENT_HEAD_PARKED_BYTES,
"announced with {before} parked bytes, criterion {SILENT_HEAD_PARKED_BYTES}"
);
let early = silent_head_ts(
STREAM_TYPE_AVC,
&[],
STREAM_TYPE_AAC_ADTS,
adts_frame,
15_000,
);
assert!(first_announce(&early).is_none());
}
#[test]
fn late_video_after_8192_stream_packets_keeps_track_one() {
let frames: Vec<Vec<u8>> = (0..4).map(|_| mp2_frame(1)).collect();
let ts = silent_head_ts(0x04, &frames, STREAM_TYPE_AAC_ADTS, adts_frame, 9000);
let seen = seen_events(&ts);
assert_eq!(added(&seen), vec![(0x0100, 1), (0x0101, 2)], "{seen:?}");
}
#[test]
fn slow_but_present_head_keeps_track_one_and_a_late_one_is_still_announced() {
let frames: Vec<Vec<u8>> = (0..4).map(|_| mp2_frame(1)).collect();
let slow = silent_head_ts(0x04, &frames, STREAM_TYPE_AAC_ADTS, adts_frame, 5000);
assert_eq!(
added(&seen_events(&slow)),
vec![(0x0100, 1), (0x0101, 2)],
"present before the bound: PMT order"
);
let late = silent_head_ts(
0x04,
&frames,
STREAM_TYPE_AAC_ADTS,
adts_frame,
FLOOD_PAST_CRITERION,
);
let seen = seen_events(&late);
assert_eq!(
added(&seen),
vec![(0x0101, 1), (0x0100, 2)],
"silent past the criterion: stepped over, announced when it arrives: {seen:?}"
);
}
#[test]
fn present_but_unresolved_video_head_still_blocks_past_the_silent_bound() {
let head = Pid::new(0x0100);
let flood = Pid::new(0x0101);
let mut ts = psi_section_packets(PAT_PID_UNDER_TEST, 0, 0x00, &pat_body());
ts.extend(psi_section_packets(
PMT_PID_UNDER_TEST,
0,
0x02,
&pmt_body_multi(&[(head, STREAM_TYPE_AVC), (flood, STREAM_TYPE_AAC_ADTS)]),
));
let mut cc = 0u8;
for i in 0..3 {
let pes = pes_packet(
ES_STREAM_ID_MPEG_AUDIO,
CARRY_BASE_PTS_90K + i as i64 * 2160,
None,
&[0xBB; 3],
);
let packets = pes_packets(head, cc, &pes);
cc = cc.wrapping_add((packets.len() / TS_PACKET_SIZE) as u8);
ts.extend_from_slice(&packets);
}
let mut fcc = 0u8;
for n in 0..FLOOD_PAST_CRITERION / 2 {
let pes = pes_packet(
ES_STREAM_ID_MPEG_AUDIO,
CARRY_BASE_PTS_90K + n as i64 * 1920,
None,
&adts_frame(n),
);
let packets = pes_packets(flood, fcc, &pes);
fcc = fcc.wrapping_add((packets.len() / TS_PACKET_SIZE) as u8);
ts.extend_from_slice(&packets);
}
let mut demux = StreamingTsDemux::new();
let mut parked = 0usize;
for chunk in ts.chunks(TS_PACKET_SIZE) {
demux.feed(chunk);
parked = parked.max(demux.streams.get(&flood).map_or(0, |s| s.backlog_bytes));
}
assert!(
parked >= SILENT_HEAD_PARKED_BYTES,
"the audio parked past the criterion ({parked})"
);
assert!(
live_track_added_pids(&ts).is_empty(),
"a head that has sent data is not a silent head"
);
}
#[test]
fn finish_abandoning_a_skipped_head_keeps_resolved_and_skipped_disjoint() {
let garbage: Vec<Vec<u8>> = (0..4).map(|_| alloc::vec![0xBBu8; 3]).collect();
let ts = silent_head_ts(
STREAM_TYPE_AVC,
&garbage,
STREAM_TYPE_AAC_ADTS,
adts_frame,
FLOOD_PAST_CRITERION,
);
let mut demux = StreamingTsDemux::new();
demux.feed(&ts);
assert!(
demux.skipped.contains(&Pid::new(0x0100)),
"the silent head was stepped over"
);
demux.finish();
assert!(
demux.resolved.contains(&Pid::new(0x0100)),
"finish abandoned the head"
);
assert!(
demux.resolved.is_disjoint(&demux.skipped),
"resolved {:?} and skipped {:?} double-count",
demux.resolved,
demux.skipped
);
}
#[test]
fn removing_an_eac3_track_flushes_the_held_final_access_unit() {
let au = crate::ac3::tests::seven_one_eac3_es();
let k = 6usize;
let mut ts = eac3_pes_ts(&vec![au; k]);
let mut empty_pmt = Vec::new();
empty_pmt.extend_from_slice(&0xE000u16.to_be_bytes());
empty_pmt.extend_from_slice(&0xF000u16.to_be_bytes());
ts.extend(psi_section_packets_versioned(
PMT_PID_UNDER_TEST,
1,
0x02,
PROGRAM_NUMBER_UNDER_TEST,
1,
&empty_pmt,
));
let seen = seen_events(&ts);
let removed = seen
.iter()
.position(|e| matches!(e, Seen::Removed(_)))
.expect("TrackRemoved");
let before = seen[..removed]
.iter()
.filter(|e| matches!(e, Seen::Sample(_)))
.count();
assert_eq!(
before,
k - 1,
"held final AU emitted before removal: {seen:?}"
);
}
#[test]
fn observed_dependent_substream_enables_the_hold_back() {
let au = crate::ac3::tests::seven_one_eac3_es();
let ind_len = {
let f = Ec3SyncframeInfo::from_es_all(&au);
(f[0].frmsiz as usize + 1) * 2
};
let (ind, dep) = (au[..ind_len].to_vec(), au[ind_len..].to_vec());
let mut lossy = ind.clone();
lossy.extend_from_slice(&[0x43, 0x8B, 0x88, 0xBC, 0xA9, 0xB4, 0x00, 0x15]);
let payloads = alloc::vec![
lossy,
[au.clone(), au.clone()].concat(),
ind.clone(),
[dep, au.clone()].concat(),
au.clone(),
au.clone(),
au.clone(),
];
let ts = single_es_pes_ts(STREAM_TYPE_EAC3, &payloads);
let mut demux = StreamingTsDemux::new();
demux.feed(&ts);
demux.finish();
let mut samples: Vec<Vec<u8>> = Vec::new();
while let Some(ev) = demux.poll_event() {
match ev {
DemuxEvent::TrackAdded(spec) => {
let CodecConfig::Eac3 { config, .. } = spec.config else {
panic!("E-AC-3");
};
assert!(
config.substreams[0].chan_loc.is_none(),
"precondition: promoted without a dependent substream"
);
}
DemuxEvent::Sample { sample, .. } => samples.push(sample.data.to_vec()),
_ => {}
}
}
assert_eq!(samples[0], ind, "the broken-chain first frame");
assert!(samples.len() >= 6, "{}", samples.len());
for (n, s) in samples.iter().enumerate().skip(1) {
assert_eq!(
s, &au,
"sample {n} must be a whole independent+dependent AU"
);
}
}
#[test]
fn eac3_probe_rewalk_is_bounded_for_tiny_pes() {
let real = {
let path = test_fixtures::path("ts/dolby/eac3.ts");
let ts = std::fs::read(path).expect("committed Dolby E-AC-3 capture");
let media = TsDemux::new().demux(&ts).expect("demux real E-AC-3");
media.tracks[0].samples[0].data.to_vec()
};
let run = |es: &[u8]| {
let payloads: Vec<Vec<u8>> = es.chunks(1).map(<[u8]>::to_vec).collect();
let ts = single_es_pes_ts(STREAM_TYPE_EAC3, &payloads);
let mut demux = StreamingTsDemux::new();
demux.feed(&ts);
let walks = demux.streams[&ES_PID_UNDER_TEST].probe_walks;
let mut tracks = 0;
while let Some(ev) = demux.poll_event() {
tracks += usize::from(matches!(ev, DemuxEvent::TrackAdded(_)));
}
(walks, tracks)
};
let mut endless = real.clone();
for n in 0..40 {
endless.extend(eac3_dependent_frame_sized(n, 2048));
}
let (walks, _) = run(&endless);
assert!(
walks < 4096 + 256,
"{walks} full window walks for {} one-byte PES",
endless.len()
);
let mut complete = real.clone();
for n in 0..8 {
complete.extend(eac3_dependent_frame_sized(n, 2048));
}
complete.extend_from_slice(&real);
complete.extend(eac3_dependent_frame_sized(9, 2048));
let (walks, tracks) = run(&complete);
assert!(walks > 4096, "the gate was exercised: {walks}");
assert_eq!(tracks, 1, "the track resolves under the gate");
}
#[test]
fn eac3_walked_len_follows_the_held_tail() {
let real = {
let path = test_fixtures::path("ts/dolby/eac3.ts");
let ts = std::fs::read(path).expect("committed Dolby E-AC-3 capture");
let media = TsDemux::new().demux(&ts).expect("demux real E-AC-3");
media.tracks[0].samples[0].data.to_vec()
};
let mut es = real;
for n in 0..40 {
es.extend(eac3_dependent_frame_sized(n, 2048));
}
let payloads: Vec<Vec<u8>> = es.chunks(1).map(<[u8]>::to_vec).collect();
let ts = single_es_pes_ts(STREAM_TYPE_EAC3, &payloads);
let mut demux = StreamingTsDemux::new();
let mut gated_with_tail = false;
for chunk in ts.chunks(TS_PACKET_SIZE) {
demux.feed(chunk);
let Some(stream) = demux.streams.get(&ES_PID_UNDER_TEST) else {
continue;
};
assert!(
stream.probe_walked_len <= stream.probe_tail.len(),
"walked_len {} outlived the held tail ({})",
stream.probe_walked_len,
stream.probe_tail.len()
);
gated_with_tail |=
stream.probe_walks >= EAC3_PROBE_FULL_WALKS && !stream.probe_tail.is_empty();
}
assert!(gated_with_tail, "the gated branch was exercised");
}
#[test]
fn aac_frame_length_variation_is_not_a_config_change() {
let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("..");
path.push("fixtures");
path.push("ts");
path.push("aac-5-1-640k.ts");
let source =
std::fs::read(&path).unwrap_or_else(|e| panic!("read fixture {}: {e}", path.display()));
let count_updates = |bytes: &[u8]| -> (usize, Vec<u16>) {
let mut demux = StreamingTsDemux::new();
demux.feed(bytes);
demux.finish();
let mut n = 0usize;
let mut channels = Vec::new();
while let Some(event) = demux.poll_event() {
if let DemuxEvent::TrackUpdated(spec) = event {
n += 1;
if let CodecConfig::Aac { channel_count, .. } = spec.config {
channels.push(channel_count);
}
}
}
(n, channels)
};
let (updates, _) = count_updates(&source);
assert_eq!(
updates, 0,
"a varying ADTS frame_length must not read as a config change; got {updates} TrackUpdated events on a real AAC capture"
);
let mut demux = TsDemux::new();
let media = demux.demux(&source).expect("demux aac fixture");
let audio = media
.tracks
.iter()
.find(|t| matches!(t.config(), CodecConfig::Aac { .. }))
.expect("the fixture has an AAC track");
assert!(!audio.samples.is_empty(), "fixture must carry AAC frames");
let (sfi, seeded_channels) = match audio.config() {
CodecConfig::Aac {
esds,
channel_count,
..
} => {
let dsi = esds
.es_descriptor
.decoder_config
.as_ref()
.and_then(|dc| dc.decoder_specific_info.as_ref())
.expect("AAC esds carries a DecoderSpecificInfo");
let asc = AudioSpecificConfig::parse(&dsi.data).expect("parse ASC");
(asc.sampling_frequency_index.raw(), *channel_count)
}
_ => unreachable!(),
};
const SEEDED_CHANNELS: u16 = 6;
assert_eq!(
seeded_channels, SEEDED_CHANNELS,
"the fixture is a real 5.1 capture, so its config is Table 1.19 configuration 6"
);
const CONFIG_REBUILT: u8 = 5;
const PES_PAYLOAD_BYTES: usize = 1800;
let aac_pid = audio
.spec
.source_pid
.expect("a TS-demuxed track carries its source PID");
let mut input = source.clone();
let mut cc = 0u8;
let mut buf: Vec<u8> = Vec::new();
let flush = |buf: &mut Vec<u8>, cc: &mut u8, input: &mut Vec<u8>| {
if buf.is_empty() {
return;
}
let pes = pes_packet(ES_STREAM_ID_AUDIO, 0, None, buf);
let packets = pes_packets(aac_pid, *cc, &pes);
*cc = cc.wrapping_add((packets.len() / TS_PACKET_SIZE) as u8);
input.extend_from_slice(&packets);
buf.clear();
};
for sample in &audio.samples {
let frame_len = (ADTS_HEADER_SIZE + sample.data.len()) as u16;
let hdr = crate::aac_asc::build_adts_header(1, sfi, CONFIG_REBUILT, frame_len);
if buf.len() + frame_len as usize > PES_PAYLOAD_BYTES {
flush(&mut buf, &mut cc, &mut input);
}
buf.extend_from_slice(&hdr);
buf.extend_from_slice(&sample.data);
}
flush(&mut buf, &mut cc, &mut input);
let (updates, channels) = count_updates(&input);
assert_eq!(
updates, 1,
"a real channel_configuration change must raise exactly one TrackUpdated, got {updates} (channels: {channels:?})"
);
assert_eq!(
channels,
vec![5],
"the event must carry the new Table 1.19 count"
);
}
#[test]
fn probe_sync_scan_work_is_linear_in_the_input_not_quadratic() {
use crate::aac_asc::build_adts_header;
const REJECTED_SFI: u8 = 13;
const FRAME_BODY: usize = 1400;
const FRAMES_PER_AU: usize = 3;
const ACCESS_UNITS: usize = 500;
let frame_len = (ADTS_HEADER_SIZE + FRAME_BODY) as u16;
let mut frame = alloc::vec![0u8; frame_len as usize];
frame[..ADTS_HEADER_SIZE].copy_from_slice(&build_adts_header(
1,
REJECTED_SFI,
2,
frame_len,
));
let mut au = Vec::with_capacity(frame.len() * FRAMES_PER_AU);
for _ in 0..FRAMES_PER_AU {
au.extend_from_slice(&frame);
}
let mut input = Vec::new();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
input.extend_from_slice(&null_packet());
}
input.extend_from_slice(&psi_section_packets(
PAT_PID_UNDER_TEST,
0,
TABLE_ID_PAT,
&pat_body(),
));
input.extend_from_slice(&psi_section_packets(
PMT_PID_UNDER_TEST,
0,
TABLE_ID_PMT,
&pmt_body(ES_PID_UNDER_TEST, STREAM_TYPE_AAC_ADTS),
));
let mut cc = 0u8;
for i in 0..ACCESS_UNITS {
let pes = pes_packet(ES_STREAM_ID_AUDIO, i as i64 * 1024, None, &au);
let packets = pes_packets(ES_PID_UNDER_TEST, cc, &pes);
cc = cc.wrapping_add((packets.len() / TS_PACKET_SIZE) as u8);
input.extend_from_slice(&packets);
}
let before = sync_probes();
let mut demux = StreamingTsDemux::new();
demux.feed(&input);
demux.finish();
let probes = sync_probes() - before;
assert!(
matches!(
demux
.streams
.get(&ES_PID_UNDER_TEST)
.and_then(|s| s.track.as_ref()),
Some(TrackState::Probing { .. })
),
"the reserved-SFI AAC PID must never resolve its config, so it stays Probing"
);
let expected = ACCESS_UNITS as u64 * 2;
assert_eq!(
probes, expected,
"the AAC probe must scan only the newest access unit ({ACCESS_UNITS} accesses => {expected} probes: one pass + one sync scan each); {probes} recorded"
);
}
#[test]
fn video_and_mpeg_audio_probe_work_is_linear_in_the_input() {
const ACCESS_UNITS: u32 = 400;
const AU_BYTES: usize = 1024;
let (sps, pps, _) = real_avc_parameter_sets();
let broken_sps = &sps[..sps.len().min(3)];
for (stream_type, au) in [
(
STREAM_TYPE_AVC,
annexb_au(&[broken_sps, &pps, &[0x41u8; 8]]),
),
(
STREAM_TYPE_MPEG1_AUDIO,
alloc::vec![0xE0u8; AU_BYTES],
),
] {
let mut au = au;
au.resize(AU_BYTES, 0x33);
let mut input = Vec::new();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
input.extend_from_slice(&null_packet());
}
input.extend_from_slice(&psi_section_packets(
PAT_PID_UNDER_TEST,
0,
TABLE_ID_PAT,
&pat_body(),
));
input.extend_from_slice(&psi_section_packets(
PMT_PID_UNDER_TEST,
0,
TABLE_ID_PMT,
&pmt_body(ES_PID_UNDER_TEST, stream_type),
));
let mut cc = 0u8;
for i in 0..ACCESS_UNITS {
let pes = pes_packet(ES_STREAM_ID_AUDIO, i as i64 * 1024, None, &au);
let packets = pes_packets(ES_PID_UNDER_TEST, cc, &pes);
cc = cc.wrapping_add((packets.len() / TS_PACKET_SIZE) as u8);
input.extend_from_slice(&packets);
}
let before = sync_probes();
let mut demux = StreamingTsDemux::new();
demux.feed(&input);
demux.finish();
let probes = sync_probes() - before;
assert!(
matches!(
demux
.streams
.get(&ES_PID_UNDER_TEST)
.and_then(|s| s.track.as_ref()),
Some(TrackState::Probing { .. }) | Some(TrackState::Abandoned)
),
"stream_type {stream_type:#04X} must never resolve its config"
);
if stream_type == STREAM_TYPE_AVC {
assert_eq!(
probes, ACCESS_UNITS as u64,
"the H.264 probe must make exactly one pass per access unit, never a backlog rescan: {probes} probes over {ACCESS_UNITS} access units"
);
} else {
let bound = ACCESS_UNITS as u64
* (1 + AU_BYTES as u64 + 2 * AUDIO_PROBE_BOUNDARY_BYTES as u64);
assert!(
probes <= bound,
"stream_type {stream_type:#04X}: {probes} probes over {ACCESS_UNITS} access units of {AU_BYTES} bytes — bound is {bound}; a quadratic rescan is ~{}",
(ACCESS_UNITS as u64).pow(2) / 2
);
}
}
}
fn avc_ts_over(aus: &[Vec<u8>]) -> Vec<u8> {
let mut cc = 0u8;
let mut input = Vec::new();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
input.extend_from_slice(&null_packet());
}
input.extend_from_slice(&psi_section_packets(
PAT_PID_UNDER_TEST,
0,
TABLE_ID_PAT,
&pat_body(),
));
input.extend_from_slice(&psi_section_packets(
PMT_PID_UNDER_TEST,
0,
TABLE_ID_PMT,
&pmt_body(ES_PID_UNDER_TEST, STREAM_TYPE_AVC),
));
for (i, au) in aus.iter().enumerate() {
let pes = pes_packet(
ES_STREAM_ID_VIDEO,
i as i64 * 3600,
Some(i as i64 * 3600),
au,
);
let packets = pes_packets(ES_PID_UNDER_TEST, cc, &pes);
cc = cc.wrapping_add((packets.len() / TS_PACKET_SIZE) as u8);
input.extend_from_slice(&packets);
}
input
}
fn updated_avc_width(bytes: &[u8]) -> Option<u16> {
let mut demux = StreamingTsDemux::new();
demux.feed(bytes);
demux.finish();
std::iter::from_fn(|| demux.poll_event()).find_map(|e| match e {
DemuxEvent::TrackUpdated(spec) => match spec.config {
CodecConfig::Avc { width, .. } => Some(width),
_ => None,
},
_ => None,
})
}
#[test]
fn signalled_discontinuity_is_scoped_to_the_signalling_program() {
const A_PMT: Pid = Pid::new(0x1000);
const A_ES: Pid = Pid::new(0x1100);
const A_ES2: Pid = Pid::new(0x1101);
const B_PMT: Pid = Pid::new(0x1001);
const B_ES: Pid = Pid::new(0x1200);
const A_PROGRAM: u16 = 1;
const B_PROGRAM: u16 = 2;
const FRAME_PERIOD: i64 = 3600;
const FRAMES: i64 = 8;
const A_BASE_B: i64 = 40_000;
const BASE_A: i64 = 900_000;
let annexb = real_annexb_frames();
let mut cc = 0u8;
let mut input = Vec::new();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
input.extend_from_slice(&null_packet());
}
let mut pat = Vec::new();
for (program, pmt_pid) in [(A_PROGRAM, A_PMT), (B_PROGRAM, B_PMT)] {
pat.extend_from_slice(&program.to_be_bytes());
pat.extend_from_slice(&(0xE000 | pmt_pid.value()).to_be_bytes());
}
input.extend_from_slice(&psi_section_packets_for(
PAT_PID_UNDER_TEST,
0,
TABLE_ID_PAT,
0,
&pat,
));
input.extend_from_slice(&psi_section_packets_for(
A_PMT,
0,
TABLE_ID_PMT,
A_PROGRAM,
&pmt_body_two_es_with_pcr_pid(A_ES, STREAM_TYPE_AVC, A_ES2, STREAM_TYPE_AVC, A_ES),
));
input.extend_from_slice(&psi_section_packets_for(
B_PMT,
0,
TABLE_ID_PMT,
B_PROGRAM,
&pmt_body_with_pcr_pid(B_ES, STREAM_TYPE_AVC, B_ES),
));
let push_au = |input: &mut Vec<u8>, cc: &mut u8, pid: Pid, au: &[u8], dts: i64| {
let wire = dts.rem_euclid(1i64 << 33) as u64;
let pes = pes_packet(ES_STREAM_ID_VIDEO, wire as i64, Some(wire as i64), au);
let packets = pes_packets(pid, *cc, &pes);
*cc = cc.wrapping_add((packets.len() / TS_PACKET_SIZE) as u8);
input.extend_from_slice(&packets);
};
for i in 0..FRAMES {
let au = annexb[(i as usize) % annexb.len()].clone();
push_au(&mut input, &mut cc, A_ES, &au, BASE_A + i * FRAME_PERIOD);
}
{
let mut pkt = [0xFFu8; TS_PACKET_SIZE];
pkt[0] = 0x47;
pkt[1] = (A_ES.value() >> 8) as u8 & PID_HI_MASK;
pkt[2] = (A_ES.value() & 0xFF) as u8;
pkt[3] = 0x20 | (cc & 0x0F); pkt[4] = 8;
pkt[5] = 0x80; input.extend_from_slice(&pkt);
cc = cc.wrapping_add(1);
}
for i in 0..FRAMES {
let au = annexb[(i as usize) % annexb.len()].clone();
push_au(&mut input, &mut cc, A_ES, &au, A_BASE_B + i * FRAME_PERIOD);
}
for i in 0..FRAMES * 2 {
let au = annexb[(i as usize) % annexb.len()].clone();
let dts = if i < FRAMES {
BASE_A + i * FRAME_PERIOD
} else {
A_BASE_B + (i - FRAMES) * FRAME_PERIOD
};
push_au(&mut input, &mut cc, A_ES2, &au, dts);
}
for i in 0..FRAMES * 2 {
let au = annexb[(i as usize) % annexb.len()].clone();
push_au(&mut input, &mut cc, B_ES, &au, BASE_A + i * FRAME_PERIOD);
}
let mut demux = StreamingTsDemux::new();
demux.feed(&input);
demux.finish();
let events: Vec<DemuxEvent> = std::iter::from_fn(|| demux.poll_event()).collect();
let ids: BTreeMap<TrackId, Pid> = events
.iter()
.filter_map(|e| match e {
DemuxEvent::TrackAdded(spec) => spec.source_pid.map(|pid| (spec.track_id, pid)),
_ => None,
})
.collect();
let mut samples_by_pid: BTreeMap<Pid, Vec<i64>> = BTreeMap::new();
for event in &events {
if let DemuxEvent::Sample { track_id, sample } = event
&& let Some(pid) = ids.get(track_id)
&& let Some(dts) = sample.dts
{
samples_by_pid
.entry(Pid::new((*pid).value()))
.or_default()
.push(dts);
}
}
let a_dts = samples_by_pid
.get(&A_ES)
.cloned()
.expect("program A's video PID must deliver samples");
let b_dts = samples_by_pid
.get(&B_ES)
.cloned()
.expect("program B must deliver samples");
assert!(
b_dts.len() >= FRAMES as usize,
"program B must deliver its samples, got {}",
b_dts.len()
);
let expected: Vec<i64> = (0..FRAMES * 2).map(|i| BASE_A + i * FRAME_PERIOD).collect();
let got: Vec<i64> = b_dts.iter().copied().take(expected.len()).collect();
assert_eq!(
got, expected,
"program B's dts must be untouched by program A's signalled discontinuity"
);
let a_steps: Vec<i64> = a_dts.windows(2).map(|w| w[1] - w[0]).collect();
assert!(
!a_steps.is_empty() && a_steps.iter().all(|&d| d > 0),
"program A's video timeline must be strictly increasing across its discontinuity: {a_dts:?}"
);
let a2 = samples_by_pid
.get(&A_ES2)
.expect("program A's second elementary stream must deliver samples");
let a2_steps: Vec<i64> = a2.windows(2).map(|w| w[1] - w[0]).collect();
assert!(
!a2_steps.is_empty() && a2_steps.iter().all(|&d| d > 0),
"program A's *second* PID must be rebased by the discontinuity on its PCR_PID too: {a2:?}"
);
}
fn real_annexb_frames() -> Vec<Vec<u8>> {
let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("..");
path.push("fixtures");
path.push("ts");
path.push("h264-aac.ts");
let source =
std::fs::read(&path).unwrap_or_else(|e| panic!("read fixture {}: {e}", path.display()));
let media = TsDemux::new().demux(&source).expect("demux h264-aac.ts");
let video = media
.tracks
.iter()
.find(|t| matches!(t.config(), CodecConfig::Avc { .. }))
.expect("h264-aac.ts has an AVC track");
video
.samples
.iter()
.map(|sample| {
let mut au = Vec::new();
for nal in crate::annexb::iter_length_prefixed_nals(&sample.data)
.expect("TsMux writes valid NAL prefixes")
{
au.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]);
au.extend_from_slice(nal);
}
au
})
.collect()
}
#[test]
fn jittered_cadence_backward_discontinuity_rebases() {
const PERIODS: [i64; 2] = [3754, 3753];
const BASE_A: i64 = 900_000;
const BASE_B: i64 = 120_000;
const FRAMES: i64 = 30;
let annexb = real_annexb_frames();
let mut cc = 0u8;
let mut input = Vec::new();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
input.extend_from_slice(&null_packet());
}
input.extend_from_slice(&psi_section_packets(
PAT_PID_UNDER_TEST,
0,
TABLE_ID_PAT,
&pat_body(),
));
input.extend_from_slice(&psi_section_packets(
PMT_PID_UNDER_TEST,
0,
TABLE_ID_PMT,
&pmt_body_with_pcr_pid(ES_PID_UNDER_TEST, STREAM_TYPE_AVC, ES_PID_UNDER_TEST),
));
let push = |input: &mut Vec<u8>, cc: &mut u8, au: &[u8], dts: i64| {
let wire = dts.rem_euclid(1i64 << 33);
let pes = pes_packet(ES_STREAM_ID_VIDEO, wire, Some(wire), au);
let packets = pes_packets(ES_PID_UNDER_TEST, *cc, &pes);
*cc = cc.wrapping_add((packets.len() / TS_PACKET_SIZE) as u8);
input.extend_from_slice(&packets);
};
let mut dts = BASE_A;
for i in 0..FRAMES * 2 {
if i == FRAMES {
let mut pkt = [0xFFu8; TS_PACKET_SIZE];
pkt[0] = 0x47;
pkt[1] = (ES_PID_UNDER_TEST.value() >> 8) as u8 & PID_HI_MASK;
pkt[2] = (ES_PID_UNDER_TEST.value() & 0xFF) as u8;
pkt[3] = 0x20 | (cc & 0x0F);
pkt[4] = 8;
pkt[5] = 0x80; input.extend_from_slice(&pkt);
cc = cc.wrapping_add(1);
dts = BASE_B;
}
let au = annexb[(i as usize) % annexb.len()].clone();
push(&mut input, &mut cc, &au, dts);
dts += PERIODS[(i as usize) % PERIODS.len()];
}
let mut demux = StreamingTsDemux::new();
demux.feed(&input);
demux.finish();
let got: Vec<i64> = std::iter::from_fn(|| demux.poll_event())
.filter_map(|e| match e {
DemuxEvent::Sample { sample, .. } => sample.dts,
_ => None,
})
.collect();
assert!(
got.len() >= FRAMES as usize,
"expected the access units of both bases, got {}",
got.len()
);
let steps: Vec<i64> = got.windows(2).map(|w| w[1] - w[0]).collect();
assert!(
steps.iter().all(|&d| d > 0),
"dts must be strictly increasing across a jittered discontinuity; dts={got:?}"
);
let seam = steps
.iter()
.copied()
.filter(|&d| d > PERIODS[1])
.max()
.unwrap_or(0);
assert!(
seam <= PERIODS[0] + 1,
"the seam must continue the cadence (one period), not jump: largest step {seam}, steps={steps:?}"
);
}
#[test]
fn an_eight_second_splice_does_not_become_the_frame_period() {
const FRAME_PERIOD: i64 = 3600;
const SPLICE: i64 = 8 * 90_000;
const STAMPED: i64 = 8;
const UNSTAMPED: usize = 4;
let annexb = real_annexb_frames();
let mut cc = 0u8;
let mut input = Vec::new();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
input.extend_from_slice(&null_packet());
}
input.extend_from_slice(&psi_section_packets(
PAT_PID_UNDER_TEST,
0,
TABLE_ID_PAT,
&pat_body(),
));
input.extend_from_slice(&psi_section_packets(
PMT_PID_UNDER_TEST,
0,
TABLE_ID_PMT,
&pmt_body(ES_PID_UNDER_TEST, STREAM_TYPE_AVC),
));
let push = |input: &mut Vec<u8>, cc: &mut u8, au: &[u8], dts: Option<i64>| {
let pes = match dts {
Some(d) => {
let wire = d.rem_euclid(1i64 << 33);
pes_packet(ES_STREAM_ID_VIDEO, wire, Some(wire), au)
}
None => pes_packet_untimed(ES_STREAM_ID_VIDEO, au),
};
let packets = pes_packets(ES_PID_UNDER_TEST, *cc, &pes);
*cc = cc.wrapping_add((packets.len() / TS_PACKET_SIZE) as u8);
input.extend_from_slice(&packets);
};
for i in 0..STAMPED {
let au = annexb[(i as usize) % annexb.len()].clone();
push(&mut input, &mut cc, &au, Some(90_000 + i * FRAME_PERIOD));
}
let mut at = 90_000 + (STAMPED - 1) * FRAME_PERIOD + SPLICE;
for i in 0..6usize {
let au = annexb[i % annexb.len()].clone();
push(&mut input, &mut cc, &au, Some(at));
at += SPLICE;
}
let after_splice = at - SPLICE;
for i in 1..=UNSTAMPED {
let au = annexb[i % annexb.len()].clone();
push(&mut input, &mut cc, &au, None);
}
let mut demux = StreamingTsDemux::new();
demux.feed(&input);
demux.finish();
let dts: Vec<i64> = std::iter::from_fn(|| demux.poll_event())
.filter_map(|e| match e {
DemuxEvent::Sample { sample, .. } => sample.dts,
_ => None,
})
.collect();
let tail: Vec<i64> = dts.iter().copied().filter(|&d| d > after_splice).collect();
assert!(
tail.len() >= UNSTAMPED - 1,
"expected the unstamped frames to be delivered, got {dts:?}"
);
let steps: Vec<i64> = tail.windows(2).map(|w| w[1] - w[0]).collect();
assert!(
steps.iter().all(|&d| d == FRAME_PERIOD),
"the splice must not become the frame period: steps={steps:?} (an adopted 8-second period would give {SPLICE})"
);
}
#[test]
fn truncated_final_bounded_pes_is_dropped_at_flush() {
const BODY: usize = 300;
let au: Vec<u8> = (0..BODY).map(|i| (i & 0xFF) as u8).collect();
let pes = untimed_pes(ES_STREAM_ID_PRIVATE_DATA, &au);
let mut input = Vec::new();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
input.extend_from_slice(&null_packet());
}
input.extend_from_slice(&psi_section_packets(
PAT_PID_UNDER_TEST,
0,
TABLE_ID_PAT,
&pat_body(),
));
input.extend_from_slice(&psi_section_packets(
PMT_PID_UNDER_TEST,
0,
TABLE_ID_PMT,
&pmt_body(ES_PID_UNDER_TEST, STREAM_TYPE_PES_PRIVATE),
));
let packets = pes_packets(ES_PID_UNDER_TEST, 0, &pes);
assert_eq!(
packets.len(),
2 * TS_PACKET_SIZE,
"the unit spans two packets"
);
input.extend_from_slice(&packets[..TS_PACKET_SIZE]);
let mut demux = StreamingTsDemux::new();
demux.feed(&input);
demux.finish();
let samples = std::iter::from_fn(|| demux.poll_event())
.filter(|e| matches!(e, DemuxEvent::Sample { .. }))
.count();
assert_eq!(
samples, 0,
"a bounded PES that never reached its declared length must not be delivered by finish()"
);
}
#[test]
fn wrap_coincident_discontinuity_keeps_one_frame_period_steps() {
const FRAME_PERIOD: i64 = 3600;
const FRAMES_A: i64 = 3;
const WRAP: i64 = 1i64 << 33;
const BASE_A: i64 = WRAP - (FRAMES_A + 1) * FRAME_PERIOD;
const A_LAST: i64 = BASE_A + (FRAMES_A - 1) * FRAME_PERIOD;
const BASE_B: i64 = BASE_A + FRAMES_A * FRAME_PERIOD - WRAP;
let annexb = real_annexb_frames();
let mut cc = 0u8;
let mut input = Vec::new();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
input.extend_from_slice(&null_packet());
}
input.extend_from_slice(&psi_section_packets(
PAT_PID_UNDER_TEST,
0,
TABLE_ID_PAT,
&pat_body(),
));
input.extend_from_slice(&psi_section_packets(
PMT_PID_UNDER_TEST,
0,
TABLE_ID_PMT,
&pmt_body_with_pcr_pid(ES_PID_UNDER_TEST, STREAM_TYPE_AVC, ES_PID_UNDER_TEST),
));
let push = |input: &mut Vec<u8>, cc: &mut u8, au: &[u8], raw: i64| {
let wire = raw.rem_euclid(WRAP);
let pes = pes_packet(ES_STREAM_ID_VIDEO, wire, Some(wire), au);
let packets = pes_packets(ES_PID_UNDER_TEST, *cc, &pes);
*cc = cc.wrapping_add((packets.len() / TS_PACKET_SIZE) as u8);
input.extend_from_slice(&packets);
};
for i in 0..FRAMES_A {
let au = annexb[(i as usize) % annexb.len()].clone();
push(&mut input, &mut cc, &au, BASE_A + i * FRAME_PERIOD);
}
{
let mut pkt = [0xFFu8; TS_PACKET_SIZE];
pkt[0] = 0x47;
pkt[1] = (ES_PID_UNDER_TEST.value() >> 8) as u8 & PID_HI_MASK;
pkt[2] = (ES_PID_UNDER_TEST.value() & 0xFF) as u8;
pkt[3] = 0x20 | (cc & 0x0F);
pkt[4] = 8;
pkt[5] = 0x80; input.extend_from_slice(&pkt);
cc = cc.wrapping_add(1);
}
for i in 0..FRAMES_A + 2 {
let au = annexb[((FRAMES_A + i) as usize) % annexb.len()].clone();
push(&mut input, &mut cc, &au, BASE_B + i * FRAME_PERIOD);
}
let mut demux = StreamingTsDemux::new();
demux.feed(&input);
demux.finish();
let dts: Vec<i64> = std::iter::from_fn(|| demux.poll_event())
.filter_map(|e| match e {
DemuxEvent::Sample { sample, .. } => sample.dts,
_ => None,
})
.collect();
assert!(
dts.len() >= (FRAMES_A + 2) as usize,
"expected the access units of both bases, got {}",
dts.len()
);
let steps: Vec<i64> = dts.windows(2).map(|w| w[1] - w[0]).collect();
assert!(
steps.iter().all(|&d| d == FRAME_PERIOD),
"every step must stay exactly one frame period across the wrap and the seam; dts={dts:?} steps={steps:?}"
);
let first_of_b = dts[dts.len() - (FRAMES_A as usize + 2)];
assert!(
first_of_b > A_LAST && first_of_b - A_LAST < 2 * FRAME_PERIOD,
"base B must continue base A across the wrap, not restart near zero: first_of_b={first_of_b} last_of_a={A_LAST}"
);
}
#[test]
fn forward_jump_at_a_signalled_discontinuity_is_not_collapsed() {
const FRAME_PERIOD: i64 = 3600;
const FRAMES: i64 = 6;
const BASE_A: i64 = 900_000;
const FORWARD_JUMP: i64 = 8 * 90_000;
let annexb = real_annexb_frames();
let mut cc = 0u8;
let mut input = Vec::new();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
input.extend_from_slice(&null_packet());
}
input.extend_from_slice(&psi_section_packets(
PAT_PID_UNDER_TEST,
0,
TABLE_ID_PAT,
&pat_body(),
));
input.extend_from_slice(&psi_section_packets(
PMT_PID_UNDER_TEST,
0,
TABLE_ID_PMT,
&pmt_body_with_pcr_pid(ES_PID_UNDER_TEST, STREAM_TYPE_AVC, ES_PID_UNDER_TEST),
));
let push = |input: &mut Vec<u8>, cc: &mut u8, au: &[u8], dts: i64| {
let wire = dts.rem_euclid(1i64 << 33);
let pes = pes_packet(ES_STREAM_ID_VIDEO, wire, Some(wire), au);
let packets = pes_packets(ES_PID_UNDER_TEST, *cc, &pes);
*cc = cc.wrapping_add((packets.len() / TS_PACKET_SIZE) as u8);
input.extend_from_slice(&packets);
};
for i in 0..FRAMES {
let au = annexb[(i as usize) % annexb.len()].clone();
push(&mut input, &mut cc, &au, BASE_A + i * FRAME_PERIOD);
}
let base_b = BASE_A + (FRAMES - 1) * FRAME_PERIOD + FORWARD_JUMP;
{
let mut pkt = [0xFFu8; TS_PACKET_SIZE];
pkt[0] = 0x47;
pkt[1] = (ES_PID_UNDER_TEST.value() >> 8) as u8 & PID_HI_MASK;
pkt[2] = (ES_PID_UNDER_TEST.value() & 0xFF) as u8;
pkt[3] = 0x20 | (cc & 0x0F);
pkt[4] = 8;
pkt[5] = 0x80;
input.extend_from_slice(&pkt);
cc = cc.wrapping_add(1);
}
for i in 0..FRAMES {
let au = annexb[(i as usize) % annexb.len()].clone();
push(&mut input, &mut cc, &au, base_b + i * FRAME_PERIOD);
}
let mut demux = StreamingTsDemux::new();
demux.feed(&input);
demux.finish();
let dts: Vec<i64> = std::iter::from_fn(|| demux.poll_event())
.filter_map(|e| match e {
DemuxEvent::Sample { sample, .. } => sample.dts,
_ => None,
})
.collect();
assert!(dts.len() >= 2, "expected samples from both bases");
let steps: Vec<i64> = dts.windows(2).map(|w| w[1] - w[0]).collect();
let seam = steps
.iter()
.copied()
.find(|&d| d > FRAME_PERIOD)
.unwrap_or_else(|| {
panic!("the forward jump must survive as a large step, got {steps:?}")
});
assert!(
seam >= FORWARD_JUMP,
"a forward jump of {FORWARD_JUMP} ticks must not be collapsed to {seam}; the offset must never be reduced"
);
assert!(
steps.iter().all(|&d| d > 0),
"and the timeline must still be strictly increasing: {steps:?}"
);
}
fn real_avc_parameter_sets() -> (Vec<u8>, Vec<u8>, u16) {
let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("..");
path.push("fixtures");
path.push("ts");
path.push("h264-aac.ts");
let source =
std::fs::read(&path).unwrap_or_else(|e| panic!("read fixture {}: {e}", path.display()));
let media = TsDemux::new().demux(&source).expect("demux h264-aac.ts");
let video = media
.tracks
.iter()
.find(|t| matches!(t.config(), CodecConfig::Avc { .. }))
.expect("h264-aac.ts has an AVC track");
let width = match video.config() {
CodecConfig::Avc { width, .. } => *width,
_ => unreachable!(),
};
let first = &video.samples[0].data;
let nals = crate::annexb::iter_length_prefixed_nals(first)
.expect("TsMux writes valid NAL prefixes");
let sps = nals
.iter()
.find(|n| (n[0] & H264_NAL_TYPE_MASK) == H264_NAL_SPS)
.expect("the first access unit carries an SPS");
let pps = nals
.iter()
.find(|n| (n[0] & H264_NAL_TYPE_MASK) == H264_NAL_PPS)
.expect("the first access unit carries a PPS");
(sps.to_vec(), pps.to_vec(), width)
}
fn annexb_au(nals: &[&[u8]]) -> Vec<u8> {
let mut au = Vec::new();
for nal in nals {
au.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]);
au.extend_from_slice(nal);
}
au
}
fn slice_only_au(tag: u8) -> Vec<u8> {
annexb_au(&[&[0x65, 0x88, 0x84, tag]])
}
#[test]
fn parameter_sets_in_one_access_unit_then_slices_emit_no_update() {
let (sps, pps, width) = real_avc_parameter_sets();
let mut aus = vec![annexb_au(&[&sps, &pps, &[0x65, 0x88, 0x84, 0x01]])];
for i in 0..4u8 {
aus.push(slice_only_au(i));
}
let bytes = avc_ts_over(&aus);
let mut demux = StreamingTsDemux::new();
demux.feed(&bytes);
demux.finish();
let added_width = std::iter::from_fn(|| demux.poll_event()).find_map(|e| match e {
DemuxEvent::TrackAdded(spec) => match spec.config {
CodecConfig::Avc { width, .. } => Some(width),
_ => None,
},
_ => None,
});
assert_eq!(
added_width,
Some(width),
"the track must resolve from the parameter sets in the first access unit"
);
assert_eq!(
updated_avc_width(&bytes),
None,
"slice-only access units carry no parameter sets, so nothing changed; a per-access-unit comparison raised a spurious TrackUpdated here"
);
}
#[test]
fn parameter_sets_split_across_access_units_emit_no_update() {
let (sps, pps, width) = real_avc_parameter_sets();
let aus = vec![
annexb_au(&[&sps, &[0x65, 0x88, 0x84, 0x01]]),
annexb_au(&[&pps, &[0x65, 0x88, 0x84, 0x02]]),
slice_only_au(0x03),
];
let bytes = avc_ts_over(&aus);
assert_eq!(
updated_avc_width(&bytes),
None,
"the PPS arriving one access unit after the SPS is not a change; a one-access-unit comparison reported one (and would have reported the SPS's access unit too). Track added at width {width}"
);
}
#[test]
fn a_changed_sps_raises_exactly_one_update() {
let (sps, pps, _) = real_avc_parameter_sets();
let path = test_fixtures::path("ts/h264-two-resolutions.ts");
let other =
std::fs::read(&path).unwrap_or_else(|e| panic!("read fixture {}: {e}", path.display()));
let media = TsDemux::new()
.demux(&other)
.expect("demux two-resolution fixture");
let second_sps = media
.tracks
.iter()
.filter(|t| matches!(t.config(), CodecConfig::Avc { .. }))
.flat_map(|t| t.samples.iter())
.flat_map(|s| {
crate::annexb::iter_length_prefixed_nals(&s.data)
.unwrap_or_default()
.into_iter()
.filter(|n| (n[0] & H264_NAL_TYPE_MASK) == H264_NAL_SPS)
.map(|n| n.to_vec())
.collect::<Vec<_>>()
})
.find(|s| s.as_slice() != sps.as_slice())
.expect("the two-resolution fixture carries a second, different SPS");
assert_ne!(second_sps, sps, "the two SPS really differ");
let aus = vec![
annexb_au(&[&sps, &pps, &[0x65, 0x88, 0x84, 0x01]]),
slice_only_au(0x02),
annexb_au(&[&second_sps, &pps, &[0x65, 0x88, 0x84, 0x03]]),
slice_only_au(0x04),
];
let bytes = avc_ts_over(&aus);
let mut demux = StreamingTsDemux::new();
demux.feed(&bytes);
demux.finish();
let updates: Vec<(u16, u16)> = std::iter::from_fn(|| demux.poll_event())
.filter_map(|e| match e {
DemuxEvent::TrackUpdated(spec) => match spec.config {
CodecConfig::Avc { width, height, .. } => Some((width, height)),
_ => None,
},
_ => None,
})
.collect();
assert_eq!(
updates.len(),
1,
"exactly one SPS change, so exactly one TrackUpdated: {updates:?}"
);
assert_ne!(
updates[0],
(0, 0),
"the event must carry the newly-decoded geometry, not zeros"
);
}
#[test]
fn parameter_sets_split_across_access_units_raise_exactly_one_update() {
let (sps, pps, _) = real_avc_parameter_sets();
let aus = vec![
annexb_au(&[&sps, &[0x65, 0x88, 0x84, 0x01]]),
annexb_au(&[&[0x65, 0x88, 0x84, 0x02]]),
annexb_au(&[&pps, &[0x65, 0x88, 0x84, 0x03]]),
slice_only_au(0x04),
slice_only_au(0x05),
];
let bytes = avc_ts_over(&aus);
let mut demux = StreamingTsDemux::new();
demux.feed(&bytes);
demux.finish();
let updates = std::iter::from_fn(|| demux.poll_event())
.filter(|e| matches!(e, DemuxEvent::TrackUpdated(_)))
.count();
assert_eq!(
updates, 0,
"no parameter set ever changes value here — the SPS and the PPS just arrive in different access units — so nothing may be reported"
);
}
#[test]
fn alternating_parameter_set_carriage_emits_no_update() {
let (sps, pps, _) = real_avc_parameter_sets();
let mut aus = vec![annexb_au(&[&sps, &pps, &[0x65, 0x88, 0x84, 0x01]])];
for i in 0..4u8 {
aus.push(annexb_au(&[&pps, &[0x65, 0x88, 0x84, i + 2]]));
aus.push(annexb_au(&[&sps, &pps, &[0x65, 0x88, 0x84, i + 10]]));
}
let bytes = avc_ts_over(&aus);
let mut demux = StreamingTsDemux::new();
demux.feed(&bytes);
demux.finish();
let updates = std::iter::from_fn(|| demux.poll_event())
.filter(|e| matches!(e, DemuxEvent::TrackUpdated(_)))
.count();
assert_eq!(
updates, 0,
"the configuration never changes, so no TrackUpdated may be raised; a per-access-unit snapshot loses the SPS during the PPS-only units and reports each following full set as a change"
);
}
fn pps_with_id(pps: &[u8], id: u64) -> Vec<u8> {
let rbsp = crate::bitreader::unescape(&pps[1..]);
let bits: Vec<bool> = rbsp
.iter()
.flat_map(|b| (0..8).rev().map(move |i| (b >> i) & 1 == 1))
.collect();
let zeros = bits.iter().take_while(|b| !**b).count();
let rest = &bits[2 * zeros + 1..];
let stop = rest.iter().rposition(|b| *b).expect("stop bit");
let v = id + 1;
let width = 64 - v.leading_zeros() as usize;
let mut out_bits: Vec<bool> = vec![false; width - 1];
out_bits.extend((0..width).rev().map(|i| (v >> i) & 1 == 1));
out_bits.extend_from_slice(&rest[..=stop]);
while !out_bits.len().is_multiple_of(8) {
out_bits.push(false);
}
let mut out = vec![pps[0]];
let mut zero_run = 0;
for chunk in out_bits.chunks(8) {
let byte = chunk.iter().fold(0u8, |a, b| (a << 1) | u8::from(*b));
if zero_run >= 2 && byte <= 3 {
out.push(3);
zero_run = 0;
}
zero_run = if byte == 0 { zero_run + 1 } else { 0 };
out.push(byte);
}
out
}
fn rotating_pps_set(pps: &[u8]) -> Vec<Vec<u8>> {
assert_eq!(pps_id(Codec::H264, pps), 0, "the real PPS is id 0");
(0u64..3).map(|i| pps_with_id(pps, i)).collect()
}
#[test]
fn pps_with_id_helper_changes_only_the_id() {
let (_, pps, _) = real_avc_parameter_sets();
assert_eq!(pps_with_id(&pps, 0), pps, "id 0 reproduces the real PPS");
for id in [1u64, 2, 3, 17, 255] {
let v = pps_with_id(&pps, id);
assert_eq!(pps_id(Codec::H264, &v), id);
assert!(parameter_set_is_valid(NalCodec::Avc, ParamKind::Pps, &v));
}
}
fn events_chunked(bytes: &[u8], chunk: usize) -> (usize, Vec<String>) {
let mut demux = StreamingTsDemux::new();
let mut log = Vec::new();
let mut updates = 0;
for c in bytes.chunks(chunk) {
demux.feed(c);
while let Some(e) = demux.poll_event() {
if matches!(e, DemuxEvent::TrackUpdated(_)) {
updates += 1;
}
log.push(format!("{e:?}"));
}
}
demux.finish();
while let Some(e) = demux.poll_event() {
if matches!(e, DemuxEvent::TrackUpdated(_)) {
updates += 1;
}
log.push(format!("{e:?}"));
}
(updates, log)
}
#[test]
fn rotating_pps_ids_emit_no_update_and_are_chunk_invariant() {
let (sps, pps, _) = real_avc_parameter_sets();
let set = rotating_pps_set(&pps);
let mut aus = vec![annexb_au(&[&sps, &set[0], &[0x65, 0x88, 0x84, 0x01]])];
for i in 0..12usize {
aus.push(annexb_au(&[
&set[(i + 1) % 3],
&[0x65, 0x88, 0x84, 0x10 + i as u8],
]));
}
let bytes = avc_ts_over(&aus);
let (baseline, base_log) = events_chunked(&bytes, bytes.len());
assert!(
baseline == 2,
"rotation must not flap: got {baseline} updates"
);
let all_in_first_unit: Vec<Vec<u8>> = vec![annexb_au(&[
&sps,
&set[0],
&set[1],
&set[2],
&[0x65, 0x88, 0x84, 0x01],
])]
.into_iter()
.chain(
(0..12usize)
.map(|i| annexb_au(&[&set[(i + 1) % 3], &[0x65, 0x88, 0x84, 0x10 + i as u8]])),
)
.collect();
let bytes2 = avc_ts_over(&all_in_first_unit);
for chunk in [1usize, 7, 187, 188, 189, 1000] {
let (u, log) = events_chunked(&bytes2, chunk);
assert_eq!(u, 0, "chunk {chunk}: all ids announced up front, so none");
let (_, ref_log) = events_chunked(&bytes2, bytes2.len());
assert_eq!(log, ref_log, "chunk {chunk}: event sequence differs");
let (ub, lb) = events_chunked(&bytes, chunk);
assert_eq!(ub, baseline, "chunk {chunk}: update count differs");
assert_eq!(lb, base_log, "chunk {chunk}: event sequence differs");
}
}
#[test]
fn same_length_pps_change_after_rotation_raises_exactly_one_update() {
let (sps, pps, _) = real_avc_parameter_sets();
let set = rotating_pps_set(&pps);
let changed = pps_with_id(&pps, 3);
assert!(!set.contains(&changed));
let mut aus = vec![annexb_au(&[
&sps,
&set[0],
&set[1],
&set[2],
&[0x65, 0x88, 0x84, 0x01],
])];
for i in 0..6usize {
aus.push(annexb_au(&[
&set[i % 3],
&[0x65, 0x88, 0x84, 0x10 + i as u8],
]));
}
aus.push(annexb_au(&[&changed, &[0x65, 0x88, 0x84, 0x40]]));
for i in 0..6usize {
aus.push(annexb_au(&[
&set[i % 3],
&[0x65, 0x88, 0x84, 0x50 + i as u8],
]));
aus.push(annexb_au(&[&changed, &[0x65, 0x88, 0x84, 0x60 + i as u8]]));
}
let bytes = avc_ts_over(&aus);
let (updates, base) = events_chunked(&bytes, bytes.len());
assert_eq!(updates, 1, "exactly one update for the one real change");
for chunk in [1usize, 188, 1000] {
assert_eq!(events_chunked(&bytes, chunk).1, base, "chunk {chunk}");
}
}
#[test]
fn pps_a_b_a_on_one_id_is_reported_at_every_switch() {
let (sps, pps, _) = real_avc_parameter_sets();
let a = pps.clone();
let mut b = pps.clone();
let mid = b.len() - 2;
b[mid] ^= 0x04;
assert!(parameter_set_is_valid(NalCodec::Avc, ParamKind::Pps, &b));
assert_eq!(pps_id(Codec::H264, &a), pps_id(Codec::H264, &b));
assert_ne!(a, b);
let idr = [0x65u8, 0x88, 0x84, 0x01];
let mut aus = vec![annexb_au(&[&sps, &a, &idr])];
let mut seq: Vec<&Vec<u8>> = Vec::new();
for _ in 0..3 {
seq.push(&b);
}
for _ in 0..3 {
seq.push(&a);
}
seq.push(&b);
seq.push(&a);
for (i, p) in seq.iter().enumerate() {
aus.push(annexb_au(&[p, &[0x65, 0x88, 0x84, 0x10 + i as u8]]));
}
let bytes = avc_ts_over(&aus);
let (updates, base) = events_chunked(&bytes, bytes.len());
assert_eq!(updates, 4, "every A<->B switch on one id is a change");
for chunk in [1usize, 188, 1000] {
assert_eq!(events_chunked(&bytes, chunk).1, base, "chunk {chunk}");
}
}
#[test]
fn pps_alternating_between_two_ids_is_rotation_not_change() {
let (sps, pps, _) = real_avc_parameter_sets();
let a = pps_with_id(&pps, 0);
let b = pps_with_id(&pps, 1);
let idr = [0x65u8, 0x88, 0x84, 0x01];
let mut aus = vec![annexb_au(&[&sps, &a, &idr])];
for i in 0..8usize {
let p = if i % 2 == 0 { &b } else { &a };
aus.push(annexb_au(&[p, &[0x65, 0x88, 0x84, 0x10 + i as u8]]));
}
let bytes = avc_ts_over(&aus);
let (updates, _) = events_chunked(&bytes, bytes.len());
assert_eq!(updates, 1, "only the first sight of id 1");
}
#[test]
fn invalid_parameter_sets_are_ignored_and_counted() {
let (sps, pps, _) = real_avc_parameter_sets();
let mut huge_sps = sps.clone();
huge_sps.resize(85_704, 0xA5);
let stub_sps = vec![0x67u8, 0x42, 0x00, 0x1E, 0xAB, 0x40];
let idr = [0x65u8, 0x88, 0x84, 0x01];
let aus = vec![
annexb_au(&[&sps, &pps, &idr]),
annexb_au(&[&huge_sps, &pps, &idr]),
annexb_au(&[&stub_sps, &idr]),
annexb_au(&[&sps, &pps, &idr]),
];
let bytes = avc_ts_over(&aus);
let (updates, base) = events_chunked(&bytes, bytes.len());
assert_eq!(updates, 0, "invalid sets must not raise TrackUpdated");
let added = base.iter().filter(|l| l.starts_with("TrackAdded")).count();
assert_eq!(added, 1, "one track, from the valid set only");
let rejected = base
.iter()
.filter(|l| l.contains("InvalidParameterSet"))
.count();
assert_eq!(rejected, 2, "the huge and the stub SPS: {base:?}");
let media = TsDemux::new().demux(&bytes).expect("demux");
for t in &media.tracks {
let CodecConfig::Avc { config, .. } = t.config() else {
continue;
};
assert!(config.try_to_bytes().is_ok());
}
for chunk in [1usize, 7, 187, 188, 189, 1000] {
assert_eq!(events_chunked(&bytes, chunk).1, base, "chunk {chunk}");
}
}
#[test]
fn hevc_rotating_parameter_sets_are_not_changes() {
let hdr = |t: u8| [t << 1, 0x01];
let nal = |t: u8, tag: u8| {
let mut n = hdr(t).to_vec();
n.push(tag);
n
};
let real_sps: Vec<u8> = test_fixtures::vectors::HEVC_MAIN_SPS_320X240.to_vec();
let mut real_sps_b = real_sps.clone();
*real_sps_b.last_mut().unwrap() ^= 0x01;
let (v, s) = (nal(H265_NAL_VPS, 1), real_sps);
let ppss = [
nal(H265_NAL_PPS, 0b0101_0000),
nal(H265_NAL_PPS, 0b0111_0000),
];
let mut sets = ParameterSets::default();
let au = |p: &Vec<u8>| annexb_au(&[&v, &s, p]);
assert!(sets.observe(Codec::Hevc, &au(&ppss[0])));
assert!(
sets.observe(Codec::Hevc, &au(&ppss[1])),
"first sight of PPS 2"
);
for i in 0..8 {
assert!(
!sets.observe(Codec::Hevc, &au(&ppss[i % 2])),
"rotation step {i} is a repeat"
);
}
assert!(sets.observe(Codec::Hevc, &au(&nal(H265_NAL_PPS, 0b0010_0100))));
let id1_other = nal(H265_NAL_PPS, 0b0101_1000);
assert!(sets.observe(Codec::Hevc, &au(&id1_other)), "id 1 content B");
assert!(sets.observe(Codec::Hevc, &au(&ppss[0])), "id 1 back to A");
let sps2 = real_sps_b;
assert!(sets.observe(Codec::Hevc, &annexb_au(&[&sps2])), "SPS B");
assert!(
sets.observe(Codec::Hevc, &annexb_au(&[&s])),
"SPS back to A"
);
}
fn two_resolution_sets() -> [(Vec<u8>, Vec<u8>, u16); 2] {
let path = test_fixtures::path("ts/h264-two-resolutions.ts");
let bytes = std::fs::read(&path).unwrap();
let mut demux = StreamingTsDemux::new();
demux.feed(&bytes);
demux.finish();
let mut out = Vec::new();
while let Some(e) = demux.poll_event() {
let spec = match e {
DemuxEvent::TrackAdded(s) | DemuxEvent::TrackUpdated(s) => s,
_ => continue,
};
if let CodecConfig::Avc { config, width, .. } = spec.config {
let r = &config.config;
out.push((r.sps[0].0.clone(), r.pps[0].0.clone(), width));
}
}
assert!(out.len() >= 2 && out[0].2 != out[1].2, "two resolutions");
[out[0].clone(), out[1].clone()]
}
#[test]
fn sps_switch_a_b_a_raises_an_update_per_switch() {
let [a, b] = two_resolution_sets();
let au =
|x: &(Vec<u8>, Vec<u8>, u16), t: u8| annexb_au(&[&x.0, &x.1, &[0x65, 0x88, 0x84, t]]);
let aus = vec![au(&a, 1), au(&b, 2), slice_only_au(3), au(&a, 4), au(&b, 5)];
let bytes = avc_ts_over(&aus);
let (n, base) = events_chunked(&bytes, bytes.len());
assert_eq!(n, 3, "one update for each of the three switches");
for chunk in [1usize, 7, 187, 188, 189, 1000] {
assert_eq!(events_chunked(&bytes, chunk).1, base, "chunk {chunk}");
}
}
#[test]
fn same_length_sps_change_raises_exactly_one_update() {
let (sps, pps, _) = real_avc_parameter_sets();
let mut other = sps.clone();
let last = other.len() - 1;
other[last] ^= 0x01;
let aus = vec![
annexb_au(&[&sps, &pps, &[0x65, 0x88, 0x84, 1]]),
annexb_au(&[&other, &pps, &[0x65, 0x88, 0x84, 2]]),
annexb_au(&[&other, &pps, &[0x65, 0x88, 0x84, 3]]),
];
let bytes = avc_ts_over(&aus);
let (n, base) = events_chunked(&bytes, bytes.len());
assert_eq!(n, 1);
for chunk in [1usize, 7, 187, 188, 189, 1000] {
assert_eq!(events_chunked(&bytes, chunk).1, base, "chunk {chunk}");
}
}
#[test]
fn a_changed_pps_raises_exactly_one_update() {
let (sps, pps, _) = real_avc_parameter_sets();
let mut other_pps = pps.clone();
let last = other_pps.len() - 1;
other_pps[last] ^= 0x55;
assert_ne!(other_pps, pps, "the two PPS really differ");
let aus = vec![
annexb_au(&[&sps, &pps, &[0x65, 0x88, 0x84, 0x01]]),
annexb_au(&[&sps, &other_pps, &[0x65, 0x88, 0x84, 0x02]]),
slice_only_au(0x03),
];
let bytes = avc_ts_over(&aus);
let mut demux = StreamingTsDemux::new();
demux.feed(&bytes);
demux.finish();
let updates = std::iter::from_fn(|| demux.poll_event())
.filter(|e| matches!(e, DemuxEvent::TrackUpdated(_)))
.count();
assert_eq!(
updates, 1,
"a changed PPS is a config change, so exactly one TrackUpdated"
);
}
#[test]
fn mid_stream_config_change_emits_track_updated_once_and_only_when_it_changes() {
let path = test_fixtures::path("ts/h264-two-resolutions.ts");
let bytes =
std::fs::read(&path).unwrap_or_else(|e| panic!("read fixture {}: {e}", path.display()));
let mut demux = StreamingTsDemux::new();
demux.feed(&bytes);
demux.finish();
let mut added: Option<(u32, u16, u16)> = None;
let mut updates: Vec<(u32, u16, u16)> = Vec::new();
while let Some(event) = demux.poll_event() {
match event {
DemuxEvent::TrackAdded(spec) => {
if let CodecConfig::Avc { width, height, .. } = spec.config {
added = Some((spec.track_id.value(), width, height));
}
}
DemuxEvent::TrackUpdated(spec) => {
if let CodecConfig::Avc { width, height, .. } = spec.config {
updates.push((spec.track_id.value(), width, height));
}
}
_ => {}
}
}
let (track_id, w0, h0) = added.expect("the fixture has one AVC track");
assert_eq!(
(w0, h0),
(320, 240),
"the first half of the fixture is 320x240 (ffprobe oracle)"
);
assert_eq!(
updates.len(),
1,
"the config changes exactly once in this fixture (320x240 -> 640x480); the encoder's repeated, unchanged SPS must not raise more. Got {updates:?}"
);
assert_eq!(
updates[0],
(track_id, 640, 480),
"TrackUpdated must carry the *new* config, on the same track id"
);
}
#[test]
fn adts_channel_configuration_is_mapped_through_table_1_19() {
const SFI_48000: u8 = 3;
const ADTS_PROFILE_AAC_LC: u8 = 1;
const CONFIG_7_1: u8 = 7;
const CONFIG_IN_BAND: u8 = 0;
for (config, expected) in [(CONFIG_7_1, 8u16), (CONFIG_IN_BAND, 0u16)] {
let frame_len = (ADTS_HEADER_SIZE + 64) as u16;
let mut au = crate::aac_asc::build_adts_header(
ADTS_PROFILE_AAC_LC,
SFI_48000,
config,
frame_len,
)
.to_vec();
au.resize(frame_len as usize, 0x21);
let mut input = Vec::new();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
input.extend_from_slice(&null_packet());
}
input.extend_from_slice(&psi_section_packets(
PAT_PID_UNDER_TEST,
0,
TABLE_ID_PAT,
&pat_body(),
));
input.extend_from_slice(&psi_section_packets(
PMT_PID_UNDER_TEST,
0,
TABLE_ID_PMT,
&pmt_body(ES_PID_UNDER_TEST, STREAM_TYPE_AAC_ADTS),
));
let mut cc = 0u8;
for i in 0..4 {
let pes = pes_packet(ES_STREAM_ID_AUDIO, i * 1024, None, &au);
let packets = pes_packets(ES_PID_UNDER_TEST, cc, &pes);
cc = cc.wrapping_add((packets.len() / TS_PACKET_SIZE) as u8);
input.extend_from_slice(&packets);
}
let mut demux = StreamingTsDemux::new();
demux.feed(&input);
demux.finish();
let channel_count = std::iter::from_fn(|| demux.poll_event())
.find_map(|e| match e {
DemuxEvent::TrackAdded(spec) => match spec.config {
CodecConfig::Aac { channel_count, .. } => Some(channel_count),
_ => None,
},
_ => None,
})
.expect("the AAC PID must resolve a track");
assert_eq!(
channel_count, expected,
"ADTS channel_configuration {config} must map through Table 1.19"
);
}
}
#[test]
fn signalled_discontinuity_keeps_dts_monotonic() {
const BASE_A_DTS: i64 = 90_000;
const BASE_B_DTS: i64 = 9_000;
const FRAME_PERIOD: i64 = 3600;
const FRAMES: usize = 12;
const WRAP: i64 = 1i64 << 33;
let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("..");
path.push("fixtures");
path.push("ts");
path.push("h264-aac.ts");
let source =
std::fs::read(&path).unwrap_or_else(|e| panic!("read fixture {}: {e}", path.display()));
let media = TsDemux::new().demux(&source).expect("demux h264-aac.ts");
let video = media
.tracks
.iter()
.find(|t| matches!(t.config(), CodecConfig::Avc { .. }))
.expect("h264-aac.ts has an AVC track");
assert!(
video.samples.len() >= FRAMES * 2,
"fixture too small for two time bases"
);
let annexb: Vec<Vec<u8>> = video
.samples
.iter()
.map(|sample| {
let mut au = Vec::new();
for nal in crate::annexb::iter_length_prefixed_nals(&sample.data)
.expect("TsMux writes valid NAL prefixes")
{
au.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]);
au.extend_from_slice(nal);
}
au
})
.collect();
let mut cc = 0u8;
let mut input = Vec::new();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
input.extend_from_slice(&null_packet());
}
input.extend_from_slice(&psi_section_packets(
PAT_PID_UNDER_TEST,
0,
TABLE_ID_PAT,
&pat_body(),
));
input.extend_from_slice(&psi_section_packets(
PMT_PID_UNDER_TEST,
0,
TABLE_ID_PMT,
&pmt_body_with_pcr_pid(ES_PID_UNDER_TEST, STREAM_TYPE_AVC, ES_PID_UNDER_TEST),
));
let push_au = |input: &mut Vec<u8>, cc: &mut u8, au: &[u8], dts: i64| {
let wire = (dts.rem_euclid(WRAP)) as u64;
let pes = pes_packet(ES_STREAM_ID_VIDEO, wire as i64, Some(wire as i64), au);
let packets = pes_packets(ES_PID_UNDER_TEST, *cc, &pes);
*cc = cc.wrapping_add((packets.len() / TS_PACKET_SIZE) as u8);
input.extend_from_slice(&packets);
};
for i in 0..FRAMES {
let au = annexb[i % annexb.len()].clone();
push_au(
&mut input,
&mut cc,
&au,
BASE_A_DTS + i as i64 * FRAME_PERIOD,
);
}
{
let af = [
0x80u8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, ];
let mut pkt = [0xFFu8; TS_PACKET_SIZE];
pkt[0] = 0x47;
pkt[1] = (ES_PID_UNDER_TEST.value() >> 8) as u8 & PID_HI_MASK;
pkt[2] = (ES_PID_UNDER_TEST.value() & 0xFF) as u8;
pkt[3] = 0x20 | (cc & 0x0F); pkt[4] = af.len() as u8;
pkt[5..5 + af.len()].copy_from_slice(&af);
input.extend_from_slice(&pkt);
cc = cc.wrapping_add(1);
}
for i in 0..FRAMES {
let au = annexb[i % annexb.len()].clone();
push_au(
&mut input,
&mut cc,
&au,
BASE_B_DTS + i as i64 * FRAME_PERIOD,
);
}
let mut demux = StreamingTsDemux::new();
demux.feed(&input);
demux.finish();
let mut saw_signalled = false;
let samples: Vec<Sample> = std::iter::from_fn(|| demux.poll_event())
.filter_map(|e| match e {
DemuxEvent::Discontinuity {
kind: DiscontinuityKind::Signalled,
..
} => {
saw_signalled = true;
None
}
DemuxEvent::Sample { sample, .. } => Some(sample),
_ => None,
})
.collect();
assert!(
saw_signalled,
"the adaptation-field discontinuity_indicator must surface as a Signalled Discontinuity"
);
let dts: Vec<i64> = samples
.iter()
.map(|s| s.dts.expect("stamped dts"))
.collect();
assert!(
dts.len() >= FRAMES,
"expected one sample per access unit, got {}",
dts.len()
);
assert!(
dts.windows(2).all(|w| w[1] >= w[0]),
"dts must stay monotonic across a signalled discontinuity; got {dts:?}"
);
let steps: Vec<i64> = dts.windows(2).map(|w| w[1] - w[0]).collect();
assert!(
steps.iter().all(|&d| d == FRAME_PERIOD),
"every decode step must remain one frame period; got {steps:?}"
);
}
#[test]
fn three_signalled_discontinuities_keep_dts_monotonic() {
const FRAME_PERIOD: i64 = 3600;
const FRAMES: i64 = 6;
const BASES: [i64; 3] = [90_000, 30_000, 5_000];
let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("..");
path.push("fixtures");
path.push("ts");
path.push("h264-aac.ts");
let source =
std::fs::read(&path).unwrap_or_else(|e| panic!("read fixture {}: {e}", path.display()));
let media = TsDemux::new().demux(&source).expect("demux h264-aac.ts");
let video = media
.tracks
.iter()
.find(|t| matches!(t.config(), CodecConfig::Avc { .. }))
.expect("h264-aac.ts has an AVC track");
let annexb: Vec<Vec<u8>> = video
.samples
.iter()
.map(|sample| {
let mut au = Vec::new();
for nal in crate::annexb::iter_length_prefixed_nals(&sample.data)
.expect("TsMux writes valid NAL prefixes")
{
au.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]);
au.extend_from_slice(nal);
}
au
})
.collect();
let mut cc = 0u8;
let mut input = Vec::new();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
input.extend_from_slice(&null_packet());
}
input.extend_from_slice(&psi_section_packets(
PAT_PID_UNDER_TEST,
0,
TABLE_ID_PAT,
&pat_body(),
));
input.extend_from_slice(&psi_section_packets(
PMT_PID_UNDER_TEST,
0,
TABLE_ID_PMT,
&pmt_body_with_pcr_pid(ES_PID_UNDER_TEST, STREAM_TYPE_AVC, ES_PID_UNDER_TEST),
));
let push_au = |input: &mut Vec<u8>, cc: &mut u8, au: &[u8], dts: i64| {
let wire = dts.rem_euclid(1i64 << 33) as u64;
let pes = pes_packet(ES_STREAM_ID_VIDEO, wire as i64, Some(wire as i64), au);
let packets = pes_packets(ES_PID_UNDER_TEST, *cc, &pes);
*cc = cc.wrapping_add((packets.len() / TS_PACKET_SIZE) as u8);
input.extend_from_slice(&packets);
};
for base in BASES {
for i in 0..FRAMES {
let au = annexb[(i as usize) % annexb.len()].clone();
push_au(&mut input, &mut cc, &au, base + i * FRAME_PERIOD);
}
let mut pkt = [0xFFu8; TS_PACKET_SIZE];
pkt[0] = 0x47;
pkt[1] = (ES_PID_UNDER_TEST.value() >> 8) as u8 & PID_HI_MASK;
pkt[2] = (ES_PID_UNDER_TEST.value() & 0xFF) as u8;
pkt[3] = 0x20 | (cc & 0x0F); pkt[4] = 8;
pkt[5] = 0x80; input.extend_from_slice(&pkt);
cc = cc.wrapping_add(1);
}
let mut demux = StreamingTsDemux::new();
demux.feed(&input);
demux.finish();
let dts: Vec<i64> = std::iter::from_fn(|| demux.poll_event())
.filter_map(|e| match e {
DemuxEvent::Sample { sample, .. } => sample.dts,
_ => None,
})
.collect();
assert!(
dts.len() >= (BASES.len() as i64 * FRAMES) as usize - 3,
"expected the access units of all three time bases, got {} (first few: {:?})",
dts.len(),
&dts[..dts.len().min(6)]
);
let steps: Vec<i64> = dts.windows(2).map(|w| w[1] - w[0]).collect();
assert!(
steps.iter().all(|&d| d > 0),
"dts must be strictly increasing across every discontinuity; dts={dts:?} steps={steps:?}"
);
}
#[test]
fn cc_gap_on_a_continuation_drops_only_that_access_unit() {
const PACKETS_PER_AU: usize = 4;
const PER_PACKET: usize = TS_PACKET_SIZE - 4;
const PES_HEADER_BYTES: usize = 9;
let body_len = PACKETS_PER_AU * PER_PACKET - PES_HEADER_BYTES;
let aus: Vec<Vec<u8>> = (0..3)
.map(|i| (0..body_len).map(|b| ((b + i * 53) & 0xFF) as u8).collect())
.collect();
let mut input = Vec::new();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
input.extend_from_slice(&null_packet());
}
input.extend_from_slice(&psi_section_packets(
PAT_PID_UNDER_TEST,
0,
TABLE_ID_PAT,
&pat_body(),
));
input.extend_from_slice(&psi_section_packets(
PMT_PID_UNDER_TEST,
0,
TABLE_ID_PMT,
&pmt_body(ES_PID_UNDER_TEST, STREAM_TYPE_PES_PRIVATE),
));
let mut cc = 0u8;
let mut expected = Vec::new();
for (i, au) in aus.iter().enumerate() {
let pes = untimed_pes(ES_STREAM_ID_PRIVATE_DATA, au);
let packets = pes_packets(ES_PID_UNDER_TEST, cc, &pes);
let n = packets.len() / TS_PACKET_SIZE;
assert_eq!(
n, PACKETS_PER_AU,
"AU {i} must span {PACKETS_PER_AU} packets"
);
if i == 1 {
input.extend_from_slice(&packets[..TS_PACKET_SIZE]);
input.extend_from_slice(&packets[2 * TS_PACKET_SIZE..]);
} else {
input.extend_from_slice(&packets);
expected.push(au.clone());
}
cc = cc.wrapping_add(n as u8);
}
let mut demux = StreamingTsDemux::new();
demux.feed(&input);
demux.finish();
let got: Vec<Vec<u8>> = std::iter::from_fn(|| demux.poll_event())
.filter_map(|e| match e {
DemuxEvent::Sample { sample, .. } => Some(sample.data.to_vec()),
_ => None,
})
.collect();
assert_eq!(
got, expected,
"only the unit whose middle packet was lost may be dropped"
);
}
#[test]
fn cc_jump_at_a_pes_start_keeps_the_previous_complete_access_unit() {
let au1 = data_au(0x00);
let au2 = data_au(0x80);
let mut input = Vec::new();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
input.extend_from_slice(&null_packet());
}
input.extend_from_slice(&psi_section_packets(
PAT_PID_UNDER_TEST,
0,
TABLE_ID_PAT,
&pat_body(),
));
input.extend_from_slice(&psi_section_packets(
PMT_PID_UNDER_TEST,
0,
TABLE_ID_PMT,
&pmt_body(ES_PID_UNDER_TEST, STREAM_TYPE_PES_PRIVATE),
));
let pes1 = untimed_pes(ES_STREAM_ID_PRIVATE_DATA, &au1);
let pes2 = untimed_pes(ES_STREAM_ID_PRIVATE_DATA, &au2);
let pkt1 = pes_packets(ES_PID_UNDER_TEST, 0, &pes1);
assert_eq!(pkt1.len(), 2 * TS_PACKET_SIZE, "unit 1 spans two packets");
let pkt2 = pes_packets(ES_PID_UNDER_TEST, 0, &pes2);
input.extend_from_slice(&pkt1);
input.extend_from_slice(&pkt2);
let mut demux = StreamingTsDemux::new();
demux.feed(&input);
demux.finish();
let got: Vec<Vec<u8>> = std::iter::from_fn(|| demux.poll_event())
.filter_map(|e| match e {
DemuxEvent::Sample { sample, .. } => Some(sample.data.to_vec()),
_ => None,
})
.collect();
assert_eq!(
got,
vec![au1.clone(), au2.clone()],
"a counter jump at a `payload_unit_start` must not drop the previous unit when its declared length was fully received"
);
}
#[test]
fn cc_restart_at_a_pes_start_keeps_an_unbounded_unit() {
let au1 = data_au(0x10);
let au2 = data_au(0x90);
let mut input = Vec::new();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
input.extend_from_slice(&null_packet());
}
input.extend_from_slice(&psi_section_packets(
PAT_PID_UNDER_TEST,
0,
TABLE_ID_PAT,
&pat_body(),
));
input.extend_from_slice(&psi_section_packets(
PMT_PID_UNDER_TEST,
0,
TABLE_ID_PMT,
&pmt_body(ES_PID_UNDER_TEST, STREAM_TYPE_PES_PRIVATE),
));
let pes1 = unbounded_pes(ES_STREAM_ID_PRIVATE_DATA, &au1);
let pes2 = unbounded_pes(ES_STREAM_ID_PRIVATE_DATA, &au2);
let pkt1 = pes_packets(ES_PID_UNDER_TEST, 0, &pes1);
assert_eq!(pkt1.len(), 2 * TS_PACKET_SIZE, "unit 1 spans two packets");
let pkt2 = pes_packets(ES_PID_UNDER_TEST, 0, &pes2);
input.extend_from_slice(&pkt1);
input.extend_from_slice(&pkt2);
let mut demux = StreamingTsDemux::new();
demux.feed(&input);
demux.finish();
let got: Vec<Vec<u8>> = std::iter::from_fn(|| demux.poll_event())
.filter_map(|e| match e {
DemuxEvent::Sample { sample, .. } => Some(sample.data.to_vec()),
_ => None,
})
.collect();
assert_eq!(
got.len(),
2,
"both units are whole on the wire, so a counter restart between them must drop neither: {got:?}"
);
assert!(got[0].starts_with(&au1), "first unit preserved");
assert!(got[1].starts_with(&au2), "second unit preserved");
}
#[test]
fn concatenated_segments_seam_keeps_every_access_unit() {
use crate::TsMux;
use crate::media::{Media, Track};
use crate::pipeline::{CodecConfig, Sample, TrackSpec};
use crate::rtp_sdp::avc_config_from_sprop;
use broadcast_common::Package;
let frame_dur = VIDEO_TIMESCALE / 25;
let avc = avc_config_from_sprop("Z0IAKeKQFAe2AtwEBAaQeJEV,aM48gA==").unwrap();
let spec = TrackSpec::new(
TrackId::new(1),
VIDEO_TIMESCALE,
CodecConfig::Avc {
config: avc,
width: 0,
height: 0,
},
);
let samples: Vec<Sample> = (0..6u32)
.map(|i| {
let nal = [0x65u8, 0xAA, i as u8];
let mut data = (nal.len() as u32).to_be_bytes().to_vec();
data.extend_from_slice(&nal);
let dts = i64::from(i) * i64::from(frame_dur);
Sample::new(data, Some(dts), Some(dts), Some(frame_dur), true)
})
.collect();
let media = Media::new(vec![Track::new(spec, samples)], VIDEO_TIMESCALE);
let segment = TsMux::default().package(&media).expect("mux to TS");
let mut concatenated = segment.clone();
concatenated.extend_from_slice(&segment);
let mut demux = StreamingTsDemux::new();
demux.feed(&concatenated);
demux.finish();
let video_samples = std::iter::from_fn(|| demux.poll_event())
.filter(|e| matches!(e, DemuxEvent::Sample { .. }))
.count();
assert_eq!(
video_samples, 12,
"a segment seam restarts the counter but loses nothing, so all 12 access units must survive"
);
}
#[test]
fn cc_gap_inside_an_unbounded_unit_drops_it() {
const PER_PACKET: usize = TS_PACKET_SIZE - 4;
const PACKETS: usize = 4;
const PES_HEADER_BYTES: usize = 9;
let au: Vec<u8> = (0..PACKETS * PER_PACKET - PES_HEADER_BYTES)
.map(|i| (i & 0xFF) as u8)
.collect();
let pes = unbounded_pes(ES_STREAM_ID_PRIVATE_DATA, &au);
let packets = pes_packets(ES_PID_UNDER_TEST, 0, &pes);
assert_eq!(packets.len(), PACKETS * TS_PACKET_SIZE);
let mut input = Vec::new();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
input.extend_from_slice(&null_packet());
}
input.extend_from_slice(&psi_section_packets(
PAT_PID_UNDER_TEST,
0,
TABLE_ID_PAT,
&pat_body(),
));
input.extend_from_slice(&psi_section_packets(
PMT_PID_UNDER_TEST,
0,
TABLE_ID_PMT,
&pmt_body(ES_PID_UNDER_TEST, STREAM_TYPE_PES_PRIVATE),
));
input.extend_from_slice(&packets[..TS_PACKET_SIZE]);
input.extend_from_slice(&packets[2 * TS_PACKET_SIZE..]);
let mut demux = StreamingTsDemux::new();
demux.feed(&input);
demux.finish();
let samples = std::iter::from_fn(|| demux.poll_event())
.filter(|e| matches!(e, DemuxEvent::Sample { .. }))
.count();
assert_eq!(
samples, 0,
"a unit missing one of its own packets must not be delivered"
);
}
#[test]
fn signalled_discontinuity_does_not_drop_the_unit_it_ends() {
const SIGNALLED_CC: u8 = 3;
let au1 = data_au(0x20);
let au2 = data_au(0xA0);
let au3 = data_au(0xF0);
let mut input = Vec::new();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
input.extend_from_slice(&null_packet());
}
input.extend_from_slice(&psi_section_packets(
PAT_PID_UNDER_TEST,
0,
TABLE_ID_PAT,
&pat_body(),
));
input.extend_from_slice(&psi_section_packets(
PMT_PID_UNDER_TEST,
0,
TABLE_ID_PMT,
&pmt_body(ES_PID_UNDER_TEST, STREAM_TYPE_PES_PRIVATE),
));
let mut cc = 0u8;
for au in [&au1, &au2] {
let pes = unbounded_pes(ES_STREAM_ID_PRIVATE_DATA, au);
let packets = pes_packets(ES_PID_UNDER_TEST, cc, &pes);
assert_eq!(
packets.len(),
2 * TS_PACKET_SIZE,
"each unit spans two packets"
);
input.extend_from_slice(&packets);
cc = cc.wrapping_add(2);
}
let pes3 = unbounded_pes(ES_STREAM_ID_PRIVATE_DATA, &au3);
input.extend_from_slice(&pes_packets_with_discontinuity(
ES_PID_UNDER_TEST,
SIGNALLED_CC,
&pes3,
));
let mut demux = StreamingTsDemux::new();
demux.feed(&input);
demux.finish();
let got: Vec<Vec<u8>> = std::iter::from_fn(|| demux.poll_event())
.filter_map(|e| match e {
DemuxEvent::Sample { sample, .. } => Some(sample.data.to_vec()),
_ => None,
})
.collect();
assert_eq!(
got.len(),
3,
"every unit here is whole on the wire, so the indicator on the packet starting unit 3 must not drop anything: {got:?}"
);
for (i, (got_au, want)) in got.iter().zip([&au1, &au2, &au3]).enumerate() {
assert!(got_au.starts_with(want), "unit {i} must be delivered whole");
}
}
#[test]
fn unbounded_unit_ended_by_a_counter_restart_is_delivered_by_design() {
let au1 = data_au(0x20);
let au2 = data_au(0xA0);
let mut input = Vec::new();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
input.extend_from_slice(&null_packet());
}
input.extend_from_slice(&psi_section_packets(
PAT_PID_UNDER_TEST,
0,
TABLE_ID_PAT,
&pat_body(),
));
input.extend_from_slice(&psi_section_packets(
PMT_PID_UNDER_TEST,
0,
TABLE_ID_PMT,
&pmt_body(ES_PID_UNDER_TEST, STREAM_TYPE_PES_PRIVATE),
));
let pes1 = unbounded_pes(ES_STREAM_ID_PRIVATE_DATA, &au1);
let pkt1 = pes_packets(ES_PID_UNDER_TEST, 0, &pes1);
input.extend_from_slice(&pkt1[..TS_PACKET_SIZE]);
let pes2 = unbounded_pes(ES_STREAM_ID_PRIVATE_DATA, &au2);
input.extend_from_slice(&pes_packets(ES_PID_UNDER_TEST, 2, &pes2));
let mut demux = StreamingTsDemux::new();
demux.feed(&input);
demux.finish();
let got: Vec<Vec<u8>> = std::iter::from_fn(|| demux.poll_event())
.filter_map(|e| match e {
DemuxEvent::Sample { sample, .. } => Some(sample.data.to_vec()),
_ => None,
})
.collect();
assert_eq!(
got.len(),
2,
"both the truncated-tail unit and the whole one are delivered: an unbounded PES gives no way to tell them apart at a seam"
);
}
#[test]
fn unstamped_pes_access_unit_is_interpolated_not_restamped() {
let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("..");
path.push("fixtures");
path.push("ts");
path.push("h264-aac.ts");
let source =
std::fs::read(&path).unwrap_or_else(|e| panic!("read fixture {}: {e}", path.display()));
let media = TsDemux::new().demux(&source).expect("demux h264-aac.ts");
const FRAME_PERIOD: i64 = 3600;
const UNSTAMPED_EVERY: usize = 2;
let mut cc = 0u8;
let mut input = Vec::new();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
input.extend_from_slice(&null_packet());
}
input.extend_from_slice(&psi_section_packets(
PAT_PID_UNDER_TEST,
0,
TABLE_ID_PAT,
&pat_body(),
));
input.extend_from_slice(&psi_section_packets(
PMT_PID_UNDER_TEST,
0,
TABLE_ID_PMT,
&pmt_body(ES_PID_UNDER_TEST, STREAM_TYPE_AVC),
));
let video = media
.tracks
.iter()
.find(|t| matches!(t.config(), CodecConfig::Avc { .. }))
.expect("h264-aac.ts has an AVC track");
let source_dts0 = video.samples[0]
.dts
.expect("the fixture's AVC track is stamped");
let mut headers = 0usize;
for (i, sample) in video.samples.iter().enumerate() {
let mut au = Vec::new();
let nals = crate::annexb::iter_length_prefixed_nals(&sample.data)
.expect("TsMux writes valid 4-byte-length NAL prefixes");
for nal in nals {
au.extend_from_slice(&[0x00, 0x00, 0x00, 0x01]);
au.extend_from_slice(nal);
}
let stamped = i < UNSTAMPED_EVERY || i % UNSTAMPED_EVERY == 0;
let dts = source_dts0 + i as i64 * FRAME_PERIOD;
let pes = if stamped {
pes_packet(ES_STREAM_ID_VIDEO, dts, Some(dts), &au)
} else {
pes_packet_untimed(ES_STREAM_ID_VIDEO, &au)
};
headers += 1;
let packets = pes_packets(ES_PID_UNDER_TEST, cc, &pes);
cc = cc.wrapping_add((packets.len() / TS_PACKET_SIZE) as u8);
input.extend_from_slice(&packets);
}
assert!(
video.samples.len() >= 20,
"fixture too small for a meaningful interpolation test"
);
let mut demux = StreamingTsDemux::new();
demux.feed(&input);
demux.finish();
let samples: Vec<Sample> = std::iter::from_fn(|| demux.poll_event())
.filter_map(|e| match e {
DemuxEvent::Sample { sample, .. } => Some(sample),
_ => None,
})
.collect();
assert_eq!(
samples.len(),
headers,
"one sample per fed access unit (video is one AU per PES here)"
);
let dts: Vec<i64> = samples.iter().map(|s| s.dts.expect("video dts")).collect();
let steps: Vec<i64> = dts.windows(2).map(|w| w[1] - w[0]).collect();
assert!(
steps.iter().all(|&d| d == FRAME_PERIOD),
"every decode step must be one frame period ({FRAME_PERIOD} ticks); got {steps:?}"
);
assert!(
samples
.iter()
.all(|s| s.duration == Some(FRAME_PERIOD as u32)),
"no zero-duration sample may survive interpolation; durations: {:?}",
samples.iter().map(|s| s.duration).collect::<Vec<_>>()
);
}
fn aac_44100_access_unit() -> Vec<u8> {
const ADTS_PROFILE_AAC_LC: u8 = 1;
const SFI_44100: u8 = 4;
const CHANNELS_STEREO: u8 = 2;
const PAYLOAD_BYTES: usize = 128;
let frame_len = (ADTS_HEADER_SIZE + PAYLOAD_BYTES) as u16;
let header = crate::aac_asc::build_adts_header(
ADTS_PROFILE_AAC_LC,
SFI_44100,
CHANNELS_STEREO,
frame_len,
);
let mut au = header.to_vec();
au.resize(ADTS_HEADER_SIZE + PAYLOAD_BYTES, 0x21);
au
}
#[test]
fn constant_increment_44100_aac_emits_no_timeline_reanchor() {
const PES_INCREMENT_TICKS: i128 = 2090;
const SAMPLE_RATE: u32 = 44_100;
const FRAMES: i128 = 500;
let au = aac_44100_access_unit();
let mut anchor = AudioAnchor::default();
let mut events: VecDeque<DemuxEvent> = VecDeque::new();
for n in 0..FRAMES {
let ts = n * PES_INCREMENT_TICKS;
emit_audio_au(
&AudioKind::Aac,
SAMPLE_RATE,
&mut anchor,
&au,
ts,
ts,
TrackId::new(1),
&mut events,
);
}
assert!(
events
.iter()
.any(|e| matches!(e, DemuxEvent::Sample { .. })),
"sanity: the synthesised ADTS frames must actually split into samples"
);
let reanchors = events
.iter()
.filter(|e| {
matches!(
e,
DemuxEvent::Discontinuity {
kind: DiscontinuityKind::TimelineReanchored,
..
}
)
})
.count();
assert_eq!(
reanchors, 0,
"a constant-increment 44.1 kHz stream is continuous — the \
rounding drift a real muxer accrues by construction must never be \
reported as a discontinuity"
);
}
#[test]
fn a_real_timeline_gap_emits_exactly_one_reanchor() {
const PES_INCREMENT_TICKS: i128 = 2090;
const SAMPLE_RATE: u32 = 44_100;
const FRAMES_BEFORE: i128 = 50;
const FRAMES_AFTER: i128 = 50;
const GAP_TICKS: i128 = 180_000;
let au = aac_44100_access_unit();
let mut anchor = AudioAnchor::default();
let mut events: VecDeque<DemuxEvent> = VecDeque::new();
let emit = |ts: i128, anchor: &mut AudioAnchor, events: &mut VecDeque<DemuxEvent>| {
emit_audio_au(
&AudioKind::Aac,
SAMPLE_RATE,
anchor,
&au,
ts,
ts,
TrackId::new(1),
events,
);
};
for n in 0..FRAMES_BEFORE {
emit(n * PES_INCREMENT_TICKS, &mut anchor, &mut events);
}
let resume = FRAMES_BEFORE * PES_INCREMENT_TICKS + GAP_TICKS;
for n in 0..FRAMES_AFTER {
emit(resume + n * PES_INCREMENT_TICKS, &mut anchor, &mut events);
}
let reanchors = events
.iter()
.filter(|e| {
matches!(
e,
DemuxEvent::Discontinuity {
kind: DiscontinuityKind::TimelineReanchored,
..
}
)
})
.count();
assert_eq!(
reanchors, 1,
"one genuine gap must produce exactly one TimelineReanchored — not \
zero (the anchor silently absorbing a real splice) and not one \
per following access unit"
);
}
#[test]
fn adts_resyncs_across_pes_boundaries() {
let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("..");
path.push("fixtures");
path.push("ts");
path.push("h264-aac.ts");
let ts_bytes =
std::fs::read(&path).unwrap_or_else(|e| panic!("read fixture {}: {e}", path.display()));
let mut demux = TsDemux::new();
let media = demux.demux(&ts_bytes).expect("demux h264-aac.ts");
let audio = media
.tracks
.iter()
.find(|t| matches!(t.config(), CodecConfig::Aac { .. }))
.expect("h264-aac.ts has an AAC track");
let esds = match audio.config() {
CodecConfig::Aac { esds, .. } => esds,
_ => unreachable!(),
};
let dsi = esds
.es_descriptor
.decoder_config
.as_ref()
.and_then(|dc| dc.decoder_specific_info.as_ref())
.expect("AAC esds carries a DecoderSpecificInfo");
let asc = AudioSpecificConfig::parse(&dsi.data).expect("parse real AudioSpecificConfig");
let mut es: Vec<u8> = Vec::new();
for s in &audio.samples {
let frame_len = (ADTS_HEADER_SIZE + s.data.len()) as u16;
let hdr = asc
.to_adts_header(frame_len)
.expect("build real ADTS header");
es.extend_from_slice(&hdr);
es.extend_from_slice(&s.data);
}
assert!(
audio.samples.len() >= 10,
"fixture too small to be a meaningful resync test"
);
const CHUNK: usize = 2000;
let mut recovered = 0usize;
let mut off = 0usize;
while off < es.len() {
let end = (off + CHUNK).min(es.len());
recovered += split_adts_frames(&es[off..end]).len();
off = end;
}
assert!(
recovered * 2 >= audio.samples.len(),
"resync must recover most real AAC frames across misaligned \
chunks (got {recovered} of {} real frames)",
audio.samples.len()
);
}
fn real_aac_asc_and_samples() -> (AudioSpecificConfig, Vec<Vec<u8>>) {
let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("..");
path.push("fixtures");
path.push("ts");
path.push("h264-aac.ts");
let ts_bytes =
std::fs::read(&path).unwrap_or_else(|e| panic!("read fixture {}: {e}", path.display()));
let mut demux = TsDemux::new();
let media = demux.demux(&ts_bytes).expect("demux h264-aac.ts");
let audio = media
.tracks
.into_iter()
.find(|t| matches!(t.config(), CodecConfig::Aac { .. }))
.expect("h264-aac.ts has an AAC track");
let esds = match audio.config() {
CodecConfig::Aac { esds, .. } => esds.clone(),
_ => unreachable!(),
};
let dsi = esds
.es_descriptor
.decoder_config
.as_ref()
.and_then(|dc| dc.decoder_specific_info.as_ref())
.expect("AAC esds carries a DecoderSpecificInfo")
.clone();
let asc = AudioSpecificConfig::parse(&dsi.data).expect("parse real AudioSpecificConfig");
let samples: Vec<Vec<u8>> = audio.samples.iter().map(|s| s.data.to_vec()).collect();
(asc, samples)
}
#[test]
fn adts_crc_protected_frame_strips_crc_not_sample_data() {
let (asc, samples) = real_aac_asc_and_samples();
assert!(samples.len() >= 2, "fixture must carry several AAC frames");
let mut events: VecDeque<DemuxEvent> = VecDeque::new();
let mut anchor = AudioAnchor::default();
let mut dts_uw: i128 = 0;
for payload in samples.iter().take(5) {
let frame_len = (ADTS_HEADER_SIZE + ADTS_CRC_SIZE + payload.len()) as u16;
let mut header = asc.to_adts_header(frame_len).expect("build ADTS header");
header[1] &= !0x01; let mut au = header.to_vec();
au.extend_from_slice(&[0x00, 0x00]); au.extend_from_slice(payload);
emit_audio_au(
&AudioKind::Aac,
44_100,
&mut anchor,
&au,
dts_uw as u64 as i128,
dts_uw,
TrackId::new(1),
&mut events,
);
dts_uw += AAC_SAMPLES_PER_FRAME as i128;
}
let emitted: Vec<Vec<u8>> = events
.iter()
.filter_map(|e| match e {
DemuxEvent::Sample { sample, .. } => Some(sample.data.to_vec()),
_ => None,
})
.collect();
assert_eq!(emitted.len(), 5, "one sample per CRC-protected ADTS frame");
for (i, (got, want)) in emitted.iter().zip(samples.iter().take(5)).enumerate() {
assert_eq!(
got, want,
"sample {i}: CRC bytes must not leak into (or truncate) the real payload"
);
}
}
#[test]
fn adts_multi_raw_data_block_duration_is_multiplied() {
let (asc, samples) = real_aac_asc_and_samples();
assert!(samples.len() >= 2, "fixture must carry several AAC frames");
let mut combined = samples[0].clone();
combined.extend_from_slice(&samples[1]);
let frame_len = (ADTS_HEADER_SIZE + combined.len()) as u16;
let mut header = asc.to_adts_header(frame_len).expect("build ADTS header");
header[6] |= 0x01; let mut au = header.to_vec();
au.extend_from_slice(&combined);
let mut events: VecDeque<DemuxEvent> = VecDeque::new();
let mut anchor = AudioAnchor::default();
emit_audio_au(
&AudioKind::Aac,
44_100,
&mut anchor,
&au,
0,
0,
TrackId::new(1),
&mut events,
);
let emitted: Vec<(Vec<u8>, Option<u32>)> = events
.iter()
.filter_map(|e| match e {
DemuxEvent::Sample { sample, .. } => Some((sample.data.to_vec(), sample.duration)),
_ => None,
})
.collect();
assert_eq!(
emitted.len(),
1,
"the whole 2-raw-data-block frame is one emitted sample"
);
assert_eq!(
emitted[0].0, combined,
"both raw data blocks' real bytes must be present, undamaged"
);
assert_eq!(
emitted[0].1,
Some(AAC_SAMPLES_PER_FRAME * 2),
"duration must be (num_raw_data_blocks + 1) * AAC_SAMPLES_PER_FRAME, not just 1024"
);
}
fn build_adts_frame(base: &[u8; 7], blocks: &[&[u8]], crc: bool) -> Vec<u8> {
let n = blocks.len() - 1;
let mut body = Vec::new();
let mut positions = Vec::new();
for (i, b) in blocks.iter().enumerate() {
if i > 0 {
positions.push(body.len() as u16); }
body.extend_from_slice(b);
if crc && n > 0 {
body.extend_from_slice(&[0xC0 | i as u8, 0xDE]); }
}
let mut tail = Vec::new();
if crc {
if n > 0 {
for p in &positions {
tail.extend_from_slice(&p.to_be_bytes());
}
}
tail.extend_from_slice(&[0xBE, 0xEF]); }
tail.extend_from_slice(&body);
let frame_length = 7 + tail.len();
assert!(frame_length < (1 << 13));
let mut h = *base;
if crc {
h[1] &= !0x01; } else {
h[1] |= 0x01;
}
h[3] = (h[3] & !0x03) | ((frame_length >> 11) & 0x03) as u8;
h[4] = ((frame_length >> 3) & 0xFF) as u8;
h[5] = (h[5] & 0x1F) | (((frame_length & 0x07) as u8) << 5);
h[6] = (h[6] & !0x03) | n as u8;
let mut out = h.to_vec();
out.extend_from_slice(&tail);
out
}
fn demux_one_adts_frame(au: &[u8]) -> Vec<(Vec<u8>, Option<u32>)> {
let mut events: VecDeque<DemuxEvent> = VecDeque::new();
let mut anchor = AudioAnchor::default();
emit_audio_au(
&AudioKind::Aac,
44_100,
&mut anchor,
au,
0,
0,
TrackId::new(1),
&mut events,
);
events
.iter()
.filter_map(|e| match e {
DemuxEvent::Sample { sample, .. } => Some((sample.data.to_vec(), sample.duration)),
_ => None,
})
.collect()
}
#[test]
fn adts_multi_block_with_crc_strips_positions_and_crcs() {
let (asc, _) = real_aac_asc_and_samples();
let base = asc.to_adts_header(7).expect("base header");
let all: [&[u8]; 4] = [
&[0x11; 5],
&[0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28],
&[0x33; 3],
&[0x44; 9],
];
for n_blocks in 1..=4usize {
for crc in [false, true] {
let blocks = &all[..n_blocks];
let au = build_adts_frame(&base, blocks, crc);
let got = demux_one_adts_frame(&au);
assert_eq!(got.len(), 1, "n_blocks={n_blocks} crc={crc}");
let want: Vec<u8> = blocks.concat();
assert_eq!(got[0].0, want, "n_blocks={n_blocks} crc={crc}");
assert_eq!(
got[0].1,
Some(AAC_SAMPLES_PER_FRAME * n_blocks as u32),
"n_blocks={n_blocks} crc={crc}"
);
}
}
}
#[test]
fn adts_multi_block_crc_reframed_real_aac_is_byte_identical() {
let (asc, samples) = real_aac_asc_and_samples();
let base = asc.to_adts_header(7).expect("base header");
for n_blocks in 2..=4usize {
let mut checked = 0;
for chunk in samples.chunks_exact(n_blocks).take(20) {
let blocks: Vec<&[u8]> = chunk.iter().map(|v| v.as_slice()).collect();
let au = build_adts_frame(&base, &blocks, true);
let got = demux_one_adts_frame(&au);
assert_eq!(got.len(), 1);
assert_eq!(got[0].0, chunk.concat(), "n_blocks={n_blocks}");
checked += 1;
}
assert!(checked >= 5, "fixture too small ({checked} groups)");
}
}
#[test]
fn adts_multi_block_crc_bad_position_table_degrades_not_rejects() {
let (asc, _) = real_aac_asc_and_samples();
let base = asc.to_adts_header(7).expect("base header");
let mut au = build_adts_frame(&base, &[&[0x11; 6], &[0x22; 6]], true);
au[7] = 0x7F;
au[8] = 0xFF;
let got = demux_one_adts_frame(&au);
assert_eq!(got.len(), 1);
assert_eq!(got[0].0.len(), au.len() - (7 + 2 + 2));
}
#[test]
fn refine_with_descriptors_recognises_every_dolby_dts_tag() {
let ac3_desc = [DESC_TAG_AC3, 1, 0x40];
assert_eq!(
Codec::Data(STREAM_TYPE_PES_PRIVATE)
.refine_with_descriptors(STREAM_TYPE_PES_PRIVATE, &ac3_desc),
Codec::Ac3
);
let dts_desc = [DESC_TAG_DTS, 1, 0x00];
assert_eq!(
Codec::Data(STREAM_TYPE_METADATA_PES)
.refine_with_descriptors(STREAM_TYPE_METADATA_PES, &dts_desc),
Codec::Dts,
"0x15 (metadata in PES) is also descriptor-disambiguated"
);
let eac3_desc = [DESC_TAG_ENHANCED_AC3, 1, 0x00];
assert_eq!(
Codec::Data(STREAM_TYPE_PES_PRIVATE)
.refine_with_descriptors(STREAM_TYPE_PES_PRIVATE, &eac3_desc),
Codec::Eac3
);
}
#[test]
fn refine_with_descriptors_leaves_non_audio_0x06_as_data() {
const DESC_TAG_SUBTITLING: u8 = 0x59;
let subtitle_desc = [DESC_TAG_SUBTITLING, 3, 0x65, 0x6E, 0x67];
assert_eq!(
Codec::Data(STREAM_TYPE_PES_PRIVATE)
.refine_with_descriptors(STREAM_TYPE_PES_PRIVATE, &subtitle_desc),
Codec::Data(STREAM_TYPE_PES_PRIVATE)
);
}
fn dovi_es_info(profile_word: u16, flags: u16, dependency: Option<u16>) -> Vec<u8> {
let mut es = vec![0x05, 4, b'D', b'O', b'V', b'I'];
let mut body = vec![1u8, 0];
body.extend_from_slice(&((profile_word << 9) | flags).to_be_bytes());
if let Some(pid) = dependency {
body.extend_from_slice(&(pid << 3).to_be_bytes());
}
body.push(0x10);
es.push(crate::dovi::DOVI_VIDEO_STREAM_DESCRIPTOR_TAG);
es.push(body.len() as u8);
es.extend_from_slice(&body);
es
}
#[test]
fn dovi_hevc_reclassification_needs_base_layer_and_hevc_profile() {
let classify = |es: &[u8]| {
Codec::Data(STREAM_TYPE_PES_PRIVATE)
.refine_with_descriptors(STREAM_TYPE_PES_PRIVATE, es)
};
let bl_rpu = 0b101;
assert_eq!(classify(&dovi_es_info(8, bl_rpu, None)), Codec::Hevc);
assert_eq!(
classify(&dovi_es_info(10, bl_rpu, None)),
Codec::Data(STREAM_TYPE_PES_PRIVATE),
"profile 10 is not HEVC"
);
let el_rpu_only = 0b110;
assert_eq!(
classify(&dovi_es_info(7, el_rpu_only, Some(0x101))),
Codec::Data(STREAM_TYPE_PES_PRIVATE),
"an EL-only PID is not a standalone HEVC track"
);
}
#[test]
fn refine_with_descriptors_ignores_other_stream_types() {
let eac3_desc = [DESC_TAG_ENHANCED_AC3, 1, 0x00];
assert_eq!(
Codec::H264.refine_with_descriptors(STREAM_TYPE_AVC, &eac3_desc),
Codec::H264
);
}
#[test]
fn codec_change_on_shared_pid_does_not_tear_down_other_program_track() {
const PMT_A: u16 = 0x1000;
const PMT_B: u16 = 0x1001;
const SHARED_PID: Pid = Pid::new(0x0050);
const STREAM_TYPE: u8 = 0x7F;
let mut demux = StreamingTsDemux::new();
demux.apply_pmt_diff(
Pid::new(PMT_A),
&BTreeSet::new(),
alloc::vec![(SHARED_PID, Codec::Data(STREAM_TYPE), Vec::new())],
);
demux.apply_pmt_diff(
Pid::new(PMT_B),
&BTreeSet::new(),
alloc::vec![(SHARED_PID, Codec::Data(STREAM_TYPE), Vec::new())],
);
assert_eq!(
demux.es_declarers.get(&SHARED_PID).map(|d| d.len()),
Some(2),
"both PMTs must be recorded as declarers of the shared PID"
);
let carriage = data_carriage(STREAM_TYPE);
assert_eq!(carriage, DataCarriage::Pes);
demux.streams.get_mut(&SHARED_PID).unwrap().track = Some(TrackState::Parked {
config: CodecConfig::Data {
stream_type: STREAM_TYPE,
descriptors: Vec::new(),
carriage,
},
timescale: VIDEO_TIMESCALE,
kind: LiveKind::Data {
pending: None,
last_duration: 0,
},
backlog: Vec::new(),
});
demux.try_promote_ready();
let track_id_before = match demux.streams.get(&SHARED_PID).unwrap().track.as_ref() {
Some(TrackState::Live(live)) => live.track_id,
_ => panic!("expected the shared PID to be Live after promotion"),
};
while demux.poll_event().is_some() {}
let mut pmt_a_applied = BTreeSet::new();
pmt_a_applied.insert(SHARED_PID);
demux.apply_pmt_diff(
Pid::new(PMT_A),
&pmt_a_applied,
alloc::vec![(SHARED_PID, Codec::Ac3, Vec::new())],
);
match demux
.streams
.get(&SHARED_PID)
.and_then(|s| s.track.as_ref())
{
Some(TrackState::Live(live)) => assert_eq!(
live.track_id, track_id_before,
"shared track must keep its original track_id"
),
_ => {
panic!("expected the shared track to survive PMT A's reclassification, still Live")
}
}
assert!(
!demux
.events
.iter()
.any(|ev| matches!(ev, DemuxEvent::TrackRemoved { .. })),
"PMT A's reclassification must not remove a track PMT B still declares"
);
assert_eq!(
demux.streams.get(&SHARED_PID).unwrap().codec,
Codec::Data(STREAM_TYPE),
"the existing classification wins while another declarer disagrees"
);
let mut pmt_b_applied = BTreeSet::new();
pmt_b_applied.insert(SHARED_PID);
demux.apply_pmt_diff(Pid::new(PMT_B), &pmt_b_applied, Vec::new());
assert_eq!(
demux.es_declarers.get(&SHARED_PID).map(|d| d.len()),
Some(1),
"PMT A must be the sole remaining declarer"
);
demux.apply_pmt_diff(
Pid::new(PMT_A),
&pmt_a_applied,
alloc::vec![(SHARED_PID, Codec::Aac, Vec::new())],
);
assert!(
demux
.events
.iter()
.any(|ev| matches!(ev, DemuxEvent::TrackRemoved { .. })),
"once PMT A is the last declarer, its reclassification must actually tear down \
the old track"
);
assert_eq!(demux.streams.get(&SHARED_PID).unwrap().codec, Codec::Aac);
}
#[test]
fn codec_change_reregistration_preserves_declaration_order_slot() {
const PMT: u16 = 0x1000;
const PID_X: Pid = Pid::new(0x0050);
const PID_Y: Pid = Pid::new(0x0051);
let mut demux = StreamingTsDemux::new();
demux.apply_pmt_diff(
Pid::new(PMT),
&BTreeSet::new(),
alloc::vec![
(PID_X, Codec::Data(0x06), Vec::new()),
(PID_Y, Codec::Data(0x07), Vec::new()),
],
);
assert_eq!(demux.data_order, alloc::vec![PID_X, PID_Y]);
let mut old_applied = BTreeSet::new();
old_applied.insert(PID_X);
old_applied.insert(PID_Y);
demux.apply_pmt_diff(
Pid::new(PMT),
&old_applied,
alloc::vec![
(PID_X, Codec::Data(0x08), Vec::new()),
(PID_Y, Codec::Data(0x07), Vec::new()),
],
);
assert_eq!(
demux.data_order,
alloc::vec![PID_X, PID_Y],
"PID X must keep its original (first) declaration-order slot, not move to the back"
);
assert_eq!(demux.streams.get(&PID_X).unwrap().codec, Codec::Data(0x08));
}
pub(crate) fn null_packet() -> [u8; TS_PACKET_SIZE] {
let mut p = [0xFFu8; TS_PACKET_SIZE];
p[0] = 0x47;
p[1] = 0x1F;
p[2] = 0xFF;
p[3] = 0x10;
p
}
fn ts_packet_degradation(
tei: bool,
pid: Pid,
cc: u8,
afc: u8,
adaptation: Option<&[u8]>,
payload: &[u8],
) -> [u8; TS_PACKET_SIZE] {
let mut p = [0xFFu8; TS_PACKET_SIZE];
p[0] = 0x47;
p[1] = (if tei { 0x80 } else { 0x00 }) | ((pid.value() >> 8) as u8 & PID_HI_MASK);
p[2] = (pid.value() & 0xFF) as u8;
p[3] = afc | (cc & 0x0F);
let mut off = 4usize;
if let Some(af) = adaptation {
let af_len = af.len() as u8;
p[off] = af_len;
off += 1;
p[off..off + af.len()].copy_from_slice(af);
off += af.len();
}
let payload_end = off + payload.len().min(TS_PACKET_SIZE - off);
p[off..payload_end].copy_from_slice(&payload[..payload_end - off]);
p
}
#[test]
fn tei_set_packet_emits_transport_error() {
let mut demux = StreamingTsDemux::new();
let null = null_packet();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
demux.feed(&null);
}
let pkt = ts_packet_degradation(
true, Pid::new(0x0100),
0,
0x10, None,
b"some payload",
);
demux.feed(&pkt);
let events: Vec<_> = std::iter::from_fn(|| demux.poll_event()).collect();
let degraded: Vec<_> = events
.iter()
.filter_map(|e| match e {
DemuxEvent::InputDegraded {
kind, provenance, ..
} => Some((*kind, *provenance)),
_ => None,
})
.collect();
assert_eq!(degraded.len(), 1, "expected exactly one InputDegraded");
assert_eq!(degraded[0].0, InputDegradation::TransportError);
assert_eq!(degraded[0].1.pid, Some(Pid::new(0x0100)));
assert_eq!(
degraded[0].1.packet_index,
Some(mpeg_ts::resync::LOCK_CONFIRMATIONS as u64 + 1)
);
}
#[test]
fn h264_aac_clean_fixture_produces_zero_input_degraded() {
let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("..");
path.push("fixtures");
path.push("ts");
path.push("h264-aac.ts");
let data = std::fs::read(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
let mut demux = StreamingTsDemux::new();
demux.feed(&data);
demux.finish();
let degraded: Vec<_> = std::iter::from_fn(|| demux.poll_event())
.filter(|e| matches!(e, DemuxEvent::InputDegraded { .. }))
.collect();
assert!(
degraded.is_empty(),
"h264-aac.ts must produce zero InputDegraded events, got {degraded:?}"
);
}
#[test]
fn m6_discontinuity_fixture_gap_count_matches_media_doctor() {
let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("..");
path.push("fixtures");
path.push("ts");
path.push("m6-discontinuity.ts");
let data = std::fs::read(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
let mut demux = StreamingTsDemux::new();
demux.feed(&data);
demux.finish();
let gap_count = std::iter::from_fn(|| demux.poll_event())
.filter(|e| {
matches!(
e,
DemuxEvent::InputDegraded {
kind: InputDegradation::ContinuityGap { .. },
..
}
)
})
.count();
assert_eq!(
gap_count, 877,
"m6-discontinuity.ts must produce exactly 877 ContinuityGap events \
(matching media-doctor CcAnomalyCheck); any divergence means the \
exclusion rules disagree with the two in-repo reference \
implementations"
);
}
#[test]
fn m6_discontinuity_fixture_plays_through() {
let mut path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("..");
path.push("fixtures");
path.push("ts");
path.push("m6-discontinuity.ts");
let data = std::fs::read(&path).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
let mut demux = StreamingTsDemux::new();
demux.feed(&data);
demux.finish();
let mut saw_discontinuity = false;
while let Some(event) = demux.poll_event() {
if matches!(event, DemuxEvent::Discontinuity { .. }) {
saw_discontinuity = true;
}
}
assert!(
saw_discontinuity,
"m6-discontinuity.ts must produce at least one Discontinuity event"
);
}
#[test]
fn legal_duplicate_same_cc_and_identical_payload_emits_nothing() {
let mut demux = StreamingTsDemux::new();
let null = null_packet();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
demux.feed(&null);
}
let payload = b"duplicate payload bytes";
let pkt = |cc: u8| -> [u8; TS_PACKET_SIZE] {
let mut p = [0xFFu8; TS_PACKET_SIZE];
p[0] = 0x47;
p[1] = 0x00; p[2] = 0x31; p[3] = 0x10 | (cc & 0x0F); let end = 4 + payload.len().min(TS_PACKET_SIZE - 4);
p[4..end].copy_from_slice(&payload[..end - 4]);
p
};
demux.feed(&pkt(0));
demux.feed(&pkt(0));
let degraded: Vec<_> = std::iter::from_fn(|| demux.poll_event())
.filter(|e| matches!(e, DemuxEvent::InputDegraded { .. }))
.collect();
assert!(
degraded.is_empty(),
"legal duplicate must not emit InputDegraded, got {degraded:?}"
);
}
#[test]
fn legal_duplicate_packets_are_not_reassembled_twice() {
const DATA_BYTES: usize = 1000;
let body: Vec<u8> = (0..DATA_BYTES).map(|i| (i & 0xFF) as u8).collect();
let pes_len = (3 + body.len()) as u16;
let mut pes = Vec::new();
pes.extend_from_slice(&[0x00, 0x00, 0x01, 0xBD]);
pes.extend_from_slice(&pes_len.to_be_bytes());
pes.push(0x80); pes.push(0x00); pes.push(0x00); pes.extend_from_slice(&body);
assert!(
pes.len() > TS_MAX_PAYLOAD_BYTES,
"the PES must span more than one TS packet for this test to mean anything"
);
let mut input = Vec::new();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
input.extend_from_slice(&null_packet());
}
input.extend_from_slice(&psi_section_packets(
PAT_PID_UNDER_TEST,
0,
TABLE_ID_PAT,
&pat_body(),
));
input.extend_from_slice(&psi_section_packets(
PMT_PID_UNDER_TEST,
0,
TABLE_ID_PMT,
&pmt_body(ES_PID_UNDER_TEST, STREAM_TYPE_PES_PRIVATE),
));
let mut cc = 0u8;
let mut sent = 0usize;
let mut dups = 0usize;
while sent < pes.len() {
let take = (pes.len() - sent).min(TS_MAX_PAYLOAD_BYTES);
let mut pkt = [0xFFu8; TS_PACKET_SIZE];
pkt[0] = 0x47;
pkt[1] = ((ES_PID_UNDER_TEST.value() >> 8) as u8 & PID_HI_MASK)
| if sent == 0 { 0x40 } else { 0x00 };
pkt[2] = (ES_PID_UNDER_TEST.value() & 0xFF) as u8;
pkt[3] = 0x10 | (cc & 0x0F);
pkt[4..4 + take].copy_from_slice(&pes[sent..sent + take]);
input.extend_from_slice(&pkt);
input.extend_from_slice(&pkt);
dups += 1;
sent += take;
cc = cc.wrapping_add(1);
}
assert!(
dups >= 3,
"the PES must span several packets for a duplicate append to be observable ({dups} packets)"
);
let mut demux = StreamingTsDemux::new();
demux.feed(&input);
demux.finish();
let samples: Vec<Sample> = std::iter::from_fn(|| demux.poll_event())
.filter_map(|e| match e {
DemuxEvent::Sample { sample, .. } => Some(sample),
_ => None,
})
.collect();
assert!(
dups >= 2,
"the fixture must really contain duplicates ({dups} emitted)"
);
assert_eq!(samples.len(), 1, "one PES => one Data sample");
assert_eq!(
samples[0].data.as_ref(),
body.as_slice(),
"the delivered sample must be the PES *payload* exactly once — a reassembled duplicate appends its 184 bytes a second time"
);
}
#[test]
fn cc_gap_drops_the_access_unit_it_truncated_but_not_the_next_one() {
let au1 = long_data_au();
let au2 = long_data_au_alt();
let pes1 = untimed_pes(ES_STREAM_ID_PRIVATE_DATA, &au1);
let pes2 = untimed_pes(ES_STREAM_ID_PRIVATE_DATA, &au2);
assert!(
pes1.len() > TS_MAX_PAYLOAD_BYTES && pes2.len() > TS_MAX_PAYLOAD_BYTES,
"both access units must span more than one TS packet"
);
let mut input = Vec::new();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
input.extend_from_slice(&null_packet());
}
input.extend_from_slice(&psi_section_packets(
PAT_PID_UNDER_TEST,
0,
TABLE_ID_PAT,
&pat_body(),
));
input.extend_from_slice(&psi_section_packets(
PMT_PID_UNDER_TEST,
0,
TABLE_ID_PMT,
&pmt_body(ES_PID_UNDER_TEST, STREAM_TYPE_PES_PRIVATE),
));
let pkt1 = pes_packets(ES_PID_UNDER_TEST, 0, &pes1);
assert_eq!(pkt1.len(), 2 * TS_PACKET_SIZE, "AU 1 must span two packets");
input.extend_from_slice(&pkt1[..TS_PACKET_SIZE]);
let pkt2 = pes_packets(ES_PID_UNDER_TEST, 2, &pes2);
let mut demux = StreamingTsDemux::new();
demux.feed(&input);
demux.feed(&pkt2);
demux.finish();
let degraded = std::iter::from_fn(|| demux.poll_event()).any(|e| {
matches!(
e,
DemuxEvent::InputDegraded {
kind: InputDegradation::ContinuityGap { .. },
..
}
)
});
assert!(
degraded,
"the lost continuation must be reported as a CC gap"
);
let mut demux = StreamingTsDemux::new();
demux.feed(&input);
demux.feed(&pkt2);
demux.finish();
let samples: Vec<Sample> = std::iter::from_fn(|| demux.poll_event())
.filter_map(|e| match e {
DemuxEvent::Sample { sample, .. } => Some(sample),
_ => None,
})
.collect();
assert_eq!(
samples.len(),
1,
"only the intact access unit may be delivered, got {} samples",
samples.len()
);
assert_eq!(
samples[0].data.as_ref(),
au2.as_slice(),
"the delivered sample must be the second, whole access unit"
);
}
#[test]
fn cc_gap_emits_continuity_gap() {
let mut demux = StreamingTsDemux::new();
let null = null_packet();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
demux.feed(&null);
}
let payload_a = b"first packet";
let payload_b = b"second different";
let pkt = |cc: u8, payload: &[u8]| -> [u8; TS_PACKET_SIZE] {
let mut p = [0xFFu8; TS_PACKET_SIZE];
p[0] = 0x47;
p[1] = 0x00;
p[2] = 0x42; p[3] = 0x10 | (cc & 0x0F); let end = 4 + payload.len().min(TS_PACKET_SIZE - 4);
p[4..end].copy_from_slice(&payload[..end - 4]);
p
};
demux.feed(&pkt(0, payload_a));
demux.feed(&pkt(5, payload_b));
let degraded: Vec<_> = std::iter::from_fn(|| demux.poll_event())
.filter_map(|e| match e {
DemuxEvent::InputDegraded {
kind, provenance, ..
} => Some((kind, provenance)),
_ => None,
})
.collect();
assert_eq!(
degraded.len(),
1,
"expected exactly one InputDegraded on CC gap"
);
assert_eq!(
degraded[0].0,
InputDegradation::ContinuityGap {
expected: 1,
got: 5
}
);
assert_eq!(degraded[0].1.pid, Some(Pid::new(0x0042)));
assert_eq!(
degraded[0].1.packet_index,
Some(mpeg_ts::resync::LOCK_CONFIRMATIONS as u64 + 2)
);
}
#[test]
fn mutation_proof_duplicate_exclusion_is_load_bearing() {
let mut demux = StreamingTsDemux::new();
let null = null_packet();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
demux.feed(&null);
}
let payload = b"identical";
let pkt = |cc: u8| -> [u8; TS_PACKET_SIZE] {
let mut p = [0xFFu8; TS_PACKET_SIZE];
p[0] = 0x47;
p[1] = 0x00;
p[2] = 0x55;
p[3] = 0x10 | (cc & 0x0F);
let end = 4 + payload.len().min(TS_PACKET_SIZE - 4);
p[4..end].copy_from_slice(&payload[..end - 4]);
p
};
demux.feed(&pkt(0));
demux.feed(&pkt(0));
let degraded: Vec<_> = std::iter::from_fn(|| demux.poll_event())
.filter(|e| matches!(e, DemuxEvent::InputDegraded { .. }))
.collect();
assert!(
degraded.is_empty(),
"legal duplicate must not fire InputDegraded; \
would produce ContinuityGap {{ expected: 1, got: 0 }} if exclusion were removed"
);
}
#[test]
fn mutation_proof_changed_payload_same_cc_is_not_a_duplicate() {
let mut demux = StreamingTsDemux::new();
let null = null_packet();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
demux.feed(&null);
}
let pkt = |cc: u8, payload: &[u8]| -> [u8; TS_PACKET_SIZE] {
let mut p = [0xFFu8; TS_PACKET_SIZE];
p[0] = 0x47;
p[1] = 0x00;
p[2] = 0x66;
p[3] = 0x10 | (cc & 0x0F);
let end = 4 + payload.len().min(TS_PACKET_SIZE - 4);
p[4..end].copy_from_slice(&payload[..end - 4]);
p
};
demux.feed(&pkt(0, b"first"));
demux.feed(&pkt(0, b"second"));
let degraded: Vec<_> = std::iter::from_fn(|| demux.poll_event())
.filter_map(|e| match e {
DemuxEvent::InputDegraded { kind, .. } => Some(kind),
_ => None,
})
.collect();
assert_eq!(
degraded.len(),
1,
"same CC + different payload must fire InputDegraded"
);
assert!(matches!(
degraded[0],
InputDegradation::ContinuityGap { .. }
));
}
#[test]
fn mutation_proof_signalled_discontinuity_suppresses_cc_gap() {
let mut demux = StreamingTsDemux::new();
let null = null_packet();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
demux.feed(&null);
}
let payload = b"payload";
demux.feed(&ts_packet_degradation(
false,
Pid::new(0x0077),
0,
0x10,
None,
payload,
));
let af = [
0x80u8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, ];
demux.feed(&ts_packet_degradation(
false,
Pid::new(0x0077),
5,
0x30,
Some(&af),
payload, ));
let mut saw_discontinuity = false;
let mut saw_cc_gap = false;
while let Some(event) = demux.poll_event() {
match event {
DemuxEvent::Discontinuity { .. } => saw_discontinuity = true,
DemuxEvent::InputDegraded {
kind: InputDegradation::ContinuityGap { .. },
..
} => saw_cc_gap = true,
_ => {}
}
}
assert!(
saw_discontinuity,
"packet with discontinuity_indicator must emit Discontinuity"
);
assert!(
!saw_cc_gap,
"packet with discontinuity_indicator must NOT emit ContinuityGap — \
the exclusion rule was removed or broken"
);
}
#[test]
fn cc_wraps_at_15_to_0_without_false_gap() {
let mut demux = StreamingTsDemux::new();
let null = null_packet();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
demux.feed(&null);
}
let pkt = |cc: u8| -> [u8; TS_PACKET_SIZE] {
let mut p = [0xFFu8; TS_PACKET_SIZE];
p[0] = 0x47;
p[1] = 0x00;
p[2] = 0x88;
p[3] = 0x10 | (cc & 0x0F);
p[4..11].copy_from_slice(b"payload");
p
};
demux.feed(&pkt(14));
demux.feed(&pkt(15));
demux.feed(&pkt(0));
let degraded: Vec<_> = std::iter::from_fn(|| demux.poll_event())
.filter(|e| matches!(e, DemuxEvent::InputDegraded { .. }))
.collect();
assert!(
degraded.is_empty(),
"normal CC wrap 15→0 must not emit InputDegraded, got {degraded:?}"
);
}
#[test]
fn post_discontinuity_legal_cc_emits_nothing() {
let mut demux = StreamingTsDemux::new();
let null = null_packet();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
demux.feed(&null);
}
let payload = b"payload";
demux.feed(&ts_packet_degradation(
false,
Pid::new(0x0083),
0,
0x10,
None,
payload,
));
let af = [0x80u8, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF];
demux.feed(&ts_packet_degradation(
false,
Pid::new(0x0083),
5,
0x30,
Some(&af),
payload,
));
demux.feed(&ts_packet_degradation(
false,
Pid::new(0x0083),
6,
0x10,
None,
payload,
));
let gaps: Vec<_> = std::iter::from_fn(|| demux.poll_event())
.filter(|e| {
matches!(
e,
DemuxEvent::InputDegraded {
kind: InputDegradation::ContinuityGap { .. },
..
}
)
})
.collect();
assert!(
gaps.is_empty(),
"post-discontinuity legal CC must not fire ContinuityGap; \
discontinuity_indicator suppresses the event but updates last_cc. \
Got {gaps:?}"
);
}
#[test]
fn pcr_variation_in_adaptation_field_is_still_a_legal_duplicate() {
let mut demux = StreamingTsDemux::new();
let null = null_packet();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
demux.feed(&null);
}
let payload = b"identical payload for duplicate test";
let af_pcr_100 = [
0x10u8, 0x00, 0x00, 0x00, 0x00, 0x7E, 0x64, ];
demux.feed(&ts_packet_degradation(
false,
Pid::new(0x0100),
0,
0x30, Some(&af_pcr_100),
payload,
));
let af_pcr_200 = [
0x10u8, 0x00, 0x00, 0x00, 0x00, 0x7E, 0xC8, ];
demux.feed(&ts_packet_degradation(
false,
Pid::new(0x0100),
0, 0x30,
Some(&af_pcr_200),
payload, ));
let gaps: Vec<_> = std::iter::from_fn(|| demux.poll_event())
.filter(|e| {
matches!(
e,
DemuxEvent::InputDegraded {
kind: InputDegradation::ContinuityGap { .. },
..
}
)
})
.collect();
assert!(
gaps.is_empty(),
"PCR-only adaptation-field change must not break duplicate detection; \
duplicate check uses pkt.payload, not raw[4..]. Got {gaps:?}"
);
}
const ES_STREAM_ID_MPEG_AUDIO: u8 = 0xC0;
const CARRY_SAMPLE_RATE: i64 = 48_000;
const CARRY_BASE_PTS_90K: i64 = 90_000;
const CARRY_BASE_TRACK: i64 = CARRY_BASE_PTS_90K * CARRY_SAMPLE_RATE / 90_000;
const MP2_SAMPLES: i64 = 1152;
const AAC_SAMPLES: i64 = 1024;
const CARRY_FRAMES: usize = 16;
fn body_bytes(seed: usize, len: usize) -> Vec<u8> {
let mut x = (seed as u32)
.wrapping_mul(2_654_435_761)
.wrapping_add(12_345);
(0..len)
.map(|_| {
x = x.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
((x >> 24) as u8) % 0xFE
})
.collect()
}
fn mp2_frame(n: usize) -> Vec<u8> {
let pad = n.is_multiple_of(3);
let len = 576 + usize::from(pad);
let mut f = alloc::vec![0xFF, 0xFD, 0xA4 | (u8::from(pad) << 1), 0x00];
f.extend_from_slice(&body_bytes(n, len - 4));
let hdr = MpegAudioFrameHeader::parse(&f).expect("valid MP2 header");
assert_eq!(hdr.frame_length, len);
assert_eq!(hdr.samples_per_frame as i64, MP2_SAMPLES);
assert_eq!(hdr.sample_rate, 48_000);
f
}
fn adts_frame(n: usize) -> Vec<u8> {
const PROFILE_AAC_LC: u8 = 1;
const SFI_48000: u8 = 3;
const CHANNELS_STEREO: u8 = 2;
let body = 100 + (n * 37) % 200;
let len = ADTS_HEADER_SIZE + body;
let mut f = crate::aac_asc::build_adts_header(
PROFILE_AAC_LC,
SFI_48000,
CHANNELS_STEREO,
len as u16,
)
.to_vec();
f.extend_from_slice(&body_bytes(n, body));
assert_eq!(
parse_adts_header(&f).expect("ADTS").frame_length as usize,
len
);
f
}
struct AudioEs {
frames: Vec<Vec<u8>>,
es: Vec<u8>,
starts: Vec<usize>,
}
fn audio_es(frames: Vec<Vec<u8>>) -> AudioEs {
let mut es = Vec::new();
let mut starts = Vec::new();
for f in &frames {
starts.push(es.len());
es.extend_from_slice(f);
}
AudioEs { frames, es, starts }
}
fn audio_ts(
stream_type: u8,
es: &AudioEs,
chunk_sizes: &[usize],
frame_ticks_90k: i64,
) -> Vec<u8> {
let mut out = Vec::new();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
out.extend_from_slice(&null_packet());
}
out.extend_from_slice(&psi_section_packets(
PAT_PID_UNDER_TEST,
0,
TABLE_ID_PAT,
&pat_body(),
));
out.extend_from_slice(&psi_section_packets(
PMT_PID_UNDER_TEST,
0,
TABLE_ID_PMT,
&pmt_body(ES_PID_UNDER_TEST, stream_type),
));
let (mut off, mut i, mut cc) = (0usize, 0usize, 0u8);
while off < es.es.len() {
let end = (off + chunk_sizes[i % chunk_sizes.len()]).min(es.es.len());
i += 1;
let first = es.starts.iter().position(|&s| s >= off && s < end);
let pes = match first {
Some(k) => pes_packet(
ES_STREAM_ID_MPEG_AUDIO,
CARRY_BASE_PTS_90K + k as i64 * frame_ticks_90k,
None,
&es.es[off..end],
),
None => pes_packet_untimed(ES_STREAM_ID_MPEG_AUDIO, &es.es[off..end]),
};
let packets = pes_packets(ES_PID_UNDER_TEST, cc, &pes);
cc = cc.wrapping_add((packets.len() / TS_PACKET_SIZE) as u8);
out.extend_from_slice(&packets);
off = end;
}
out
}
fn splits_inside_a_header(es: &AudioEs, chunk_sizes: &[usize], hdr_len: usize) -> bool {
let (mut off, mut i) = (0usize, 0usize);
while off < es.es.len() {
off += chunk_sizes[i % chunk_sizes.len()];
i += 1;
if es.starts.iter().any(|&s| off > s && off < s + hdr_len) {
return true;
}
}
false
}
type Observed = (Vec<(Vec<u8>, Option<i64>, Option<i64>, Option<u32>)>, usize);
fn observe(ts: &[u8], chunk: usize) -> Observed {
let mut d = StreamingTsDemux::new();
let mut samples = Vec::new();
let mut disc = 0usize;
let mut drain = |d: &mut StreamingTsDemux| {
while let Some(e) = d.poll_event() {
match e {
DemuxEvent::Sample { sample, .. } => samples.push((
sample.data.to_vec(),
sample.dts,
sample.pts,
sample.duration,
)),
DemuxEvent::Discontinuity { .. } => disc += 1,
_ => {}
}
}
};
for c in ts.chunks(chunk) {
d.feed(c);
drain(&mut d);
}
d.finish();
drain(&mut d);
(samples, disc)
}
fn assert_carry_contract(
label: &str,
stream_type: u8,
es: &AudioEs,
strip: usize,
samples_per_frame: i64,
chunk_sizes: &[usize],
) {
let frame_ticks_90k = samples_per_frame * 90_000 / CARRY_SAMPLE_RATE;
let ts = audio_ts(stream_type, es, chunk_sizes, frame_ticks_90k);
let (whole, disc) = observe(&ts, ts.len());
assert_eq!(
disc, 0,
"{label}: a continuous stream raises no Discontinuity"
);
let missing: Vec<usize> = (0..es.frames.len())
.filter(|&n| !whole.iter().any(|w| w.0 == es.frames[n][strip..]))
.collect();
assert_eq!(
whole.len(),
es.frames.len(),
"{label}: every frame recovered, none dropped, none invented (missing {missing:?})"
);
for (n, (data, dts, pts, dur)) in whole.iter().enumerate() {
assert_eq!(
data,
&es.frames[n][strip..],
"{label}: frame {n} byte-exact"
);
let want = CARRY_BASE_TRACK + n as i64 * samples_per_frame;
assert_eq!(*dts, Some(want), "{label}: frame {n} dts");
assert_eq!(*pts, Some(want), "{label}: frame {n} pts");
assert_eq!(
*dur,
Some(samples_per_frame as u32),
"{label}: frame {n} duration"
);
}
let media = TsDemux::new().demux(&ts).expect("batch demux");
let batch: Vec<_> = media.tracks[0]
.samples
.iter()
.map(|s| (s.data.to_vec(), s.dts, s.pts, s.duration))
.collect();
assert_eq!(batch, whole, "{label}: batch == streaming");
for chunk in [1usize, 7, 187, 188, 189, 1000] {
let (got, d) = observe(&ts, chunk);
assert_eq!((got, d), (whole.clone(), 0), "{label}: feed chunk {chunk}");
}
let ts2 = TsMux::new().package(&media).expect("remux to TS");
let (again, disc2) = observe(&ts2, ts2.len());
assert_eq!(disc2, 0, "{label}: remux has no Discontinuity");
assert_eq!(again.len(), whole.len(), "{label}: remux frame count");
for (n, (a, w)) in again.iter().zip(&whole).enumerate() {
assert_eq!(a.0, w.0, "{label}: remux frame {n} bytes");
}
for pair in again.windows(2) {
assert_eq!(
pair[1].1.unwrap() - pair[0].1.unwrap(),
samples_per_frame,
"{label}: remux has no timeline gap"
);
}
}
#[test]
fn mp2_frames_straddling_pes_boundaries_are_all_recovered() {
let es = audio_es((0..CARRY_FRAMES).map(mp2_frame).collect());
let patterns: &[&[usize]] = &[
&[2000], &[577], &[575], &[574],
&[573],
&[576], &[1000, 13], &[3], &[1], ];
assert!(
splits_inside_a_header(&es, &[577], 4),
"the matrix must cut inside the 4-byte header"
);
for p in patterns {
assert_carry_contract(
&alloc::format!("mp2 {p:?}"),
STREAM_TYPE_MPEG1_AUDIO,
&es,
0,
MP2_SAMPLES,
p,
);
}
}
#[test]
fn adts_frames_straddling_pes_boundaries_are_all_recovered() {
let es = audio_es((0..CARRY_FRAMES).map(adts_frame).collect());
let patterns: &[&[usize]] = &[
&[2000],
&[301],
&[299],
&[7],
&[6],
&[3],
&[1],
&[900, 17],
];
assert!(
splits_inside_a_header(&es, &[7], ADTS_HEADER_SIZE),
"the matrix must cut inside the 7-byte header"
);
for p in patterns {
assert_carry_contract(
&alloc::format!("adts {p:?}"),
STREAM_TYPE_AAC_ADTS,
&es,
ADTS_HEADER_SIZE,
AAC_SAMPLES,
p,
);
}
}
#[test]
fn split_reports_the_unfinished_tail() {
let (f0, f1) = (mp2_frame(1), mp2_frame(2));
let mut buf = f0.clone();
buf.extend_from_slice(&f1[..100]);
let s = split_frames_with_tail::<MpegAudioSync>(&buf);
assert_eq!(s.frames.len(), 1);
assert_eq!(s.tail_start, f0.len(), "valid header, frame overruns");
for k in 1..4 {
let mut buf = f0.clone();
buf.extend_from_slice(&f1[..k]);
let s = split_frames_with_tail::<MpegAudioSync>(&buf);
assert_eq!(s.frames.len(), 1);
assert_eq!(s.tail_start, f0.len(), "{k}-byte header prefix is held");
}
let s = split_frames_with_tail::<MpegAudioSync>(&f0);
assert_eq!(s.tail_start, f0.len(), "exact end: nothing to carry");
let mut buf = f0.clone();
buf.extend_from_slice(&[0x12, 0x34, 0x56]);
let s = split_frames_with_tail::<MpegAudioSync>(&buf);
assert_eq!(
s.tail_start,
buf.len(),
"non-sync trailing bytes are not held"
);
}
#[test]
fn false_sync_tail_does_not_swallow_the_next_pes() {
let frames: Vec<Vec<u8>> = (0..6).map(mp2_frame).collect();
let mut p1 = frames[0].clone();
p1.extend_from_slice(&frames[1]);
let mut junk = alloc::vec![0xFF, 0xFD, 0xA4, 0x00];
junk.extend_from_slice(&body_bytes(99, 100));
p1.extend_from_slice(&junk);
let p2: Vec<u8> = frames[2..].concat();
let mut ts = Vec::new();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
ts.extend_from_slice(&null_packet());
}
ts.extend_from_slice(&psi_section_packets(
PAT_PID_UNDER_TEST,
0,
TABLE_ID_PAT,
&pat_body(),
));
ts.extend_from_slice(&psi_section_packets(
PMT_PID_UNDER_TEST,
0,
TABLE_ID_PMT,
&pmt_body(ES_PID_UNDER_TEST, STREAM_TYPE_MPEG1_AUDIO),
));
let mut cc = 0u8;
for (k, p) in [(0i64, &p1), (2, &p2)] {
let pes = pes_packet(
ES_STREAM_ID_MPEG_AUDIO,
CARRY_BASE_PTS_90K + k * 2160,
None,
p,
);
let pk = pes_packets(ES_PID_UNDER_TEST, cc, &pes);
cc = cc.wrapping_add((pk.len() / TS_PACKET_SIZE) as u8);
ts.extend_from_slice(&pk);
}
let (got, _) = observe(&ts, ts.len());
let want: Vec<&Vec<u8>> = frames.iter().collect();
assert_eq!(got.len(), want.len(), "all six real frames, junk discarded");
for (g, w) in got.iter().zip(&want) {
assert_eq!(&g.0, *w);
}
}
#[test]
fn dangling_partial_frame_at_flush_emits_nothing() {
let mut frames: Vec<Vec<u8>> = (0..5).map(mp2_frame).collect();
let last = frames.pop().unwrap();
let mut es = audio_es(frames.clone());
es.es.extend_from_slice(&last[..300]); let ts = audio_ts(STREAM_TYPE_MPEG1_AUDIO, &es, &[700], 2160);
let (got, _) = observe(&ts, ts.len());
assert_eq!(got.len(), 4, "the four complete frames only");
for (g, w) in got.iter().zip(&frames) {
assert_eq!(&g.0, w);
}
}
#[test]
fn continuity_gap_drops_the_carry_and_emits_only_genuine_frames() {
let es = audio_es((0..CARRY_FRAMES).map(mp2_frame).collect());
let ts = audio_ts(STREAM_TYPE_MPEG1_AUDIO, &es, &[1000], 2160);
let mut kept = Vec::new();
let mut removed = false;
for (i, p) in ts.chunks_exact(TS_PACKET_SIZE).enumerate() {
let pid = (u16::from(p[1] & 0x1F) << 8) | u16::from(p[2]);
if !removed && i > 12 && pid == ES_PID_UNDER_TEST.value() && p[1] & 0x40 == 0 {
removed = true;
continue;
}
kept.extend_from_slice(p);
}
assert!(removed);
let (got, _) = observe(&kept, kept.len());
let genuine: std::collections::HashSet<&Vec<u8>> = es.frames.iter().collect();
for (i, g) in got.iter().enumerate() {
assert!(genuine.contains(&g.0), "sample {i} is not a genuine frame");
}
assert!(
got.len() < es.frames.len() && got.len() + 5 >= es.frames.len(),
"loss bounded to the damaged PES (got {} of {})",
got.len(),
es.frames.len()
);
}
fn put_bits(out: &mut Vec<u8>, nbits: &mut usize, value: u64, bits: usize) {
for i in (0..bits).rev() {
if (*nbits).is_multiple_of(8) {
out.push(0);
}
let bit = ((value >> i) & 1) as u8;
*out.last_mut().unwrap() |= bit << (7 - (*nbits % 8));
*nbits += 1;
}
}
fn ac3_header(fscod_frmsizecod: u8, body: usize) -> Vec<u8> {
let mut h = alloc::vec![0x0B, 0x77, 0x00, 0x00, fscod_frmsizecod];
let mut n = h.len() * 8;
put_bits(&mut h, &mut n, 8, 5); put_bits(&mut h, &mut n, 0, 3); put_bits(&mut h, &mut n, 2, 3); put_bits(&mut h, &mut n, 0, 2); put_bits(&mut h, &mut n, 0, 1); h.extend_from_slice(&body_bytes(3, body));
h
}
fn ac3_frame(n: usize) -> Vec<u8> {
let mut f = ac3_header(0x00, 0);
let hdr = f.len();
f.extend_from_slice(&body_bytes(n, 128 - hdr));
assert_eq!(f.len(), 128);
assert_eq!(
Ac3SyncframeInfo::parse_at(&f, 0).unwrap().frame_len_bytes(),
Some(128)
);
f
}
fn eac3_header(frmsiz: u16, body: usize) -> Vec<u8> {
let mut h = alloc::vec![0x0B, 0x77];
let mut n = 16;
put_bits(&mut h, &mut n, 0, 2); put_bits(&mut h, &mut n, 0, 3); put_bits(&mut h, &mut n, frmsiz as u64, 11);
put_bits(&mut h, &mut n, 0, 2); put_bits(&mut h, &mut n, 3, 2); put_bits(&mut h, &mut n, 2, 3); put_bits(&mut h, &mut n, 0, 1); put_bits(&mut h, &mut n, 16, 5); h.extend_from_slice(&body_bytes(4, body));
h
}
fn eac3_frame(n: usize) -> Vec<u8> {
let mut f = eac3_header(63, 0);
let hdr = f.len();
f.extend_from_slice(&body_bytes(n, 128 - hdr));
assert_eq!(f.len(), 128);
let info = Ec3SyncframeInfo::parse_at(&f, 0).unwrap();
assert_eq!((info.frmsiz as usize + 1) * 2, 128);
assert_eq!(info.samples_per_frame(), 1536);
f
}
fn dts_header(fsize: u16, body: usize) -> Vec<u8> {
let mut h = DTS_CORE_SYNC_WORD.to_be_bytes().to_vec();
let mut n = 32;
put_bits(&mut h, &mut n, 1, 1); put_bits(&mut h, &mut n, 31, 5); put_bits(&mut h, &mut n, 0, 1); put_bits(&mut h, &mut n, 15, 7); put_bits(&mut h, &mut n, fsize as u64, 14);
put_bits(&mut h, &mut n, 2, 6); put_bits(&mut h, &mut n, 13, 4); put_bits(&mut h, &mut n, 15, 5); put_bits(&mut h, &mut n, 0, 1); put_bits(&mut h, &mut n, 0, 1); put_bits(&mut h, &mut n, 0, 1); put_bits(&mut h, &mut n, 0, 1); put_bits(&mut h, &mut n, 0, 1); put_bits(&mut h, &mut n, 0, 3); put_bits(&mut h, &mut n, 0, 1); put_bits(&mut h, &mut n, 0, 1); put_bits(&mut h, &mut n, 0, 2); put_bits(&mut h, &mut n, 0, 1); h.extend_from_slice(&body_bytes(5, body));
h
}
fn dts_frame(n: usize) -> Vec<u8> {
let mut f = dts_header(511, 0);
let hdr = f.len();
f.extend_from_slice(&body_bytes(n, 512 - hdr));
assert_eq!(f.len(), 512);
let info = DtsCoreFrameInfo::parse_at(&f, 0).unwrap();
assert_eq!(
(info.frame_size, info.samples_per_frame, info.sample_rate),
(512, 512, 48_000)
);
f
}
#[test]
fn ac3_frames_straddling_pes_boundaries_are_all_recovered() {
let es = audio_es((0..CARRY_FRAMES).map(ac3_frame).collect());
for p in [
&[2000][..],
&[129],
&[127],
&[7],
&[4],
&[3],
&[1],
&[300, 13],
] {
assert_carry_contract(
&alloc::format!("ac3 {p:?}"),
STREAM_TYPE_AC3,
&es,
0,
1536,
p,
);
}
}
#[test]
fn eac3_frames_straddling_pes_boundaries_are_all_recovered() {
let es = audio_es((0..CARRY_FRAMES).map(eac3_frame).collect());
for p in [
&[2000][..],
&[129],
&[127],
&[7],
&[4],
&[3],
&[1],
&[300, 13],
] {
assert_carry_contract(
&alloc::format!("eac3 {p:?}"),
STREAM_TYPE_EAC3,
&es,
0,
1536,
p,
);
}
}
fn eac3_dependent_frame(n: usize) -> Vec<u8> {
eac3_dependent_frame_sized(n, 64)
}
fn eac3_dependent_frame_sized(n: usize, len: usize) -> Vec<u8> {
let mut h = alloc::vec![0x0B, 0x77];
let mut nb = 16;
put_bits(&mut h, &mut nb, 1, 2); put_bits(&mut h, &mut nb, 0, 3); put_bits(&mut h, &mut nb, (len / 2 - 1) as u64, 11); put_bits(&mut h, &mut nb, 0, 2); put_bits(&mut h, &mut nb, 3, 2); put_bits(&mut h, &mut nb, 2, 3); put_bits(&mut h, &mut nb, 0, 1); put_bits(&mut h, &mut nb, 16, 5); put_bits(&mut h, &mut nb, 31, 5); put_bits(&mut h, &mut nb, 0, 1); put_bits(&mut h, &mut nb, 1, 1); put_bits(&mut h, &mut nb, (1 << 9) | (1 << 8), 16); put_bits(&mut h, &mut nb, 0, 1); put_bits(&mut h, &mut nb, 0, 1); let hdr = h.len();
h.extend_from_slice(&body_bytes(n + 1000, len - hdr));
assert_eq!(h.len(), len);
crate::ac3::tests::stamp_crc2(&mut h);
let info = Ec3SyncframeInfo::parse_at(&h, 0).unwrap();
assert_eq!(info.strmtyp, EAC3_STRMTYP_DEPENDENT);
h
}
fn eac3_aus(independent: impl Fn(usize) -> Vec<u8>, n: usize) -> AudioEs {
audio_es(
(0..n)
.map(|i| [independent(i), eac3_dependent_frame(i)].concat())
.collect(),
)
}
fn real_eac3_independent_frames() -> Vec<Vec<u8>> {
let path = test_fixtures::path("ts/dolby/eac3.ts");
let ts = std::fs::read(path).expect("committed Dolby E-AC-3 capture");
let media = TsDemux::new().demux(&ts).expect("demux real E-AC-3");
media.tracks[0]
.samples
.iter()
.map(|s| s.data.to_vec())
.collect()
}
fn boundary_sizes(split: usize, au_len: usize) -> Vec<usize> {
let mut v = alloc::vec![split];
v.extend(core::iter::repeat_n(au_len, 64));
v
}
fn first_track_config(ts: &[u8]) -> CodecConfig {
let media = TsDemux::new().demux(ts).expect("demux");
media.tracks[0].spec.config.clone()
}
#[test]
fn eac3_dependent_substream_starting_a_pes_joins_its_access_unit() {
let es = eac3_aus(eac3_frame, 12);
let au_len = es.frames[0].len();
for split in [128usize, 129, 131, 133, 140, 190, 192, 100, 5, au_len] {
assert_carry_contract(
&alloc::format!("eac3+dep split {split}"),
STREAM_TYPE_EAC3,
&es,
0,
1536,
&boundary_sizes(split, au_len),
);
}
for p in [&[1usize][..], &[7], &[300, 13], &[1000]] {
assert_carry_contract(
&alloc::format!("eac3+dep {p:?}"),
STREAM_TYPE_EAC3,
&es,
0,
1536,
p,
);
}
}
#[test]
fn eac3_orphan_dependent_at_stream_start_is_dropped() {
let real = real_eac3_independent_frames();
const REAL_RATE: i64 = 44_100;
let aus = eac3_aus(|i| real[i % real.len()].clone(), 8);
let mut frames = alloc::vec![eac3_dependent_frame(99)];
frames.extend(aus.frames.iter().cloned());
let es = audio_es(frames);
let ts = audio_ts(
STREAM_TYPE_EAC3,
&es,
&[usize::MAX],
1536 * 90_000 / REAL_RATE,
);
let (samples, disc) = observe(&ts, ts.len());
assert_eq!(disc, 0);
assert_eq!(
samples.len(),
aus.frames.len(),
"the orphan must not be a sample of its own"
);
for (n, (data, dts, _, dur)) in samples.iter().enumerate() {
assert_eq!(data, &aus.frames[n], "AU {n} byte-exact");
assert_eq!(*dur, Some(1536));
assert_eq!(*dts, Some(samples[0].1.unwrap() + n as i64 * 1536));
}
}
#[test]
fn eac3_real_independent_frames_with_dependents_join_across_pes() {
let real = real_eac3_independent_frames();
assert!(real.len() >= 12, "the real capture carries many frames");
const REAL_RATE: i64 = 44_100;
let es = eac3_aus(|i| real[i % real.len()].clone(), 12);
let ind_len = real[0].len();
for split in [ind_len, ind_len + 3, ind_len + 40] {
let ts = audio_ts(
STREAM_TYPE_EAC3,
&es,
&boundary_sizes(split, es.frames[0].len()),
1536 * 90_000 / REAL_RATE,
);
let (samples, disc) = observe(&ts, ts.len());
assert_eq!(disc, 0, "split {split}: no Discontinuity");
assert_eq!(
samples.len(),
es.frames.len(),
"split {split}: one sample per access unit — the dependent frame that starts a PES is NOT its own sample"
);
for (n, (data, dts, _, dur)) in samples.iter().enumerate() {
assert_eq!(data, &es.frames[n], "split {split}: AU {n} byte-exact");
assert_eq!(*dur, Some(1536));
assert_eq!(*dts, Some(samples[0].1.unwrap() + n as i64 * 1536));
}
let CodecConfig::Eac3 { config, .. } = first_track_config(&ts) else {
panic!("E-AC-3 track expected");
};
assert!(
config.substreams[0].chan_loc.is_some(),
"dec3 carries the dependent substream"
);
}
}
#[test]
fn eac3_config_recovery_waits_for_the_dependent_substream() {
let es = eac3_aus(eac3_frame, 8);
let au_len = es.frames[0].len();
for split in [128usize, 131, 190] {
let ts = audio_ts(
STREAM_TYPE_EAC3,
&es,
&boundary_sizes(split, au_len),
1536 * 90_000 / CARRY_SAMPLE_RATE,
);
let CodecConfig::Eac3 { config, .. } = first_track_config(&ts) else {
panic!("E-AC-3 track expected");
};
let s0 = &config.substreams[0];
assert!(
s0.chan_loc.is_some() && s0.num_dep_sub == 1,
"split {split}: dec3 must describe the dependent substream, got {config:?}"
);
assert_eq!(s0.chan_loc, Some(0b110), "Lrs/Rrs + Cs");
}
}
#[test]
fn eac3_single_access_unit_stream_resolves_at_end_of_input() {
let es = eac3_aus(eac3_frame, 1);
let au_len = es.frames[0].len();
let ts = audio_ts(
STREAM_TYPE_EAC3,
&es,
&boundary_sizes(128, au_len),
1536 * 90_000 / CARRY_SAMPLE_RATE,
);
let media = TsDemux::new().demux(&ts).expect("demux");
let CodecConfig::Eac3 { config, .. } = &media.tracks[0].spec.config else {
panic!("E-AC-3 track expected");
};
assert!(config.substreams[0].chan_loc.is_some());
assert_eq!(media.tracks[0].samples.len(), 1);
assert_eq!(&media.tracks[0].samples[0].data[..], &es.frames[0][..]);
}
#[test]
fn eac3_without_dependents_emits_each_access_unit_immediately() {
let real = real_eac3_independent_frames();
let ts = std::fs::read(test_fixtures::path("ts/dolby/eac3.ts")).unwrap();
let mut d = StreamingTsDemux::new();
let mut got = 0usize;
for c in ts.chunks(188) {
d.feed(c);
while let Some(e) = d.poll_event() {
got += usize::from(matches!(e, DemuxEvent::Sample { .. }));
}
}
assert!(
got + 8 >= real.len(),
"no per-PES hold-back without dependents: {got} of {}",
real.len()
);
}
#[test]
fn dts_frames_straddling_pes_boundaries_are_all_recovered() {
let es = audio_es((0..CARRY_FRAMES).map(dts_frame).collect());
for p in [
&[3000][..],
&[513],
&[511],
&[11],
&[7],
&[4],
&[3],
&[1],
&[600, 13],
] {
assert_carry_contract(
&alloc::format!("dts {p:?}"),
STREAM_TYPE_DTS_82,
&es,
0,
512,
p,
);
}
}
fn carry_len(d: &StreamingTsDemux) -> Option<usize> {
match d.streams.get(&ES_PID_UNDER_TEST)?.track.as_ref()? {
TrackState::Live(LiveTrack {
kind: LiveKind::Audio { anchor, .. },
..
}) => Some(anchor.carry.len()),
_ => None,
}
}
#[test]
fn carry_is_bounded_under_hostile_input() {
let adts_max = {
let mut h = crate::aac_asc::build_adts_header(1, 3, 2, 8191).to_vec();
h.extend_from_slice(&body_bytes(7, 40));
h
};
let mp2_max = {
let mut h = alloc::vec![0xFF, 0xFD, 0xE8, 0x00];
h.extend_from_slice(&body_bytes(8, 40));
h
};
type HostileCase<'a> = (&'a str, u8, Vec<Vec<u8>>, Vec<u8>, usize);
let cases: [HostileCase; 5] = [
(
"ac3",
STREAM_TYPE_AC3,
(0..4).map(ac3_frame).collect(),
ac3_header(0xA5, 40),
3840,
),
(
"eac3",
STREAM_TYPE_EAC3,
(0..4).map(eac3_frame).collect(),
eac3_header(2047, 40),
4096,
),
(
"dts",
STREAM_TYPE_DTS_82,
(0..4).map(dts_frame).collect(),
dts_header(16_383, 40),
16_384,
),
(
"adts",
STREAM_TYPE_AAC_ADTS,
(0..4).map(adts_frame).collect(),
adts_max,
8191,
),
(
"mp2",
STREAM_TYPE_MPEG1_AUDIO,
(0..4).map(mp2_frame).collect(),
mp2_max,
1728,
),
];
for (label, stream_type, warm, header, claimed) in cases {
assert!(claimed <= AUDIO_CARRY_MAX_BYTES);
let mut ts = Vec::new();
for _ in 0..mpeg_ts::resync::LOCK_CONFIRMATIONS + 1 {
ts.extend_from_slice(&null_packet());
}
ts.extend_from_slice(&psi_section_packets(
PAT_PID_UNDER_TEST,
0,
TABLE_ID_PAT,
&pat_body(),
));
ts.extend_from_slice(&psi_section_packets(
PMT_PID_UNDER_TEST,
0,
TABLE_ID_PMT,
&pmt_body(ES_PID_UNDER_TEST, stream_type),
));
let mut cc = 0u8;
let mut push = |ts: &mut Vec<u8>, payload: &[u8]| {
let pes = pes_packet_untimed(ES_STREAM_ID_MPEG_AUDIO, payload);
let pk = pes_packets(ES_PID_UNDER_TEST, cc, &pes);
cc = cc.wrapping_add((pk.len() / TS_PACKET_SIZE) as u8);
ts.extend_from_slice(&pk);
};
push(&mut ts, &warm.concat());
push(&mut ts, &header);
for i in 0..4000 {
push(&mut ts, &body_bytes(1000 + i, 60));
}
let mut d = StreamingTsDemux::new();
let mut max_carry = 0usize;
for c in ts.chunks(TS_PACKET_SIZE) {
d.feed(c);
let n = carry_len(&d).unwrap_or(0);
assert!(
n <= AUDIO_CARRY_MAX_BYTES,
"{label}: carry {n} exceeds the one-frame bound"
);
assert!(
n < claimed.max(4),
"{label}: carry {n} >= claimed frame {claimed}"
);
max_carry = max_carry.max(n);
}
d.finish();
assert!(
max_carry > 40,
"{label}: the hostile tail must have actually been carried (max {max_carry})"
);
}
}
}