From a0a4066b076b79d8a07e242e0050b1f61f27d315 Mon Sep 17 00:00:00 2001 From: Juan Moreno Date: Mon, 2 Sep 2024 05:07:58 +0200 Subject: [PATCH] Added low-latency elements Co-authored-by: Jendrik Weise --- src/parser.rs | 81 +++++++++++- src/playlist.rs | 325 +++++++++++++++++++++++++++++++++++++++++++++++- tests/lib.rs | 121 ++++++++++++++++++ 3 files changed, 522 insertions(+), 5 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index 10344ac..67c639f 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -352,6 +352,12 @@ enum MediaPlaylistTag { IFramesOnly, Start(Start), IndependentSegments, + + ServerControl(ServerControl), + PartInf(PartInf), + Skip(Skip), + PreloadHint(PreloadHint), + RenditionReport(RenditionReport), } fn media_playlist_tag(i: &[u8]) -> IResult<&[u8], MediaPlaylistTag> { @@ -384,6 +390,11 @@ fn media_playlist_tag(i: &[u8]) -> IResult<&[u8], MediaPlaylistTag> { MediaPlaylistTag::IndependentSegments }), map(tag("#EXT-X-ENDLIST"), |_| MediaPlaylistTag::EndList), + map(server_control_tag, MediaPlaylistTag::ServerControl), + map(part_inf_tag, MediaPlaylistTag::PartInf), + map(skip_tag, MediaPlaylistTag::Skip), + map(preload_hint_tag, MediaPlaylistTag::PreloadHint), + map(rendition_report_tag, MediaPlaylistTag::RenditionReport), map(media_segment_tag, MediaPlaylistTag::Segment), ))(i) } @@ -423,6 +434,21 @@ fn media_playlist_from_tags(mut tags: Vec) -> MediaPlaylist { MediaPlaylistTag::IndependentSegments => { media_playlist.independent_segments = true; } + MediaPlaylistTag::ServerControl(s) => { + media_playlist.server_control = Some(s); + } + MediaPlaylistTag::PartInf(p) => { + media_playlist.part_inf = Some(p); + } + MediaPlaylistTag::Skip(s) => { + media_playlist.skip = Some(s); + } + MediaPlaylistTag::PreloadHint(p) => { + media_playlist.preload_hint = Some(p); + } + MediaPlaylistTag::RenditionReport(r) => { + media_playlist.rendition_report = Some(r); + } MediaPlaylistTag::Segment(segment_tag) => match segment_tag { SegmentTag::Extinf(d, t) => { next_segment.duration = d; @@ -446,9 +472,6 @@ fn media_playlist_from_tags(mut tags: Vec) -> MediaPlaylist { SegmentTag::DateRange(d) => { next_segment.daterange = Some(d); } - SegmentTag::Unknown(t) => { - next_segment.unknown_tags.push(t); - } SegmentTag::Uri(u) => { next_segment.key = encryption_key.clone(); next_segment.map = map.clone(); @@ -458,6 +481,12 @@ fn media_playlist_from_tags(mut tags: Vec) -> MediaPlaylist { encryption_key = None; map = None; } + SegmentTag::Part(p) => { + next_segment.parts.push(p); + } + SegmentTag::Unknown(t) => { + next_segment.unknown_tags.push(t); + } _ => (), }, } @@ -489,6 +518,7 @@ enum SegmentTag { Unknown(ExtTag), Comment(Option), Uri(String), + Part(Part), } fn media_segment_tag(i: &[u8]) -> IResult<&[u8], SegmentTag> { @@ -515,6 +545,7 @@ fn media_segment_tag(i: &[u8]) -> IResult<&[u8], SegmentTag> { map(pair(tag("#EXT-X-DATERANGE:"), daterange), |(_, range)| { SegmentTag::DateRange(range) }), + map(part_tag, SegmentTag::Part), // Ensure part_tag is integrated here map(ext_tag, SegmentTag::Unknown), map(comment_tag, SegmentTag::Comment), map(consume_line, SegmentTag::Uri), @@ -781,6 +812,50 @@ fn unquoted_from_utf8_slice(s: &[u8]) -> Result IResult<&[u8], ServerControl> { + map_res( + pair(tag("#EXT-X-SERVER-CONTROL:"), key_value_pairs), + |(_, attributes)| ServerControl::from_hashmap(attributes), + )(i) +} + +fn part_inf_tag(i: &[u8]) -> IResult<&[u8], PartInf> { + map_res( + pair(tag("#EXT-X-PART-INF:"), key_value_pairs), + |(_, attributes)| PartInf::from_hashmap(attributes), + )(i) +} + +fn part_tag(i: &[u8]) -> IResult<&[u8], Part> { + map_res( + pair(tag("#EXT-X-PART:"), key_value_pairs), + |(_, attributes)| Part::from_hashmap(attributes), + )(i) +} + +fn skip_tag(i: &[u8]) -> IResult<&[u8], Skip> { + map_res( + pair(tag("#EXT-X-SKIP:"), key_value_pairs), + |(_, attributes)| Skip::from_hashmap(attributes), + )(i) +} + +fn preload_hint_tag(i: &[u8]) -> IResult<&[u8], PreloadHint> { + map_res( + pair(tag("#EXT-X-PRELOAD-HINT:"), key_value_pairs), + |(_, attributes)| PreloadHint::from_hashmap(attributes), + )(i) +} + +fn rendition_report_tag(i: &[u8]) -> IResult<&[u8], RenditionReport> { + map_res( + pair(tag("#EXT-X-RENDITION-REPORT:"), key_value_pairs), + |(_, attributes)| RenditionReport::from_hashmap(attributes), + )(i) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/playlist.rs b/src/playlist.rs index 8b3fa0c..bc9e835 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -27,6 +27,23 @@ macro_rules! write_some_attribute_quoted { }; } +macro_rules! write_some_float_attribute { + ($w:expr, $tag:expr, $o:expr) => { + if let &Some(ref v) = $o { + match WRITE_OPT_FLOAT_PRECISION.load(Ordering::Relaxed) { + MAX => { + write!($w, "{}={}", $tag, v) + }, + precision => { + write!($w, "{}={:.*}", $tag, precision, v) + }, + } + } else { + Ok(()) + } + }; +} + macro_rules! write_some_attribute { ($w:expr, $tag:expr, $o:expr) => { if let &Some(ref v) = $o { @@ -736,6 +753,13 @@ pub struct MediaPlaylist { pub independent_segments: bool, /// Unknown tags before the first media segment pub unknown_tags: Vec, + + // LL-HLS specific fields + pub server_control: Option, + pub part_inf: Option, + pub skip: Option, + pub preload_hint: Option, + pub rendition_report: Option, } impl MediaPlaylist { @@ -748,6 +772,22 @@ impl MediaPlaylist { if self.independent_segments { writeln!(w, "#EXT-X-INDEPENDENT-SEGMENTS")?; } + if let Some(ref server_control) = self.server_control { + server_control.write_to(w)?; + } + if let Some(ref part_inf) = self.part_inf { + part_inf.write_to(w)?; + } + if let Some(ref skip) = self.skip { + skip.write_to(w)?; + } + if let Some(ref preload_hint) = self.preload_hint { + preload_hint.write_to(w)?; + } + if let Some(ref rendition_report) = self.rendition_report { + rendition_report.write_to(w)?; + } + writeln!(w, "#EXT-X-TARGETDURATION:{}", self.target_duration)?; if self.media_sequence != 0 { @@ -776,6 +816,10 @@ impl MediaPlaylist { writeln!(w, "#EXT-X-ENDLIST")?; } + for unknown_tag in &self.unknown_tags { + writeln!(w, "{}", unknown_tag)?; + } + Ok(()) } } @@ -847,6 +891,9 @@ pub struct MediaSegment { pub daterange: Option, /// `#EXT-` pub unknown_tags: Vec, + + // LL-HLS specific fields + pub parts: Vec, } impl MediaSegment { @@ -885,6 +932,9 @@ impl MediaSegment { v.write_attributes_to(w)?; writeln!(w)?; } + for part in &self.parts { + part.write_to(w)?; + } for unknown_tag in &self.unknown_tags { writeln!(w, "{}", unknown_tag)?; } @@ -1048,6 +1098,37 @@ impl ByteRange { } } +impl Display for ByteRange { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.length)?; + if let Some(offset) = self.offset { + write!(f, "@{}", offset)?; + } + Ok(()) + } +} + +impl FromStr for ByteRange { + type Err = String; + + fn from_str(s: &str) -> Result { + let mut parts = s.split('@'); + let length = parts + .next() + .ok_or_else(|| String::from("Invalid BYTERANGE format"))? + .parse::() + .map_err(|err| format!("Failed to parse length in BYTERANGE: {}", err))?; + let offset = parts + .next() + .map(|o| { + o.parse::() + .map_err(|err| format!("Failed to parse offset in BYTERANGE: {}", err)) + }) + .transpose()?; + Ok(ByteRange { length, offset }) + } +} + /// [`#EXT-X-DATERANGE:`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.2.7) /// /// The EXT-X-DATERANGE tag associates a Date Range (i.e. a range of time @@ -1125,8 +1206,8 @@ impl DateRange { ",END-DATE", &self.end_date.as_ref().map(|dt| dt.to_rfc3339()) )?; - write_some_attribute!(w, ",DURATION", &self.duration)?; - write_some_attribute!(w, ",PLANNED-DURATION", &self.planned_duration)?; + write_some_float_attribute!(w, ",DURATION", &self.duration)?; + write_some_float_attribute!(w, ",PLANNED-DURATION", &self.planned_duration)?; if let Some(x_prefixed) = &self.x_prefixed { for (name, attr) in x_prefixed { write!(w, ",{}={}", name, attr)?; @@ -1144,6 +1225,246 @@ impl DateRange { } } +// Implementing structs for LL-HLS +#[derive(Debug, Default, PartialEq, Clone)] +pub struct ServerControl { + pub can_skip_until: Option, + pub can_skip_dateranges: bool, + pub hold_back: Option, + pub part_hold_back: Option, + pub can_block_reload: bool, +} + +impl ServerControl { + pub(crate) fn from_hashmap( + mut attrs: HashMap, + ) -> Result { + let can_skip_until = unquoted_string_parse!(attrs, "CAN-SKIP-UNTIL", |s: &str| s + .parse::() + .map_err(|err| format!("Failed to parse CAN-SKIP-UNTIL attribute: {}", err))); + let can_skip_dateranges = is_yes!(attrs, "CAN-SKIP-DATERANGES"); + if can_skip_dateranges && can_skip_until.is_none() { + return Err(String::from( + "CAN-SKIP-DATERANGES attribute must be used with CAN-SKIP-UNTIL attribute", + )); + } + let hold_back = unquoted_string_parse!(attrs, "HOLD-BACK", |s: &str| s + .parse::() + .map_err(|err| format!("Failed to parse HOLD-BACK attribute: {}", err))); + let part_hold_back = unquoted_string_parse!(attrs, "PART-HOLD-BACK", |s: &str| s + .parse::() + .map_err(|err| format!("Failed to parse PART-HOLD-BACK attribute: {}", err))); + let can_block_reload = is_yes!(attrs, "CAN-BLOCK-RELOAD"); + + Ok(ServerControl { + can_skip_until, + can_skip_dateranges, + hold_back, + part_hold_back, + can_block_reload, + }) + } + + pub(crate) fn write_to(&self, w: &mut T) -> std::io::Result<()> { + write!(w, "#EXT-X-SERVER-CONTROL:")?; + write_some_float_attribute!(w, "CAN-SKIP-UNTIL", &self.can_skip_until)?; + if self.can_skip_dateranges { + write!(w, ",CAN-SKIP-DATERANGES=YES")?; + } + write_some_float_attribute!(w, ",HOLD-BACK", &self.hold_back)?; + write_some_float_attribute!(w, ",PART-HOLD-BACK", &self.part_hold_back)?; + if self.can_block_reload { + write!(w, ",CAN-BLOCK-RELOAD=YES")?; + } + writeln!(w) + } +} + +#[derive(Debug, Default, PartialEq, Clone)] +pub struct PartInf { + pub part_target: f64, +} + +impl PartInf { + pub(crate) fn from_hashmap( + mut attrs: HashMap, + ) -> Result { + let part_target = unquoted_string_parse!(attrs, "PART-TARGET", |s: &str| s + .parse::() + .map_err(|err| format!("Failed to parse PART-TARGET attribute: {}", err))) + .ok_or_else(|| String::from("EXT-X-PART-INF without mandatory PART-TARGET attribute"))?; + + Ok(PartInf { part_target }) + } + + pub(crate) fn write_to(&self, w: &mut T) -> std::io::Result<()> { + match WRITE_OPT_FLOAT_PRECISION.load(Ordering::Relaxed) { + MAX => { + write!(w, "#EXT-X-PART-INF:PART-TARGET={}", self.part_target)?; + } + n => { + write!(w, "#EXT-X-PART-INF:PART-TARGET={:.*}", n, self.part_target)?; + } + }; + + writeln!(w) + } +} + +#[derive(Debug, Default, PartialEq, Clone)] +pub struct Part { + pub uri: String, + pub duration: f64, + pub independent: bool, + pub gap: bool, + pub byte_range: Option, +} + +impl Part { + pub(crate) fn from_hashmap( + mut attrs: HashMap, + ) -> Result { + let uri = quoted_string!(attrs, "URI").ok_or_else(|| { + String::from("EXT-X-PART without mandatory URI attribute") + })?; + let duration = unquoted_string_parse!(attrs, "DURATION", |s: &str| s + .parse::() + .map_err(|err| format!("Failed to parse DURATION attribute: {}", err))) + .ok_or_else(|| String::from("EXT-X-PART without mandatory DURATION attribute"))?; + let independent = is_yes!(attrs, "INDEPENDENT"); + let gap = is_yes!(attrs, "GAP"); + let byte_range = quoted_string_parse!(attrs, "BYTERANGE", |s: &str| s.parse::()); + + Ok(Part { + uri, + duration, + independent, + gap, + byte_range, + }) + } + + pub(crate) fn write_to(&self, w: &mut T) -> std::io::Result<()> { + match WRITE_OPT_FLOAT_PRECISION.load(Ordering::Relaxed) { + MAX => { + write!(w, "#EXT-X-PART:URI=\"{}\",DURATION={}", self.uri, self.duration)?; + } + n => { + write!(w, "#EXT-X-PART:URI=\"{}\",DURATION={:.*}", self.uri, n, self.duration)?; + } + }; + if self.independent { + write!(w, ",INDEPENDENT=YES")?; + } + if self.gap { + write!(w, ",GAP=YES")?; + } + if let Some(ref byte_range) = self.byte_range { + write!(w, ",BYTERANGE=\"")?; + byte_range.write_value_to(w)?; + write!(w, "\"")?; + } + writeln!(w) + } +} + +#[derive(Debug, Default, PartialEq, Clone)] +pub struct Skip { + pub skipped_segments: u64, +} + +impl Skip { + pub(crate) fn from_hashmap( + mut attrs: HashMap, + ) -> Result { + let skipped_segments = unquoted_string_parse!(attrs, "SKIPPED-SEGMENTS", |s: &str| s + .parse::() + .map_err(|err| format!("Failed to parse SKIPPED-SEGMENTS attribute: {}", err))) + .ok_or_else(|| String::from("EXT-X-SKIP without mandatory SKIPPED-SEGMENTS attribute"))?; + + Ok(Skip { skipped_segments }) + } + + pub(crate) fn write_to(&self, w: &mut T) -> std::io::Result<()> { + write!(w, "#EXT-X-SKIP:SKIPPED-SEGMENTS={}", self.skipped_segments)?; + writeln!(w) + } +} + +#[derive(Debug, Default, PartialEq, Clone)] +pub struct PreloadHint { + pub hint_type: String, + pub uri: String, + pub byte_range: Option, +} + +impl PreloadHint { + pub(crate) fn from_hashmap( + mut attrs: HashMap, + ) -> Result { + let hint_type = unquoted_string!(attrs, "TYPE") + .ok_or_else(|| String::from("EXT-X-PRELOAD-HINT without mandatory TYPE attribute"))?; + let uri = quoted_string!(attrs, "URI") + .ok_or_else(|| String::from("EXT-X-PRELOAD-HINT without mandatory URI attribute"))?; + let byte_range = quoted_string_parse!(attrs, "BYTERANGE", |s: &str| s.parse::()); + Ok(PreloadHint { + hint_type, + uri, + byte_range, + }) + } + + pub(crate) fn write_to(&self, w: &mut T) -> std::io::Result<()> { + write!( + w, + "#EXT-X-PRELOAD-HINT:TYPE={},URI=\"{}\"", + self.hint_type, self.uri + )?; + if let Some(ref byte_range) = self.byte_range { + write!(w, ",BYTERANGE=\"")?; + byte_range.write_value_to(w)?; + write!(w, "\"")?; + } + writeln!(w) + } +} + +#[derive(Debug, Default, PartialEq, Clone)] +pub struct RenditionReport { + pub uri: String, + pub last_msn: Option, + pub last_part: Option, +} + +impl RenditionReport { + pub(crate) fn from_hashmap( + mut attrs: HashMap, + ) -> Result { + let uri = quoted_string!(attrs, "URI").ok_or_else(|| { + String::from("EXT-X-RENDITION-REPORT without mandatory URI attribute") + })?; + let last_msn = unquoted_string_parse!(attrs, "LAST-MSN", |s: &str| s + .parse::() + .map_err(|err| format!("Failed to parse LAST-MSN attribute: {}", err))); + let last_part = unquoted_string_parse!(attrs, "LAST-PART", |s: &str| s + .parse::() + .map_err(|err| format!("Failed to parse LAST-PART attribute: {}", err))); + + Ok(RenditionReport { + uri, + last_msn, + last_part, + }) + } + + pub(crate) fn write_to(&self, w: &mut T) -> std::io::Result<()> { + write!(w, "#EXT-X-RENDITION-REPORT:URI=\"{}\"", self.uri)?; + write_some_attribute!(w, ",LAST-MSN", &self.last_msn)?; + write_some_attribute!(w, ",LAST-PART", &self.last_part)?; + writeln!(w) + } +} + // ----------------------------------------------------------------------------------------------- // Rest // ----------------------------------------------------------------------------------------------- diff --git a/tests/lib.rs b/tests/lib.rs index a388365..f15649c 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -416,6 +416,12 @@ fn create_and_parse_media_playlist_full() { ..Default::default() }], unknown_tags: vec![], + + server_control: Default::default(), + part_inf: Default::default(), + skip: Default::default(), + preload_hint: Default::default(), + rendition_report: Default::default(), }); let playlist_parsed = print_create_and_parse_playlist(&mut playlist_original); assert_eq!(playlist_original, playlist_parsed); @@ -470,3 +476,118 @@ fn parsing_binary_data_should_fail_cleanly() { assert!(res.is_err()); } +#[test] +fn create_and_parse_media_playlist_llhls() { + let mut playlist_original = Playlist::MediaPlaylist(MediaPlaylist { + version: Some(9), + target_duration: 2, + media_sequence: 338559, + discontinuity_sequence: 1234, + end_list: false, + playlist_type: Some(MediaPlaylistType::Event), + i_frames_only: false, + start: Some(Start { + time_offset: "9999".parse().unwrap(), + precise: Some(true), + other_attributes: Default::default(), + }), + independent_segments: true, + segments: vec![MediaSegment { + uri: "20140311T113819-01-338559live.ts".into(), + duration: 2.002, + title: Some("338559".into()), + byte_range: Some(ByteRange { + length: 137116, + offset: Some(4559), + }), + discontinuity: true, + key: Some(Key { + method: KeyMethod::None, + uri: Some("https://secure.domain.com".into()), + iv: Some("0xb059217aa2649ce170b734".into()), + keyformat: Some("xXkeyformatXx".into()), + keyformatversions: Some("xXFormatVers".into()), + }), + map: Some(Map { + uri: "www.map-uri.com".into(), + byte_range: Some(ByteRange { + length: 137116, + offset: Some(4559), + }), + other_attributes: Default::default(), + }), + program_date_time: Some( + chrono::FixedOffset::east(8 * 3600) + .ymd(2010, 2, 19) + .and_hms_milli(14, 54, 23, 31), + ), + daterange: Some(DateRange { + id: "9999".into(), + class: Some("class".into()), + start_date: chrono::FixedOffset::east(8 * 3600) + .ymd(2010, 2, 19) + .and_hms_milli(14, 54, 23, 31), + end_date: None, + duration: None, + planned_duration: Some("40.000".parse().unwrap()), + x_prefixed: Some(HashMap::from([( + "X-client-attribute".into(), + "whatever".into(), + )])), + end_on_next: false, + other_attributes: Default::default(), + }), + unknown_tags: vec![], + parts: vec![ + Part { + uri: "part0.ts".into(), + duration: 0.5, + independent: true, + gap: false, + byte_range: Some(ByteRange { + length: 50000, + offset: Some(0), + }), + }, + Part { + uri: "part1.ts".into(), + duration: 0.5, + independent: false, + gap: false, + byte_range: Some(ByteRange { + length: 50000, + offset: Some(50000), + }), + }, + ], + ..Default::default() + }], + unknown_tags: vec![], + server_control: Some(ServerControl { + can_skip_until: Some(12.0), + can_skip_dateranges: false, + hold_back: Some(3.0), + part_hold_back: Some(1.5), + can_block_reload: true, + }), + part_inf: Some(PartInf { part_target: 0.5 }), + skip: Some(Skip { + skipped_segments: 3, + }), + preload_hint: Some(PreloadHint { + hint_type: "PART".into(), + uri: "next_part.ts".into(), + byte_range: Some(ByteRange { + length: 50000, + offset: Some(100000), + }), + }), + rendition_report: Some(RenditionReport { + uri: "rendition.m3u8".into(), + last_msn: Some(338559), + last_part: Some(1), + }), + }); + let playlist_parsed = print_create_and_parse_playlist(&mut playlist_original); + assert_eq!(playlist_original, playlist_parsed); +}