use broadcast_hls::{LowLatencyConfig, MapTag, MediaPlaylist, MediaSegment, OpenSegment, PartSpec};
use hls_runtime::client::{Action, HlsClient, Output, ResourceId};
use transmux::ll_hls::{LlHlsSegmenter, PartInfo, SegmentInfo};
use transmux::{
AVCConfigurationBox, AVCDecoderConfigurationRecord, AvcPps, AvcSps, CodecConfig, Sample,
TrackSpec,
};
const INIT_URL: &str = "http://origin/live/init.mp4";
const PLAYLIST_URL: &str = "http://origin/live/stream.m3u8";
const VID_DUR: u32 = 3000;
fn dummy_avc_config() -> AVCConfigurationBox {
AVCConfigurationBox::new(AVCDecoderConfigurationRecord {
configuration_version: 1,
profile_indication: 66,
profile_compatibility: 0,
level_indication: 30,
length_size_minus_one: 3,
sps: vec![AvcSps(vec![0x67, 66, 0, 30, 0x00])],
pps: vec![AvcPps(vec![0x68, 0xCE, 0x3C, 0x80])],
chroma_format: None,
bit_depth_luma_minus8: None,
bit_depth_chroma_minus8: None,
sps_ext: vec![],
})
}
fn video_track() -> TrackSpec {
TrackSpec::new(
1,
90_000,
CodecConfig::Avc {
config: dummy_avc_config(),
width: 320,
height: 240,
},
)
}
fn vsample(is_sync: bool, byte: u8) -> Sample {
Sample::new(vec![byte; 32], None, None, Some(VID_DUR), is_sync)
}
fn part_uri(p: &PartInfo) -> String {
format!("seg{}.{}.m4s", p.segment_seq, p.part_index)
}
fn segment_uri(s: &SegmentInfo) -> String {
format!("seg{}.m4s", s.segment_seq)
}
fn abs_uri(relative: &str) -> String {
format!("http://origin/live/{relative}")
}
fn part_spec(p: &PartInfo) -> PartSpec {
PartSpec {
uri: part_uri(p),
duration: p.duration,
independent: p.independent,
byte_range: None,
gap: false,
extra_attrs: vec![],
}
}
struct PlaylistBuilder<'a> {
media_sequence: u64,
part_target_secs: f64,
closed: &'a [(SegmentInfo, Vec<PartInfo>)],
open_seq: Option<u32>,
open_parts: &'a [PartInfo],
preload_hint_next_index: Option<u64>,
endlist: bool,
}
impl PlaylistBuilder<'_> {
fn build(&self) -> MediaPlaylist {
let segments: Vec<MediaSegment> = self
.closed
.iter()
.enumerate()
.map(|(i, (info, parts))| MediaSegment {
uri: segment_uri(info),
duration: info.duration,
discontinuous: false,
parts: parts.iter().map(part_spec).collect(),
byte_range: None,
map: if i == 0 {
Some(MapTag {
uri: INIT_URL.to_string(),
byte_range: None,
extra_attrs: vec![],
})
} else {
None
},
..Default::default()
})
.collect();
let mut segments = segments;
for i in 1..segments.len() {
if segments[i].map.is_none() {
segments[i].map = segments[i - 1].map.clone();
}
}
let open_segment = self
.open_seq
.map(|_| OpenSegment::new(self.open_parts.iter().map(part_spec).collect()));
let preload_hint_part = self.preload_hint_next_index.map(|idx| {
let seq = self.open_seq.unwrap_or(self.closed.len() as u32 + 1);
format!("seg{seq}.{idx}.m4s")
});
MediaPlaylist {
version: 9,
target_duration: 1,
media_sequence: self.media_sequence,
discontinuity_sequence: 0,
segments,
open_segment,
endlist: self.endlist,
extra_tags: vec![],
low_latency: Some(LowLatencyConfig {
part_target: self.part_target_secs,
part_hold_back: 3.0 * self.part_target_secs,
preload_hint_part,
..Default::default()
}),
iframes_only: false,
rendition_reports: vec![],
skip: None,
..Default::default()
}
}
}
struct Origin {
init_bytes: Vec<u8>,
parts_by_uri: std::collections::HashMap<String, Vec<u8>>,
segments_by_uri: std::collections::HashMap<String, Vec<u8>>,
}
impl Origin {
fn fetch(&self, action: &Action) -> Option<(ResourceId, Vec<u8>)> {
match action {
Action::FetchResource { id, url, .. } => {
let bytes = if *id == ResourceId::Init {
self.init_bytes.clone()
} else if let Some(b) = self.parts_by_uri.get(url) {
b.clone()
} else {
self.segments_by_uri.get(url).cloned()?
};
Some((*id, bytes))
}
_ => None,
}
}
}
fn drain_actions(client: &mut HlsClient) -> Vec<Action> {
let mut out = Vec::new();
while let Some(a) = client.poll() {
out.push(a);
}
out
}
fn drain_outputs(client: &mut HlsClient) -> Vec<Output> {
let mut out = Vec::new();
while let Some(o) = client.next_output() {
out.push(o);
}
out
}
#[test]
fn origin_client_loop_blocking_reload_prefetch_dedup_and_ordered_output() {
let mut seg = LlHlsSegmenter::with_part_target(vec![video_track()], 1000, 1.0, 334).unwrap();
let mut fed_samples: Vec<Sample> = Vec::new();
for i in 0..30u8 {
let s = vsample(i == 0, i);
fed_samples.push(s.clone());
seg.push(1, s).unwrap();
}
let s = vsample(true, 200);
fed_samples.push(s.clone());
seg.push(1, s).unwrap();
for i in 0..10u8 {
let s = vsample(false, 100 + i);
fed_samples.push(s.clone());
seg.push(1, s).unwrap();
}
for i in 0..11u8 {
let s = vsample(false, 120 + i);
fed_samples.push(s.clone());
seg.push(1, s).unwrap();
}
let init_bytes = seg.init_segment().unwrap();
let part_target_secs = seg.part_target_secs();
let mut ready_parts = seg.take_ready_parts();
let ready_segments = seg.take_ready_segments();
assert_eq!(ready_segments.len(), 1, "segment 1 must have closed");
assert!(
ready_parts.iter().any(|p| p.segment_seq == 2),
"segment 2 must have an open part"
);
let seg1_parts: Vec<PartInfo> = ready_parts
.iter()
.filter(|p| p.segment_seq == 1)
.cloned()
.collect();
let seg2_all_parts: Vec<PartInfo> = ready_parts
.iter()
.filter(|p| p.segment_seq == 2)
.cloned()
.collect();
assert_eq!(seg1_parts.len(), 3);
assert_eq!(seg2_all_parts.len(), 2, "segment 2 has parts 0 and 1 ready");
let seg2_known_parts: Vec<PartInfo> = seg2_all_parts
.iter()
.filter(|p| p.part_index == 0)
.cloned()
.collect();
assert_eq!(seg2_known_parts.len(), 1);
let closed = vec![(ready_segments[0].clone(), seg1_parts.clone())];
let pl1 = PlaylistBuilder {
media_sequence: 1,
part_target_secs,
closed: &closed,
open_seq: Some(2),
open_parts: &seg2_known_parts,
preload_hint_next_index: Some(1),
endlist: false,
}
.build();
let pl1_text = pl1.to_m3u8();
let mut parts_by_uri = std::collections::HashMap::new();
for p in &seg1_parts {
parts_by_uri.insert(abs_uri(&part_uri(p)), p.bytes.clone());
}
for p in &seg2_all_parts {
parts_by_uri.insert(abs_uri(&part_uri(p)), p.bytes.clone());
}
let mut segments_by_uri = std::collections::HashMap::new();
for s in &ready_segments {
segments_by_uri.insert(abs_uri(&segment_uri(s)), s.bytes.clone());
}
let origin = Origin {
init_bytes: init_bytes.clone(),
parts_by_uri,
segments_by_uri,
};
let mut client = HlsClient::new(PLAYLIST_URL);
let first = client.poll().expect("first action");
match &first {
Action::FetchPlaylist {
url,
blocking,
skip,
} => {
assert_eq!(url, PLAYLIST_URL);
assert!(blocking.is_none());
assert!(!skip);
}
other => panic!("expected FetchPlaylist, got {other:?}"),
}
assert!(client.poll().is_none(), "nothing else queued yet");
client.on_playlist(pl1_text.as_bytes()).unwrap();
let actions = drain_actions(&mut client);
assert!(
actions
.iter()
.any(|a| matches!(a, Action::FetchResource { id: ResourceId::Init, url, .. } if url == INIT_URL)),
"must request the init segment: {actions:#?}"
);
for p in &seg1_parts {
let want_url = format!("http://origin/live/{}", part_uri(p));
assert!(
actions.iter().any(|a| matches!(a,
Action::FetchResource { id: ResourceId::Part { msn: 1, part }, url, .. }
if *part == p.part_index as u64 && *url == want_url
)),
"must request seg1 part {}: {actions:#?}",
p.part_index
);
}
assert!(
actions.iter().any(|a| matches!(
a,
Action::FetchResource {
id: ResourceId::Part { msn: 2, part: 0 },
..
}
)),
"must request seg2 part 0: {actions:#?}"
);
assert!(
actions.iter().any(|a| matches!(a,
Action::FetchResource { id: ResourceId::Part { msn: 2, part: 1 }, url, .. }
if url == "http://origin/live/seg2.1.m4s"
)),
"must prefetch the EXT-X-PRELOAD-HINT part (seg2 part 1): {actions:#?}"
);
let reload = actions
.iter()
.find(|a| matches!(a, Action::FetchPlaylist { .. }))
.expect("a reload action must be queued");
match reload {
Action::FetchPlaylist { url, blocking, .. } => {
assert_eq!(url, PLAYLIST_URL);
let b = blocking.expect("must be a blocking reload (LL playlist)");
assert_eq!(b.msn, 2, "blocking reload must target seg 2");
assert_eq!(
b.part,
Some(1),
"blocking reload must target the next unseen part"
);
}
_ => unreachable!(),
}
let reload_url = reload.playlist_request_url().unwrap();
assert!(reload_url.contains("_HLS_msn=2"), "{reload_url}");
assert!(reload_url.contains("_HLS_part=1"), "{reload_url}");
for a in &actions {
if let Some((id, bytes)) = origin.fetch(a) {
client.on_resource(id, &bytes).unwrap();
}
}
let outputs = drain_outputs(&mut client);
let mut got_init = false;
let mut got_samples: Vec<Sample> = Vec::new();
for o in outputs {
match o {
Output::Init(bytes) => {
assert!(!got_init, "init must be emitted exactly once");
assert_eq!(
bytes, init_bytes,
"init bytes must match the origin's init segment"
);
got_init = true;
}
Output::Samples { track_id, samples } => {
assert!(got_init, "samples must follow init");
assert_eq!(track_id, 1);
got_samples.extend(samples);
}
other => panic!("unexpected output: {other:?}"),
}
}
assert!(got_init);
assert_eq!(
got_samples.len(),
fed_samples.len(),
"must reconstruct every sample fed to the segmenter (no gaps, no duplicates)"
);
for (got, want) in got_samples.iter().zip(fed_samples.iter()) {
assert_eq!(got.data, want.data, "sample bytes must match exactly");
assert_eq!(got.duration, want.duration);
assert_eq!(got.flags.is_sync, want.flags.is_sync);
}
let seg2_closed = SegmentInfo {
bytes: Vec::new(), duration: seg2_all_parts.iter().map(|p| p.duration).sum(),
segment_seq: 2,
part_count: 2,
};
let closed2 = vec![
(ready_segments[0].clone(), seg1_parts.clone()),
(seg2_closed, seg2_all_parts.clone()),
];
let pl2 = PlaylistBuilder {
media_sequence: 1,
part_target_secs,
closed: &closed2,
open_seq: None,
open_parts: &[],
preload_hint_next_index: None,
endlist: true, }
.build();
client.on_playlist(pl2.to_m3u8().as_bytes()).unwrap();
let actions2 = drain_actions(&mut client);
assert!(
actions2.is_empty(),
"closing an already-fully-delivered segment must not trigger new fetches: {actions2:#?}"
);
let outputs2 = drain_outputs(&mut client);
assert_eq!(
outputs2.len(),
1,
"no new samples (dedup) — just EndOfStream: {outputs2:#?}"
);
assert!(matches!(outputs2[0], Output::EndOfStream));
let _ = seg;
let _ = ready_parts.drain(..); }
#[test]
fn can_block_reload_no_yields_non_blocking_reload_with_backoff() {
let pl = MediaPlaylist {
version: 9,
target_duration: 2,
media_sequence: 0,
discontinuity_sequence: 0,
segments: vec![MediaSegment {
uri: "seg0.m4s".to_string(),
duration: 1.0,
discontinuous: false,
parts: vec![PartSpec {
uri: "seg0.0.m4s".to_string(),
duration: 1.0,
independent: true,
byte_range: None,
gap: false,
extra_attrs: vec![],
}],
byte_range: None,
map: Some(MapTag {
uri: INIT_URL.to_string(),
byte_range: None,
extra_attrs: vec![],
}),
..Default::default()
}],
open_segment: None,
endlist: false,
extra_tags: vec![],
low_latency: Some(LowLatencyConfig {
part_target: 0.5,
part_hold_back: 1.5,
can_block_reload: false,
..Default::default()
}),
iframes_only: false,
rendition_reports: vec![],
skip: None,
..Default::default()
};
let mut client = HlsClient::new(PLAYLIST_URL);
let _ = client.poll(); client.on_playlist(pl.to_m3u8().as_bytes()).unwrap();
let actions = drain_actions(&mut client);
let reload = actions
.iter()
.find(|a| matches!(a, Action::FetchPlaylist { .. }))
.expect("a reload action must be queued");
match reload {
Action::FetchPlaylist { blocking, .. } => {
assert!(
blocking.is_none(),
"CAN-BLOCK-RELOAD=NO must never produce a blocking reload: {reload:?}"
);
}
_ => unreachable!(),
}
assert!(
actions.iter().any(|a| matches!(a, Action::WaitMs(_))),
"a non-blocking reload must be paced with a WaitMs backoff hint: {actions:#?}"
);
}
#[test]
fn non_ll_playlist_plays_via_full_segment_fallback() {
let mut seg =
LlHlsSegmenter::with_part_target(vec![video_track()], 1000, 1.0, 100_000).unwrap();
let mut fed_samples: Vec<Sample> = Vec::new();
for i in 0..30u8 {
let s = vsample(i == 0, i);
fed_samples.push(s.clone());
seg.push(1, s).unwrap();
}
seg.push(1, vsample(true, 200)).unwrap();
seg.flush().unwrap();
let init_bytes = seg.init_segment().unwrap();
let segments = seg.take_ready_segments();
let seg1_info = segments
.into_iter()
.find(|s| s.segment_seq == 1)
.expect("segment 1 must have closed");
let _ = seg.take_ready_parts();
let pl = MediaPlaylist {
version: 3,
target_duration: 2,
media_sequence: 0,
discontinuity_sequence: 0,
segments: vec![MediaSegment {
uri: "seg1.m4s".to_string(),
duration: seg1_info.duration,
discontinuous: false,
parts: vec![],
byte_range: None,
map: Some(MapTag {
uri: INIT_URL.to_string(),
byte_range: None,
extra_attrs: vec![],
}),
..Default::default()
}],
open_segment: None,
endlist: true,
extra_tags: vec![],
low_latency: None,
iframes_only: false,
rendition_reports: vec![],
skip: None,
..Default::default()
};
let mut client = HlsClient::new(PLAYLIST_URL);
let _ = client.poll();
client.on_playlist(pl.to_m3u8().as_bytes()).unwrap();
let actions = drain_actions(&mut client);
assert!(
!actions.iter().any(|a| matches!(
a,
Action::FetchPlaylist {
blocking: Some(_),
..
}
)),
"non-LL playlist must never request a blocking reload: {actions:#?}"
);
assert!(actions.iter().any(|a| matches!(
a,
Action::FetchResource {
id: ResourceId::Init,
..
}
)));
assert!(actions.iter().any(|a| matches!(
a,
Action::FetchResource { id: ResourceId::Segment { msn: 0 }, url, .. }
if url == "http://origin/live/seg1.m4s"
)));
for a in &actions {
match a {
Action::FetchResource {
id: ResourceId::Init,
..
} => {
client.on_resource(ResourceId::Init, &init_bytes).unwrap();
}
Action::FetchResource {
id: id @ ResourceId::Segment { msn: 0 },
..
} => {
client.on_resource(*id, &seg1_info.bytes).unwrap();
}
_ => {}
}
}
let outputs = drain_outputs(&mut client);
let mut got_samples: Vec<Sample> = Vec::new();
let mut saw_init = false;
let mut saw_end = false;
for o in outputs {
match o {
Output::Init(bytes) => {
assert_eq!(bytes, init_bytes);
saw_init = true;
}
Output::Samples { samples, .. } => got_samples.extend(samples),
Output::EndOfStream => saw_end = true,
other => panic!("unexpected output: {other:?}"),
}
}
assert!(saw_init, "must emit init even on the fallback path");
assert!(saw_end, "endlist playlist must emit EndOfStream");
assert_eq!(got_samples.len(), fed_samples.len());
for (got, want) in got_samples.iter().zip(fed_samples.iter()) {
assert_eq!(got.data, want.data);
}
}