mirror of
https://github.com/rutgersc/m3u8-rs.git
synced 2024-12-22 14:36:28 +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,
|
||||
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::*;
|
||||
|
|
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 {
|
||||
($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
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
|
121
tests/lib.rs
121
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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue