1use alloc::collections::{BTreeMap, BTreeSet, VecDeque};
4use alloc::format;
5use alloc::string::{String, ToString};
6use alloc::vec::Vec;
7use container_probe::segment;
8use transmux::TrackId;
9
10use broadcast_common::Unpackage;
11use broadcast_hls::{
12 ByteRange, EncryptionMethod, MapTag, MasterPlaylist, MediaKey, MediaPlaylist, MediaSegment,
13 OpenSegment, PreloadHintType,
14};
15use transmux::sample_aes::{BLOCK_LEN, KEY_LEN, aes128_decrypt_segment};
16use transmux::{DemuxEvent, Fmp4Demux, InputDegradation, Sample, StreamingTsDemux, TrackSpec};
17
18use super::action::{Action, BlockingReload, KeyId, ResourceId};
19use super::error::{Error, Result};
20use super::output::{MasterSelection, Output, RenditionInfo, VariantInfo, VariantPolicy};
21use super::url;
22
23const TS_SYNC_BYTE: u8 = 0x47;
31
32const TS_MOVIE_TIMESCALE: u32 = 90_000;
37
38const LIVE_EDGE_TARGET_DURATIONS: f64 = 3.0;
43
44const MAX_PENDING_KEYED_BYTES: usize = 64 * 1024 * 1024;
46
47const MAX_RELOAD_WAIT_MS: u64 = 60_000;
50
51const MILLIS_PER_SECOND: u64 = 1000;
52
53const MOVIE_BOX: [u8; 4] = *b"moov";
58
59const MAP_PROBE_VERSION: u32 = 6;
60
61struct TsDemuxState(StreamingTsDemux);
64
65impl core::fmt::Debug for TsDemuxState {
66 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
67 f.write_str("TsDemuxState(StreamingTsDemux)")
68 }
69}
70
71fn index_u64(i: usize) -> u64 {
74 u64::try_from(i).unwrap_or(u64::MAX)
75}
76
77fn reload_wait_ms(target_duration: u32) -> u64 {
81 (u64::from(target_duration.max(1)).saturating_mul(MILLIS_PER_SECOND) / 2)
82 .min(MAX_RELOAD_WAIT_MS)
83}
84
85fn keys_at_maps(text: &str) -> Vec<(MapTag, Option<MediaKey>)> {
90 const MAP_PREFIX: &str = "#EXT-X-MAP:";
91 const KEY_PREFIX: &str = "#EXT-X-KEY:";
92 let mut current: Option<MediaKey> = None;
93 let mut out = Vec::new();
94 for line in text.lines() {
95 if line.starts_with(KEY_PREFIX) {
96 let probe = format!(
97 "#EXTM3U\n#EXT-X-TARGETDURATION:1\n{}\n#EXTINF:1,\nx\n",
98 line.trim_end()
99 );
100 if let Ok(parsed) = MediaPlaylist::parse(&probe)
101 && let Ok(mut keys) = parsed.segment_keys()
102 && let Some(key) = keys.pop()
103 {
104 current = key;
105 }
106 } else if line.starts_with(MAP_PREFIX) {
107 let probe = format!(
108 "#EXTM3U\n#EXT-X-VERSION:{MAP_PROBE_VERSION}\n#EXT-X-TARGETDURATION:1\n{}\n#EXTINF:1,\nx\n",
109 line.trim_end()
110 );
111 if let Ok(parsed) = MediaPlaylist::parse(&probe)
112 && let Some(map) = parsed.segments.first().and_then(|seg| seg.map.clone())
113 {
114 out.push((map, current.clone()));
115 }
116 }
117 }
118 out
119}
120
121fn has_movie_box(bytes: &[u8]) -> bool {
124 for step in transmux::box_iter(bytes) {
125 let Ok((boxed, _)) = step else { return false };
126 if boxed.header.box_type.0 == MOVIE_BOX {
127 return true;
128 }
129 }
130 false
131}
132
133fn without_query(uri: &str) -> &str {
136 uri.split_once('?').map_or(uri, |(path, _)| path)
137}
138
139fn carried_key_tags(dropped: &[MediaSegment]) -> Vec<String> {
143 const KEY_PREFIX: &str = "#EXT-X-KEY:";
144 const KEYFORMAT_ATTR: &str = "KEYFORMAT=\"";
145 const IDENTITY: &str = "identity";
146 let mut latest: BTreeMap<&str, &String> = BTreeMap::new();
147 for tag in dropped.iter().flat_map(|seg| &seg.pre_tags) {
148 if !tag.starts_with(KEY_PREFIX) {
149 continue;
150 }
151 let format = tag
152 .split_once(KEYFORMAT_ATTR)
153 .and_then(|(_, rest)| rest.split_once('"'))
154 .map_or(IDENTITY, |(format, _)| format);
155 latest.insert(format, tag);
156 }
157 latest.into_values().cloned().collect()
158}
159
160fn decrypt_segment(
161 id: ResourceId,
162 crypto: SegmentCrypto,
163 key: &[u8; KEY_LEN],
164 ciphertext: &[u8],
165) -> Result<Vec<u8>> {
166 aes128_decrypt_segment(key, &crypto.iv, ciphertext).map_err(|_| Error::DecryptFailed {
167 id,
168 key_id: crypto.key_id,
169 })
170}
171
172fn resource_msn(id: &ResourceId) -> Option<u64> {
175 match id {
176 ResourceId::Part { msn, .. } | ResourceId::Segment { msn } => Some(*msn),
177 _ => None,
178 }
179}
180
181type InitId = u32;
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq)]
187enum InitFetch {
188 Wanted,
190 InFlight,
192 Failed,
194 Arrived,
196 Ready,
198}
199
200#[derive(Debug)]
204struct InitSlot {
205 map: MapTag,
206 url: String,
207 byte_range: Option<(u64, u64)>,
208 encryption: Option<MediaKey>,
209 crypto: Option<SegmentCrypto>,
210 fetch: InitFetch,
211 cipher: Option<Vec<u8>>,
212 bytes: Option<Vec<u8>>,
213 skipped_reported: BTreeSet<(Option<u32>, String)>,
214}
215
216#[derive(Debug)]
219struct MapEntry {
220 map: MapTag,
221 encryption: Option<MediaKey>,
222 first_msn: u64,
223}
224
225impl MapEntry {
226 fn unusable(&self) -> Option<Error> {
227 let key = self.encryption.as_ref()?;
228 let what = if key.method != EncryptionMethod::Aes128 {
229 "encrypted EXT-X-MAP: only METHOD=AES-128 is decrypted"
230 } else if key.uri.is_none() {
231 "encrypted EXT-X-MAP: key without URI"
232 } else {
233 return None;
234 };
235 Some(Error::UnsupportedEncryption {
236 method: key.method,
237 what,
238 })
239 }
240}
241
242#[derive(Clone, Copy)]
244struct KeyBytes([u8; KEY_LEN]);
245
246impl core::fmt::Debug for KeyBytes {
247 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
248 f.write_str("KeyBytes(<redacted>)")
249 }
250}
251
252#[derive(Debug, Clone, Copy)]
254struct SegmentCrypto {
255 key_id: KeyId,
256 iv: [u8; BLOCK_LEN],
257}
258
259fn media_sequence_iv(msn: u64) -> [u8; BLOCK_LEN] {
262 let mut iv = [0u8; BLOCK_LEN];
263 let msn_bytes = msn.to_be_bytes();
264 let start = BLOCK_LEN - msn_bytes.len();
265 iv[start..].copy_from_slice(&msn_bytes);
266 iv
267}
268
269#[derive(Debug, Clone, Copy, PartialEq, Eq)]
272enum Continuity {
273 Continues,
275 Lagging,
278 OlderWindow,
282 Restart,
284}
285
286fn program_date_time(seg: &MediaSegment) -> Option<broadcast_hls::ProgramDateTime> {
288 seg.program_date_time
289}
290
291fn program_date_time_ms(seg: &MediaSegment) -> Option<i64> {
293 program_date_time(seg).map(|pdt| pdt.timestamp().as_millisecond())
294}
295
296#[cfg(test)]
301fn parse_rfc3339_ms(text: &str) -> Option<i64> {
302 text.parse::<jiff::Timestamp>()
303 .ok()
304 .map(|t| t.as_millisecond())
305}
306
307#[derive(Debug)]
380pub struct HlsClient {
381 playlist_url: String,
382 base_override: Option<String>,
383 variant_policy: VariantPolicy,
384 master: Option<MasterSelection>,
385
386 pending_actions: VecDeque<Action>,
387 #[cfg(feature = "std")]
392 wait_anchor: Option<std::time::Instant>,
393 pending_outputs: VecDeque<Output>,
394
395 inits: BTreeMap<InitId, InitSlot>,
397 next_init_id: InitId,
398 init_target: Option<InitId>,
401 announced_init: Option<InitId>,
403 msn_init: BTreeMap<u64, InitId>,
406 playlist_maps: Vec<MapEntry>,
408 last_fmp4_specs: Option<Vec<TrackSpec>>,
411 pending_demux: VecDeque<(ResourceId, Vec<u8>)>,
416
417 key_ids: BTreeMap<String, KeyId>,
419 key_urls: BTreeMap<KeyId, String>,
421 key_material: BTreeMap<KeyId, KeyBytes>,
423 keys_in_flight: BTreeSet<KeyId>,
425 segment_crypto: BTreeMap<u64, SegmentCrypto>,
427 pending_keyed: Vec<(ResourceId, Vec<u8>)>,
429 next_key_id: u32,
432 full_reload_requested: bool,
435
436 requested: BTreeSet<ResourceId>,
437 resource_requests: BTreeMap<ResourceId, (String, Option<(u64, u64)>)>,
440 fulfilled: BTreeSet<ResourceId>,
444 joined: bool,
447 ts_demux: Option<TsDemuxState>,
451 ts_specs: BTreeMap<TrackId, TrackSpec>,
453 last_ts_init: Option<Vec<u8>>,
456 delivered_parts: BTreeSet<(u64, u64)>,
457 delivered_segments: BTreeSet<u64>,
458 discontinuous_msns: BTreeSet<u64>,
459 discontinuity_emitted: BTreeSet<u64>,
460 byte_range_cursor: BTreeMap<String, u64>,
461
462 outstanding_fetches: u64,
463 saw_endlist: bool,
464 end_emitted: bool,
465 last_full_playlist: Option<MediaPlaylist>,
466 last_media_sequence: Option<u64>,
470}
471
472fn choose_variant(variants: &[VariantInfo], policy: &VariantPolicy) -> usize {
474 if variants.is_empty() {
475 return 0;
476 }
477 let mut candidates: Vec<usize> = Vec::new();
478 for (i, v) in variants.iter().enumerate() {
479 let mut ok = true;
480 if let Some(max_bw) = policy.max_bandwidth
481 && v.bandwidth > max_bw
482 {
483 ok = false;
484 }
485 if ok
486 && let Some(max_h) = policy.max_height
487 && let Some(res) = v.resolution
488 && res.1 > max_h
489 {
490 ok = false;
491 }
492 if ok
493 && let Some(supported) = &policy.supported_codecs
494 && let Some(codecs) = &v.codecs
495 {
496 for entry in codecs.split(',') {
497 let entry = entry.trim();
498 if entry.is_empty() {
499 continue;
500 }
501 if !supported.iter().any(|s| entry.starts_with(s)) {
502 ok = false;
503 break;
504 }
505 }
506 }
507 if ok {
508 candidates.push(i);
509 }
510 }
511 if !candidates.is_empty() {
512 let mut best = candidates[0];
513 for &i in &candidates[1..] {
514 if variants[i].bandwidth > variants[best].bandwidth {
515 best = i;
516 }
517 }
518 best
519 } else {
520 let mut best = 0usize;
521 for i in 1..variants.len() {
522 if variants[i].bandwidth < variants[best].bandwidth {
523 best = i;
524 }
525 }
526 best
527 }
528}
529
530impl HlsClient {
531 pub fn new(playlist_url: impl Into<String>) -> Self {
535 let playlist_url = playlist_url.into();
536 let mut pending_actions = VecDeque::new();
537 pending_actions.push_back(Action::FetchPlaylist {
538 url: playlist_url.clone(),
539 blocking: None,
540 skip: false,
541 });
542 Self {
543 playlist_url,
544 base_override: None,
545 variant_policy: VariantPolicy::default(),
546 master: None,
547 pending_actions,
548 #[cfg(feature = "std")]
549 wait_anchor: None,
550 pending_outputs: VecDeque::new(),
551 inits: BTreeMap::new(),
552 next_init_id: 0,
553 init_target: None,
554 announced_init: None,
555 msn_init: BTreeMap::new(),
556 playlist_maps: Vec::new(),
557 last_fmp4_specs: None,
558 pending_demux: VecDeque::new(),
559 key_ids: BTreeMap::new(),
560 key_urls: BTreeMap::new(),
561 key_material: BTreeMap::new(),
562 keys_in_flight: BTreeSet::new(),
563 segment_crypto: BTreeMap::new(),
564 pending_keyed: Vec::new(),
565 next_key_id: 0,
566 full_reload_requested: false,
567 requested: BTreeSet::new(),
568 resource_requests: BTreeMap::new(),
569 fulfilled: BTreeSet::new(),
570 joined: false,
571 ts_demux: None,
572 ts_specs: BTreeMap::new(),
573 last_ts_init: None,
574 delivered_parts: BTreeSet::new(),
575 delivered_segments: BTreeSet::new(),
576 discontinuous_msns: BTreeSet::new(),
577 discontinuity_emitted: BTreeSet::new(),
578 byte_range_cursor: BTreeMap::new(),
579 outstanding_fetches: 0,
580 saw_endlist: false,
581 end_emitted: false,
582 last_full_playlist: None,
583 last_media_sequence: None,
584 }
585 }
586
587 pub fn playlist_url(&self) -> &str {
589 &self.playlist_url
590 }
591
592 pub fn set_playlist_base(&mut self, base: impl Into<String>) {
597 self.base_override = Some(base.into());
598 }
599
600 fn base(&self) -> &str {
601 self.base_override.as_deref().unwrap_or(&self.playlist_url)
602 }
603
604 pub fn endlist_seen(&self) -> bool {
607 self.saw_endlist
608 }
609
610 pub fn target_duration_secs(&self) -> Option<u32> {
613 self.last_full_playlist.as_ref().map(|p| p.target_duration)
614 }
615
616 pub fn set_variant_policy(&mut self, policy: VariantPolicy) {
619 self.variant_policy = policy;
620 }
621
622 pub fn select_variant(&mut self, index: usize) -> Result<()> {
624 let variants_len = self
625 .master
626 .as_ref()
627 .ok_or(Error::NoSuchVariant { index })?
628 .variants
629 .len();
630 if index >= variants_len {
631 return Err(Error::NoSuchVariant { index });
632 }
633 let url = self
634 .master
635 .as_ref()
636 .and_then(|m| m.variants.get(index))
637 .map(|v| v.url.clone())
638 .ok_or(Error::NoSuchVariant { index })?;
639 self.restart_after_regression()?;
640 self.pending_actions.clear();
641 self.playlist_url = url.clone();
642 self.base_override = None;
643 self.pending_actions.push_back(Action::FetchPlaylist {
644 url,
645 blocking: None,
646 skip: false,
647 });
648 if let Some(m) = &mut self.master {
649 m.chosen = index;
650 }
651 Ok(())
652 }
653
654 pub fn poll(&mut self) -> Option<Action> {
656 #[cfg(feature = "std")]
657 {
658 self.wait_anchor = None;
659 }
660 self.pending_actions.pop_front()
661 }
662
663 pub fn next_wait(&self) -> Option<core::time::Duration> {
669 match self.pending_actions.front() {
670 Some(Action::WaitMs(ms)) => Some(core::time::Duration::from_millis(*ms)),
671 _ => None,
672 }
673 }
674
675 #[cfg(feature = "std")]
686 pub fn poll_timeout(&mut self, now: std::time::Instant) -> Option<std::time::Instant> {
687 let wait = self.next_wait()?;
688 let anchor = *self.wait_anchor.get_or_insert(now);
689 Some(anchor + wait)
690 }
691
692 pub fn next_output(&mut self) -> Option<Output> {
694 self.pending_outputs.pop_front()
695 }
696
697 pub fn on_playlist(&mut self, bytes: &[u8]) -> Result<()> {
703 let text = core::str::from_utf8(bytes)?;
704 if text.contains("#EXT-X-STREAM-INF") {
705 let master = MasterPlaylist::parse(text)?;
706 let base = self.base().to_string();
707 let mut variants: Vec<VariantInfo> = Vec::new();
708 for v in &master.variants {
709 variants.push(VariantInfo {
710 url: url::resolve(&base, &v.uri),
711 bandwidth: v.bandwidth,
712 average_bandwidth: v.average_bandwidth,
713 resolution: v.resolution,
714 codecs: v.codecs.clone(),
715 audio_group: v.audio.clone(),
716 subtitles_group: v.subtitles.clone(),
717 });
718 }
719 let mut renditions: Vec<RenditionInfo> = Vec::new();
720 for m in &master.media {
721 renditions.push(RenditionInfo {
722 media_type: m.media_type,
723 group_id: m.group_id.clone(),
724 name: m.name.clone(),
725 language: m.language.clone(),
726 default: m.default,
727 autoselect: m.autoselect,
728 url: m.uri.as_ref().map(|u| url::resolve(&base, u)),
729 });
730 }
731 let chosen = choose_variant(&variants, &self.variant_policy);
732 let selection = MasterSelection {
733 variants,
734 chosen,
735 renditions,
736 };
737 self.pending_outputs
738 .push_back(Output::Master(selection.clone()));
739 self.master = Some(selection);
740 let Some(chosen_url) = self
741 .master
742 .as_ref()
743 .and_then(|m| m.variants.get(chosen))
744 .map(|v| v.url.clone())
745 else {
746 return Err(Error::NoSuchVariant { index: chosen });
747 };
748 self.playlist_url = chosen_url.clone();
749 self.base_override = None;
750 self.pending_actions.clear();
751 self.pending_actions.push_back(Action::FetchPlaylist {
752 url: chosen_url,
753 blocking: None,
754 skip: false,
755 });
756 return Ok(());
757 }
758 let playlist = MediaPlaylist::parse(text)?;
759 let playlist = self.merge_delta(playlist);
760 if playlist.skip.is_some() {
761 return Err(self.request_full_playlist(&playlist));
762 }
763 self.full_reload_requested = false;
764 let segment_keys = playlist.segment_key_results()?;
765 if let Some(Err(e)) = segment_keys.first()
766 && segment_keys.iter().all(|r| r.is_err())
767 {
768 return Err(Error::PlaylistParse(e.clone()));
769 }
770 let open_key = if playlist.open_segment.is_some() {
771 playlist.open_segment_key()?
772 } else {
773 None
774 };
775 let mut first_error: Option<Error> = None;
776 let mut lag_floor: Option<u64> = None;
779 if self.last_media_sequence.is_some() {
780 match self.continuity(&playlist) {
781 Continuity::Restart => self.restart_after_regression()?,
782 Continuity::OlderWindow => {
783 lag_floor = self.last_full_playlist.as_ref().map(|p| p.media_sequence);
784 }
785 Continuity::Lagging | Continuity::Continues => {}
786 }
787 }
788 self.last_media_sequence = Some(
790 self.last_media_sequence
791 .map_or(playlist.media_sequence, |p| p.max(playlist.media_sequence)),
792 );
793 self.forget_keys_below(playlist.media_sequence);
794
795 let next_msn = playlist
799 .media_sequence
800 .checked_add(index_u64(playlist.segments.len()))
801 .ok_or(Error::MediaSequenceOverflow {
802 media_sequence: playlist.media_sequence,
803 segments: playlist.segments.len(),
804 })?;
805
806 if !self.joined {
807 self.joined = true;
808 self.join_start(&playlist)?;
809 }
810
811 self.playlist_maps = self.map_entries(&playlist, text, next_msn);
812
813 for (i, seg) in playlist.segments.iter().enumerate() {
814 let msn = playlist.media_sequence.saturating_add(index_u64(i));
815 if lag_floor.is_some_and(|floor| msn < floor) {
816 self.skip_segment(msn, seg)?;
817 continue;
818 }
819 if let Some(e) = seg.map.as_ref().and_then(|m| self.unusable_map(m)) {
820 self.skip_segment(msn, seg)?;
821 first_error.get_or_insert(e);
822 continue;
823 }
824 let key = match segment_keys.get(i) {
825 Some(Ok(key)) => key.as_ref(),
826 Some(Err(e)) => {
827 self.skip_segment(msn, seg)?;
828 first_error.get_or_insert(Error::PlaylistParse(e.clone()));
829 continue;
830 }
831 None => None,
832 };
833 match self.process_closed_segment(msn, seg, key, playlist.iframes_only) {
834 Ok(()) => {}
835 Err(e @ Error::UnsupportedEncryption { .. }) => {
836 self.skip_segment(msn, seg)?;
837 first_error.get_or_insert(e);
838 }
839 Err(e) => return Err(e),
840 }
841 }
842
843 if let Some(open) = &playlist.open_segment {
844 if let Some(e) = open.map.as_ref().and_then(|m| self.unusable_map(m)) {
845 first_error.get_or_insert(e);
846 } else {
847 match &open_key {
848 Some(key) => {
849 first_error.get_or_insert(Error::UnsupportedEncryption {
850 method: key.method,
851 what: "encrypted partial segments",
852 });
853 }
854 None => self.process_open_segment(next_msn, open)?,
855 }
856 }
857 }
858
859 self.pump_init_fetch();
860
861 if let Some(ll) = &playlist.low_latency
862 && let Some(hint_uri) = &ll.preload_hint_part
863 {
864 match ll.preload_hint_type {
865 PreloadHintType::Part => {
866 let part_idx = playlist
867 .open_segment
868 .as_ref()
869 .map(|o| index_u64(o.parts.len()))
870 .unwrap_or(0);
871 let id = ResourceId::Part {
872 msn: next_msn,
873 part: part_idx,
874 };
875 if !self.requested.contains(&id)
876 && !self.delivered_parts.contains(&(next_msn, part_idx))
877 {
878 let url = url::resolve(self.base(), hint_uri);
879 let byte_range = self.resolve_hint_byte_range(&url, ll)?;
880 self.request_resource(id, url, byte_range);
881 }
882 }
883 PreloadHintType::Map => {
884 let map = MapTag {
885 uri: hint_uri.clone(),
886 byte_range: ll.preload_hint_byte_range_length.map(|length| ByteRange {
887 length,
888 offset: ll.preload_hint_byte_range_start,
889 }),
890 extra_attrs: Vec::new(),
891 };
892 self.ensure_init_requested(&map)?;
893 }
894 _ => {
895 }
900 }
901 }
902
903 if playlist.endlist {
904 self.saw_endlist = true;
905 } else {
906 let blocking = playlist
911 .low_latency
912 .as_ref()
913 .filter(|ll| ll.can_block_reload)
914 .map(|_| {
915 let part = playlist
916 .open_segment
917 .as_ref()
918 .map(|o| index_u64(o.parts.len()))
919 .unwrap_or(0);
920 BlockingReload {
921 msn: next_msn,
922 part: Some(part),
923 }
924 });
925 let can_skip = playlist
926 .low_latency
927 .as_ref()
928 .and_then(|ll| ll.can_skip_until)
929 .is_some();
930 let skip = can_skip && self.last_full_playlist.is_some();
931 self.pending_actions.push_back(Action::FetchPlaylist {
932 url: self.playlist_url.clone(),
933 blocking,
934 skip,
935 });
936 if blocking.is_none() {
937 self.pending_actions
938 .push_back(Action::WaitMs(reload_wait_ms(playlist.target_duration)));
939 }
940 }
941
942 self.prune(&playlist);
943 self.last_full_playlist = Some(playlist);
944
945 self.maybe_emit_end_of_stream()?;
946 match first_error {
947 Some(e) => Err(e),
948 None => Ok(()),
949 }
950 }
951
952 fn request_full_playlist(&mut self, delta: &MediaPlaylist) -> Error {
956 if self.full_reload_requested {
957 self.pending_actions
958 .push_back(Action::WaitMs(reload_wait_ms(delta.target_duration)));
959 }
960 self.full_reload_requested = true;
961 self.pending_actions.push_back(Action::FetchPlaylist {
962 url: self.playlist_url.clone(),
963 blocking: None,
964 skip: false,
965 });
966 Error::DeltaNotMergeable {
967 media_sequence: delta.media_sequence,
968 skipped_segments: delta.skip.as_ref().map_or(0, |s| s.skipped_segments),
969 }
970 }
971
972 pub fn ensure_playlist_reload(&mut self) -> bool {
976 if self
977 .pending_actions
978 .iter()
979 .any(|a| matches!(a, Action::FetchPlaylist { .. }))
980 {
981 return false;
982 }
983 self.pending_actions.push_back(Action::FetchPlaylist {
984 url: self.playlist_url.clone(),
985 blocking: None,
986 skip: false,
987 });
988 true
989 }
990
991 pub fn retry_init(&mut self) -> bool {
997 if self
998 .inits
999 .values()
1000 .any(|slot| slot.fetch == InitFetch::InFlight)
1001 {
1002 return false;
1003 }
1004 let failed: Vec<InitId> = self
1005 .inits
1006 .iter()
1007 .filter(|(_, slot)| slot.fetch == InitFetch::Failed)
1008 .map(|(id, _)| *id)
1009 .collect();
1010 for id in &failed {
1011 if let Some(slot) = self.inits.get_mut(id) {
1012 slot.fetch = InitFetch::Wanted;
1013 }
1014 }
1015 self.pump_init_fetch();
1016 !failed.is_empty()
1017 }
1018
1019 pub fn retry_resource(&mut self, id: ResourceId) -> bool {
1025 if self.requested.contains(&id) {
1026 return false;
1027 }
1028 let Some((url, byte_range)) = self.resource_requests.get(&id).cloned() else {
1029 return false;
1030 };
1031 self.request_resource(id, url, byte_range);
1032 true
1033 }
1034
1035 pub fn skip_resource(&mut self, id: ResourceId) {
1041 match id {
1042 ResourceId::Init => return,
1043 ResourceId::Segment { msn } => {
1044 self.delivered_segments.insert(msn);
1045 }
1046 ResourceId::Part { msn, part } => {
1047 self.delivered_parts.insert((msn, part));
1048 }
1049 }
1050 self.requested.remove(&id);
1051 self.resource_requests.remove(&id);
1052 let _ = self.restart_ts_demux(id);
1054 self.pending_outputs.push_back(Output::Discontinuity);
1055 let _ = self.maybe_emit_end_of_stream();
1056 }
1057
1058 fn map_entries(&self, playlist: &MediaPlaylist, text: &str, next_msn: u64) -> Vec<MapEntry> {
1061 let keyed = keys_at_maps(text);
1062 let mut entries: Vec<MapEntry> = Vec::new();
1063 let listed = playlist
1064 .segments
1065 .iter()
1066 .enumerate()
1067 .filter_map(|(i, s)| {
1068 s.map
1069 .as_ref()
1070 .map(|m| (m, playlist.media_sequence.saturating_add(index_u64(i))))
1071 })
1072 .chain(
1073 playlist
1074 .open_segment
1075 .as_ref()
1076 .and_then(|o| o.map.as_ref())
1077 .map(|m| (m, next_msn)),
1078 );
1079 for (map, msn) in listed {
1080 if entries.iter().any(|e| e.map == *map) {
1081 continue;
1082 }
1083 let key = match keyed.iter().rev().find(|(m, _)| m == map) {
1084 Some((_, key)) => key.clone(),
1085 None => self
1086 .init_slot_for(&url::resolve(self.base(), &map.uri), &map.byte_range)
1087 .and_then(|id| self.inits.get(&id))
1088 .and_then(|slot| slot.encryption.clone()),
1089 };
1090 entries.push(MapEntry {
1091 map: map.clone(),
1092 encryption: key,
1093 first_msn: msn,
1094 });
1095 }
1096 entries
1097 }
1098
1099 fn unusable_map(&self, map: &MapTag) -> Option<Error> {
1100 self.playlist_maps
1101 .iter()
1102 .find(|e| e.map == *map)
1103 .and_then(MapEntry::unusable)
1104 }
1105
1106 fn init_slot_for(&self, url: &str, byte_range: &Option<ByteRange>) -> Option<InitId> {
1111 self.inits
1112 .iter()
1113 .find(|(_, slot)| {
1114 without_query(&slot.url) == without_query(url) && slot.map.byte_range == *byte_range
1115 })
1116 .map(|(id, _)| *id)
1117 }
1118
1119 fn init_for_map(&mut self, map: &MapTag) -> Result<InitId> {
1123 let url = url::resolve(self.base(), &map.uri);
1124 if let Some(id) = self.init_slot_for(&url, &map.byte_range)
1125 && let Some(slot) = self.inits.get_mut(&id)
1126 {
1127 if slot.url != url {
1128 slot.url = url;
1129 slot.map = map.clone();
1130 }
1131 if slot.fetch == InitFetch::Failed {
1132 slot.fetch = InitFetch::Wanted;
1133 }
1134 return Ok(id);
1135 }
1136 let (key, first_msn) = self
1137 .playlist_maps
1138 .iter()
1139 .find(|e| e.map == *map)
1140 .map_or((None, 0), |e| (e.encryption.clone(), e.first_msn));
1141 let byte_range = self.resolve_byte_range(&url, &map.byte_range)?;
1142 let crypto = match key.as_ref().and_then(|k| k.uri.as_deref().map(|u| (k, u))) {
1143 Some((key, uri)) => {
1144 let key_id = self.ensure_key_requested(uri);
1145 let iv = key.iv.unwrap_or_else(|| media_sequence_iv(first_msn));
1146 Some(SegmentCrypto { key_id, iv })
1147 }
1148 None => None,
1149 };
1150 let id = self.next_init_id;
1151 self.next_init_id = self.next_init_id.saturating_add(1);
1152 self.inits.insert(
1153 id,
1154 InitSlot {
1155 map: map.clone(),
1156 url,
1157 byte_range,
1158 encryption: key,
1159 crypto,
1160 fetch: InitFetch::Wanted,
1161 cipher: None,
1162 bytes: None,
1163 skipped_reported: BTreeSet::new(),
1164 },
1165 );
1166 Ok(id)
1167 }
1168
1169 fn pump_init_fetch(&mut self) {
1173 if self
1174 .inits
1175 .values()
1176 .any(|slot| slot.fetch == InitFetch::InFlight)
1177 {
1178 return;
1179 }
1180 let Some((id, slot)) = self
1181 .inits
1182 .iter_mut()
1183 .find(|(_, slot)| slot.fetch == InitFetch::Wanted)
1184 else {
1185 return;
1186 };
1187 slot.fetch = InitFetch::InFlight;
1188 let id = *id;
1189 let fetch = Action::FetchResource {
1190 id: ResourceId::Init,
1191 url: slot.url.clone(),
1192 byte_range: slot.byte_range,
1193 };
1194 self.init_target = Some(id);
1195 self.pending_actions.push_back(fetch);
1196 self.outstanding_fetches += 1;
1197 }
1198
1199 fn bind_init(&mut self, msn: u64, map: Option<&MapTag>) -> Result<()> {
1202 if let Some(map) = map {
1203 let id = self.init_for_map(map)?;
1204 self.msn_init.insert(msn, id);
1205 }
1206 Ok(())
1207 }
1208
1209 fn init_of(&self, id: &ResourceId) -> Option<InitId> {
1212 resource_msn(id)
1213 .and_then(|msn| self.msn_init.get(&msn).copied())
1214 .or_else(|| self.inits.keys().next_back().copied())
1215 }
1216
1217 fn init_ready(&self, slot: Option<InitId>) -> bool {
1218 slot.and_then(|id| self.inits.get(&id))
1219 .is_some_and(|slot| slot.bytes.is_some())
1220 }
1221
1222 pub fn on_resource(&mut self, id: ResourceId, bytes: &[u8]) -> Result<()> {
1235 let was_requested = match id {
1236 ResourceId::Init => self.init_target.is_some(),
1237 ResourceId::Part { .. } | ResourceId::Segment { .. } => self.requested.contains(&id),
1238 };
1239 if !was_requested {
1240 return Err(Error::UnrequestedResource { id });
1241 }
1242 let duplicate = match id {
1243 ResourceId::Init => !self
1244 .init_target
1245 .and_then(|t| self.inits.get(&t))
1246 .is_some_and(|slot| slot.fetch == InitFetch::InFlight),
1247 ResourceId::Part { .. } | ResourceId::Segment { .. } => self.fulfilled.contains(&id),
1248 };
1249 if duplicate {
1250 return Err(Error::DuplicateResource { id });
1251 }
1252 self.outstanding_fetches = self.outstanding_fetches.saturating_sub(1);
1253 if id != ResourceId::Init {
1254 self.fulfilled.insert(id);
1255 }
1256 match id {
1257 ResourceId::Init => self.accept_init(bytes)?,
1258 ResourceId::Part { .. } | ResourceId::Segment { .. } => {
1259 let processed = self.accept_media_resource(id, bytes);
1260 match processed {
1261 Ok(()) => {}
1262 Err(e @ Error::PendingKeyedOverflow { .. }) => return Err(e),
1263 Err(e) => {
1264 self.fulfilled.remove(&id);
1265 self.requested.remove(&id);
1266 return Err(e);
1267 }
1268 }
1269 }
1270 }
1271 self.maybe_emit_end_of_stream()?;
1272 Ok(())
1273 }
1274
1275 fn accept_init(&mut self, bytes: &[u8]) -> Result<()> {
1276 let Some(target) = self.init_target else {
1277 return Ok(());
1278 };
1279 let Some(slot) = self.inits.get_mut(&target) else {
1280 return Ok(());
1281 };
1282 if slot.crypto.is_none() && !has_movie_box(bytes) {
1283 slot.fetch = InitFetch::Failed;
1284 return Err(Error::InitUndecodable);
1285 }
1286 match slot.crypto {
1287 Some(crypto) => match self.key_material.get(&crypto.key_id).copied() {
1288 Some(KeyBytes(key)) => self.complete_encrypted_init(target, crypto, &key, bytes),
1289 None => {
1290 slot.cipher = Some(bytes.to_vec());
1291 slot.fetch = InitFetch::Arrived;
1292 Ok(())
1293 }
1294 },
1295 None => self.complete_init(target, bytes),
1296 }
1297 }
1298
1299 fn complete_init(&mut self, target: InitId, plain: &[u8]) -> Result<()> {
1303 if let Some(slot) = self.inits.get_mut(&target) {
1304 slot.bytes = Some(plain.to_vec());
1305 slot.cipher = None;
1306 slot.fetch = InitFetch::Ready;
1307 }
1308 if self.announced_init.is_none() {
1309 self.announce_init(target);
1310 }
1311 self.pump_init_fetch();
1312 let buffered: Vec<_> = self.pending_demux.drain(..).collect();
1313 let mut waiting = VecDeque::new();
1314 let mut failure = None;
1315 for (bid, bbytes) in buffered {
1316 if failure.is_some() || !self.init_ready(self.init_of(&bid)) {
1317 waiting.push_back((bid, bbytes));
1318 continue;
1319 }
1320 if let Err(e) = self.finish_media_resource(bid, &bbytes) {
1321 self.fulfilled.remove(&bid);
1322 self.requested.remove(&bid);
1323 failure = Some(e);
1324 }
1325 }
1326 self.pending_demux = waiting;
1327 match failure {
1328 Some(e) => Err(e),
1329 None => Ok(()),
1330 }
1331 }
1332
1333 fn announce_init(&mut self, slot: InitId) {
1336 if self.announced_init == Some(slot) {
1337 return;
1338 }
1339 let Some(bytes) = self.inits.get(&slot).and_then(|s| s.bytes.clone()) else {
1340 return;
1341 };
1342 self.pending_outputs.push_back(Output::Init(bytes));
1343 self.announced_init = Some(slot);
1344 self.last_fmp4_specs = None;
1345 }
1346
1347 fn complete_encrypted_init(
1348 &mut self,
1349 target: InitId,
1350 crypto: SegmentCrypto,
1351 key: &[u8; KEY_LEN],
1352 cipher: &[u8],
1353 ) -> Result<()> {
1354 match decrypt_segment(ResourceId::Init, crypto, key, cipher) {
1355 Ok(plain) if !has_movie_box(&plain) => {
1356 if let Some(slot) = self.inits.get_mut(&target) {
1357 slot.fetch = InitFetch::Failed;
1358 }
1359 Err(Error::InitUndecodable)
1360 }
1361 Ok(plain) => self.complete_init(target, &plain),
1362 Err(e) => {
1363 if let Some(slot) = self.inits.get_mut(&target) {
1364 slot.fetch = InitFetch::Failed;
1365 }
1366 Err(e)
1367 }
1368 }
1369 }
1370
1371 fn accept_media_resource(&mut self, id: ResourceId, bytes: &[u8]) -> Result<()> {
1374 if let ResourceId::Segment { msn } = id
1375 && let Some(crypto) = self.segment_crypto.get(&msn).copied()
1376 {
1377 return match self.key_material.get(&crypto.key_id).copied() {
1378 Some(KeyBytes(key)) => {
1379 let plain = decrypt_segment(id, crypto, &key, bytes)?;
1380 self.route_media_resource(id, &plain)
1381 }
1382 None => self.buffer_until_keyed(id, bytes),
1383 };
1384 }
1385 self.route_media_resource(id, bytes)
1386 }
1387
1388 fn buffer_until_keyed(&mut self, id: ResourceId, bytes: &[u8]) -> Result<()> {
1393 if bytes.len() > MAX_PENDING_KEYED_BYTES {
1394 self.fulfilled.remove(&id);
1395 self.requested.remove(&id);
1396 return Err(Error::PendingKeyedOverflow { dropped: id });
1397 }
1398 let mut held: usize = self.pending_keyed.iter().map(|(_, b)| b.len()).sum();
1399 let mut dropped: Option<ResourceId> = None;
1400 while held.saturating_add(bytes.len()) > MAX_PENDING_KEYED_BYTES
1401 && !self.pending_keyed.is_empty()
1402 {
1403 let (old_id, old) = self.pending_keyed.remove(0);
1404 held = held.saturating_sub(old.len());
1405 self.fulfilled.remove(&old_id);
1406 self.requested.remove(&old_id);
1407 dropped.get_or_insert(old_id);
1408 }
1409 self.pending_keyed.push((id, bytes.to_vec()));
1410 match dropped {
1411 Some(dropped) => Err(Error::PendingKeyedOverflow { dropped }),
1412 None => Ok(()),
1413 }
1414 }
1415
1416 fn route_media_resource(&mut self, id: ResourceId, bytes: &[u8]) -> Result<()> {
1417 let recognised = if self.is_ts_segment(bytes) {
1418 segment::ts_structure(bytes)
1419 } else {
1420 segment::media_fragment(bytes) || segment::progressive_movie(bytes)
1421 };
1422 if !recognised {
1423 return Err(Error::NotMedia { id });
1424 }
1425 if self.is_ts_segment(bytes) {
1426 self.finish_ts_resource(id, bytes)
1431 } else if !self.init_ready(self.init_of(&id)) {
1432 self.pending_demux.push_back((id, bytes.to_vec()));
1433 Ok(())
1434 } else {
1435 self.finish_media_resource(id, bytes)
1436 }
1437 }
1438
1439 pub fn on_key(&mut self, id: KeyId, bytes: &[u8]) -> Result<()> {
1448 if !self.key_urls.contains_key(&id) || self.key_material.contains_key(&id) {
1449 return Err(Error::UnrequestedKey { id });
1450 }
1451 if self.keys_in_flight.remove(&id) {
1452 self.outstanding_fetches = self.outstanding_fetches.saturating_sub(1);
1453 }
1454 let key: [u8; KEY_LEN] = bytes.try_into().map_err(|_| Error::BadKeyLength {
1455 id,
1456 len: bytes.len(),
1457 })?;
1458 self.key_material.insert(id, KeyBytes(key));
1459 let waiting_inits: Vec<(InitId, SegmentCrypto)> = self
1460 .inits
1461 .iter()
1462 .filter(|(_, slot)| slot.fetch == InitFetch::Arrived)
1463 .filter_map(|(init, slot)| slot.crypto.filter(|c| c.key_id == id).map(|c| (*init, c)))
1464 .collect();
1465 for (init, crypto) in waiting_inits {
1466 let cipher = self
1467 .inits
1468 .get_mut(&init)
1469 .and_then(|slot| slot.cipher.take());
1470 if let Some(cipher) = cipher {
1471 self.complete_encrypted_init(init, crypto, &key, &cipher)?;
1472 }
1473 }
1474 let (mut ready, rest): (Vec<_>, Vec<_>) = core::mem::take(&mut self.pending_keyed)
1475 .into_iter()
1476 .partition(|(rid, _)| {
1477 resource_msn(rid)
1478 .and_then(|msn| self.segment_crypto.get(&msn))
1479 .is_some_and(|c| c.key_id == id)
1480 });
1481 self.pending_keyed = rest;
1482 ready.sort_by_key(|(rid, _)| resource_msn(rid));
1483 let mut ready = ready.into_iter();
1484 while let Some((rid, cipher)) = ready.next() {
1485 let crypto = resource_msn(&rid).and_then(|msn| self.segment_crypto.get(&msn).copied());
1486 let result = match crypto {
1487 Some(crypto) => decrypt_segment(rid, crypto, &key, &cipher)
1488 .and_then(|plain| self.route_media_resource(rid, &plain)),
1489 None => Ok(()),
1490 };
1491 if let Err(e) = result {
1492 self.fulfilled.remove(&rid);
1493 self.requested.remove(&rid);
1494 self.pending_keyed.extend(ready);
1495 return Err(e);
1496 }
1497 }
1498 self.maybe_emit_end_of_stream()?;
1499 Ok(())
1500 }
1501
1502 pub fn on_key_error(&mut self, id: KeyId) {
1505 if self.keys_in_flight.remove(&id) {
1506 self.outstanding_fetches = self.outstanding_fetches.saturating_sub(1);
1507 }
1508 }
1509
1510 fn ensure_key_requested(&mut self, uri: &str) -> KeyId {
1513 let url = url::resolve(self.base(), uri);
1514 let id = match self.key_ids.get(&url) {
1515 Some(id) => *id,
1516 None => {
1517 let id = KeyId(self.next_key_id);
1518 self.next_key_id = self.next_key_id.saturating_add(1);
1519 self.key_ids.insert(url.clone(), id);
1520 self.key_urls.insert(id, url);
1521 id
1522 }
1523 };
1524 self.request_key(id);
1525 id
1526 }
1527
1528 fn request_key(&mut self, id: KeyId) {
1530 let Some(url) = self.key_urls.get(&id).cloned() else {
1531 return;
1532 };
1533 if !self.key_material.contains_key(&id) && self.keys_in_flight.insert(id) {
1534 self.outstanding_fetches += 1;
1535 self.pending_actions.push_back(Action::FetchKey { id, url });
1536 }
1537 }
1538
1539 fn forget_keys_below(&mut self, first_msn: u64) {
1544 self.segment_crypto.retain(|&m, _| m >= first_msn);
1545 let mut live: BTreeSet<KeyId> = self.segment_crypto.values().map(|c| c.key_id).collect();
1546 live.extend(
1547 self.inits
1548 .values()
1549 .filter_map(|slot| slot.crypto.map(|c| c.key_id)),
1550 );
1551 let abandoned: Vec<KeyId> = self
1552 .keys_in_flight
1553 .iter()
1554 .copied()
1555 .filter(|id| !live.contains(id))
1556 .collect();
1557 for id in abandoned {
1558 self.keys_in_flight.remove(&id);
1559 self.outstanding_fetches = self.outstanding_fetches.saturating_sub(1);
1560 self.pending_actions
1561 .retain(|a| !matches!(a, Action::FetchKey { id: queued, .. } if *queued == id));
1562 }
1563 self.key_ids.retain(|_, id| live.contains(id));
1564 self.key_urls.retain(|id, _| live.contains(id));
1565 self.key_material.retain(|id, _| live.contains(id));
1566 }
1567
1568 fn prepare_encrypted_segment(
1571 &mut self,
1572 msn: u64,
1573 seg: &MediaSegment,
1574 key: &MediaKey,
1575 iframes_only: bool,
1576 ) -> Result<()> {
1577 let unsupported = |what| Error::UnsupportedEncryption {
1578 method: key.method,
1579 what,
1580 };
1581 if key.method != EncryptionMethod::Aes128 {
1582 return Err(unsupported("only METHOD=AES-128 is decrypted"));
1583 }
1584 if iframes_only {
1585 return Err(unsupported("EXT-X-I-FRAMES-ONLY with encryption"));
1586 }
1587 if !seg.parts.is_empty() {
1588 return Err(unsupported("encrypted partial segments"));
1589 }
1590 let Some(uri) = key.uri.as_deref() else {
1591 return Err(unsupported("key without URI"));
1592 };
1593 if let Some(existing) = self.segment_crypto.get(&msn).copied() {
1594 self.request_key(existing.key_id);
1595 return Ok(());
1596 }
1597 let id = self.ensure_key_requested(uri);
1598 let iv = key.iv.unwrap_or_else(|| media_sequence_iv(msn));
1599 self.segment_crypto
1600 .insert(msn, SegmentCrypto { key_id: id, iv });
1601 Ok(())
1602 }
1603
1604 fn finish_media_resource(&mut self, id: ResourceId, bytes: &[u8]) -> Result<()> {
1607 match id {
1608 ResourceId::Part { msn, part } => {
1609 self.emit_discontinuity_if_needed(msn);
1610 self.demux_and_emit(id, bytes)?;
1611 self.delivered_parts.insert((msn, part));
1612 }
1613 ResourceId::Segment { msn } => {
1614 self.emit_discontinuity_if_needed(msn);
1615 self.demux_and_emit(id, bytes)?;
1616 self.delivered_segments.insert(msn);
1617 }
1618 ResourceId::Init => {}
1619 }
1620 Ok(())
1621 }
1622
1623 fn finish_ts_resource(&mut self, id: ResourceId, bytes: &[u8]) -> Result<()> {
1629 match id {
1630 ResourceId::Part { msn, part } => {
1631 self.begin_ts_resource(id, msn)?;
1632 self.demux_and_emit_ts(id, bytes)?;
1633 self.delivered_parts.insert((msn, part));
1634 }
1635 ResourceId::Segment { msn } => {
1636 self.begin_ts_resource(id, msn)?;
1637 self.demux_and_emit_ts(id, bytes)?;
1638 self.release_ts_segment_end(id)?;
1639 self.delivered_segments.insert(msn);
1640 }
1641 ResourceId::Init => {}
1642 }
1643 Ok(())
1644 }
1645
1646 fn is_ts_segment(&self, bytes: &[u8]) -> bool {
1657 self.inits.is_empty()
1658 && (bytes.first() == Some(&TS_SYNC_BYTE) || segment::ts_structure(bytes))
1659 }
1660
1661 pub fn on_error(&mut self, id: Option<ResourceId>) {
1666 if let Some(id) = id {
1667 self.outstanding_fetches = self.outstanding_fetches.saturating_sub(1);
1668 match id {
1669 ResourceId::Init => {
1670 if let Some(slot) = self
1671 .init_target
1672 .and_then(|target| self.inits.get_mut(&target))
1673 .filter(|slot| slot.fetch == InitFetch::InFlight)
1674 {
1675 slot.fetch = InitFetch::Failed;
1676 }
1677 self.pump_init_fetch();
1678 }
1679 other => {
1680 self.requested.remove(&other);
1681 }
1682 }
1683 }
1684 let _ = self.maybe_emit_end_of_stream();
1688 }
1689
1690 fn merge_delta(&self, playlist: MediaPlaylist) -> MediaPlaylist {
1700 let Some(skip) = &playlist.skip else {
1701 return playlist;
1702 };
1703 if skip.skipped_segments == 0 {
1704 let mut merged = playlist;
1705 merged.skip = None;
1706 return merged;
1707 }
1708 let Some(prev) = &self.last_full_playlist else {
1709 return playlist;
1710 };
1711 if playlist.media_sequence < prev.media_sequence {
1712 return playlist;
1713 }
1714 let Ok(prefix_start) = usize::try_from(playlist.media_sequence - prev.media_sequence)
1715 else {
1716 return playlist;
1717 };
1718 let prefix_end = usize::try_from(skip.skipped_segments)
1728 .ok()
1729 .and_then(|skipped| prefix_start.checked_add(skipped));
1730 let Some(prefix) = prefix_end.and_then(|end| prev.segments.get(prefix_start..end)) else {
1731 return playlist;
1732 };
1733 let mut merged = playlist;
1734 let mut segments = prefix.to_vec();
1735 if let (Some(first), Some(dropped)) =
1736 (segments.first_mut(), prev.segments.get(..prefix_start))
1737 {
1738 let mut carried = carried_key_tags(dropped);
1739 carried.append(&mut first.pre_tags);
1740 first.pre_tags = carried;
1741 }
1742 segments.extend(merged.segments);
1743 merged.segments = segments;
1744 merged.skip = None;
1745 merged
1746 }
1747
1748 fn process_closed_segment(
1749 &mut self,
1750 msn: u64,
1751 seg: &MediaSegment,
1752 key: Option<&MediaKey>,
1753 iframes_only: bool,
1754 ) -> Result<()> {
1755 if seg.discontinuous {
1756 self.discontinuous_msns.insert(msn);
1757 }
1758 if self.delivered_segments.contains(&msn) {
1759 return Ok(());
1760 }
1761 match key {
1762 Some(key) => self.prepare_encrypted_segment(msn, seg, key, iframes_only)?,
1763 None => {
1764 self.segment_crypto.remove(&msn);
1765 }
1766 }
1767 if seg.parts.is_empty() {
1768 let already_have_parts = self
1785 .delivered_parts
1786 .range((msn, 0)..=(msn, u64::MAX))
1787 .next()
1788 .is_some();
1789 if already_have_parts {
1790 self.delivered_segments.insert(msn);
1791 return Ok(());
1792 }
1793 let id = ResourceId::Segment { msn };
1794 if !self.requested.contains(&id) {
1795 self.bind_init(msn, seg.map.as_ref())?;
1796 let url = url::resolve(self.base(), &seg.uri);
1797 let byte_range = self.resolve_byte_range(&url, &seg.byte_range)?;
1798 self.request_resource(id, url, byte_range);
1799 }
1800 return Ok(());
1801 }
1802
1803 let mut fully_accounted = true;
1804 for (i, part) in seg.parts.iter().enumerate() {
1805 let i = index_u64(i);
1806 if part.gap || self.delivered_parts.contains(&(msn, i)) {
1807 continue;
1808 }
1809 fully_accounted = false;
1810 let id = ResourceId::Part { msn, part: i };
1811 if !self.requested.contains(&id) {
1812 self.bind_init(msn, seg.map.as_ref())?;
1813 let url = url::resolve(self.base(), &part.uri);
1814 let byte_range = self.resolve_byte_range(&url, &part.byte_range)?;
1815 self.request_resource(id, url, byte_range);
1816 }
1817 }
1818 if fully_accounted {
1819 self.delivered_segments.insert(msn);
1820 }
1821 Ok(())
1822 }
1823
1824 fn process_open_segment(&mut self, msn: u64, open: &OpenSegment) -> Result<()> {
1825 for (i, part) in open.parts.iter().enumerate() {
1826 let i = index_u64(i);
1827 if part.gap || self.delivered_parts.contains(&(msn, i)) {
1828 continue;
1829 }
1830 let id = ResourceId::Part { msn, part: i };
1831 if !self.requested.contains(&id) {
1832 self.bind_init(msn, open.map.as_ref())?;
1833 let url = url::resolve(self.base(), &part.uri);
1834 let byte_range = self.resolve_byte_range(&url, &part.byte_range)?;
1835 self.request_resource(id, url, byte_range);
1836 }
1837 }
1838 Ok(())
1839 }
1840
1841 fn ensure_init_requested(&mut self, map: &MapTag) -> Result<()> {
1842 self.init_for_map(map)?;
1843 self.pump_init_fetch();
1844 Ok(())
1845 }
1846
1847 fn request_resource(&mut self, id: ResourceId, url: String, byte_range: Option<(u64, u64)>) {
1848 self.requested.insert(id);
1849 self.resource_requests.insert(id, (url.clone(), byte_range));
1850 self.outstanding_fetches += 1;
1851 self.pending_actions.push_back(Action::FetchResource {
1852 id,
1853 url,
1854 byte_range,
1855 });
1856 }
1857
1858 fn resolve_byte_range(
1868 &mut self,
1869 url: &str,
1870 br: &Option<ByteRange>,
1871 ) -> Result<Option<(u64, u64)>> {
1872 let Some(br) = br.as_ref() else {
1873 return Ok(None);
1874 };
1875 let offset = br
1876 .offset
1877 .unwrap_or_else(|| *self.byte_range_cursor.get(url).unwrap_or(&0));
1878 let next_cursor =
1879 offset
1880 .checked_add(br.length)
1881 .ok_or_else(|| Error::ByteRangeOverflow {
1882 url: url.to_string(),
1883 offset,
1884 length: br.length,
1885 })?;
1886 self.byte_range_cursor.insert(url.to_string(), next_cursor);
1887 Ok(Some((offset, br.length)))
1888 }
1889
1890 fn resolve_hint_byte_range(
1893 &mut self,
1894 url: &str,
1895 ll: &broadcast_hls::LowLatencyConfig,
1896 ) -> Result<Option<(u64, u64)>> {
1897 let Some(length) = ll.preload_hint_byte_range_length else {
1898 return Ok(None);
1899 };
1900 let br = ByteRange {
1901 length,
1902 offset: ll.preload_hint_byte_range_start,
1903 };
1904 self.resolve_byte_range(url, &Some(br))
1905 }
1906
1907 fn demux_and_emit(&mut self, id: ResourceId, bytes: &[u8]) -> Result<()> {
1908 let slot = self.init_of(&id).filter(|s| self.init_ready(Some(*s)));
1909 let Some(slot) = slot else {
1910 return Err(Error::InitNotYetAvailable { id });
1911 };
1912 self.announce_init(slot);
1913 let init = self
1914 .inits
1915 .get(&slot)
1916 .and_then(|s| s.bytes.as_ref())
1917 .ok_or(Error::InitNotYetAvailable { id })?;
1918 if segment::progressive_movie(bytes) {
1919 return Err(Error::NotFragmented { id });
1920 }
1921 let mut combined = Vec::with_capacity(init.len() + bytes.len());
1922 combined.extend_from_slice(init);
1923 combined.extend_from_slice(bytes);
1924 let mut demux = Fmp4Demux::new();
1925 let media = demux
1926 .unpackage(combined.as_slice())
1927 .map_err(|source| Error::Demux { id, source })?;
1928 let damaged = media
1929 .degraded
1930 .iter()
1931 .filter(|d| matches!(d, InputDegradation::MalformedUnit { .. }))
1932 .count();
1933 if damaged > 0 {
1934 self.pending_outputs.push_back(Output::FragmentDamaged {
1935 id,
1936 skipped: damaged,
1937 });
1938 }
1939 for skipped in &media.skipped {
1940 let key = (skipped.track_id.map(TrackId::value), skipped.fourcc.clone());
1941 let first = self
1942 .inits
1943 .get_mut(&slot)
1944 .is_some_and(|s| s.skipped_reported.insert(key));
1945 if first {
1946 self.pending_outputs.push_back(Output::TrackSkipped {
1947 track_id: skipped.track_id,
1948 fourcc: skipped.fourcc.clone(),
1949 reason: skipped.reason.clone(),
1950 });
1951 }
1952 }
1953 let specs: Vec<TrackSpec> = media.tracks.iter().map(|t| t.spec.clone()).collect();
1954 let specs_changed = self
1955 .last_fmp4_specs
1956 .as_ref()
1957 .map(|prev| format!("{:?}", prev) != format!("{:?}", specs))
1958 .unwrap_or(true);
1959 if specs_changed {
1960 self.pending_outputs
1961 .push_back(Output::Tracks(specs.clone()));
1962 self.last_fmp4_specs = Some(specs);
1963 }
1964 for track in media.tracks {
1965 if !track.samples.is_empty() {
1966 self.pending_outputs.push_back(Output::Samples {
1967 track_id: track.spec.track_id,
1968 samples: track.samples,
1969 });
1970 }
1971 }
1972 Ok(())
1973 }
1974
1975 fn begin_ts_resource(&mut self, id: ResourceId, msn: u64) -> Result<()> {
1985 if self.discontinuous_msns.contains(&msn) && !self.discontinuity_emitted.contains(&msn) {
1986 self.restart_ts_demux(id)?;
1987 self.emit_discontinuity_if_needed(msn);
1988 }
1989 Ok(())
1990 }
1991
1992 fn restart_ts_demux(&mut self, id: ResourceId) -> Result<()> {
1993 let Some(TsDemuxState(demux)) = self.ts_demux.as_mut() else {
1994 return Ok(());
1995 };
1996 demux.restart_timeline();
1997 self.drain_ts_demux(id)
1998 }
1999
2000 fn demux_and_emit_ts(&mut self, id: ResourceId, bytes: &[u8]) -> Result<()> {
2024 let TsDemuxState(demux) = self
2025 .ts_demux
2026 .get_or_insert_with(|| TsDemuxState(StreamingTsDemux::new()));
2027 demux.feed(bytes);
2028 self.drain_ts_demux(id)
2029 }
2030
2031 fn release_ts_segment_end(&mut self, id: ResourceId) -> Result<()> {
2035 let Some(TsDemuxState(demux)) = self.ts_demux.as_mut() else {
2036 return Ok(());
2037 };
2038 demux.release_silent_heads();
2039 self.drain_ts_demux(id)
2040 }
2041
2042 fn flush_ts_demux(&mut self, id: ResourceId) -> Result<()> {
2045 let Some(TsDemuxState(demux)) = self.ts_demux.as_mut() else {
2046 return Ok(());
2047 };
2048 demux.finish();
2049 self.drain_ts_demux(id)
2050 }
2051
2052 fn drain_ts_demux(&mut self, id: ResourceId) -> Result<()> {
2055 let Some(TsDemuxState(demux)) = self.ts_demux.as_mut() else {
2056 return Ok(());
2057 };
2058 let mut per_track: BTreeMap<TrackId, Vec<Sample>> = BTreeMap::new();
2059 let mut events: Vec<DemuxEvent> = Vec::new();
2060 while let Some(event) = demux.poll_event() {
2061 match event {
2062 DemuxEvent::TrackAdded(spec) | DemuxEvent::TrackUpdated(spec) => {
2063 if spec.config.is_muxable_in_bmff() {
2064 self.ts_specs.insert(spec.track_id, spec);
2065 } else {
2066 self.ts_specs.remove(&spec.track_id);
2067 let stream_type = match &spec.config {
2068 transmux::CodecConfig::Data { stream_type, .. } => Some(*stream_type),
2069 _ => None,
2070 };
2071 self.pending_outputs.push_back(Output::TrackDropped {
2072 track_id: spec.track_id,
2073 stream_type,
2074 });
2075 }
2076 }
2077 DemuxEvent::TrackRemoved { track_id, .. } => {
2078 self.ts_specs.remove(&track_id);
2079 }
2080 DemuxEvent::Sample { track_id, .. }
2081 | DemuxEvent::DisplayChanged { track_id, .. }
2082 if self.ts_specs.contains_key(&track_id) =>
2083 {
2084 events.push(event);
2085 }
2086 _ => {}
2087 }
2088 }
2089 if !self.ts_specs.is_empty() {
2090 let specs: Vec<TrackSpec> = self.ts_specs.values().cloned().collect();
2091 let init_bytes = transmux::build_init_segment(&specs, TS_MOVIE_TIMESCALE)
2092 .map_err(|source| Error::Demux { id, source })?;
2093 if self.last_ts_init.as_deref() != Some(init_bytes.as_slice()) {
2094 self.pending_outputs
2095 .push_back(Output::Tracks(specs.clone()));
2096 self.pending_outputs
2097 .push_back(Output::Init(init_bytes.clone()));
2098 self.last_ts_init = Some(init_bytes);
2099 }
2100 }
2101 for event in events {
2102 match event {
2103 DemuxEvent::Sample {
2104 track_id, sample, ..
2105 } => {
2106 per_track.entry(track_id).or_default().push(sample);
2107 }
2108 DemuxEvent::DisplayChanged {
2109 track_id,
2110 display,
2111 pts,
2112 ..
2113 } => {
2114 if let Some(samples) = per_track.remove(&track_id)
2115 && !samples.is_empty()
2116 {
2117 self.pending_outputs
2118 .push_back(Output::Samples { track_id, samples });
2119 }
2120 self.pending_outputs.push_back(Output::DisplayChanged {
2121 track_id,
2122 pts,
2123 display,
2124 });
2125 }
2126 _ => {}
2127 }
2128 }
2129 for (track_id, samples) in per_track {
2130 if !samples.is_empty() {
2131 self.pending_outputs
2132 .push_back(Output::Samples { track_id, samples });
2133 }
2134 }
2135 Ok(())
2136 }
2137
2138 fn emit_discontinuity_if_needed(&mut self, msn: u64) -> bool {
2141 if self.discontinuous_msns.contains(&msn) && !self.discontinuity_emitted.contains(&msn) {
2142 self.pending_outputs.push_back(Output::Discontinuity);
2143 self.discontinuity_emitted.insert(msn);
2144 return true;
2145 }
2146 false
2147 }
2148
2149 fn join_start(&mut self, playlist: &MediaPlaylist) -> Result<()> {
2175 let segments = &playlist.segments;
2176 let target = f64::from(playlist.target_duration.max(1));
2177 let duration_of = |seg: &MediaSegment| {
2178 let d = seg.duration.get();
2179 if d > 0.0 { d } else { target }
2180 };
2181
2182 let from_start_tag = playlist.start.as_ref().map(|start| {
2183 let total: f64 = segments.iter().map(duration_of).sum();
2184 let offset = start.time_offset.get();
2185 let position = if offset >= 0.0 {
2186 offset
2187 } else {
2188 total + offset
2189 };
2190 let position = position.clamp(0.0, total);
2191 let mut elapsed = 0.0_f64;
2192 for (i, seg) in segments.iter().enumerate() {
2193 elapsed += duration_of(seg);
2194 if elapsed > position {
2195 return i;
2196 }
2197 }
2198 segments.len().saturating_sub(1)
2200 });
2201
2202 let from_hold_back = (!playlist.endlist).then(|| {
2203 let ll = playlist.low_latency.as_ref();
2204 let wanted = match ll.filter(|ll| ll.can_block_reload) {
2205 Some(ll) if ll.part_hold_back.is_some() => {
2206 ll.part_hold_back.map_or(0.0, |p| p.get())
2207 }
2208 _ => {
2209 let hold_back = ll.and_then(|ll| ll.hold_back).map_or(0.0, |h| h.get());
2210 (LIVE_EDGE_TARGET_DURATIONS * target).max(hold_back)
2211 }
2212 };
2213 let mut behind: f64 = playlist
2214 .open_segment
2215 .as_ref()
2216 .map_or(0.0, |o| o.parts.iter().map(|p| p.duration.get()).sum());
2217 if behind >= wanted {
2218 return segments.len();
2219 }
2220 for (i, seg) in segments.iter().enumerate().rev() {
2221 behind += duration_of(seg);
2222 if behind >= wanted {
2223 return i;
2224 }
2225 }
2226 0
2227 });
2228
2229 let start = match (from_start_tag, from_hold_back) {
2230 (Some(s), Some(h)) => s.min(h),
2231 (Some(s), None) => s,
2232 (None, Some(h)) => h,
2233 (None, None) => 0,
2234 };
2235 for (i, seg) in segments.iter().enumerate().take(start) {
2236 let msn = playlist.media_sequence.saturating_add(index_u64(i));
2237 self.skip_segment(msn, seg)?;
2238 }
2239 Ok(())
2240 }
2241
2242 fn skip_segment(&mut self, msn: u64, seg: &MediaSegment) -> Result<()> {
2246 if seg.parts.is_empty() {
2247 let url = url::resolve(self.base(), &seg.uri);
2248 self.resolve_byte_range(&url, &seg.byte_range)?;
2249 } else {
2250 for part in seg.parts.iter().filter(|p| !p.gap) {
2251 let url = url::resolve(self.base(), &part.uri);
2252 self.resolve_byte_range(&url, &part.byte_range)?;
2253 }
2254 }
2255 self.delivered_segments.insert(msn);
2256 Ok(())
2257 }
2258
2259 fn prune(&mut self, playlist: &MediaPlaylist) {
2266 let first = playlist.media_sequence;
2267 let below = |id: &ResourceId| resource_msn(id).is_some_and(|m| m < first);
2268 let fulfilled = &self.fulfilled;
2269 self.requested
2270 .retain(|id| !(below(id) && fulfilled.contains(id)));
2271 self.resource_requests.retain(|id, _| !below(id));
2272 self.fulfilled.retain(|id| !below(id));
2273 self.pending_keyed.retain(|(id, _)| !below(id));
2274 self.segment_crypto.retain(|&m, _| m >= first);
2275 self.delivered_parts.retain(|&(m, _)| m >= first);
2276 self.delivered_segments.retain(|&m| m >= first);
2277 self.discontinuous_msns.retain(|&m| m >= first);
2278 self.discontinuity_emitted.retain(|&m| m >= first);
2279 self.msn_init.retain(|&m, _| m >= first);
2280 self.prune_inits();
2281
2282 if playlist.skip.is_none() {
2283 let base = self.base();
2284 let mut live: BTreeSet<String> = BTreeSet::new();
2285 for seg in &playlist.segments {
2286 live.insert(url::resolve(base, &seg.uri));
2287 for part in &seg.parts {
2288 live.insert(url::resolve(base, &part.uri));
2289 }
2290 if let Some(map) = &seg.map {
2291 live.insert(url::resolve(base, &map.uri));
2292 }
2293 }
2294 if let Some(open) = &playlist.open_segment {
2295 for part in &open.parts {
2296 live.insert(url::resolve(base, &part.uri));
2297 }
2298 if let Some(map) = &open.map {
2299 live.insert(url::resolve(base, &map.uri));
2300 }
2301 }
2302 if let Some(hint) = playlist
2303 .low_latency
2304 .as_ref()
2305 .and_then(|ll| ll.preload_hint_part.as_ref())
2306 {
2307 live.insert(url::resolve(base, hint));
2308 }
2309 self.byte_range_cursor.retain(|u, _| live.contains(u));
2310 }
2311 }
2312
2313 fn prune_inits(&mut self) {
2316 let newest = self.inits.keys().next_back().copied();
2317 let bound: BTreeSet<InitId> = self.msn_init.values().copied().collect();
2318 self.inits.retain(|id, slot| {
2319 Some(*id) == newest || bound.contains(id) || slot.fetch != InitFetch::Ready
2320 });
2321 }
2322
2323 fn continuity(&self, new: &MediaPlaylist) -> Continuity {
2359 let Some(prev) = &self.last_full_playlist else {
2360 return if self
2361 .last_media_sequence
2362 .is_some_and(|p| new.media_sequence < p)
2363 {
2364 Continuity::Restart
2365 } else {
2366 Continuity::Continues
2367 };
2368 };
2369 let (p0, n0) = (prev.media_sequence, new.media_sequence);
2370 let regressed = n0 < self.last_media_sequence.unwrap_or(p0).max(p0);
2371
2372 let mut overlap = false;
2376 let mut shared = 0usize;
2377 let mut requeried = 0usize;
2378 for (i, seg) in new.segments.iter().enumerate() {
2379 let Some(msn) = n0.checked_add(index_u64(i)) else {
2380 return Continuity::Restart;
2381 };
2382 let Some(old) = msn
2383 .checked_sub(p0)
2384 .and_then(|d| usize::try_from(d).ok())
2385 .and_then(|d| prev.segments.get(d))
2386 else {
2387 continue;
2388 };
2389 if without_query(&old.uri) != without_query(&seg.uri)
2390 || old.duration != seg.duration
2391 || old.byte_range != seg.byte_range
2392 {
2393 return Continuity::Restart;
2394 }
2395 if let (Some(a), Some(b)) = (program_date_time(old), program_date_time(seg))
2396 && a != b
2397 {
2398 return Continuity::Restart;
2399 }
2400 overlap = true;
2401 shared = shared.saturating_add(1);
2402 if old.uri != seg.uri {
2403 requeried = requeried.saturating_add(1);
2404 }
2405 }
2406 if requeried != 0 && requeried != shared {
2407 return Continuity::Restart;
2408 }
2409
2410 if !regressed && new.discontinuity_sequence < prev.discontinuity_sequence {
2413 return Continuity::Restart;
2414 }
2415 if !regressed {
2416 return Continuity::Continues;
2417 }
2418
2419 if overlap {
2421 return Continuity::Lagging;
2422 }
2423 let old_uris: BTreeSet<&str> = prev.segments.iter().map(|s| s.uri.as_str()).collect();
2424 if new
2425 .segments
2426 .iter()
2427 .any(|s| old_uris.contains(s.uri.as_str()))
2428 {
2429 return Continuity::Restart;
2431 }
2432 let new_last = new.segments.iter().rev().find_map(program_date_time_ms);
2433 let prev_first = prev.segments.iter().find_map(program_date_time_ms);
2434 match (new_last, prev_first) {
2435 (Some(end), Some(start)) if end < start => Continuity::OlderWindow,
2436 _ => Continuity::Restart,
2437 }
2438 }
2439
2440 fn restart_after_regression(&mut self) -> Result<()> {
2452 self.flush_ts_demux(ResourceId::Init)?;
2455 self.ts_demux = None;
2456 self.ts_specs.clear();
2457 self.last_ts_init = None;
2458 self.pending_actions
2459 .retain(|a| !matches!(a, Action::FetchResource { .. }));
2460 self.pending_demux.clear();
2461 self.pending_actions
2462 .retain(|a| !matches!(a, Action::FetchKey { .. }));
2463 self.keys_in_flight.clear();
2464 self.segment_crypto.clear();
2465 self.pending_keyed.clear();
2466 self.requested.clear();
2467 self.fulfilled.clear();
2468 self.delivered_parts.clear();
2469 self.delivered_segments.clear();
2470 self.discontinuous_msns.clear();
2471 self.discontinuity_emitted.clear();
2472 self.byte_range_cursor.clear();
2473 self.outstanding_fetches = 0;
2474 self.last_full_playlist = None;
2475 self.inits.clear();
2476 self.init_target = None;
2477 self.announced_init = None;
2478 self.msn_init.clear();
2479 self.last_fmp4_specs = None;
2480 self.full_reload_requested = false;
2481 self.joined = false;
2482 self.pending_outputs.push_back(Output::Discontinuity);
2483 Ok(())
2484 }
2485
2486 fn all_playlist_segments_delivered(&self) -> bool {
2487 let Some(playlist) = &self.last_full_playlist else {
2488 return true;
2489 };
2490 (0..playlist.segments.len() as u64).all(|i| {
2491 let msn = playlist.media_sequence.saturating_add(i);
2492 self.delivered_segments.contains(&msn)
2493 || self.fulfilled.contains(&ResourceId::Segment { msn })
2494 })
2495 }
2496
2497 fn maybe_emit_end_of_stream(&mut self) -> Result<()> {
2498 if self.saw_endlist
2499 && !self.end_emitted
2500 && self.outstanding_fetches == 0
2501 && self.pending_keyed.is_empty()
2502 && self.inits.values().all(|slot| slot.bytes.is_some())
2503 && self.all_playlist_segments_delivered()
2504 {
2505 self.flush_ts_demux(ResourceId::Init)?;
2508 self.pending_outputs.push_back(Output::EndOfStream);
2509 self.end_emitted = true;
2510 }
2511 Ok(())
2512 }
2513}
2514
2515#[cfg(test)]
2516mod tests {
2517 use super::*;
2518 use transmux::sample_aes::aes128_encrypt_segment;
2519
2520 const EMPTY_FRAGMENT: &[u8] = &[
2521 0, 0, 0, 8, b'm', b'o', b'o', b'f', 0, 0, 0, 8, b'm', b'd', b'a', b't',
2522 ];
2523
2524 #[test]
2525 fn decrypt_segment_strips_pkcs7_padding_to_the_exact_plaintext() {
2526 let key = [0x5Au8; KEY_LEN];
2527 let iv = [0xC3u8; BLOCK_LEN];
2528 let id = ResourceId::Segment { msn: 9 };
2529 let crypto = SegmentCrypto {
2530 key_id: KeyId(0),
2531 iv,
2532 };
2533 for len in [1usize, 15, 16, 17, 37, 188] {
2534 let plain: Vec<u8> = (0..len).map(|i| (i % 251) as u8).collect();
2535 let cipher = aes128_encrypt_segment(&key, &iv, &plain);
2536 assert_eq!(decrypt_segment(id, crypto, &key, &cipher).unwrap(), plain);
2537 }
2538 }
2539
2540 #[test]
2541 fn media_sequence_iv_is_big_endian_left_padded() {
2542 let iv = media_sequence_iv(0x0102_0304_0506_0708);
2543 assert_eq!(iv[..8], [0u8; 8]);
2544 assert_eq!(iv[8..], [1, 2, 3, 4, 5, 6, 7, 8]);
2545 assert_eq!(media_sequence_iv(0)[..], [0u8; 16]);
2546 assert_eq!(media_sequence_iv(0x1FF)[14..], [1, 0xFF]);
2547 }
2548
2549 #[test]
2555 fn on_resource_rejects_a_never_requested_id() {
2556 let mut client = HlsClient::new("http://example.com/playlist.m3u8");
2557 let id = ResourceId::Segment { msn: 0 };
2558
2559 let err = client
2560 .on_resource(id, b"some bytes")
2561 .expect_err("an id the client never requested must be rejected");
2562 assert!(
2563 matches!(err, Error::UnrequestedResource { id: got } if got == id),
2564 "wrong error variant: {err:?}"
2565 );
2566
2567 let err = client
2570 .on_resource(ResourceId::Init, b"init bytes")
2571 .expect_err("an unrequested Init must be rejected");
2572 assert!(
2573 matches!(
2574 err,
2575 Error::UnrequestedResource {
2576 id: ResourceId::Init
2577 }
2578 ),
2579 "wrong error variant: {err:?}"
2580 );
2581 }
2582
2583 #[test]
2588 fn on_resource_accepts_a_previously_requested_id() {
2589 let mut client = HlsClient::new("http://example.com/playlist.m3u8");
2590 let id = ResourceId::Segment { msn: 0 };
2591 client.request_resource(id, "http://example.com/seg0.m4s".to_string(), None);
2592
2593 let result = client.on_resource(id, EMPTY_FRAGMENT);
2597 assert!(
2598 result.is_ok(),
2599 "a requested id must be accepted: {result:?}"
2600 );
2601 assert!(
2602 client.pending_demux.iter().any(|(bid, _)| *bid == id),
2603 "expected the resource to be buffered pending the init segment"
2604 );
2605 }
2606
2607 #[test]
2610 fn is_ts_segment_true_when_no_map_seen_and_sync_byte_present() {
2611 let client = HlsClient::new("http://example.com/playlist.m3u8");
2612 assert!(client.is_ts_segment(&[TS_SYNC_BYTE, 0x40, 0x11, 0x00]));
2613 }
2614
2615 #[test]
2620 fn is_ts_segment_false_for_an_isobmff_resource_with_no_map_seen() {
2621 let client = HlsClient::new("http://example.com/playlist.m3u8");
2622 let ftyp_box = b"\x00\x00\x00\x18ftypiso5\x00\x00\x02\x00iso5iso6mp41";
2623 assert!(!client.is_ts_segment(ftyp_box));
2624 }
2625
2626 #[test]
2633 fn is_ts_segment_false_once_a_map_has_been_requested() {
2634 let mut client = HlsClient::new("http://example.com/playlist.m3u8");
2635 client
2636 .ensure_init_requested(&MapTag {
2637 uri: "init.mp4".to_string(),
2638 byte_range: None,
2639 extra_attrs: Vec::new(),
2640 })
2641 .expect("ensure_init_requested succeeds");
2642 assert!(!client.is_ts_segment(&[TS_SYNC_BYTE, 0x40, 0x11, 0x00]));
2643 }
2644
2645 #[test]
2661 fn resolve_byte_range_rejects_an_offset_plus_length_that_overflows_u64() {
2662 let mut client = HlsClient::new("http://example.com/playlist.m3u8");
2663 let br = Some(ByteRange {
2664 length: u64::MAX,
2665 offset: Some(1),
2666 });
2667 let err = client
2668 .resolve_byte_range("http://example.com/seg0.m4s", &br)
2669 .expect_err("offset 1 + length u64::MAX must overflow and be rejected");
2670 assert!(
2671 matches!(
2672 err,
2673 Error::ByteRangeOverflow {
2674 offset: 1,
2675 length: u64::MAX,
2676 ..
2677 }
2678 ),
2679 "wrong error variant/fields: {err:?}"
2680 );
2681 }
2682
2683 #[test]
2690 fn resolve_byte_range_rejects_a_cursor_accumulation_that_overflows_u64() {
2691 let mut client = HlsClient::new("http://example.com/playlist.m3u8");
2692 let url = "http://example.com/seg0.m4s";
2693 let seed = Some(ByteRange {
2696 length: u64::MAX - 10,
2697 offset: Some(5),
2698 });
2699 let (offset, length) = client
2700 .resolve_byte_range(url, &seed)
2701 .expect("seed range does not itself overflow")
2702 .expect("Some for a Some(ByteRange)");
2703 assert_eq!((offset, length), (5, u64::MAX - 10));
2704
2705 let next = Some(ByteRange {
2706 length: 100,
2707 offset: None, });
2709 let err = client
2710 .resolve_byte_range(url, &next)
2711 .expect_err("cursor + next length must overflow and be rejected");
2712 assert!(
2713 matches!(err, Error::ByteRangeOverflow { length: 100, .. }),
2714 "wrong error variant/fields: {err:?}"
2715 );
2716 }
2717
2718 #[test]
2739 fn merge_delta_does_not_panic_on_an_overflowing_skipped_segments() {
2740 let mut client = HlsClient::new("http://example.com/playlist.m3u8");
2741 let prev = MediaPlaylist {
2742 media_sequence: 0,
2743 segments: vec![MediaSegment::default(); 2],
2744 ..Default::default()
2745 };
2746 client.last_full_playlist = Some(prev);
2750
2751 let delta = MediaPlaylist {
2752 media_sequence: 10, skip: Some(broadcast_hls::SkipInfo {
2754 skipped_segments: u64::MAX - 5,
2755 ..Default::default()
2756 }),
2757 segments: Vec::new(),
2758 ..Default::default()
2759 };
2760
2761 let merged = client.merge_delta(delta.clone());
2762 assert_eq!(
2763 merged, delta,
2764 "an unmergeable skip count must fall back to the delta as-is, not panic"
2765 );
2766 }
2767
2768 fn byte_range_playlist(first_msn: u64, file: &str) -> String {
2769 format!(
2772 "#EXTM3U\n#EXT-X-VERSION:4\n#EXT-X-TARGETDURATION:2\n\
2773 #EXT-X-MEDIA-SEQUENCE:{first_msn}\n\
2774 #EXTINF:2.0,\n#EXT-X-BYTERANGE:100@0\n{file}\n\
2775 #EXTINF:2.0,\n#EXT-X-BYTERANGE:50\n{file}\n"
2776 )
2777 }
2778
2779 #[test]
2783 fn state_is_pruned_to_the_current_window() {
2784 let mut client = HlsClient::new("http://example.com/p.m3u8");
2785 client
2786 .on_playlist(byte_range_playlist(0, "a.ts").as_bytes())
2787 .unwrap();
2788 assert_eq!(
2789 client.byte_range_cursor.iter().collect::<Vec<_>>(),
2790 vec![(&"http://example.com/a.ts".to_string(), &150u64)]
2791 );
2792 client
2794 .on_resource(ResourceId::Segment { msn: 0 }, EMPTY_FRAGMENT)
2795 .unwrap();
2796
2797 client
2798 .on_playlist(byte_range_playlist(1000, "b.ts").as_bytes())
2799 .unwrap();
2800
2801 let requested: Vec<ResourceId> = client.requested.iter().copied().collect();
2802 assert_eq!(
2803 requested,
2804 vec![
2805 ResourceId::Segment { msn: 1 },
2807 ResourceId::Segment { msn: 1000 },
2808 ResourceId::Segment { msn: 1001 },
2809 ]
2810 );
2811 assert!(client.fulfilled.is_empty(), "{:?}", client.fulfilled);
2812 assert_eq!(
2813 client.byte_range_cursor.iter().collect::<Vec<_>>(),
2814 vec![(&"http://example.com/b.ts".to_string(), &150u64)],
2815 "the cursor of the unreferenced a.ts must be dropped"
2816 );
2817 }
2818
2819 #[test]
2822 fn discontinuity_bookkeeping_below_the_window_is_dropped() {
2823 let mut client = HlsClient::new("http://example.com/p.m3u8");
2824 let pl = |first: u64| {
2825 format!(
2826 "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:2\n\
2827 #EXT-X-MEDIA-SEQUENCE:{first}\n#EXT-X-DISCONTINUITY\n#EXTINF:2.0,\ns.ts\n"
2828 )
2829 };
2830 client.on_playlist(pl(5).as_bytes()).unwrap();
2831 client
2832 .on_resource(ResourceId::Segment { msn: 5 }, EMPTY_FRAGMENT)
2833 .unwrap();
2834 assert_eq!(
2835 client
2836 .discontinuous_msns
2837 .iter()
2838 .copied()
2839 .collect::<Vec<_>>(),
2840 vec![5]
2841 );
2842 client.on_playlist(pl(9).as_bytes()).unwrap();
2843 assert_eq!(
2844 client
2845 .discontinuous_msns
2846 .iter()
2847 .copied()
2848 .collect::<Vec<_>>(),
2849 vec![9]
2850 );
2851 assert!(client.discontinuity_emitted.is_empty());
2852 assert!(client.delivered_segments.is_empty());
2853 }
2854
2855 fn keyed_playlist(first_msn: u64, count: u64, key_uri: &str) -> String {
2856 let mut text = format!(
2857 "#EXTM3U\n#EXT-X-VERSION:5\n#EXT-X-TARGETDURATION:2\n\
2858 #EXT-X-MEDIA-SEQUENCE:{first_msn}\n\
2859 #EXT-X-KEY:METHOD=AES-128,URI=\"{key_uri}\"\n"
2860 );
2861 for msn in first_msn..first_msn + count {
2862 text.push_str(&format!("#EXTINF:2.0,\nseg{msn}.ts\n"));
2863 }
2864 text
2865 }
2866
2867 fn pending_keyed_bytes(client: &HlsClient) -> usize {
2868 client.pending_keyed.iter().map(|(_, b)| b.len()).sum()
2869 }
2870
2871 #[test]
2872 fn pending_keyed_is_pruned_with_the_window() {
2873 let mut client = HlsClient::new("http://example.com/p.m3u8");
2874 client
2875 .on_playlist(keyed_playlist(0, 1, "key.bin").as_bytes())
2876 .unwrap();
2877 client
2878 .on_resource(ResourceId::Segment { msn: 0 }, &[0u8; 32])
2879 .unwrap();
2880 assert_eq!(client.pending_keyed.len(), 1);
2881 client
2882 .on_playlist(keyed_playlist(5, 1, "key.bin").as_bytes())
2883 .unwrap();
2884 assert!(
2885 client.pending_keyed.is_empty(),
2886 "ciphertext below the window can never be played: {:?}",
2887 client
2888 .pending_keyed
2889 .iter()
2890 .map(|(id, _)| id)
2891 .collect::<Vec<_>>()
2892 );
2893 }
2894
2895 #[test]
2896 fn pending_keyed_is_capped_and_drops_the_oldest_with_a_typed_error() {
2897 let mut client = HlsClient::new("http://example.com/p.m3u8");
2898 client
2899 .on_playlist(keyed_playlist(0, 2, "key.bin").as_bytes())
2900 .unwrap();
2901 let big = vec![0u8; 40 * 1024 * 1024];
2902 client
2903 .on_resource(ResourceId::Segment { msn: 0 }, &big)
2904 .unwrap();
2905 let err = client
2906 .on_resource(ResourceId::Segment { msn: 1 }, &big)
2907 .expect_err("the second 40 MiB segment exceeds the 64 MiB cap");
2908 assert!(
2909 matches!(err, Error::PendingKeyedOverflow { dropped } if dropped == ResourceId::Segment { msn: 0 }),
2910 "{err:?}"
2911 );
2912 assert!(
2913 pending_keyed_bytes(&client) <= MAX_PENDING_KEYED_BYTES,
2914 "{} bytes buffered",
2915 pending_keyed_bytes(&client)
2916 );
2917 assert_eq!(
2918 client
2919 .pending_keyed
2920 .iter()
2921 .map(|(id, _)| *id)
2922 .collect::<Vec<_>>(),
2923 vec![ResourceId::Segment { msn: 1 }]
2924 );
2925 }
2926
2927 #[test]
2928 fn key_maps_do_not_grow_with_per_reload_tokenised_key_urls() {
2929 let mut client = HlsClient::new("http://example.com/p.m3u8");
2930 for n in 0..8u64 {
2931 client
2932 .on_playlist(keyed_playlist(n, 2, &format!("key.bin?t={n}")).as_bytes())
2933 .unwrap();
2934 }
2935 assert!(
2936 client.key_ids.len() <= 3 && client.key_urls.len() <= 3,
2937 "{:?} {:?}",
2938 client.key_ids,
2939 client.key_urls
2940 );
2941 }
2942
2943 #[test]
2944 fn next_wait_reports_the_queued_wait_hint_without_consuming_it() {
2945 use core::time::Duration;
2946 let mut c = HlsClient::new("http://h/p.m3u8");
2947 assert_eq!(c.next_wait(), None, "the first queued action is a fetch");
2948 let _ = c.poll();
2949 c.on_playlist(
2950 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",
2951 )
2952 .unwrap();
2953 while c.next_wait().is_none() {
2954 c.poll()
2955 .expect("a WaitMs must be queued after a non-blocking live playlist");
2956 }
2957 assert_eq!(
2958 c.next_wait(),
2959 Some(Duration::from_millis(1000)),
2960 "half the 2 s target duration"
2961 );
2962 assert_eq!(
2963 c.next_wait(),
2964 Some(Duration::from_millis(1000)),
2965 "peeking does not consume"
2966 );
2967 let t0 = std::time::Instant::now();
2970 let first = c.poll_timeout(t0).expect("a wait is at the front");
2971 assert_eq!(first, t0 + Duration::from_millis(1000));
2972 for later in [250u64, 600, 999, 5000] {
2973 assert_eq!(
2974 c.poll_timeout(t0 + Duration::from_millis(later)),
2975 Some(first),
2976 "re-query at +{later} ms must not slide the deadline"
2977 );
2978 }
2979 assert!(matches!(c.poll(), Some(Action::WaitMs(1000))));
2981 assert_eq!(c.poll_timeout(t0 + Duration::from_secs(9)), None);
2982 }
2983
2984 #[test]
2985 fn rfc3339_accepts_the_spellings_the_old_parser_accepted() {
2986 assert_eq!(
2987 parse_rfc3339_ms("2026-10-02 10:00:00Z"),
2988 Some(1_790_935_200_000),
2989 "space separator"
2990 );
2991 assert_eq!(
2992 parse_rfc3339_ms("2026-10-02t10:00:00z"),
2993 Some(1_790_935_200_000),
2994 "lower-case t and z"
2995 );
2996 assert_eq!(
2997 parse_rfc3339_ms("2026-10-02T10:00:00.123456789Z"),
2998 Some(1_790_935_200_123),
2999 "extra digits truncated"
3000 );
3001 assert_eq!(
3002 parse_rfc3339_ms("1969-12-31T23:59:59Z"),
3003 Some(-1_000),
3004 "before the epoch"
3005 );
3006 }
3007
3008 #[test]
3009 fn rfc3339_offset_spellings_all_name_the_same_instant() {
3010 let want = Some(1_790_935_200_000);
3011 for text in [
3012 "2026-10-02T12:00:00+02:00",
3013 "2026-10-02T12:00:00+0200",
3014 "2026-10-02T05:00:00-05:00",
3015 "2026-10-02T05:00:00-0500",
3016 "2026-10-02T10:00:00+00:00",
3017 "2026-10-02T10:00:00-00:00",
3018 ] {
3019 assert_eq!(parse_rfc3339_ms(text), want, "{text}");
3020 }
3021 let hour_only = parse_rfc3339_ms("2026-10-02T12:00:00+02");
3023 assert!(hour_only == want || hour_only.is_none(), "{hour_only:?}");
3024 }
3025
3026 #[test]
3028 fn a_leap_second_is_clamped_to_the_last_second_of_the_minute() {
3029 assert_eq!(
3030 parse_rfc3339_ms("2016-12-31T23:59:60Z"),
3031 Some(1_483_228_799_000)
3032 );
3033 }
3034
3035 #[test]
3036 fn rfc3339_parses_to_literal_epoch_milliseconds() {
3037 assert_eq!(
3039 parse_rfc3339_ms("2026-10-02T10:00:00Z"),
3040 Some(1_790_935_200_000)
3041 );
3042 assert_eq!(
3043 parse_rfc3339_ms("2026-10-02T10:00:00.250Z"),
3044 Some(1_790_935_200_250)
3045 );
3046 assert_eq!(
3047 parse_rfc3339_ms("2026-10-02T10:00:00.5Z"),
3048 Some(1_790_935_200_500)
3049 );
3050 assert_eq!(parse_rfc3339_ms("1970-01-01T00:00:00Z"), Some(0));
3051 assert_eq!(
3052 parse_rfc3339_ms("2000-02-29T23:59:59Z"),
3053 Some(951_868_799_000)
3054 );
3055 assert_eq!(
3057 parse_rfc3339_ms("2026-10-02T12:00:00+02:00"),
3058 Some(1_790_935_200_000)
3059 );
3060 assert_eq!(
3061 parse_rfc3339_ms("2026-10-02T05:00:00-0500"),
3062 Some(1_790_935_200_000)
3063 );
3064 for bad in [
3066 "",
3067 "2026-10-02",
3068 "2026-13-02T10:00:00Z",
3069 "2026-10-02T25:00:00Z",
3070 "2026-10-02T10:00:00",
3071 "2026-10-02T10:00:00Zjunk",
3072 "2026-10-02T10:00:00.Z",
3073 "2026-10-02T10:00:00+2:00",
3074 "xxxx-10-02T10:00:00Z",
3075 ] {
3076 assert_eq!(parse_rfc3339_ms(bad), None, "{bad:?}");
3077 }
3078 }
3079}