use alloc::collections::VecDeque;
use alloc::format;
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use broadcast_common::Package;
use crate::error::{Error, Result};
use crate::hls::{MediaPlaylist, MediaSegment};
use crate::media::{Media, Track};
use crate::pipeline::{CodecConfig, Sample, TrackSpec};
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)| t.samples[..r.start].iter().map(|s| s.duration as u64).sum())
.collect();
let bytes = mux_tracks_at(&media.tracks, &sample_slices, &base_dts)?;
ts_segments.push(bytes);
let anchor_ticks: u64 = media.tracks[anchor].samples[seg.ranges[anchor].clone()]
.iter()
.map(|s| s.duration as u64)
.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 choose_anchor<'a, I>(configs: I) -> usize
where
I: Iterator<Item = &'a CodecConfig>,
{
configs
.enumerate()
.find(|(_, c)| c.is_video())
.map(|(i, _)| i)
.unwrap_or(0)
}
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 buffered: u64 = 0;
for (i, s) in samples.iter().enumerate() {
if is_cut_point(i > 0, s.is_sync, buffered, target_ticks) {
starts.push(i);
buffered = 0;
}
buffered += s.duration as u64;
}
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 mut start_times: Vec<f64> = Vec::with_capacity(n_segs);
{
let mut acc: u64 = 0;
let mut cursor = 0usize;
for &b in anchor_boundaries {
while cursor < b {
acc += anchor_samples[cursor].duration as u64;
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;
for (i, s) in track.samples.iter().enumerate() {
let start_time = acc_ticks as f64 / scale as f64;
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 += s.duration as u64;
}
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,
}
pub struct StreamingTsHlsSegmenter {
tracks: Vec<StreamTrackState>,
anchor: usize,
target_secs: u32,
target_ticks: u64,
anchor_pending_dur: u64,
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,
}
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,
})
.collect();
Ok(Self {
tracks,
anchor,
target_secs,
target_ticks,
anchor_pending_dur: 0,
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,
})
}
pub fn push(&mut self, track_id: u32, sample: Sample) -> Result<Option<TsSegment>> {
let idx = self
.tracks
.iter()
.position(|t| t.spec.track_id == track_id)
.ok_or(Error::InvalidInput("push: unknown track_id"))?;
let mut cut = None;
if idx == self.anchor
&& is_cut_point(
!self.tracks[self.anchor].pending.is_empty(),
sample.is_sync,
self.anchor_pending_dur,
self.target_ticks,
)
{
cut = Some(self.cut_segment(false)?);
}
if idx == self.anchor {
self.anchor_pending_dur += sample.duration as u64;
}
self.tracks[idx].pending.push(sample);
Ok(cut)
}
pub fn finish(&mut self) -> Result<Option<TsSegment>> {
self.finished = true;
if self.tracks.iter().any(|t| !t.pending.is_empty()) {
Ok(Some(self.cut_segment(true)?))
} else {
Ok(None)
}
}
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,
});
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<TsSegment> {
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;
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;
for (j, s) in t.pending.iter().enumerate() {
let start_secs = acc as f64 / scale as f64;
if start_secs >= next_start_secs {
return j;
}
acc += s.duration as u64;
}
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 dur: u64 = t.pending[..n].iter().map(|s| s.duration as u64).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;
}
}
}
Ok(TsSegment {
bytes,
duration,
discontinuous,
uri,
sequence,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample(dur: u32, sync: bool) -> Sample {
Sample {
data: vec![0u8; 4],
duration: dur,
is_sync: sync,
composition_offset: 0,
source_timing: 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");
}
}