Added low-latency elements

Co-authored-by: Jendrik Weise <jewe37@gmail.com>
This commit is contained in:
Juan Moreno 2024-09-02 05:07:58 +02:00 committed by Jendrik Weise
parent 381ac7732f
commit e4004a142e
3 changed files with 529 additions and 5 deletions

View file

@ -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<MediaPlaylistTag>) -> 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<MediaPlaylistTag>) -> 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<MediaPlaylistTag>) -> 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<String>),
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<QuotedOrUnquoted, string::FromUt
}
}
// Low latency HLS parsers
fn server_control_tag(i: &[u8]) -> 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::*;

View file

@ -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<ExtTag>,
// LL-HLS specific fields
pub server_control: Option<ServerControl>,
pub part_inf: Option<PartInf>,
pub skip: Option<Skip>,
pub preload_hint: Option<PreloadHint>,
pub rendition_report: Option<RenditionReport>,
}
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<DateRange>,
/// `#EXT-`
pub unknown_tags: Vec<ExtTag>,
// LL-HLS specific fields
pub parts: Vec<Part>,
}
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<ByteRange, String> {
let mut parts = s.split('@');
let length = parts
.next()
.ok_or_else(|| String::from("Invalid BYTERANGE format"))?
.parse::<u64>()
.map_err(|err| format!("Failed to parse length in BYTERANGE: {}", err))?;
let offset = parts
.next()
.map(|o| {
o.parse::<u64>()
.map_err(|err| format!("Failed to parse offset in BYTERANGE: {}", err))
})
.transpose()?;
Ok(ByteRange { length, offset })
}
}
/// [`#EXT-X-DATERANGE:<attribute-list>`](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,253 @@ impl DateRange {
}
}
// Implementing structs for LL-HLS
#[derive(Debug, Default, PartialEq, Clone)]
pub struct ServerControl {
pub can_skip_until: Option<f64>,
pub can_skip_dateranges: bool,
pub hold_back: Option<f64>,
pub part_hold_back: Option<f64>,
pub can_block_reload: bool,
}
impl ServerControl {
pub(crate) fn from_hashmap(
mut attrs: HashMap<String, QuotedOrUnquoted>,
) -> Result<ServerControl, String> {
let can_skip_until = unquoted_string_parse!(attrs, "CAN-SKIP-UNTIL", |s: &str| s
.parse::<f64>()
.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::<f64>()
.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::<f64>()
.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<T: Write>(&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<String, QuotedOrUnquoted>,
) -> Result<PartInf, String> {
let part_target = unquoted_string_parse!(attrs, "PART-TARGET", |s: &str| s
.parse::<f64>()
.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<T: Write>(&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<ByteRange>,
}
impl Part {
pub(crate) fn from_hashmap(
mut attrs: HashMap<String, QuotedOrUnquoted>,
) -> Result<Part, String> {
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::<f64>()
.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::<ByteRange>());
Ok(Part {
uri,
duration,
independent,
gap,
byte_range,
})
}
pub(crate) fn write_to<T: Write>(&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<String, QuotedOrUnquoted>,
) -> Result<Skip, String> {
let skipped_segments = unquoted_string_parse!(attrs, "SKIPPED-SEGMENTS", |s: &str| s
.parse::<u64>()
.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<T: Write>(&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<ByteRange>,
}
impl PreloadHint {
pub(crate) fn from_hashmap(
mut attrs: HashMap<String, QuotedOrUnquoted>,
) -> Result<PreloadHint, String> {
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::<ByteRange>());
Ok(PreloadHint {
hint_type,
uri,
byte_range,
})
}
pub(crate) fn write_to<T: Write>(&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<u64>,
pub last_part: Option<u64>,
}
impl RenditionReport {
pub(crate) fn from_hashmap(
mut attrs: HashMap<String, QuotedOrUnquoted>,
) -> Result<RenditionReport, String> {
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::<u64>()
.map_err(|err| format!("Failed to parse LAST-MSN attribute: {}", err)));
let last_part = unquoted_string_parse!(attrs, "LAST-PART", |s: &str| s
.parse::<u64>()
.map_err(|err| format!("Failed to parse LAST-PART attribute: {}", err)));
Ok(RenditionReport {
uri,
last_msn,
last_part,
})
}
pub(crate) fn write_to<T: Write>(&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
// -----------------------------------------------------------------------------------------------

View file

@ -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);
}