use std::collections::VecDeque;
use std::path::PathBuf;
use std::time::Duration;
use broadcast_common::{Demand, Stage, Timestamp, Unpackage};
use container_probe::{Format, IsobmffLayout, Probe};
use thiserror::Error;
use transmux::{
DemuxEvent, Fmp4Demux, Media, ProgressiveDemux, PsDemux, Sample, StreamingFlvDemux,
StreamingTsDemux, Track, TrackId, TrackSpec, WebmDemux,
};
use crate::ingress::{IngestSession, ProgramId, SessionEvent};
use crate::trunk::RetentionClass;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum FileReaderError {
#[error("failed to read file {path:?}: {source}")]
Read {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("container ambiguous — candidates: {candidates:?}")]
AmbiguousProbe {
candidates: Vec<String>,
},
#[error(
"file is too short to identify: {file_bytes} bytes, and the probe needs at least {need_at_least}"
)]
FileTooShortToIdentify {
need_at_least: usize,
file_bytes: usize,
},
#[error("unknown/unsupported container")]
UnknownProbe,
#[error("ISOBMFF layout undetermined — cannot pick a demuxer without guessing")]
UndeterminedLayout,
#[error("format {format} is identified but not supported as a playout source")]
UnsupportedFormat {
format: &'static str,
},
#[error("demux failed: {0}")]
Demux(#[from] transmux::Error),
#[error("flv demux failed: {0}")]
Flv(#[from] transmux::flv::FlvError),
#[error("the configured trunk's writer is already claimed")]
WriterUnavailable,
#[error("path {path:?} is not a regular file (kind: {kind})")]
NotARegularFile {
path: PathBuf,
kind: &'static str,
},
#[error("file {path:?} is {size} bytes, over the {max} byte cap")]
FileTooLarge {
path: PathBuf,
size: u64,
max: usize,
},
#[error("file yielded no playable (timed) samples")]
NoPlayableSamples,
#[error("loop offset overflow applying per-track PTS offset")]
OffsetOverflow,
#[error("sample pacing time is not representable as a wall-clock duration: {value}")]
PacingOverflow { value: f64 },
#[error("file reader cancelled")]
Cancelled,
}
#[derive(Debug, Clone, Copy)]
pub struct PassRange {
pub span_ticks: i64,
pub frame_ticks: i64,
pub span_secs: f64,
pub frame_secs: f64,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DemuxerKind {
StreamingTs,
Fmp4,
Progressive,
Webm,
Ps,
StreamingFlv,
}
impl DemuxerKind {
pub fn name(&self) -> &'static str {
match self {
DemuxerKind::StreamingTs => "StreamingTs",
DemuxerKind::Fmp4 => "Fmp4",
DemuxerKind::Progressive => "Progressive",
DemuxerKind::Webm => "Webm",
DemuxerKind::Ps => "Ps",
DemuxerKind::StreamingFlv => "StreamingFlv",
}
}
}
broadcast_common::impl_spec_display!(DemuxerKind);
pub struct PublishItem {
pub track_ref: usize,
pub sample: Sample,
pub due_seconds: f64,
pub decode_seconds: f64,
}
pub struct ParsedFile {
pub tracks: Vec<Track>,
}
impl ParsedFile {
pub fn build_playout(&self) -> (Vec<TrackSpec>, Vec<PublishItem>, Vec<PassRange>) {
let specs: Vec<TrackSpec> = self.tracks.iter().map(|t| t.spec.clone()).collect();
let mut per_track: Vec<std::collections::VecDeque<PublishItem>> = Vec::new();
let mut ranges: Vec<PassRange> = Vec::new();
for (ti, track) in self.tracks.iter().enumerate() {
let timescale = track.spec.timescale.max(1) as f64;
let mut pres_pts: Vec<i64> = track.samples.iter().filter_map(|s| s.pts).collect();
if pres_pts.is_empty() {
ranges.push(PassRange {
span_ticks: 0,
frame_ticks: 1,
span_secs: 0.0,
frame_secs: 0.0,
});
per_track.push(std::collections::VecDeque::new());
continue;
}
pres_pts.sort_unstable();
pres_pts.dedup();
let first_pts = pres_pts[0];
let max_pts = pres_pts.last().copied().unwrap_or(first_pts);
let span_ticks = max_pts.checked_sub(first_pts).unwrap_or(0);
let mut frame = 0i64;
for pair in pres_pts.windows(2) {
let d = match pair[1].checked_sub(pair[0]) {
Some(d) => d,
None => continue,
};
if d > 0 && (frame == 0 || d < frame) {
frame = d;
}
}
if frame == 0 {
frame = track
.samples
.iter()
.filter_map(|s| s.duration.map(i64::from).filter(|&d| d > 0))
.next()
.unwrap_or(1);
}
let timescale_secs = 1.0f64 / timescale;
ranges.push(PassRange {
span_ticks,
frame_ticks: frame,
span_secs: span_ticks as f64 * timescale_secs,
frame_secs: frame as f64 * timescale_secs,
});
let mut mine: Vec<(f64, f64, Sample)> = track
.samples
.iter()
.filter_map(|s| {
let pts = s.pts?;
let dts = s.dts.or(s.pts)?;
Some((dts as f64 / timescale, pts as f64 / timescale, s.clone()))
})
.collect();
mine.sort_by(|a, b| a.0.total_cmp(&b.0));
per_track.push(
mine.into_iter()
.map(|(decode_seconds, due_seconds, sample)| PublishItem {
track_ref: ti,
sample,
due_seconds,
decode_seconds,
})
.collect(),
);
}
let mut order: Vec<PublishItem> = Vec::new();
loop {
let mut best: Option<(usize, f64, usize, f64)> = None;
for (ti, q) in per_track.iter().enumerate() {
let Some(front) = q.front() else { continue };
let key = (ti, front.due_seconds, front.track_ref, front.decode_seconds);
let better = match &best {
None => true,
Some((_, due, tref, dec)) => key
.1
.total_cmp(due)
.then(key.2.cmp(tref))
.then(key.3.total_cmp(dec))
.is_lt(),
};
if better {
best = Some(key);
}
}
let Some((bi, ..)) = best else { break };
let Some(item) = per_track[bi].pop_front() else {
break;
};
order.push(item);
}
(specs, order, ranges)
}
}
pub fn select_demuxer(
format: Format,
detail: container_probe::Detail,
) -> Result<DemuxerKind, FileReaderError> {
match format {
Format::MpegTs => Ok(DemuxerKind::StreamingTs),
Format::Isobmff => {
let layout = match detail {
container_probe::Detail::Isobmff { layout, .. } => layout,
_ => return Err(FileReaderError::UndeterminedLayout),
};
match layout {
IsobmffLayout::Fragmented => Ok(DemuxerKind::Fmp4),
IsobmffLayout::Progressive => Ok(DemuxerKind::Progressive),
IsobmffLayout::Unknown => Err(FileReaderError::UndeterminedLayout),
_ => Err(FileReaderError::UndeterminedLayout),
}
}
Format::Matroska | Format::WebM => Ok(DemuxerKind::Webm),
Format::MpegPs => Ok(DemuxerKind::Ps),
Format::Flv => Ok(DemuxerKind::StreamingFlv),
Format::Mxf => Err(FileReaderError::UnsupportedFormat { format: "Mxf" }),
Format::Wav => Err(FileReaderError::UnsupportedFormat { format: "Wav" }),
Format::Ogg => Err(FileReaderError::UnsupportedFormat { format: "Ogg" }),
Format::Asf => Err(FileReaderError::UnsupportedFormat { format: "Asf" }),
Format::AdtsAac => Err(FileReaderError::UnsupportedFormat { format: "AdtsAac" }),
Format::Mp3 => Err(FileReaderError::UnsupportedFormat { format: "Mp3" }),
Format::AnnexB => Err(FileReaderError::UnsupportedFormat { format: "AnnexB" }),
other => Err(FileReaderError::UnsupportedFormat {
format: other.name(),
}),
}
}
pub fn probe_and_demux(bytes: &[u8]) -> Result<ParsedFile, FileReaderError> {
match container_probe::probe_with_budget(bytes, bytes.len()) {
Probe::Identified { format, detail, .. } => demux_identified(bytes, format, detail),
Probe::Ambiguous { candidates, .. } => Err(FileReaderError::AmbiguousProbe {
candidates: candidates
.iter()
.map(|c| c.format.name().to_string())
.collect(),
}),
Probe::Insufficient { need_at_least, .. } => Err(FileReaderError::FileTooShortToIdentify {
need_at_least,
file_bytes: bytes.len(),
}),
_ => Err(FileReaderError::UnknownProbe),
}
}
pub fn demux_identified(
bytes: &[u8],
format: Format,
detail: container_probe::Detail,
) -> Result<ParsedFile, FileReaderError> {
match select_demuxer(format, detail)? {
DemuxerKind::StreamingTs => demux_streaming_ts(bytes)
.and_then(media_into_parsed)
.map_err(map_demux_error),
DemuxerKind::Fmp4 => {
let mut d = Fmp4Demux::new();
Ok(ParsedFile {
tracks: d.unpackage(bytes)?.tracks,
})
}
DemuxerKind::Progressive => {
let mut d = ProgressiveDemux::new(bytes.len().max(1))?;
Ok(ParsedFile {
tracks: d.unpackage(bytes)?.tracks,
})
}
DemuxerKind::Webm => {
let mut d = WebmDemux::new();
Ok(ParsedFile {
tracks: d.unpackage(bytes)?.tracks,
})
}
DemuxerKind::Ps => {
let mut d = PsDemux::new();
Ok(ParsedFile {
tracks: d.unpackage(bytes)?.tracks,
})
}
DemuxerKind::StreamingFlv => demux_streaming_flv(bytes)
.and_then(media_into_parsed)
.map_err(map_demux_error),
}
}
fn media_into_parsed(media: Media) -> Result<ParsedFile, DemuxError> {
Ok(ParsedFile {
tracks: media.tracks,
})
}
enum DemuxError {
Transmux(transmux::Error),
Flv(transmux::flv::FlvError),
}
impl From<transmux::Error> for DemuxError {
fn from(e: transmux::Error) -> Self {
DemuxError::Transmux(e)
}
}
fn map_demux_error(e: DemuxError) -> FileReaderError {
match e {
DemuxError::Transmux(inner) => FileReaderError::Demux(inner),
DemuxError::Flv(inner) => FileReaderError::Flv(inner),
}
}
fn demux_streaming_ts(bytes: &[u8]) -> Result<Media, DemuxError> {
let mut demux = StreamingTsDemux::new();
demux.feed(bytes);
demux.finish();
collect_streaming_media(&mut demux, |d| d.poll_event())
}
fn demux_streaming_flv(bytes: &[u8]) -> Result<Media, DemuxError> {
let mut demux = StreamingFlvDemux::new();
demux.feed(bytes).map_err(DemuxError::Flv)?;
demux.finish();
collect_streaming_media(&mut demux, |d| d.poll_event())
}
fn collect_streaming_media<D, F>(demux: &mut D, mut poll: F) -> Result<Media, DemuxError>
where
F: FnMut(&mut D) -> Option<DemuxEvent>,
{
let mut tracks: Vec<(TrackSpec, Vec<Sample>)> = Vec::new();
let mut index: std::collections::HashMap<TrackId, usize> = std::collections::HashMap::new();
while let Some(event) = poll(demux) {
match event {
DemuxEvent::TrackAdded(spec) => {
match index.get(&spec.track_id) {
Some(&i) => tracks[i].0 = spec,
None => {
let idx = tracks.len();
index.insert(spec.track_id, idx);
tracks.push((spec, Vec::new()));
}
}
}
DemuxEvent::TrackUpdated(spec) => {
if let Some(&i) = index.get(&spec.track_id) {
tracks[i].0 = spec;
}
}
DemuxEvent::Sample {
track_id, sample, ..
} => {
if let Some(&i) = index.get(&track_id) {
tracks[i].1.push(sample);
}
}
DemuxEvent::TrackRemoved { .. }
| DemuxEvent::TrackAbandoned { .. }
| DemuxEvent::ClockReference { .. }
| DemuxEvent::Discontinuity { .. }
| DemuxEvent::InputDegraded { .. }
| DemuxEvent::TracksResolved { .. } => {}
_ => {}
}
}
let movie: Vec<Track> = tracks
.into_iter()
.map(|(spec, samples)| Track::new(spec, samples))
.collect();
Ok(Media::new(movie, MPEG_TS_MOVIE_TIMESCALE))
}
const MPEG_TS_MOVIE_TIMESCALE: u32 = 90_000;
pub fn secs_to_duration(value: f64) -> Result<Duration, FileReaderError> {
Duration::try_from_secs_f64(value).map_err(|_| FileReaderError::PacingOverflow { value })
}
pub fn advance_offsets(offsets: &mut [i64], ranges: &[PassRange]) -> Result<(), FileReaderError> {
for (o, r) in offsets.iter_mut().zip(ranges) {
let step = r.span_ticks.checked_add(r.frame_ticks);
let Some(step) = step else {
return Err(FileReaderError::OffsetOverflow);
};
let Some(next) = o.checked_add(step) else {
return Err(FileReaderError::OffsetOverflow);
};
*o = next;
}
Ok(())
}
pub struct FileIngestSession {
program: ProgramId,
tracks: std::sync::Arc<[TrackSpec]>,
order: Vec<PublishItem>,
ranges: Vec<PassRange>,
offsets: Vec<i64>,
track_ids: Vec<TrackId>,
file_start_secs: f64,
queue: VecDeque<(u32, Sample, f64)>,
pass_wall_start: Option<std::time::Instant>,
pace: bool,
loop_file: bool,
established: bool,
announced: bool,
started: bool,
drained_this_feed: usize,
}
impl FileIngestSession {
pub fn new(
parsed: ParsedFile,
program: ProgramId,
loop_file: bool,
pace: bool,
) -> Result<Self, FileReaderError> {
let (specs, order, ranges) = parsed.build_playout();
if order.is_empty() {
return Err(FileReaderError::NoPlayableSamples);
}
let file_start_secs = order
.iter()
.map(|i| i.due_seconds)
.fold(f64::INFINITY, f64::min);
let track_ids: Vec<TrackId> = specs.iter().map(|t| t.track_id).collect();
let mut session = FileIngestSession {
program,
tracks: specs.into(),
order,
ranges,
offsets: vec![0i64; track_ids.len()],
track_ids,
file_start_secs,
queue: VecDeque::new(),
pass_wall_start: None,
pace,
loop_file,
established: false,
announced: false,
started: false,
drained_this_feed: 0,
};
session.refill()?;
Ok(session)
}
pub fn drained(&self) -> bool {
self.started && !self.loop_file && self.queue.is_empty()
}
pub fn next_due(&self) -> Option<std::time::Instant> {
let rel = self.queue.front()?.2;
let base = self.pass_wall_start?;
let dur = secs_to_duration(rel).ok()?;
base.checked_add(dur).or_else(|| {
std::time::Instant::now().checked_sub(Duration::ZERO)
})
}
fn refill(&mut self) -> Result<(), FileReaderError> {
let pass_duration_secs = self
.ranges
.iter()
.map(|r| r.span_secs + r.frame_secs)
.fold(0.0f64, f64::max);
if self.pace
&& let Some(base) = &mut self.pass_wall_start
{
*base = base
.checked_add(secs_to_duration(pass_duration_secs)?)
.ok_or(FileReaderError::PacingOverflow {
value: pass_duration_secs,
})?;
}
self.queue.clear();
for item in &self.order {
let mut sample = item.sample.clone();
let off = self.offsets[item.track_ref];
if let Some(p) = sample.pts {
sample.pts = Some(p.checked_add(off).ok_or(FileReaderError::OffsetOverflow)?);
}
if let Some(d) = sample.dts {
sample.dts = Some(d.checked_add(off).ok_or(FileReaderError::OffsetOverflow)?);
}
let within_pass = (item.due_seconds - self.file_start_secs).max(0.0);
self.queue
.push_back((self.track_ids[item.track_ref].value(), sample, within_pass));
}
advance_offsets(&mut self.offsets, &self.ranges)?;
Ok(())
}
}
impl Stage for FileIngestSession {
type In<'a> = ();
type Out = SessionEvent;
type Error = FileReaderError;
fn feed(&mut self, _input: (), _now: Timestamp) -> Result<(), Self::Error> {
self.drained_this_feed = 0;
if self.loop_file && self.queue.is_empty() {
self.refill()?;
}
Ok(())
}
fn poll(&mut self) -> Option<SessionEvent> {
if !self.established {
self.established = true;
return Some(SessionEvent::Established);
}
if !self.announced {
self.announced = true;
return Some(SessionEvent::NewProgram {
program: self.program,
tracks: self.tracks.to_vec(),
});
}
if !self.started {
self.started = true;
if self.pace {
self.pass_wall_start = Some(std::time::Instant::now());
}
return None;
}
if self.pace {
if self.drained_this_feed >= MAX_SAMPLES_PER_DRAIN {
return None;
}
let rel = self.queue.front()?.2;
let base = self.pass_wall_start?;
match secs_to_duration(rel) {
Ok(dur) if base.elapsed() < dur => return None,
Ok(_) | Err(_) => {}
}
}
self.drained_this_feed = self.drained_this_feed.saturating_add(1);
self.queue
.pop_front()
.map(|(track_id, sample, _)| SessionEvent::Sample {
program: self.program,
track_id: TrackId::new(track_id),
retention: RetentionClass::Timed,
sample,
})
}
fn finish(&mut self) -> Result<(), Self::Error> {
Ok(())
}
fn demand(&self) -> Demand {
Demand::new(0)
}
}
impl IngestSession for FileIngestSession {
type Request = bytes::Bytes;
}
pub const MAX_SAMPLES_PER_DRAIN: usize = 256;
pub const FILE_TIMED_RING: usize = MAX_SAMPLES_PER_DRAIN * 4;
#[cfg(test)]
mod tests {
use super::*;
use transmux::avc_config::{AVCConfigurationBox, AVCDecoderConfigurationRecord};
fn demux_fixture() -> ParsedFile {
let bytes = std::fs::read(test_fixtures::path("ts/h264-aac.ts")).expect("read fixture");
probe_and_demux(&bytes).expect("demux fixture")
}
fn drain_dts(sess: &mut FileIngestSession) -> Vec<(TrackId, i64)> {
let mut dts = Vec::new();
while let Some(ev) = sess.poll() {
if let SessionEvent::Sample {
track_id, sample, ..
} = ev
&& let Some(d) = sample.dts
{
dts.push((track_id, d));
}
}
dts
}
#[test]
fn loop_refill_keeps_dts_monotonic_across_loop_point() {
let parsed = demux_fixture();
let mut sess = FileIngestSession::new(parsed, ProgramId(0), true, false)
.expect("fixture must have playable samples");
let _ = sess.feed((), broadcast_common::Timestamp::from_nanos(0));
let _setup = drain_dts(&mut sess);
let pass0 = drain_dts(&mut sess);
assert!(!pass0.is_empty(), "pass 0 must emit samples");
let _ = sess.feed((), broadcast_common::Timestamp::from_nanos(0));
let pass1 = drain_dts(&mut sess);
assert_eq!(
pass0.len(),
pass1.len(),
"each pass emits the same sample count"
);
assert!(!pass1.is_empty(), "a looped pass must emit samples");
let both: Vec<(TrackId, i64)> = pass0.into_iter().chain(pass1).collect();
let mut by_track: std::collections::HashMap<TrackId, Vec<i64>> =
std::collections::HashMap::new();
for (track_id, d) in &both {
by_track.entry(*track_id).or_default().push(*d);
}
for (track_id, seq) in &by_track {
for pair in seq.windows(2) {
assert!(
pair[1] > pair[0],
"DTS must be strictly monotonic across the loop point (track {track_id}): {} then {}",
pair[0],
pair[1]
);
}
}
}
#[test]
fn no_loop_drains_once_then_stops() {
let parsed = demux_fixture();
let mut sess = FileIngestSession::new(parsed, ProgramId(0), false, false)
.expect("fixture must have playable samples");
let _ = sess.feed((), broadcast_common::Timestamp::from_nanos(0));
let _setup = drain_dts(&mut sess);
let pass0 = drain_dts(&mut sess);
assert!(!pass0.is_empty());
let _ = sess.feed((), broadcast_common::Timestamp::from_nanos(0));
assert_eq!(drain_dts(&mut sess).len(), 0, "loop:false must not refill");
}
#[test]
fn queue_length_stays_bounded_across_passes() {
let parsed = demux_fixture();
let mut sess = FileIngestSession::new(parsed, ProgramId(0), true, false)
.expect("fixture must have playable samples");
let first = sess.queue.len();
assert!(first > 0, "pass 0 fills the queue");
let _ = sess.feed((), broadcast_common::Timestamp::from_nanos(0));
let _ = drain_dts(&mut sess);
let _ = drain_dts(&mut sess);
for _ in 0..5 {
let _ = drain_dts(&mut sess); let _ = sess.feed((), broadcast_common::Timestamp::from_nanos(0)); let after = sess.queue.len();
assert_eq!(
after, first,
"queue length must not grow per pass: after a pass it holds {} not {}",
after, first
);
}
}
fn avc_spec(track_id: TrackId, timescale: u32) -> TrackSpec {
TrackSpec::new(
track_id,
timescale,
transmux::CodecConfig::Avc {
config: AVCConfigurationBox {
config: AVCDecoderConfigurationRecord {
configuration_version: 1,
profile_indication: 66,
profile_compatibility: 0,
level_indication: 30,
length_size_minus_one: 3,
sps: vec![],
pps: vec![],
chroma_format: None,
bit_depth_luma_minus8: None,
bit_depth_chroma_minus8: None,
sps_ext: vec![],
},
},
width: 1920,
height: 1080,
},
)
}
fn one_track_file(pts: &[i64]) -> ParsedFile {
let samples = pts
.iter()
.map(|&p| Sample::new(vec![0], Some(p), Some(p), Some(10), true))
.collect();
ParsedFile {
tracks: vec![Track::new(avc_spec(TrackId::new(1), 90_000), samples)],
}
}
#[test]
fn negative_pts_merge_ascending() {
let parsed = one_track_file(&[-10, 0, -5, 5]);
let (_, order, _) = parsed.build_playout();
let due: Vec<i64> = order
.iter()
.map(|i| (i.due_seconds * 90_000.0).round() as i64)
.collect();
assert_eq!(
due,
vec![-10, -5, 0, 5],
"presentation order must be ascending, negatives included"
);
}
#[test]
fn zero_duration_falls_back_to_unit_frame() {
let samples = vec![Sample::new(vec![0], Some(0), Some(0), Some(0), true)];
let parsed = ParsedFile {
tracks: vec![Track::new(avc_spec(TrackId::new(1), 90_000), samples)],
};
let (_, _, ranges) = parsed.build_playout();
assert_eq!(
ranges[0].frame_ticks, 1,
"a zero duration must fall back to a unit frame, never 0"
);
}
#[test]
fn advance_offsets_overflow_fails_cleanly() {
let mut offsets = vec![i64::MAX - 5];
let ranges = vec![PassRange {
span_ticks: 100,
frame_ticks: 10,
span_secs: 1.0,
frame_secs: 1.0,
}];
let err = advance_offsets(&mut offsets, &ranges);
assert!(
matches!(err, Err(FileReaderError::OffsetOverflow)),
"offset past i64::MAX must be OffsetOverflow, got {err:?}"
);
}
#[test]
fn no_playable_samples_fails_session() {
let samples = vec![Sample::new(vec![0], None, None, None, false)];
let parsed = ParsedFile {
tracks: vec![Track::new(avc_spec(TrackId::new(1), 90_000), samples)],
};
let err = FileIngestSession::new(parsed, ProgramId(0), true, false);
assert!(
matches!(err, Err(FileReaderError::NoPlayableSamples)),
"a file with no timed samples must fail"
);
}
#[test]
fn a_due_burst_is_drained_in_bounded_steps_without_overflowing_the_ring() {
let parsed = demux_fixture();
let session = FileIngestSession::new(parsed, ProgramId(0), true, true).unwrap();
let mut driver = crate::ingress::IngestDriver::new(
session,
crate::IngestConfig::new(crate::ingress::HandshakePolicy::establish_by(
broadcast_common::Timestamp::from_nanos(u64::MAX),
))
.with_trunk(
crate::trunk::TrunkConfig::default()
.with_timed_capacity(std::num::NonZeroUsize::new(FILE_TIMED_RING).unwrap()),
)
.with_max_programs(std::num::NonZeroUsize::MIN),
);
driver.feed((), broadcast_common::Timestamp::ZERO);
let trunk = driver
.trunk(ProgramId(0))
.cloned()
.expect("the program was announced");
let mut cursor = trunk.subscribe();
let burst = FILE_TIMED_RING * 2;
let template = driver
.session()
.queue
.front()
.map(|(track, sample, _)| (*track, sample.clone()))
.expect("the first pass is queued");
{
let session = driver.session_mut();
session.queue.clear();
for i in 0..burst {
let mut sample = template.1.clone();
sample.dts = Some(i64::try_from(i).unwrap());
sample.pts = sample.dts;
session.queue.push_back((template.0, sample, 0.0));
}
session.pass_wall_start =
std::time::Instant::now().checked_sub(std::time::Duration::from_secs(3600));
}
let mut delivered = 0usize;
let mut feeds = 0usize;
while delivered < burst {
feeds += 1;
assert!(feeds <= burst, "no progress draining the burst");
driver.feed((), broadcast_common::Timestamp::ZERO);
let mut this_feed = 0usize;
while let Some(item) = cursor.poll() {
match item {
crate::trunk::SampleCursorItem::Timed { .. } => this_feed += 1,
other => panic!("the ring overflowed: {other:?}"),
}
}
assert!(
this_feed <= MAX_SAMPLES_PER_DRAIN,
"one drain handed out {this_feed} samples (bound {MAX_SAMPLES_PER_DRAIN})"
);
delivered += this_feed;
}
assert_eq!(delivered, burst);
assert!(
feeds >= burst / MAX_SAMPLES_PER_DRAIN,
"the burst must have taken several bounded drains, took {feeds}"
);
}
}