Use more specific types for playlist fields and improve parsing/writing

Added
* DateRange struct and implementations
* Parsing DateRange (wasn't parsed due to a typo)
* fn daterange() to parse DateRange
* `write_some_other_attributes` macro
* resolved `FIXME required unless None` for method and iv

Changed / fixed:
* trim_matches() potentially too greedy, replaced with strip_suffix/prefix
* removed a bunch of "Unnecessary qualification", eg `fmt::` for `Display`
* changed `fn extmap()` to use `QuotedOrUnquoted` and `other_attributes`
* `fmt` for `QuotedOrUnquoted` are now printing quoted strings in `"`
* `bool_default_false` macro renamed to `is_yes`
* qualified error message for `quoted_string_parse` and `unquoted_string_parse`
* `other_attributes` now `Option<>`
* `ClosedCaptionGroupId::Other(s)` variant processing added to `write_to`
* `HDCPLevel::Other(s)` variant processing added to `fmt`
* `AlternativeMediaType::Other(s)` variant processing added to `fmt`
* `InstreamId::Other(s)` variant processing added to `fmt`
* `MediaPlaylistType::Other(s)` variant processing added to `fmt`
* `KeyMethod::Other(s)` variant processing added to `fmt`
* included `DateRange` to tests
* included `other_attributes` to tests

Minor:
Typos corrections
rustfmt applied
This commit is contained in:
Vadim Getmanshchuk 2022-06-30 00:41:58 -07:00 committed by Sebastian Dröge
parent 6559e45b49
commit 7247e02ee5
3 changed files with 574 additions and 242 deletions

View file

@ -13,6 +13,7 @@ use nom::IResult;
use std::collections::HashMap; use std::collections::HashMap;
use std::f32; use std::f32;
use std::fmt; use std::fmt;
use std::fmt::Display;
use std::result::Result; use std::result::Result;
use std::str; use std::str;
use std::str::FromStr; use std::str::FromStr;
@ -134,7 +135,7 @@ pub fn is_master_playlist(input: &[u8]) -> bool {
/// ///
/// Returns `Some(true/false)` when a master/media tag is found. Otherwise returns `None`. /// Returns `Some(true/false)` when a master/media tag is found. Otherwise returns `None`.
/// ///
/// - None: Unkown tag or empty line /// - None: Unknown tag or empty line
/// - Some(true, tagstring): Line contains a master playlist tag /// - Some(true, tagstring): Line contains a master playlist tag
/// - Some(false, tagstring): Line contains a media playlist tag /// - Some(false, tagstring): Line contains a media playlist tag
fn contains_master_tag(input: &[u8]) -> Option<(bool, String)> { fn contains_master_tag(input: &[u8]) -> Option<(bool, String)> {
@ -299,7 +300,7 @@ fn variant_i_frame_stream_tag(i: &[u8]) -> IResult<&[u8], VariantStream> {
} }
fn alternative_media_tag(i: &[u8]) -> IResult<&[u8], AlternativeMedia> { fn alternative_media_tag(i: &[u8]) -> IResult<&[u8], AlternativeMedia> {
map(pair(tag("#EXT-X-MEDIA:"), key_value_pairs), |(_, media)| { map_res(pair(tag("#EXT-X-MEDIA:"), key_value_pairs), |(_, media)| {
AlternativeMedia::from_hashmap(media) AlternativeMedia::from_hashmap(media)
})(i) })(i)
} }
@ -484,7 +485,7 @@ enum SegmentTag {
Key(Key), Key(Key),
Map(Map), Map(Map),
ProgramDateTime(String), ProgramDateTime(String),
DateRange(String), DateRange(DateRange),
Unknown(ExtTag), Unknown(ExtTag),
Comment(String), Comment(String),
Uri(String), Uri(String),
@ -511,10 +512,9 @@ fn media_segment_tag(i: &[u8]) -> IResult<&[u8], SegmentTag> {
pair(tag("#EXT-X-PROGRAM-DATE-TIME:"), consume_line), pair(tag("#EXT-X-PROGRAM-DATE-TIME:"), consume_line),
|(_, line)| SegmentTag::ProgramDateTime(line), |(_, line)| SegmentTag::ProgramDateTime(line),
), ),
map( map(pair(tag("#EXT-X-DATERANGE:"), daterange), |(_, range)| {
pair(tag("#EXT-X-DATE-RANGE:"), consume_line), SegmentTag::DateRange(range)
|(_, line)| SegmentTag::DateRange(line), }),
),
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),
@ -535,23 +535,34 @@ fn duration_title_tag(i: &[u8]) -> IResult<&[u8], (f32, Option<String>)> {
} }
fn key(i: &[u8]) -> IResult<&[u8], Key> { fn key(i: &[u8]) -> IResult<&[u8], Key> {
map(key_value_pairs, Key::from_hashmap)(i) map_res(key_value_pairs, Key::from_hashmap)(i)
}
fn daterange(i: &[u8]) -> IResult<&[u8], DateRange> {
map_res(key_value_pairs, DateRange::from_hashmap)(i)
} }
fn extmap(i: &[u8]) -> IResult<&[u8], Map> { fn extmap(i: &[u8]) -> IResult<&[u8], Map> {
map_res(key_value_pairs, |attrs| -> Result<Map, &str> { map_res(key_value_pairs, |mut attrs| -> Result<Map, &str> {
let uri = attrs.get("URI").cloned().unwrap_or_default(); let uri = match attrs.remove("URI") {
Some(QuotedOrUnquoted::Quoted(s)) => Ok(s),
Some(QuotedOrUnquoted::Unquoted(_)) => {
Err("Can't create URI attribute from unquoted string")
}
None => Err("URI is empty"),
}?;
let byte_range = attrs let byte_range = attrs
.get("BYTERANGE") .remove("BYTERANGE")
.map(|range| match byte_range_val(range.to_string().as_bytes()) { .map(|range| match byte_range_val(range.to_string().as_bytes()) {
IResult::Ok((_, range)) => Ok(range), IResult::Ok((_, range)) => Ok(range),
IResult::Err(_) => Err("invalid byte range"), IResult::Err(_) => Err("Invalid byte range"),
}) })
.transpose()?; .transpose()?;
Ok(Map { Ok(Map {
uri: uri.to_string(), uri,
byte_range, byte_range,
other_attributes: attrs,
}) })
})(i) })(i)
} }
@ -572,7 +583,7 @@ fn version_tag(i: &[u8]) -> IResult<&[u8], usize> {
} }
fn start_tag(i: &[u8]) -> IResult<&[u8], Start> { fn start_tag(i: &[u8]) -> IResult<&[u8], Start> {
map( map_res(
pair(tag("#EXT-X-START:"), key_value_pairs), pair(tag("#EXT-X-START:"), key_value_pairs),
|(_, attributes)| Start::from_hashmap(attributes), |(_, attributes)| Start::from_hashmap(attributes),
)(i) )(i)
@ -654,22 +665,23 @@ impl QuotedOrUnquoted {
impl From<&str> for QuotedOrUnquoted { impl From<&str> for QuotedOrUnquoted {
fn from(s: &str) -> Self { fn from(s: &str) -> Self {
if s.starts_with('"') && s.ends_with('"') { if s.starts_with('"') && s.ends_with('"') {
return QuotedOrUnquoted::Quoted(s.trim_matches('"').to_string()); return QuotedOrUnquoted::Quoted(
s.strip_prefix('"')
.and_then(|s| s.strip_suffix('"'))
.unwrap_or_default()
.to_string(),
);
} }
QuotedOrUnquoted::Unquoted(s.to_string()) QuotedOrUnquoted::Unquoted(s.to_string())
} }
} }
impl fmt::Display for QuotedOrUnquoted { impl Display for QuotedOrUnquoted {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!( match self {
f, QuotedOrUnquoted::Unquoted(s) => write!(f, "{}", s),
"{}", QuotedOrUnquoted::Quoted(u) => write!(f, "\"{}\"", u),
match self { }
QuotedOrUnquoted::Unquoted(s) => s,
QuotedOrUnquoted::Quoted(u) => u,
}
)
} }
} }
@ -790,6 +802,7 @@ mod tests {
video: None, video: None,
subtitles: None, subtitles: None,
closed_captions: None, closed_captions: None,
other_attributes: Default::default(),
} }
)) ))
); );
@ -808,9 +821,9 @@ mod tests {
("BANDWIDTH", "395000"), ("BANDWIDTH", "395000"),
("CODECS", "\"avc1.4d001f,mp4a.40.2\"") ("CODECS", "\"avc1.4d001f,mp4a.40.2\"")
] ]
.into_iter() .into_iter()
.map(|(k, v)| (String::from(k), v.into())) .map(|(k, v)| (String::from(k), v.into()))
.collect::<HashMap<_, _>>(), .collect::<HashMap<_, _>>(),
)), )),
); );
} }
@ -828,8 +841,8 @@ mod tests {
("RESOLUTION", "\"1x1\""), ("RESOLUTION", "\"1x1\""),
("VIDEO", "1") ("VIDEO", "1")
].into_iter() ].into_iter()
.map(|(k, v)| (String::from(k), v.into())) .map(|(k, v)| (String::from(k), v.into()))
.collect::<HashMap<_,_>>() .collect::<HashMap<_,_>>()
)) ))
); );
} }
@ -844,9 +857,9 @@ mod tests {
("BANDWIDTH", "300000"), ("BANDWIDTH", "300000"),
("CODECS", "\"avc1.42c015,mp4a.40.2\"") ("CODECS", "\"avc1.42c015,mp4a.40.2\"")
] ]
.into_iter() .into_iter()
.map(|(k, v)| (String::from(k), v.into())) .map(|(k, v)| (String::from(k), v.into()))
.collect::<HashMap<_, _>>() .collect::<HashMap<_, _>>()
)) ))
); );
} }
@ -862,9 +875,9 @@ mod tests {
("RESOLUTION", "22x22"), ("RESOLUTION", "22x22"),
("VIDEO", "1") ("VIDEO", "1")
] ]
.into_iter() .into_iter()
.map(|(k, v)| (String::from(k), v.into())) .map(|(k, v)| (String::from(k), v.into()))
.collect::<HashMap<_, _>>() .collect::<HashMap<_, _>>()
)) ))
); );
} }

View file

@ -32,15 +32,110 @@ macro_rules! write_some_attribute {
}; };
} }
macro_rules! bool_default_false { macro_rules! write_some_other_attributes {
($optional:expr) => { ($w:expr, $attr:expr) => {
match $optional { if let &Some(ref attributes) = $attr {
Some(ref s) if s == "YES" => true, let mut status = std::io::Result::Ok(());
Some(_) | None => false, for (name, val) in attributes {
let res = write!($w, ",{}={}", name, val);
if res.is_err() {
status = res;
break;
}
}
status
} else {
Ok(())
} }
}; };
} }
macro_rules! is_yes {
($attrs:expr, $attr:expr) => {
match $attrs.remove($attr) {
Some(QuotedOrUnquoted::Unquoted(ref s)) if s == "YES" => true,
Some(QuotedOrUnquoted::Unquoted(ref s)) if s == "NO" => false,
Some(ref s) => {
return Err(format!(
"Can't create bool from {} for {} attribute",
s, $attr
))
}
None => false,
}
};
}
macro_rules! quoted_string {
($attrs:expr, $attr:expr) => {
match $attrs.remove($attr) {
Some(QuotedOrUnquoted::Quoted(s)) => Some(s),
Some(QuotedOrUnquoted::Unquoted(_)) => {
return Err(format!(
"Can't create {} attribute from unquoted string",
$attr
))
}
None => None,
}
};
}
macro_rules! unquoted_string {
($attrs:expr, $attr:expr) => {
match $attrs.remove($attr) {
Some(QuotedOrUnquoted::Unquoted(s)) => Some(s),
Some(QuotedOrUnquoted::Quoted(_)) => {
return Err(format!(
"Can't create {} attribute from quoted string",
$attr
))
}
None => None,
}
};
}
macro_rules! unquoted_string_parse {
($attrs:expr, $attr:expr, $parse:expr) => {
match $attrs.remove($attr) {
Some(QuotedOrUnquoted::Unquoted(s)) => Some(
($parse(s.as_str())).map_err(|_| format!("Can't create attribute {}", $attr))?,
),
Some(QuotedOrUnquoted::Quoted(_)) => {
return Err(format!(
"Can't create {} attribute from quoted string",
$attr
))
}
None => None,
}
};
($attrs:expr, $attr:expr) => {
unquoted_string_parse!($attrs, $attr, |s: &str| s.parse())
};
}
macro_rules! quoted_string_parse {
($attrs:expr, $attr:expr, $parse:expr) => {
match $attrs.remove($attr) {
Some(QuotedOrUnquoted::Quoted(s)) => Some(
($parse(s.as_str())).map_err(|_| format!("Can't create attribute {}", $attr))?,
),
Some(QuotedOrUnquoted::Unquoted(_)) => {
return Err(format!(
"Can't create {} attribute from unquoted string",
$attr
))
}
None => None,
}
};
($attrs:expr, $attr:expr) => {
quoted_string_parse!($attrs, $attr, |s: &str| s.parse())
};
}
/// [Playlist](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.1), /// [Playlist](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.1),
/// can either be a `MasterPlaylist` or a `MediaPlaylist`. /// can either be a `MasterPlaylist` or a `MediaPlaylist`.
/// ///
@ -152,109 +247,37 @@ pub struct VariantStream {
pub subtitles: Option<String>, pub subtitles: Option<String>,
pub closed_captions: Option<ClosedCaptionGroupId>, pub closed_captions: Option<ClosedCaptionGroupId>,
// PROGRAM-ID tag was removed in protocol version 6 // PROGRAM-ID tag was removed in protocol version 6
pub other_attributes: Option<HashMap<String, QuotedOrUnquoted>>,
} }
impl VariantStream { impl VariantStream {
pub fn from_hashmap( pub(crate) fn from_hashmap(
mut attrs: HashMap<String, QuotedOrUnquoted>, mut attrs: HashMap<String, QuotedOrUnquoted>,
is_i_frame: bool, is_i_frame: bool,
) -> Result<VariantStream, String> { ) -> Result<VariantStream, String> {
let uri = attrs let uri = quoted_string!(attrs, "URI").unwrap_or_default();
.remove("URI") // TODO: keep in attrs if parsing optional attributes fails
.map(|c| { let bandwidth = unquoted_string_parse!(attrs, "BANDWIDTH", |s: &str| s
c.as_quoted() .parse::<u64>()
.ok_or_else(|| format!("URI attribute is an unquoted string")) .map_err(|err| format!("Failed to parse BANDWIDTH attribute: {}", err)))
.map(|s| s.to_string()) .ok_or_else(|| String::from("EXT-X-STREAM-INF without mandatory BANDWIDTH attribute"))?;
}) let average_bandwidth = unquoted_string_parse!(attrs, "AVERAGE-BANDWIDTH", |s: &str| s
.transpose()? .parse::<u64>()
.unwrap_or_default(); .map_err(|err| format!("Failed to parse AVERAGE-BANDWIDTH: {}", err)));
let bandwidth = attrs let codecs = quoted_string!(attrs, "CODECS");
.remove("BANDWIDTH") let resolution = unquoted_string_parse!(attrs, "RESOLUTION");
.ok_or_else(|| String::from("Mandatory bandwidth attribute not included")) let frame_rate = unquoted_string_parse!(attrs, "FRAME-RATE", |s: &str| s
.and_then(|s| { .parse::<f64>()
s.as_unquoted() .map_err(|err| format!("Failed to parse FRAME-RATE attribute: {}", err)));
.ok_or_else(|| String::from("Bandwidth attribute is a quoted string")) let hdcp_level = unquoted_string_parse!(attrs, "HDCP-LEVEL");
.and_then(|s| { let audio = quoted_string!(attrs, "AUDIO");
s.trim() let video = quoted_string!(attrs, "VIDEO");
.parse::<u64>() let subtitles = quoted_string!(attrs, "SUBTITLES");
.map_err(|err| format!("Failed to parse bandwidth attribute: {}", err))
})
})?;
let average_bandwidth = attrs
.remove("AVERAGE-BANDWIDTH")
.map(|s| {
s.as_unquoted()
.ok_or_else(|| String::from("Average bandwidth attribute is a quoted string"))
.and_then(|s| {
s.trim().parse::<u64>().map_err(|err| {
format!("Failed to parse average bandwidth attribute: {}", err)
})
})
})
.transpose()?;
let codecs = attrs
.remove("CODECS")
.map(|c| {
c.as_quoted()
.ok_or_else(|| format!("Codecs attribute is an unquoted string"))
.map(|s| s.to_string())
})
.transpose()?;
let resolution = attrs
.remove("RESOLUTION")
.map(|r| {
r.as_unquoted()
.ok_or_else(|| format!("Resolution attribute is a quoted string"))
.and_then(|s| s.parse::<Resolution>())
})
.transpose()?;
let frame_rate = attrs
.remove("FRAME-RATE")
.map(|f| {
f.as_unquoted()
.ok_or_else(|| format!("Framerate attribute is a quoted string"))
.and_then(|s| {
s.parse::<f64>()
.map_err(|err| format!("Failed to parse framerate: {}", err))
})
})
.transpose()?;
let hdcp_level = attrs
.remove("HDCP-LEVEL")
.map(|r| {
r.as_unquoted()
.ok_or_else(|| format!("HDCP level attribute is a quoted string"))
.and_then(|s| s.parse::<HDCPLevel>())
})
.transpose()?;
let audio = attrs
.remove("AUDIO")
.map(|c| {
c.as_quoted()
.ok_or_else(|| format!("Audio attribute is an unquoted string"))
.map(|s| s.to_string())
})
.transpose()?;
let video = attrs
.remove("VIDEO")
.map(|c| {
c.as_quoted()
.ok_or_else(|| format!("Video attribute is an unquoted string"))
.map(|s| s.to_string())
})
.transpose()?;
let subtitles = attrs
.remove("SUBTITLES")
.map(|c| {
c.as_quoted()
.ok_or_else(|| format!("Subtitles attribute is an unquoted string"))
.map(|s| s.to_string())
})
.transpose()?;
let closed_captions = attrs let closed_captions = attrs
.remove("CLOSED-CAPTIONS") .remove("CLOSED-CAPTIONS")
.map(|c| c.try_into()) .map(|c| c.try_into())
.transpose()?; .transpose()?;
let other_attributes = if attrs.is_empty() { None } else { Some(attrs) };
Ok(VariantStream { Ok(VariantStream {
is_i_frame, is_i_frame,
@ -269,10 +292,11 @@ impl VariantStream {
video, video,
subtitles, subtitles,
closed_captions, closed_captions,
other_attributes,
}) })
} }
pub fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> { pub(crate) fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
if self.is_i_frame { if self.is_i_frame {
write!(w, "#EXT-X-I-FRAME-STREAM-INF:")?; write!(w, "#EXT-X-I-FRAME-STREAM-INF:")?;
self.write_stream_inf_common_attributes(w)?; self.write_stream_inf_common_attributes(w)?;
@ -286,6 +310,7 @@ impl VariantStream {
match closed_captions { match closed_captions {
ClosedCaptionGroupId::None => write!(w, ",CLOSED-CAPTIONS=NONE")?, ClosedCaptionGroupId::None => write!(w, ",CLOSED-CAPTIONS=NONE")?,
ClosedCaptionGroupId::GroupId(s) => write!(w, ",CLOSED-CAPTIONS=\"{}\"", s)?, ClosedCaptionGroupId::GroupId(s) => write!(w, ",CLOSED-CAPTIONS=\"{}\"", s)?,
ClosedCaptionGroupId::Other(s) => write!(w, ",CLOSED-CAPTIONS={}", s)?,
} }
} }
writeln!(w)?; writeln!(w)?;
@ -300,7 +325,9 @@ impl VariantStream {
write_some_attribute!(w, ",RESOLUTION", &self.resolution)?; write_some_attribute!(w, ",RESOLUTION", &self.resolution)?;
write_some_attribute!(w, ",FRAME-RATE", &self.frame_rate)?; write_some_attribute!(w, ",FRAME-RATE", &self.frame_rate)?;
write_some_attribute!(w, ",HDCP-LEVEL", &self.hdcp_level)?; write_some_attribute!(w, ",HDCP-LEVEL", &self.hdcp_level)?;
write_some_attribute_quoted!(w, ",VIDEO", &self.video) write_some_attribute_quoted!(w, ",VIDEO", &self.video)?;
write_some_other_attributes!(w, &self.other_attributes)?;
Ok(())
} }
} }
@ -310,7 +337,7 @@ pub struct Resolution {
pub height: u64, pub height: u64,
} }
impl fmt::Display for Resolution { impl Display for Resolution {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}x{}", self.width, self.height) write!(f, "{}x{}", self.width, self.height)
} }
@ -324,22 +351,23 @@ impl FromStr for Resolution {
Some((width, height)) => { Some((width, height)) => {
let width = width let width = width
.parse::<u64>() .parse::<u64>()
.map_err(|err| format!("Can't parse resolution attribute: {}", err))?; .map_err(|err| format!("Can't parse RESOLUTION attribute width: {}", err))?;
let height = height let height = height
.parse::<u64>() .parse::<u64>()
.map_err(|err| format!("Can't parse resolution attribute: {}", err))?; .map_err(|err| format!("Can't parse RESOLUTION attribute height: {}", err))?;
Ok(Resolution { width, height }) Ok(Resolution { width, height })
} }
None => Err(String::from("Invalid resolution attribute")), None => Err(String::from("Invalid RESOLUTION attribute")),
} }
} }
} }
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
pub enum HDCPLevel { pub enum HDCPLevel {
Type0, Type0,
Type1, Type1,
None, None,
Other(String),
} }
impl FromStr for HDCPLevel { impl FromStr for HDCPLevel {
@ -355,24 +383,27 @@ impl FromStr for HDCPLevel {
} }
} }
impl fmt::Display for HDCPLevel { impl Display for HDCPLevel {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!( write!(
f, f,
"{}", "{}",
match *self { match self {
HDCPLevel::Type0 => "TYPE-0", HDCPLevel::Type0 => "TYPE-0",
HDCPLevel::Type1 => "TYPE-1", HDCPLevel::Type1 => "TYPE-1",
HDCPLevel::None => "NONE", HDCPLevel::None => "NONE",
HDCPLevel::Other(s) => s,
} }
) )
} }
} }
/// TODO docs
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
pub enum ClosedCaptionGroupId { pub enum ClosedCaptionGroupId {
None, None,
GroupId(String), GroupId(String),
Other(String),
} }
impl TryFrom<QuotedOrUnquoted> for ClosedCaptionGroupId { impl TryFrom<QuotedOrUnquoted> for ClosedCaptionGroupId {
@ -410,36 +441,79 @@ pub struct AlternativeMedia {
pub default: bool, // Its absence indicates an implicit value of NO pub default: bool, // Its absence indicates an implicit value of NO
pub autoselect: bool, // Its absence indicates an implicit value of NO pub autoselect: bool, // Its absence indicates an implicit value of NO
pub forced: bool, // Its absence indicates an implicit value of NO pub forced: bool, // Its absence indicates an implicit value of NO
pub instream_id: Option<String>, pub instream_id: Option<InstreamId>,
pub characteristics: Option<String>, pub characteristics: Option<String>,
pub channels: Option<String>, pub channels: Option<String>,
pub other_attributes: Option<HashMap<String, QuotedOrUnquoted>>,
} }
impl AlternativeMedia { impl AlternativeMedia {
pub fn from_hashmap(mut attrs: HashMap<String, QuotedOrUnquoted>) -> AlternativeMedia { pub(crate) fn from_hashmap(
AlternativeMedia { mut attrs: HashMap<String, QuotedOrUnquoted>,
media_type: attrs ) -> Result<AlternativeMedia, String> {
.get("TYPE") let media_type = unquoted_string_parse!(attrs, "TYPE")
.and_then(|s| AlternativeMediaType::from_str(s.to_string().as_str()).ok()) .ok_or_else(|| String::from("EXT-X-MEDIA without mandatory TYPE attribute"))?;
.unwrap_or_default(), let uri = quoted_string!(attrs, "URI");
uri: attrs.remove("URI").map(|u| u.to_string()),
group_id: attrs.remove("GROUP-ID").unwrap_or_default().to_string(), if media_type == AlternativeMediaType::ClosedCaptions && uri.is_some() {
language: attrs.remove("LANGUAGE").map(|l| l.to_string()), return Err(String::from(
assoc_language: attrs.remove("ASSOC-LANGUAGE").map(|a| a.to_string()), "URI attribute must not be included in CLOSED-CAPTIONS Alternative Medias",
name: attrs.remove("NAME").unwrap_or_default().to_string(), ));
default: bool_default_false!(attrs.remove("DEFAULT").map(|s| s.to_string())),
autoselect: bool_default_false!(attrs.remove("AUTOSELECT").map(|s| s.to_string())),
forced: bool_default_false!(attrs.remove("FORCED").map(|f| f.to_string())),
instream_id: attrs.remove("INSTREAM-ID").map(|i| i.to_string()),
characteristics: attrs.remove("CHARACTERISTICS").map(|c| c.to_string()),
channels: attrs.remove("CHANNELS").map(|c| c.to_string()),
} }
let group_id = quoted_string!(attrs, "GROUP-ID")
.ok_or_else(|| String::from("EXT-X-MEDIA without mandatory GROUP-ID attribute"))?;
let language = quoted_string!(attrs, "LANGUAGE");
let assoc_language = quoted_string!(attrs, "ASSOC-LANGUAGE");
let name = quoted_string!(attrs, "NAME")
.ok_or_else(|| String::from("EXT-X-MEDIA without mandatory NAME attribute"))?;
let default = is_yes!(attrs, "DEFAULT");
let autoselect = is_yes!(attrs, "AUTOSELECT");
if media_type != AlternativeMediaType::Subtitles && attrs.contains_key("FORCED") {
return Err(String::from(
"FORCED attribute must not be included in non-SUBTITLE Alternative Medias",
));
}
let forced = is_yes!(attrs, "FORCED");
if media_type != AlternativeMediaType::ClosedCaptions && attrs.contains_key("INSTREAM-ID") {
return Err(String::from("INSTREAM-ID attribute must not be included in non-CLOSED-CAPTIONS Alternative Medias"));
} else if media_type == AlternativeMediaType::ClosedCaptions
&& !attrs.contains_key("INSTREAM-ID")
{
return Err(String::from(
"INSTREAM-ID attribute must be included in CLOSED-CAPTIONS Alternative Medias",
));
}
let instream_id = quoted_string_parse!(attrs, "INSTREAM-ID");
let characteristics = quoted_string!(attrs, "CHARACTERISTICS");
let channels = quoted_string!(attrs, "CHANNELS");
let other_attributes = if attrs.is_empty() { None } else { Some(attrs) };
Ok(AlternativeMedia {
media_type,
uri,
group_id,
language,
assoc_language,
name,
default,
autoselect,
forced,
instream_id,
characteristics,
channels,
other_attributes,
})
} }
pub fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> { pub(crate) fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
write!(w, "#EXT-X-MEDIA:")?; write!(w, "#EXT-X-MEDIA:")?;
write!(w, "TYPE={}", self.media_type)?; write!(w, "TYPE={}", self.media_type)?;
write_some_attribute_quoted!(w, ",URI", &self.uri)?; if self.media_type != AlternativeMediaType::ClosedCaptions {
write_some_attribute_quoted!(w, ",URI", &self.uri)?;
}
write!(w, ",GROUP-ID=\"{}\"", self.group_id)?; write!(w, ",GROUP-ID=\"{}\"", self.group_id)?;
write_some_attribute_quoted!(w, ",LANGUAGE", &self.language)?; write_some_attribute_quoted!(w, ",LANGUAGE", &self.language)?;
write_some_attribute_quoted!(w, ",ASSOC-LANGUAGE", &self.assoc_language)?; write_some_attribute_quoted!(w, ",ASSOC-LANGUAGE", &self.assoc_language)?;
@ -450,22 +524,27 @@ impl AlternativeMedia {
if self.autoselect { if self.autoselect {
write!(w, ",AUTOSELECT=YES")?; write!(w, ",AUTOSELECT=YES")?;
} }
if self.forced { if self.forced && self.media_type == AlternativeMediaType::Subtitles {
write!(w, ",FORCED=YES")?; write!(w, ",FORCED=YES")?;
} }
write_some_attribute_quoted!(w, ",INSTREAM-ID", &self.instream_id)?; if self.media_type == AlternativeMediaType::ClosedCaptions {
// FIXME: Mandatory for closed captions
write_some_attribute_quoted!(w, ",INSTREAM-ID", &self.instream_id)?;
}
write_some_attribute_quoted!(w, ",CHARACTERISTICS", &self.characteristics)?; write_some_attribute_quoted!(w, ",CHARACTERISTICS", &self.characteristics)?;
write_some_attribute_quoted!(w, ",CHANNELS", &self.channels)?; write_some_attribute_quoted!(w, ",CHANNELS", &self.channels)?;
write_some_other_attributes!(w, &self.other_attributes)?;
writeln!(w) writeln!(w)
} }
} }
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
pub enum AlternativeMediaType { pub enum AlternativeMediaType {
Audio, Audio,
Video, Video,
Subtitles, Subtitles,
ClosedCaptions, ClosedCaptions,
Other(String),
} }
impl FromStr for AlternativeMediaType { impl FromStr for AlternativeMediaType {
@ -491,21 +570,59 @@ impl Default for AlternativeMediaType {
} }
} }
impl fmt::Display for AlternativeMediaType { impl Display for AlternativeMediaType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!( write!(
f, f,
"{}", "{}",
match *self { match self {
AlternativeMediaType::Audio => "AUDIO", AlternativeMediaType::Audio => "AUDIO",
AlternativeMediaType::Video => "VIDEO", AlternativeMediaType::Video => "VIDEO",
AlternativeMediaType::Subtitles => "SUBTITLES", AlternativeMediaType::Subtitles => "SUBTITLES",
AlternativeMediaType::ClosedCaptions => "CLOSED-CAPTIONS", AlternativeMediaType::ClosedCaptions => "CLOSED-CAPTIONS",
AlternativeMediaType::Other(s) => s.as_str(),
} }
) )
} }
} }
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
pub enum InstreamId {
CC(u8),
Service(u8),
Other(String),
}
impl FromStr for InstreamId {
type Err = String;
fn from_str(s: &str) -> Result<InstreamId, String> {
if let Some(cc) = s.strip_prefix("CC") {
let cc = cc
.parse::<u8>()
.map_err(|err| format!("Unable to create InstreamId from {:?}: {}", s, err))?;
Ok(InstreamId::CC(cc))
} else if let Some(service) = s.strip_prefix("SERVICE") {
let service = service
.parse::<u8>()
.map_err(|err| format!("Unable to create InstreamId from {:?}: {}", s, err))?;
Ok(InstreamId::Service(service))
} else {
Err(format!("Unable to create InstreamId from {:?}", s))
}
}
}
impl Display for InstreamId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
InstreamId::CC(cc) => write!(f, "CC{}", cc),
InstreamId::Service(service) => write!(f, "SERVICE{}", service),
InstreamId::Other(s) => write!(f, "{}", s),
}
}
}
/// [`#EXT-X-SESSION-KEY:<attribute-list>`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.5) /// [`#EXT-X-SESSION-KEY:<attribute-list>`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.5)
/// The EXT-X-SESSION-KEY tag allows encryption keys from Media Playlists /// The EXT-X-SESSION-KEY tag allows encryption keys from Media Playlists
/// to be specified in a Master Playlist. This allows the client to /// to be specified in a Master Playlist. This allows the client to
@ -514,7 +631,7 @@ impl fmt::Display for AlternativeMediaType {
pub struct SessionKey(pub Key); pub struct SessionKey(pub Key);
impl SessionKey { impl SessionKey {
pub fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> { pub(crate) fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
write!(w, "#EXT-X-SESSION-KEY:")?; write!(w, "#EXT-X-SESSION-KEY:")?;
self.0.write_attributes_to(w)?; self.0.write_attributes_to(w)?;
writeln!(w) writeln!(w)
@ -535,19 +652,18 @@ pub struct SessionData {
pub data_id: String, pub data_id: String,
pub field: SessionDataField, pub field: SessionDataField,
pub language: Option<String>, pub language: Option<String>,
pub other_attributes: Option<HashMap<String, QuotedOrUnquoted>>,
} }
impl SessionData { impl SessionData {
pub fn from_hashmap( pub(crate) fn from_hashmap(
mut attrs: HashMap<String, QuotedOrUnquoted>, mut attrs: HashMap<String, QuotedOrUnquoted>,
) -> Result<SessionData, String> { ) -> Result<SessionData, String> {
let data_id = match attrs.remove("DATA-ID") { let data_id = quoted_string!(attrs, "DATA-ID")
Some(data_id) => data_id, .ok_or_else(|| String::from("EXT-X-SESSION-DATA field without DATA-ID attribute"))?;
None => return Err("EXT-X-SESSION-DATA field without DATA-ID".to_string()),
};
let value = attrs.remove("VALUE").map(|v| v.to_string()); let value = quoted_string!(attrs, "VALUE");
let uri = attrs.remove("URI").map(|u| u.to_string()); let uri = quoted_string!(attrs, "URI");
// SessionData must contain either a VALUE or a URI, // SessionData must contain either a VALUE or a URI,
// but not both https://tools.ietf.org/html/rfc8216#section-4.3.4.4 // but not both https://tools.ietf.org/html/rfc8216#section-4.3.4.4
@ -556,26 +672,30 @@ impl SessionData {
(None, Some(uri)) => SessionDataField::Uri(uri), (None, Some(uri)) => SessionDataField::Uri(uri),
(Some(_), Some(_)) => { (Some(_), Some(_)) => {
return Err(format![ return Err(format![
"EXT-X-SESSION-DATA tag {} contains both a value and a uri", "EXT-X-SESSION-DATA tag {} contains both a value and an URI",
data_id data_id
]) ])
} }
(None, None) => { (None, None) => {
return Err(format![ return Err(format![
"EXT-X-SESSION-DATA tag {} must contain either a value or a uri", "EXT-X-SESSION-DATA tag {} must contain either a value or an URI",
data_id data_id
]) ])
} }
}; };
let language = quoted_string!(attrs, "LANGUAGE");
let other_attributes = if attrs.is_empty() { None } else { Some(attrs) };
Ok(SessionData { Ok(SessionData {
data_id: data_id.to_string(), data_id,
field, field,
language: attrs.remove("LANGUAGE").map(|s| s.to_string()), language,
other_attributes,
}) })
} }
pub fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> { pub(crate) fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
write!(w, "#EXT-X-SESSION-DATA:")?; write!(w, "#EXT-X-SESSION-DATA:")?;
write!(w, "DATA-ID=\"{}\"", self.data_id)?; write!(w, "DATA-ID=\"{}\"", self.data_id)?;
match &self.field { match &self.field {
@ -583,6 +703,7 @@ impl SessionData {
SessionDataField::Uri(uri) => write!(w, ",URI=\"{}\"", uri)?, SessionDataField::Uri(uri) => write!(w, ",URI=\"{}\"", uri)?,
}; };
write_some_attribute_quoted!(w, ",LANGUAGE", &self.language)?; write_some_attribute_quoted!(w, ",LANGUAGE", &self.language)?;
write_some_other_attributes!(w, &self.other_attributes)?;
writeln!(w) writeln!(w)
} }
} }
@ -614,6 +735,8 @@ pub struct MediaPlaylist {
pub start: Option<Start>, pub start: Option<Start>,
/// `#EXT-X-INDEPENDENT-SEGMENTS` /// `#EXT-X-INDEPENDENT-SEGMENTS`
pub independent_segments: bool, pub independent_segments: bool,
/// Unknown tags before the first media segment
pub unknown_tags: Vec<ExtTag>,
} }
impl MediaPlaylist { impl MediaPlaylist {
@ -659,10 +782,11 @@ impl MediaPlaylist {
} }
/// [`#EXT-X-PLAYLIST-TYPE:<EVENT|VOD>`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.3.5) /// [`#EXT-X-PLAYLIST-TYPE:<EVENT|VOD>`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.3.5)
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
pub enum MediaPlaylistType { pub enum MediaPlaylistType {
Event, Event,
Vod, Vod,
Other(String),
} }
impl FromStr for MediaPlaylistType { impl FromStr for MediaPlaylistType {
@ -677,14 +801,15 @@ impl FromStr for MediaPlaylistType {
} }
} }
impl fmt::Display for MediaPlaylistType { impl Display for MediaPlaylistType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!( write!(
f, f,
"{}", "{}",
match *self { match self {
MediaPlaylistType::Event => "EVENT", MediaPlaylistType::Event => "EVENT",
MediaPlaylistType::Vod => "VOD", MediaPlaylistType::Vod => "VOD",
MediaPlaylistType::Other(s) => s,
} }
) )
} }
@ -720,7 +845,7 @@ pub struct MediaSegment {
/// `#EXT-X-PROGRAM-DATE-TIME:<YYYY-MM-DDThh:mm:ssZ>` /// `#EXT-X-PROGRAM-DATE-TIME:<YYYY-MM-DDThh:mm:ssZ>`
pub program_date_time: Option<String>, pub program_date_time: Option<String>,
/// `#EXT-X-DATERANGE:<attribute-list>` /// `#EXT-X-DATERANGE:<attribute-list>`
pub daterange: Option<String>, pub daterange: Option<DateRange>,
/// `#EXT-` /// `#EXT-`
pub unknown_tags: Vec<ExtTag>, pub unknown_tags: Vec<ExtTag>,
} }
@ -730,7 +855,7 @@ impl MediaSegment {
Default::default() Default::default()
} }
pub fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> { pub(crate) fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
if let Some(ref byte_range) = self.byte_range { if let Some(ref byte_range) = self.byte_range {
write!(w, "#EXT-X-BYTERANGE:")?; write!(w, "#EXT-X-BYTERANGE:")?;
byte_range.write_value_to(w)?; byte_range.write_value_to(w)?;
@ -753,7 +878,9 @@ impl MediaSegment {
writeln!(w, "#EXT-X-PROGRAM-DATE-TIME:{}", v)?; writeln!(w, "#EXT-X-PROGRAM-DATE-TIME:{}", v)?;
} }
if let Some(ref v) = self.daterange { if let Some(ref v) = self.daterange {
writeln!(w, "#EXT-X-DATERANGE:{}", v)?; write!(w, "#EXT-X-DATERANGE:")?;
v.write_attributes_to(w)?;
writeln!(w)?;
} }
for unknown_tag in &self.unknown_tags { for unknown_tag in &self.unknown_tags {
writeln!(w, "{}", unknown_tag)?; writeln!(w, "{}", unknown_tag)?;
@ -771,6 +898,48 @@ impl MediaSegment {
} }
} }
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
pub enum KeyMethod {
None,
AES128,
SampleAES,
Other(String),
}
impl Default for KeyMethod {
fn default() -> Self {
KeyMethod::None
}
}
impl FromStr for KeyMethod {
type Err = String;
fn from_str(s: &str) -> Result<KeyMethod, String> {
match s {
"NONE" => Ok(KeyMethod::None),
"AES-128" => Ok(KeyMethod::AES128),
"SAMPLE-AES" => Ok(KeyMethod::SampleAES),
_ => Err(format!("Unable to create KeyMethod from {:?}", s)),
}
}
}
impl Display for KeyMethod {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}",
match self {
KeyMethod::None => "NONE",
KeyMethod::AES128 => "AES-128",
KeyMethod::SampleAES => "SAMPLE-AES",
KeyMethod::Other(s) => s,
}
)
}
}
/// [`#EXT-X-KEY:<attribute-list>`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.2.4) /// [`#EXT-X-KEY:<attribute-list>`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.2.4)
/// ///
/// Media Segments MAY be encrypted. The EXT-X-KEY tag specifies how to /// Media Segments MAY be encrypted. The EXT-X-KEY tag specifies how to
@ -781,7 +950,7 @@ impl MediaSegment {
/// same Media Segment if they ultimately produce the same decryption key. /// same Media Segment if they ultimately produce the same decryption key.
#[derive(Debug, Default, PartialEq, Eq, Clone)] #[derive(Debug, Default, PartialEq, Eq, Clone)]
pub struct Key { pub struct Key {
pub method: String, pub method: KeyMethod,
pub uri: Option<String>, pub uri: Option<String>,
pub iv: Option<String>, pub iv: Option<String>,
pub keyformat: Option<String>, pub keyformat: Option<String>,
@ -789,22 +958,35 @@ pub struct Key {
} }
impl Key { impl Key {
pub fn from_hashmap(mut attrs: HashMap<String, QuotedOrUnquoted>) -> Key { pub(crate) fn from_hashmap(
Key { mut attrs: HashMap<String, QuotedOrUnquoted>,
method: attrs.remove("METHOD").unwrap_or_default().to_string(), ) -> Result<Key, String> {
uri: attrs.remove("URI").map(|u| u.to_string()), let method: KeyMethod = unquoted_string_parse!(attrs, "METHOD")
iv: attrs.remove("IV").map(|i| i.to_string()), .ok_or_else(|| String::from("EXT-X-KEY without mandatory METHOD attribute"))?;
keyformat: attrs.remove("KEYFORMAT").map(|k| k.to_string()),
keyformatversions: attrs.remove("KEYFORMATVERSIONS").map(|k| k.to_string()), let uri = quoted_string!(attrs, "URI");
let iv = unquoted_string!(attrs, "IV");
if method == KeyMethod::None && iv.is_none() {
return Err("IV is required unless METHOD is NONE".parse().unwrap());
} }
let keyformat = quoted_string!(attrs, "KEYFORMAT");
let keyformatversions = quoted_string!(attrs, "KEYFORMATVERSIONS");
Ok(Key {
method,
uri,
iv,
keyformat,
keyformatversions,
})
} }
pub fn write_attributes_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> { pub fn write_attributes_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
write!(w, "METHOD={}", self.method)?; write!(w, "METHOD={}", self.method)?;
write_some_attribute_quoted!(w, ",URI", &self.uri)?; write_some_attribute_quoted!(w, ",URI", &self.uri)?;
write_some_attribute!(w, ",IV", &self.iv)?; write_some_attribute!(w, ",IV", &self.iv)?;
write_some_attribute!(w, ",KEYFORMAT", &self.keyformat)?; write_some_attribute_quoted!(w, ",KEYFORMAT", &self.keyformat)?;
write_some_attribute!(w, ",KEYFORMATVERSIONS", &self.keyformatversions) write_some_attribute_quoted!(w, ",KEYFORMATVERSIONS", &self.keyformatversions)
} }
} }
@ -820,6 +1002,7 @@ impl Key {
pub struct Map { pub struct Map {
pub uri: String, pub uri: String,
pub byte_range: Option<ByteRange>, pub byte_range: Option<ByteRange>,
pub other_attributes: HashMap<String, QuotedOrUnquoted>,
} }
impl Map { impl Map {
@ -859,16 +1042,88 @@ impl ByteRange {
/// 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
/// defined by a starting and ending date) with a set of attribute / /// defined by a starting and ending date) with a set of attribute /
/// value pairs. /// value pairs.
#[derive(Debug, Default, PartialEq, Eq, Clone)] #[derive(Debug, Default, PartialEq, Clone)]
pub struct DateRange { pub struct DateRange {
pub id: String, pub id: String,
pub class: Option<String>, pub class: Option<String>,
pub start_date: String, pub start_date: String,
pub end_date: Option<String>, pub end_date: Option<String>,
pub duration: Option<String>, pub duration: Option<f64>,
pub planned_duration: Option<String>, pub planned_duration: Option<f64>,
pub x_prefixed: Option<String>, // X-<client-attribute> pub x_prefixed: Option<HashMap<String, QuotedOrUnquoted>>, // X-<client-attribute>
pub end_on_next: bool, pub end_on_next: bool,
pub other_attributes: Option<HashMap<String, QuotedOrUnquoted>>,
}
impl DateRange {
pub fn from_hashmap(mut attrs: HashMap<String, QuotedOrUnquoted>) -> Result<DateRange, String> {
let id = quoted_string!(attrs, "ID")
.ok_or_else(|| String::from("EXT-X-DATERANGE without mandatory ID attribute"))?;
let class = quoted_string!(attrs, "CLASS");
let start_date = quoted_string!(attrs, "START-DATE").ok_or_else(|| {
String::from("EXT-X-DATERANGE without mandatory START-DATE attribute")
})?;
let end_date = quoted_string!(attrs, "END-DATE");
let duration = unquoted_string_parse!(attrs, "DURATION", |s: &str| s
.parse::<f64>()
.map_err(|err| format!("Failed to parse DURATION attribute: {}", err)));
let planned_duration = unquoted_string_parse!(attrs, "PLANNED-DURATION", |s: &str| s
.parse::<f64>()
.map_err(|err| format!("Failed to parse PLANNED-DURATION attribute: {}", err)));
let end_on_next = is_yes!(attrs, "END-ON-NEXT");
let mut x_prefixed = HashMap::new();
let mut other_attributes = HashMap::new();
for (k, v) in attrs.into_iter() {
if k.starts_with("X-") {
x_prefixed.insert(k, v);
} else {
other_attributes.insert(k, v);
}
}
Ok(DateRange {
id,
class,
start_date,
end_date,
duration,
planned_duration,
x_prefixed: if x_prefixed.is_empty() {
None
} else {
Some(x_prefixed)
},
end_on_next,
other_attributes: if other_attributes.is_empty() {
None
} else {
Some(other_attributes)
},
})
}
pub fn write_attributes_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
write_some_attribute_quoted!(w, "ID", &Some(&self.id))?;
write_some_attribute_quoted!(w, ",CLASS", &self.class)?;
write_some_attribute_quoted!(w, ",START-DATE", &Some(&self.start_date))?;
write_some_attribute_quoted!(w, ",END-DATE", &self.end_date)?;
write_some_attribute!(w, ",DURATION", &self.duration)?;
write_some_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)?;
}
}
if self.end_on_next {
write!(w, ",END-ON-NEXT=YES")?;
}
if let Some(other_attributes) = &self.other_attributes {
for (name, attr) in other_attributes {
write!(w, ",{}={}", name, attr)?;
}
}
Ok(())
}
} }
// ----------------------------------------------------------------------------------------------- // -----------------------------------------------------------------------------------------------
@ -880,27 +1135,38 @@ pub struct DateRange {
/// The EXT-X-START tag indicates a preferred point at which to start /// The EXT-X-START tag indicates a preferred point at which to start
/// playing a Playlist. By default, clients SHOULD start playback at /// playing a Playlist. By default, clients SHOULD start playback at
/// this point when beginning a playback session. /// this point when beginning a playback session.
#[derive(Debug, Default, PartialEq, Eq, Clone)] #[derive(Debug, Default, PartialEq, Clone)]
pub struct Start { pub struct Start {
pub time_offset: String, pub time_offset: f64,
pub precise: Option<String>, pub precise: Option<bool>,
pub other_attributes: HashMap<String, QuotedOrUnquoted>,
} }
impl Start { impl Start {
pub fn from_hashmap(mut attrs: HashMap<String, QuotedOrUnquoted>) -> Start { pub(crate) fn from_hashmap(
Start { mut attrs: HashMap<String, QuotedOrUnquoted>,
time_offset: attrs.remove("TIME-OFFSET").unwrap_or_default().to_string(), ) -> Result<Start, String> {
precise: attrs let time_offset = unquoted_string_parse!(attrs, "TIME-OFFSET", |s: &str| s
.remove("PRECISE") .parse::<f64>()
.map(|a| a.to_string()) .map_err(|err| format!("Failed to parse TIME-OFFSET attribute: {}", err)))
.or_else(|| Some("NO".to_string())), .ok_or_else(|| String::from("EXT-X-START without mandatory TIME-OFFSET attribute"))?;
} Ok(Start {
time_offset,
precise: is_yes!(attrs, "PRECISE").into(),
other_attributes: attrs,
})
} }
pub fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> { pub(crate) fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
write!(w, "#EXT-X-START:TIME-OFFSET={}", self.time_offset)?; write!(w, "#EXT-X-START:TIME-OFFSET={}", self.time_offset)?;
write_some_attribute!(w, ",PRECISE", &self.precise)?; if let Some(precise) = self.precise {
writeln!(w) if precise {
write!(w, ",PRECISE=YES")?;
}
}
writeln!(w)?;
Ok(())
} }
} }
@ -912,7 +1178,7 @@ pub struct ExtTag {
} }
impl Display for ExtTag { impl Display for ExtTag {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "#EXT-{}", self.tag)?; write!(f, "#EXT-{}", self.tag)?;
if let Some(v) = &self.rest { if let Some(v) = &self.rest {
write!(f, ":{}", v)?; write!(f, ":{}", v)?;

View file

@ -1,15 +1,16 @@
#![allow(unused_variables, unused_imports, dead_code)] #![allow(unused_variables, unused_imports, dead_code)]
use m3u8_rs::QuotedOrUnquoted::Quoted;
use m3u8_rs::*; use m3u8_rs::*;
use nom::AsBytes; use nom::AsBytes;
use std::collections::HashMap; use std::collections::HashMap;
use std::fs;
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
use std::path; use std::path;
use std::{fs, io};
fn all_sample_m3u_playlists() -> Vec<path::PathBuf> { fn all_sample_m3u_playlists() -> Vec<path::PathBuf> {
let path: std::path::PathBuf = ["sample-playlists"].iter().collect(); let path: path::PathBuf = ["sample-playlists"].iter().collect();
fs::read_dir(path.to_str().unwrap()) fs::read_dir(path.to_str().unwrap())
.unwrap() .unwrap()
.filter_map(Result::ok) .filter_map(Result::ok)
@ -20,13 +21,13 @@ fn all_sample_m3u_playlists() -> Vec<path::PathBuf> {
fn getm3u(path: &str) -> String { fn getm3u(path: &str) -> String {
let mut buf = String::new(); let mut buf = String::new();
let mut file = fs::File::open(path).unwrap_or_else(|_| panic!("Can't find m3u8: {}", path)); let mut file = File::open(path).unwrap_or_else(|_| panic!("Can't find m3u8: {}", path));
let u = file.read_to_string(&mut buf).expect("Can't read file"); let u = file.read_to_string(&mut buf).expect("Can't read file");
buf buf
} }
fn get_sample_playlist(name: &str) -> String { fn get_sample_playlist(name: &str) -> String {
let path: std::path::PathBuf = ["sample-playlists", name].iter().collect(); let path: path::PathBuf = ["sample-playlists", name].iter().collect();
getm3u(path.to_str().unwrap()) getm3u(path.to_str().unwrap())
} }
@ -38,7 +39,7 @@ fn print_parse_playlist_test(playlist_name: &str) -> bool {
println!("Parsing playlist file: {:?}", playlist_name); println!("Parsing playlist file: {:?}", playlist_name);
let parsed = parse_playlist(input.as_bytes()); let parsed = parse_playlist(input.as_bytes());
if let Result::Ok((i, o)) = parsed { if let Ok((i, o)) = &parsed {
println!("{:?}", o); println!("{:?}", o);
true true
} else { } else {
@ -158,7 +159,7 @@ fn playlist_types() {
println!("{:?} = {:?}", path, is_master); println!("{:?} = {:?}", path, is_master);
assert!(path.to_lowercase().contains("master") == is_master); assert_eq!(path.to_lowercase().contains("master"), is_master);
} }
} }
@ -200,20 +201,53 @@ fn create_and_parse_master_playlist_empty() {
fn create_and_parse_master_playlist_full() { fn create_and_parse_master_playlist_full() {
let mut playlist_original = Playlist::MasterPlaylist(MasterPlaylist { let mut playlist_original = Playlist::MasterPlaylist(MasterPlaylist {
version: Some(6), version: Some(6),
alternatives: vec![AlternativeMedia { alternatives: vec![
media_type: AlternativeMediaType::Audio, AlternativeMedia {
uri: Some("alt-media-uri".into()), media_type: AlternativeMediaType::Audio,
group_id: "group-id".into(), uri: Some("alt-media-uri".into()),
language: Some("language".into()), group_id: "group-id".into(),
assoc_language: Some("assoc-language".into()), language: Some("language".into()),
name: "Xmedia".into(), assoc_language: Some("assoc-language".into()),
default: true, // Its absence indicates an implicit value of NO name: "Xmedia".into(),
autoselect: true, // Its absence indicates an implicit value of NO default: true, // Its absence indicates an implicit value of NO
forced: true, // Its absence indicates an implicit value of NO autoselect: true, // Its absence indicates an implicit value of NO
instream_id: Some("instream_id".into()), forced: false, // Its absence indicates an implicit value of NO
characteristics: Some("characteristics".into()), instream_id: None,
channels: Some("channels".into()), characteristics: Some("characteristics".into()),
}], channels: Some("channels".into()),
other_attributes: Default::default(),
},
AlternativeMedia {
media_type: AlternativeMediaType::Subtitles,
uri: Some("alt-media-uri".into()),
group_id: "group-id".into(),
language: Some("language".into()),
assoc_language: Some("assoc-language".into()),
name: "Xmedia".into(),
default: true, // Its absence indicates an implicit value of NO
autoselect: true, // Its absence indicates an implicit value of NO
forced: true, // Its absence indicates an implicit value of NO
instream_id: None,
characteristics: Some("characteristics".into()),
channels: Some("channels".into()),
other_attributes: Default::default(),
},
AlternativeMedia {
media_type: AlternativeMediaType::ClosedCaptions,
uri: None,
group_id: "group-id".into(),
language: Some("language".into()),
assoc_language: Some("assoc-language".into()),
name: "Xmedia".into(),
default: true, // Its absence indicates an implicit value of NO
autoselect: true, // Its absence indicates an implicit value of NO
forced: false, // Its absence indicates an implicit value of NO
instream_id: Some(InstreamId::CC(1)),
characteristics: Some("characteristics".into()),
channels: Some("channels".into()),
other_attributes: Default::default(),
},
],
variants: vec![VariantStream { variants: vec![VariantStream {
is_i_frame: false, is_i_frame: false,
uri: "masterplaylist-uri".into(), uri: "masterplaylist-uri".into(),
@ -230,22 +264,25 @@ fn create_and_parse_master_playlist_full() {
video: Some("video".into()), video: Some("video".into()),
subtitles: Some("subtitles".into()), subtitles: Some("subtitles".into()),
closed_captions: Some(ClosedCaptionGroupId::GroupId("closed_captions".into())), closed_captions: Some(ClosedCaptionGroupId::GroupId("closed_captions".into())),
other_attributes: Default::default(),
}], }],
session_data: vec![SessionData { session_data: vec![SessionData {
data_id: "****".into(), data_id: "****".into(),
field: SessionDataField::Value("%%%%".to_string()), field: SessionDataField::Value("%%%%".to_string()),
language: Some("SessionDataLanguage".into()), language: Some("SessionDataLanguage".into()),
other_attributes: Default::default(),
}], }],
session_key: vec![SessionKey(Key { session_key: vec![SessionKey(Key {
method: "AES-128".into(), method: KeyMethod::AES128,
uri: Some("https://secure.domain.com".into()), uri: Some("https://secure.domain.com".into()),
iv: Some("0xb059217aa2649ce170b734".into()), iv: Some("0xb059217aa2649ce170b734".into()),
keyformat: Some("xXkeyformatXx".into()), keyformat: Some("xXkeyformatXx".into()),
keyformatversions: Some("xXFormatVers".into()), keyformatversions: Some("xXFormatVers".into()),
})], })],
start: Some(Start { start: Some(Start {
time_offset: "123123123".into(), time_offset: "123123123".parse().unwrap(),
precise: Some("YES".into()), precise: Some(true),
other_attributes: Default::default(),
}), }),
independent_segments: true, independent_segments: true,
unknown_tags: vec![], unknown_tags: vec![],
@ -290,8 +327,9 @@ fn create_and_parse_media_playlist_full() {
playlist_type: Some(MediaPlaylistType::Vod), playlist_type: Some(MediaPlaylistType::Vod),
i_frames_only: true, i_frames_only: true,
start: Some(Start { start: Some(Start {
time_offset: "9999".into(), time_offset: "9999".parse().unwrap(),
precise: Some("YES".into()), precise: Some(true),
other_attributes: Default::default(),
}), }),
independent_segments: true, independent_segments: true,
segments: vec![MediaSegment { segments: vec![MediaSegment {
@ -304,7 +342,7 @@ fn create_and_parse_media_playlist_full() {
}), }),
discontinuity: true, discontinuity: true,
key: Some(Key { key: Some(Key {
method: "AES-128".into(), method: KeyMethod::None,
uri: Some("https://secure.domain.com".into()), uri: Some("https://secure.domain.com".into()),
iv: Some("0xb059217aa2649ce170b734".into()), iv: Some("0xb059217aa2649ce170b734".into()),
keyformat: Some("xXkeyformatXx".into()), keyformat: Some("xXkeyformatXx".into()),
@ -316,14 +354,29 @@ fn create_and_parse_media_playlist_full() {
length: 137116, length: 137116,
offset: Some(4559), offset: Some(4559),
}), }),
other_attributes: Default::default(),
}), }),
program_date_time: Some("broodlordinfestorgg".into()), program_date_time: Some("broodlordinfestorgg".into()),
daterange: None, daterange: Some(DateRange {
id: "9999".into(),
class: Some("class".into()),
start_date: "2018-08-22T21:54:00.079Z".into(),
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![ExtTag { unknown_tags: vec![ExtTag {
tag: "X-CUE-OUT".into(), tag: "X-CUE-OUT".into(),
rest: Some("DURATION=2.002".into()), rest: Some("DURATION=2.002".into()),
}], }],
}], }],
unknown_tags: vec![],
}); });
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);