mirror of
https://github.com/rutgersc/m3u8-rs.git
synced 2024-12-22 22:46:37 +00:00
Added low-latency elements
Co-authored-by: Jendrik Weise <jewe37@gmail.com>
This commit is contained in:
parent
381ac7732f
commit
e4004a142e
3 changed files with 529 additions and 5 deletions
|
@ -352,6 +352,12 @@ enum MediaPlaylistTag {
|
||||||
IFramesOnly,
|
IFramesOnly,
|
||||||
Start(Start),
|
Start(Start),
|
||||||
IndependentSegments,
|
IndependentSegments,
|
||||||
|
|
||||||
|
ServerControl(ServerControl),
|
||||||
|
PartInf(PartInf),
|
||||||
|
Skip(Skip),
|
||||||
|
PreloadHint(PreloadHint),
|
||||||
|
RenditionReport(RenditionReport),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn media_playlist_tag(i: &[u8]) -> IResult<&[u8], MediaPlaylistTag> {
|
fn media_playlist_tag(i: &[u8]) -> IResult<&[u8], MediaPlaylistTag> {
|
||||||
|
@ -384,6 +390,11 @@ fn media_playlist_tag(i: &[u8]) -> IResult<&[u8], MediaPlaylistTag> {
|
||||||
MediaPlaylistTag::IndependentSegments
|
MediaPlaylistTag::IndependentSegments
|
||||||
}),
|
}),
|
||||||
map(tag("#EXT-X-ENDLIST"), |_| MediaPlaylistTag::EndList),
|
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),
|
map(media_segment_tag, MediaPlaylistTag::Segment),
|
||||||
))(i)
|
))(i)
|
||||||
}
|
}
|
||||||
|
@ -423,6 +434,21 @@ fn media_playlist_from_tags(mut tags: Vec<MediaPlaylistTag>) -> MediaPlaylist {
|
||||||
MediaPlaylistTag::IndependentSegments => {
|
MediaPlaylistTag::IndependentSegments => {
|
||||||
media_playlist.independent_segments = true;
|
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 {
|
MediaPlaylistTag::Segment(segment_tag) => match segment_tag {
|
||||||
SegmentTag::Extinf(d, t) => {
|
SegmentTag::Extinf(d, t) => {
|
||||||
next_segment.duration = d;
|
next_segment.duration = d;
|
||||||
|
@ -446,9 +472,6 @@ fn media_playlist_from_tags(mut tags: Vec<MediaPlaylistTag>) -> MediaPlaylist {
|
||||||
SegmentTag::DateRange(d) => {
|
SegmentTag::DateRange(d) => {
|
||||||
next_segment.daterange = Some(d);
|
next_segment.daterange = Some(d);
|
||||||
}
|
}
|
||||||
SegmentTag::Unknown(t) => {
|
|
||||||
next_segment.unknown_tags.push(t);
|
|
||||||
}
|
|
||||||
SegmentTag::Uri(u) => {
|
SegmentTag::Uri(u) => {
|
||||||
next_segment.key = encryption_key.clone();
|
next_segment.key = encryption_key.clone();
|
||||||
next_segment.map = map.clone();
|
next_segment.map = map.clone();
|
||||||
|
@ -458,6 +481,12 @@ fn media_playlist_from_tags(mut tags: Vec<MediaPlaylistTag>) -> MediaPlaylist {
|
||||||
encryption_key = None;
|
encryption_key = None;
|
||||||
map = 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),
|
Unknown(ExtTag),
|
||||||
Comment(Option<String>),
|
Comment(Option<String>),
|
||||||
Uri(String),
|
Uri(String),
|
||||||
|
Part(Part),
|
||||||
}
|
}
|
||||||
|
|
||||||
fn media_segment_tag(i: &[u8]) -> IResult<&[u8], SegmentTag> {
|
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)| {
|
map(pair(tag("#EXT-X-DATERANGE:"), daterange), |(_, range)| {
|
||||||
SegmentTag::DateRange(range)
|
SegmentTag::DateRange(range)
|
||||||
}),
|
}),
|
||||||
|
map(part_tag, SegmentTag::Part), // Ensure part_tag is integrated here
|
||||||
map(ext_tag, SegmentTag::Unknown),
|
map(ext_tag, SegmentTag::Unknown),
|
||||||
map(comment_tag, SegmentTag::Comment),
|
map(comment_tag, SegmentTag::Comment),
|
||||||
map(consume_line, SegmentTag::Uri),
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
332
src/playlist.rs
332
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 {
|
macro_rules! write_some_attribute {
|
||||||
($w:expr, $tag:expr, $o:expr) => {
|
($w:expr, $tag:expr, $o:expr) => {
|
||||||
if let &Some(ref v) = $o {
|
if let &Some(ref v) = $o {
|
||||||
|
@ -736,6 +753,13 @@ pub struct MediaPlaylist {
|
||||||
pub independent_segments: bool,
|
pub independent_segments: bool,
|
||||||
/// Unknown tags before the first media segment
|
/// Unknown tags before the first media segment
|
||||||
pub unknown_tags: Vec<ExtTag>,
|
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 {
|
impl MediaPlaylist {
|
||||||
|
@ -748,6 +772,22 @@ impl MediaPlaylist {
|
||||||
if self.independent_segments {
|
if self.independent_segments {
|
||||||
writeln!(w, "#EXT-X-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)?;
|
writeln!(w, "#EXT-X-TARGETDURATION:{}", self.target_duration)?;
|
||||||
|
|
||||||
if self.media_sequence != 0 {
|
if self.media_sequence != 0 {
|
||||||
|
@ -776,6 +816,10 @@ impl MediaPlaylist {
|
||||||
writeln!(w, "#EXT-X-ENDLIST")?;
|
writeln!(w, "#EXT-X-ENDLIST")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for unknown_tag in &self.unknown_tags {
|
||||||
|
writeln!(w, "{}", unknown_tag)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -847,6 +891,9 @@ pub struct MediaSegment {
|
||||||
pub daterange: Option<DateRange>,
|
pub daterange: Option<DateRange>,
|
||||||
/// `#EXT-`
|
/// `#EXT-`
|
||||||
pub unknown_tags: Vec<ExtTag>,
|
pub unknown_tags: Vec<ExtTag>,
|
||||||
|
|
||||||
|
// LL-HLS specific fields
|
||||||
|
pub parts: Vec<Part>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MediaSegment {
|
impl MediaSegment {
|
||||||
|
@ -885,6 +932,9 @@ impl MediaSegment {
|
||||||
v.write_attributes_to(w)?;
|
v.write_attributes_to(w)?;
|
||||||
writeln!(w)?;
|
writeln!(w)?;
|
||||||
}
|
}
|
||||||
|
for part in &self.parts {
|
||||||
|
part.write_to(w)?;
|
||||||
|
}
|
||||||
for unknown_tag in &self.unknown_tags {
|
for unknown_tag in &self.unknown_tags {
|
||||||
writeln!(w, "{}", unknown_tag)?;
|
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)
|
/// [`#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
|
/// The EXT-X-DATERANGE tag associates a Date Range (i.e. a range of time
|
||||||
|
@ -1125,8 +1206,8 @@ impl DateRange {
|
||||||
",END-DATE",
|
",END-DATE",
|
||||||
&self.end_date.as_ref().map(|dt| dt.to_rfc3339())
|
&self.end_date.as_ref().map(|dt| dt.to_rfc3339())
|
||||||
)?;
|
)?;
|
||||||
write_some_attribute!(w, ",DURATION", &self.duration)?;
|
write_some_float_attribute!(w, ",DURATION", &self.duration)?;
|
||||||
write_some_attribute!(w, ",PLANNED-DURATION", &self.planned_duration)?;
|
write_some_float_attribute!(w, ",PLANNED-DURATION", &self.planned_duration)?;
|
||||||
if let Some(x_prefixed) = &self.x_prefixed {
|
if let Some(x_prefixed) = &self.x_prefixed {
|
||||||
for (name, attr) in x_prefixed {
|
for (name, attr) in x_prefixed {
|
||||||
write!(w, ",{}={}", name, attr)?;
|
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
|
// Rest
|
||||||
// -----------------------------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------------------------
|
||||||
|
|
121
tests/lib.rs
121
tests/lib.rs
|
@ -416,6 +416,12 @@ fn create_and_parse_media_playlist_full() {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}],
|
}],
|
||||||
unknown_tags: vec![],
|
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);
|
let playlist_parsed = print_create_and_parse_playlist(&mut playlist_original);
|
||||||
assert_eq!(playlist_original, playlist_parsed);
|
assert_eq!(playlist_original, playlist_parsed);
|
||||||
|
@ -470,3 +476,118 @@ fn parsing_binary_data_should_fail_cleanly() {
|
||||||
|
|
||||||
assert!(res.is_err());
|
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);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue