use std::collections::VecDeque;
use std::sync::Mutex;
use tokio::sync::watch;
use transmux::hls::{LowLatencyConfig, MediaPlaylist, MediaSegment, OpenSegment, PartSpec};
use transmux::ll_hls::{PartInfo, SegmentInfo};
const LL_HLS_VERSION: u8 = 9;
const PART_HOLD_BACK_MULTIPLIER: f64 = 3.0;
const MIN_MAX_LIVE_PARTS: usize = 8;
const MAX_LIVE_PARTS_SAFETY_MARGIN: usize = 4;
fn compute_max_live_parts(target_duration_secs: f64, part_target_ms: u32) -> usize {
let part_target_secs = f64::from(part_target_ms) / 1000.0;
let nominal_parts = if part_target_secs > 0.0 {
(target_duration_secs / part_target_secs).ceil() as usize
} else {
0
};
(nominal_parts + MAX_LIVE_PARTS_SAFETY_MARGIN).max(MIN_MAX_LIVE_PARTS)
}
struct Inner {
init: Option<Vec<u8>>,
segments: VecDeque<SegmentInfo>,
live_parts: Vec<PartInfo>,
recent_parts: VecDeque<PartInfo>,
window_segments: usize,
}
pub struct StreamStore {
inner: Mutex<Inner>,
target_duration_secs: f64,
part_target_ms: u32,
max_live_parts: usize,
progress_tx: watch::Sender<u64>,
}
impl StreamStore {
pub fn new(target_duration_secs: f64, part_target_ms: u32, window_segments: usize) -> Self {
let (tx, _rx) = watch::channel(0u64);
StreamStore {
inner: Mutex::new(Inner {
init: None,
segments: VecDeque::new(),
live_parts: Vec::new(),
recent_parts: VecDeque::new(),
window_segments,
}),
target_duration_secs,
part_target_ms,
max_live_parts: compute_max_live_parts(target_duration_secs, part_target_ms),
progress_tx: tx,
}
}
fn bump(&self) {
self.progress_tx.send_modify(|v| *v = v.wrapping_add(1));
}
pub fn set_init(&self, bytes: Vec<u8>) {
self.inner.lock().unwrap().init = Some(bytes);
self.bump();
}
pub fn add_part(&self, part: PartInfo) {
let mut g = self.inner.lock().unwrap();
g.live_parts.push(part);
while g.live_parts.len() > self.max_live_parts {
g.live_parts.remove(0);
}
drop(g);
self.bump();
}
#[cfg(test)]
pub fn live_part_count(&self) -> usize {
self.inner.lock().unwrap().live_parts.len()
}
pub fn add_segment(&self, seg: SegmentInfo) {
let mut g = self.inner.lock().unwrap();
let seq = seg.segment_seq;
let (closed, still_live): (Vec<PartInfo>, Vec<PartInfo>) =
core::mem::take(&mut g.live_parts)
.into_iter()
.partition(|p| p.segment_seq <= seq);
g.live_parts = still_live;
for p in closed {
g.recent_parts.push_back(p);
}
while g.recent_parts.len() > self.max_live_parts {
g.recent_parts.pop_front();
}
g.segments.push_back(seg);
while g.segments.len() > g.window_segments {
g.segments.pop_front();
}
drop(g);
self.bump();
}
pub fn init_bytes(&self) -> Option<Vec<u8>> {
self.inner.lock().unwrap().init.clone()
}
pub fn segment_bytes(&self, seq: u32) -> Option<Vec<u8>> {
let g = self.inner.lock().unwrap();
g.segments
.iter()
.find(|s| s.segment_seq == seq)
.map(|s| s.bytes.clone())
}
pub fn part_bytes(&self, seq: u32, part_index: u32) -> Option<Vec<u8>> {
let g = self.inner.lock().unwrap();
let matches = |p: &&PartInfo| p.segment_seq == seq && p.part_index == part_index;
g.live_parts
.iter()
.find(matches)
.or_else(|| g.recent_parts.iter().find(matches))
.map(|p| p.bytes.clone())
}
pub fn latest_progress(&self) -> (u32, u32) {
let g = self.inner.lock().unwrap();
let last_closed_seg = g.segments.back().map(|s| s.segment_seq).unwrap_or(0);
let in_progress_seg = g
.live_parts
.last()
.map(|p| p.segment_seq)
.unwrap_or(last_closed_seg);
let part_count = g
.live_parts
.iter()
.filter(|p| p.segment_seq == in_progress_seg)
.count() as u32;
(in_progress_seg, part_count)
}
pub fn subscribe(&self) -> watch::Receiver<u64> {
self.progress_tx.subscribe()
}
pub fn media_playlist_m3u8(&self, track_id: u32) -> String {
let g = self.inner.lock().unwrap();
let media_sequence = g
.segments
.front()
.map(|s| u64::from(s.segment_seq))
.or_else(|| g.live_parts.first().map(|p| u64::from(p.segment_seq)))
.unwrap_or(1);
let segments: Vec<MediaSegment> = g
.segments
.iter()
.map(|s| MediaSegment {
uri: format!("seg-{track_id}-{}.m4s", s.segment_seq),
duration: s.duration,
discontinuous: false,
parts: Vec::new(),
})
.collect();
let part_target = f64::from(self.part_target_ms) / 1000.0;
let open_seq = g.live_parts.first().map(|p| p.segment_seq);
let open_segment = open_seq.map(|seq| {
OpenSegment::new(
g.live_parts
.iter()
.filter(|p| p.segment_seq == seq)
.map(|p| PartSpec {
uri: format!("part-{track_id}-{}.{}.m4s", p.segment_seq, p.part_index),
duration: p.duration,
independent: p.independent,
})
.collect(),
)
});
let next_part_hint = open_seq.map(|seq| {
let next_idx = g
.live_parts
.iter()
.filter(|p| p.segment_seq == seq)
.map(|p| p.part_index)
.max()
.map(|idx| idx + 1)
.unwrap_or(0);
format!("part-{track_id}-{seq}.{next_idx}.m4s")
});
let playlist = MediaPlaylist {
version: LL_HLS_VERSION,
target_duration: self.target_duration_secs.ceil() as u32,
media_sequence,
discontinuity_sequence: 0,
segments,
open_segment,
endlist: false,
extra_tags: vec![format!("#EXT-X-MAP:URI=\"init-{track_id}.mp4\"")],
low_latency: Some(LowLatencyConfig {
part_target,
part_hold_back: part_target * PART_HOLD_BACK_MULTIPLIER,
preload_hint_part: next_part_hint,
}),
iframes_only: false,
};
playlist.to_m3u8()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn seg(seq: u32, parts: u32) -> SegmentInfo {
SegmentInfo {
bytes: vec![seq as u8; 8],
duration: 4.0,
segment_seq: seq,
part_count: parts,
}
}
fn part(seq: u32, idx: u32) -> PartInfo {
PartInfo {
bytes: vec![idx as u8; 4],
duration: 0.5,
independent: idx == 0,
segment_seq: seq,
part_index: idx,
}
}
#[test]
fn window_evicts_oldest_and_serves_bytes() {
let s = StreamStore::new(4.0, 500, 2);
s.set_init(vec![0xAA; 10]);
s.add_segment(seg(1, 8));
s.add_segment(seg(2, 8));
s.add_segment(seg(3, 8)); assert!(s.segment_bytes(1).is_none(), "seq 1 evicted");
assert!(s.segment_bytes(2).is_some());
assert!(s.segment_bytes(3).is_some());
assert_eq!(s.init_bytes().unwrap(), vec![0xAA; 10]);
}
#[test]
fn playlist_has_llhls_tags_and_parts() {
let s = StreamStore::new(4.0, 500, 4);
s.set_init(vec![0; 4]);
s.add_part(part(1, 0));
s.add_part(part(1, 1));
let m = s.media_playlist_m3u8(1);
assert!(m.contains("#EXT-X-PART-INF"), "PART-INF present");
assert!(
m.contains("#EXT-X-SERVER-CONTROL"),
"SERVER-CONTROL present"
);
assert!(m.contains("#EXT-X-PART"), "at least one PART");
assert!(
m.contains("part-1-1.0.m4s") || m.contains("part-1-1.1.m4s"),
"part URI"
);
}
#[test]
fn open_segment_has_parts_but_no_extinf() {
let s = StreamStore::new(4.0, 500, 4);
s.set_init(vec![0; 4]);
s.add_part(part(1, 0));
s.add_part(part(1, 1));
let m = s.media_playlist_m3u8(1);
assert!(m.contains("#EXT-X-PART"), "at least one PART line");
assert!(m.contains("part-1-1.0.m4s"), "part 0 URI present");
assert!(m.contains("part-1-1.1.m4s"), "part 1 URI present");
assert!(
!m.contains("seg-1-1.m4s"),
"no full-segment URI for the open segment: {m}"
);
assert!(
!m.contains("#EXTINF"),
"no EXTINF for the open segment: {m}"
);
}
#[test]
fn final_part_fetchable_after_its_segment_closes() {
let s = StreamStore::new(4.0, 500, 4);
s.set_init(vec![0; 4]);
s.add_part(part(1, 0));
s.add_part(part(1, 1)); s.add_segment(seg(1, 2)); assert_eq!(
s.part_bytes(1, 1),
Some(vec![1; 4]),
"final part of a just-closed segment must still be individually fetchable"
);
assert_eq!(s.part_bytes(1, 0), Some(vec![0; 4]), "earlier parts too");
assert_eq!(s.part_bytes(1, 9), None);
let m = s.media_playlist_m3u8(1);
assert!(
m.contains("seg-1-1.m4s"),
"closed segment rendered whole: {m}"
);
assert!(
!m.contains("part-1-1."),
"closed parts not rendered as open: {m}"
);
}
#[test]
fn recent_parts_bounded_across_many_closes() {
let s = StreamStore::new(4.0, 500, 4);
s.set_init(vec![0; 4]);
let cap = compute_max_live_parts(4.0, 500);
for seq in 1..=20u32 {
for idx in 0..4u32 {
s.add_part(part(seq, idx));
}
s.add_segment(seg(seq, 4));
}
assert!(s.part_bytes(1, 0).is_none(), "old closed part evicted");
assert!(
s.part_bytes(20, 3).is_some(),
"most-recent closed part retained (within the {cap}-part bound)"
);
}
#[test]
fn watch_bumps_on_new_data() {
let s = StreamStore::new(4.0, 500, 4);
let mut rx = s.subscribe();
let before = *rx.borrow_and_update();
s.add_part(part(1, 0));
assert_ne!(*rx.borrow(), before, "watch value changed");
}
#[test]
fn live_parts_capped_when_segment_never_closes() {
let s = StreamStore::new(4.0, 500, 4);
let cap = compute_max_live_parts(4.0, 500);
assert_eq!(cap, 12, "sanity-check the expected cap for these params");
s.set_init(vec![0; 4]);
for i in 0..(cap as u32 * 5) {
s.add_part(part(1, i));
}
assert_eq!(
s.live_part_count(),
cap,
"live_parts must stay capped even though the segment never closed"
);
let m = s.media_playlist_m3u8(1);
assert!(m.contains("#EXT-X-PART"), "still has PART lines: {m}");
let last_idx = cap as u32 * 5 - 1;
assert!(
m.contains(&format!("part-1-1.{last_idx}.m4s")),
"most recent part must survive the cap: {m}"
);
let first_idx = cap as u32 * 5 - cap as u32;
assert!(
!m.contains(&format!("part-1-1.{}.m4s", first_idx - 1)),
"an older part beyond the cap must have been dropped: {m}"
);
}
}