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 crate::error::{Error, Result};
use crate::hls::{MediaPlaylist, MediaSegment};
use crate::media::{Media, Track};
use crate::pipeline::{Sample, TrackSpec};
use crate::segmenter::{
MAX_PENDING_SAMPLES_PER_TRACK, MediaClock, choose_anchor, no_sync_sample_error,
};
use crate::ts_mux::mux_tracks_at;
const DEFAULT_HLS_VERSION: u8 = 3;
#[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 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;
for (i, seg) in segments.iter().enumerate() {
let sample_slices: Vec<&[Sample]> = media
.tracks
.iter()
.zip(&seg.ranges)
.map(|(t, r)| &t.samples[r.clone()])
.collect();
let base_dts: Vec<u64> = media
.tracks
.iter()
.zip(&seg.ranges)
.map(|(t, r)| {
let mut clock = MediaClock::new();
t.samples[..r.start].iter().map(|s| clock.tick(s)).sum()
})
.collect();
let bytes = mux_tracks_at(&media.tracks, &sample_slices, &base_dts)?;
ts_segments.push(bytes);
let anchor_ticks: u64 = {
let anchor_samples = &media.tracks[anchor].samples;
let mut clock = MediaClock::new();
for s in &anchor_samples[..seg.ranges[anchor].start] {
clock.tick(s);
}
anchor_samples[seg.ranges[anchor].clone()]
.iter()
.map(|s| clock.tick(s))
.sum()
};
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,
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: f64,
discontinuous: bool,
}
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,
anchor_clock: MediaClock,
origin_secs: Option<f64>,
pending_discontinuity: bool,
finished: bool,
pub version: u8,
pub uri_prefix: String,
window: usize,
window_segments: VecDeque<WindowEntry>,
total_segments: u64,
discontinuity_sequence: u64,
target_duration: u32,
ready: VecDeque<TsSegment>,
}
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,
anchor_clock: MediaClock::new(),
origin_secs: None,
pending_discontinuity: false,
finished: false,
version: DEFAULT_HLS_VERSION,
uri_prefix: String::from("seg"),
window,
window_segments: VecDeque::new(),
total_segments: 0,
discontinuity_sequence: 0,
target_duration: 0,
ready: VecDeque::new(),
})
}
pub fn push(&mut self, track_id: u32, sample: Sample) -> Result<()> {
self.push_inner(track_id, sample)
}
fn push_inner(&mut self, track_id: u32, 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 idx == self.anchor
&& is_cut_point(
!self.tracks[self.anchor].pending.is_empty(),
sample.flags.is_sync,
self.anchor_pending_dur,
self.target_ticks,
)
{
self.cut_segment(false)?;
}
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);
Ok(())
}
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;
if self.tracks.iter().any(|t| !t.pending.is_empty()) {
self.cut_segment(true)?;
}
Ok(())
}
pub fn mark_discontinuity(&mut self) {
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 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 playlist(&self) -> 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;
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<()> {
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 + self.anchor_pending_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 || i == anchor {
return t.pending.len();
}
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(&mux_tracks, &sample_slices, &base_dts)?;
let (duration, ceil_secs) = segment_duration_secs(self.anchor_pending_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);
}
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,
discontinuous,
});
self.total_segments += 1;
while self.window_segments.len() > self.window {
if let Some(dropped) = self.window_segments.pop_front() {
if dropped.discontinuous {
self.discontinuity_sequence += 1;
}
}
}
self.ready.push_back(TsSegment {
bytes,
duration,
discontinuous,
uri,
sequence,
});
Ok(())
}
}
impl Stage for StreamingTsHlsSegmenter {
type In<'a> = (u32, 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 next_deadline(&self) -> Option<Timestamp> {
None
}
fn on_deadline(&mut self, _now: Timestamp) {}
fn demand(&self) -> Demand {
if self
.tracks
.iter()
.any(|t| t.pending.len() >= MAX_PENDING_SAMPLES_PER_TRACK)
{
Demand::saturated()
} else {
Demand::default()
}
}
}
#[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,
}
}
#[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, ObjectTypeIndication,
SLConfigDescriptor, StreamType,
};
use broadcast_common::Unpackage;
fn dummy_esds() -> EsdsBox {
EsdsBox::new(ESDescriptor {
es_id: 1,
stream_dependence_flag: false,
url_flag: false,
ocr_stream_flag: false,
stream_priority: 0,
depends_on_es_id: None,
url: None,
ocr_es_id: None,
decoder_config: Some(DecoderConfigDescriptor {
object_type_indication: ObjectTypeIndication(0x40),
stream_type: StreamType(0x05),
up_stream: false,
buffer_size_db: 0,
max_bitrate: 0,
avg_bitrate: 0,
decoder_specific_info: Some(DecoderSpecificInfo {
data: vec![0x12, 0x10],
}),
}),
sl_config: Some(SLConfigDescriptor { body: vec![0x02] }),
})
}
fn scte35_track(track_id: u32) -> 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,
}
}
fn aac_track(track_id: u32, 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(SCTE_ID), aac_track(AUDIO_ID, TIMESCALE)],
TARGET_SECS,
usize::MAX,
)
.expect("construct: audio is anchor-capable even though it isn't first");
seg.push(SCTE_ID, section_sample()).expect("push section");
seg.push(SCTE_ID, section_sample()).expect("push section");
for _ in 0..12 {
seg.push(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 == 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(10), scte35_track(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(AUDIO_ID, TIMESCALE)], 1000, usize::MAX)
.expect("construct");
Stage::feed(&mut seg, (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"
);
}
}