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};
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 = media
.tracks
.iter()
.position(|t| matches!(t.spec.config, CodecConfig::Avc { .. }))
.unwrap_or(0);
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 = anchor_ticks as f64 / ts_scale as f64;
let ceil_secs = anchor_ticks.div_ceil(ts_scale) as u32;
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![],
});
}
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,
}
.to_m3u8();
Ok(TsHlsOutput {
segments: ts_segments,
playlist,
})
}
}
impl TsHlsPackager {
fn anchor_target_ticks(&self, anchor: &Track) -> u64 {
let ts_scale = anchor.spec.timescale.max(1) as u64;
(self.target_secs as u64 * ts_scale).max(1)
}
}
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 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
}
#[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,
}
}
#[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");
}
}