mirror of
https://github.com/sile/hls_m3u8.git
synced 2024-11-25 08:31:00 +00:00
some improvements
This commit is contained in:
parent
25f9691c75
commit
8cced1ac53
16 changed files with 331 additions and 152 deletions
|
@ -35,6 +35,6 @@ script:
|
|||
after_success: |
|
||||
# this does require a -Z flag for Doctests, which is unstable!
|
||||
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)
|
||||
fi
|
||||
|
|
47
src/line.rs
47
src/line.rs
|
@ -1,7 +1,9 @@
|
|||
use core::convert::TryFrom;
|
||||
use core::fmt;
|
||||
use core::iter::FusedIterator;
|
||||
use core::str::FromStr;
|
||||
|
||||
use derive_more::Display;
|
||||
|
||||
use crate::tags;
|
||||
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> {
|
||||
fn from(buffer: &'a str) -> Self {
|
||||
Self {
|
||||
lines: buffer.lines().filter_map(|line| {
|
||||
if line.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(line.trim())
|
||||
}
|
||||
}),
|
||||
lines: buffer
|
||||
.lines()
|
||||
.filter_map(|line| Some(line.trim()).filter(|v| !v.is_empty())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -55,7 +55,8 @@ pub(crate) enum Line<'a> {
|
|||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Display)]
|
||||
#[display(fmt = "{}")]
|
||||
pub(crate) enum Tag<'a> {
|
||||
ExtXVersion(tags::ExtXVersion),
|
||||
ExtInf(tags::ExtInf),
|
||||
|
@ -80,34 +81,6 @@ pub(crate) enum Tag<'a> {
|
|||
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> {
|
||||
type Error = Error;
|
||||
|
||||
|
|
|
@ -15,8 +15,9 @@ use crate::utils::tag;
|
|||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// The master playlist describes all of the available variants for your
|
||||
/// content. Each variant is a version of the stream at a particular bitrate
|
||||
/// and is contained in a separate playlist.
|
||||
/// content.
|
||||
/// 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)]
|
||||
#[builder(build_fn(validate = "Self::validate"))]
|
||||
#[builder(setter(into, strip_option))]
|
||||
|
@ -67,21 +68,24 @@ pub struct MasterPlaylist {
|
|||
/// This tag is optional.
|
||||
#[builder(default)]
|
||||
variants: Vec<VariantStream>,
|
||||
/// The [`ExtXSessionData`] tags of the playlist.
|
||||
/// The [`ExtXSessionData`] tag allows arbitrary session data to be
|
||||
/// carried in a [`MasterPlaylist`].
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// This tag is optional.
|
||||
#[builder(default)]
|
||||
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
|
||||
///
|
||||
/// This tag is optional.
|
||||
#[builder(default)]
|
||||
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
|
||||
///
|
||||
|
@ -140,7 +144,7 @@ impl MasterPlaylistBuilder {
|
|||
}
|
||||
|
||||
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 {
|
||||
if let VariantStream::ExtXStreamInf {
|
||||
|
@ -170,31 +174,24 @@ impl MasterPlaylistBuilder {
|
|||
if let Some(closed_captions) = &closed_captions {
|
||||
match &closed_captions {
|
||||
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) {
|
||||
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(())
|
||||
|
@ -218,6 +215,8 @@ impl MasterPlaylistBuilder {
|
|||
let mut set = HashSet::new();
|
||||
|
||||
if let Some(value) = &self.session_data {
|
||||
set.reserve(value.len());
|
||||
|
||||
for t in value {
|
||||
if !set.insert((t.data_id(), t.language())) {
|
||||
return Err(Error::custom(format!("Conflict: {}", t)));
|
||||
|
@ -264,20 +263,20 @@ impl fmt::Display for MasterPlaylist {
|
|||
writeln!(f, "{}", ExtXVersion::new(self.required_version()))?;
|
||||
}
|
||||
|
||||
for t in &self.media {
|
||||
writeln!(f, "{}", t)?;
|
||||
for value in &self.media {
|
||||
writeln!(f, "{}", value)?;
|
||||
}
|
||||
|
||||
for t in &self.variants {
|
||||
writeln!(f, "{}", t)?;
|
||||
for value in &self.variants {
|
||||
writeln!(f, "{}", value)?;
|
||||
}
|
||||
|
||||
for t in &self.session_data {
|
||||
writeln!(f, "{}", t)?;
|
||||
for value in &self.session_data {
|
||||
writeln!(f, "{}", value)?;
|
||||
}
|
||||
|
||||
for t in &self.session_keys {
|
||||
writeln!(f, "{}", t)?;
|
||||
for value in &self.session_keys {
|
||||
writeln!(f, "{}", value)?;
|
||||
}
|
||||
|
||||
if let Some(value) = &self.independent_segments {
|
||||
|
@ -288,8 +287,8 @@ impl fmt::Display for MasterPlaylist {
|
|||
writeln!(f, "{}", value)?;
|
||||
}
|
||||
|
||||
for t in &self.unknown_tags {
|
||||
writeln!(f, "{}", t)?;
|
||||
for value in &self.unknown_tags {
|
||||
writeln!(f, "{}", value)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -382,40 +381,194 @@ impl FromStr for MasterPlaylist {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::types::StreamData;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
"#EXTM3U\n\
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=150000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n\
|
||||
http://example.com/low/index.m3u8\n\
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=240000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n\
|
||||
http://example.com/lo_mid/index.m3u8\n\
|
||||
#EXT-X-STREAM-INF: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"
|
||||
assert_eq!(
|
||||
concat!(
|
||||
"#EXTM3U\n",
|
||||
"#EXT-X-STREAM-INF:",
|
||||
"BANDWIDTH=150000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n",
|
||||
"http://example.com/low/index.m3u8\n",
|
||||
"#EXT-X-STREAM-INF:",
|
||||
"BANDWIDTH=240000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n",
|
||||
"http://example.com/lo_mid/index.m3u8\n",
|
||||
"#EXT-X-STREAM-INF:",
|
||||
"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>()
|
||||
.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]
|
||||
fn test_display() {
|
||||
let input = "#EXTM3U\n\
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=150000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n\
|
||||
http://example.com/low/index.m3u8\n\
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=240000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n\
|
||||
http://example.com/lo_mid/index.m3u8\n\
|
||||
#EXT-X-STREAM-INF: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";
|
||||
|
||||
let playlist = input.parse::<MasterPlaylist>().unwrap();
|
||||
assert_eq!(playlist.to_string(), input);
|
||||
assert_eq!(
|
||||
concat!(
|
||||
"#EXTM3U\n",
|
||||
"#EXT-X-STREAM-INF:",
|
||||
"BANDWIDTH=150000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n",
|
||||
"http://example.com/low/index.m3u8\n",
|
||||
"#EXT-X-STREAM-INF:",
|
||||
"BANDWIDTH=240000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n",
|
||||
"http://example.com/lo_mid/index.m3u8\n",
|
||||
"#EXT-X-STREAM-INF:",
|
||||
"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"
|
||||
)
|
||||
.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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,9 +41,7 @@ pub struct MediaSegment {
|
|||
}
|
||||
|
||||
impl MediaSegment {
|
||||
/// Returns a builder for a [`MasterPlaylist`].
|
||||
///
|
||||
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
||||
/// Returns a builder for a [`MediaSegment`].
|
||||
pub fn builder() -> MediaSegmentBuilder { MediaSegmentBuilder::default() }
|
||||
}
|
||||
|
||||
|
@ -131,12 +129,14 @@ mod tests {
|
|||
.build()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
"#EXT-X-KEY:METHOD=NONE\n\
|
||||
#EXT-X-MAP:URI=\"https://www.example.com/\"\n\
|
||||
#EXT-X-BYTERANGE:20@5\n\
|
||||
#EXT-X-DISCONTINUITY\n\
|
||||
#EXTINF:4,\n\
|
||||
http://www.uri.com/\n"
|
||||
concat!(
|
||||
"#EXT-X-KEY:METHOD=NONE\n",
|
||||
"#EXT-X-MAP:URI=\"https://www.example.com/\"\n",
|
||||
"#EXT-X-BYTERANGE:20@5\n",
|
||||
"#EXT-X-DISCONTINUITY\n",
|
||||
"#EXTINF:4,\n",
|
||||
"http://www.uri.com/\n"
|
||||
)
|
||||
.to_string()
|
||||
);
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@ pub struct ExtXSessionData {
|
|||
///
|
||||
/// This field is required.
|
||||
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).
|
||||
#[builder(setter(into, strip_option), default)]
|
||||
language: Option<String>,
|
||||
|
|
|
@ -11,9 +11,11 @@ use crate::RequiredVersion;
|
|||
/// # [4.4.3.3. EXT-X-DISCONTINUITY-SEQUENCE]
|
||||
///
|
||||
/// The [`ExtXDiscontinuitySequence`] tag allows synchronization between
|
||||
/// different Renditions of the same Variant Stream or different Variant
|
||||
/// Streams that have [`ExtXDiscontinuity`] tags in their [`Media Playlist`]s.
|
||||
/// different renditions of the same [`VariantStream`] or different
|
||||
/// [`VariantStream`]s that have [`ExtXDiscontinuity`] tags in their
|
||||
/// [`MediaPlaylist`]s.
|
||||
///
|
||||
/// [`VariantStream`]: crate::tags::VariantStream
|
||||
/// [`ExtXDiscontinuity`]: crate::tags::ExtXDiscontinuity
|
||||
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||
/// [4.4.3.3. EXT-X-DISCONTINUITY-SEQUENCE]:
|
||||
|
|
|
@ -65,7 +65,10 @@ impl RequiredVersion 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 {
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
//! The tags in this section can appear in either Master Playlists or
|
||||
//! Media Playlists. If one of these tags appears in a Master Playlist,
|
||||
//! it should not appear in any Media Playlist referenced by that Master
|
||||
//! Playlist. A tag that appears in both must have the same value;
|
||||
//! otherwise, clients should ignore the value in the Media Playlist(s).
|
||||
//! The tags in this section can appear in either [`MasterPlaylist`]s or
|
||||
//! [`MediaPlaylist`]s. If one of these tags appears in a [`MasterPlaylist`],
|
||||
//! it should not appear in any [`MediaPlaylist`] referenced by that
|
||||
//! [`MasterPlaylist`]. A tag that appears in both must have the same value;
|
||||
//! otherwise, clients should ignore the value in the [`MediaPlaylist`](s).
|
||||
//!
|
||||
//! 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.
|
||||
//!
|
||||
//! [`MediaPlaylist`]: crate::MediaPlaylist
|
||||
//! [`MasterPlaylist`]: crate::MasterPlaylist
|
||||
mod independent_segments;
|
||||
mod start;
|
||||
|
||||
|
|
|
@ -87,6 +87,7 @@ pub trait Encrypted {
|
|||
if self.keys().is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.keys()
|
||||
.iter()
|
||||
.any(|k| k.method() != EncryptionMethod::None)
|
||||
|
@ -140,7 +141,7 @@ pub trait RequiredVersion {
|
|||
impl<T: RequiredVersion> RequiredVersion for Vec<T> {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
self.iter()
|
||||
.map(|v| v.required_version())
|
||||
.map(RequiredVersion::required_version)
|
||||
.max()
|
||||
// return ProtocolVersion::V1, if the iterator is empty:
|
||||
.unwrap_or_default()
|
||||
|
@ -150,8 +151,26 @@ impl<T: RequiredVersion> RequiredVersion for Vec<T> {
|
|||
impl<T: RequiredVersion> RequiredVersion for Option<T> {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
self.iter()
|
||||
.map(|v| v.required_version())
|
||||
.map(RequiredVersion::required_version)
|
||||
.max()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
|
26
src/utils.rs
26
src/utils.rs
|
@ -86,6 +86,32 @@ mod tests {
|
|||
use super::*;
|
||||
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]
|
||||
fn test_parse_yes_or_no() {
|
||||
assert!(parse_yes_or_no("YES").unwrap());
|
||||
|
|
Loading…
Reference in a new issue