use crate::TrackId;
use alloc::collections::VecDeque;
use alloc::format;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use broadcast_common::{Demand, Package, Stage, Timestamp};
use broadcast_hls::{DecimalSeconds, MediaPlaylist, MediaSegment};
use crate::error::{Error, Result};
use crate::media::{Media, Track};
use crate::nal::{NalCodec, access_unit_is_idr};
use crate::pipeline::{CodecConfig, Sample, TrackSpec};
use crate::segment_cut::SegmentCutPolicy;
use crate::segmenter::{
MAX_PENDING_SAMPLES_PER_TRACK, MediaClock, choose_anchor, no_sync_sample_error,
};
use crate::ts_mux::{TsContinuity, mux_tracks_at_continuing};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum UpdateTrackOutcome {
Changed,
Unchanged,
Unknown,
}
const DEFAULT_HLS_VERSION: u8 = 3;
#[cfg(test)]
std::thread_local! {
pub(crate) static PACKAGE_TICK_CALLS: core::cell::Cell<usize> =
const { core::cell::Cell::new(0) };
}
#[cfg(test)]
#[inline]
fn count_tick() {
PACKAGE_TICK_CALLS.with(|c| c.set(c.get() + 1));
}
#[derive(Debug, Clone)]
pub struct TsHlsOutput {
pub segments: Vec<Vec<u8>>,
pub playlist: String,
}
#[derive(Debug, Clone)]
pub struct TsHlsPackager {
pub target_secs: u32,
pub version: u8,
pub media_sequence: u64,
pub uri_prefix: String,
}
impl Default for TsHlsPackager {
fn default() -> Self {
Self::new(6)
}
}
impl TsHlsPackager {
pub fn new(target_secs: u32) -> Self {
Self {
target_secs: target_secs.max(1),
version: DEFAULT_HLS_VERSION,
media_sequence: 0,
uri_prefix: String::from("seg"),
}
}
}
struct SegmentRanges {
ranges: Vec<core::ops::Range<usize>>,
}
impl Package for TsHlsPackager {
type Media = Media;
type Output = TsHlsOutput;
type Error = Error;
fn package(&mut self, media: &Media) -> Result<TsHlsOutput> {
if media.tracks.is_empty() {
return Err(Error::InvalidInput("cannot package a Media with no tracks"));
}
let anchor = choose_anchor(media.tracks.iter().map(|t| &t.spec.config))?;
let target_ticks = self.anchor_target_ticks(&media.tracks[anchor]);
let boundaries = anchor_segment_boundaries(&media.tracks[anchor].samples, target_ticks);
let segments = partition_tracks(&media.tracks, anchor, &boundaries);
let mut continuity = TsContinuity::new();
let mut ts_segments: Vec<Vec<u8>> = Vec::with_capacity(segments.len());
let mut playlist_segments: Vec<MediaSegment> = Vec::with_capacity(segments.len());
let mut target_duration: u32 = 0;
let mut clocks: Vec<MediaClock> = alloc::vec![MediaClock::new(); media.tracks.len()];
let mut base_dts: Vec<u64> = alloc::vec![0u64; media.tracks.len()];
let mut cursor: Vec<usize> = alloc::vec![0usize; media.tracks.len()];
for (i, seg) in segments.iter().enumerate() {
for (ti, track) in media.tracks.iter().enumerate() {
let start = seg.ranges[ti].start.max(cursor[ti]);
while cursor[ti] < start {
#[cfg(test)]
count_tick();
base_dts[ti] += clocks[ti].tick(&track.samples[cursor[ti]]);
cursor[ti] += 1;
}
}
let sample_slices: Vec<&[Sample]> = media
.tracks
.iter()
.zip(&seg.ranges)
.map(|(t, r)| &t.samples[r.clone()])
.collect();
let bytes = mux_tracks_at_continuing(
&media.tracks,
&sample_slices,
&base_dts,
&mut continuity,
)?;
ts_segments.push(bytes);
let mut ticks: Vec<u64> = alloc::vec![0u64; media.tracks.len()];
for (ti, track) in media.tracks.iter().enumerate() {
let range = seg.ranges[ti].clone();
let mut segment_ticks: u64 = 0;
for idx in cursor[ti].max(range.start)..range.end {
#[cfg(test)]
count_tick();
segment_ticks += clocks[ti].tick(&track.samples[idx]);
}
base_dts[ti] += segment_ticks;
cursor[ti] = cursor[ti].max(range.end);
ticks[ti] = segment_ticks;
}
let anchor_ticks = ticks[anchor];
let ts_scale = media.tracks[anchor].spec.timescale.max(1) as u64;
let (duration, ceil_secs) = segment_duration_secs(anchor_ticks, ts_scale);
if ceil_secs > target_duration {
target_duration = ceil_secs;
}
playlist_segments.push(MediaSegment {
uri: format!("{}{}.ts", self.uri_prefix, i),
duration: DecimalSeconds::new(duration)
.expect("segment_duration_secs is finite, >= 0"),
discontinuous: false,
parts: vec![],
..Default::default()
});
}
let playlist = MediaPlaylist {
version: self.version,
target_duration: target_duration.max(1),
media_sequence: self.media_sequence,
discontinuity_sequence: 0,
segments: playlist_segments,
endlist: true,
extra_tags: vec![],
low_latency: None,
iframes_only: false,
open_segment: None,
..Default::default()
}
.to_m3u8()?;
Ok(TsHlsOutput {
segments: ts_segments,
playlist,
})
}
}
impl TsHlsPackager {
fn anchor_target_ticks(&self, anchor: &Track) -> u64 {
target_ticks_for(self.target_secs, anchor.spec.timescale)
}
}
fn placement_secs(sample: &Sample, acc_ticks: u64, timescale: u64, origin_secs: f64) -> f64 {
let timescale = timescale.max(1) as f64;
match (sample.duration, sample.dts) {
(None, Some(dts)) => (dts.max(0) as f64 / timescale) - origin_secs,
_ => acc_ticks as f64 / timescale,
}
}
fn timeline_origin_secs(anchor_first_dts: Option<i64>, anchor_scale: u64) -> f64 {
anchor_first_dts.map_or(0.0, |d| d.max(0) as f64 / anchor_scale.max(1) as f64)
}
fn target_ticks_for(target_secs: u32, ts_scale: u32) -> u64 {
(target_secs.max(1) as u64) * (ts_scale.max(1) as u64)
}
fn segment_duration_secs(anchor_ticks: u64, ts_scale: u64) -> (f64, u32) {
let ts_scale = ts_scale.max(1);
let duration = anchor_ticks as f64 / ts_scale as f64;
let ceil_secs = anchor_ticks.div_ceil(ts_scale) as u32;
(duration, ceil_secs)
}
fn is_cut_point(has_pending: bool, is_sync: bool, buffered_ticks: u64, target_ticks: u64) -> bool {
has_pending && is_sync && buffered_ticks >= target_ticks
}
fn anchor_segment_boundaries(samples: &[Sample], target_ticks: u64) -> Vec<usize> {
let mut starts = vec![0usize];
if samples.is_empty() {
return starts;
}
let mut clock = MediaClock::new();
let mut buffered: u64 = 0;
for (i, s) in samples.iter().enumerate() {
if is_cut_point(i > 0, s.flags.is_sync, buffered, target_ticks) {
starts.push(i);
buffered = 0;
}
buffered += clock.tick(s);
}
starts
}
fn partition_tracks(
tracks: &[Track],
anchor: usize,
anchor_boundaries: &[usize],
) -> Vec<SegmentRanges> {
let n_segs = anchor_boundaries.len();
let anchor_samples = &tracks[anchor].samples;
let anchor_scale = tracks[anchor].spec.timescale.max(1) as u64;
let origin_secs =
timeline_origin_secs(anchor_samples.first().and_then(|s| s.dts), anchor_scale);
let mut start_times: Vec<f64> = Vec::with_capacity(n_segs);
{
let mut clock = MediaClock::new();
let mut acc: u64 = 0;
let mut cursor = 0usize;
for &b in anchor_boundaries {
while cursor < b {
acc += clock.tick(&anchor_samples[cursor]);
cursor += 1;
}
start_times.push(acc as f64 / anchor_scale as f64);
}
}
let mut out: Vec<SegmentRanges> = (0..n_segs)
.map(|_| SegmentRanges {
ranges: vec![0..0; tracks.len()],
})
.collect();
for (t_idx, track) in tracks.iter().enumerate() {
if t_idx == anchor {
for (seg, &start) in anchor_boundaries.iter().enumerate() {
let end = if seg + 1 < n_segs {
anchor_boundaries[seg + 1]
} else {
anchor_samples.len()
};
out[seg].ranges[t_idx] = start..end;
}
continue;
}
let scale = track.spec.timescale.max(1) as u64;
let mut seg = 0usize;
let mut seg_start_idx = 0usize;
let mut acc_ticks: u64 = 0;
let mut clock = MediaClock::new();
for (i, s) in track.samples.iter().enumerate() {
let start_time = placement_secs(s, acc_ticks, scale, origin_secs);
while seg + 1 < n_segs && start_time >= start_times[seg + 1] {
out[seg].ranges[t_idx] = seg_start_idx..i;
seg += 1;
seg_start_idx = i;
}
acc_ticks += clock.tick(s);
}
out[seg].ranges[t_idx] = seg_start_idx..track.samples.len();
}
out
}
#[derive(Debug, Clone)]
pub struct TsSegment {
pub bytes: Vec<u8>,
pub duration: f64,
pub discontinuous: bool,
pub uri: String,
pub sequence: u64,
}
#[derive(Debug, Clone)]
struct WindowEntry {
uri: String,
duration: DecimalSeconds,
discontinuous: bool,
}
#[derive(Debug, Clone, Copy)]
struct DeferredCut {
anchor_split: usize,
closed_anchor_dur: u64,
next_start_secs: f64,
}
const CATCH_UP_MAX_SECS: u64 = 1;
struct StreamTrackState {
spec: TrackSpec,
pending: Vec<Sample>,
base_decode: u64,
flush_clock: MediaClock,
}
pub struct StreamingTsHlsSegmenter {
tracks: Vec<StreamTrackState>,
anchor: usize,
target_secs: u32,
target_ticks: u64,
anchor_pending_dur: u64,
deferred_cut: Option<DeferredCut>,
anchor_clock: MediaClock,
origin_secs: Option<f64>,
pending_discontinuity: bool,
cut_policy: SegmentCutPolicy,
awaiting_idr: bool,
discontinuity_at_resync: bool,
dropped_leading_aus: u64,
idr_wait: Option<crate::segment_cut::IdrWait>,
idr_fallback_active: bool,
finished: bool,
pub version: u8,
pub uri_prefix: String,
window: usize,
window_segments: VecDeque<WindowEntry>,
continuity: TsContinuity,
total_segments: u64,
discontinuity_sequence: u64,
target_duration: u32,
ready: VecDeque<TsSegment>,
}
fn audio_discontinuity_trigger(old: &CodecConfig, new: &CodecConfig) -> bool {
if core::mem::discriminant(old) != core::mem::discriminant(new) {
return false;
}
if old.audio_sample_rate() != new.audio_sample_rate()
|| old.audio_channel_field() != new.audio_channel_field()
{
return true;
}
match (old, new) {
(CodecConfig::Aac { esds: e1, .. }, CodecConfig::Aac { esds: e2, .. }) => {
let asc = |e: &crate::mp4esds::EsdsBox| {
e.es_descriptor
.decoder_config
.as_ref()
.and_then(|dc| dc.decoder_specific_info.as_ref())
.map(|d| d.data.clone())
};
asc(e1) != asc(e2)
}
_ => false,
}
}
impl StreamingTsHlsSegmenter {
pub fn new(tracks: Vec<TrackSpec>, target_secs: u32, window: usize) -> Result<Self> {
if tracks.is_empty() {
return Err(Error::InvalidInput(
"streaming ts-hls segmenter needs at least one track",
));
}
if window == 0 {
return Err(Error::InvalidInput("window must be >= 1"));
}
for (i, a) in tracks.iter().enumerate() {
if tracks[i + 1..].iter().any(|b| b.track_id == a.track_id) {
return Err(Error::InvalidInput("duplicate track_id"));
}
}
let anchor = choose_anchor(tracks.iter().map(|t| &t.config))?;
let target_secs = target_secs.max(1);
let target_ticks = target_ticks_for(target_secs, tracks[anchor].timescale);
let tracks = tracks
.into_iter()
.map(|spec| StreamTrackState {
spec,
pending: Vec::new(),
base_decode: 0,
flush_clock: MediaClock::new(),
})
.collect();
Ok(Self {
tracks,
anchor,
target_secs,
target_ticks,
anchor_pending_dur: 0,
deferred_cut: None,
anchor_clock: MediaClock::new(),
origin_secs: None,
pending_discontinuity: false,
cut_policy: SegmentCutPolicy::AnyRap,
awaiting_idr: false,
discontinuity_at_resync: false,
dropped_leading_aus: 0,
idr_wait: None,
idr_fallback_active: false,
finished: false,
version: DEFAULT_HLS_VERSION,
uri_prefix: String::from("seg"),
window,
window_segments: VecDeque::new(),
continuity: TsContinuity::new(),
total_segments: 0,
discontinuity_sequence: 0,
target_duration: 0,
ready: VecDeque::new(),
})
}
#[doc(hidden)]
pub fn with_start_sequence(
tracks: Vec<TrackSpec>,
target_secs: u32,
window: usize,
next_sequence: u64,
) -> Result<Self> {
let mut seg = Self::new(tracks, target_secs, window)?;
seg.total_segments = next_sequence;
Ok(seg)
}
#[must_use]
pub fn with_cut_policy(mut self, policy: SegmentCutPolicy) -> Self {
self.cut_policy = policy;
let untouched =
self.total_segments == 0 && self.tracks.iter().all(|t| t.pending.is_empty());
self.awaiting_idr = policy == SegmentCutPolicy::IdrOnly && untouched;
self
}
#[must_use]
pub fn with_idr_fallback(mut self, bound: core::time::Duration) -> Self {
self.idr_wait = Some(crate::segment_cut::IdrWait::new(bound));
self
}
pub fn fall_back_to_any_rap(&mut self) {
self.idr_fallback_active = true;
}
pub fn idr_fallback_active(&self) -> bool {
self.idr_fallback_active
}
pub fn cut_policy(&self) -> SegmentCutPolicy {
self.cut_policy
}
pub fn dropped_leading_aus(&self) -> u64 {
self.dropped_leading_aus
}
fn anchor_nal_codec(&self) -> Option<NalCodec> {
match self.tracks[self.anchor].spec.config {
CodecConfig::Avc { .. } => Some(NalCodec::Avc),
CodecConfig::Hevc { .. } => Some(NalCodec::Hevc),
CodecConfig::Vvc { .. } => Some(NalCodec::Vvc),
_ => None,
}
}
fn opens_segment(&self, sample: &Sample) -> bool {
match (self.cut_policy, self.anchor_nal_codec()) {
(SegmentCutPolicy::IdrOnly, Some(codec)) if !self.idr_fallback_active => {
access_unit_is_idr(codec, &sample.data)
}
_ => sample.flags.is_sync,
}
}
pub fn push(&mut self, track_id: TrackId, sample: Sample) -> Result<()> {
self.push_inner(track_id, sample)
}
fn push_inner(&mut self, track_id: TrackId, sample: Sample) -> Result<()> {
let idx = self
.tracks
.iter()
.position(|t| t.spec.track_id == track_id)
.ok_or(Error::InvalidInput("push: unknown track_id"))?;
if self.awaiting_idr {
if idx != self.anchor {
return Ok(());
}
if !self.opens_segment(&sample) {
self.dropped_leading_aus += 1;
let timescale = self.tracks[self.anchor].spec.timescale;
if let Some(wait) = self.idr_wait.as_mut()
&& wait.observe_dropped(&sample, timescale)
{
self.idr_fallback_active = true;
}
return Ok(());
}
self.awaiting_idr = false;
self.complete_deferred()?;
if !self.tracks[self.anchor].pending.is_empty() {
self.cut_segment(false)?;
}
if self.discontinuity_at_resync {
self.pending_discontinuity = self.total_segments > 0;
self.discontinuity_at_resync = false;
}
}
if idx == self.anchor
&& is_cut_point(
!self.tracks[self.anchor].pending.is_empty(),
self.opens_segment(&sample),
self.anchor_pending_dur,
self.target_ticks,
)
{
self.cut_or_defer()?;
}
if self.tracks[idx].pending.len() >= MAX_PENDING_SAMPLES_PER_TRACK {
return Err(no_sync_sample_error());
}
if idx == self.anchor {
if self.origin_secs.is_none() {
self.origin_secs = Some(timeline_origin_secs(
sample.dts,
self.tracks[self.anchor].spec.timescale.max(1) as u64,
));
}
self.anchor_pending_dur += self.anchor_clock.tick(&sample);
}
self.tracks[idx].pending.push(sample);
self.try_complete_deferred()
}
fn cut_or_defer(&mut self) -> Result<()> {
self.complete_deferred()?;
let anchor = self.anchor;
let anchor_scale = self.tracks[anchor].spec.timescale.max(1) as u64;
let next_start_ticks = self.tracks[anchor].base_decode + self.anchor_pending_dur;
let next_start_secs = next_start_ticks as f64 / anchor_scale as f64;
if !self.audio_lags(next_start_secs) {
return self.cut_segment(false);
}
self.deferred_cut = Some(DeferredCut {
anchor_split: self.tracks[anchor].pending.len(),
closed_anchor_dur: self.anchor_pending_dur,
next_start_secs,
});
self.anchor_pending_dur = 0;
Ok(())
}
fn complete_deferred(&mut self) -> Result<()> {
match self.deferred_cut.take() {
Some(cut) => self.cut_segment_with(false, Some(cut)),
None => Ok(()),
}
}
fn try_complete_deferred(&mut self) -> Result<()> {
let Some(cut) = self.deferred_cut else {
return Ok(());
};
let scale = self.tracks[self.anchor].spec.timescale.max(1) as u64;
let waited_out = self.anchor_pending_dur >= CATCH_UP_MAX_SECS.saturating_mul(scale);
if waited_out || !self.audio_lags(cut.next_start_secs) {
self.complete_deferred()?;
}
Ok(())
}
fn audio_lags(&self, next_start_secs: f64) -> bool {
let origin_secs = self.origin_secs.unwrap_or(0.0);
self.tracks.iter().enumerate().any(|(i, t)| {
if i == self.anchor
|| t.spec.config.audio_sample_rate().is_none()
|| (t.base_decode == 0 && t.pending.is_empty())
{
return false;
}
let scale = t.spec.timescale.max(1) as u64;
let mut acc = t.base_decode;
let mut clock = MediaClock::resumed_at(t.flush_clock.last_dts());
for s in &t.pending {
if placement_secs(s, acc, scale, origin_secs) >= next_start_secs {
return false;
}
acc += clock.tick(s);
}
true
})
}
pub fn finish(&mut self) -> Result<()> {
self.finish_inner()
}
pub fn take_ready(&mut self) -> Vec<TsSegment> {
self.ready.drain(..).collect()
}
fn finish_inner(&mut self) -> Result<()> {
self.finished = true;
self.complete_deferred()?;
if self.tracks.iter().any(|t| !t.pending.is_empty()) {
self.cut_segment(true)?;
}
Ok(())
}
pub fn mark_discontinuity(&mut self) {
if self.cut_policy == SegmentCutPolicy::IdrOnly {
self.awaiting_idr = true;
self.discontinuity_at_resync = true;
} else {
self.pending_discontinuity = true;
}
}
pub fn add_track(&mut self, spec: TrackSpec) -> Result<()> {
if self.tracks.iter().any(|t| t.spec.track_id == spec.track_id) {
return Err(Error::InvalidInput("add_track: duplicate track_id"));
}
let nothing_cut_or_buffered =
self.total_segments == 0 && self.tracks.iter().all(|t| t.pending.is_empty());
let current_anchor_is_video = self.tracks[self.anchor].spec.config.is_video();
let new_is_video = spec.config.is_video();
let new_index = self.tracks.len();
self.tracks.push(StreamTrackState {
spec,
pending: Vec::new(),
base_decode: 0,
flush_clock: MediaClock::new(),
});
if self.total_segments > 0 {
self.pending_discontinuity = true;
}
if nothing_cut_or_buffered && new_is_video && !current_anchor_is_video {
self.anchor = new_index;
self.target_ticks =
target_ticks_for(self.target_secs, self.tracks[self.anchor].spec.timescale);
}
Ok(())
}
pub fn update_track(&mut self, spec: TrackSpec) -> Result<UpdateTrackOutcome> {
let Some(idx) = self
.tracks
.iter()
.position(|t| t.spec.track_id == spec.track_id)
else {
return Ok(UpdateTrackOutcome::Unknown);
};
let old = &self.tracks[idx].spec;
let discontinuity = old.timescale != spec.timescale
|| old.display != spec.display
|| audio_discontinuity_trigger(&old.config, &spec.config);
let old_timescale = self.tracks[idx].spec.timescale;
self.tracks[idx].spec = spec;
if discontinuity {
if idx == self.anchor && self.tracks[idx].spec.timescale != old_timescale {
self.target_ticks =
target_ticks_for(self.target_secs, self.tracks[self.anchor].spec.timescale);
}
self.mark_discontinuity();
Ok(UpdateTrackOutcome::Changed)
} else {
Ok(UpdateTrackOutcome::Unchanged)
}
}
pub fn playlist(&self) -> crate::Result<String> {
let segments: Vec<MediaSegment> = self
.window_segments
.iter()
.map(|e| MediaSegment {
uri: e.uri.clone(),
duration: e.duration,
discontinuous: e.discontinuous,
parts: vec![],
..Default::default()
})
.collect();
let media_sequence = self.total_segments - self.window_segments.len() as u64;
Ok(MediaPlaylist {
version: self.version,
target_duration: self.target_duration.max(1),
media_sequence,
discontinuity_sequence: self.discontinuity_sequence,
segments,
endlist: self.finished,
extra_tags: vec![],
low_latency: None,
iframes_only: false,
open_segment: None,
..Default::default()
}
.to_m3u8()?)
}
fn cut_segment(&mut self, final_cut: bool) -> Result<()> {
self.cut_segment_with(final_cut, None)
}
fn cut_segment_with(&mut self, final_cut: bool, deferred: Option<DeferredCut>) -> Result<()> {
let closed_anchor_dur = deferred.map_or(self.anchor_pending_dur, |d| d.closed_anchor_dur);
let anchor = self.anchor;
let anchor_scale = self.tracks[anchor].spec.timescale.max(1) as u64;
let origin_secs = self.origin_secs.unwrap_or(0.0);
let next_start_ticks = self.tracks[anchor].base_decode + closed_anchor_dur;
let next_start_secs = next_start_ticks as f64 / anchor_scale as f64;
let split_at: Vec<usize> = self
.tracks
.iter()
.enumerate()
.map(|(i, t)| {
if final_cut {
return t.pending.len();
}
if i == anchor {
return deferred.map_or(t.pending.len(), |d| d.anchor_split);
}
let scale = t.spec.timescale.max(1) as u64;
let mut acc = t.base_decode;
let mut clock = MediaClock::resumed_at(t.flush_clock.last_dts());
for (j, s) in t.pending.iter().enumerate() {
let start_secs = placement_secs(s, acc, scale, origin_secs);
if start_secs >= next_start_secs {
return j;
}
acc += clock.tick(s);
}
t.pending.len()
})
.collect();
let mux_tracks: Vec<Track> = self
.tracks
.iter()
.map(|t| Track::new(t.spec.clone(), Vec::new()))
.collect();
let sample_slices: Vec<&[Sample]> = self
.tracks
.iter()
.zip(&split_at)
.map(|(t, &n)| &t.pending[..n])
.collect();
let base_dts: Vec<u64> = self.tracks.iter().map(|t| t.base_decode).collect();
let bytes =
mux_tracks_at_continuing(&mux_tracks, &sample_slices, &base_dts, &mut self.continuity)?;
let (duration, ceil_secs) = segment_duration_secs(closed_anchor_dur, anchor_scale);
if ceil_secs > self.target_duration {
self.target_duration = ceil_secs;
}
let discontinuous = self.pending_discontinuity;
self.pending_discontinuity = false;
for (t, &n) in self.tracks.iter_mut().zip(&split_at) {
let clock = &mut t.flush_clock;
let dur: u64 = t.pending[..n].iter().map(|s| clock.tick(s)).sum();
t.base_decode += dur;
t.pending.drain(..n);
}
if deferred.is_none() {
self.anchor_pending_dur = 0;
}
let sequence = self.total_segments;
let uri = format!("{}{}.ts", self.uri_prefix, sequence);
self.window_segments.push_back(WindowEntry {
uri: uri.clone(),
duration: DecimalSeconds::new(duration).expect("segment_duration_secs is finite, >= 0"),
discontinuous,
});
self.total_segments += 1;
while self.window_segments.len() > self.window {
if let Some(dropped) = self.window_segments.pop_front()
&& dropped.discontinuous
{
self.discontinuity_sequence += 1;
}
}
self.ready.push_back(TsSegment {
bytes,
duration,
discontinuous,
uri,
sequence,
});
Ok(())
}
}
impl Stage for StreamingTsHlsSegmenter {
type In<'a> = (TrackId, Sample);
type Out = TsSegment;
type Error = Error;
fn feed(&mut self, (track_id, sample): Self::In<'_>, _now: Timestamp) -> Result<()> {
self.push_inner(track_id, sample)
}
fn poll(&mut self) -> Option<Self::Out> {
self.ready.pop_front()
}
fn finish(&mut self) -> Result<()> {
self.finish_inner()
}
fn demand(&self) -> Demand {
Demand::saturated_if(
self.tracks
.iter()
.any(|t| t.pending.len() >= MAX_PENDING_SAMPLES_PER_TRACK),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pipeline::{CodecConfig, DataCarriage};
fn sample(dur: u32, sync: bool) -> Sample {
Sample {
data: vec![0u8; 4].into(),
dts: None,
pts: None,
duration: Some(dur),
flags: crate::ir::SampleFlags::new(sync),
provenance: None,
display: None,
}
}
#[test]
fn boundaries_cut_on_keyframe_past_target() {
let s: Vec<Sample> = (0..8).map(|i| sample(1, i % 2 == 0)).collect();
let b = anchor_segment_boundaries(&s, 2);
assert_eq!(b, vec![0, 2, 4, 6]);
}
#[test]
fn boundaries_single_when_target_exceeds_stream() {
let s: Vec<Sample> = (0..4).map(|i| sample(1, i == 0)).collect();
let b = anchor_segment_boundaries(&s, 1000);
assert_eq!(b, vec![0], "one segment when target dwarfs the stream");
}
use crate::mp4esds::{
DecoderConfigDescriptor, DecoderSpecificInfo, ESDescriptor, EsdsBox, SLConfigDescriptor,
};
use broadcast_common::Unpackage;
fn dummy_esds() -> EsdsBox {
EsdsBox::new(ESDescriptor::new(
1,
0,
Some(DecoderConfigDescriptor::new(
0x40,
0x05,
false,
0,
0,
0,
Some(DecoderSpecificInfo::new(vec![0x12, 0x10])),
)),
Some(SLConfigDescriptor::predefined_two()),
))
}
fn scte35_track(track_id: TrackId) -> TrackSpec {
TrackSpec::new(
track_id,
90_000,
CodecConfig::Data {
stream_type: 0x86,
descriptors: Vec::new(),
carriage: DataCarriage::Sections,
},
)
}
fn section_sample() -> Sample {
Sample {
data: vec![0xFCu8, 0x30, 0x11].into(),
dts: None,
pts: None,
duration: None,
flags: crate::ir::SampleFlags::SYNC,
provenance: None,
display: None,
}
}
fn aac_track(track_id: TrackId, timescale: u32) -> TrackSpec {
TrackSpec::new(
track_id,
timescale,
CodecConfig::Aac {
esds: dummy_esds(),
channel_count: 2,
sample_rate: timescale,
sample_size: 16,
},
)
}
fn audio_sample(duration: u32) -> Sample {
Sample::from_raw(vec![0u8; 4], None, None, Some(duration))
}
#[test]
fn streaming_segmenter_advances_past_a_section_track_anchor_and_cuts_real_segments() {
const SCTE_ID: u32 = 10;
const AUDIO_ID: u32 = 20;
const TIMESCALE: u32 = 1000;
const TARGET_SECS: u32 = 1;
let mut seg = StreamingTsHlsSegmenter::new(
vec![
scte35_track(TrackId::new(SCTE_ID)),
aac_track(TrackId::new(AUDIO_ID), TIMESCALE),
],
TARGET_SECS,
usize::MAX,
)
.expect("construct: audio is anchor-capable even though it isn't first");
seg.push(TrackId::new(SCTE_ID), section_sample())
.expect("push section");
seg.push(TrackId::new(SCTE_ID), section_sample())
.expect("push section");
for _ in 0..12 {
seg.push(TrackId::new(AUDIO_ID), audio_sample(200))
.expect("push audio");
}
let cuts = seg.take_ready();
assert_eq!(
cuts.len(),
2,
"the audio anchor must have advanced past the 1000-tick target twice \
— before the fix this was 0 (the segmenter stalled forever)"
);
for (i, c) in cuts.iter().enumerate() {
assert!(
!c.bytes.is_empty(),
"cut segment {i} must carry real TS bytes"
);
assert!(
c.duration > 0.0,
"cut segment {i} must have a positive duration"
);
}
let demuxed = crate::ts_demux::TsDemux::new()
.unpackage(&cuts[0].bytes)
.expect("cut segment must be a valid TS stream");
assert!(
demuxed
.tracks
.iter()
.any(|t| matches!(t.spec.config, CodecConfig::Aac { .. }) && !t.samples.is_empty()),
"cut segment must carry the audio track's samples, not just PSI"
);
let audio_track_state = seg
.tracks
.iter()
.find(|t| t.spec.track_id == TrackId::new(AUDIO_ID))
.expect("audio track state");
assert_eq!(
audio_track_state.pending.len(),
2,
"pending must be bounded by the cut cadence, not grow without bound"
);
}
#[test]
fn streaming_segmenter_construction_errors_loudly_with_no_anchorable_track() {
let result = StreamingTsHlsSegmenter::new(
vec![
scte35_track(TrackId::new(10)),
scte35_track(TrackId::new(11)),
],
1,
usize::MAX,
);
match result {
Err(Error::InvalidInput(_)) => {}
Err(other) => panic!("expected InvalidInput, got a different error: {other:?}"),
Ok(_) => panic!("a track set of only section-carried tracks must not construct"),
}
}
#[test]
fn finish_bare_call_does_not_lose_the_trailing_segment_to_a_poll_driver() {
const AUDIO_ID: u32 = 1;
const TIMESCALE: u32 = 1000;
let mut seg = StreamingTsHlsSegmenter::new(
vec![aac_track(TrackId::new(AUDIO_ID), TIMESCALE)],
1000,
usize::MAX,
)
.expect("construct");
Stage::feed(
&mut seg,
(TrackId::new(AUDIO_ID), audio_sample(200)),
Timestamp::ZERO,
)
.expect("feed via Stage");
assert!(
Stage::poll(&mut seg).is_none(),
"no cut yet: nothing should be ready before finish"
);
seg.finish().expect("finish");
assert!(
Stage::poll(&mut seg).is_some(),
"the trailing segment cut by a bare finish() call must still be retrievable via \
Stage::poll — it must not have been silently handed back inline and dropped"
);
}
#[test]
fn with_start_sequence_numbers_the_first_cut_segment_from_the_seed() {
const AUDIO_ID: u32 = 1;
const TIMESCALE: u32 = 1000;
const SEED: u64 = 41;
let mut seg = StreamingTsHlsSegmenter::with_start_sequence(
vec![aac_track(TrackId::new(AUDIO_ID), TIMESCALE)],
1,
usize::MAX,
SEED,
)
.expect("construct");
seg.push(TrackId::new(AUDIO_ID), audio_sample(1000))
.expect("push 1");
seg.push(TrackId::new(AUDIO_ID), audio_sample(1000))
.expect("push 2");
seg.finish().expect("finish");
let cut: Vec<TsSegment> = seg.take_ready().into_iter().collect();
assert_eq!(
cut.iter().map(|s| s.sequence).collect::<Vec<_>>(),
vec![SEED, SEED + 1],
"segments cut by a seeded segmenter must number from the seed, not from 0"
);
}
fn countable_media(count: usize, gop: usize) -> Media {
use crate::nalu_types::{AvcPps, AvcSps};
let record = crate::avc_config::AVCDecoderConfigurationRecord {
configuration_version: 1,
profile_indication: 0x42,
profile_compatibility: 0,
level_indication: 0x1E,
length_size_minus_one: 3,
sps: vec![AvcSps(vec![0x67, 0x42, 0xc0, 0x1e, 0xd9])],
pps: vec![AvcPps(vec![0x68, 0xce, 0x3c, 0x80])],
chroma_format: None,
bit_depth_luma_minus8: None,
bit_depth_chroma_minus8: None,
sps_ext: vec![],
};
let video_spec = TrackSpec::new(
TrackId::new(1),
90_000,
CodecConfig::Avc {
config: crate::avc_config::AVCConfigurationBox::new(record),
width: 320,
height: 240,
},
);
let audio_spec = aac_track(TrackId::new(2), 90_000);
let video: Vec<Sample> = (0..count).map(|i| sample(90_000, i % gop == 0)).collect();
let audio: Vec<Sample> = (0..count).map(|i| sample(90_000, i % gop == 0)).collect();
Media::new(
vec![Track::new(video_spec, video), Track::new(audio_spec, audio)],
1000,
)
}
#[test]
fn batch_package_ticks_each_sample_once() {
const COUNT: usize = 400;
const GOP: usize = 4;
let media = countable_media(COUNT, GOP);
let mut packager = TsHlsPackager::new(1);
PACKAGE_TICK_CALLS.with(|c| c.set(0));
let out = packager.package(&media).expect("package");
let visits = PACKAGE_TICK_CALLS.with(|c| c.get());
assert_eq!(
out.segments.len(),
COUNT / GOP,
"the fixture must cut many segments for the bound to bite"
);
assert_eq!(
visits,
COUNT * 2,
"each of the two tracks' {COUNT} samples must be ticked exactly once
(the pre-fix code re-walked every earlier sample per segment)"
);
}
}