1
0
Fork 0
mirror of https://github.com/sile/hls_m3u8.git synced 2024-05-24 19:28:31 +00:00

added more tests

This commit is contained in:
Luro02 2019-09-15 10:40:45 +02:00
parent b954ae1134
commit 3acf67df6a
10 changed files with 365 additions and 138 deletions

View file

@ -7,7 +7,7 @@ use failure::{Backtrace, Context, Fail};
pub type Result<T> = std::result::Result<T, Error>;
/// The ErrorKind.
#[derive(Debug, Fail, Clone)]
#[derive(Debug, Fail, Clone, PartialEq, Eq)]
pub enum ErrorKind {
#[fail(display = "UnknownError: {}", _0)]
/// An unknown error occured.

View file

@ -43,6 +43,7 @@ impl FromStr for Lines {
} else if line.starts_with("#") {
continue; // ignore comments
} else {
// stream inf line needs special treatment
if stream_inf {
stream_inf = false;
if let Some(first_line) = stream_inf_line {

View file

@ -319,6 +319,7 @@ fn parse_media_playlist(
let mut has_partial_segment = false;
let mut has_discontinuity_tag = false;
let mut has_version = false; // m3u8 files without ExtXVersion tags are ProtocolVersion::V1
for (i, line) in input.parse::<Lines>()?.into_iter().enumerate() {
match line {
@ -333,6 +334,7 @@ fn parse_media_playlist(
Tag::ExtM3u(_) => return Err(Error::invalid_input()),
Tag::ExtXVersion(t) => {
builder.version(t.version());
has_version = true;
}
Tag::ExtInf(t) => {
has_partial_segment = true;
@ -417,6 +419,9 @@ fn parse_media_playlist(
if has_partial_segment {
return Err(Error::invalid_input());
}
if !has_version {
builder.version(ProtocolVersion::V1);
}
builder.segments(segments);
builder.build().map_err(Error::builder_error)
@ -436,36 +441,37 @@ mod tests {
#[test]
fn too_large_segment_duration_test() {
let m3u8 = "#EXTM3U\n\
#EXT-X-TARGETDURATION:8\n\
#EXT-X-VERSION:3\n\
#EXTINF:9.009,\n\
http://media.example.com/first.ts\n\
#EXTINF:9.509,\n\
http://media.example.com/second.ts\n\
#EXTINF:3.003,\n\
http://media.example.com/third.ts\n\
#EXT-X-ENDLIST";
let playlist = r#"
#EXTM3U
#EXT-X-TARGETDURATION:8
#EXT-X-VERSION:3
#EXTINF:9.009,
http://media.example.com/first.ts
#EXTINF:9.509,
http://media.example.com/second.ts
#EXTINF:3.003,
http://media.example.com/third.ts
#EXT-X-ENDLIST"#;
// Error (allowable segment duration = target duration = 8)
assert!(m3u8.parse::<MediaPlaylist>().is_err());
assert!(playlist.parse::<MediaPlaylist>().is_err());
// Error (allowable segment duration = 9)
assert!(MediaPlaylist::builder()
.allowable_excess_duration(Duration::from_secs(1))
.parse(m3u8)
.parse(playlist)
.is_err());
// Ok (allowable segment duration = 10)
MediaPlaylist::builder()
.allowable_excess_duration(Duration::from_secs(2))
.parse(m3u8)
.parse(playlist)
.unwrap();
}
#[test]
fn test_parser() {
let m3u8 = "";
assert!(m3u8.parse::<MediaPlaylist>().is_err());
fn test_empty_playlist() {
let playlist = "";
assert!(playlist.parse::<MediaPlaylist>().is_err());
}
}

View file

@ -1,40 +1,49 @@
use std::fmt;
use std::str::FromStr;
use crate::types::{PlaylistType, ProtocolVersion};
use crate::types::ProtocolVersion;
use crate::utils::tag;
use crate::Error;
/// [4.3.3.5. EXT-X-PLAYLIST-TYPE]
/// [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
/// The EXT-X-PLAYLIST-TYPE tag provides mutability information about the
/// Media Playlist. It applies to the entire Media Playlist.
/// It is OPTIONAL. Its format is:
///
/// ```text
/// #EXT-X-PLAYLIST-TYPE:<type-enum>
/// ```
///
/// # Note
/// If the EXT-X-PLAYLIST-TYPE tag is omitted from a Media Playlist, the
/// Playlist can be updated according to the rules in Section 6.2.1 with
/// no additional restrictions.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ExtXPlaylistType {
playlist_type: PlaylistType,
pub enum ExtXPlaylistType {
/// If the ExtXPlaylistType is Event, Media Segments can only be added to
/// the end of the Media Playlist.
Event,
/// If the ExtXPlaylistType is Video On Demand (Vod),
/// the Media Playlist cannot change.
Vod,
}
impl ExtXPlaylistType {
pub(crate) const PREFIX: &'static str = "#EXT-X-PLAYLIST-TYPE:";
/// Makes a new `ExtXPlaylistType` tag.
pub const fn new(playlist_type: PlaylistType) -> Self {
ExtXPlaylistType { playlist_type }
}
/// Returns the type of the associated media playlist.
pub const fn playlist_type(self) -> PlaylistType {
self.playlist_type
}
/// Returns the protocol compatibility version that this tag requires.
pub const fn requires_version(self) -> ProtocolVersion {
pub const fn requires_version(&self) -> ProtocolVersion {
ProtocolVersion::V1
}
}
impl fmt::Display for ExtXPlaylistType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}{}", Self::PREFIX, self.playlist_type)
match &self {
Self::Event => write!(f, "{}EVENT", Self::PREFIX),
Self::Vod => write!(f, "{}VOD", Self::PREFIX),
}
}
}
@ -42,9 +51,12 @@ impl FromStr for ExtXPlaylistType {
type Err = Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let input = tag(input, Self::PREFIX)?.parse()?;
Ok(ExtXPlaylistType::new(input))
let input = tag(input, Self::PREFIX)?;
match input {
"EVENT" => Ok(Self::Event),
"VOD" => Ok(Self::Vod),
_ => Err(Error::custom(format!("Unknown playlist type: {:?}", input))),
}
}
}
@ -53,11 +65,48 @@ mod test {
use super::*;
#[test]
fn ext_x_playlist_type() {
let tag = ExtXPlaylistType::new(PlaylistType::Vod);
let text = "#EXT-X-PLAYLIST-TYPE:VOD";
assert_eq!(text.parse().ok(), Some(tag));
assert_eq!(tag.to_string(), text);
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
fn test_parser() {
assert_eq!(
"#EXT-X-PLAYLIST-TYPE:VOD"
.parse::<ExtXPlaylistType>()
.unwrap(),
ExtXPlaylistType::Vod,
);
assert_eq!(
"#EXT-X-PLAYLIST-TYPE:EVENT"
.parse::<ExtXPlaylistType>()
.unwrap(),
ExtXPlaylistType::Event,
);
assert!("#EXT-X-PLAYLIST-TYPE:H"
.parse::<ExtXPlaylistType>()
.is_err());
}
#[test]
fn test_display() {
assert_eq!(
"#EXT-X-PLAYLIST-TYPE:VOD".to_string(),
ExtXPlaylistType::Vod.to_string(),
);
assert_eq!(
"#EXT-X-PLAYLIST-TYPE:EVENT".to_string(),
ExtXPlaylistType::Event.to_string(),
);
}
#[test]
fn test_requires_version() {
assert_eq!(
ExtXPlaylistType::Vod.requires_version(),
ProtocolVersion::V1
);
assert_eq!(
ExtXPlaylistType::Event.requires_version(),
ProtocolVersion::V1
);
}
}

View file

@ -6,10 +6,46 @@ use crate::types::{DecimalFloatingPoint, ProtocolVersion, SingleLineString};
use crate::utils::tag;
use crate::Error;
/// [4.3.2.1. EXTINF]
/// [4.3.2.1. EXTINF](https://tools.ietf.org/html/rfc8216#section-4.3.2.1)
///
/// [4.3.2.1. EXTINF]: https://tools.ietf.org/html/rfc8216#section-4.3.2.1
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
/// The [ExtInf] tag specifies the duration of a [Media Segment]. It applies
/// only to the next [Media Segment]. This tag is REQUIRED for each [Media Segment].
///
/// Its format is:
/// ```text
/// #EXTINF:<duration>,[<title>]
/// ```
/// The title is an optional informative title about the [Media Segment].
///
/// [Media Segment]: crate::media_segment::MediaSegment
///
/// # Examples
/// Parsing from a String:
/// ```
/// use std::time::Duration;
/// use hls_m3u8::tags::ExtInf;
///
/// let ext_inf = "#EXTINF:8,".parse::<ExtInf>().expect("Failed to parse tag!");
///
/// assert_eq!(ext_inf.duration(), Duration::from_secs(8));
/// assert_eq!(ext_inf.title(), None);
/// ```
///
/// Converting to a String:
/// ```
/// use std::time::Duration;
/// use hls_m3u8::tags::ExtInf;
/// use hls_m3u8::types::SingleLineString;
///
/// let ext_inf = ExtInf::with_title(
/// Duration::from_millis(88),
/// SingleLineString::new("title").unwrap()
/// );
///
/// assert_eq!(ext_inf.duration(), Duration::from_millis(88));
/// assert_eq!(ext_inf.to_string(), "#EXTINF:0.088,title".to_string());
/// ```
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ExtInf {
duration: Duration,
title: Option<SingleLineString>,
@ -60,10 +96,10 @@ impl fmt::Display for ExtInf {
let duration = (self.duration.as_secs() as f64)
+ (f64::from(self.duration.subsec_nanos()) / 1_000_000_000.0);
write!(f, "{}", duration)?;
write!(f, "{},", duration)?;
if let Some(value) = &self.title {
write!(f, ",{}", value)?;
write!(f, "{}", value)?;
}
Ok(())
}
@ -74,44 +110,129 @@ impl FromStr for ExtInf {
fn from_str(input: &str) -> Result<Self, Self::Err> {
let input = tag(input, Self::PREFIX)?;
let mut tokens = input.splitn(2, ',');
dbg!(&input);
let tokens = input.splitn(2, ',').collect::<Vec<_>>();
let seconds: DecimalFloatingPoint = tokens.next().expect("Never fails").parse()?;
let duration = seconds.to_duration();
if tokens.len() == 0 {
return Err(Error::custom(format!(
"failed to parse #EXTINF tag, couldn't split input: {:?}",
input
)));
}
let duration = tokens[0].parse::<DecimalFloatingPoint>()?.to_duration();
let title = {
if let Some(title) = tokens.next() {
Some((SingleLineString::new(title))?)
if tokens.len() >= 2 {
if tokens[1].trim().is_empty() {
None
} else {
Some(SingleLineString::new(tokens[1])?)
}
} else {
None
}
};
Ok(ExtInf { duration, title })
}
}
impl From<Duration> for ExtInf {
fn from(value: Duration) -> Self {
Self::new(value)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn extinf() {
let tag = ExtInf::new(Duration::from_secs(5));
assert_eq!("#EXTINF:5".parse().ok(), Some(tag.clone()));
assert_eq!(tag.to_string(), "#EXTINF:5");
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
let tag = ExtInf::with_title(
Duration::from_secs(5),
SingleLineString::new("foo").unwrap(),
fn test_display() {
assert_eq!(
"#EXTINF:5,".to_string(),
ExtInf::new(Duration::from_secs(5)).to_string()
);
assert_eq!("#EXTINF:5,foo".parse().ok(), Some(tag.clone()));
assert_eq!(tag.to_string(), "#EXTINF:5,foo");
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
assert_eq!(
"#EXTINF:5.5,".to_string(),
ExtInf::new(Duration::from_millis(5500)).to_string()
);
assert_eq!(
"#EXTINF:5.5,title".to_string(),
ExtInf::with_title(
Duration::from_millis(5500),
SingleLineString::new("title").unwrap()
)
.to_string()
);
assert_eq!(
"#EXTINF:5,title".to_string(),
ExtInf::with_title(
Duration::from_secs(5),
SingleLineString::new("title").unwrap()
)
.to_string()
);
}
let tag = ExtInf::new(Duration::from_millis(1234));
assert_eq!("#EXTINF:1.234".parse().ok(), Some(tag.clone()));
assert_eq!(tag.to_string(), "#EXTINF:1.234");
assert_eq!(tag.requires_version(), ProtocolVersion::V3);
#[test]
fn test_parser() {
// #EXTINF:<duration>,[<title>]
assert_eq!(
"#EXTINF:5".parse::<ExtInf>().unwrap(),
ExtInf::new(Duration::from_secs(5))
);
assert_eq!(
"#EXTINF:5,".parse::<ExtInf>().unwrap(),
ExtInf::new(Duration::from_secs(5))
);
assert_eq!(
"#EXTINF:5.5".parse::<ExtInf>().unwrap(),
ExtInf::new(Duration::from_millis(5500))
);
assert_eq!(
"#EXTINF:5.5,".parse::<ExtInf>().unwrap(),
ExtInf::new(Duration::from_millis(5500))
);
assert_eq!(
"#EXTINF:5.5,title".parse::<ExtInf>().unwrap(),
ExtInf::with_title(
Duration::from_millis(5500),
SingleLineString::new("title").unwrap()
)
);
assert_eq!(
"#EXTINF:5,title".parse::<ExtInf>().unwrap(),
ExtInf::with_title(
Duration::from_secs(5),
SingleLineString::new("title").unwrap()
)
);
}
#[test]
fn test_title() {
assert_eq!(ExtInf::new(Duration::from_secs(5)).title(), None);
assert_eq!(
ExtInf::with_title(
Duration::from_secs(5),
SingleLineString::new("title").unwrap()
)
.title(),
Some(&SingleLineString::new("title").unwrap())
);
}
#[test]
fn test_requires_version() {
assert_eq!(
ExtInf::new(Duration::from_secs(4)).requires_version(),
ProtocolVersion::V1
);
assert_eq!(
ExtInf::new(Duration::from_millis(4400)).requires_version(),
ProtocolVersion::V3
);
}
}

View file

@ -1,5 +1,5 @@
use std::fmt;
use std::str::{self, FromStr};
use std::str::FromStr;
use crate::Error;
@ -10,11 +10,37 @@ use crate::Error;
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DecimalResolution {
width: usize,
height: usize,
}
impl DecimalResolution {
/// Creates a new DecimalResolution.
pub const fn new(width: usize, height: usize) -> Self {
Self { width, height }
}
/// Horizontal pixel dimension.
pub width: usize,
pub const fn width(&self) -> usize {
self.width
}
/// Sets Horizontal pixel dimension.
pub fn set_width(&mut self, value: usize) -> &mut Self {
self.width = value;
self
}
/// Vertical pixel dimension.
pub height: usize,
pub const fn height(&self) -> usize {
self.height
}
/// Sets Vertical pixel dimension.
pub fn set_height(&mut self, value: usize) -> &mut Self {
self.height = value;
self
}
}
impl fmt::Display for DecimalResolution {
@ -27,13 +53,21 @@ impl FromStr for DecimalResolution {
type Err = Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let mut tokens = input.splitn(2, 'x');
let width = tokens.next().ok_or(Error::missing_value("width"))?;
let height = tokens.next().ok_or(Error::missing_value("height"))?;
let tokens = input.splitn(2, 'x').collect::<Vec<_>>();
if tokens.len() != 2 {
return Err(Error::custom(format!(
"InvalidInput: Expected input format: [width]x[height] (ex. 1920x1080), got {:?}",
input,
)));
}
let width = tokens[0];
let height = tokens[1];
Ok(DecimalResolution {
width: width.parse().map_err(|e| Error::custom(e))?,
height: height.parse().map_err(|e| Error::custom(e))?,
width: width.parse()?,
height: height.parse()?,
})
}
}
@ -44,37 +78,44 @@ mod tests {
#[test]
fn test_display() {
let decimal_resolution = DecimalResolution {
width: 1920,
height: 1080,
};
assert_eq!(decimal_resolution.to_string(), "1920x1080".to_string());
assert_eq!(
DecimalResolution::new(1920, 1080).to_string(),
"1920x1080".to_string()
);
let decimal_resolution = DecimalResolution {
width: 1280,
height: 720,
};
assert_eq!(decimal_resolution.to_string(), "1280x720".to_string());
assert_eq!(
DecimalResolution::new(1280, 720).to_string(),
"1280x720".to_string()
);
}
#[test]
fn test_parse() {
let decimal_resolution = DecimalResolution {
width: 1920,
height: 1080,
};
assert_eq!(
decimal_resolution,
DecimalResolution::new(1920, 1080),
"1920x1080".parse::<DecimalResolution>().unwrap()
);
let decimal_resolution = DecimalResolution {
width: 1280,
height: 720,
};
assert_eq!(
decimal_resolution,
DecimalResolution::new(1280, 720),
"1280x720".parse::<DecimalResolution>().unwrap()
);
assert!("1280".parse::<DecimalResolution>().is_err());
}
#[test]
fn test_width() {
assert_eq!(DecimalResolution::new(1920, 1080).width(), 1920);
assert_eq!(DecimalResolution::new(1920, 1080).set_width(12).width(), 12);
}
#[test]
fn test_height() {
assert_eq!(DecimalResolution::new(1920, 1080).height(), 1080);
assert_eq!(
DecimalResolution::new(1920, 1080).set_height(12).height(),
12
);
}
}

View file

@ -10,7 +10,6 @@ mod hexadecimal_sequence;
mod in_stream_id;
mod initialization_vector;
mod media_type;
mod playlist_type;
mod protocol_version;
mod session_data;
mod signed_decimal_floating_point;
@ -27,7 +26,6 @@ pub use hexadecimal_sequence::*;
pub use in_stream_id::*;
pub use initialization_vector::*;
pub use media_type::*;
pub use playlist_type::*;
pub use protocol_version::*;
pub use session_data::*;
pub use signed_decimal_floating_point::*;

View file

@ -1,37 +1 @@
use std::fmt;
use std::str::FromStr;
use crate::Error;
/// Playlist type.
///
/// See: [4.3.3.5. EXT-X-PLAYLIST-TYPE]
///
/// [4.3.3.5. EXT-X-PLAYLIST-TYPE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.5
#[allow(missing_docs)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PlaylistType {
Event,
Vod,
}
impl fmt::Display for PlaylistType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
PlaylistType::Event => write!(f, "EVENT"),
PlaylistType::Vod => write!(f, "VOD"),
}
}
}
impl FromStr for PlaylistType {
type Err = Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
match input {
"EVENT" => Ok(PlaylistType::Event),
"VOD" => Ok(PlaylistType::Vod),
_ => Err(Error::custom(format!("Unknown playlist type: {:?}", input))),
}
}
}

View file

@ -1,6 +1,6 @@
use crate::{Error, Result};
use crate::Error;
pub(crate) fn parse_yes_or_no<T: AsRef<str>>(s: T) -> Result<bool> {
pub(crate) fn parse_yes_or_no<T: AsRef<str>>(s: T) -> crate::Result<bool> {
match s.as_ref() {
"YES" => Ok(true),
"NO" => Ok(false),
@ -8,7 +8,7 @@ pub(crate) fn parse_yes_or_no<T: AsRef<str>>(s: T) -> Result<bool> {
}
}
pub(crate) fn parse_u64<T: AsRef<str>>(s: T) -> Result<u64> {
pub(crate) fn parse_u64<T: AsRef<str>>(s: T) -> crate::Result<u64> {
let n = s.as_ref().parse().map_err(Error::unknown)?; // TODO: Error::number
Ok(n)
}

47
tests/playlist.rs Normal file
View file

@ -0,0 +1,47 @@
//! Credits go to
//! - https://github.com/globocom/m3u8/blob/master/tests/playlists.py
use hls_m3u8::tags::*;
use hls_m3u8::types::*;
use hls_m3u8::MediaPlaylist;
use std::time::Duration;
#[test]
fn test_simple_playlist() {
let playlist = r#"
#EXTM3U
#EXT-X-TARGETDURATION:5220
#EXTINF:0,
http://media.example.com/entire1.ts
#EXTINF:5220,
http://media.example.com/entire2.ts
#EXT-X-ENDLIST"#;
let media_playlist = playlist.parse::<MediaPlaylist>().unwrap();
assert_eq!(
media_playlist.target_duration_tag(),
ExtXTargetDuration::new(Duration::from_secs(5220))
);
assert_eq!(media_playlist.segments().len(), 2);
assert_eq!(
media_playlist.segments()[0].inf_tag(),
&ExtInf::new(Duration::from_secs(0))
);
assert_eq!(
media_playlist.segments()[1].inf_tag(),
&ExtInf::new(Duration::from_secs(5220))
);
assert_eq!(
media_playlist.segments()[0].uri(),
&SingleLineString::new("http://media.example.com/entire1.ts").unwrap()
);
assert_eq!(
media_playlist.segments()[1].uri(),
&SingleLineString::new("http://media.example.com/entire2.ts").unwrap()
);
}