use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
use std::convert::Infallible;
use transmux::{ProgramNumber, TrackId};
use crate::ingress::{IngestSession, ProgramId, SessionEvent};
use crate::trunk::RetentionClass;
use broadcast_common::{Demand, Stage, Timestamp};
use transmux::pipeline::{Sample, TrackSpec};
use transmux::{DemuxEvent, StreamingTsDemux};
pub const MAX_TS_READ: usize = 65_536;
pub const MAX_HELD_SAMPLES: usize = 65_536;
pub const MAX_HELD_BYTES: usize = 64 * 1024 * 1024;
pub struct ProgramTracker {
held: VecDeque<(TrackId, Sample)>,
held_bytes: usize,
pending: VecDeque<SessionEvent>,
resolving: BTreeMap<Option<ProgramNumber>, Vec<TrackSpec>>,
resolved_once: bool,
programs: BTreeMap<Option<ProgramNumber>, PerProgram>,
track_program: HashMap<TrackId, ProgramId>,
changed: HashSet<ProgramId>,
next_program_id: u32,
backlog_drop_observer: Option<fn(u64)>,
}
struct PerProgram {
program_id: ProgramId,
track_specs: BTreeMap<TrackId, TrackSpec>,
}
impl Default for ProgramTracker {
fn default() -> Self {
Self::new()
}
}
impl ProgramTracker {
pub fn new() -> Self {
ProgramTracker {
held: VecDeque::new(),
held_bytes: 0,
pending: VecDeque::from(vec![SessionEvent::Established]),
resolving: BTreeMap::new(),
resolved_once: false,
programs: BTreeMap::new(),
track_program: HashMap::new(),
changed: HashSet::new(),
next_program_id: 0,
backlog_drop_observer: None,
}
}
#[must_use]
pub fn with_backlog_drop_observer(mut self, observer: fn(u64)) -> Self {
self.backlog_drop_observer = Some(observer);
self
}
pub fn handle(&mut self, event: DemuxEvent) {
match event {
DemuxEvent::TrackAdded(spec) => self.track_added(spec),
DemuxEvent::TracksResolved { .. } => self.tracks_resolved(),
DemuxEvent::Sample {
track_id, sample, ..
} => self.sample(track_id, sample),
DemuxEvent::TrackRemoved { track_id, .. } => self.track_removed(track_id),
DemuxEvent::TrackUpdated(spec) => self.track_updated(spec),
DemuxEvent::TrackAbandoned { .. } => {}
_ => {}
}
}
pub fn track_added(&mut self, spec: TrackSpec) {
let pn = spec.program_number;
if self.resolved_once {
let program = self.find_or_create_program(pn);
self.track_specs_for_program(program)
.insert(spec.track_id, spec.clone());
self.track_program.insert(spec.track_id, program);
self.changed.insert(program);
} else {
self.resolving.entry(pn).or_default().push(spec);
}
}
pub fn tracks_resolved(&mut self) {
if !self.resolved_once {
if !self.resolving.is_empty() {
self.resolved_once = true;
let groups: Vec<_> = std::mem::take(&mut self.resolving).into_iter().collect();
for (pn, tracks) in groups {
if tracks.is_empty() {
continue;
}
let program = ProgramId(self.next_program_id);
self.next_program_id += 1;
let mut track_specs = BTreeMap::new();
for spec in &tracks {
self.track_program.insert(spec.track_id, program);
track_specs.insert(spec.track_id, spec.clone());
}
self.programs.insert(
pn,
PerProgram {
program_id: program,
track_specs,
},
);
self.pending
.push_back(SessionEvent::NewProgram { program, tracks });
}
self.held_bytes = 0;
for (track_id, sample) in std::mem::take(&mut self.held) {
if let Some(&program) = self.track_program.get(&track_id) {
self.pending.push_back(SessionEvent::Sample {
program,
track_id,
retention: RetentionClass::Timed,
sample,
});
}
}
}
} else {
let dirty = std::mem::take(&mut self.changed);
for prog in self.programs.values() {
if !dirty.contains(&prog.program_id) {
continue;
}
let tracks: Vec<TrackSpec> = prog.track_specs.values().cloned().collect();
if !tracks.is_empty() {
self.pending.push_back(SessionEvent::TracksChanged {
program: prog.program_id,
tracks,
});
}
}
}
}
pub fn sample(&mut self, track_id: TrackId, sample: Sample) {
if let Some(&program) = self.track_program.get(&track_id) {
self.pending.push_back(SessionEvent::Sample {
program,
track_id,
retention: RetentionClass::Timed,
sample,
});
} else if !self.resolved_once
&& self
.resolving
.values()
.any(|specs| specs.iter().any(|s| s.track_id == track_id))
{
self.hold(track_id, sample);
}
}
pub fn track_removed(&mut self, track_id: TrackId) {
if let Some(program) = self.track_program.remove(&track_id) {
if let Some(prog) = self.program_for_program_id_mut(program) {
prog.track_specs.remove(&track_id);
}
self.changed.insert(program);
}
}
pub fn track_updated(&mut self, spec: TrackSpec) {
let track_id = spec.track_id;
if self.resolved_once {
if let Some(&program) = self.track_program.get(&track_id)
&& let Some(prog) = self.program_for_program_id_mut(program)
{
prog.track_specs.insert(track_id, spec);
let mut tracks: Vec<TrackSpec> = prog.track_specs.values().cloned().collect();
tracks.sort_by_key(|t| t.track_id);
self.pending
.push_back(SessionEvent::TracksChanged { program, tracks });
}
} else if let Some(buffered) = self
.resolving
.values_mut()
.flat_map(|specs| specs.iter_mut())
.find(|s| s.track_id == track_id)
{
*buffered = spec;
}
}
pub fn poll(&mut self) -> Option<SessionEvent> {
self.pending.pop_front()
}
fn hold(&mut self, track_id: TrackId, sample: Sample) {
self.held_bytes += sample.data.len();
self.held.push_back((track_id, sample));
let mut dropped = 0u64;
while self.held.len() > MAX_HELD_SAMPLES || self.held_bytes > MAX_HELD_BYTES {
let Some((_, old)) = self.held.pop_front() else {
break;
};
self.held_bytes -= old.data.len();
dropped += 1;
}
if dropped > 0
&& let Some(observer) = self.backlog_drop_observer
{
observer(dropped);
}
}
fn find_or_create_program(&mut self, pn: Option<ProgramNumber>) -> ProgramId {
if let Some(prog) = self.programs.get(&pn) {
return prog.program_id;
}
let program = ProgramId(self.next_program_id);
self.next_program_id += 1;
self.programs.insert(
pn,
PerProgram {
program_id: program,
track_specs: BTreeMap::new(),
},
);
self.pending.push_back(SessionEvent::NewProgram {
program,
tracks: Vec::new(),
});
program
}
fn track_specs_for_program(&mut self, program: ProgramId) -> &mut BTreeMap<TrackId, TrackSpec> {
&mut self
.program_for_program_id_mut(program)
.expect("program must exist")
.track_specs
}
fn program_for_program_id_mut(&mut self, program: ProgramId) -> Option<&mut PerProgram> {
self.programs.values_mut().find(|p| p.program_id == program)
}
}
pub struct TsIngestSession {
demux: StreamingTsDemux,
tracker: ProgramTracker,
first_fed_at: Option<Timestamp>,
silent_heads_released: bool,
}
pub const SILENT_PID_SETTLE: std::time::Duration = std::time::Duration::from_secs(1);
impl TsIngestSession {
#[must_use]
pub fn with_backlog_drop_observer(mut self, observer: fn(u64)) -> Self {
self.tracker = self.tracker.with_backlog_drop_observer(observer);
self
}
pub fn new() -> Self {
TsIngestSession {
demux: StreamingTsDemux::new(),
tracker: ProgramTracker::new(),
first_fed_at: None,
silent_heads_released: false,
}
}
fn drain_demux(&mut self) {
while let Some(event) = self.demux.poll_event() {
self.tracker.handle(event);
}
}
}
impl Default for TsIngestSession {
fn default() -> Self {
Self::new()
}
}
impl Stage for TsIngestSession {
type In<'a> = &'a [u8];
type Out = SessionEvent;
type Error = Infallible;
fn feed(&mut self, input: &[u8], now: Timestamp) -> Result<(), Infallible> {
self.demux.feed(input);
self.drain_demux();
let first = *self.first_fed_at.get_or_insert(now);
if !self.silent_heads_released && now.saturating_sub(first) >= SILENT_PID_SETTLE {
self.silent_heads_released = true;
self.demux.release_silent_heads();
self.drain_demux();
}
Ok(())
}
fn poll(&mut self) -> Option<SessionEvent> {
self.tracker.poll()
}
fn finish(&mut self) -> Result<(), Infallible> {
self.demux.finish();
while let Some(event) = self.demux.poll_event() {
self.tracker.handle(event);
}
Ok(())
}
fn demand(&self) -> Demand {
Demand::new(MAX_TS_READ)
}
}
impl IngestSession for TsIngestSession {
type Request = bytes::Bytes;
}
#[cfg(any(test, feature = "test-support"))]
pub mod test_support {
use crate::ingress::HandshakePolicy;
use crate::trunk::TrunkConfig;
use transmux::TrackId;
use broadcast_common::Timestamp;
use std::num::NonZeroUsize;
use transmux::TsMux;
use transmux::media::Track;
use transmux::pipeline::{CodecConfig, Sample, TrackSpec};
fn nz(n: usize) -> NonZeroUsize {
NonZeroUsize::new(n).expect("test capacity must be non-zero")
}
pub fn trunk_config() -> TrunkConfig {
TrunkConfig::default()
.with_timed_capacity(nz(64))
.with_sparse_capacity(nz(16))
.with_segment_capacity(nz(8))
.with_event_capacity(nz(8))
.with_part_capacity(nz(8))
}
pub fn handshake() -> HandshakePolicy {
HandshakePolicy::establish_by(Timestamp::from_nanos(u64::MAX))
}
pub fn track_spec(track_id: TrackId) -> TrackSpec {
let avc = transmux::avc_config_from_sprop("Z0IAKeKQFAe2AtwEBAaQeJEV,aM48gA==")
.expect("valid sprop");
TrackSpec::new(
track_id,
90_000,
CodecConfig::Avc {
config: avc,
width: 0,
height: 0,
},
)
}
pub fn build_ts_bytes(track_id: TrackId, nal_byte: u8, count: u32) -> Vec<u8> {
build_ts_bytes_with_gop(track_id, nal_byte, count, u32::MAX)
}
pub fn build_ts_bytes_with_gop(
track_id: TrackId,
nal_byte: u8,
count: u32,
gop: u32,
) -> Vec<u8> {
use broadcast_common::Package;
let spec = track_spec(track_id);
let frame_dur = 90_000 / 30;
let samples: Vec<Sample> = (0..count)
.map(|i| {
let nal = [0x65u8, nal_byte, (i % 256) as u8];
let mut data = (nal.len() as u32).to_be_bytes().to_vec();
data.extend_from_slice(&nal);
Sample::new(
data,
Some(i64::from(i) * i64::from(frame_dur)),
Some(i64::from(i) * i64::from(frame_dur)),
Some(frame_dur),
i % gop == 0,
)
})
.collect();
let track = Track::new(spec, samples);
let media = transmux::media::Media::new(vec![track], 90_000);
TsMux::default().package(&media).expect("mux to TS")
}
}
#[cfg(test)]
mod tests {
use super::test_support::track_spec;
use super::*;
use transmux::pipeline::Sample;
fn sample_at(nal: u8) -> Sample {
Sample::new(vec![0x65, nal], Some(0), Some(0), Some(3000), true)
}
#[test]
fn mpts_programs_announced_in_ascending_program_number_order() {
for _ in 0..200 {
let mut tracker = ProgramTracker::new();
for (pn, tid) in [(300u16, 31u32), (100, 11), (200, 21), (100, 12)] {
tracker.handle(DemuxEvent::TrackAdded(
track_spec(TrackId::new(tid)).with_program(ProgramNumber::new(pn)),
));
}
tracker.handle(DemuxEvent::tracks_resolved(0));
assert!(matches!(tracker.poll(), Some(SessionEvent::Established)));
let mut got = Vec::new();
while let Some(ev) = tracker.poll() {
match ev {
SessionEvent::NewProgram { program, tracks } => {
got.push((
program,
tracks.iter().map(|t| t.track_id).collect::<Vec<_>>(),
));
}
other => panic!("unexpected {other:?}"),
}
}
assert_eq!(
got,
vec![
(ProgramId(0), vec![TrackId::new(11), TrackId::new(12)]),
(ProgramId(1), vec![TrackId::new(21)]),
(ProgramId(2), vec![TrackId::new(31)]),
]
);
}
}
#[test]
fn mid_stream_tracks_changed_is_ordered() {
for _ in 0..200 {
let mut tracker = ProgramTracker::new();
for (pn, tid) in [(100u16, 11u32), (200, 21), (300, 31)] {
tracker.handle(DemuxEvent::TrackAdded(
track_spec(TrackId::new(tid)).with_program(ProgramNumber::new(pn)),
));
}
tracker.handle(DemuxEvent::tracks_resolved(0));
while tracker.poll().is_some() {}
for (pn, tid) in [(300u16, 39u32), (100, 19), (200, 29), (100, 15), (300, 33)] {
tracker.handle(DemuxEvent::TrackAdded(
track_spec(TrackId::new(tid)).with_program(ProgramNumber::new(pn)),
));
}
tracker.handle(DemuxEvent::tracks_resolved(1));
let mut got = Vec::new();
while let Some(ev) = tracker.poll() {
match ev {
SessionEvent::TracksChanged { program, tracks } => {
got.push((
program,
tracks
.iter()
.map(|t| t.track_id.value())
.collect::<Vec<_>>(),
));
}
other => panic!("unexpected {other:?}"),
}
}
assert_eq!(
got,
vec![
(ProgramId(0), vec![11, 15, 19]),
(ProgramId(1), vec![21, 29]),
(ProgramId(2), vec![31, 33, 39]),
]
);
}
}
#[test]
fn first_tracks_resolved_mints_program_zero() {
let mut tracker = ProgramTracker::new();
tracker.handle(DemuxEvent::TrackAdded(track_spec(TrackId::new(1))));
tracker.handle(DemuxEvent::tracks_resolved(0));
assert!(matches!(tracker.poll(), Some(SessionEvent::Established)));
match tracker.poll() {
Some(SessionEvent::NewProgram { program, tracks }) => {
assert_eq!(program, ProgramId(0));
assert_eq!(tracks.len(), 1);
}
other => panic!("expected NewProgram(0), got {other:?}"),
}
}
#[test]
fn late_track_added_joins_the_same_program_not_a_new_one() {
let mut tracker = ProgramTracker::new();
tracker.handle(DemuxEvent::TrackAdded(track_spec(TrackId::new(1))));
tracker.handle(DemuxEvent::tracks_resolved(0));
let _established = tracker.poll();
let first = match tracker.poll() {
Some(SessionEvent::NewProgram { program, .. }) => program,
other => panic!("expected NewProgram, got {other:?}"),
};
tracker.handle(DemuxEvent::TrackAdded(track_spec(TrackId::new(9))));
assert!(
tracker.poll().is_none(),
"TrackAdded alone must not emit anything — TracksChanged waits for TracksResolved"
);
tracker.handle(DemuxEvent::tracks_resolved(1));
match tracker.poll() {
Some(SessionEvent::TracksChanged { program, tracks }) => {
assert_eq!(program, first, "TracksChanged must go to the same program");
assert_eq!(tracks.len(), 2, "complete current set: both tracks");
let ids: Vec<TrackId> = tracks.iter().map(|t| t.track_id).collect();
assert!(ids.contains(&TrackId::new(1)));
assert!(ids.contains(&TrackId::new(9)));
}
other => panic!("expected TracksChanged, got {other:?}"),
}
tracker.handle(DemuxEvent::sample(TrackId::new(9), sample_at(0xAA)));
match tracker.poll() {
Some(SessionEvent::Sample {
program, track_id, ..
}) => {
assert_eq!(program, first);
assert_eq!(track_id, TrackId::new(9));
}
other => panic!("expected Sample routed to the first program, got {other:?}"),
}
}
#[test]
fn sample_for_unannounced_track_is_dropped() {
let mut tracker = ProgramTracker::new();
tracker.handle(DemuxEvent::sample(TrackId::new(42), sample_at(0x01)));
assert!(matches!(tracker.poll(), Some(SessionEvent::Established)));
assert!(
tracker.poll().is_none(),
"no event for an unannounced track's sample"
);
}
#[test]
fn removed_track_vanishes_from_tracks_changed() {
let mut tracker = ProgramTracker::new();
tracker.handle(DemuxEvent::TrackAdded(track_spec(TrackId::new(1))));
tracker.handle(DemuxEvent::TrackAdded(track_spec(TrackId::new(2))));
tracker.handle(DemuxEvent::tracks_resolved(0));
let _established = tracker.poll();
let _new_program = tracker.poll();
tracker.handle(DemuxEvent::track_removed(
TrackId::new(2),
transmux::EventProvenance::default(),
));
tracker.handle(DemuxEvent::tracks_resolved(1));
match tracker.poll() {
Some(SessionEvent::TracksChanged { tracks, .. }) => {
assert_eq!(tracks.len(), 1);
assert_eq!(tracks[0].track_id, TrackId::new(1));
}
other => panic!("expected TracksChanged with track 1 only, got {other:?}"),
}
tracker.handle(DemuxEvent::sample(TrackId::new(2), sample_at(0xBB)));
assert!(
tracker.poll().is_none(),
"sample for removed track must be dropped"
);
}
#[test]
fn a_segmenter_can_hold_a_segment_writer_while_ingest_holds_the_sample_writer() {
use super::test_support::{build_ts_bytes, handshake, track_spec, trunk_config};
use crate::ingress::{IngestDriver, ProgramId};
use crate::trunk::{PartEntry, SampleCursorItem, SegmentEntry};
use broadcast_common::Timestamp;
use std::time::Duration as StdDuration;
use transmux::ll_hls::LlHlsSegmenter;
use transmux::segmenter::SegmentMeta;
let mut driver = IngestDriver::new(
TsIngestSession::new(),
crate::IngestConfig::new(handshake()).with_trunk(trunk_config()),
);
let ts_bytes = build_ts_bytes(TrackId::new(1), 0xAB, 90);
driver.feed(&ts_bytes, Timestamp::ZERO);
let trunk = driver
.trunk(ProgramId(0))
.cloned()
.expect("program 0 resolved from the muxed TS");
assert!(
trunk.writer().is_none(),
"the IngestDriver must already hold this Trunk's sample writer"
);
let segment_writer = trunk
.segment_writer()
.expect("the segments+parts ring group has its own single-take writer");
assert!(
trunk.segment_writer().is_none(),
"the segment writer is itself single-take, on its own flag"
);
let mut cursor = trunk.subscribe();
let mut seg = LlHlsSegmenter::with_part_target(
vec![track_spec(TrackId::new(1))],
transmux::VIDEO_CLOCK_RATE,
1.0,
250,
)
.expect("segmenter builds from the resolved track spec");
let more = build_ts_bytes(TrackId::new(1), 0xCD, 90);
driver.feed(&more, Timestamp::from_nanos(1));
let mut pushed = 0usize;
while let Some(item) = cursor.poll() {
if let SampleCursorItem::Timed { track_id, sample } = item {
seg.push(track_id, sample)
.expect("segmenter accepts sample");
pushed += 1;
}
}
assert!(pushed > 0, "the cursor must have observed real samples");
seg.flush().expect("flush the trailing partial segment");
let mut parts_published = 0usize;
for part in seg.take_ready_parts() {
segment_writer.publish_part(PartEntry::new(
part.bytes,
crate::ids::SegmentSeq::new(part.segment_seq),
crate::ids::PartIndex::new(part.part_index),
StdDuration::from_secs_f64(part.duration),
part.independent,
));
parts_published += 1;
}
let mut segments_published = 0usize;
for segment in seg.take_ready_segments() {
segment_writer
.publish_segment(SegmentEntry::new(
segment.bytes,
crate::ids::SegmentSeq::new(segment.segment_seq),
StdDuration::from_secs_f64(segment.duration),
Timestamp::ZERO,
SegmentMeta {
discontinuous: false,
},
))
.unwrap();
segments_published += 1;
}
assert!(
segments_published > 0,
"the segmenter must have produced at least one closed segment"
);
assert!(
trunk.last_closed_segment().is_some(),
"a published segment must be visible via Trunk::last_closed_segment"
);
assert_eq!(
trunk.segment_len(),
segments_published,
"every published segment must be in the Trunk's segment log"
);
if parts_published > 0 {
assert!(
trunk.part_len() > 0,
"published parts must be visible in the Trunk's live-part log"
);
}
}
fn stalled_head_ts_bytes() -> Vec<u8> {
stalled_head_ts_bytes_n(540)
}
fn stalled_head_ts_bytes_n(head_aus: u32) -> Vec<u8> {
use broadcast_common::Package;
use transmux::media::{Media, Track};
use transmux::pipeline::{CodecConfig, DataCarriage};
let dur = 3000u32;
let samples = |n: u32, len: usize, sync: bool| -> Vec<Sample> {
(0..n)
.map(|i| {
let t = i64::from(i) * i64::from(dur);
Sample::new(vec![0xBBu8; len], Some(t), Some(t), Some(dur), sync)
})
.collect()
};
let data = |stream_type: u8| CodecConfig::Data {
stream_type,
descriptors: Vec::new(),
carriage: DataCarriage::Pes,
};
let tracks = vec![
Track::new(
TrackSpec::new(TrackId::new(1), 90_000, data(0x04)),
samples(head_aus, 3, true),
),
Track::new(
TrackSpec::new(TrackId::new(2), 90_000, data(0x7F)),
samples(600, 32, true),
),
Track::new(
TrackSpec::new(TrackId::new(3), 90_000, data(0x7E)),
samples(600, 32, true),
),
];
transmux::TsMux::default()
.package(&Media::new(tracks, 90_000))
.expect("mux to TS")
}
#[test]
fn stalled_head_does_not_hide_the_programme_from_the_session() {
use crate::ingress::IngestDriver;
use test_support::{handshake, trunk_config};
let ts = stalled_head_ts_bytes();
let mut session = TsIngestSession::new();
session
.feed(&ts, Timestamp::ZERO)
.expect("TS feed is infallible");
let mut programs = Vec::new();
let mut samples = std::collections::BTreeSet::new();
while let Some(ev) = session.poll() {
match ev {
SessionEvent::NewProgram { tracks, .. } => programs.push(tracks),
SessionEvent::Sample { track_id, .. } => {
samples.insert(track_id);
}
_ => {}
}
}
assert_eq!(programs.len(), 1, "the programme is announced");
let mut ids: Vec<TrackId> = programs[0].iter().map(|t| t.track_id).collect();
ids.sort();
assert_eq!(ids, vec![TrackId::new(1), TrackId::new(2)]);
assert_eq!(
samples,
[TrackId::new(1), TrackId::new(2)].into_iter().collect(),
"both healthy tracks' samples flow"
);
let mut driver = IngestDriver::new(
TsIngestSession::new(),
crate::IngestConfig::new(handshake()).with_trunk(trunk_config()),
);
driver.feed(&ts, Timestamp::ZERO);
assert!(
driver.trunk(ProgramId(0)).is_some(),
"the driver minted the programme's Trunk"
);
}
type TrackSamples =
std::collections::BTreeMap<TrackId, Vec<(Vec<u8>, Option<i64>, Option<i64>)>>;
fn raw_demux_samples(ts: &[u8], finish: bool) -> (TrackSamples, usize) {
let mut demux = StreamingTsDemux::new();
demux.feed(ts);
if finish {
demux.finish();
}
let mut by_track: std::collections::BTreeMap<_, Vec<_>> = Default::default();
let (mut resolved, mut before) = (false, 0usize);
while let Some(ev) = demux.poll_event() {
match ev {
DemuxEvent::TracksResolved { .. } => resolved = true,
DemuxEvent::Sample {
track_id, sample, ..
} => {
if !resolved {
before += 1;
}
by_track.entry(track_id).or_default().push((
sample.data.to_vec(),
sample.dts,
sample.pts,
));
}
_ => {}
}
}
(by_track, before)
}
fn session_samples(ts: &[u8], finish: bool) -> TrackSamples {
let mut session = TsIngestSession::new();
session.feed(ts, Timestamp::ZERO).expect("infallible");
if finish {
session.finish().expect("infallible");
}
let mut announced: HashMap<ProgramId, HashSet<TrackId>> = HashMap::new();
let mut by_track: std::collections::BTreeMap<_, Vec<_>> = Default::default();
while let Some(ev) = session.poll() {
match ev {
SessionEvent::NewProgram { program, tracks } => {
announced.insert(program, tracks.iter().map(|t| t.track_id).collect());
}
SessionEvent::Sample {
program,
track_id,
sample,
..
} => {
assert!(
announced
.get(&program)
.is_some_and(|t| t.contains(&track_id)),
"a sample for {track_id:?} preceded its programme's announcement"
);
by_track.entry(track_id).or_default().push((
sample.data.to_vec(),
sample.dts,
sample.pts,
));
}
_ => {}
}
}
by_track
}
fn assert_session_equals_raw_demux(ts: &[u8]) {
assert_session_equals_raw_demux_with(ts, false);
}
fn assert_session_equals_raw_demux_with(ts: &[u8], finish: bool) {
let (raw, before_resolved) = raw_demux_samples(ts, finish);
let published = session_samples(ts, finish);
assert!(!raw.is_empty(), "fixture demuxed no samples");
println!("samples demuxed before TracksResolved: {before_resolved}");
for (track, expect) in &raw {
let got = published.get(track).map_or(&[][..], |v| v.as_slice());
assert_eq!(
got.len(),
expect.len(),
"{track:?}: the session lost {} of {} samples (first-AU data loss at \
stream start; {before_resolved} samples were demuxed before TracksResolved)",
expect.len() - got.len().min(expect.len()),
expect.len()
);
assert_eq!(
got,
expect.as_slice(),
"{track:?}: bytes/timestamps/order differ"
);
}
assert_eq!(published.len(), raw.len(), "a track went missing");
}
#[test]
fn samples_before_tracks_resolved_reach_the_session_synthetic() {
let ts = stalled_head_ts_bytes();
let (_, before) = raw_demux_samples(&ts, false);
assert!(before > 0, "premise: samples precede TracksResolved");
assert_session_equals_raw_demux(&ts);
}
#[test]
fn samples_before_tracks_resolved_reach_the_session_france_tnt() {
let Some(path) =
test_fixtures::require("ts/france-tnt-dvbt-20s.ts", "multimux backlog replay")
else {
return;
};
assert_session_equals_raw_demux(&std::fs::read(path).expect("read fixture"));
}
#[test]
fn finish_announces_programme_for_a_silent_declared_pid_synthetic() {
let ts = stalled_head_ts_bytes_n(100);
let mut session = TsIngestSession::new();
session.feed(&ts, Timestamp::ZERO).expect("infallible");
let mut announced_before = false;
while let Some(ev) = session.poll() {
announced_before |= matches!(ev, SessionEvent::NewProgram { .. });
}
assert!(!announced_before, "premise: no NewProgram before finish()");
let (raw, _) = raw_demux_samples(&ts, true);
assert!(
!raw.is_empty(),
"premise: the demux emits media at finish()"
);
assert_session_equals_raw_demux_with(&ts, true);
}
#[test]
fn finish_twice_emits_nothing_more() {
let ts = stalled_head_ts_bytes_n(100);
let mut session = TsIngestSession::new();
session.feed(&ts, Timestamp::ZERO).expect("infallible");
session.finish().expect("infallible");
let mut first = 0usize;
while session.poll().is_some() {
first += 1;
}
assert!(first > 1, "finish() produced events");
session.finish().expect("infallible");
assert!(
session.poll().is_none(),
"a second finish() must not duplicate events"
);
}
#[test]
fn finish_announces_programme_for_the_zenith_capture() {
let Some(path) = test_fixtures::require(
"ts/france-tnt-m6-eac3-zenith.ts",
"multimux end-of-input demux finish",
) else {
return;
};
let ts = std::fs::read(path).expect("read fixture");
assert_session_equals_raw_demux_with(&ts, true);
}
#[test]
fn pre_resolution_samples_flush_in_order_after_new_program() {
let mut tracker = ProgramTracker::new();
tracker.handle(DemuxEvent::TrackAdded(track_spec(TrackId::new(1))));
tracker.handle(DemuxEvent::sample(TrackId::new(1), sample_at(0x01)));
tracker.handle(DemuxEvent::TrackAdded(track_spec(TrackId::new(2))));
tracker.handle(DemuxEvent::sample(TrackId::new(2), sample_at(0x02)));
tracker.handle(DemuxEvent::sample(TrackId::new(1), sample_at(0x03)));
tracker.handle(DemuxEvent::sample(TrackId::new(9), sample_at(0x09)));
assert!(matches!(tracker.poll(), Some(SessionEvent::Established)));
assert!(tracker.poll().is_none(), "nothing before the programme");
tracker.handle(DemuxEvent::tracks_resolved(0));
assert!(matches!(
tracker.poll(),
Some(SessionEvent::NewProgram { .. })
));
let mut got = Vec::new();
while let Some(ev) = tracker.poll() {
match ev {
SessionEvent::Sample {
track_id, sample, ..
} => got.push((track_id, sample.data.to_vec())),
other => panic!("unexpected {other:?}"),
}
}
assert_eq!(
got,
vec![
(TrackId::new(1), vec![0x65, 0x01]),
(TrackId::new(2), vec![0x65, 0x02]),
(TrackId::new(1), vec![0x65, 0x03]),
]
);
tracker.handle(DemuxEvent::sample(TrackId::new(2), sample_at(0x04)));
assert!(matches!(tracker.poll(), Some(SessionEvent::Sample { .. })));
}
fn flushed_dts(tracker: &mut ProgramTracker) -> Vec<i64> {
let mut out = Vec::new();
while let Some(ev) = tracker.poll() {
if let SessionEvent::Sample { sample, .. } = ev {
out.push(sample.dts.expect("dts"));
}
}
out
}
#[test]
fn held_backlog_is_bounded_by_count_dropping_oldest() {
let mut tracker = ProgramTracker::new();
tracker.handle(DemuxEvent::TrackAdded(track_spec(TrackId::new(1))));
let total = MAX_HELD_SAMPLES + 5;
for i in 0..total {
let s = Sample::new(vec![0x65], Some(i as i64), Some(i as i64), Some(1), true);
tracker.handle(DemuxEvent::sample(TrackId::new(1), s));
}
tracker.handle(DemuxEvent::tracks_resolved(0));
let got = flushed_dts(&mut tracker);
assert_eq!(got.len(), MAX_HELD_SAMPLES);
assert_eq!(got[0], 5, "the 5 oldest were dropped");
assert_eq!(*got.last().unwrap(), total as i64 - 1, "newest kept");
assert!(got.windows(2).all(|w| w[0] + 1 == w[1]), "order preserved");
}
#[test]
fn held_backlog_is_bounded_by_bytes_dropping_oldest() {
let mut tracker = ProgramTracker::new();
tracker.handle(DemuxEvent::TrackAdded(track_spec(TrackId::new(1))));
const MIB: usize = 1024 * 1024;
let total = MAX_HELD_BYTES / MIB + 3;
for i in 0..total {
let s = Sample::new(
vec![0u8; MIB],
Some(i as i64),
Some(i as i64),
Some(1),
true,
);
tracker.handle(DemuxEvent::sample(TrackId::new(1), s));
}
tracker.handle(DemuxEvent::tracks_resolved(0));
let got = flushed_dts(&mut tracker);
assert_eq!(got.len(), MAX_HELD_BYTES / MIB, "capped at the byte budget");
assert_eq!(got[0], 3, "the 3 oldest were dropped");
assert_eq!(*got.last().unwrap(), total as i64 - 1);
}
fn track_spec_sized(track_id: TrackId, width: u16, height: u16) -> TrackSpec {
let avc = transmux::avc_config_from_sprop("Z0IAKeKQFAe2AtwEBAaQeJEV,aM48gA==")
.expect("valid sprop");
TrackSpec::new(
track_id,
90_000,
transmux::pipeline::CodecConfig::Avc {
config: avc,
width,
height,
},
)
}
fn video_size(spec: &TrackSpec) -> (u16, u16) {
match &spec.config {
transmux::pipeline::CodecConfig::Avc { width, height, .. } => (*width, *height),
other => panic!("expected an AVC track, got {other:?}"),
}
}
#[test]
fn track_updated_announces_the_new_config() {
let mut tracker = ProgramTracker::new();
tracker.handle(DemuxEvent::TrackAdded(track_spec_sized(
TrackId::new(1),
640,
360,
)));
tracker.handle(DemuxEvent::TrackAdded(track_spec_sized(
TrackId::new(2),
0,
0,
)));
tracker.handle(DemuxEvent::tracks_resolved(0));
let _established = tracker.poll();
let _new_program = tracker.poll();
tracker.handle(DemuxEvent::TrackUpdated(track_spec_sized(
TrackId::new(1),
1920,
1080,
)));
match tracker.poll() {
Some(SessionEvent::TracksChanged { program, tracks }) => {
assert_eq!(program, ProgramId(0));
assert_eq!(
tracks.iter().map(|t| t.track_id).collect::<Vec<_>>(),
vec![TrackId::new(1), TrackId::new(2)],
"the complete track set, in track-id order"
);
assert_eq!(video_size(&tracks[0]), (1920, 1080));
assert_eq!(
video_size(&tracks[1]),
(0, 0),
"the other track is untouched"
);
}
other => panic!("expected TracksChanged, got {other:?}"),
}
assert!(tracker.poll().is_none());
tracker.handle(DemuxEvent::TrackUpdated(track_spec_sized(
TrackId::new(9),
1,
1,
)));
assert!(tracker.poll().is_none());
}
#[test]
fn track_updated_before_resolution_replaces_the_buffered_spec() {
let mut tracker = ProgramTracker::new();
tracker.handle(DemuxEvent::TrackAdded(track_spec_sized(
TrackId::new(1),
640,
360,
)));
tracker.handle(DemuxEvent::TrackUpdated(track_spec_sized(
TrackId::new(1),
1280,
720,
)));
tracker.handle(DemuxEvent::tracks_resolved(0));
let _established = tracker.poll();
match tracker.poll() {
Some(SessionEvent::NewProgram { tracks, .. }) => {
assert_eq!(tracks.len(), 1);
assert_eq!(video_size(&tracks[0]), (1280, 720));
}
other => panic!("expected NewProgram, got {other:?}"),
}
}
#[test]
fn removed_track_stops_routing_its_samples() {
let mut tracker = ProgramTracker::new();
tracker.handle(DemuxEvent::TrackAdded(track_spec(TrackId::new(1))));
tracker.handle(DemuxEvent::tracks_resolved(0));
let _established = tracker.poll();
let _new_program = tracker.poll();
tracker.handle(DemuxEvent::sample(TrackId::new(1), sample_at(0xAA)));
assert!(matches!(tracker.poll(), Some(SessionEvent::Sample { .. })));
tracker.handle(DemuxEvent::track_removed(
TrackId::new(1),
Default::default(),
));
tracker.handle(DemuxEvent::sample(TrackId::new(1), sample_at(0xBB)));
assert!(
tracker.poll().is_none(),
"a removed track's samples must no longer be routed"
);
}
#[test]
fn mpts_two_programmes_produce_two_program_ids() {
let mut tracker = ProgramTracker::new();
let mut spec1 = track_spec(TrackId::new(1));
spec1.program_number = Some(ProgramNumber::new(100));
tracker.handle(DemuxEvent::TrackAdded(spec1));
let mut spec2 = track_spec(TrackId::new(2));
spec2.program_number = Some(ProgramNumber::new(200));
tracker.handle(DemuxEvent::TrackAdded(spec2));
tracker.handle(DemuxEvent::tracks_resolved(0));
let _established = tracker.poll();
let mut new_programs = Vec::new();
while let Some(event) = tracker.poll() {
if let SessionEvent::NewProgram { program, tracks } = event {
new_programs.push((program, tracks.len()));
}
}
assert_eq!(new_programs.len(), 2, "MPTS: two distinct programmes");
assert_ne!(new_programs[0].0, new_programs[1].0, "distinct ProgramIds");
assert_eq!(new_programs[0].1, 1, "each programme has one track");
assert_eq!(new_programs[1].1, 1, "each programme has one track");
}
#[test]
fn mpts_mid_stream_track_joins_correct_programme() {
let mut tracker = ProgramTracker::new();
let mut spec1 = track_spec(TrackId::new(1));
spec1.program_number = Some(ProgramNumber::new(100));
tracker.handle(DemuxEvent::TrackAdded(spec1));
let mut spec2 = track_spec(TrackId::new(2));
spec2.program_number = Some(ProgramNumber::new(200));
tracker.handle(DemuxEvent::TrackAdded(spec2));
tracker.handle(DemuxEvent::tracks_resolved(0));
let _established = tracker.poll();
let mut programs: HashMap<ProgramNumber, ProgramId> = HashMap::new();
while let Some(event) = tracker.poll() {
if let SessionEvent::NewProgram { program, tracks } = event
&& let Some(t) = tracks.first()
&& let Some(pn) = t.program_number
{
programs.insert(pn, program);
}
}
let mut spec3 = track_spec(TrackId::new(3));
spec3.program_number = Some(ProgramNumber::new(100));
tracker.handle(DemuxEvent::TrackAdded(spec3));
tracker.handle(DemuxEvent::tracks_resolved(1));
let prog100 = programs[&ProgramNumber::new(100)];
let prog200 = programs[&ProgramNumber::new(200)];
let mut saw_tracks_changed_100 = false;
while let Some(event) = tracker.poll() {
if let SessionEvent::TracksChanged { program, tracks } = event {
assert_ne!(
program, prog200,
"programme 200 must NOT get a spurious TracksChanged"
);
if program == prog100 {
assert_eq!(tracks.len(), 2, "both tracks in programme 100");
let ids: Vec<TrackId> = tracks.iter().map(|t| t.track_id).collect();
assert!(ids.contains(&TrackId::new(1)));
assert!(ids.contains(&TrackId::new(3)));
saw_tracks_changed_100 = true;
}
}
}
assert!(
saw_tracks_changed_100,
"must see TracksChanged for programme 100"
);
tracker.handle(DemuxEvent::sample(TrackId::new(3), sample_at(0xCC)));
let sample_event = tracker.poll();
match sample_event {
Some(SessionEvent::Sample {
program, track_id, ..
}) => {
assert_eq!(program, prog100);
assert_eq!(track_id, TrackId::new(3));
}
other => panic!("expected Sample routed to programme 100, got {other:?}"),
}
}
#[test]
fn spts_none_programme_works_as_before() {
let mut tracker = ProgramTracker::new();
tracker.handle(DemuxEvent::TrackAdded(track_spec(TrackId::new(1))));
tracker.handle(DemuxEvent::TrackAdded(track_spec(TrackId::new(2))));
tracker.handle(DemuxEvent::tracks_resolved(0));
let _established = tracker.poll();
match tracker.poll() {
Some(SessionEvent::NewProgram { program, tracks }) => {
assert_eq!(program, ProgramId(0));
assert_eq!(tracks.len(), 2);
}
other => panic!("expected single NewProgram with 2 tracks, got {other:?}"),
}
let mut spec3 = track_spec(TrackId::new(3));
spec3.program_number = None;
tracker.handle(DemuxEvent::TrackAdded(spec3));
tracker.handle(DemuxEvent::tracks_resolved(1));
match tracker.poll() {
Some(SessionEvent::TracksChanged { program, tracks }) => {
assert_eq!(program, ProgramId(0));
assert_eq!(tracks.len(), 3, "all 3 tracks in TracksChanged");
}
other => panic!("expected TracksChanged for SPTS, got {other:?}"),
}
}
}