1use alloc::collections::{BTreeMap, BTreeSet, VecDeque};
4use alloc::format;
5use alloc::string::{String, ToString};
6use alloc::vec::Vec;
7use container_probe::segment;
8use mpeg_ts::ts::TS_SYNC_BYTE;
9use transmux::TrackId;
10
11use broadcast_common::Unpackage;
12use broadcast_common::url_ref;
13use broadcast_hls::{
14 ByteRange, EncryptionMethod, MapTag, MasterPlaylist, MediaKey, MediaPlaylist, MediaSegment,
15 OpenSegment, PreloadHintType,
16};
17use transmux::sample_aes::{BLOCK_LEN, KEY_LEN, aes128_decrypt_segment};
18use transmux::{DemuxEvent, Fmp4Demux, InputDegradation, Sample, StreamingTsDemux, TrackSpec};
19
20use super::action::{Action, BlockingReload, KeyId, ResourceId};
21use super::error::{Error, Result};
22use super::output::{MasterSelection, Output, RenditionInfo, VariantInfo, VariantPolicy};
23use super::url;
24use broadcast_common::units::MILLIS_PER_SEC;
25
26const TS_MOVIE_TIMESCALE: u32 = 90_000;
31
32const LIVE_EDGE_TARGET_DURATIONS: f64 = 3.0;
37
38const MAX_PENDING_KEYED_BYTES: usize = 64 * 1024 * 1024;
40
41const MAX_RELOAD_WAIT_MS: u64 = 60_000;
44
45const MOVIE_BOX: [u8; 4] = *b"moov";
50
51const MAP_PROBE_VERSION: u32 = 6;
52
53struct TsDemuxState(StreamingTsDemux);
56
57impl core::fmt::Debug for TsDemuxState {
58 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
59 f.write_str("TsDemuxState(StreamingTsDemux)")
60 }
61}
62
63fn index_u64(i: usize) -> u64 {
66 u64::try_from(i).unwrap_or(u64::MAX)
67}
68
69fn reload_wait_ms(target_duration: u32) -> u64 {
73 (u64::from(target_duration.max(1)).saturating_mul(MILLIS_PER_SEC) / 2).min(MAX_RELOAD_WAIT_MS)
74}
75
76fn keys_at_maps(text: &str) -> Vec<(MapTag, Option<MediaKey>)> {
81 const MAP_PREFIX: &str = "#EXT-X-MAP:";
82 const KEY_PREFIX: &str = "#EXT-X-KEY:";
83 let mut current: Option<MediaKey> = None;
84 let mut out = Vec::new();
85 for line in text.lines() {
86 if line.starts_with(KEY_PREFIX) {
87 let probe = format!(
88 "#EXTM3U\n#EXT-X-TARGETDURATION:1\n{}\n#EXTINF:1,\nx\n",
89 line.trim_end()
90 );
91 if let Ok(parsed) = MediaPlaylist::parse(&probe)
92 && let Ok(mut keys) = parsed.segment_keys()
93 && let Some(key) = keys.pop()
94 {
95 current = key;
96 }
97 } else if line.starts_with(MAP_PREFIX) {
98 let probe = format!(
99 "#EXTM3U\n#EXT-X-VERSION:{MAP_PROBE_VERSION}\n#EXT-X-TARGETDURATION:1\n{}\n#EXTINF:1,\nx\n",
100 line.trim_end()
101 );
102 if let Ok(parsed) = MediaPlaylist::parse(&probe)
103 && let Some(map) = parsed.segments.first().and_then(|seg| seg.map.clone())
104 {
105 out.push((map, current.clone()));
106 }
107 }
108 }
109 out
110}
111
112fn has_movie_box(bytes: &[u8]) -> bool {
115 for step in transmux::box_iter(bytes) {
116 let Ok((boxed, _)) = step else { return false };
117 if boxed.header.box_type.0 == MOVIE_BOX {
118 return true;
119 }
120 }
121 false
122}
123
124fn without_query(uri: &str) -> &str {
127 uri.split_once('?').map_or(uri, |(path, _)| path)
128}
129
130fn carried_key_tags(dropped: &[MediaSegment]) -> Vec<String> {
134 const KEY_PREFIX: &str = "#EXT-X-KEY:";
135 const KEYFORMAT_ATTR: &str = "KEYFORMAT=\"";
136 const IDENTITY: &str = "identity";
137 let mut latest: BTreeMap<&str, &String> = BTreeMap::new();
138 for tag in dropped.iter().flat_map(|seg| &seg.pre_tags) {
139 if !tag.starts_with(KEY_PREFIX) {
140 continue;
141 }
142 let format = tag
143 .split_once(KEYFORMAT_ATTR)
144 .and_then(|(_, rest)| rest.split_once('"'))
145 .map_or(IDENTITY, |(format, _)| format);
146 latest.insert(format, tag);
147 }
148 latest.into_values().cloned().collect()
149}
150
151fn decrypt_segment(
152 id: ResourceId,
153 crypto: SegmentCrypto,
154 key: &[u8; KEY_LEN],
155 ciphertext: &[u8],
156) -> Result<Vec<u8>> {
157 aes128_decrypt_segment(key, &crypto.iv, ciphertext).map_err(|_| Error::DecryptFailed {
158 id,
159 key_id: crypto.key_id,
160 })
161}
162
163fn resource_msn(id: &ResourceId) -> Option<u64> {
166 match id {
167 ResourceId::Part { msn, .. } | ResourceId::Segment { msn } => Some(*msn),
168 _ => None,
169 }
170}
171
172type InitId = u32;
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178enum InitFetch {
179 Wanted,
181 InFlight,
183 Failed,
185 Arrived,
187 Ready,
189}
190
191#[derive(Debug)]
195struct InitSlot {
196 map: MapTag,
197 url: String,
198 byte_range: Option<(u64, u64)>,
199 encryption: Option<MediaKey>,
200 crypto: Option<SegmentCrypto>,
201 fetch: InitFetch,
202 cipher: Option<Vec<u8>>,
203 bytes: Option<Vec<u8>>,
204 skipped_reported: BTreeSet<(Option<u32>, String)>,
205}
206
207#[derive(Debug)]
210struct MapEntry {
211 map: MapTag,
212 encryption: Option<MediaKey>,
213 first_msn: u64,
214}
215
216impl MapEntry {
217 fn unusable(&self) -> Option<Error> {
218 let key = self.encryption.as_ref()?;
219 let what = if key.method != EncryptionMethod::Aes128 {
220 "encrypted EXT-X-MAP: only METHOD=AES-128 is decrypted"
221 } else if key.uri.is_none() {
222 "encrypted EXT-X-MAP: key without URI"
223 } else {
224 return None;
225 };
226 Some(Error::UnsupportedEncryption {
227 method: key.method,
228 what,
229 })
230 }
231}
232
233#[derive(Clone, Copy)]
235struct KeyBytes([u8; KEY_LEN]);
236
237impl core::fmt::Debug for KeyBytes {
238 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
239 f.write_str("KeyBytes(<redacted>)")
240 }
241}
242
243#[derive(Debug, Clone, Copy)]
245struct SegmentCrypto {
246 key_id: KeyId,
247 iv: [u8; BLOCK_LEN],
248}
249
250fn media_sequence_iv(msn: u64) -> [u8; BLOCK_LEN] {
253 let mut iv = [0u8; BLOCK_LEN];
254 let msn_bytes = msn.to_be_bytes();
255 let start = BLOCK_LEN - msn_bytes.len();
256 iv[start..].copy_from_slice(&msn_bytes);
257 iv
258}
259
260#[derive(Debug, Clone, Copy, PartialEq, Eq)]
263enum Continuity {
264 Continues,
266 Lagging,
269 OlderWindow,
273 Restart,
275}
276
277fn program_date_time(seg: &MediaSegment) -> Option<broadcast_hls::ProgramDateTime> {
279 seg.program_date_time
280}
281
282fn program_date_time_ms(seg: &MediaSegment) -> Option<i64> {
284 program_date_time(seg).map(|pdt| pdt.timestamp().as_millisecond())
285}
286
287#[derive(Debug)]
360pub struct HlsClient {
361 playlist_url: String,
362 base_override: Option<String>,
363 variant_policy: VariantPolicy,
364 master: Option<MasterSelection>,
365
366 pending_actions: VecDeque<Action>,
367 #[cfg(feature = "std")]
372 wait_anchor: Option<std::time::Instant>,
373 pending_outputs: VecDeque<Output>,
374 pending_errors: VecDeque<Error>,
377
378 inits: BTreeMap<InitId, InitSlot>,
380 next_init_id: InitId,
381 init_target: Option<InitId>,
384 announced_init: Option<InitId>,
386 msn_init: BTreeMap<u64, InitId>,
389 playlist_maps: Vec<MapEntry>,
391 last_fmp4_specs: Option<Vec<TrackSpec>>,
394 pending_demux: VecDeque<(ResourceId, Vec<u8>)>,
399
400 key_ids: BTreeMap<String, KeyId>,
402 key_urls: BTreeMap<KeyId, String>,
404 key_material: BTreeMap<KeyId, KeyBytes>,
406 keys_in_flight: BTreeSet<KeyId>,
408 segment_crypto: BTreeMap<u64, SegmentCrypto>,
410 pending_keyed: Vec<(ResourceId, Vec<u8>)>,
412 next_key_id: u32,
415 full_reload_requested: bool,
418
419 requested: BTreeSet<ResourceId>,
420 resource_requests: BTreeMap<ResourceId, (String, Option<(u64, u64)>)>,
423 fulfilled: BTreeSet<ResourceId>,
427 joined: bool,
430 ts_demux: Option<TsDemuxState>,
434 ts_specs: BTreeMap<TrackId, TrackSpec>,
436 last_ts_init: Option<Vec<u8>>,
439 delivered_parts: BTreeSet<(u64, u64)>,
440 delivered_segments: BTreeSet<u64>,
441 discontinuous_msns: BTreeSet<u64>,
442 discontinuity_emitted: BTreeSet<u64>,
443 byte_range_cursor: BTreeMap<String, u64>,
444
445 outstanding_fetches: u64,
446 saw_endlist: bool,
447 end_emitted: bool,
448 last_full_playlist: Option<MediaPlaylist>,
449 last_media_sequence: Option<u64>,
453}
454
455#[must_use]
458pub fn choose_variant(variants: &[VariantInfo], policy: &VariantPolicy) -> usize {
459 if variants.is_empty() {
460 return 0;
461 }
462 let mut candidates: Vec<usize> = Vec::new();
463 for (i, v) in variants.iter().enumerate() {
464 let mut ok = true;
465 if let Some(max_bw) = policy.max_bandwidth
466 && v.bandwidth > max_bw
467 {
468 ok = false;
469 }
470 if ok
471 && let Some(max_h) = policy.max_height
472 && let Some(res) = v.resolution
473 && res.1 > max_h
474 {
475 ok = false;
476 }
477 if ok
478 && let Some(supported) = &policy.supported_codecs
479 && let Some(codecs) = &v.codecs
480 {
481 for entry in codecs.split(',') {
482 let entry = entry.trim();
483 if entry.is_empty() {
484 continue;
485 }
486 if !supported.iter().any(|s| entry.starts_with(s)) {
487 ok = false;
488 break;
489 }
490 }
491 }
492 if ok {
493 candidates.push(i);
494 }
495 }
496 if !candidates.is_empty() {
497 let mut best = candidates[0];
498 for &i in &candidates[1..] {
499 if variants[i].bandwidth > variants[best].bandwidth {
500 best = i;
501 }
502 }
503 best
504 } else {
505 let mut best = 0usize;
506 for i in 1..variants.len() {
507 if variants[i].bandwidth < variants[best].bandwidth {
508 best = i;
509 }
510 }
511 best
512 }
513}
514
515#[derive(Debug, Clone)]
518#[non_exhaustive]
519pub struct HlsClientConfig {
520 pub playlist_url: String,
522 pub variant_policy: VariantPolicy,
524 pub playlist_base: Option<String>,
527}
528
529impl HlsClientConfig {
530 pub fn new(playlist_url: impl Into<String>) -> Self {
532 HlsClientConfig {
533 playlist_url: playlist_url.into(),
534 variant_policy: VariantPolicy::default(),
535 playlist_base: None,
536 }
537 }
538
539 #[must_use]
541 pub fn with_variant_policy(mut self, variant_policy: VariantPolicy) -> Self {
542 self.variant_policy = variant_policy;
543 self
544 }
545
546 #[must_use]
548 pub fn with_playlist_base(mut self, playlist_base: impl Into<String>) -> Self {
549 self.playlist_base = Some(playlist_base.into());
550 self
551 }
552}
553
554impl HlsClient {
555 pub fn new(config: HlsClientConfig) -> Self {
559 let playlist_url = config.playlist_url;
560 let mut pending_actions = VecDeque::new();
561 pending_actions.push_back(Action::FetchPlaylist {
562 url: playlist_url.clone(),
563 blocking: None,
564 skip: false,
565 });
566 Self {
567 playlist_url,
568 base_override: config.playlist_base,
569 variant_policy: config.variant_policy,
570 master: None,
571 pending_actions,
572 pending_errors: VecDeque::new(),
573 #[cfg(feature = "std")]
574 wait_anchor: None,
575 pending_outputs: VecDeque::new(),
576 inits: BTreeMap::new(),
577 next_init_id: 0,
578 init_target: None,
579 announced_init: None,
580 msn_init: BTreeMap::new(),
581 playlist_maps: Vec::new(),
582 last_fmp4_specs: None,
583 pending_demux: VecDeque::new(),
584 key_ids: BTreeMap::new(),
585 key_urls: BTreeMap::new(),
586 key_material: BTreeMap::new(),
587 keys_in_flight: BTreeSet::new(),
588 segment_crypto: BTreeMap::new(),
589 pending_keyed: Vec::new(),
590 next_key_id: 0,
591 full_reload_requested: false,
592 requested: BTreeSet::new(),
593 resource_requests: BTreeMap::new(),
594 fulfilled: BTreeSet::new(),
595 joined: false,
596 ts_demux: None,
597 ts_specs: BTreeMap::new(),
598 last_ts_init: None,
599 delivered_parts: BTreeSet::new(),
600 delivered_segments: BTreeSet::new(),
601 discontinuous_msns: BTreeSet::new(),
602 discontinuity_emitted: BTreeSet::new(),
603 byte_range_cursor: BTreeMap::new(),
604 outstanding_fetches: 0,
605 saw_endlist: false,
606 end_emitted: false,
607 last_full_playlist: None,
608 last_media_sequence: None,
609 }
610 }
611
612 pub fn playlist_url(&self) -> &str {
614 &self.playlist_url
615 }
616
617 pub fn set_playlist_base(&mut self, base: impl Into<String>) {
622 self.base_override = Some(base.into());
623 }
624
625 fn base(&self) -> &str {
626 self.base_override.as_deref().unwrap_or(&self.playlist_url)
627 }
628
629 pub fn endlist_seen(&self) -> bool {
632 self.saw_endlist
633 }
634
635 pub fn target_duration_secs(&self) -> Option<u32> {
638 self.last_full_playlist.as_ref().map(|p| p.target_duration)
639 }
640
641 pub fn select_variant(&mut self, index: usize) -> Result<()> {
643 let variants_len = self
644 .master
645 .as_ref()
646 .ok_or(Error::NoSuchVariant { index })?
647 .variants
648 .len();
649 if index >= variants_len {
650 return Err(Error::NoSuchVariant { index });
651 }
652 let url = self
653 .master
654 .as_ref()
655 .and_then(|m| m.variants.get(index))
656 .map(|v| v.url.clone())
657 .ok_or(Error::NoSuchVariant { index })?;
658 self.restart_after_regression()?;
659 self.pending_actions.clear();
660 self.playlist_url = url.clone();
661 self.base_override = None;
662 self.pending_actions.push_back(Action::FetchPlaylist {
663 url,
664 blocking: None,
665 skip: false,
666 });
667 if let Some(m) = &mut self.master {
668 m.chosen = index;
669 }
670 Ok(())
671 }
672
673 pub fn poll(&mut self) -> Option<Action> {
675 #[cfg(feature = "std")]
676 {
677 self.wait_anchor = None;
678 }
679 self.pending_actions.pop_front()
680 }
681
682 pub fn next_wait(&self) -> Option<core::time::Duration> {
688 match self.pending_actions.front() {
689 Some(Action::WaitMs(ms)) => Some(core::time::Duration::from_millis(*ms)),
690 _ => None,
691 }
692 }
693
694 #[cfg(feature = "std")]
705 pub fn poll_timeout(&mut self, now: std::time::Instant) -> Option<std::time::Instant> {
706 let wait = self.next_wait()?;
707 let anchor = *self.wait_anchor.get_or_insert(now);
708 Some(anchor + wait)
709 }
710
711 pub fn next_output(&mut self) -> Option<Output> {
713 self.pending_outputs.pop_front()
714 }
715
716 pub fn on_playlist(&mut self, bytes: &[u8]) -> Result<()> {
722 let text = core::str::from_utf8(bytes)?;
723 if text.contains("#EXT-X-STREAM-INF") {
724 let master = MasterPlaylist::parse(text)?;
725 let base = self.base().to_string();
726 let mut variants: Vec<VariantInfo> = Vec::new();
727 for v in &master.variants {
728 let Some(resolved) = self.fetch_url(&base, &v.uri) else {
729 continue;
730 };
731 variants.push(VariantInfo {
732 url: resolved,
733 bandwidth: v.bandwidth,
734 average_bandwidth: v.average_bandwidth,
735 resolution: v.resolution,
736 codecs: v.codecs.clone(),
737 audio_group: v.audio.clone(),
738 subtitles_group: v.subtitles.clone(),
739 });
740 }
741 let mut renditions: Vec<RenditionInfo> = Vec::new();
742 for m in &master.media {
743 renditions.push(RenditionInfo {
744 media_type: m.media_type,
745 group_id: m.group_id.clone(),
746 name: m.name.clone(),
747 language: m.language.clone(),
748 default: m.default,
749 autoselect: m.autoselect,
750 url: match m.uri.as_deref() {
751 Some(u) => self.fetch_url(&base, u),
752 None => None,
753 },
754 });
755 }
756 let chosen = choose_variant(&variants, &self.variant_policy);
757 let selection = MasterSelection {
758 variants,
759 chosen,
760 renditions,
761 };
762 self.pending_outputs
763 .push_back(Output::Master(selection.clone()));
764 self.master = Some(selection);
765 let Some(chosen_url) = self
766 .master
767 .as_ref()
768 .and_then(|m| m.variants.get(chosen))
769 .map(|v| v.url.clone())
770 else {
771 return Err(Error::NoSuchVariant { index: chosen });
772 };
773 self.playlist_url = chosen_url.clone();
774 self.base_override = None;
775 self.pending_actions.clear();
776 self.pending_actions.push_back(Action::FetchPlaylist {
777 url: chosen_url,
778 blocking: None,
779 skip: false,
780 });
781 return Ok(());
782 }
783 let playlist = MediaPlaylist::parse(text)?;
784 let playlist = self.merge_delta(playlist);
785 if playlist.skip.is_some() {
786 return Err(self.request_full_playlist(&playlist));
787 }
788 self.full_reload_requested = false;
789 let segment_keys = playlist.segment_key_results()?;
790 if let Some(Err(e)) = segment_keys.first()
791 && segment_keys.iter().all(|r| r.is_err())
792 {
793 return Err(Error::PlaylistParse(e.clone()));
794 }
795 let open_key = if playlist.open_segment.is_some() {
796 playlist.open_segment_key()?
797 } else {
798 None
799 };
800 let mut first_error: Option<Error> = None;
801 let mut lag_floor: Option<u64> = None;
804 if self.last_media_sequence.is_some() {
805 match self.continuity(&playlist) {
806 Continuity::Restart => self.restart_after_regression()?,
807 Continuity::OlderWindow => {
808 lag_floor = self.last_full_playlist.as_ref().map(|p| p.media_sequence);
809 }
810 Continuity::Lagging | Continuity::Continues => {}
811 }
812 }
813 self.last_media_sequence = Some(
815 self.last_media_sequence
816 .map_or(playlist.media_sequence, |p| p.max(playlist.media_sequence)),
817 );
818 self.forget_keys_below(playlist.media_sequence);
819
820 let next_msn = playlist
824 .media_sequence
825 .checked_add(index_u64(playlist.segments.len()))
826 .ok_or(Error::MediaSequenceOverflow {
827 media_sequence: playlist.media_sequence,
828 segments: playlist.segments.len(),
829 })?;
830
831 if !self.joined {
832 self.joined = true;
833 self.join_start(&playlist)?;
834 }
835
836 self.playlist_maps = self.map_entries(&playlist, text, next_msn);
837
838 for (i, seg) in playlist.segments.iter().enumerate() {
839 let msn = playlist.media_sequence.saturating_add(index_u64(i));
840 if lag_floor.is_some_and(|floor| msn < floor) {
841 self.skip_segment(msn, seg)?;
842 continue;
843 }
844 if let Some(e) = seg.map.as_ref().and_then(|m| self.unusable_map(m)) {
845 self.skip_segment(msn, seg)?;
846 first_error.get_or_insert(e);
847 continue;
848 }
849 let key = match segment_keys.get(i) {
850 Some(Ok(key)) => key.as_ref(),
851 Some(Err(e)) => {
852 self.skip_segment(msn, seg)?;
853 first_error.get_or_insert(Error::PlaylistParse(e.clone()));
854 continue;
855 }
856 None => None,
857 };
858 match self.process_closed_segment(msn, seg, key, playlist.iframes_only) {
859 Ok(()) => {}
860 Err(e @ Error::UnsupportedEncryption { .. }) => {
861 self.skip_segment(msn, seg)?;
862 first_error.get_or_insert(e);
863 }
864 Err(e) => return Err(e),
865 }
866 }
867
868 if let Some(open) = &playlist.open_segment {
869 if let Some(e) = open.map.as_ref().and_then(|m| self.unusable_map(m)) {
870 first_error.get_or_insert(e);
871 } else {
872 match &open_key {
873 Some(key) => {
874 first_error.get_or_insert(Error::UnsupportedEncryption {
875 method: key.method,
876 what: "encrypted partial segments",
877 });
878 }
879 None => self.process_open_segment(next_msn, open)?,
880 }
881 }
882 }
883
884 self.pump_init_fetch();
885
886 if let Some(ll) = &playlist.low_latency
887 && let Some(hint_uri) = &ll.preload_hint_part
888 {
889 match ll.preload_hint_type {
890 PreloadHintType::Part => {
891 let part_idx = playlist
892 .open_segment
893 .as_ref()
894 .map(|o| index_u64(o.parts.len()))
895 .unwrap_or(0);
896 let id = ResourceId::Part {
897 msn: next_msn,
898 part: part_idx,
899 };
900 if !self.requested.contains(&id)
901 && !self.delivered_parts.contains(&(next_msn, part_idx))
902 {
903 let url = url::resolve(self.base(), hint_uri);
904 let byte_range = self.resolve_hint_byte_range(&url, ll)?;
905 self.request_resource(id, url, byte_range);
906 }
907 }
908 PreloadHintType::Map => {
909 let map = MapTag {
910 uri: hint_uri.clone(),
911 byte_range: ll.preload_hint_byte_range_length.map(|length| ByteRange {
912 length,
913 offset: ll.preload_hint_byte_range_start,
914 }),
915 extra_attrs: Vec::new(),
916 };
917 self.ensure_init_requested(&map)?;
918 }
919 _ => {
920 }
925 }
926 }
927
928 if playlist.endlist {
929 self.saw_endlist = true;
930 } else {
931 let blocking = playlist
936 .low_latency
937 .as_ref()
938 .filter(|ll| ll.can_block_reload)
939 .map(|_| {
940 let part = playlist
941 .open_segment
942 .as_ref()
943 .map(|o| index_u64(o.parts.len()))
944 .unwrap_or(0);
945 BlockingReload {
946 msn: next_msn,
947 part: Some(part),
948 }
949 });
950 let can_skip = playlist
951 .low_latency
952 .as_ref()
953 .and_then(|ll| ll.can_skip_until)
954 .is_some();
955 let skip = can_skip && self.last_full_playlist.is_some();
956 self.pending_actions.push_back(Action::FetchPlaylist {
957 url: self.playlist_url.clone(),
958 blocking,
959 skip,
960 });
961 if blocking.is_none() {
962 self.pending_actions
963 .push_back(Action::WaitMs(reload_wait_ms(playlist.target_duration)));
964 }
965 }
966
967 self.prune(&playlist);
968 self.last_full_playlist = Some(playlist);
969
970 self.maybe_emit_end_of_stream()?;
971 match first_error {
972 Some(e) => Err(e),
973 None => Ok(()),
974 }
975 }
976
977 fn request_full_playlist(&mut self, delta: &MediaPlaylist) -> Error {
981 if self.full_reload_requested {
982 self.pending_actions
983 .push_back(Action::WaitMs(reload_wait_ms(delta.target_duration)));
984 }
985 self.full_reload_requested = true;
986 self.pending_actions.push_back(Action::FetchPlaylist {
987 url: self.playlist_url.clone(),
988 blocking: None,
989 skip: false,
990 });
991 Error::DeltaNotMergeable {
992 media_sequence: delta.media_sequence,
993 skipped_segments: delta.skip.as_ref().map_or(0, |s| s.skipped_segments),
994 }
995 }
996
997 pub fn ensure_playlist_reload(&mut self) -> bool {
1001 if self
1002 .pending_actions
1003 .iter()
1004 .any(|a| matches!(a, Action::FetchPlaylist { .. }))
1005 {
1006 return false;
1007 }
1008 self.pending_actions.push_back(Action::FetchPlaylist {
1009 url: self.playlist_url.clone(),
1010 blocking: None,
1011 skip: false,
1012 });
1013 true
1014 }
1015
1016 pub fn retry_init(&mut self) -> bool {
1022 if self
1023 .inits
1024 .values()
1025 .any(|slot| slot.fetch == InitFetch::InFlight)
1026 {
1027 return false;
1028 }
1029 let failed: Vec<InitId> = self
1030 .inits
1031 .iter()
1032 .filter(|(_, slot)| slot.fetch == InitFetch::Failed)
1033 .map(|(id, _)| *id)
1034 .collect();
1035 for id in &failed {
1036 if let Some(slot) = self.inits.get_mut(id) {
1037 slot.fetch = InitFetch::Wanted;
1038 }
1039 }
1040 self.pump_init_fetch();
1041 !failed.is_empty()
1042 }
1043
1044 pub fn retry_resource(&mut self, id: ResourceId) -> bool {
1050 if self.requested.contains(&id) {
1051 return false;
1052 }
1053 let Some((url, byte_range)) = self.resource_requests.get(&id).cloned() else {
1054 return false;
1055 };
1056 self.request_resource(id, url, byte_range);
1057 true
1058 }
1059
1060 pub fn skip_resource(&mut self, id: ResourceId) {
1066 match id {
1067 ResourceId::Init => return,
1068 ResourceId::Segment { msn } => {
1069 self.delivered_segments.insert(msn);
1070 }
1071 ResourceId::Part { msn, part } => {
1072 self.delivered_parts.insert((msn, part));
1073 }
1074 }
1075 self.requested.remove(&id);
1076 self.resource_requests.remove(&id);
1077 let _ = self.restart_ts_demux(id);
1079 self.pending_outputs.push_back(Output::Discontinuity);
1080 let _ = self.maybe_emit_end_of_stream();
1081 }
1082
1083 fn map_entries(&self, playlist: &MediaPlaylist, text: &str, next_msn: u64) -> Vec<MapEntry> {
1086 let keyed = keys_at_maps(text);
1087 let mut entries: Vec<MapEntry> = Vec::new();
1088 let listed = playlist
1089 .segments
1090 .iter()
1091 .enumerate()
1092 .filter_map(|(i, s)| {
1093 s.map
1094 .as_ref()
1095 .map(|m| (m, playlist.media_sequence.saturating_add(index_u64(i))))
1096 })
1097 .chain(
1098 playlist
1099 .open_segment
1100 .as_ref()
1101 .and_then(|o| o.map.as_ref())
1102 .map(|m| (m, next_msn)),
1103 );
1104 for (map, msn) in listed {
1105 if entries.iter().any(|e| e.map == *map) {
1106 continue;
1107 }
1108 let key = match keyed.iter().rev().find(|(m, _)| m == map) {
1109 Some((_, key)) => key.clone(),
1110 None => self
1111 .resolved_url(self.base(), &map.uri)
1112 .ok()
1113 .and_then(|url| self.init_slot_for(&url, &map.byte_range))
1114 .and_then(|id| self.inits.get(&id))
1115 .and_then(|slot| slot.encryption.clone()),
1116 };
1117 entries.push(MapEntry {
1118 map: map.clone(),
1119 encryption: key,
1120 first_msn: msn,
1121 });
1122 }
1123 entries
1124 }
1125
1126 fn unusable_map(&self, map: &MapTag) -> Option<Error> {
1127 self.playlist_maps
1128 .iter()
1129 .find(|e| e.map == *map)
1130 .and_then(MapEntry::unusable)
1131 }
1132
1133 fn init_slot_for(&self, url: &str, byte_range: &Option<ByteRange>) -> Option<InitId> {
1138 self.inits
1139 .iter()
1140 .find(|(_, slot)| {
1141 without_query(&slot.url) == without_query(url) && slot.map.byte_range == *byte_range
1142 })
1143 .map(|(id, _)| *id)
1144 }
1145
1146 fn init_for_map(&mut self, map: &MapTag) -> Result<InitId> {
1150 let url = self.resolved_url(self.base(), &map.uri)?;
1151 if let Some(id) = self.init_slot_for(&url, &map.byte_range)
1152 && let Some(slot) = self.inits.get_mut(&id)
1153 {
1154 if slot.url != url {
1155 slot.url = url;
1156 slot.map = map.clone();
1157 }
1158 if slot.fetch == InitFetch::Failed {
1159 slot.fetch = InitFetch::Wanted;
1160 }
1161 return Ok(id);
1162 }
1163 let (key, first_msn) = self
1164 .playlist_maps
1165 .iter()
1166 .find(|e| e.map == *map)
1167 .map_or((None, 0), |e| (e.encryption.clone(), e.first_msn));
1168 let byte_range = self.resolve_byte_range(&url, &map.byte_range)?;
1169 let crypto = match key.as_ref().and_then(|k| k.uri.as_deref().map(|u| (k, u))) {
1170 Some((key, uri)) => {
1171 let key_id = self.ensure_key_requested(uri)?;
1172 let iv = key.iv.unwrap_or_else(|| media_sequence_iv(first_msn));
1173 Some(SegmentCrypto { key_id, iv })
1174 }
1175 None => None,
1176 };
1177 let id = self.next_init_id;
1178 self.next_init_id = self.next_init_id.saturating_add(1);
1179 self.inits.insert(
1180 id,
1181 InitSlot {
1182 map: map.clone(),
1183 url,
1184 byte_range,
1185 encryption: key,
1186 crypto,
1187 fetch: InitFetch::Wanted,
1188 cipher: None,
1189 bytes: None,
1190 skipped_reported: BTreeSet::new(),
1191 },
1192 );
1193 Ok(id)
1194 }
1195
1196 fn pump_init_fetch(&mut self) {
1200 if self
1201 .inits
1202 .values()
1203 .any(|slot| slot.fetch == InitFetch::InFlight)
1204 {
1205 return;
1206 }
1207 let Some((id, slot)) = self
1208 .inits
1209 .iter_mut()
1210 .find(|(_, slot)| slot.fetch == InitFetch::Wanted)
1211 else {
1212 return;
1213 };
1214 slot.fetch = InitFetch::InFlight;
1215 let id = *id;
1216 let fetch = Action::FetchResource {
1217 id: ResourceId::Init,
1218 url: slot.url.clone(),
1219 byte_range: slot.byte_range,
1220 };
1221 self.init_target = Some(id);
1222 self.pending_actions.push_back(fetch);
1223 self.outstanding_fetches += 1;
1224 }
1225
1226 fn bind_init(&mut self, msn: u64, map: Option<&MapTag>) -> Result<()> {
1229 if let Some(map) = map {
1230 let id = self.init_for_map(map)?;
1231 self.msn_init.insert(msn, id);
1232 }
1233 Ok(())
1234 }
1235
1236 fn init_of(&self, id: &ResourceId) -> Option<InitId> {
1239 resource_msn(id)
1240 .and_then(|msn| self.msn_init.get(&msn).copied())
1241 .or_else(|| self.inits.keys().next_back().copied())
1242 }
1243
1244 fn init_ready(&self, slot: Option<InitId>) -> bool {
1245 slot.and_then(|id| self.inits.get(&id))
1246 .is_some_and(|slot| slot.bytes.is_some())
1247 }
1248
1249 pub fn on_resource(&mut self, id: ResourceId, bytes: &[u8]) -> Result<()> {
1262 let was_requested = match id {
1263 ResourceId::Init => self.init_target.is_some(),
1264 ResourceId::Part { .. } | ResourceId::Segment { .. } => self.requested.contains(&id),
1265 };
1266 if !was_requested {
1267 return Err(Error::UnrequestedResource { id });
1268 }
1269 let duplicate = match id {
1270 ResourceId::Init => !self
1271 .init_target
1272 .and_then(|t| self.inits.get(&t))
1273 .is_some_and(|slot| slot.fetch == InitFetch::InFlight),
1274 ResourceId::Part { .. } | ResourceId::Segment { .. } => self.fulfilled.contains(&id),
1275 };
1276 if duplicate {
1277 return Err(Error::DuplicateResource { id });
1278 }
1279 self.outstanding_fetches = self.outstanding_fetches.saturating_sub(1);
1280 if id != ResourceId::Init {
1281 self.fulfilled.insert(id);
1282 }
1283 match id {
1284 ResourceId::Init => self.accept_init(bytes)?,
1285 ResourceId::Part { .. } | ResourceId::Segment { .. } => {
1286 let processed = self.accept_media_resource(id, bytes);
1287 match processed {
1288 Ok(()) => {}
1289 Err(e @ Error::PendingKeyedOverflow { .. }) => return Err(e),
1290 Err(e) => {
1291 self.fulfilled.remove(&id);
1292 self.requested.remove(&id);
1293 return Err(e);
1294 }
1295 }
1296 }
1297 }
1298 self.maybe_emit_end_of_stream()?;
1299 Ok(())
1300 }
1301
1302 fn accept_init(&mut self, bytes: &[u8]) -> Result<()> {
1303 let Some(target) = self.init_target else {
1304 return Ok(());
1305 };
1306 let Some(slot) = self.inits.get_mut(&target) else {
1307 return Ok(());
1308 };
1309 if slot.crypto.is_none() && !has_movie_box(bytes) {
1310 slot.fetch = InitFetch::Failed;
1311 return Err(Error::InitUndecodable);
1312 }
1313 match slot.crypto {
1314 Some(crypto) => match self.key_material.get(&crypto.key_id).copied() {
1315 Some(KeyBytes(key)) => self.complete_encrypted_init(target, crypto, &key, bytes),
1316 None => {
1317 slot.cipher = Some(bytes.to_vec());
1318 slot.fetch = InitFetch::Arrived;
1319 Ok(())
1320 }
1321 },
1322 None => self.complete_init(target, bytes),
1323 }
1324 }
1325
1326 fn complete_init(&mut self, target: InitId, plain: &[u8]) -> Result<()> {
1330 if let Some(slot) = self.inits.get_mut(&target) {
1331 slot.bytes = Some(plain.to_vec());
1332 slot.cipher = None;
1333 slot.fetch = InitFetch::Ready;
1334 }
1335 if self.announced_init.is_none() {
1336 self.announce_init(target);
1337 }
1338 self.pump_init_fetch();
1339 let buffered: Vec<_> = self.pending_demux.drain(..).collect();
1340 let mut waiting = VecDeque::new();
1341 let mut failure = None;
1342 for (bid, bbytes) in buffered {
1343 if failure.is_some() || !self.init_ready(self.init_of(&bid)) {
1344 waiting.push_back((bid, bbytes));
1345 continue;
1346 }
1347 if let Err(e) = self.finish_media_resource(bid, &bbytes) {
1348 self.fulfilled.remove(&bid);
1349 self.requested.remove(&bid);
1350 failure = Some(e);
1351 }
1352 }
1353 self.pending_demux = waiting;
1354 match failure {
1355 Some(e) => Err(e),
1356 None => Ok(()),
1357 }
1358 }
1359
1360 fn announce_init(&mut self, slot: InitId) {
1363 if self.announced_init == Some(slot) {
1364 return;
1365 }
1366 let Some(bytes) = self.inits.get(&slot).and_then(|s| s.bytes.clone()) else {
1367 return;
1368 };
1369 self.pending_outputs.push_back(Output::Init(bytes));
1370 self.announced_init = Some(slot);
1371 self.last_fmp4_specs = None;
1372 }
1373
1374 fn complete_encrypted_init(
1375 &mut self,
1376 target: InitId,
1377 crypto: SegmentCrypto,
1378 key: &[u8; KEY_LEN],
1379 cipher: &[u8],
1380 ) -> Result<()> {
1381 match decrypt_segment(ResourceId::Init, crypto, key, cipher) {
1382 Ok(plain) if !has_movie_box(&plain) => {
1383 if let Some(slot) = self.inits.get_mut(&target) {
1384 slot.fetch = InitFetch::Failed;
1385 }
1386 Err(Error::InitUndecodable)
1387 }
1388 Ok(plain) => self.complete_init(target, &plain),
1389 Err(e) => {
1390 if let Some(slot) = self.inits.get_mut(&target) {
1391 slot.fetch = InitFetch::Failed;
1392 }
1393 Err(e)
1394 }
1395 }
1396 }
1397
1398 fn accept_media_resource(&mut self, id: ResourceId, bytes: &[u8]) -> Result<()> {
1401 if let ResourceId::Segment { msn } = id
1402 && let Some(crypto) = self.segment_crypto.get(&msn).copied()
1403 {
1404 return match self.key_material.get(&crypto.key_id).copied() {
1405 Some(KeyBytes(key)) => {
1406 let plain = decrypt_segment(id, crypto, &key, bytes)?;
1407 self.route_media_resource(id, &plain)
1408 }
1409 None => self.buffer_until_keyed(id, bytes),
1410 };
1411 }
1412 self.route_media_resource(id, bytes)
1413 }
1414
1415 fn buffer_until_keyed(&mut self, id: ResourceId, bytes: &[u8]) -> Result<()> {
1420 if bytes.len() > MAX_PENDING_KEYED_BYTES {
1421 self.fulfilled.remove(&id);
1422 self.requested.remove(&id);
1423 return Err(Error::PendingKeyedOverflow { dropped: id });
1424 }
1425 let mut held: usize = self.pending_keyed.iter().map(|(_, b)| b.len()).sum();
1426 let mut dropped: Option<ResourceId> = None;
1427 while held.saturating_add(bytes.len()) > MAX_PENDING_KEYED_BYTES
1428 && !self.pending_keyed.is_empty()
1429 {
1430 let (old_id, old) = self.pending_keyed.remove(0);
1431 held = held.saturating_sub(old.len());
1432 self.fulfilled.remove(&old_id);
1433 self.requested.remove(&old_id);
1434 dropped.get_or_insert(old_id);
1435 }
1436 self.pending_keyed.push((id, bytes.to_vec()));
1437 match dropped {
1438 Some(dropped) => Err(Error::PendingKeyedOverflow { dropped }),
1439 None => Ok(()),
1440 }
1441 }
1442
1443 fn route_media_resource(&mut self, id: ResourceId, bytes: &[u8]) -> Result<()> {
1444 let recognised = if self.is_ts_segment(bytes) {
1445 segment::ts_structure(bytes)
1446 } else {
1447 segment::media_fragment(bytes) || segment::progressive_movie(bytes)
1448 };
1449 if !recognised {
1450 return Err(Error::NotMedia { id });
1451 }
1452 if self.is_ts_segment(bytes) {
1453 self.finish_ts_resource(id, bytes)
1458 } else if !self.init_ready(self.init_of(&id)) {
1459 self.pending_demux.push_back((id, bytes.to_vec()));
1460 Ok(())
1461 } else {
1462 self.finish_media_resource(id, bytes)
1463 }
1464 }
1465
1466 pub fn on_key(&mut self, id: KeyId, bytes: &[u8]) -> Result<()> {
1475 if !self.key_urls.contains_key(&id) || self.key_material.contains_key(&id) {
1476 return Err(Error::UnrequestedKey { id });
1477 }
1478 if self.keys_in_flight.remove(&id) {
1479 self.outstanding_fetches = self.outstanding_fetches.saturating_sub(1);
1480 }
1481 let key: [u8; KEY_LEN] = bytes.try_into().map_err(|_| Error::BadKeyLength {
1482 id,
1483 len: bytes.len(),
1484 })?;
1485 self.key_material.insert(id, KeyBytes(key));
1486 let waiting_inits: Vec<(InitId, SegmentCrypto)> = self
1487 .inits
1488 .iter()
1489 .filter(|(_, slot)| slot.fetch == InitFetch::Arrived)
1490 .filter_map(|(init, slot)| slot.crypto.filter(|c| c.key_id == id).map(|c| (*init, c)))
1491 .collect();
1492 for (init, crypto) in waiting_inits {
1493 let cipher = self
1494 .inits
1495 .get_mut(&init)
1496 .and_then(|slot| slot.cipher.take());
1497 if let Some(cipher) = cipher {
1498 self.complete_encrypted_init(init, crypto, &key, &cipher)?;
1499 }
1500 }
1501 let (mut ready, rest): (Vec<_>, Vec<_>) = core::mem::take(&mut self.pending_keyed)
1502 .into_iter()
1503 .partition(|(rid, _)| {
1504 resource_msn(rid)
1505 .and_then(|msn| self.segment_crypto.get(&msn))
1506 .is_some_and(|c| c.key_id == id)
1507 });
1508 self.pending_keyed = rest;
1509 ready.sort_by_key(|(rid, _)| resource_msn(rid));
1510 let mut ready = ready.into_iter();
1511 while let Some((rid, cipher)) = ready.next() {
1512 let crypto = resource_msn(&rid).and_then(|msn| self.segment_crypto.get(&msn).copied());
1513 let result = match crypto {
1514 Some(crypto) => decrypt_segment(rid, crypto, &key, &cipher)
1515 .and_then(|plain| self.route_media_resource(rid, &plain)),
1516 None => Ok(()),
1517 };
1518 if let Err(e) = result {
1519 self.fulfilled.remove(&rid);
1520 self.requested.remove(&rid);
1521 self.pending_keyed.extend(ready);
1522 return Err(e);
1523 }
1524 }
1525 self.maybe_emit_end_of_stream()?;
1526 Ok(())
1527 }
1528
1529 pub fn on_key_error(&mut self, id: KeyId) {
1532 if self.keys_in_flight.remove(&id) {
1533 self.outstanding_fetches = self.outstanding_fetches.saturating_sub(1);
1534 }
1535 }
1536
1537 fn ensure_key_requested(&mut self, uri: &str) -> Result<KeyId> {
1540 let url = self.resolved_url(self.base(), uri)?;
1541 let id = match self.key_ids.get(&url) {
1542 Some(id) => *id,
1543 None => {
1544 let id = KeyId(self.next_key_id);
1545 self.next_key_id = self.next_key_id.saturating_add(1);
1546 self.key_ids.insert(url.clone(), id);
1547 self.key_urls.insert(id, url);
1548 id
1549 }
1550 };
1551 self.request_key(id);
1552 Ok(id)
1553 }
1554
1555 fn request_key(&mut self, id: KeyId) {
1557 let Some(url) = self.key_urls.get(&id).cloned() else {
1558 return;
1559 };
1560 if !self.key_material.contains_key(&id) && self.keys_in_flight.insert(id) {
1561 self.outstanding_fetches += 1;
1562 self.pending_actions.push_back(Action::FetchKey { id, url });
1563 }
1564 }
1565
1566 fn forget_keys_below(&mut self, first_msn: u64) {
1571 self.segment_crypto.retain(|&m, _| m >= first_msn);
1572 let mut live: BTreeSet<KeyId> = self.segment_crypto.values().map(|c| c.key_id).collect();
1573 live.extend(
1574 self.inits
1575 .values()
1576 .filter_map(|slot| slot.crypto.map(|c| c.key_id)),
1577 );
1578 let abandoned: Vec<KeyId> = self
1579 .keys_in_flight
1580 .iter()
1581 .copied()
1582 .filter(|id| !live.contains(id))
1583 .collect();
1584 for id in abandoned {
1585 self.keys_in_flight.remove(&id);
1586 self.outstanding_fetches = self.outstanding_fetches.saturating_sub(1);
1587 self.pending_actions
1588 .retain(|a| !matches!(a, Action::FetchKey { id: queued, .. } if *queued == id));
1589 }
1590 self.key_ids.retain(|_, id| live.contains(id));
1591 self.key_urls.retain(|id, _| live.contains(id));
1592 self.key_material.retain(|id, _| live.contains(id));
1593 }
1594
1595 fn prepare_encrypted_segment(
1598 &mut self,
1599 msn: u64,
1600 seg: &MediaSegment,
1601 key: &MediaKey,
1602 iframes_only: bool,
1603 ) -> Result<()> {
1604 let unsupported = |what| Error::UnsupportedEncryption {
1605 method: key.method,
1606 what,
1607 };
1608 if key.method != EncryptionMethod::Aes128 {
1609 return Err(unsupported("only METHOD=AES-128 is decrypted"));
1610 }
1611 if iframes_only {
1612 return Err(unsupported("EXT-X-I-FRAMES-ONLY with encryption"));
1613 }
1614 if !seg.parts.is_empty() {
1615 return Err(unsupported("encrypted partial segments"));
1616 }
1617 let Some(uri) = key.uri.as_deref() else {
1618 return Err(unsupported("key without URI"));
1619 };
1620 if let Some(existing) = self.segment_crypto.get(&msn).copied() {
1621 self.request_key(existing.key_id);
1622 return Ok(());
1623 }
1624 let id = self.ensure_key_requested(uri)?;
1625 let iv = key.iv.unwrap_or_else(|| media_sequence_iv(msn));
1626 self.segment_crypto
1627 .insert(msn, SegmentCrypto { key_id: id, iv });
1628 Ok(())
1629 }
1630
1631 fn finish_media_resource(&mut self, id: ResourceId, bytes: &[u8]) -> Result<()> {
1634 match id {
1635 ResourceId::Part { msn, part } => {
1636 self.emit_discontinuity_if_needed(msn);
1637 self.demux_and_emit(id, bytes)?;
1638 self.delivered_parts.insert((msn, part));
1639 }
1640 ResourceId::Segment { msn } => {
1641 self.emit_discontinuity_if_needed(msn);
1642 self.demux_and_emit(id, bytes)?;
1643 self.delivered_segments.insert(msn);
1644 }
1645 ResourceId::Init => {}
1646 }
1647 Ok(())
1648 }
1649
1650 fn finish_ts_resource(&mut self, id: ResourceId, bytes: &[u8]) -> Result<()> {
1656 match id {
1657 ResourceId::Part { msn, part } => {
1658 self.begin_ts_resource(id, msn)?;
1659 self.demux_and_emit_ts(id, bytes)?;
1660 self.delivered_parts.insert((msn, part));
1661 }
1662 ResourceId::Segment { msn } => {
1663 self.begin_ts_resource(id, msn)?;
1664 self.demux_and_emit_ts(id, bytes)?;
1665 self.release_ts_segment_end(id)?;
1666 self.delivered_segments.insert(msn);
1667 }
1668 ResourceId::Init => {}
1669 }
1670 Ok(())
1671 }
1672
1673 fn is_ts_segment(&self, bytes: &[u8]) -> bool {
1685 self.inits.is_empty()
1686 && (bytes.first() == Some(&TS_SYNC_BYTE) || segment::ts_structure(bytes))
1687 }
1688
1689 pub fn on_error(&mut self, id: Option<ResourceId>) {
1694 if let Some(id) = id {
1695 self.outstanding_fetches = self.outstanding_fetches.saturating_sub(1);
1696 match id {
1697 ResourceId::Init => {
1698 if let Some(slot) = self
1699 .init_target
1700 .and_then(|target| self.inits.get_mut(&target))
1701 .filter(|slot| slot.fetch == InitFetch::InFlight)
1702 {
1703 slot.fetch = InitFetch::Failed;
1704 }
1705 self.pump_init_fetch();
1706 }
1707 other => {
1708 self.requested.remove(&other);
1709 }
1710 }
1711 }
1712 let _ = self.maybe_emit_end_of_stream();
1716 }
1717
1718 fn merge_delta(&self, playlist: MediaPlaylist) -> MediaPlaylist {
1728 let Some(skip) = &playlist.skip else {
1729 return playlist;
1730 };
1731 if skip.skipped_segments == 0 {
1732 let mut merged = playlist;
1733 merged.skip = None;
1734 return merged;
1735 }
1736 let Some(prev) = &self.last_full_playlist else {
1737 return playlist;
1738 };
1739 if playlist.media_sequence < prev.media_sequence {
1740 return playlist;
1741 }
1742 let Ok(prefix_start) = usize::try_from(playlist.media_sequence - prev.media_sequence)
1743 else {
1744 return playlist;
1745 };
1746 let prefix_end = usize::try_from(skip.skipped_segments)
1756 .ok()
1757 .and_then(|skipped| prefix_start.checked_add(skipped));
1758 let Some(prefix) = prefix_end.and_then(|end| prev.segments.get(prefix_start..end)) else {
1759 return playlist;
1760 };
1761 let mut merged = playlist;
1762 let mut segments = prefix.to_vec();
1763 if let (Some(first), Some(dropped)) =
1764 (segments.first_mut(), prev.segments.get(..prefix_start))
1765 {
1766 let mut carried = carried_key_tags(dropped);
1767 carried.append(&mut first.pre_tags);
1768 first.pre_tags = carried;
1769 }
1770 segments.extend(merged.segments);
1771 merged.segments = segments;
1772 merged.skip = None;
1773 merged
1774 }
1775
1776 fn process_closed_segment(
1777 &mut self,
1778 msn: u64,
1779 seg: &MediaSegment,
1780 key: Option<&MediaKey>,
1781 iframes_only: bool,
1782 ) -> Result<()> {
1783 if seg.discontinuous {
1784 self.discontinuous_msns.insert(msn);
1785 }
1786 if self.delivered_segments.contains(&msn) {
1787 return Ok(());
1788 }
1789 match key {
1790 Some(key) => self.prepare_encrypted_segment(msn, seg, key, iframes_only)?,
1791 None => {
1792 self.segment_crypto.remove(&msn);
1793 }
1794 }
1795 if seg.parts.is_empty() {
1796 if self.has_delivered_part(msn) {
1813 self.begin_ts_resource(ResourceId::Segment { msn }, msn)?;
1814 self.retry_undelivered_parts(msn);
1815 if self.undelivered_parts(msn).is_empty() {
1816 self.delivered_segments.insert(msn);
1817 }
1818 return Ok(());
1819 }
1820 let id = ResourceId::Segment { msn };
1821 if !self.requested.contains(&id) {
1822 let Some(url) = self.segment_url(msn, &seg.uri) else {
1823 return Ok(());
1824 };
1825 self.bind_init(msn, seg.map.as_ref())?;
1826 let byte_range = self.request_byte_range(id, &url, &seg.byte_range)?;
1827 self.request_resource(id, url, byte_range);
1828 }
1829 return Ok(());
1830 }
1831
1832 let mut fully_accounted = true;
1833 for (i, part) in seg.parts.iter().enumerate() {
1834 let i = index_u64(i);
1835 if part.gap || self.delivered_parts.contains(&(msn, i)) {
1836 continue;
1837 }
1838 fully_accounted = false;
1839 let id = ResourceId::Part { msn, part: i };
1840 if !self.requested.contains(&id) {
1841 let Some(url) = self.segment_url(msn, &part.uri) else {
1842 return Ok(());
1843 };
1844 self.bind_init(msn, seg.map.as_ref())?;
1845 let byte_range = self.request_byte_range(id, &url, &part.byte_range)?;
1846 self.request_resource(id, url, byte_range);
1847 }
1848 }
1849 if fully_accounted {
1850 self.delivered_segments.insert(msn);
1851 }
1852 Ok(())
1853 }
1854
1855 fn undelivered_parts(&self, msn: u64) -> Vec<ResourceId> {
1856 self.resource_requests
1857 .range(
1858 ResourceId::Part { msn, part: 0 }..=ResourceId::Part {
1859 msn,
1860 part: u64::MAX,
1861 },
1862 )
1863 .map(|(id, _)| *id)
1864 .filter(|id| match id {
1865 ResourceId::Part { part, .. } => !self.delivered_parts.contains(&(msn, *part)),
1866 _ => false,
1867 })
1868 .collect()
1869 }
1870
1871 fn retry_undelivered_parts(&mut self, msn: u64) {
1872 for id in self.undelivered_parts(msn) {
1873 self.retry_resource(id);
1874 }
1875 }
1876
1877 fn has_delivered_part(&self, msn: u64) -> bool {
1878 self.delivered_parts
1879 .range((msn, 0)..=(msn, u64::MAX))
1880 .next()
1881 .is_some()
1882 }
1883
1884 fn covered_by_parts(&self, msn: u64) -> bool {
1885 self.has_delivered_part(msn) && self.undelivered_parts(msn).is_empty()
1886 }
1887
1888 fn process_open_segment(&mut self, msn: u64, open: &OpenSegment) -> Result<()> {
1889 for (i, part) in open.parts.iter().enumerate() {
1890 let i = index_u64(i);
1891 if part.gap || self.delivered_parts.contains(&(msn, i)) {
1892 continue;
1893 }
1894 let id = ResourceId::Part { msn, part: i };
1895 if !self.requested.contains(&id) {
1896 let Some(url) = self.segment_url(msn, &part.uri) else {
1897 return Ok(());
1898 };
1899 self.bind_init(msn, open.map.as_ref())?;
1900 let byte_range = self.request_byte_range(id, &url, &part.byte_range)?;
1901 self.request_resource(id, url, byte_range);
1902 }
1903 }
1904 Ok(())
1905 }
1906
1907 fn ensure_init_requested(&mut self, map: &MapTag) -> Result<()> {
1908 self.init_for_map(map)?;
1909 self.pump_init_fetch();
1910 Ok(())
1911 }
1912
1913 fn fetch_url(&mut self, base: &str, uri: &str) -> Option<String> {
1919 match self.resolved_url(base, uri) {
1920 Ok(url) => Some(url),
1921 Err(report) => {
1922 self.pending_errors.push_back(report);
1923 None
1924 }
1925 }
1926 }
1927
1928 fn resolved_url(&self, base: &str, uri: &str) -> core::result::Result<String, Error> {
1932 url_ref::resolve_str(base, uri).map_err(|source| Error::UnresolvableReference {
1933 uri: uri.to_string(),
1934 source,
1935 })
1936 }
1937
1938 fn segment_url(&mut self, msn: u64, uri: &str) -> Option<String> {
1943 match url_ref::resolve_str(self.base(), uri) {
1944 Ok(url) => Some(url),
1945 Err(source) => {
1946 self.report_unresolvable(msn, uri, source);
1947 None
1948 }
1949 }
1950 }
1951
1952 fn report_unresolvable(
1956 &mut self,
1957 msn: u64,
1958 uri: &str,
1959 source: broadcast_common::url_ref::Error,
1960 ) {
1961 self.segment_crypto.remove(&msn);
1962 self.pending_errors.push_back(Error::UnresolvableReference {
1963 uri: uri.to_string(),
1964 source,
1965 });
1966 }
1967
1968 pub fn next_error(&mut self) -> Option<Error> {
1973 self.pending_errors.pop_front()
1974 }
1975
1976 fn request_resource(&mut self, id: ResourceId, url: String, byte_range: Option<(u64, u64)>) {
1977 self.requested.insert(id);
1978 self.resource_requests.insert(id, (url.clone(), byte_range));
1979 self.outstanding_fetches += 1;
1980 self.pending_actions.push_back(Action::FetchResource {
1981 id,
1982 url,
1983 byte_range,
1984 });
1985 }
1986
1987 fn request_byte_range(
1991 &mut self,
1992 id: ResourceId,
1993 url: &str,
1994 br: &Option<ByteRange>,
1995 ) -> Result<Option<(u64, u64)>> {
1996 if let Some((_, stored)) = self.resource_requests.get(&id) {
1997 return Ok(*stored);
1998 }
1999 self.resolve_byte_range(url, br)
2000 }
2001
2002 fn resolve_byte_range(
2012 &mut self,
2013 url: &str,
2014 br: &Option<ByteRange>,
2015 ) -> Result<Option<(u64, u64)>> {
2016 let Some(br) = br.as_ref() else {
2017 return Ok(None);
2018 };
2019 let offset = br
2020 .offset
2021 .unwrap_or_else(|| *self.byte_range_cursor.get(url).unwrap_or(&0));
2022 let next_cursor =
2023 offset
2024 .checked_add(br.length)
2025 .ok_or_else(|| Error::ByteRangeOverflow {
2026 url: url.to_string(),
2027 offset,
2028 length: br.length,
2029 })?;
2030 self.byte_range_cursor.insert(url.to_string(), next_cursor);
2031 Ok(Some((offset, br.length)))
2032 }
2033
2034 fn resolve_hint_byte_range(
2037 &mut self,
2038 url: &str,
2039 ll: &broadcast_hls::LowLatencyConfig,
2040 ) -> Result<Option<(u64, u64)>> {
2041 let Some(length) = ll.preload_hint_byte_range_length else {
2042 return Ok(None);
2043 };
2044 let br = ByteRange {
2045 length,
2046 offset: ll.preload_hint_byte_range_start,
2047 };
2048 self.resolve_byte_range(url, &Some(br))
2049 }
2050
2051 fn demux_and_emit(&mut self, id: ResourceId, bytes: &[u8]) -> Result<()> {
2052 let slot = self.init_of(&id).filter(|s| self.init_ready(Some(*s)));
2053 let Some(slot) = slot else {
2054 return Err(Error::InitNotYetAvailable { id });
2055 };
2056 self.announce_init(slot);
2057 let init = self
2058 .inits
2059 .get(&slot)
2060 .and_then(|s| s.bytes.as_ref())
2061 .ok_or(Error::InitNotYetAvailable { id })?;
2062 if segment::progressive_movie(bytes) {
2063 return Err(Error::NotFragmented { id });
2064 }
2065 let mut combined = Vec::with_capacity(init.len() + bytes.len());
2066 combined.extend_from_slice(init);
2067 combined.extend_from_slice(bytes);
2068 let mut demux = Fmp4Demux::new();
2069 let media = demux
2070 .unpackage(combined.as_slice())
2071 .map_err(|source| Error::Demux { id, source })?;
2072 let damaged = media
2073 .degraded
2074 .iter()
2075 .filter(|d| matches!(d, InputDegradation::MalformedUnit { .. }))
2076 .count();
2077 if damaged > 0 {
2078 self.pending_outputs.push_back(Output::FragmentDamaged {
2079 id,
2080 skipped: damaged,
2081 });
2082 }
2083 for skipped in &media.skipped {
2084 let key = (skipped.track_id.map(TrackId::value), skipped.fourcc.clone());
2085 let first = self
2086 .inits
2087 .get_mut(&slot)
2088 .is_some_and(|s| s.skipped_reported.insert(key));
2089 if first {
2090 self.pending_outputs.push_back(Output::TrackSkipped {
2091 track_id: skipped.track_id,
2092 fourcc: skipped.fourcc.clone(),
2093 reason: skipped.reason.clone(),
2094 });
2095 }
2096 }
2097 let specs: Vec<TrackSpec> = media.tracks.iter().map(|t| t.spec.clone()).collect();
2098 let specs_changed = self
2099 .last_fmp4_specs
2100 .as_ref()
2101 .map(|prev| format!("{:?}", prev) != format!("{:?}", specs))
2102 .unwrap_or(true);
2103 if specs_changed {
2104 self.pending_outputs
2105 .push_back(Output::Tracks(specs.clone()));
2106 self.last_fmp4_specs = Some(specs);
2107 }
2108 for track in media.tracks {
2109 if !track.samples.is_empty() {
2110 self.pending_outputs.push_back(Output::Samples {
2111 track_id: track.spec.track_id,
2112 samples: track.samples,
2113 });
2114 }
2115 }
2116 Ok(())
2117 }
2118
2119 fn begin_ts_resource(&mut self, id: ResourceId, msn: u64) -> Result<()> {
2129 if self.discontinuous_msns.contains(&msn) && !self.discontinuity_emitted.contains(&msn) {
2130 self.restart_ts_demux(id)?;
2131 self.emit_discontinuity_if_needed(msn);
2132 }
2133 Ok(())
2134 }
2135
2136 fn restart_ts_demux(&mut self, id: ResourceId) -> Result<()> {
2137 let Some(TsDemuxState(demux)) = self.ts_demux.as_mut() else {
2138 return Ok(());
2139 };
2140 demux.restart_timeline();
2141 self.drain_ts_demux(id)
2142 }
2143
2144 fn demux_and_emit_ts(&mut self, id: ResourceId, bytes: &[u8]) -> Result<()> {
2168 let TsDemuxState(demux) = self
2169 .ts_demux
2170 .get_or_insert_with(|| TsDemuxState(StreamingTsDemux::new()));
2171 demux.feed(bytes);
2172 self.drain_ts_demux(id)
2173 }
2174
2175 fn release_ts_segment_end(&mut self, id: ResourceId) -> Result<()> {
2179 let Some(TsDemuxState(demux)) = self.ts_demux.as_mut() else {
2180 return Ok(());
2181 };
2182 demux.release_silent_heads();
2183 self.drain_ts_demux(id)
2184 }
2185
2186 fn flush_ts_demux(&mut self, id: ResourceId) -> Result<()> {
2189 let Some(TsDemuxState(demux)) = self.ts_demux.as_mut() else {
2190 return Ok(());
2191 };
2192 demux.finish();
2193 self.drain_ts_demux(id)
2194 }
2195
2196 fn drain_ts_demux(&mut self, id: ResourceId) -> Result<()> {
2199 let Some(TsDemuxState(demux)) = self.ts_demux.as_mut() else {
2200 return Ok(());
2201 };
2202 let mut per_track: BTreeMap<TrackId, Vec<Sample>> = BTreeMap::new();
2203 let mut events: Vec<DemuxEvent> = Vec::new();
2204 while let Some(event) = demux.poll_event() {
2205 match event {
2206 DemuxEvent::TrackAdded(spec) | DemuxEvent::TrackUpdated(spec) => {
2207 if spec.config.is_muxable_in_bmff() {
2208 self.ts_specs.insert(spec.track_id, spec);
2209 } else {
2210 self.ts_specs.remove(&spec.track_id);
2211 let stream_type = match &spec.config {
2212 transmux::CodecConfig::Data { stream_type, .. } => Some(*stream_type),
2213 _ => None,
2214 };
2215 self.pending_outputs.push_back(Output::TrackDropped {
2216 track_id: spec.track_id,
2217 stream_type,
2218 });
2219 }
2220 }
2221 DemuxEvent::TrackRemoved { track_id, .. } => {
2222 self.ts_specs.remove(&track_id);
2223 }
2224 DemuxEvent::Sample { track_id, .. }
2225 | DemuxEvent::DisplayChanged { track_id, .. }
2226 if self.ts_specs.contains_key(&track_id) =>
2227 {
2228 events.push(event);
2229 }
2230 _ => {}
2231 }
2232 }
2233 if !self.ts_specs.is_empty() {
2234 let specs: Vec<TrackSpec> = self.ts_specs.values().cloned().collect();
2235 let init_bytes = transmux::build_init_segment(&specs, TS_MOVIE_TIMESCALE)
2236 .map_err(|source| Error::Demux { id, source })?;
2237 if self.last_ts_init.as_deref() != Some(init_bytes.as_slice()) {
2238 self.pending_outputs
2239 .push_back(Output::Tracks(specs.clone()));
2240 self.pending_outputs
2241 .push_back(Output::Init(init_bytes.clone()));
2242 self.last_ts_init = Some(init_bytes);
2243 }
2244 }
2245 for event in events {
2246 match event {
2247 DemuxEvent::Sample {
2248 track_id, sample, ..
2249 } => {
2250 per_track.entry(track_id).or_default().push(sample);
2251 }
2252 DemuxEvent::DisplayChanged {
2253 track_id,
2254 display,
2255 pts,
2256 ..
2257 } => {
2258 if let Some(samples) = per_track.remove(&track_id)
2259 && !samples.is_empty()
2260 {
2261 self.pending_outputs
2262 .push_back(Output::Samples { track_id, samples });
2263 }
2264 self.pending_outputs.push_back(Output::DisplayChanged {
2265 track_id,
2266 pts,
2267 display,
2268 });
2269 }
2270 _ => {}
2271 }
2272 }
2273 for (track_id, samples) in per_track {
2274 if !samples.is_empty() {
2275 self.pending_outputs
2276 .push_back(Output::Samples { track_id, samples });
2277 }
2278 }
2279 Ok(())
2280 }
2281
2282 fn emit_discontinuity_if_needed(&mut self, msn: u64) -> bool {
2285 if self.discontinuous_msns.contains(&msn) && !self.discontinuity_emitted.contains(&msn) {
2286 self.pending_outputs.push_back(Output::Discontinuity);
2287 self.discontinuity_emitted.insert(msn);
2288 return true;
2289 }
2290 false
2291 }
2292
2293 fn join_start(&mut self, playlist: &MediaPlaylist) -> Result<()> {
2319 let segments = &playlist.segments;
2320 let target = f64::from(playlist.target_duration.max(1));
2321 let duration_of = |seg: &MediaSegment| {
2322 let d = seg.duration.get();
2323 if d > 0.0 { d } else { target }
2324 };
2325
2326 let from_start_tag = playlist.start.as_ref().map(|start| {
2327 let total: f64 = segments.iter().map(duration_of).sum();
2328 let offset = start.time_offset.get();
2329 let position = if offset >= 0.0 {
2330 offset
2331 } else {
2332 total + offset
2333 };
2334 let position = position.clamp(0.0, total);
2335 let mut elapsed = 0.0_f64;
2336 for (i, seg) in segments.iter().enumerate() {
2337 elapsed += duration_of(seg);
2338 if elapsed > position {
2339 return i;
2340 }
2341 }
2342 segments.len().saturating_sub(1)
2344 });
2345
2346 let from_hold_back = (!playlist.endlist).then(|| {
2347 let ll = playlist.low_latency.as_ref();
2348 let wanted = match ll.filter(|ll| ll.can_block_reload) {
2349 Some(ll) if ll.part_hold_back.is_some() => {
2350 ll.part_hold_back.map_or(0.0, |p| p.get())
2351 }
2352 _ => {
2353 let hold_back = ll.and_then(|ll| ll.hold_back).map_or(0.0, |h| h.get());
2354 (LIVE_EDGE_TARGET_DURATIONS * target).max(hold_back)
2355 }
2356 };
2357 let mut behind: f64 = playlist
2358 .open_segment
2359 .as_ref()
2360 .map_or(0.0, |o| o.parts.iter().map(|p| p.duration.get()).sum());
2361 if behind >= wanted {
2362 return segments.len();
2363 }
2364 for (i, seg) in segments.iter().enumerate().rev() {
2365 behind += duration_of(seg);
2366 if behind >= wanted {
2367 return i;
2368 }
2369 }
2370 0
2371 });
2372
2373 let start = match (from_start_tag, from_hold_back) {
2374 (Some(s), Some(h)) => s.min(h),
2375 (Some(s), None) => s,
2376 (None, Some(h)) => h,
2377 (None, None) => 0,
2378 };
2379 for (i, seg) in segments.iter().enumerate().take(start) {
2380 let msn = playlist.media_sequence.saturating_add(index_u64(i));
2381 self.skip_segment(msn, seg)?;
2382 }
2383 Ok(())
2384 }
2385
2386 fn skip_segment(&mut self, msn: u64, seg: &MediaSegment) -> Result<()> {
2390 if self.delivered_segments.contains(&msn) {
2391 return Ok(());
2392 }
2393 if seg.parts.is_empty() {
2394 let url = url::resolve(self.base(), &seg.uri);
2395 self.resolve_byte_range(&url, &seg.byte_range)?;
2396 } else {
2397 for part in seg.parts.iter().filter(|p| !p.gap) {
2398 let url = url::resolve(self.base(), &part.uri);
2399 self.resolve_byte_range(&url, &part.byte_range)?;
2400 }
2401 }
2402 self.delivered_segments.insert(msn);
2403 Ok(())
2404 }
2405
2406 fn prune(&mut self, playlist: &MediaPlaylist) {
2413 let first = playlist.media_sequence;
2414 let below = |id: &ResourceId| resource_msn(id).is_some_and(|m| m < first);
2415 let fulfilled = &self.fulfilled;
2416 self.requested
2417 .retain(|id| !(below(id) && fulfilled.contains(id)));
2418 self.resource_requests.retain(|id, _| !below(id));
2419 self.fulfilled.retain(|id| !below(id));
2420 self.pending_keyed.retain(|(id, _)| !below(id));
2421 self.segment_crypto.retain(|&m, _| m >= first);
2422 self.delivered_parts.retain(|&(m, _)| m >= first);
2423 self.delivered_segments.retain(|&m| m >= first);
2424 self.discontinuous_msns.retain(|&m| m >= first);
2425 self.discontinuity_emitted.retain(|&m| m >= first);
2426 self.msn_init.retain(|&m, _| m >= first);
2427 self.prune_inits();
2428
2429 if playlist.skip.is_none() {
2430 let base = self.base();
2431 let mut live: BTreeSet<String> = BTreeSet::new();
2432 for seg in &playlist.segments {
2433 live.insert(url::resolve(base, &seg.uri));
2434 for part in &seg.parts {
2435 live.insert(url::resolve(base, &part.uri));
2436 }
2437 if let Some(map) = &seg.map {
2438 live.insert(url::resolve(base, &map.uri));
2439 }
2440 }
2441 if let Some(open) = &playlist.open_segment {
2442 for part in &open.parts {
2443 live.insert(url::resolve(base, &part.uri));
2444 }
2445 if let Some(map) = &open.map {
2446 live.insert(url::resolve(base, &map.uri));
2447 }
2448 }
2449 if let Some(hint) = playlist
2450 .low_latency
2451 .as_ref()
2452 .and_then(|ll| ll.preload_hint_part.as_ref())
2453 {
2454 live.insert(url::resolve(base, hint));
2455 }
2456 self.byte_range_cursor.retain(|u, _| live.contains(u));
2457 }
2458 }
2459
2460 fn prune_inits(&mut self) {
2463 let newest = self.inits.keys().next_back().copied();
2464 let bound: BTreeSet<InitId> = self.msn_init.values().copied().collect();
2465 self.inits.retain(|id, slot| {
2466 Some(*id) == newest || bound.contains(id) || slot.fetch != InitFetch::Ready
2467 });
2468 }
2469
2470 fn continuity(&self, new: &MediaPlaylist) -> Continuity {
2506 let Some(prev) = &self.last_full_playlist else {
2507 return if self
2508 .last_media_sequence
2509 .is_some_and(|p| new.media_sequence < p)
2510 {
2511 Continuity::Restart
2512 } else {
2513 Continuity::Continues
2514 };
2515 };
2516 let (p0, n0) = (prev.media_sequence, new.media_sequence);
2517 let regressed = n0 < self.last_media_sequence.unwrap_or(p0).max(p0);
2518
2519 let mut overlap = false;
2523 let mut shared = 0usize;
2524 let mut requeried = 0usize;
2525 for (i, seg) in new.segments.iter().enumerate() {
2526 let Some(msn) = n0.checked_add(index_u64(i)) else {
2527 return Continuity::Restart;
2528 };
2529 let Some(old) = msn
2530 .checked_sub(p0)
2531 .and_then(|d| usize::try_from(d).ok())
2532 .and_then(|d| prev.segments.get(d))
2533 else {
2534 continue;
2535 };
2536 if without_query(&old.uri) != without_query(&seg.uri)
2537 || old.duration != seg.duration
2538 || old.byte_range != seg.byte_range
2539 {
2540 return Continuity::Restart;
2541 }
2542 if let (Some(a), Some(b)) = (program_date_time(old), program_date_time(seg))
2543 && a != b
2544 {
2545 return Continuity::Restart;
2546 }
2547 overlap = true;
2548 shared = shared.saturating_add(1);
2549 if old.uri != seg.uri {
2550 requeried = requeried.saturating_add(1);
2551 }
2552 }
2553 if requeried != 0 && requeried != shared {
2554 return Continuity::Restart;
2555 }
2556
2557 if !regressed && new.discontinuity_sequence < prev.discontinuity_sequence {
2560 return Continuity::Restart;
2561 }
2562 if !regressed {
2563 return Continuity::Continues;
2564 }
2565
2566 if overlap {
2568 return Continuity::Lagging;
2569 }
2570 let old_uris: BTreeSet<&str> = prev.segments.iter().map(|s| s.uri.as_str()).collect();
2571 if new
2572 .segments
2573 .iter()
2574 .any(|s| old_uris.contains(s.uri.as_str()))
2575 {
2576 return Continuity::Restart;
2578 }
2579 let new_last = new.segments.iter().rev().find_map(program_date_time_ms);
2580 let prev_first = prev.segments.iter().find_map(program_date_time_ms);
2581 match (new_last, prev_first) {
2582 (Some(end), Some(start)) if end < start => Continuity::OlderWindow,
2583 _ => Continuity::Restart,
2584 }
2585 }
2586
2587 fn restart_after_regression(&mut self) -> Result<()> {
2599 self.flush_ts_demux(ResourceId::Init)?;
2602 self.ts_demux = None;
2603 self.ts_specs.clear();
2604 self.last_ts_init = None;
2605 self.pending_actions
2606 .retain(|a| !matches!(a, Action::FetchResource { .. }));
2607 self.pending_demux.clear();
2608 self.pending_actions
2609 .retain(|a| !matches!(a, Action::FetchKey { .. }));
2610 self.keys_in_flight.clear();
2611 self.segment_crypto.clear();
2612 self.pending_keyed.clear();
2613 self.requested.clear();
2614 self.fulfilled.clear();
2615 self.delivered_parts.clear();
2616 self.delivered_segments.clear();
2617 self.discontinuous_msns.clear();
2618 self.discontinuity_emitted.clear();
2619 self.byte_range_cursor.clear();
2620 self.resource_requests.clear();
2621 self.last_media_sequence = None;
2622 self.outstanding_fetches = 0;
2623 self.last_full_playlist = None;
2624 self.inits.clear();
2625 self.init_target = None;
2626 self.announced_init = None;
2627 self.msn_init.clear();
2628 self.last_fmp4_specs = None;
2629 self.full_reload_requested = false;
2630 self.joined = false;
2631 self.pending_outputs.push_back(Output::Discontinuity);
2632 Ok(())
2633 }
2634
2635 fn all_playlist_segments_delivered(&self) -> bool {
2636 let Some(playlist) = &self.last_full_playlist else {
2637 return true;
2638 };
2639 (0..playlist.segments.len() as u64).all(|i| {
2640 let msn = playlist.media_sequence.saturating_add(i);
2641 self.delivered_segments.contains(&msn)
2642 || self.fulfilled.contains(&ResourceId::Segment { msn })
2643 || self.covered_by_parts(msn)
2644 })
2645 }
2646
2647 fn maybe_emit_end_of_stream(&mut self) -> Result<()> {
2648 if self.saw_endlist
2649 && !self.end_emitted
2650 && self.outstanding_fetches == 0
2651 && self.pending_keyed.is_empty()
2652 && self.inits.values().all(|slot| slot.bytes.is_some())
2653 && self.all_playlist_segments_delivered()
2654 {
2655 self.flush_ts_demux(ResourceId::Init)?;
2658 self.pending_outputs.push_back(Output::EndOfStream);
2659 self.end_emitted = true;
2660 }
2661 Ok(())
2662 }
2663}
2664
2665#[cfg(test)]
2666mod tests {
2667 use super::*;
2668 use alloc::vec;
2669 use transmux::sample_aes::aes128_encrypt_segment;
2670
2671 const EMPTY_FRAGMENT: &[u8] = &[
2672 0, 0, 0, 8, b'm', b'o', b'o', b'f', 0, 0, 0, 8, b'm', b'd', b'a', b't',
2673 ];
2674
2675 #[test]
2676 fn decrypt_segment_strips_pkcs7_padding_to_the_exact_plaintext() {
2677 let key = [0x5Au8; KEY_LEN];
2678 let iv = [0xC3u8; BLOCK_LEN];
2679 let id = ResourceId::Segment { msn: 9 };
2680 let crypto = SegmentCrypto {
2681 key_id: KeyId(0),
2682 iv,
2683 };
2684 for len in [1usize, 15, 16, 17, 37, 188] {
2685 let plain: Vec<u8> = (0..len).map(|i| (i % 251) as u8).collect();
2686 let cipher = aes128_encrypt_segment(&key, &iv, &plain);
2687 assert_eq!(decrypt_segment(id, crypto, &key, &cipher).unwrap(), plain);
2688 }
2689 }
2690
2691 #[test]
2692 fn media_sequence_iv_is_big_endian_left_padded() {
2693 let iv = media_sequence_iv(0x0102_0304_0506_0708);
2694 assert_eq!(iv[..8], [0u8; 8]);
2695 assert_eq!(iv[8..], [1, 2, 3, 4, 5, 6, 7, 8]);
2696 assert_eq!(media_sequence_iv(0)[..], [0u8; 16]);
2697 assert_eq!(media_sequence_iv(0x1FF)[14..], [1, 0xFF]);
2698 }
2699
2700 #[test]
2706 fn on_resource_rejects_a_never_requested_id() {
2707 let mut client = HlsClient::new(HlsClientConfig::new("http://example.com/playlist.m3u8"));
2708 let id = ResourceId::Segment { msn: 0 };
2709
2710 let err = client
2711 .on_resource(id, b"some bytes")
2712 .expect_err("an id the client never requested must be rejected");
2713 assert!(
2714 matches!(err, Error::UnrequestedResource { id: got } if got == id),
2715 "wrong error variant: {err:?}"
2716 );
2717
2718 let err = client
2721 .on_resource(ResourceId::Init, b"init bytes")
2722 .expect_err("an unrequested Init must be rejected");
2723 assert!(
2724 matches!(
2725 err,
2726 Error::UnrequestedResource {
2727 id: ResourceId::Init
2728 }
2729 ),
2730 "wrong error variant: {err:?}"
2731 );
2732 }
2733
2734 #[test]
2739 fn on_resource_accepts_a_previously_requested_id() {
2740 let mut client = HlsClient::new(HlsClientConfig::new("http://example.com/playlist.m3u8"));
2741 let id = ResourceId::Segment { msn: 0 };
2742 client.request_resource(id, "http://example.com/seg0.m4s".to_string(), None);
2743
2744 let result = client.on_resource(id, EMPTY_FRAGMENT);
2748 assert!(
2749 result.is_ok(),
2750 "a requested id must be accepted: {result:?}"
2751 );
2752 assert!(
2753 client.pending_demux.iter().any(|(bid, _)| *bid == id),
2754 "expected the resource to be buffered pending the init segment"
2755 );
2756 }
2757
2758 #[test]
2761 fn is_ts_segment_true_when_no_map_seen_and_sync_byte_present() {
2762 let client = HlsClient::new(HlsClientConfig::new("http://example.com/playlist.m3u8"));
2763 assert!(client.is_ts_segment(&[TS_SYNC_BYTE, 0x40, 0x11, 0x00]));
2764 }
2765
2766 #[test]
2771 fn is_ts_segment_false_for_an_isobmff_resource_with_no_map_seen() {
2772 let client = HlsClient::new(HlsClientConfig::new("http://example.com/playlist.m3u8"));
2773 let ftyp_box = b"\x00\x00\x00\x18ftypiso5\x00\x00\x02\x00iso5iso6mp41";
2774 assert!(!client.is_ts_segment(ftyp_box));
2775 }
2776
2777 #[test]
2784 fn is_ts_segment_false_once_a_map_has_been_requested() {
2785 let mut client = HlsClient::new(HlsClientConfig::new("http://example.com/playlist.m3u8"));
2786 client
2787 .ensure_init_requested(&MapTag {
2788 uri: "init.mp4".to_string(),
2789 byte_range: None,
2790 extra_attrs: Vec::new(),
2791 })
2792 .expect("ensure_init_requested succeeds");
2793 assert!(!client.is_ts_segment(&[TS_SYNC_BYTE, 0x40, 0x11, 0x00]));
2794 }
2795
2796 #[test]
2812 fn resolve_byte_range_rejects_an_offset_plus_length_that_overflows_u64() {
2813 let mut client = HlsClient::new(HlsClientConfig::new("http://example.com/playlist.m3u8"));
2814 let br = Some(ByteRange {
2815 length: u64::MAX,
2816 offset: Some(1),
2817 });
2818 let err = client
2819 .resolve_byte_range("http://example.com/seg0.m4s", &br)
2820 .expect_err("offset 1 + length u64::MAX must overflow and be rejected");
2821 assert!(
2822 matches!(
2823 err,
2824 Error::ByteRangeOverflow {
2825 offset: 1,
2826 length: u64::MAX,
2827 ..
2828 }
2829 ),
2830 "wrong error variant/fields: {err:?}"
2831 );
2832 }
2833
2834 #[test]
2841 fn resolve_byte_range_rejects_a_cursor_accumulation_that_overflows_u64() {
2842 let mut client = HlsClient::new(HlsClientConfig::new("http://example.com/playlist.m3u8"));
2843 let url = "http://example.com/seg0.m4s";
2844 let seed = Some(ByteRange {
2847 length: u64::MAX - 10,
2848 offset: Some(5),
2849 });
2850 let (offset, length) = client
2851 .resolve_byte_range(url, &seed)
2852 .expect("seed range does not itself overflow")
2853 .expect("Some for a Some(ByteRange)");
2854 assert_eq!((offset, length), (5, u64::MAX - 10));
2855
2856 let next = Some(ByteRange {
2857 length: 100,
2858 offset: None, });
2860 let err = client
2861 .resolve_byte_range(url, &next)
2862 .expect_err("cursor + next length must overflow and be rejected");
2863 assert!(
2864 matches!(err, Error::ByteRangeOverflow { length: 100, .. }),
2865 "wrong error variant/fields: {err:?}"
2866 );
2867 }
2868
2869 #[test]
2890 fn merge_delta_does_not_panic_on_an_overflowing_skipped_segments() {
2891 let mut client = HlsClient::new(HlsClientConfig::new("http://example.com/playlist.m3u8"));
2892 let prev = MediaPlaylist {
2893 media_sequence: 0,
2894 segments: vec![MediaSegment::default(); 2],
2895 ..Default::default()
2896 };
2897 client.last_full_playlist = Some(prev);
2901
2902 let delta = MediaPlaylist {
2903 media_sequence: 10, skip: Some(broadcast_hls::SkipInfo {
2905 skipped_segments: u64::MAX - 5,
2906 ..Default::default()
2907 }),
2908 segments: Vec::new(),
2909 ..Default::default()
2910 };
2911
2912 let merged = client.merge_delta(delta.clone());
2913 assert_eq!(
2914 merged, delta,
2915 "an unmergeable skip count must fall back to the delta as-is, not panic"
2916 );
2917 }
2918
2919 fn byte_range_playlist(first_msn: u64, file: &str) -> String {
2920 format!(
2923 "#EXTM3U\n#EXT-X-VERSION:4\n#EXT-X-TARGETDURATION:2\n\
2924 #EXT-X-MEDIA-SEQUENCE:{first_msn}\n\
2925 #EXTINF:2.0,\n#EXT-X-BYTERANGE:100@0\n{file}\n\
2926 #EXTINF:2.0,\n#EXT-X-BYTERANGE:50\n{file}\n"
2927 )
2928 }
2929
2930 #[test]
2934 fn state_is_pruned_to_the_current_window() {
2935 let mut client = HlsClient::new(HlsClientConfig::new("http://example.com/p.m3u8"));
2936 client
2937 .on_playlist(byte_range_playlist(0, "a.ts").as_bytes())
2938 .unwrap();
2939 assert_eq!(
2940 client.byte_range_cursor.iter().collect::<Vec<_>>(),
2941 vec![(&"http://example.com/a.ts".to_string(), &150u64)]
2942 );
2943 client
2945 .on_resource(ResourceId::Segment { msn: 0 }, EMPTY_FRAGMENT)
2946 .unwrap();
2947
2948 client
2949 .on_playlist(byte_range_playlist(1000, "b.ts").as_bytes())
2950 .unwrap();
2951
2952 let requested: Vec<ResourceId> = client.requested.iter().copied().collect();
2953 assert_eq!(
2954 requested,
2955 vec![
2956 ResourceId::Segment { msn: 1 },
2958 ResourceId::Segment { msn: 1000 },
2959 ResourceId::Segment { msn: 1001 },
2960 ]
2961 );
2962 assert!(client.fulfilled.is_empty(), "{:?}", client.fulfilled);
2963 assert_eq!(
2964 client.byte_range_cursor.iter().collect::<Vec<_>>(),
2965 vec![(&"http://example.com/b.ts".to_string(), &150u64)],
2966 "the cursor of the unreferenced a.ts must be dropped"
2967 );
2968 }
2969
2970 #[test]
2973 fn discontinuity_bookkeeping_below_the_window_is_dropped() {
2974 let mut client = HlsClient::new(HlsClientConfig::new("http://example.com/p.m3u8"));
2975 let pl = |first: u64| {
2976 format!(
2977 "#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:2\n\
2978 #EXT-X-MEDIA-SEQUENCE:{first}\n#EXT-X-DISCONTINUITY\n#EXTINF:2.0,\ns.ts\n"
2979 )
2980 };
2981 client.on_playlist(pl(5).as_bytes()).unwrap();
2982 client
2983 .on_resource(ResourceId::Segment { msn: 5 }, EMPTY_FRAGMENT)
2984 .unwrap();
2985 assert_eq!(
2986 client
2987 .discontinuous_msns
2988 .iter()
2989 .copied()
2990 .collect::<Vec<_>>(),
2991 vec![5]
2992 );
2993 client.on_playlist(pl(9).as_bytes()).unwrap();
2994 assert_eq!(
2995 client
2996 .discontinuous_msns
2997 .iter()
2998 .copied()
2999 .collect::<Vec<_>>(),
3000 vec![9]
3001 );
3002 assert!(client.discontinuity_emitted.is_empty());
3003 assert!(client.delivered_segments.is_empty());
3004 }
3005
3006 fn keyed_playlist(first_msn: u64, count: u64, key_uri: &str) -> String {
3007 let mut text = format!(
3008 "#EXTM3U\n#EXT-X-VERSION:5\n#EXT-X-TARGETDURATION:2\n\
3009 #EXT-X-MEDIA-SEQUENCE:{first_msn}\n\
3010 #EXT-X-KEY:METHOD=AES-128,URI=\"{key_uri}\"\n"
3011 );
3012 for msn in first_msn..first_msn + count {
3013 text.push_str(&format!("#EXTINF:2.0,\nseg{msn}.ts\n"));
3014 }
3015 text
3016 }
3017
3018 fn pending_keyed_bytes(client: &HlsClient) -> usize {
3019 client.pending_keyed.iter().map(|(_, b)| b.len()).sum()
3020 }
3021
3022 #[test]
3023 fn pending_keyed_is_pruned_with_the_window() {
3024 let mut client = HlsClient::new(HlsClientConfig::new("http://example.com/p.m3u8"));
3025 client
3026 .on_playlist(keyed_playlist(0, 1, "key.bin").as_bytes())
3027 .unwrap();
3028 client
3029 .on_resource(ResourceId::Segment { msn: 0 }, &[0u8; 32])
3030 .unwrap();
3031 assert_eq!(client.pending_keyed.len(), 1);
3032 client
3033 .on_playlist(keyed_playlist(5, 1, "key.bin").as_bytes())
3034 .unwrap();
3035 assert!(
3036 client.pending_keyed.is_empty(),
3037 "ciphertext below the window can never be played: {:?}",
3038 client
3039 .pending_keyed
3040 .iter()
3041 .map(|(id, _)| id)
3042 .collect::<Vec<_>>()
3043 );
3044 }
3045
3046 #[test]
3047 fn pending_keyed_is_capped_and_drops_the_oldest_with_a_typed_error() {
3048 let mut client = HlsClient::new(HlsClientConfig::new("http://example.com/p.m3u8"));
3049 client
3050 .on_playlist(keyed_playlist(0, 2, "key.bin").as_bytes())
3051 .unwrap();
3052 let big = vec![0u8; 40 * 1024 * 1024];
3053 client
3054 .on_resource(ResourceId::Segment { msn: 0 }, &big)
3055 .unwrap();
3056 let err = client
3057 .on_resource(ResourceId::Segment { msn: 1 }, &big)
3058 .expect_err("the second 40 MiB segment exceeds the 64 MiB cap");
3059 assert!(
3060 matches!(err, Error::PendingKeyedOverflow { dropped } if dropped == ResourceId::Segment { msn: 0 }),
3061 "{err:?}"
3062 );
3063 assert!(
3064 pending_keyed_bytes(&client) <= MAX_PENDING_KEYED_BYTES,
3065 "{} bytes buffered",
3066 pending_keyed_bytes(&client)
3067 );
3068 assert_eq!(
3069 client
3070 .pending_keyed
3071 .iter()
3072 .map(|(id, _)| *id)
3073 .collect::<Vec<_>>(),
3074 vec![ResourceId::Segment { msn: 1 }]
3075 );
3076 }
3077
3078 #[test]
3079 fn key_maps_do_not_grow_with_per_reload_tokenised_key_urls() {
3080 let mut client = HlsClient::new(HlsClientConfig::new("http://example.com/p.m3u8"));
3081 for n in 0..8u64 {
3082 client
3083 .on_playlist(keyed_playlist(n, 2, &format!("key.bin?t={n}")).as_bytes())
3084 .unwrap();
3085 }
3086 assert!(
3087 client.key_ids.len() <= 3 && client.key_urls.len() <= 3,
3088 "{:?} {:?}",
3089 client.key_ids,
3090 client.key_urls
3091 );
3092 }
3093
3094 #[test]
3095 #[cfg(feature = "std")]
3096 fn next_wait_reports_the_queued_wait_hint_without_consuming_it() {
3097 use core::time::Duration;
3098 let mut c = HlsClient::new(HlsClientConfig::new("http://h/p.m3u8"));
3099 assert_eq!(c.next_wait(), None, "the first queued action is a fetch");
3100 let _ = c.poll();
3101 c.on_playlist(
3102 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",
3103 )
3104 .unwrap();
3105 while c.next_wait().is_none() {
3106 c.poll()
3107 .expect("a WaitMs must be queued after a non-blocking live playlist");
3108 }
3109 assert_eq!(
3110 c.next_wait(),
3111 Some(Duration::from_millis(1000)),
3112 "half the 2 s target duration"
3113 );
3114 assert_eq!(
3115 c.next_wait(),
3116 Some(Duration::from_millis(1000)),
3117 "peeking does not consume"
3118 );
3119 let t0 = std::time::Instant::now();
3122 let first = c.poll_timeout(t0).expect("a wait is at the front");
3123 assert_eq!(first, t0 + Duration::from_millis(1000));
3124 for later in [250u64, 600, 999, 5000] {
3125 assert_eq!(
3126 c.poll_timeout(t0 + Duration::from_millis(later)),
3127 Some(first),
3128 "re-query at +{later} ms must not slide the deadline"
3129 );
3130 }
3131 assert!(matches!(c.poll(), Some(Action::WaitMs(1000))));
3133 assert_eq!(c.poll_timeout(t0 + Duration::from_secs(9)), None);
3134 }
3135
3136 fn program_date_ms(text: &str) -> Option<i64> {
3139 broadcast_hls::ProgramDateTime::parse(text)
3140 .ok()
3141 .map(|pdt| pdt.timestamp().as_millisecond())
3142 }
3143
3144 #[test]
3147 fn program_date_time_accepts_a_zone_less_value_as_utc_and_ignores_whitespace() {
3148 assert_eq!(
3149 program_date_ms("2026-10-02T10:00:00"),
3150 Some(1_790_935_200_000)
3151 );
3152 assert_eq!(
3153 program_date_ms(" 2026-10-02T10:00:00Z "),
3154 Some(1_790_935_200_000)
3155 );
3156 assert_eq!(program_date_ms("not a date"), None);
3157 }
3158
3159 #[test]
3160 fn program_date_time_accepts_the_common_spellings() {
3161 assert_eq!(
3162 program_date_ms("2026-10-02 10:00:00Z"),
3163 Some(1_790_935_200_000),
3164 "space separator"
3165 );
3166 assert_eq!(
3167 program_date_ms("2026-10-02t10:00:00z"),
3168 Some(1_790_935_200_000),
3169 "lower-case t and z"
3170 );
3171 assert_eq!(
3172 program_date_ms("2026-10-02T10:00:00.123456789Z"),
3173 Some(1_790_935_200_123),
3174 "extra digits truncated"
3175 );
3176 assert_eq!(
3177 program_date_ms("1969-12-31T23:59:59Z"),
3178 Some(-1_000),
3179 "before the epoch"
3180 );
3181 }
3182
3183 #[test]
3184 fn rfc3339_offset_spellings_all_name_the_same_instant() {
3185 let want = Some(1_790_935_200_000);
3186 for text in [
3187 "2026-10-02T12:00:00+02:00",
3188 "2026-10-02T12:00:00+0200",
3189 "2026-10-02T05:00:00-05:00",
3190 "2026-10-02T05:00:00-0500",
3191 "2026-10-02T10:00:00+00:00",
3192 "2026-10-02T10:00:00-00:00",
3193 ] {
3194 assert_eq!(program_date_ms(text), want, "{text}");
3195 }
3196 let hour_only = program_date_ms("2026-10-02T12:00:00+02");
3198 assert!(hour_only == want || hour_only.is_none(), "{hour_only:?}");
3199 }
3200
3201 #[test]
3203 fn a_leap_second_is_clamped_to_the_last_second_of_the_minute() {
3204 assert_eq!(
3205 program_date_ms("2016-12-31T23:59:60Z"),
3206 Some(1_483_228_799_000)
3207 );
3208 }
3209
3210 #[test]
3211 fn rfc3339_parses_to_literal_epoch_milliseconds() {
3212 assert_eq!(
3214 program_date_ms("2026-10-02T10:00:00Z"),
3215 Some(1_790_935_200_000)
3216 );
3217 assert_eq!(
3218 program_date_ms("2026-10-02T10:00:00.250Z"),
3219 Some(1_790_935_200_250)
3220 );
3221 assert_eq!(
3222 program_date_ms("2026-10-02T10:00:00.5Z"),
3223 Some(1_790_935_200_500)
3224 );
3225 assert_eq!(program_date_ms("1970-01-01T00:00:00Z"), Some(0));
3226 assert_eq!(
3227 program_date_ms("2000-02-29T23:59:59Z"),
3228 Some(951_868_799_000)
3229 );
3230 assert_eq!(
3232 program_date_ms("2026-10-02T12:00:00+02:00"),
3233 Some(1_790_935_200_000)
3234 );
3235 assert_eq!(
3236 program_date_ms("2026-10-02T05:00:00-0500"),
3237 Some(1_790_935_200_000)
3238 );
3239 assert_eq!(program_date_ms("2026-10-02"), None, "a bare date");
3242 for bad in [
3243 "",
3244 "2026-13-02T10:00:00Z",
3245 "2026-10-02T25:00:00Z",
3246 "2026-10-02T10:00:00Zjunk",
3247 "2026-10-02T10:00:00.Z",
3248 "2026-10-02T10:00:00+2:00",
3249 "xxxx-10-02T10:00:00Z",
3250 ] {
3251 assert_eq!(program_date_ms(bad), None, "{bad:?}");
3252 }
3253 }
3254}