1
0
Fork 0
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:
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: |
# 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

View file

@ -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;

View file

@ -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()
);
}
}

View file

@ -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()
);
}

View file

@ -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>,

View file

@ -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]:

View file

@ -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 {

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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());