use alloc::collections::{BTreeMap, BTreeSet, VecDeque};
use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use container_probe::segment;
use transmux::TrackId;
use broadcast_common::Unpackage;
use broadcast_hls::{
ByteRange, EncryptionMethod, MapTag, MasterPlaylist, MediaKey, MediaPlaylist, MediaSegment,
OpenSegment, PreloadHintType,
};
use transmux::sample_aes::{BLOCK_LEN, KEY_LEN, aes128_decrypt_segment};
use transmux::{DemuxEvent, Fmp4Demux, InputDegradation, Sample, StreamingTsDemux, TrackSpec};
use super::action::{Action, BlockingReload, KeyId, ResourceId};
use super::error::{Error, Result};
use super::output::{MasterSelection, Output, RenditionInfo, VariantInfo, VariantPolicy};
use super::url;
const TS_SYNC_BYTE: u8 = 0x47;
const TS_MOVIE_TIMESCALE: u32 = 90_000;
const LIVE_EDGE_TARGET_DURATIONS: f64 = 3.0;
const MAX_PENDING_KEYED_BYTES: usize = 64 * 1024 * 1024;
const MAX_RELOAD_WAIT_MS: u64 = 60_000;
const MILLIS_PER_SECOND: u64 = 1000;
const MOVIE_BOX: [u8; 4] = *b"moov";
const MAP_PROBE_VERSION: u32 = 6;
struct TsDemuxState(StreamingTsDemux);
impl core::fmt::Debug for TsDemuxState {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("TsDemuxState(StreamingTsDemux)")
}
}
fn index_u64(i: usize) -> u64 {
u64::try_from(i).unwrap_or(u64::MAX)
}
fn reload_wait_ms(target_duration: u32) -> u64 {
(u64::from(target_duration.max(1)).saturating_mul(MILLIS_PER_SECOND) / 2)
.min(MAX_RELOAD_WAIT_MS)
}
fn keys_at_maps(text: &str) -> Vec<(MapTag, Option<MediaKey>)> {
const MAP_PREFIX: &str = "#EXT-X-MAP:";
const KEY_PREFIX: &str = "#EXT-X-KEY:";
let mut current: Option<MediaKey> = None;
let mut out = Vec::new();
for line in text.lines() {
if line.starts_with(KEY_PREFIX) {
let probe = format!(
"#EXTM3U\n#EXT-X-TARGETDURATION:1\n{}\n#EXTINF:1,\nx\n",
line.trim_end()
);
if let Ok(parsed) = MediaPlaylist::parse(&probe)
&& let Ok(mut keys) = parsed.segment_keys()
&& let Some(key) = keys.pop()
{
current = key;
}
} else if line.starts_with(MAP_PREFIX) {
let probe = format!(
"#EXTM3U\n#EXT-X-VERSION:{MAP_PROBE_VERSION}\n#EXT-X-TARGETDURATION:1\n{}\n#EXTINF:1,\nx\n",
line.trim_end()
);
if let Ok(parsed) = MediaPlaylist::parse(&probe)
&& let Some(map) = parsed.segments.first().and_then(|seg| seg.map.clone())
{
out.push((map, current.clone()));
}
}
}
out
}
fn has_movie_box(bytes: &[u8]) -> bool {
for step in transmux::box_iter(bytes) {
let Ok((boxed, _)) = step else { return false };
if boxed.header.box_type.0 == MOVIE_BOX {
return true;
}
}
false
}
fn without_query(uri: &str) -> &str {
uri.split_once('?').map_or(uri, |(path, _)| path)
}
fn carried_key_tags(dropped: &[MediaSegment]) -> Vec<String> {
const KEY_PREFIX: &str = "#EXT-X-KEY:";
const KEYFORMAT_ATTR: &str = "KEYFORMAT=\"";
const IDENTITY: &str = "identity";
let mut latest: BTreeMap<&str, &String> = BTreeMap::new();
for tag in dropped.iter().flat_map(|seg| &seg.pre_tags) {
if !tag.starts_with(KEY_PREFIX) {
continue;
}
let format = tag
.split_once(KEYFORMAT_ATTR)
.and_then(|(_, rest)| rest.split_once('"'))
.map_or(IDENTITY, |(format, _)| format);
latest.insert(format, tag);
}
latest.into_values().cloned().collect()
}
fn decrypt_segment(
id: ResourceId,
crypto: SegmentCrypto,
key: &[u8; KEY_LEN],
ciphertext: &[u8],
) -> Result<Vec<u8>> {
aes128_decrypt_segment(key, &crypto.iv, ciphertext).map_err(|_| Error::DecryptFailed {
id,
key_id: crypto.key_id,
})
}
fn resource_msn(id: &ResourceId) -> Option<u64> {
match id {
ResourceId::Part { msn, .. } | ResourceId::Segment { msn } => Some(*msn),
_ => None,
}
}
type InitId = u32;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum InitFetch {
Wanted,
InFlight,
Failed,
Arrived,
Ready,
}
#[derive(Debug)]
struct InitSlot {
map: MapTag,
url: String,
byte_range: Option<(u64, u64)>,
encryption: Option<MediaKey>,
crypto: Option<SegmentCrypto>,
fetch: InitFetch,
cipher: Option<Vec<u8>>,
bytes: Option<Vec<u8>>,
skipped_reported: BTreeSet<(Option<u32>, String)>,
}
#[derive(Debug)]
struct MapEntry {
map: MapTag,
encryption: Option<MediaKey>,
first_msn: u64,
}
impl MapEntry {
fn unusable(&self) -> Option<Error> {
let key = self.encryption.as_ref()?;
let what = if key.method != EncryptionMethod::Aes128 {
"encrypted EXT-X-MAP: only METHOD=AES-128 is decrypted"
} else if key.uri.is_none() {
"encrypted EXT-X-MAP: key without URI"
} else {
return None;
};
Some(Error::UnsupportedEncryption {
method: key.method,
what,
})
}
}
#[derive(Clone, Copy)]
struct KeyBytes([u8; KEY_LEN]);
impl core::fmt::Debug for KeyBytes {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str("KeyBytes(<redacted>)")
}
}
#[derive(Debug, Clone, Copy)]
struct SegmentCrypto {
key_id: KeyId,
iv: [u8; BLOCK_LEN],
}
fn media_sequence_iv(msn: u64) -> [u8; BLOCK_LEN] {
let mut iv = [0u8; BLOCK_LEN];
let msn_bytes = msn.to_be_bytes();
let start = BLOCK_LEN - msn_bytes.len();
iv[start..].copy_from_slice(&msn_bytes);
iv
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Continuity {
Continues,
Lagging,
OlderWindow,
Restart,
}
fn program_date_time(seg: &MediaSegment) -> Option<broadcast_hls::ProgramDateTime> {
seg.program_date_time
}
fn program_date_time_ms(seg: &MediaSegment) -> Option<i64> {
program_date_time(seg).map(|pdt| pdt.timestamp().as_millisecond())
}
#[cfg(test)]
fn parse_rfc3339_ms(text: &str) -> Option<i64> {
text.parse::<jiff::Timestamp>()
.ok()
.map(|t| t.as_millisecond())
}
#[derive(Debug)]
pub struct HlsClient {
playlist_url: String,
base_override: Option<String>,
variant_policy: VariantPolicy,
master: Option<MasterSelection>,
pending_actions: VecDeque<Action>,
#[cfg(feature = "std")]
wait_anchor: Option<std::time::Instant>,
pending_outputs: VecDeque<Output>,
inits: BTreeMap<InitId, InitSlot>,
next_init_id: InitId,
init_target: Option<InitId>,
announced_init: Option<InitId>,
msn_init: BTreeMap<u64, InitId>,
playlist_maps: Vec<MapEntry>,
last_fmp4_specs: Option<Vec<TrackSpec>>,
pending_demux: VecDeque<(ResourceId, Vec<u8>)>,
key_ids: BTreeMap<String, KeyId>,
key_urls: BTreeMap<KeyId, String>,
key_material: BTreeMap<KeyId, KeyBytes>,
keys_in_flight: BTreeSet<KeyId>,
segment_crypto: BTreeMap<u64, SegmentCrypto>,
pending_keyed: Vec<(ResourceId, Vec<u8>)>,
next_key_id: u32,
full_reload_requested: bool,
requested: BTreeSet<ResourceId>,
resource_requests: BTreeMap<ResourceId, (String, Option<(u64, u64)>)>,
fulfilled: BTreeSet<ResourceId>,
joined: bool,
ts_demux: Option<TsDemuxState>,
ts_specs: BTreeMap<TrackId, TrackSpec>,
last_ts_init: Option<Vec<u8>>,
delivered_parts: BTreeSet<(u64, u64)>,
delivered_segments: BTreeSet<u64>,
discontinuous_msns: BTreeSet<u64>,
discontinuity_emitted: BTreeSet<u64>,
byte_range_cursor: BTreeMap<String, u64>,
outstanding_fetches: u64,
saw_endlist: bool,
end_emitted: bool,
last_full_playlist: Option<MediaPlaylist>,
last_media_sequence: Option<u64>,
}
fn choose_variant(variants: &[VariantInfo], policy: &VariantPolicy) -> usize {
if variants.is_empty() {
return 0;
}
let mut candidates: Vec<usize> = Vec::new();
for (i, v) in variants.iter().enumerate() {
let mut ok = true;
if let Some(max_bw) = policy.max_bandwidth
&& v.bandwidth > max_bw
{
ok = false;
}
if ok
&& let Some(max_h) = policy.max_height
&& let Some(res) = v.resolution
&& res.1 > max_h
{
ok = false;
}
if ok
&& let Some(supported) = &policy.supported_codecs
&& let Some(codecs) = &v.codecs
{
for entry in codecs.split(',') {
let entry = entry.trim();
if entry.is_empty() {
continue;
}
if !supported.iter().any(|s| entry.starts_with(s)) {
ok = false;
break;
}
}
}
if ok {
candidates.push(i);
}
}
if !candidates.is_empty() {
let mut best = candidates[0];
for &i in &candidates[1..] {
if variants[i].bandwidth > variants[best].bandwidth {
best = i;
}
}
best
} else {
let mut best = 0usize;
for i in 1..variants.len() {
if variants[i].bandwidth < variants[best].bandwidth {
best = i;
}
}
best
}
}
impl HlsClient {
pub fn new(playlist_url: impl Into<String>) -> Self {
let playlist_url = playlist_url.into();
let mut pending_actions = VecDeque::new();
pending_actions.push_back(Action::FetchPlaylist {
url: playlist_url.clone(),
blocking: None,
skip: false,
});
Self {
playlist_url,
base_override: None,
variant_policy: VariantPolicy::default(),
master: None,
pending_actions,
#[cfg(feature = "std")]
wait_anchor: None,
pending_outputs: VecDeque::new(),
inits: BTreeMap::new(),
next_init_id: 0,
init_target: None,
announced_init: None,
msn_init: BTreeMap::new(),
playlist_maps: Vec::new(),
last_fmp4_specs: None,
pending_demux: VecDeque::new(),
key_ids: BTreeMap::new(),
key_urls: BTreeMap::new(),
key_material: BTreeMap::new(),
keys_in_flight: BTreeSet::new(),
segment_crypto: BTreeMap::new(),
pending_keyed: Vec::new(),
next_key_id: 0,
full_reload_requested: false,
requested: BTreeSet::new(),
resource_requests: BTreeMap::new(),
fulfilled: BTreeSet::new(),
joined: false,
ts_demux: None,
ts_specs: BTreeMap::new(),
last_ts_init: None,
delivered_parts: BTreeSet::new(),
delivered_segments: BTreeSet::new(),
discontinuous_msns: BTreeSet::new(),
discontinuity_emitted: BTreeSet::new(),
byte_range_cursor: BTreeMap::new(),
outstanding_fetches: 0,
saw_endlist: false,
end_emitted: false,
last_full_playlist: None,
last_media_sequence: None,
}
}
pub fn playlist_url(&self) -> &str {
&self.playlist_url
}
pub fn set_playlist_base(&mut self, base: impl Into<String>) {
self.base_override = Some(base.into());
}
fn base(&self) -> &str {
self.base_override.as_deref().unwrap_or(&self.playlist_url)
}
pub fn endlist_seen(&self) -> bool {
self.saw_endlist
}
pub fn target_duration_secs(&self) -> Option<u32> {
self.last_full_playlist.as_ref().map(|p| p.target_duration)
}
pub fn set_variant_policy(&mut self, policy: VariantPolicy) {
self.variant_policy = policy;
}
pub fn select_variant(&mut self, index: usize) -> Result<()> {
let variants_len = self
.master
.as_ref()
.ok_or(Error::NoSuchVariant { index })?
.variants
.len();
if index >= variants_len {
return Err(Error::NoSuchVariant { index });
}
let url = self
.master
.as_ref()
.and_then(|m| m.variants.get(index))
.map(|v| v.url.clone())
.ok_or(Error::NoSuchVariant { index })?;
self.restart_after_regression()?;
self.pending_actions.clear();
self.playlist_url = url.clone();
self.base_override = None;
self.pending_actions.push_back(Action::FetchPlaylist {
url,
blocking: None,
skip: false,
});
if let Some(m) = &mut self.master {
m.chosen = index;
}
Ok(())
}
pub fn poll(&mut self) -> Option<Action> {
#[cfg(feature = "std")]
{
self.wait_anchor = None;
}
self.pending_actions.pop_front()
}
pub fn next_wait(&self) -> Option<core::time::Duration> {
match self.pending_actions.front() {
Some(Action::WaitMs(ms)) => Some(core::time::Duration::from_millis(*ms)),
_ => None,
}
}
#[cfg(feature = "std")]
pub fn poll_timeout(&mut self, now: std::time::Instant) -> Option<std::time::Instant> {
let wait = self.next_wait()?;
let anchor = *self.wait_anchor.get_or_insert(now);
Some(anchor + wait)
}
pub fn next_output(&mut self) -> Option<Output> {
self.pending_outputs.pop_front()
}
pub fn on_playlist(&mut self, bytes: &[u8]) -> Result<()> {
let text = core::str::from_utf8(bytes)?;
if text.contains("#EXT-X-STREAM-INF") {
let master = MasterPlaylist::parse(text)?;
let base = self.base().to_string();
let mut variants: Vec<VariantInfo> = Vec::new();
for v in &master.variants {
variants.push(VariantInfo {
url: url::resolve(&base, &v.uri),
bandwidth: v.bandwidth,
average_bandwidth: v.average_bandwidth,
resolution: v.resolution,
codecs: v.codecs.clone(),
audio_group: v.audio.clone(),
subtitles_group: v.subtitles.clone(),
});
}
let mut renditions: Vec<RenditionInfo> = Vec::new();
for m in &master.media {
renditions.push(RenditionInfo {
media_type: m.media_type,
group_id: m.group_id.clone(),
name: m.name.clone(),
language: m.language.clone(),
default: m.default,
autoselect: m.autoselect,
url: m.uri.as_ref().map(|u| url::resolve(&base, u)),
});
}
let chosen = choose_variant(&variants, &self.variant_policy);
let selection = MasterSelection {
variants,
chosen,
renditions,
};
self.pending_outputs
.push_back(Output::Master(selection.clone()));
self.master = Some(selection);
let Some(chosen_url) = self
.master
.as_ref()
.and_then(|m| m.variants.get(chosen))
.map(|v| v.url.clone())
else {
return Err(Error::NoSuchVariant { index: chosen });
};
self.playlist_url = chosen_url.clone();
self.base_override = None;
self.pending_actions.clear();
self.pending_actions.push_back(Action::FetchPlaylist {
url: chosen_url,
blocking: None,
skip: false,
});
return Ok(());
}
let playlist = MediaPlaylist::parse(text)?;
let playlist = self.merge_delta(playlist);
if playlist.skip.is_some() {
return Err(self.request_full_playlist(&playlist));
}
self.full_reload_requested = false;
let segment_keys = playlist.segment_key_results()?;
if let Some(Err(e)) = segment_keys.first()
&& segment_keys.iter().all(|r| r.is_err())
{
return Err(Error::PlaylistParse(e.clone()));
}
let open_key = if playlist.open_segment.is_some() {
playlist.open_segment_key()?
} else {
None
};
let mut first_error: Option<Error> = None;
let mut lag_floor: Option<u64> = None;
if self.last_media_sequence.is_some() {
match self.continuity(&playlist) {
Continuity::Restart => self.restart_after_regression()?,
Continuity::OlderWindow => {
lag_floor = self.last_full_playlist.as_ref().map(|p| p.media_sequence);
}
Continuity::Lagging | Continuity::Continues => {}
}
}
self.last_media_sequence = Some(
self.last_media_sequence
.map_or(playlist.media_sequence, |p| p.max(playlist.media_sequence)),
);
self.forget_keys_below(playlist.media_sequence);
let next_msn = playlist
.media_sequence
.checked_add(index_u64(playlist.segments.len()))
.ok_or(Error::MediaSequenceOverflow {
media_sequence: playlist.media_sequence,
segments: playlist.segments.len(),
})?;
if !self.joined {
self.joined = true;
self.join_start(&playlist)?;
}
self.playlist_maps = self.map_entries(&playlist, text, next_msn);
for (i, seg) in playlist.segments.iter().enumerate() {
let msn = playlist.media_sequence.saturating_add(index_u64(i));
if lag_floor.is_some_and(|floor| msn < floor) {
self.skip_segment(msn, seg)?;
continue;
}
if let Some(e) = seg.map.as_ref().and_then(|m| self.unusable_map(m)) {
self.skip_segment(msn, seg)?;
first_error.get_or_insert(e);
continue;
}
let key = match segment_keys.get(i) {
Some(Ok(key)) => key.as_ref(),
Some(Err(e)) => {
self.skip_segment(msn, seg)?;
first_error.get_or_insert(Error::PlaylistParse(e.clone()));
continue;
}
None => None,
};
match self.process_closed_segment(msn, seg, key, playlist.iframes_only) {
Ok(()) => {}
Err(e @ Error::UnsupportedEncryption { .. }) => {
self.skip_segment(msn, seg)?;
first_error.get_or_insert(e);
}
Err(e) => return Err(e),
}
}
if let Some(open) = &playlist.open_segment {
if let Some(e) = open.map.as_ref().and_then(|m| self.unusable_map(m)) {
first_error.get_or_insert(e);
} else {
match &open_key {
Some(key) => {
first_error.get_or_insert(Error::UnsupportedEncryption {
method: key.method,
what: "encrypted partial segments",
});
}
None => self.process_open_segment(next_msn, open)?,
}
}
}
self.pump_init_fetch();
if let Some(ll) = &playlist.low_latency
&& let Some(hint_uri) = &ll.preload_hint_part
{
match ll.preload_hint_type {
PreloadHintType::Part => {
let part_idx = playlist
.open_segment
.as_ref()
.map(|o| index_u64(o.parts.len()))
.unwrap_or(0);
let id = ResourceId::Part {
msn: next_msn,
part: part_idx,
};
if !self.requested.contains(&id)
&& !self.delivered_parts.contains(&(next_msn, part_idx))
{
let url = url::resolve(self.base(), hint_uri);
let byte_range = self.resolve_hint_byte_range(&url, ll)?;
self.request_resource(id, url, byte_range);
}
}
PreloadHintType::Map => {
let map = MapTag {
uri: hint_uri.clone(),
byte_range: ll.preload_hint_byte_range_length.map(|length| ByteRange {
length,
offset: ll.preload_hint_byte_range_start,
}),
extra_attrs: Vec::new(),
};
self.ensure_init_requested(&map)?;
}
_ => {
}
}
}
if playlist.endlist {
self.saw_endlist = true;
} else {
let blocking = playlist
.low_latency
.as_ref()
.filter(|ll| ll.can_block_reload)
.map(|_| {
let part = playlist
.open_segment
.as_ref()
.map(|o| index_u64(o.parts.len()))
.unwrap_or(0);
BlockingReload {
msn: next_msn,
part: Some(part),
}
});
let can_skip = playlist
.low_latency
.as_ref()
.and_then(|ll| ll.can_skip_until)
.is_some();
let skip = can_skip && self.last_full_playlist.is_some();
self.pending_actions.push_back(Action::FetchPlaylist {
url: self.playlist_url.clone(),
blocking,
skip,
});
if blocking.is_none() {
self.pending_actions
.push_back(Action::WaitMs(reload_wait_ms(playlist.target_duration)));
}
}
self.prune(&playlist);
self.last_full_playlist = Some(playlist);
self.maybe_emit_end_of_stream()?;
match first_error {
Some(e) => Err(e),
None => Ok(()),
}
}
fn request_full_playlist(&mut self, delta: &MediaPlaylist) -> Error {
if self.full_reload_requested {
self.pending_actions
.push_back(Action::WaitMs(reload_wait_ms(delta.target_duration)));
}
self.full_reload_requested = true;
self.pending_actions.push_back(Action::FetchPlaylist {
url: self.playlist_url.clone(),
blocking: None,
skip: false,
});
Error::DeltaNotMergeable {
media_sequence: delta.media_sequence,
skipped_segments: delta.skip.as_ref().map_or(0, |s| s.skipped_segments),
}
}
pub fn ensure_playlist_reload(&mut self) -> bool {
if self
.pending_actions
.iter()
.any(|a| matches!(a, Action::FetchPlaylist { .. }))
{
return false;
}
self.pending_actions.push_back(Action::FetchPlaylist {
url: self.playlist_url.clone(),
blocking: None,
skip: false,
});
true
}
pub fn retry_init(&mut self) -> bool {
if self
.inits
.values()
.any(|slot| slot.fetch == InitFetch::InFlight)
{
return false;
}
let failed: Vec<InitId> = self
.inits
.iter()
.filter(|(_, slot)| slot.fetch == InitFetch::Failed)
.map(|(id, _)| *id)
.collect();
for id in &failed {
if let Some(slot) = self.inits.get_mut(id) {
slot.fetch = InitFetch::Wanted;
}
}
self.pump_init_fetch();
!failed.is_empty()
}
pub fn retry_resource(&mut self, id: ResourceId) -> bool {
if self.requested.contains(&id) {
return false;
}
let Some((url, byte_range)) = self.resource_requests.get(&id).cloned() else {
return false;
};
self.request_resource(id, url, byte_range);
true
}
pub fn skip_resource(&mut self, id: ResourceId) {
match id {
ResourceId::Init => return,
ResourceId::Segment { msn } => {
self.delivered_segments.insert(msn);
}
ResourceId::Part { msn, part } => {
self.delivered_parts.insert((msn, part));
}
}
self.requested.remove(&id);
self.resource_requests.remove(&id);
let _ = self.restart_ts_demux(id);
self.pending_outputs.push_back(Output::Discontinuity);
let _ = self.maybe_emit_end_of_stream();
}
fn map_entries(&self, playlist: &MediaPlaylist, text: &str, next_msn: u64) -> Vec<MapEntry> {
let keyed = keys_at_maps(text);
let mut entries: Vec<MapEntry> = Vec::new();
let listed = playlist
.segments
.iter()
.enumerate()
.filter_map(|(i, s)| {
s.map
.as_ref()
.map(|m| (m, playlist.media_sequence.saturating_add(index_u64(i))))
})
.chain(
playlist
.open_segment
.as_ref()
.and_then(|o| o.map.as_ref())
.map(|m| (m, next_msn)),
);
for (map, msn) in listed {
if entries.iter().any(|e| e.map == *map) {
continue;
}
let key = match keyed.iter().rev().find(|(m, _)| m == map) {
Some((_, key)) => key.clone(),
None => self
.init_slot_for(&url::resolve(self.base(), &map.uri), &map.byte_range)
.and_then(|id| self.inits.get(&id))
.and_then(|slot| slot.encryption.clone()),
};
entries.push(MapEntry {
map: map.clone(),
encryption: key,
first_msn: msn,
});
}
entries
}
fn unusable_map(&self, map: &MapTag) -> Option<Error> {
self.playlist_maps
.iter()
.find(|e| e.map == *map)
.and_then(MapEntry::unusable)
}
fn init_slot_for(&self, url: &str, byte_range: &Option<ByteRange>) -> Option<InitId> {
self.inits
.iter()
.find(|(_, slot)| {
without_query(&slot.url) == without_query(url) && slot.map.byte_range == *byte_range
})
.map(|(id, _)| *id)
}
fn init_for_map(&mut self, map: &MapTag) -> Result<InitId> {
let url = url::resolve(self.base(), &map.uri);
if let Some(id) = self.init_slot_for(&url, &map.byte_range)
&& let Some(slot) = self.inits.get_mut(&id)
{
if slot.url != url {
slot.url = url;
slot.map = map.clone();
}
if slot.fetch == InitFetch::Failed {
slot.fetch = InitFetch::Wanted;
}
return Ok(id);
}
let (key, first_msn) = self
.playlist_maps
.iter()
.find(|e| e.map == *map)
.map_or((None, 0), |e| (e.encryption.clone(), e.first_msn));
let byte_range = self.resolve_byte_range(&url, &map.byte_range)?;
let crypto = match key.as_ref().and_then(|k| k.uri.as_deref().map(|u| (k, u))) {
Some((key, uri)) => {
let key_id = self.ensure_key_requested(uri);
let iv = key.iv.unwrap_or_else(|| media_sequence_iv(first_msn));
Some(SegmentCrypto { key_id, iv })
}
None => None,
};
let id = self.next_init_id;
self.next_init_id = self.next_init_id.saturating_add(1);
self.inits.insert(
id,
InitSlot {
map: map.clone(),
url,
byte_range,
encryption: key,
crypto,
fetch: InitFetch::Wanted,
cipher: None,
bytes: None,
skipped_reported: BTreeSet::new(),
},
);
Ok(id)
}
fn pump_init_fetch(&mut self) {
if self
.inits
.values()
.any(|slot| slot.fetch == InitFetch::InFlight)
{
return;
}
let Some((id, slot)) = self
.inits
.iter_mut()
.find(|(_, slot)| slot.fetch == InitFetch::Wanted)
else {
return;
};
slot.fetch = InitFetch::InFlight;
let id = *id;
let fetch = Action::FetchResource {
id: ResourceId::Init,
url: slot.url.clone(),
byte_range: slot.byte_range,
};
self.init_target = Some(id);
self.pending_actions.push_back(fetch);
self.outstanding_fetches += 1;
}
fn bind_init(&mut self, msn: u64, map: Option<&MapTag>) -> Result<()> {
if let Some(map) = map {
let id = self.init_for_map(map)?;
self.msn_init.insert(msn, id);
}
Ok(())
}
fn init_of(&self, id: &ResourceId) -> Option<InitId> {
resource_msn(id)
.and_then(|msn| self.msn_init.get(&msn).copied())
.or_else(|| self.inits.keys().next_back().copied())
}
fn init_ready(&self, slot: Option<InitId>) -> bool {
slot.and_then(|id| self.inits.get(&id))
.is_some_and(|slot| slot.bytes.is_some())
}
pub fn on_resource(&mut self, id: ResourceId, bytes: &[u8]) -> Result<()> {
let was_requested = match id {
ResourceId::Init => self.init_target.is_some(),
ResourceId::Part { .. } | ResourceId::Segment { .. } => self.requested.contains(&id),
};
if !was_requested {
return Err(Error::UnrequestedResource { id });
}
let duplicate = match id {
ResourceId::Init => !self
.init_target
.and_then(|t| self.inits.get(&t))
.is_some_and(|slot| slot.fetch == InitFetch::InFlight),
ResourceId::Part { .. } | ResourceId::Segment { .. } => self.fulfilled.contains(&id),
};
if duplicate {
return Err(Error::DuplicateResource { id });
}
self.outstanding_fetches = self.outstanding_fetches.saturating_sub(1);
if id != ResourceId::Init {
self.fulfilled.insert(id);
}
match id {
ResourceId::Init => self.accept_init(bytes)?,
ResourceId::Part { .. } | ResourceId::Segment { .. } => {
let processed = self.accept_media_resource(id, bytes);
match processed {
Ok(()) => {}
Err(e @ Error::PendingKeyedOverflow { .. }) => return Err(e),
Err(e) => {
self.fulfilled.remove(&id);
self.requested.remove(&id);
return Err(e);
}
}
}
}
self.maybe_emit_end_of_stream()?;
Ok(())
}
fn accept_init(&mut self, bytes: &[u8]) -> Result<()> {
let Some(target) = self.init_target else {
return Ok(());
};
let Some(slot) = self.inits.get_mut(&target) else {
return Ok(());
};
if slot.crypto.is_none() && !has_movie_box(bytes) {
slot.fetch = InitFetch::Failed;
return Err(Error::InitUndecodable);
}
match slot.crypto {
Some(crypto) => match self.key_material.get(&crypto.key_id).copied() {
Some(KeyBytes(key)) => self.complete_encrypted_init(target, crypto, &key, bytes),
None => {
slot.cipher = Some(bytes.to_vec());
slot.fetch = InitFetch::Arrived;
Ok(())
}
},
None => self.complete_init(target, bytes),
}
}
fn complete_init(&mut self, target: InitId, plain: &[u8]) -> Result<()> {
if let Some(slot) = self.inits.get_mut(&target) {
slot.bytes = Some(plain.to_vec());
slot.cipher = None;
slot.fetch = InitFetch::Ready;
}
if self.announced_init.is_none() {
self.announce_init(target);
}
self.pump_init_fetch();
let buffered: Vec<_> = self.pending_demux.drain(..).collect();
let mut waiting = VecDeque::new();
let mut failure = None;
for (bid, bbytes) in buffered {
if failure.is_some() || !self.init_ready(self.init_of(&bid)) {
waiting.push_back((bid, bbytes));
continue;
}
if let Err(e) = self.finish_media_resource(bid, &bbytes) {
self.fulfilled.remove(&bid);
self.requested.remove(&bid);
failure = Some(e);
}
}
self.pending_demux = waiting;
match failure {
Some(e) => Err(e),
None => Ok(()),
}
}
fn announce_init(&mut self, slot: InitId) {
if self.announced_init == Some(slot) {
return;
}
let Some(bytes) = self.inits.get(&slot).and_then(|s| s.bytes.clone()) else {
return;
};
self.pending_outputs.push_back(Output::Init(bytes));
self.announced_init = Some(slot);
self.last_fmp4_specs = None;
}
fn complete_encrypted_init(
&mut self,
target: InitId,
crypto: SegmentCrypto,
key: &[u8; KEY_LEN],
cipher: &[u8],
) -> Result<()> {
match decrypt_segment(ResourceId::Init, crypto, key, cipher) {
Ok(plain) if !has_movie_box(&plain) => {
if let Some(slot) = self.inits.get_mut(&target) {
slot.fetch = InitFetch::Failed;
}
Err(Error::InitUndecodable)
}
Ok(plain) => self.complete_init(target, &plain),
Err(e) => {
if let Some(slot) = self.inits.get_mut(&target) {
slot.fetch = InitFetch::Failed;
}
Err(e)
}
}
}
fn accept_media_resource(&mut self, id: ResourceId, bytes: &[u8]) -> Result<()> {
if let ResourceId::Segment { msn } = id
&& let Some(crypto) = self.segment_crypto.get(&msn).copied()
{
return match self.key_material.get(&crypto.key_id).copied() {
Some(KeyBytes(key)) => {
let plain = decrypt_segment(id, crypto, &key, bytes)?;
self.route_media_resource(id, &plain)
}
None => self.buffer_until_keyed(id, bytes),
};
}
self.route_media_resource(id, bytes)
}
fn buffer_until_keyed(&mut self, id: ResourceId, bytes: &[u8]) -> Result<()> {
if bytes.len() > MAX_PENDING_KEYED_BYTES {
self.fulfilled.remove(&id);
self.requested.remove(&id);
return Err(Error::PendingKeyedOverflow { dropped: id });
}
let mut held: usize = self.pending_keyed.iter().map(|(_, b)| b.len()).sum();
let mut dropped: Option<ResourceId> = None;
while held.saturating_add(bytes.len()) > MAX_PENDING_KEYED_BYTES
&& !self.pending_keyed.is_empty()
{
let (old_id, old) = self.pending_keyed.remove(0);
held = held.saturating_sub(old.len());
self.fulfilled.remove(&old_id);
self.requested.remove(&old_id);
dropped.get_or_insert(old_id);
}
self.pending_keyed.push((id, bytes.to_vec()));
match dropped {
Some(dropped) => Err(Error::PendingKeyedOverflow { dropped }),
None => Ok(()),
}
}
fn route_media_resource(&mut self, id: ResourceId, bytes: &[u8]) -> Result<()> {
let recognised = if self.is_ts_segment(bytes) {
segment::ts_structure(bytes)
} else {
segment::media_fragment(bytes) || segment::progressive_movie(bytes)
};
if !recognised {
return Err(Error::NotMedia { id });
}
if self.is_ts_segment(bytes) {
self.finish_ts_resource(id, bytes)
} else if !self.init_ready(self.init_of(&id)) {
self.pending_demux.push_back((id, bytes.to_vec()));
Ok(())
} else {
self.finish_media_resource(id, bytes)
}
}
pub fn on_key(&mut self, id: KeyId, bytes: &[u8]) -> Result<()> {
if !self.key_urls.contains_key(&id) || self.key_material.contains_key(&id) {
return Err(Error::UnrequestedKey { id });
}
if self.keys_in_flight.remove(&id) {
self.outstanding_fetches = self.outstanding_fetches.saturating_sub(1);
}
let key: [u8; KEY_LEN] = bytes.try_into().map_err(|_| Error::BadKeyLength {
id,
len: bytes.len(),
})?;
self.key_material.insert(id, KeyBytes(key));
let waiting_inits: Vec<(InitId, SegmentCrypto)> = self
.inits
.iter()
.filter(|(_, slot)| slot.fetch == InitFetch::Arrived)
.filter_map(|(init, slot)| slot.crypto.filter(|c| c.key_id == id).map(|c| (*init, c)))
.collect();
for (init, crypto) in waiting_inits {
let cipher = self
.inits
.get_mut(&init)
.and_then(|slot| slot.cipher.take());
if let Some(cipher) = cipher {
self.complete_encrypted_init(init, crypto, &key, &cipher)?;
}
}
let (mut ready, rest): (Vec<_>, Vec<_>) = core::mem::take(&mut self.pending_keyed)
.into_iter()
.partition(|(rid, _)| {
resource_msn(rid)
.and_then(|msn| self.segment_crypto.get(&msn))
.is_some_and(|c| c.key_id == id)
});
self.pending_keyed = rest;
ready.sort_by_key(|(rid, _)| resource_msn(rid));
let mut ready = ready.into_iter();
while let Some((rid, cipher)) = ready.next() {
let crypto = resource_msn(&rid).and_then(|msn| self.segment_crypto.get(&msn).copied());
let result = match crypto {
Some(crypto) => decrypt_segment(rid, crypto, &key, &cipher)
.and_then(|plain| self.route_media_resource(rid, &plain)),
None => Ok(()),
};
if let Err(e) = result {
self.fulfilled.remove(&rid);
self.requested.remove(&rid);
self.pending_keyed.extend(ready);
return Err(e);
}
}
self.maybe_emit_end_of_stream()?;
Ok(())
}
pub fn on_key_error(&mut self, id: KeyId) {
if self.keys_in_flight.remove(&id) {
self.outstanding_fetches = self.outstanding_fetches.saturating_sub(1);
}
}
fn ensure_key_requested(&mut self, uri: &str) -> KeyId {
let url = url::resolve(self.base(), uri);
let id = match self.key_ids.get(&url) {
Some(id) => *id,
None => {
let id = KeyId(self.next_key_id);
self.next_key_id = self.next_key_id.saturating_add(1);
self.key_ids.insert(url.clone(), id);
self.key_urls.insert(id, url);
id
}
};
self.request_key(id);
id
}
fn request_key(&mut self, id: KeyId) {
let Some(url) = self.key_urls.get(&id).cloned() else {
return;
};
if !self.key_material.contains_key(&id) && self.keys_in_flight.insert(id) {
self.outstanding_fetches += 1;
self.pending_actions.push_back(Action::FetchKey { id, url });
}
}
fn forget_keys_below(&mut self, first_msn: u64) {
self.segment_crypto.retain(|&m, _| m >= first_msn);
let mut live: BTreeSet<KeyId> = self.segment_crypto.values().map(|c| c.key_id).collect();
live.extend(
self.inits
.values()
.filter_map(|slot| slot.crypto.map(|c| c.key_id)),
);
let abandoned: Vec<KeyId> = self
.keys_in_flight
.iter()
.copied()
.filter(|id| !live.contains(id))
.collect();
for id in abandoned {
self.keys_in_flight.remove(&id);
self.outstanding_fetches = self.outstanding_fetches.saturating_sub(1);
self.pending_actions
.retain(|a| !matches!(a, Action::FetchKey { id: queued, .. } if *queued == id));
}
self.key_ids.retain(|_, id| live.contains(id));
self.key_urls.retain(|id, _| live.contains(id));
self.key_material.retain(|id, _| live.contains(id));
}
fn prepare_encrypted_segment(
&mut self,
msn: u64,
seg: &MediaSegment,
key: &MediaKey,
iframes_only: bool,
) -> Result<()> {
let unsupported = |what| Error::UnsupportedEncryption {
method: key.method,
what,
};
if key.method != EncryptionMethod::Aes128 {
return Err(unsupported("only METHOD=AES-128 is decrypted"));
}
if iframes_only {
return Err(unsupported("EXT-X-I-FRAMES-ONLY with encryption"));
}
if !seg.parts.is_empty() {
return Err(unsupported("encrypted partial segments"));
}
let Some(uri) = key.uri.as_deref() else {
return Err(unsupported("key without URI"));
};
if let Some(existing) = self.segment_crypto.get(&msn).copied() {
self.request_key(existing.key_id);
return Ok(());
}
let id = self.ensure_key_requested(uri);
let iv = key.iv.unwrap_or_else(|| media_sequence_iv(msn));
self.segment_crypto
.insert(msn, SegmentCrypto { key_id: id, iv });
Ok(())
}
fn finish_media_resource(&mut self, id: ResourceId, bytes: &[u8]) -> Result<()> {
match id {
ResourceId::Part { msn, part } => {
self.emit_discontinuity_if_needed(msn);
self.demux_and_emit(id, bytes)?;
self.delivered_parts.insert((msn, part));
}
ResourceId::Segment { msn } => {
self.emit_discontinuity_if_needed(msn);
self.demux_and_emit(id, bytes)?;
self.delivered_segments.insert(msn);
}
ResourceId::Init => {}
}
Ok(())
}
fn finish_ts_resource(&mut self, id: ResourceId, bytes: &[u8]) -> Result<()> {
match id {
ResourceId::Part { msn, part } => {
self.begin_ts_resource(id, msn)?;
self.demux_and_emit_ts(id, bytes)?;
self.delivered_parts.insert((msn, part));
}
ResourceId::Segment { msn } => {
self.begin_ts_resource(id, msn)?;
self.demux_and_emit_ts(id, bytes)?;
self.release_ts_segment_end(id)?;
self.delivered_segments.insert(msn);
}
ResourceId::Init => {}
}
Ok(())
}
fn is_ts_segment(&self, bytes: &[u8]) -> bool {
self.inits.is_empty()
&& (bytes.first() == Some(&TS_SYNC_BYTE) || segment::ts_structure(bytes))
}
pub fn on_error(&mut self, id: Option<ResourceId>) {
if let Some(id) = id {
self.outstanding_fetches = self.outstanding_fetches.saturating_sub(1);
match id {
ResourceId::Init => {
if let Some(slot) = self
.init_target
.and_then(|target| self.inits.get_mut(&target))
.filter(|slot| slot.fetch == InitFetch::InFlight)
{
slot.fetch = InitFetch::Failed;
}
self.pump_init_fetch();
}
other => {
self.requested.remove(&other);
}
}
}
let _ = self.maybe_emit_end_of_stream();
}
fn merge_delta(&self, playlist: MediaPlaylist) -> MediaPlaylist {
let Some(skip) = &playlist.skip else {
return playlist;
};
if skip.skipped_segments == 0 {
let mut merged = playlist;
merged.skip = None;
return merged;
}
let Some(prev) = &self.last_full_playlist else {
return playlist;
};
if playlist.media_sequence < prev.media_sequence {
return playlist;
}
let Ok(prefix_start) = usize::try_from(playlist.media_sequence - prev.media_sequence)
else {
return playlist;
};
let prefix_end = usize::try_from(skip.skipped_segments)
.ok()
.and_then(|skipped| prefix_start.checked_add(skipped));
let Some(prefix) = prefix_end.and_then(|end| prev.segments.get(prefix_start..end)) else {
return playlist;
};
let mut merged = playlist;
let mut segments = prefix.to_vec();
if let (Some(first), Some(dropped)) =
(segments.first_mut(), prev.segments.get(..prefix_start))
{
let mut carried = carried_key_tags(dropped);
carried.append(&mut first.pre_tags);
first.pre_tags = carried;
}
segments.extend(merged.segments);
merged.segments = segments;
merged.skip = None;
merged
}
fn process_closed_segment(
&mut self,
msn: u64,
seg: &MediaSegment,
key: Option<&MediaKey>,
iframes_only: bool,
) -> Result<()> {
if seg.discontinuous {
self.discontinuous_msns.insert(msn);
}
if self.delivered_segments.contains(&msn) {
return Ok(());
}
match key {
Some(key) => self.prepare_encrypted_segment(msn, seg, key, iframes_only)?,
None => {
self.segment_crypto.remove(&msn);
}
}
if seg.parts.is_empty() {
let already_have_parts = self
.delivered_parts
.range((msn, 0)..=(msn, u64::MAX))
.next()
.is_some();
if already_have_parts {
self.delivered_segments.insert(msn);
return Ok(());
}
let id = ResourceId::Segment { msn };
if !self.requested.contains(&id) {
self.bind_init(msn, seg.map.as_ref())?;
let url = url::resolve(self.base(), &seg.uri);
let byte_range = self.resolve_byte_range(&url, &seg.byte_range)?;
self.request_resource(id, url, byte_range);
}
return Ok(());
}
let mut fully_accounted = true;
for (i, part) in seg.parts.iter().enumerate() {
let i = index_u64(i);
if part.gap || self.delivered_parts.contains(&(msn, i)) {
continue;
}
fully_accounted = false;
let id = ResourceId::Part { msn, part: i };
if !self.requested.contains(&id) {
self.bind_init(msn, seg.map.as_ref())?;
let url = url::resolve(self.base(), &part.uri);
let byte_range = self.resolve_byte_range(&url, &part.byte_range)?;
self.request_resource(id, url, byte_range);
}
}
if fully_accounted {
self.delivered_segments.insert(msn);
}
Ok(())
}
fn process_open_segment(&mut self, msn: u64, open: &OpenSegment) -> Result<()> {
for (i, part) in open.parts.iter().enumerate() {
let i = index_u64(i);
if part.gap || self.delivered_parts.contains(&(msn, i)) {
continue;
}
let id = ResourceId::Part { msn, part: i };
if !self.requested.contains(&id) {
self.bind_init(msn, open.map.as_ref())?;
let url = url::resolve(self.base(), &part.uri);
let byte_range = self.resolve_byte_range(&url, &part.byte_range)?;
self.request_resource(id, url, byte_range);
}
}
Ok(())
}
fn ensure_init_requested(&mut self, map: &MapTag) -> Result<()> {
self.init_for_map(map)?;
self.pump_init_fetch();
Ok(())
}
fn request_resource(&mut self, id: ResourceId, url: String, byte_range: Option<(u64, u64)>) {
self.requested.insert(id);
self.resource_requests.insert(id, (url.clone(), byte_range));
self.outstanding_fetches += 1;
self.pending_actions.push_back(Action::FetchResource {
id,
url,
byte_range,
});
}
fn resolve_byte_range(
&mut self,
url: &str,
br: &Option<ByteRange>,
) -> Result<Option<(u64, u64)>> {
let Some(br) = br.as_ref() else {
return Ok(None);
};
let offset = br
.offset
.unwrap_or_else(|| *self.byte_range_cursor.get(url).unwrap_or(&0));
let next_cursor =
offset
.checked_add(br.length)
.ok_or_else(|| Error::ByteRangeOverflow {
url: url.to_string(),
offset,
length: br.length,
})?;
self.byte_range_cursor.insert(url.to_string(), next_cursor);
Ok(Some((offset, br.length)))
}
fn resolve_hint_byte_range(
&mut self,
url: &str,
ll: &broadcast_hls::LowLatencyConfig,
) -> Result<Option<(u64, u64)>> {
let Some(length) = ll.preload_hint_byte_range_length else {
return Ok(None);
};
let br = ByteRange {
length,
offset: ll.preload_hint_byte_range_start,
};
self.resolve_byte_range(url, &Some(br))
}
fn demux_and_emit(&mut self, id: ResourceId, bytes: &[u8]) -> Result<()> {
let slot = self.init_of(&id).filter(|s| self.init_ready(Some(*s)));
let Some(slot) = slot else {
return Err(Error::InitNotYetAvailable { id });
};
self.announce_init(slot);
let init = self
.inits
.get(&slot)
.and_then(|s| s.bytes.as_ref())
.ok_or(Error::InitNotYetAvailable { id })?;
if segment::progressive_movie(bytes) {
return Err(Error::NotFragmented { id });
}
let mut combined = Vec::with_capacity(init.len() + bytes.len());
combined.extend_from_slice(init);
combined.extend_from_slice(bytes);
let mut demux = Fmp4Demux::new();
let media = demux
.unpackage(combined.as_slice())
.map_err(|source| Error::Demux { id, source })?;
let damaged = media
.degraded
.iter()
.filter(|d| matches!(d, InputDegradation::MalformedUnit { .. }))
.count();
if damaged > 0 {
self.pending_outputs.push_back(Output::FragmentDamaged {
id,
skipped: damaged,
});
}
for skipped in &media.skipped {
let key = (skipped.track_id.map(TrackId::value), skipped.fourcc.clone());
let first = self
.inits
.get_mut(&slot)
.is_some_and(|s| s.skipped_reported.insert(key));
if first {
self.pending_outputs.push_back(Output::TrackSkipped {
track_id: skipped.track_id,
fourcc: skipped.fourcc.clone(),
reason: skipped.reason.clone(),
});
}
}
let specs: Vec<TrackSpec> = media.tracks.iter().map(|t| t.spec.clone()).collect();
let specs_changed = self
.last_fmp4_specs
.as_ref()
.map(|prev| format!("{:?}", prev) != format!("{:?}", specs))
.unwrap_or(true);
if specs_changed {
self.pending_outputs
.push_back(Output::Tracks(specs.clone()));
self.last_fmp4_specs = Some(specs);
}
for track in media.tracks {
if !track.samples.is_empty() {
self.pending_outputs.push_back(Output::Samples {
track_id: track.spec.track_id,
samples: track.samples,
});
}
}
Ok(())
}
fn begin_ts_resource(&mut self, id: ResourceId, msn: u64) -> Result<()> {
if self.discontinuous_msns.contains(&msn) && !self.discontinuity_emitted.contains(&msn) {
self.restart_ts_demux(id)?;
self.emit_discontinuity_if_needed(msn);
}
Ok(())
}
fn restart_ts_demux(&mut self, id: ResourceId) -> Result<()> {
let Some(TsDemuxState(demux)) = self.ts_demux.as_mut() else {
return Ok(());
};
demux.restart_timeline();
self.drain_ts_demux(id)
}
fn demux_and_emit_ts(&mut self, id: ResourceId, bytes: &[u8]) -> Result<()> {
let TsDemuxState(demux) = self
.ts_demux
.get_or_insert_with(|| TsDemuxState(StreamingTsDemux::new()));
demux.feed(bytes);
self.drain_ts_demux(id)
}
fn release_ts_segment_end(&mut self, id: ResourceId) -> Result<()> {
let Some(TsDemuxState(demux)) = self.ts_demux.as_mut() else {
return Ok(());
};
demux.release_silent_heads();
self.drain_ts_demux(id)
}
fn flush_ts_demux(&mut self, id: ResourceId) -> Result<()> {
let Some(TsDemuxState(demux)) = self.ts_demux.as_mut() else {
return Ok(());
};
demux.finish();
self.drain_ts_demux(id)
}
fn drain_ts_demux(&mut self, id: ResourceId) -> Result<()> {
let Some(TsDemuxState(demux)) = self.ts_demux.as_mut() else {
return Ok(());
};
let mut per_track: BTreeMap<TrackId, Vec<Sample>> = BTreeMap::new();
let mut events: Vec<DemuxEvent> = Vec::new();
while let Some(event) = demux.poll_event() {
match event {
DemuxEvent::TrackAdded(spec) | DemuxEvent::TrackUpdated(spec) => {
if spec.config.is_muxable_in_bmff() {
self.ts_specs.insert(spec.track_id, spec);
} else {
self.ts_specs.remove(&spec.track_id);
let stream_type = match &spec.config {
transmux::CodecConfig::Data { stream_type, .. } => Some(*stream_type),
_ => None,
};
self.pending_outputs.push_back(Output::TrackDropped {
track_id: spec.track_id,
stream_type,
});
}
}
DemuxEvent::TrackRemoved { track_id, .. } => {
self.ts_specs.remove(&track_id);
}
DemuxEvent::Sample { track_id, .. }
| DemuxEvent::DisplayChanged { track_id, .. }
if self.ts_specs.contains_key(&track_id) =>
{
events.push(event);
}
_ => {}
}
}
if !self.ts_specs.is_empty() {
let specs: Vec<TrackSpec> = self.ts_specs.values().cloned().collect();
let init_bytes = transmux::build_init_segment(&specs, TS_MOVIE_TIMESCALE)
.map_err(|source| Error::Demux { id, source })?;
if self.last_ts_init.as_deref() != Some(init_bytes.as_slice()) {
self.pending_outputs
.push_back(Output::Tracks(specs.clone()));
self.pending_outputs
.push_back(Output::Init(init_bytes.clone()));
self.last_ts_init = Some(init_bytes);
}
}
for event in events {
match event {
DemuxEvent::Sample {
track_id, sample, ..
} => {
per_track.entry(track_id).or_default().push(sample);
}
DemuxEvent::DisplayChanged {
track_id,
display,
pts,
..
} => {
if let Some(samples) = per_track.remove(&track_id)
&& !samples.is_empty()
{
self.pending_outputs
.push_back(Output::Samples { track_id, samples });
}
self.pending_outputs.push_back(Output::DisplayChanged {
track_id,
pts,
display,
});
}
_ => {}
}
}
for (track_id, samples) in per_track {
if !samples.is_empty() {
self.pending_outputs
.push_back(Output::Samples { track_id, samples });
}
}
Ok(())
}
fn emit_discontinuity_if_needed(&mut self, msn: u64) -> bool {
if self.discontinuous_msns.contains(&msn) && !self.discontinuity_emitted.contains(&msn) {
self.pending_outputs.push_back(Output::Discontinuity);
self.discontinuity_emitted.insert(msn);
return true;
}
false
}
fn join_start(&mut self, playlist: &MediaPlaylist) -> Result<()> {
let segments = &playlist.segments;
let target = f64::from(playlist.target_duration.max(1));
let duration_of = |seg: &MediaSegment| {
let d = seg.duration.get();
if d > 0.0 { d } else { target }
};
let from_start_tag = playlist.start.as_ref().map(|start| {
let total: f64 = segments.iter().map(duration_of).sum();
let offset = start.time_offset.get();
let position = if offset >= 0.0 {
offset
} else {
total + offset
};
let position = position.clamp(0.0, total);
let mut elapsed = 0.0_f64;
for (i, seg) in segments.iter().enumerate() {
elapsed += duration_of(seg);
if elapsed > position {
return i;
}
}
segments.len().saturating_sub(1)
});
let from_hold_back = (!playlist.endlist).then(|| {
let ll = playlist.low_latency.as_ref();
let wanted = match ll.filter(|ll| ll.can_block_reload) {
Some(ll) if ll.part_hold_back.is_some() => {
ll.part_hold_back.map_or(0.0, |p| p.get())
}
_ => {
let hold_back = ll.and_then(|ll| ll.hold_back).map_or(0.0, |h| h.get());
(LIVE_EDGE_TARGET_DURATIONS * target).max(hold_back)
}
};
let mut behind: f64 = playlist
.open_segment
.as_ref()
.map_or(0.0, |o| o.parts.iter().map(|p| p.duration.get()).sum());
if behind >= wanted {
return segments.len();
}
for (i, seg) in segments.iter().enumerate().rev() {
behind += duration_of(seg);
if behind >= wanted {
return i;
}
}
0
});
let start = match (from_start_tag, from_hold_back) {
(Some(s), Some(h)) => s.min(h),
(Some(s), None) => s,
(None, Some(h)) => h,
(None, None) => 0,
};
for (i, seg) in segments.iter().enumerate().take(start) {
let msn = playlist.media_sequence.saturating_add(index_u64(i));
self.skip_segment(msn, seg)?;
}
Ok(())
}
fn skip_segment(&mut self, msn: u64, seg: &MediaSegment) -> Result<()> {
if seg.parts.is_empty() {
let url = url::resolve(self.base(), &seg.uri);
self.resolve_byte_range(&url, &seg.byte_range)?;
} else {
for part in seg.parts.iter().filter(|p| !p.gap) {
let url = url::resolve(self.base(), &part.uri);
self.resolve_byte_range(&url, &part.byte_range)?;
}
}
self.delivered_segments.insert(msn);
Ok(())
}
fn prune(&mut self, playlist: &MediaPlaylist) {
let first = playlist.media_sequence;
let below = |id: &ResourceId| resource_msn(id).is_some_and(|m| m < first);
let fulfilled = &self.fulfilled;
self.requested
.retain(|id| !(below(id) && fulfilled.contains(id)));
self.resource_requests.retain(|id, _| !below(id));
self.fulfilled.retain(|id| !below(id));
self.pending_keyed.retain(|(id, _)| !below(id));
self.segment_crypto.retain(|&m, _| m >= first);
self.delivered_parts.retain(|&(m, _)| m >= first);
self.delivered_segments.retain(|&m| m >= first);
self.discontinuous_msns.retain(|&m| m >= first);
self.discontinuity_emitted.retain(|&m| m >= first);
self.msn_init.retain(|&m, _| m >= first);
self.prune_inits();
if playlist.skip.is_none() {
let base = self.base();
let mut live: BTreeSet<String> = BTreeSet::new();
for seg in &playlist.segments {
live.insert(url::resolve(base, &seg.uri));
for part in &seg.parts {
live.insert(url::resolve(base, &part.uri));
}
if let Some(map) = &seg.map {
live.insert(url::resolve(base, &map.uri));
}
}
if let Some(open) = &playlist.open_segment {
for part in &open.parts {
live.insert(url::resolve(base, &part.uri));
}
if let Some(map) = &open.map {
live.insert(url::resolve(base, &map.uri));
}
}
if let Some(hint) = playlist
.low_latency
.as_ref()
.and_then(|ll| ll.preload_hint_part.as_ref())
{
live.insert(url::resolve(base, hint));
}
self.byte_range_cursor.retain(|u, _| live.contains(u));
}
}
fn prune_inits(&mut self) {
let newest = self.inits.keys().next_back().copied();
let bound: BTreeSet<InitId> = self.msn_init.values().copied().collect();
self.inits.retain(|id, slot| {
Some(*id) == newest || bound.contains(id) || slot.fetch != InitFetch::Ready
});
}
fn continuity(&self, new: &MediaPlaylist) -> Continuity {
let Some(prev) = &self.last_full_playlist else {
return if self
.last_media_sequence
.is_some_and(|p| new.media_sequence < p)
{
Continuity::Restart
} else {
Continuity::Continues
};
};
let (p0, n0) = (prev.media_sequence, new.media_sequence);
let regressed = n0 < self.last_media_sequence.unwrap_or(p0).max(p0);
let mut overlap = false;
let mut shared = 0usize;
let mut requeried = 0usize;
for (i, seg) in new.segments.iter().enumerate() {
let Some(msn) = n0.checked_add(index_u64(i)) else {
return Continuity::Restart;
};
let Some(old) = msn
.checked_sub(p0)
.and_then(|d| usize::try_from(d).ok())
.and_then(|d| prev.segments.get(d))
else {
continue;
};
if without_query(&old.uri) != without_query(&seg.uri)
|| old.duration != seg.duration
|| old.byte_range != seg.byte_range
{
return Continuity::Restart;
}
if let (Some(a), Some(b)) = (program_date_time(old), program_date_time(seg))
&& a != b
{
return Continuity::Restart;
}
overlap = true;
shared = shared.saturating_add(1);
if old.uri != seg.uri {
requeried = requeried.saturating_add(1);
}
}
if requeried != 0 && requeried != shared {
return Continuity::Restart;
}
if !regressed && new.discontinuity_sequence < prev.discontinuity_sequence {
return Continuity::Restart;
}
if !regressed {
return Continuity::Continues;
}
if overlap {
return Continuity::Lagging;
}
let old_uris: BTreeSet<&str> = prev.segments.iter().map(|s| s.uri.as_str()).collect();
if new
.segments
.iter()
.any(|s| old_uris.contains(s.uri.as_str()))
{
return Continuity::Restart;
}
let new_last = new.segments.iter().rev().find_map(program_date_time_ms);
let prev_first = prev.segments.iter().find_map(program_date_time_ms);
match (new_last, prev_first) {
(Some(end), Some(start)) if end < start => Continuity::OlderWindow,
_ => Continuity::Restart,
}
}
fn restart_after_regression(&mut self) -> Result<()> {
self.flush_ts_demux(ResourceId::Init)?;
self.ts_demux = None;
self.ts_specs.clear();
self.last_ts_init = None;
self.pending_actions
.retain(|a| !matches!(a, Action::FetchResource { .. }));
self.pending_demux.clear();
self.pending_actions
.retain(|a| !matches!(a, Action::FetchKey { .. }));
self.keys_in_flight.clear();
self.segment_crypto.clear();
self.pending_keyed.clear();
self.requested.clear();
self.fulfilled.clear();
self.delivered_parts.clear();
self.delivered_segments.clear();
self.discontinuous_msns.clear();
self.discontinuity_emitted.clear();
self.byte_range_cursor.clear();
self.outstanding_fetches = 0;
self.last_full_playlist = None;
self.inits.clear();
self.init_target = None;
self.announced_init = None;
self.msn_init.clear();
self.last_fmp4_specs = None;
self.full_reload_requested = false;
self.joined = false;
self.pending_outputs.push_back(Output::Discontinuity);
Ok(())
}
fn all_playlist_segments_delivered(&self) -> bool {
let Some(playlist) = &self.last_full_playlist else {
return true;
};
(0..playlist.segments.len() as u64).all(|i| {
let msn = playlist.media_sequence.saturating_add(i);
self.delivered_segments.contains(&msn)
|| self.fulfilled.contains(&ResourceId::Segment { msn })
})
}
fn maybe_emit_end_of_stream(&mut self) -> Result<()> {
if self.saw_endlist
&& !self.end_emitted
&& self.outstanding_fetches == 0
&& self.pending_keyed.is_empty()
&& self.inits.values().all(|slot| slot.bytes.is_some())
&& self.all_playlist_segments_delivered()
{
self.flush_ts_demux(ResourceId::Init)?;
self.pending_outputs.push_back(Output::EndOfStream);
self.end_emitted = true;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use transmux::sample_aes::aes128_encrypt_segment;
const EMPTY_FRAGMENT: &[u8] = &[
0, 0, 0, 8, b'm', b'o', b'o', b'f', 0, 0, 0, 8, b'm', b'd', b'a', b't',
];
#[test]
fn decrypt_segment_strips_pkcs7_padding_to_the_exact_plaintext() {
let key = [0x5Au8; KEY_LEN];
let iv = [0xC3u8; BLOCK_LEN];
let id = ResourceId::Segment { msn: 9 };
let crypto = SegmentCrypto {
key_id: KeyId(0),
iv,
};
for len in [1usize, 15, 16, 17, 37, 188] {
let plain: Vec<u8> = (0..len).map(|i| (i % 251) as u8).collect();
let cipher = aes128_encrypt_segment(&key, &iv, &plain);
assert_eq!(decrypt_segment(id, crypto, &key, &cipher).unwrap(), plain);
}
}
#[test]
fn media_sequence_iv_is_big_endian_left_padded() {
let iv = media_sequence_iv(0x0102_0304_0506_0708);
assert_eq!(iv[..8], [0u8; 8]);
assert_eq!(iv[8..], [1, 2, 3, 4, 5, 6, 7, 8]);
assert_eq!(media_sequence_iv(0)[..], [0u8; 16]);
assert_eq!(media_sequence_iv(0x1FF)[14..], [1, 0xFF]);
}
#[test]
fn on_resource_rejects_a_never_requested_id() {
let mut client = HlsClient::new("http://example.com/playlist.m3u8");
let id = ResourceId::Segment { msn: 0 };
let err = client
.on_resource(id, b"some bytes")
.expect_err("an id the client never requested must be rejected");
assert!(
matches!(err, Error::UnrequestedResource { id: got } if got == id),
"wrong error variant: {err:?}"
);
let err = client
.on_resource(ResourceId::Init, b"init bytes")
.expect_err("an unrequested Init must be rejected");
assert!(
matches!(
err,
Error::UnrequestedResource {
id: ResourceId::Init
}
),
"wrong error variant: {err:?}"
);
}
#[test]
fn on_resource_accepts_a_previously_requested_id() {
let mut client = HlsClient::new("http://example.com/playlist.m3u8");
let id = ResourceId::Segment { msn: 0 };
client.request_resource(id, "http://example.com/seg0.m4s".to_string(), None);
let result = client.on_resource(id, EMPTY_FRAGMENT);
assert!(
result.is_ok(),
"a requested id must be accepted: {result:?}"
);
assert!(
client.pending_demux.iter().any(|(bid, _)| *bid == id),
"expected the resource to be buffered pending the init segment"
);
}
#[test]
fn is_ts_segment_true_when_no_map_seen_and_sync_byte_present() {
let client = HlsClient::new("http://example.com/playlist.m3u8");
assert!(client.is_ts_segment(&[TS_SYNC_BYTE, 0x40, 0x11, 0x00]));
}
#[test]
fn is_ts_segment_false_for_an_isobmff_resource_with_no_map_seen() {
let client = HlsClient::new("http://example.com/playlist.m3u8");
let ftyp_box = b"\x00\x00\x00\x18ftypiso5\x00\x00\x02\x00iso5iso6mp41";
assert!(!client.is_ts_segment(ftyp_box));
}
#[test]
fn is_ts_segment_false_once_a_map_has_been_requested() {
let mut client = HlsClient::new("http://example.com/playlist.m3u8");
client
.ensure_init_requested(&MapTag {
uri: "init.mp4".to_string(),
byte_range: None,
extra_attrs: Vec::new(),
})
.expect("ensure_init_requested succeeds");
assert!(!client.is_ts_segment(&[TS_SYNC_BYTE, 0x40, 0x11, 0x00]));
}
#[test]
fn resolve_byte_range_rejects_an_offset_plus_length_that_overflows_u64() {
let mut client = HlsClient::new("http://example.com/playlist.m3u8");
let br = Some(ByteRange {
length: u64::MAX,
offset: Some(1),
});
let err = client
.resolve_byte_range("http://example.com/seg0.m4s", &br)
.expect_err("offset 1 + length u64::MAX must overflow and be rejected");
assert!(
matches!(
err,
Error::ByteRangeOverflow {
offset: 1,
length: u64::MAX,
..
}
),
"wrong error variant/fields: {err:?}"
);
}
#[test]
fn resolve_byte_range_rejects_a_cursor_accumulation_that_overflows_u64() {
let mut client = HlsClient::new("http://example.com/playlist.m3u8");
let url = "http://example.com/seg0.m4s";
let seed = Some(ByteRange {
length: u64::MAX - 10,
offset: Some(5),
});
let (offset, length) = client
.resolve_byte_range(url, &seed)
.expect("seed range does not itself overflow")
.expect("Some for a Some(ByteRange)");
assert_eq!((offset, length), (5, u64::MAX - 10));
let next = Some(ByteRange {
length: 100,
offset: None, });
let err = client
.resolve_byte_range(url, &next)
.expect_err("cursor + next length must overflow and be rejected");
assert!(
matches!(err, Error::ByteRangeOverflow { length: 100, .. }),
"wrong error variant/fields: {err:?}"
);
}
#[test]
fn merge_delta_does_not_panic_on_an_overflowing_skipped_segments() {
let mut client = HlsClient::new("http://example.com/playlist.m3u8");
let prev = MediaPlaylist {
media_sequence: 0,
segments: vec![MediaSegment::default(); 2],
..Default::default()
};
client.last_full_playlist = Some(prev);
let delta = MediaPlaylist {
media_sequence: 10, skip: Some(broadcast_hls::SkipInfo {
skipped_segments: u64::MAX - 5,
..Default::default()
}),
segments: Vec::new(),
..Default::default()
};
let merged = client.merge_delta(delta.clone());
assert_eq!(
merged, delta,
"an unmergeable skip count must fall back to the delta as-is, not panic"
);
}
fn byte_range_playlist(first_msn: u64, file: &str) -> String {
format!(
"#EXTM3U\n#EXT-X-VERSION:4\n#EXT-X-TARGETDURATION:2\n\
#EXT-X-MEDIA-SEQUENCE:{first_msn}\n\
#EXTINF:2.0,\n#EXT-X-BYTERANGE:100@0\n{file}\n\
#EXTINF:2.0,\n#EXT-X-BYTERANGE:50\n{file}\n"
)
}
#[test]
fn state_is_pruned_to_the_current_window() {
let mut client = HlsClient::new("http://example.com/p.m3u8");
client
.on_playlist(byte_range_playlist(0, "a.ts").as_bytes())
.unwrap();
assert_eq!(
client.byte_range_cursor.iter().collect::<Vec<_>>(),
vec![(&"http://example.com/a.ts".to_string(), &150u64)]
);
client
.on_resource(ResourceId::Segment { msn: 0 }, EMPTY_FRAGMENT)
.unwrap();
client
.on_playlist(byte_range_playlist(1000, "b.ts").as_bytes())
.unwrap();
let requested: Vec<ResourceId> = client.requested.iter().copied().collect();
assert_eq!(
requested,
vec![
ResourceId::Segment { msn: 1 },
ResourceId::Segment { msn: 1000 },
ResourceId::Segment { msn: 1001 },
]
);
assert!(client.fulfilled.is_empty(), "{:?}", client.fulfilled);
assert_eq!(
client.byte_range_cursor.iter().collect::<Vec<_>>(),
vec![(&"http://example.com/b.ts".to_string(), &150u64)],
"the cursor of the unreferenced a.ts must be dropped"
);
}
#[test]
fn discontinuity_bookkeeping_below_the_window_is_dropped() {
let mut client = HlsClient::new("http://example.com/p.m3u8");
let pl = |first: u64| {
format!(
"#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:2\n\
#EXT-X-MEDIA-SEQUENCE:{first}\n#EXT-X-DISCONTINUITY\n#EXTINF:2.0,\ns.ts\n"
)
};
client.on_playlist(pl(5).as_bytes()).unwrap();
client
.on_resource(ResourceId::Segment { msn: 5 }, EMPTY_FRAGMENT)
.unwrap();
assert_eq!(
client
.discontinuous_msns
.iter()
.copied()
.collect::<Vec<_>>(),
vec![5]
);
client.on_playlist(pl(9).as_bytes()).unwrap();
assert_eq!(
client
.discontinuous_msns
.iter()
.copied()
.collect::<Vec<_>>(),
vec![9]
);
assert!(client.discontinuity_emitted.is_empty());
assert!(client.delivered_segments.is_empty());
}
fn keyed_playlist(first_msn: u64, count: u64, key_uri: &str) -> String {
let mut text = format!(
"#EXTM3U\n#EXT-X-VERSION:5\n#EXT-X-TARGETDURATION:2\n\
#EXT-X-MEDIA-SEQUENCE:{first_msn}\n\
#EXT-X-KEY:METHOD=AES-128,URI=\"{key_uri}\"\n"
);
for msn in first_msn..first_msn + count {
text.push_str(&format!("#EXTINF:2.0,\nseg{msn}.ts\n"));
}
text
}
fn pending_keyed_bytes(client: &HlsClient) -> usize {
client.pending_keyed.iter().map(|(_, b)| b.len()).sum()
}
#[test]
fn pending_keyed_is_pruned_with_the_window() {
let mut client = HlsClient::new("http://example.com/p.m3u8");
client
.on_playlist(keyed_playlist(0, 1, "key.bin").as_bytes())
.unwrap();
client
.on_resource(ResourceId::Segment { msn: 0 }, &[0u8; 32])
.unwrap();
assert_eq!(client.pending_keyed.len(), 1);
client
.on_playlist(keyed_playlist(5, 1, "key.bin").as_bytes())
.unwrap();
assert!(
client.pending_keyed.is_empty(),
"ciphertext below the window can never be played: {:?}",
client
.pending_keyed
.iter()
.map(|(id, _)| id)
.collect::<Vec<_>>()
);
}
#[test]
fn pending_keyed_is_capped_and_drops_the_oldest_with_a_typed_error() {
let mut client = HlsClient::new("http://example.com/p.m3u8");
client
.on_playlist(keyed_playlist(0, 2, "key.bin").as_bytes())
.unwrap();
let big = vec![0u8; 40 * 1024 * 1024];
client
.on_resource(ResourceId::Segment { msn: 0 }, &big)
.unwrap();
let err = client
.on_resource(ResourceId::Segment { msn: 1 }, &big)
.expect_err("the second 40 MiB segment exceeds the 64 MiB cap");
assert!(
matches!(err, Error::PendingKeyedOverflow { dropped } if dropped == ResourceId::Segment { msn: 0 }),
"{err:?}"
);
assert!(
pending_keyed_bytes(&client) <= MAX_PENDING_KEYED_BYTES,
"{} bytes buffered",
pending_keyed_bytes(&client)
);
assert_eq!(
client
.pending_keyed
.iter()
.map(|(id, _)| *id)
.collect::<Vec<_>>(),
vec![ResourceId::Segment { msn: 1 }]
);
}
#[test]
fn key_maps_do_not_grow_with_per_reload_tokenised_key_urls() {
let mut client = HlsClient::new("http://example.com/p.m3u8");
for n in 0..8u64 {
client
.on_playlist(keyed_playlist(n, 2, &format!("key.bin?t={n}")).as_bytes())
.unwrap();
}
assert!(
client.key_ids.len() <= 3 && client.key_urls.len() <= 3,
"{:?} {:?}",
client.key_ids,
client.key_urls
);
}
#[test]
fn next_wait_reports_the_queued_wait_hint_without_consuming_it() {
use core::time::Duration;
let mut c = HlsClient::new("http://h/p.m3u8");
assert_eq!(c.next_wait(), None, "the first queued action is a fetch");
let _ = c.poll();
c.on_playlist(
b"#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:2\n#EXT-X-MEDIA-SEQUENCE:0\n#EXTINF:2.0,\nseg0.ts\n",
)
.unwrap();
while c.next_wait().is_none() {
c.poll()
.expect("a WaitMs must be queued after a non-blocking live playlist");
}
assert_eq!(
c.next_wait(),
Some(Duration::from_millis(1000)),
"half the 2 s target duration"
);
assert_eq!(
c.next_wait(),
Some(Duration::from_millis(1000)),
"peeking does not consume"
);
let t0 = std::time::Instant::now();
let first = c.poll_timeout(t0).expect("a wait is at the front");
assert_eq!(first, t0 + Duration::from_millis(1000));
for later in [250u64, 600, 999, 5000] {
assert_eq!(
c.poll_timeout(t0 + Duration::from_millis(later)),
Some(first),
"re-query at +{later} ms must not slide the deadline"
);
}
assert!(matches!(c.poll(), Some(Action::WaitMs(1000))));
assert_eq!(c.poll_timeout(t0 + Duration::from_secs(9)), None);
}
#[test]
fn rfc3339_accepts_the_spellings_the_old_parser_accepted() {
assert_eq!(
parse_rfc3339_ms("2026-10-02 10:00:00Z"),
Some(1_790_935_200_000),
"space separator"
);
assert_eq!(
parse_rfc3339_ms("2026-10-02t10:00:00z"),
Some(1_790_935_200_000),
"lower-case t and z"
);
assert_eq!(
parse_rfc3339_ms("2026-10-02T10:00:00.123456789Z"),
Some(1_790_935_200_123),
"extra digits truncated"
);
assert_eq!(
parse_rfc3339_ms("1969-12-31T23:59:59Z"),
Some(-1_000),
"before the epoch"
);
}
#[test]
fn rfc3339_offset_spellings_all_name_the_same_instant() {
let want = Some(1_790_935_200_000);
for text in [
"2026-10-02T12:00:00+02:00",
"2026-10-02T12:00:00+0200",
"2026-10-02T05:00:00-05:00",
"2026-10-02T05:00:00-0500",
"2026-10-02T10:00:00+00:00",
"2026-10-02T10:00:00-00:00",
] {
assert_eq!(parse_rfc3339_ms(text), want, "{text}");
}
let hour_only = parse_rfc3339_ms("2026-10-02T12:00:00+02");
assert!(hour_only == want || hour_only.is_none(), "{hour_only:?}");
}
#[test]
fn a_leap_second_is_clamped_to_the_last_second_of_the_minute() {
assert_eq!(
parse_rfc3339_ms("2016-12-31T23:59:60Z"),
Some(1_483_228_799_000)
);
}
#[test]
fn rfc3339_parses_to_literal_epoch_milliseconds() {
assert_eq!(
parse_rfc3339_ms("2026-10-02T10:00:00Z"),
Some(1_790_935_200_000)
);
assert_eq!(
parse_rfc3339_ms("2026-10-02T10:00:00.250Z"),
Some(1_790_935_200_250)
);
assert_eq!(
parse_rfc3339_ms("2026-10-02T10:00:00.5Z"),
Some(1_790_935_200_500)
);
assert_eq!(parse_rfc3339_ms("1970-01-01T00:00:00Z"), Some(0));
assert_eq!(
parse_rfc3339_ms("2000-02-29T23:59:59Z"),
Some(951_868_799_000)
);
assert_eq!(
parse_rfc3339_ms("2026-10-02T12:00:00+02:00"),
Some(1_790_935_200_000)
);
assert_eq!(
parse_rfc3339_ms("2026-10-02T05:00:00-0500"),
Some(1_790_935_200_000)
);
for bad in [
"",
"2026-10-02",
"2026-13-02T10:00:00Z",
"2026-10-02T25:00:00Z",
"2026-10-02T10:00:00",
"2026-10-02T10:00:00Zjunk",
"2026-10-02T10:00:00.Z",
"2026-10-02T10:00:00+2:00",
"xxxx-10-02T10:00:00Z",
] {
assert_eq!(parse_rfc3339_ms(bad), None, "{bad:?}");
}
}
}