1
0
Fork 0
mirror of https://github.com/sile/hls_m3u8.git synced 2024-11-22 07:10:59 +00:00

some improvements

This commit is contained in:
Luro02 2020-02-14 13:05:18 +01:00
parent 25f9691c75
commit 8cced1ac53
No known key found for this signature in database
GPG key ID: B66FD4F74501A9CF
16 changed files with 331 additions and 152 deletions

View file

@ -35,6 +35,6 @@ script:
after_success: | after_success: |
# this does require a -Z flag for Doctests, which is unstable! # this does require a -Z flag for Doctests, which is unstable!
if [[ "$TRAVIS_RUST_VERSION" == nightly ]]; then if [[ "$TRAVIS_RUST_VERSION" == nightly ]]; then
cargo tarpaulin --run-types Tests Doctests --out Xml cargo tarpaulin --ignore-panics --ignore-tests --run-types Tests Doctests --out Xml
bash <(curl -s https://codecov.io/bash) bash <(curl -s https://codecov.io/bash)
fi fi

View file

@ -1,7 +1,9 @@
use core::convert::TryFrom; use core::convert::TryFrom;
use core::fmt; use core::iter::FusedIterator;
use core::str::FromStr; use core::str::FromStr;
use derive_more::Display;
use crate::tags; use crate::tags;
use crate::Error; use crate::Error;
@ -33,16 +35,14 @@ impl<'a> Iterator for Lines<'a> {
} }
} }
impl<'a> FusedIterator for Lines<'a> {}
impl<'a> From<&'a str> for Lines<'a> { impl<'a> From<&'a str> for Lines<'a> {
fn from(buffer: &'a str) -> Self { fn from(buffer: &'a str) -> Self {
Self { Self {
lines: buffer.lines().filter_map(|line| { lines: buffer
if line.trim().is_empty() { .lines()
None .filter_map(|line| Some(line.trim()).filter(|v| !v.is_empty())),
} else {
Some(line.trim())
}
}),
} }
} }
} }
@ -55,7 +55,8 @@ pub(crate) enum Line<'a> {
} }
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq, Display)]
#[display(fmt = "{}")]
pub(crate) enum Tag<'a> { pub(crate) enum Tag<'a> {
ExtXVersion(tags::ExtXVersion), ExtXVersion(tags::ExtXVersion),
ExtInf(tags::ExtInf), ExtInf(tags::ExtInf),
@ -80,34 +81,6 @@ pub(crate) enum Tag<'a> {
Unknown(&'a str), Unknown(&'a str),
} }
impl<'a> fmt::Display for Tag<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self {
Self::ExtXVersion(value) => value.fmt(f),
Self::ExtInf(value) => value.fmt(f),
Self::ExtXByteRange(value) => value.fmt(f),
Self::ExtXDiscontinuity(value) => value.fmt(f),
Self::ExtXKey(value) => value.fmt(f),
Self::ExtXMap(value) => value.fmt(f),
Self::ExtXProgramDateTime(value) => value.fmt(f),
Self::ExtXDateRange(value) => value.fmt(f),
Self::ExtXTargetDuration(value) => value.fmt(f),
Self::ExtXMediaSequence(value) => value.fmt(f),
Self::ExtXDiscontinuitySequence(value) => value.fmt(f),
Self::ExtXEndList(value) => value.fmt(f),
Self::ExtXPlaylistType(value) => value.fmt(f),
Self::ExtXIFramesOnly(value) => value.fmt(f),
Self::ExtXMedia(value) => value.fmt(f),
Self::VariantStream(value) => value.fmt(f),
Self::ExtXSessionData(value) => value.fmt(f),
Self::ExtXSessionKey(value) => value.fmt(f),
Self::ExtXIndependentSegments(value) => value.fmt(f),
Self::ExtXStart(value) => value.fmt(f),
Self::Unknown(value) => value.fmt(f),
}
}
}
impl<'a> TryFrom<&'a str> for Tag<'a> { impl<'a> TryFrom<&'a str> for Tag<'a> {
type Error = Error; type Error = Error;

View file

@ -15,8 +15,9 @@ use crate::utils::tag;
use crate::{Error, RequiredVersion}; use crate::{Error, RequiredVersion};
/// The master playlist describes all of the available variants for your /// The master playlist describes all of the available variants for your
/// content. Each variant is a version of the stream at a particular bitrate /// content.
/// and is contained in a separate playlist. /// Each variant is a version of the stream at a particular bitrate and is
/// contained in a separate playlist.
#[derive(ShortHand, Debug, Clone, Builder, PartialEq)] #[derive(ShortHand, Debug, Clone, Builder, PartialEq)]
#[builder(build_fn(validate = "Self::validate"))] #[builder(build_fn(validate = "Self::validate"))]
#[builder(setter(into, strip_option))] #[builder(setter(into, strip_option))]
@ -67,21 +68,24 @@ pub struct MasterPlaylist {
/// This tag is optional. /// This tag is optional.
#[builder(default)] #[builder(default)]
variants: Vec<VariantStream>, variants: Vec<VariantStream>,
/// The [`ExtXSessionData`] tags of the playlist. /// The [`ExtXSessionData`] tag allows arbitrary session data to be
/// carried in a [`MasterPlaylist`].
/// ///
/// # Note /// # Note
/// ///
/// This tag is optional. /// This tag is optional.
#[builder(default)] #[builder(default)]
session_data: Vec<ExtXSessionData>, session_data: Vec<ExtXSessionData>,
/// The [`ExtXSessionKey`] tags of the playlist. /// This is a list of [`ExtXSessionKey`]s, that allows the client to preload
/// these keys without having to read the [`MediaPlaylist`]s first.
/// ///
/// # Note /// # Note
/// ///
/// This tag is optional. /// This tag is optional.
#[builder(default)] #[builder(default)]
session_keys: Vec<ExtXSessionKey>, session_keys: Vec<ExtXSessionKey>,
/// A list of tags that are unknown. /// This is a list of all tags that could not be identified while parsing
/// the input.
/// ///
/// # Note /// # Note
/// ///
@ -140,7 +144,7 @@ impl MasterPlaylistBuilder {
} }
fn validate_stream_inf(&self, value: &[VariantStream]) -> crate::Result<()> { fn validate_stream_inf(&self, value: &[VariantStream]) -> crate::Result<()> {
let mut has_none_closed_captions = false; let mut closed_captions_none = false;
for t in value { for t in value {
if let VariantStream::ExtXStreamInf { if let VariantStream::ExtXStreamInf {
@ -170,33 +174,26 @@ impl MasterPlaylistBuilder {
if let Some(closed_captions) = &closed_captions { if let Some(closed_captions) = &closed_captions {
match &closed_captions { match &closed_captions {
ClosedCaptions::GroupId(group_id) => { ClosedCaptions::GroupId(group_id) => {
if closed_captions_none {
return Err(Error::custom(
"If one ClosedCaptions is None all have to be None!",
));
}
if !self.check_media_group(MediaType::ClosedCaptions, group_id) { if !self.check_media_group(MediaType::ClosedCaptions, group_id) {
return Err(Error::unmatched_group(group_id)); return Err(Error::unmatched_group(group_id));
} }
} }
ClosedCaptions::None => { _ => {
has_none_closed_captions = true; if !closed_captions_none {
closed_captions_none = true;
}
} }
} }
} }
} }
} }
if has_none_closed_captions
&& !value.iter().all(|t| {
if let VariantStream::ExtXStreamInf {
closed_captions, ..
} = &t
{
closed_captions == &Some(ClosedCaptions::None)
} else {
false
}
})
{
return Err(Error::invalid_input());
}
Ok(()) Ok(())
} }
@ -218,6 +215,8 @@ impl MasterPlaylistBuilder {
let mut set = HashSet::new(); let mut set = HashSet::new();
if let Some(value) = &self.session_data { if let Some(value) = &self.session_data {
set.reserve(value.len());
for t in value { for t in value {
if !set.insert((t.data_id(), t.language())) { if !set.insert((t.data_id(), t.language())) {
return Err(Error::custom(format!("Conflict: {}", t))); return Err(Error::custom(format!("Conflict: {}", t)));
@ -264,20 +263,20 @@ impl fmt::Display for MasterPlaylist {
writeln!(f, "{}", ExtXVersion::new(self.required_version()))?; writeln!(f, "{}", ExtXVersion::new(self.required_version()))?;
} }
for t in &self.media { for value in &self.media {
writeln!(f, "{}", t)?; writeln!(f, "{}", value)?;
} }
for t in &self.variants { for value in &self.variants {
writeln!(f, "{}", t)?; writeln!(f, "{}", value)?;
} }
for t in &self.session_data { for value in &self.session_data {
writeln!(f, "{}", t)?; writeln!(f, "{}", value)?;
} }
for t in &self.session_keys { for value in &self.session_keys {
writeln!(f, "{}", t)?; writeln!(f, "{}", value)?;
} }
if let Some(value) = &self.independent_segments { if let Some(value) = &self.independent_segments {
@ -288,8 +287,8 @@ impl fmt::Display for MasterPlaylist {
writeln!(f, "{}", value)?; writeln!(f, "{}", value)?;
} }
for t in &self.unknown_tags { for value in &self.unknown_tags {
writeln!(f, "{}", t)?; writeln!(f, "{}", value)?;
} }
Ok(()) Ok(())
@ -382,40 +381,194 @@ impl FromStr for MasterPlaylist {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::types::StreamData;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
#[test] #[test]
fn test_parser() { fn test_parser() {
"#EXTM3U\n\ assert_eq!(
#EXT-X-STREAM-INF:BANDWIDTH=150000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n\ concat!(
http://example.com/low/index.m3u8\n\ "#EXTM3U\n",
#EXT-X-STREAM-INF:BANDWIDTH=240000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n\ "#EXT-X-STREAM-INF:",
http://example.com/lo_mid/index.m3u8\n\ "BANDWIDTH=150000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n",
#EXT-X-STREAM-INF:BANDWIDTH=440000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n\ "http://example.com/low/index.m3u8\n",
http://example.com/hi_mid/index.m3u8\n\ "#EXT-X-STREAM-INF:",
#EXT-X-STREAM-INF:BANDWIDTH=640000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=640x360\n\ "BANDWIDTH=240000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n",
http://example.com/high/index.m3u8\n\ "http://example.com/lo_mid/index.m3u8\n",
#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS=\"mp4a.40.5\"\n\ "#EXT-X-STREAM-INF:",
http://example.com/audio/index.m3u8\n" "BANDWIDTH=440000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n",
"http://example.com/hi_mid/index.m3u8\n",
"#EXT-X-STREAM-INF:",
"BANDWIDTH=640000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=640x360\n",
"http://example.com/high/index.m3u8\n",
"#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS=\"mp4a.40.5\"\n",
"http://example.com/audio/index.m3u8\n"
)
.parse::<MasterPlaylist>() .parse::<MasterPlaylist>()
.unwrap(); .unwrap(),
MasterPlaylist::builder()
.variants(vec![
VariantStream::ExtXStreamInf {
uri: "http://example.com/low/index.m3u8".into(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
.bandwidth(150000)
.codecs("avc1.42e00a,mp4a.40.2")
.resolution((416, 234))
.build()
.unwrap()
},
VariantStream::ExtXStreamInf {
uri: "http://example.com/lo_mid/index.m3u8".into(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
.bandwidth(240000)
.codecs("avc1.42e00a,mp4a.40.2")
.resolution((416, 234))
.build()
.unwrap()
},
VariantStream::ExtXStreamInf {
uri: "http://example.com/hi_mid/index.m3u8".into(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
.bandwidth(440000)
.codecs("avc1.42e00a,mp4a.40.2")
.resolution((416, 234))
.build()
.unwrap()
},
VariantStream::ExtXStreamInf {
uri: "http://example.com/high/index.m3u8".into(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
.bandwidth(640000)
.codecs("avc1.42e00a,mp4a.40.2")
.resolution((640, 360))
.build()
.unwrap()
},
VariantStream::ExtXStreamInf {
uri: "http://example.com/audio/index.m3u8".into(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
.bandwidth(64000)
.codecs("mp4a.40.5")
.build()
.unwrap()
},
])
.build()
.unwrap()
);
} }
#[test] #[test]
fn test_display() { fn test_display() {
let input = "#EXTM3U\n\ assert_eq!(
#EXT-X-STREAM-INF:BANDWIDTH=150000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n\ concat!(
http://example.com/low/index.m3u8\n\ "#EXTM3U\n",
#EXT-X-STREAM-INF:BANDWIDTH=240000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n\ "#EXT-X-STREAM-INF:",
http://example.com/lo_mid/index.m3u8\n\ "BANDWIDTH=150000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n",
#EXT-X-STREAM-INF:BANDWIDTH=440000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n\ "http://example.com/low/index.m3u8\n",
http://example.com/hi_mid/index.m3u8\n\ "#EXT-X-STREAM-INF:",
#EXT-X-STREAM-INF:BANDWIDTH=640000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=640x360\n\ "BANDWIDTH=240000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n",
http://example.com/high/index.m3u8\n\ "http://example.com/lo_mid/index.m3u8\n",
#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS=\"mp4a.40.5\"\n\ "#EXT-X-STREAM-INF:",
http://example.com/audio/index.m3u8\n"; "BANDWIDTH=440000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n",
"http://example.com/hi_mid/index.m3u8\n",
let playlist = input.parse::<MasterPlaylist>().unwrap(); "#EXT-X-STREAM-INF:",
assert_eq!(playlist.to_string(), input); "BANDWIDTH=640000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=640x360\n",
"http://example.com/high/index.m3u8\n",
"#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS=\"mp4a.40.5\"\n",
"http://example.com/audio/index.m3u8\n"
)
.to_string(),
MasterPlaylist::builder()
.variants(vec![
VariantStream::ExtXStreamInf {
uri: "http://example.com/low/index.m3u8".into(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
.bandwidth(150000)
.codecs("avc1.42e00a,mp4a.40.2")
.resolution((416, 234))
.build()
.unwrap()
},
VariantStream::ExtXStreamInf {
uri: "http://example.com/lo_mid/index.m3u8".into(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
.bandwidth(240000)
.codecs("avc1.42e00a,mp4a.40.2")
.resolution((416, 234))
.build()
.unwrap()
},
VariantStream::ExtXStreamInf {
uri: "http://example.com/hi_mid/index.m3u8".into(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
.bandwidth(440000)
.codecs("avc1.42e00a,mp4a.40.2")
.resolution((416, 234))
.build()
.unwrap()
},
VariantStream::ExtXStreamInf {
uri: "http://example.com/high/index.m3u8".into(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
.bandwidth(640000)
.codecs("avc1.42e00a,mp4a.40.2")
.resolution((640, 360))
.build()
.unwrap()
},
VariantStream::ExtXStreamInf {
uri: "http://example.com/audio/index.m3u8".into(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
.bandwidth(64000)
.codecs("mp4a.40.5")
.build()
.unwrap()
},
])
.build()
.unwrap()
.to_string()
);
} }
} }

View file

@ -41,9 +41,7 @@ pub struct MediaSegment {
} }
impl MediaSegment { impl MediaSegment {
/// Returns a builder for a [`MasterPlaylist`]. /// Returns a builder for a [`MediaSegment`].
///
/// [`MasterPlaylist`]: crate::MasterPlaylist
pub fn builder() -> MediaSegmentBuilder { MediaSegmentBuilder::default() } pub fn builder() -> MediaSegmentBuilder { MediaSegmentBuilder::default() }
} }
@ -131,13 +129,15 @@ mod tests {
.build() .build()
.unwrap() .unwrap()
.to_string(), .to_string(),
"#EXT-X-KEY:METHOD=NONE\n\ concat!(
#EXT-X-MAP:URI=\"https://www.example.com/\"\n\ "#EXT-X-KEY:METHOD=NONE\n",
#EXT-X-BYTERANGE:20@5\n\ "#EXT-X-MAP:URI=\"https://www.example.com/\"\n",
#EXT-X-DISCONTINUITY\n\ "#EXT-X-BYTERANGE:20@5\n",
#EXTINF:4,\n\ "#EXT-X-DISCONTINUITY\n",
http://www.uri.com/\n" "#EXTINF:4,\n",
.to_string() "http://www.uri.com/\n"
)
.to_string()
); );
} }
} }

View file

@ -9,10 +9,10 @@ use crate::{Error, RequiredVersion};
/// ///
/// The [`ExtM3u`] tag indicates that the file is an **Ext**ended **[`M3U`]** /// The [`ExtM3u`] tag indicates that the file is an **Ext**ended **[`M3U`]**
/// Playlist file. /// Playlist file.
/// It is the at the start of every [`Media Playlist`] and [`Master Playlist`]. /// It is the at the start of every [`MediaPlaylist`] and [`MasterPlaylist`].
/// ///
/// [`Media Playlist`]: crate::MediaPlaylist /// [`MediaPlaylist`]: crate::MediaPlaylist
/// [`Master Playlist`]: crate::MasterPlaylist /// [`MasterPlaylist`]: crate::MasterPlaylist
/// [`M3U`]: https://en.wikipedia.org/wiki/M3U /// [`M3U`]: https://en.wikipedia.org/wiki/M3U
/// [4.3.1.1. EXTM3U]: https://tools.ietf.org/html/rfc8216#section-4.3.1.1 /// [4.3.1.1. EXTM3U]: https://tools.ietf.org/html/rfc8216#section-4.3.1.1
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)] #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]

View file

@ -8,7 +8,7 @@ use crate::{Error, RequiredVersion};
/// # [4.3.1.2. EXT-X-VERSION] /// # [4.3.1.2. EXT-X-VERSION]
/// ///
/// The [`ExtXVersion`] tag indicates the compatibility version of the /// The [`ExtXVersion`] tag indicates the compatibility version of the
/// [`Master Playlist`] or [`Media Playlist`] file. /// [`MasterPlaylist`] or [`MediaPlaylist`] file.
/// It applies to the entire Playlist. /// It applies to the entire Playlist.
/// ///
/// # Examples /// # Examples
@ -40,8 +40,8 @@ use crate::{Error, RequiredVersion};
/// ); /// );
/// ``` /// ```
/// ///
/// [`Media Playlist`]: crate::MediaPlaylist /// [`MediaPlaylist`]: crate::MediaPlaylist
/// [`Master Playlist`]: crate::MasterPlaylist /// [`MasterPlaylist`]: crate::MasterPlaylist
/// [4.3.1.2. EXT-X-VERSION]: https://tools.ietf.org/html/rfc8216#section-4.3.1.2 /// [4.3.1.2. EXT-X-VERSION]: https://tools.ietf.org/html/rfc8216#section-4.3.1.2
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] #[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub struct ExtXVersion(ProtocolVersion); pub struct ExtXVersion(ProtocolVersion);

View file

@ -11,16 +11,16 @@ use crate::{Error, RequiredVersion};
/// # [4.4.5.1. EXT-X-MEDIA] /// # [4.4.5.1. EXT-X-MEDIA]
/// ///
/// The [`ExtXMedia`] tag is used to relate [`Media Playlist`]s, /// The [`ExtXMedia`] tag is used to relate [`MediaPlaylist`]s,
/// that contain alternative renditions of the same content. /// that contain alternative renditions of the same content.
/// ///
/// For example, three [`ExtXMedia`] tags can be used to identify audio-only /// For example, three [`ExtXMedia`] tags can be used to identify audio-only
/// [`Media Playlist`]s, that contain English, French, and Spanish renditions /// [`MediaPlaylist`]s, that contain English, French, and Spanish renditions
/// of the same presentation. Or, two [`ExtXMedia`] tags can be used to /// of the same presentation. Or, two [`ExtXMedia`] tags can be used to
/// identify video-only [`Media Playlist`]s that show two different camera /// identify video-only [`MediaPlaylist`]s that show two different camera
/// angles. /// angles.
/// ///
/// [`Media Playlist`]: crate::MediaPlaylist /// [`MediaPlaylist`]: crate::MediaPlaylist
/// [4.4.5.1. EXT-X-MEDIA]: /// [4.4.5.1. EXT-X-MEDIA]:
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.4.5.1 /// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.4.5.1
#[derive(ShortHand, Builder, Debug, Clone, PartialEq, Eq, Hash)] #[derive(ShortHand, Builder, Debug, Clone, PartialEq, Eq, Hash)]
@ -35,7 +35,7 @@ pub struct ExtXMedia {
/// This attribute is **required**. /// This attribute is **required**.
#[shorthand(enable(copy))] #[shorthand(enable(copy))]
media_type: MediaType, media_type: MediaType,
/// An `URI` to a [`Media Playlist`]. /// An `URI` to a [`MediaPlaylist`].
/// ///
/// # Note /// # Note
/// ///
@ -44,7 +44,7 @@ pub struct ExtXMedia {
/// - This attribute is **not allowed**, if the [`MediaType`] is /// - This attribute is **not allowed**, if the [`MediaType`] is
/// [`MediaType::ClosedCaptions`]. /// [`MediaType::ClosedCaptions`].
/// ///
/// [`Media Playlist`]: crate::MediaPlaylist /// [`MediaPlaylist`]: crate::MediaPlaylist
#[builder(setter(strip_option), default)] #[builder(setter(strip_option), default)]
uri: Option<String>, uri: Option<String>,
/// The identifier that specifies the group to which the rendition /// The identifier that specifies the group to which the rendition
@ -112,9 +112,9 @@ pub struct ExtXMedia {
#[builder(default)] #[builder(default)]
is_forced: bool, is_forced: bool,
/// An [`InStreamId`] specifies a rendition within the /// An [`InStreamId`] specifies a rendition within the
/// segments in the [`Media Playlist`]. /// segments in the [`MediaPlaylist`].
/// ///
/// [`Media Playlist`]: crate::MediaPlaylist /// [`MediaPlaylist`]: crate::MediaPlaylist
#[builder(setter(strip_option), default)] #[builder(setter(strip_option), default)]
#[shorthand(enable(copy))] #[shorthand(enable(copy))]
instream_id: Option<InStreamId>, instream_id: Option<InStreamId>,

View file

@ -31,9 +31,9 @@ pub enum SessionData {
/// # [4.3.4.4. EXT-X-SESSION-DATA] /// # [4.3.4.4. EXT-X-SESSION-DATA]
/// ///
/// The [`ExtXSessionData`] tag allows arbitrary session data to be /// The [`ExtXSessionData`] tag allows arbitrary session data to be
/// carried in a [`Master Playlist`]. /// carried in a [`MasterPlaylist`].
/// ///
/// [`Master Playlist`]: crate::MasterPlaylist /// [`MasterPlaylist`]: crate::MasterPlaylist
/// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4 /// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4
#[derive(ShortHand, Builder, Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)] #[derive(ShortHand, Builder, Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)]
#[builder(setter(into))] #[builder(setter(into))]
@ -58,7 +58,7 @@ pub struct ExtXSessionData {
/// ///
/// This field is required. /// This field is required.
data: SessionData, data: SessionData,
/// The `language` attribute identifies the language of [`SessionData`]. /// The `language` attribute identifies the language of the [`SessionData`].
/// See [rfc5646](https://tools.ietf.org/html/rfc5646). /// See [rfc5646](https://tools.ietf.org/html/rfc5646).
#[builder(setter(into, strip_option), default)] #[builder(setter(into, strip_option), default)]
language: Option<String>, language: Option<String>,

View file

@ -9,13 +9,13 @@ use crate::{Error, RequiredVersion};
/// # [4.3.4.5. EXT-X-SESSION-KEY] /// # [4.3.4.5. EXT-X-SESSION-KEY]
/// ///
/// The [`ExtXSessionKey`] tag allows encryption keys from [`Media Playlist`]s /// The [`ExtXSessionKey`] tag allows encryption keys from [`MediaPlaylist`]s
/// to be specified in a [`Master Playlist`]. This allows the client to /// to be specified in a [`MasterPlaylist`]. This allows the client to
/// preload these keys without having to read the [`Media Playlist`]s /// preload these keys without having to read the [`MediaPlaylist`]s
/// first. /// first.
/// ///
/// [`Media Playlist`]: crate::MediaPlaylist /// [`MediaPlaylist`]: crate::MediaPlaylist
/// [`Master Playlist`]: crate::MasterPlaylist /// [`MasterPlaylist`]: crate::MasterPlaylist
/// [4.3.4.5. EXT-X-SESSION-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5 /// [4.3.4.5. EXT-X-SESSION-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5
#[derive(Deref, DerefMut, Debug, Clone, PartialEq, Eq, Hash)] #[derive(Deref, DerefMut, Debug, Clone, PartialEq, Eq, Hash)]
pub struct ExtXSessionKey(DecryptionKey); pub struct ExtXSessionKey(DecryptionKey);

View file

@ -11,11 +11,13 @@ use crate::RequiredVersion;
/// # [4.4.3.3. EXT-X-DISCONTINUITY-SEQUENCE] /// # [4.4.3.3. EXT-X-DISCONTINUITY-SEQUENCE]
/// ///
/// The [`ExtXDiscontinuitySequence`] tag allows synchronization between /// The [`ExtXDiscontinuitySequence`] tag allows synchronization between
/// different Renditions of the same Variant Stream or different Variant /// different renditions of the same [`VariantStream`] or different
/// Streams that have [`ExtXDiscontinuity`] tags in their [`Media Playlist`]s. /// [`VariantStream`]s that have [`ExtXDiscontinuity`] tags in their
/// [`MediaPlaylist`]s.
/// ///
/// [`VariantStream`]: crate::tags::VariantStream
/// [`ExtXDiscontinuity`]: crate::tags::ExtXDiscontinuity /// [`ExtXDiscontinuity`]: crate::tags::ExtXDiscontinuity
/// [`Media Playlist`]: crate::MediaPlaylist /// [`MediaPlaylist`]: crate::MediaPlaylist
/// [4.4.3.3. EXT-X-DISCONTINUITY-SEQUENCE]: /// [4.4.3.3. EXT-X-DISCONTINUITY-SEQUENCE]:
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.3 /// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.3
#[derive(ShortHand, Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] #[derive(ShortHand, Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]

View file

@ -7,11 +7,11 @@ use crate::{Error, RequiredVersion};
/// # [4.4.3.4. EXT-X-ENDLIST] /// # [4.4.3.4. EXT-X-ENDLIST]
/// ///
/// The [`ExtXEndList`] tag indicates, that no more [`Media Segment`]s will be /// The [`ExtXEndList`] tag indicates, that no more [`MediaSegment`]s will be
/// added to the [`Media Playlist`] file. /// added to the [`MediaPlaylist`] file.
/// ///
/// [`Media Segment`]: crate::MediaSegment /// [`MediaSegment`]: crate::MediaSegment
/// [`Media Playlist`]: crate::MediaPlaylist /// [`MediaPlaylist`]: crate::MediaPlaylist
/// [4.4.3.4. EXT-X-ENDLIST]: /// [4.4.3.4. EXT-X-ENDLIST]:
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.4 /// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.4
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]

View file

@ -65,7 +65,10 @@ impl RequiredVersion for ExtXMediaSequence {
} }
impl fmt::Display for ExtXMediaSequence { impl fmt::Display for ExtXMediaSequence {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}{}", Self::PREFIX, self.0) } fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
//
write!(f, "{}{}", Self::PREFIX, self.0)
}
} }
impl FromStr for ExtXMediaSequence { impl FromStr for ExtXMediaSequence {

View file

@ -8,22 +8,22 @@ use crate::{Error, RequiredVersion};
/// # [4.3.3.5. EXT-X-PLAYLIST-TYPE] /// # [4.3.3.5. EXT-X-PLAYLIST-TYPE]
/// ///
/// The [`ExtXPlaylistType`] tag provides mutability information about the /// The [`ExtXPlaylistType`] tag provides mutability information about the
/// [`Media Playlist`]. It applies to the entire [`Media Playlist`]. /// [`MediaPlaylist`]. It applies to the entire [`MediaPlaylist`].
/// ///
/// [`Media Playlist`]: crate::MediaPlaylist /// [`MediaPlaylist`]: crate::MediaPlaylist
/// [4.3.3.5. EXT-X-PLAYLIST-TYPE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.5 /// [4.3.3.5. EXT-X-PLAYLIST-TYPE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.5
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum ExtXPlaylistType { pub enum ExtXPlaylistType {
/// If the [`ExtXPlaylistType`] is Event, [`Media Segment`]s /// If the [`ExtXPlaylistType`] is Event, [`MediaSegment`]s
/// can only be added to the end of the [`Media Playlist`]. /// can only be added to the end of the [`MediaPlaylist`].
/// ///
/// [`Media Segment`]: crate::MediaSegment /// [`MediaSegment`]: crate::MediaSegment
/// [`Media Playlist`]: crate::MediaPlaylist /// [`MediaPlaylist`]: crate::MediaPlaylist
Event, Event,
/// If the [`ExtXPlaylistType`] is Video On Demand (Vod), /// If the [`ExtXPlaylistType`] is Video On Demand (Vod),
/// the [`Media Playlist`] cannot change. /// the [`MediaPlaylist`] cannot change.
/// ///
/// [`Media Playlist`]: crate::MediaPlaylist /// [`MediaPlaylist`]: crate::MediaPlaylist
Vod, Vod,
} }

View file

@ -1,11 +1,14 @@
//! The tags in this section can appear in either Master Playlists or //! The tags in this section can appear in either [`MasterPlaylist`]s or
//! Media Playlists. If one of these tags appears in a Master Playlist, //! [`MediaPlaylist`]s. If one of these tags appears in a [`MasterPlaylist`],
//! it should not appear in any Media Playlist referenced by that Master //! it should not appear in any [`MediaPlaylist`] referenced by that
//! Playlist. A tag that appears in both must have the same value; //! [`MasterPlaylist`]. A tag that appears in both must have the same value;
//! otherwise, clients should ignore the value in the Media Playlist(s). //! otherwise, clients should ignore the value in the [`MediaPlaylist`](s).
//! //!
//! These tags must not appear more than once in a Playlist. If a tag //! These tags must not appear more than once in a Playlist. If a tag
//! appears more than once, clients must fail to parse the Playlist. //! appears more than once, clients must fail to parse the Playlist.
//!
//! [`MediaPlaylist`]: crate::MediaPlaylist
//! [`MasterPlaylist`]: crate::MasterPlaylist
mod independent_segments; mod independent_segments;
mod start; mod start;

View file

@ -87,6 +87,7 @@ pub trait Encrypted {
if self.keys().is_empty() { if self.keys().is_empty() {
return false; return false;
} }
self.keys() self.keys()
.iter() .iter()
.any(|k| k.method() != EncryptionMethod::None) .any(|k| k.method() != EncryptionMethod::None)
@ -140,7 +141,7 @@ pub trait RequiredVersion {
impl<T: RequiredVersion> RequiredVersion for Vec<T> { impl<T: RequiredVersion> RequiredVersion for Vec<T> {
fn required_version(&self) -> ProtocolVersion { fn required_version(&self) -> ProtocolVersion {
self.iter() self.iter()
.map(|v| v.required_version()) .map(RequiredVersion::required_version)
.max() .max()
// return ProtocolVersion::V1, if the iterator is empty: // return ProtocolVersion::V1, if the iterator is empty:
.unwrap_or_default() .unwrap_or_default()
@ -150,8 +151,26 @@ impl<T: RequiredVersion> RequiredVersion for Vec<T> {
impl<T: RequiredVersion> RequiredVersion for Option<T> { impl<T: RequiredVersion> RequiredVersion for Option<T> {
fn required_version(&self) -> ProtocolVersion { fn required_version(&self) -> ProtocolVersion {
self.iter() self.iter()
.map(|v| v.required_version()) .map(RequiredVersion::required_version)
.max() .max()
.unwrap_or_default() .unwrap_or_default()
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_required_version_trait() {
struct Example;
impl RequiredVersion for Example {
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V3 }
}
assert_eq!(Example.required_version(), ProtocolVersion::V3);
assert_eq!(Example.introduced_version(), ProtocolVersion::V3);
}
}

View file

@ -86,6 +86,32 @@ mod tests {
use super::*; use super::*;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
#[test]
fn test_parse_iv_from_str() {
assert_eq!(
parse_iv_from_str("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").unwrap(),
[
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF
]
);
assert_eq!(
parse_iv_from_str("0XFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").unwrap(),
[
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF
]
);
// missing `0x` at the start:
assert!(parse_iv_from_str("0FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").is_err());
// too small:
assert!(parse_iv_from_str("0xFF").is_err());
// too large:
assert!(parse_iv_from_str("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF").is_err());
}
#[test] #[test]
fn test_parse_yes_or_no() { fn test_parse_yes_or_no() {
assert!(parse_yes_or_no("YES").unwrap()); assert!(parse_yes_or_no("YES").unwrap());