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