mirror of
https://github.com/sile/hls_m3u8.git
synced 2025-01-24 18:28:11 +00:00
Merge branch 'master' into master
This commit is contained in:
commit
1876adbaf8
52 changed files with 2759 additions and 841 deletions
|
@ -17,10 +17,12 @@ codecov = { repository = "sile/hls_m3u8" }
|
|||
|
||||
[dependencies]
|
||||
failure = "0.1.5"
|
||||
derive_builder = "0.7.2"
|
||||
derive_builder = "0.8.0"
|
||||
chrono = "0.4.9"
|
||||
strum = { version = "0.16.0", features = ["derive"] }
|
||||
derive_more = "0.15.0"
|
||||
hex = "0.4.0"
|
||||
|
||||
[dev-dependencies]
|
||||
clap = "2"
|
||||
clap = "2.33.0"
|
||||
pretty_assertions = "0.6.1"
|
||||
|
|
|
@ -39,7 +39,7 @@ impl FromStr for AttributePairs {
|
|||
type Err = Error;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
let mut result = AttributePairs::new();
|
||||
let mut result = Self::new();
|
||||
|
||||
for line in split(input, ',') {
|
||||
let pair = split(line.trim(), '=');
|
||||
|
@ -67,16 +67,12 @@ fn split(value: &str, terminator: char) -> Vec<String> {
|
|||
let mut result = vec![];
|
||||
|
||||
let mut inside_quotes = false;
|
||||
let mut temp_string = String::new();
|
||||
let mut temp_string = String::with_capacity(1024);
|
||||
|
||||
for c in value.chars() {
|
||||
match c {
|
||||
'"' => {
|
||||
if inside_quotes {
|
||||
inside_quotes = false;
|
||||
} else {
|
||||
inside_quotes = true;
|
||||
}
|
||||
inside_quotes = !inside_quotes;
|
||||
temp_string.push(c);
|
||||
}
|
||||
k if (k == terminator) => {
|
||||
|
@ -84,7 +80,7 @@ fn split(value: &str, terminator: char) -> Vec<String> {
|
|||
temp_string.push(c);
|
||||
} else {
|
||||
result.push(temp_string);
|
||||
temp_string = String::new();
|
||||
temp_string = String::with_capacity(1024);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
|
@ -100,6 +96,7 @@ fn split(value: &str, terminator: char) -> Vec<String> {
|
|||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
|
|
49
src/error.rs
49
src/error.rs
|
@ -76,6 +76,10 @@ pub enum ErrorKind {
|
|||
/// An unexpected value.
|
||||
UnexpectedAttribute(String),
|
||||
|
||||
#[fail(display = "Unexpected Tag: {:?}", _0)]
|
||||
/// An unexpected tag.
|
||||
UnexpectedTag(String),
|
||||
|
||||
/// Hints that destructuring should not be exhaustive.
|
||||
///
|
||||
/// This enum may grow additional variants, so this makes sure clients
|
||||
|
@ -103,11 +107,11 @@ impl fmt::Display for Error {
|
|||
}
|
||||
|
||||
impl From<ErrorKind> for Error {
|
||||
fn from(kind: ErrorKind) -> Error { Error::from(Context::new(kind)) }
|
||||
fn from(kind: ErrorKind) -> Self { Self::from(Context::new(kind)) }
|
||||
}
|
||||
|
||||
impl From<Context<ErrorKind>> for Error {
|
||||
fn from(inner: Context<ErrorKind>) -> Error { Error { inner } }
|
||||
fn from(inner: Context<ErrorKind>) -> Self { Self { inner } }
|
||||
}
|
||||
|
||||
impl Error {
|
||||
|
@ -119,6 +123,10 @@ impl Error {
|
|||
Self::from(ErrorKind::UnexpectedAttribute(value.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) fn unexpected_tag<T: ToString>(value: T) -> Self {
|
||||
Self::from(ErrorKind::UnexpectedTag(value.to_string()))
|
||||
}
|
||||
|
||||
pub(crate) fn invalid_input() -> Self { Self::from(ErrorKind::InvalidInput) }
|
||||
|
||||
pub(crate) fn parse_int_error<T: ToString>(value: T) -> Self {
|
||||
|
@ -157,17 +165,6 @@ impl Error {
|
|||
|
||||
pub(crate) fn io<T: ToString>(value: T) -> Self { Self::from(ErrorKind::Io(value.to_string())) }
|
||||
|
||||
pub(crate) fn required_version<T, U>(required_version: T, specified_version: U) -> Self
|
||||
where
|
||||
T: ToString,
|
||||
U: ToString,
|
||||
{
|
||||
Self::from(ErrorKind::VersionError(
|
||||
required_version.to_string(),
|
||||
specified_version.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn builder_error<T: ToString>(value: T) -> Self {
|
||||
Self::from(ErrorKind::BuilderError(value.to_string()))
|
||||
}
|
||||
|
@ -182,23 +179,39 @@ impl Error {
|
|||
}
|
||||
|
||||
impl From<::std::num::ParseIntError> for Error {
|
||||
fn from(value: ::std::num::ParseIntError) -> Self { Error::parse_int_error(value) }
|
||||
fn from(value: ::std::num::ParseIntError) -> Self { Self::parse_int_error(value) }
|
||||
}
|
||||
|
||||
impl From<::std::num::ParseFloatError> for Error {
|
||||
fn from(value: ::std::num::ParseFloatError) -> Self { Error::parse_float_error(value) }
|
||||
fn from(value: ::std::num::ParseFloatError) -> Self { Self::parse_float_error(value) }
|
||||
}
|
||||
|
||||
impl From<::std::io::Error> for Error {
|
||||
fn from(value: ::std::io::Error) -> Self { Error::io(value) }
|
||||
fn from(value: ::std::io::Error) -> Self { Self::io(value) }
|
||||
}
|
||||
|
||||
impl From<::chrono::ParseError> for Error {
|
||||
fn from(value: ::chrono::ParseError) -> Self { Error::chrono(value) }
|
||||
fn from(value: ::chrono::ParseError) -> Self { Self::chrono(value) }
|
||||
}
|
||||
|
||||
impl From<::strum::ParseError> for Error {
|
||||
fn from(value: ::strum::ParseError) -> Self {
|
||||
Error::custom(value) // TODO!
|
||||
Self::custom(value) // TODO!
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Error {
|
||||
fn from(value: String) -> Self { Self::custom(value) }
|
||||
}
|
||||
|
||||
impl From<::core::convert::Infallible> for Error {
|
||||
fn from(_: ::core::convert::Infallible) -> Self {
|
||||
Self::custom("An Infallible error has been returned! (this should never happen!)")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<::hex::FromHexError> for Error {
|
||||
fn from(value: ::hex::FromHexError) -> Self {
|
||||
Self::custom(value) // TODO!
|
||||
}
|
||||
}
|
||||
|
|
14
src/lib.rs
14
src/lib.rs
|
@ -1,4 +1,5 @@
|
|||
#![forbid(unsafe_code)]
|
||||
#![feature(option_flattening)]
|
||||
#![warn(
|
||||
//clippy::pedantic,
|
||||
clippy::nursery,
|
||||
|
@ -35,20 +36,23 @@
|
|||
//! assert!(m3u8.parse::<MediaPlaylist>().is_ok());
|
||||
//! ```
|
||||
|
||||
pub use error::{Error, ErrorKind};
|
||||
pub use master_playlist::{MasterPlaylist, MasterPlaylistBuilder};
|
||||
pub use media_playlist::{MediaPlaylist, MediaPlaylistBuilder};
|
||||
pub use media_segment::{MediaSegment, MediaSegmentBuilder};
|
||||
pub use error::Error;
|
||||
pub use master_playlist::MasterPlaylist;
|
||||
pub use media_playlist::MediaPlaylist;
|
||||
pub use media_segment::MediaSegment;
|
||||
|
||||
pub mod tags;
|
||||
pub mod types;
|
||||
|
||||
#[macro_use]
|
||||
mod utils;
|
||||
mod attribute;
|
||||
mod error;
|
||||
mod line;
|
||||
mod master_playlist;
|
||||
mod media_playlist;
|
||||
mod media_segment;
|
||||
mod utils;
|
||||
mod traits;
|
||||
|
||||
pub use error::Result;
|
||||
pub use traits::*;
|
||||
|
|
120
src/line.rs
120
src/line.rs
|
@ -16,46 +16,46 @@ impl FromStr for Lines {
|
|||
type Err = Error;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
let mut result = Lines::new();
|
||||
let mut result = Self::new();
|
||||
|
||||
let mut stream_inf = false;
|
||||
let mut stream_inf_line = None;
|
||||
|
||||
for l in input.lines() {
|
||||
let line = l.trim();
|
||||
let raw_line = l.trim();
|
||||
|
||||
if line.is_empty() {
|
||||
if raw_line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pline = {
|
||||
if line.starts_with(tags::ExtXStreamInf::PREFIX) {
|
||||
let line = {
|
||||
if raw_line.starts_with(tags::ExtXStreamInf::PREFIX) {
|
||||
stream_inf = true;
|
||||
stream_inf_line = Some(line);
|
||||
stream_inf_line = Some(raw_line);
|
||||
|
||||
continue;
|
||||
} else if line.starts_with("#EXT") {
|
||||
Line::Tag(line.parse()?)
|
||||
} else if line.starts_with('#') {
|
||||
} else if raw_line.starts_with("#EXT") {
|
||||
Line::Tag(raw_line.parse()?)
|
||||
} else if raw_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 {
|
||||
let res = Line::Tag(format!("{}\n{}", first_line, line).parse()?);
|
||||
let res = Line::Tag(format!("{}\n{}", first_line, raw_line).parse()?);
|
||||
stream_inf_line = None;
|
||||
res
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
Line::Uri(line.trim().to_string())
|
||||
Line::Uri(raw_line.to_string())
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
result.push(pline);
|
||||
result.push(line);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
|
@ -79,14 +79,14 @@ impl DerefMut for Lines {
|
|||
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Line {
|
||||
Tag(Tag),
|
||||
Uri(String),
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Tag {
|
||||
ExtM3u(tags::ExtM3u),
|
||||
ExtXVersion(tags::ExtXVersion),
|
||||
|
@ -116,29 +116,29 @@ pub enum Tag {
|
|||
impl fmt::Display for Tag {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match &self {
|
||||
Tag::ExtM3u(value) => value.fmt(f),
|
||||
Tag::ExtXVersion(value) => value.fmt(f),
|
||||
Tag::ExtInf(value) => value.fmt(f),
|
||||
Tag::ExtXByteRange(value) => value.fmt(f),
|
||||
Tag::ExtXDiscontinuity(value) => value.fmt(f),
|
||||
Tag::ExtXKey(value) => value.fmt(f),
|
||||
Tag::ExtXMap(value) => value.fmt(f),
|
||||
Tag::ExtXProgramDateTime(value) => value.fmt(f),
|
||||
Tag::ExtXDateRange(value) => value.fmt(f),
|
||||
Tag::ExtXTargetDuration(value) => value.fmt(f),
|
||||
Tag::ExtXMediaSequence(value) => value.fmt(f),
|
||||
Tag::ExtXDiscontinuitySequence(value) => value.fmt(f),
|
||||
Tag::ExtXEndList(value) => value.fmt(f),
|
||||
Tag::ExtXPlaylistType(value) => value.fmt(f),
|
||||
Tag::ExtXIFramesOnly(value) => value.fmt(f),
|
||||
Tag::ExtXMedia(value) => value.fmt(f),
|
||||
Tag::ExtXStreamInf(value) => value.fmt(f),
|
||||
Tag::ExtXIFrameStreamInf(value) => value.fmt(f),
|
||||
Tag::ExtXSessionData(value) => value.fmt(f),
|
||||
Tag::ExtXSessionKey(value) => value.fmt(f),
|
||||
Tag::ExtXIndependentSegments(value) => value.fmt(f),
|
||||
Tag::ExtXStart(value) => value.fmt(f),
|
||||
Tag::Unknown(value) => value.fmt(f),
|
||||
Self::ExtM3u(value) => value.fmt(f),
|
||||
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::ExtXStreamInf(value) => value.fmt(f),
|
||||
Self::ExtXIFrameStreamInf(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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -148,51 +148,51 @@ impl FromStr for Tag {
|
|||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
if input.starts_with(tags::ExtM3u::PREFIX) {
|
||||
input.parse().map(Tag::ExtM3u)
|
||||
input.parse().map(Self::ExtM3u)
|
||||
} else if input.starts_with(tags::ExtXVersion::PREFIX) {
|
||||
input.parse().map(Tag::ExtXVersion)
|
||||
input.parse().map(Self::ExtXVersion)
|
||||
} else if input.starts_with(tags::ExtInf::PREFIX) {
|
||||
input.parse().map(Tag::ExtInf)
|
||||
input.parse().map(Self::ExtInf)
|
||||
} else if input.starts_with(tags::ExtXByteRange::PREFIX) {
|
||||
input.parse().map(Tag::ExtXByteRange)
|
||||
input.parse().map(Self::ExtXByteRange)
|
||||
} else if input.starts_with(tags::ExtXDiscontinuity::PREFIX) {
|
||||
input.parse().map(Tag::ExtXDiscontinuity)
|
||||
input.parse().map(Self::ExtXDiscontinuity)
|
||||
} else if input.starts_with(tags::ExtXKey::PREFIX) {
|
||||
input.parse().map(Tag::ExtXKey)
|
||||
input.parse().map(Self::ExtXKey)
|
||||
} else if input.starts_with(tags::ExtXMap::PREFIX) {
|
||||
input.parse().map(Tag::ExtXMap)
|
||||
input.parse().map(Self::ExtXMap)
|
||||
} else if input.starts_with(tags::ExtXProgramDateTime::PREFIX) {
|
||||
input.parse().map(Tag::ExtXProgramDateTime)
|
||||
input.parse().map(Self::ExtXProgramDateTime)
|
||||
} else if input.starts_with(tags::ExtXTargetDuration::PREFIX) {
|
||||
input.parse().map(Tag::ExtXTargetDuration)
|
||||
input.parse().map(Self::ExtXTargetDuration)
|
||||
} else if input.starts_with(tags::ExtXDateRange::PREFIX) {
|
||||
input.parse().map(Tag::ExtXDateRange)
|
||||
input.parse().map(Self::ExtXDateRange)
|
||||
} else if input.starts_with(tags::ExtXMediaSequence::PREFIX) {
|
||||
input.parse().map(Tag::ExtXMediaSequence)
|
||||
input.parse().map(Self::ExtXMediaSequence)
|
||||
} else if input.starts_with(tags::ExtXDiscontinuitySequence::PREFIX) {
|
||||
input.parse().map(Tag::ExtXDiscontinuitySequence)
|
||||
input.parse().map(Self::ExtXDiscontinuitySequence)
|
||||
} else if input.starts_with(tags::ExtXEndList::PREFIX) {
|
||||
input.parse().map(Tag::ExtXEndList)
|
||||
input.parse().map(Self::ExtXEndList)
|
||||
} else if input.starts_with(tags::ExtXPlaylistType::PREFIX) {
|
||||
input.parse().map(Tag::ExtXPlaylistType)
|
||||
input.parse().map(Self::ExtXPlaylistType)
|
||||
} else if input.starts_with(tags::ExtXIFramesOnly::PREFIX) {
|
||||
input.parse().map(Tag::ExtXIFramesOnly)
|
||||
input.parse().map(Self::ExtXIFramesOnly)
|
||||
} else if input.starts_with(tags::ExtXMedia::PREFIX) {
|
||||
input.parse().map(Tag::ExtXMedia).map_err(Error::custom)
|
||||
input.parse().map(Self::ExtXMedia).map_err(Error::custom)
|
||||
} else if input.starts_with(tags::ExtXStreamInf::PREFIX) {
|
||||
input.parse().map(Tag::ExtXStreamInf)
|
||||
input.parse().map(Self::ExtXStreamInf)
|
||||
} else if input.starts_with(tags::ExtXIFrameStreamInf::PREFIX) {
|
||||
input.parse().map(Tag::ExtXIFrameStreamInf)
|
||||
input.parse().map(Self::ExtXIFrameStreamInf)
|
||||
} else if input.starts_with(tags::ExtXSessionData::PREFIX) {
|
||||
input.parse().map(Tag::ExtXSessionData)
|
||||
input.parse().map(Self::ExtXSessionData)
|
||||
} else if input.starts_with(tags::ExtXSessionKey::PREFIX) {
|
||||
input.parse().map(Tag::ExtXSessionKey)
|
||||
input.parse().map(Self::ExtXSessionKey)
|
||||
} else if input.starts_with(tags::ExtXIndependentSegments::PREFIX) {
|
||||
input.parse().map(Tag::ExtXIndependentSegments)
|
||||
input.parse().map(Self::ExtXIndependentSegments)
|
||||
} else if input.starts_with(tags::ExtXStart::PREFIX) {
|
||||
input.parse().map(Tag::ExtXStart)
|
||||
input.parse().map(Self::ExtXStart)
|
||||
} else {
|
||||
Ok(Tag::Unknown(input.to_string()))
|
||||
Ok(Self::Unknown(input.to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use std::collections::HashSet;
|
||||
use std::fmt;
|
||||
use std::iter;
|
||||
use std::str::FromStr;
|
||||
|
||||
use derive_builder::Builder;
|
||||
|
@ -10,90 +9,210 @@ use crate::tags::{
|
|||
ExtM3u, ExtXIFrameStreamInf, ExtXIndependentSegments, ExtXMedia, ExtXSessionData,
|
||||
ExtXSessionKey, ExtXStart, ExtXStreamInf, ExtXVersion,
|
||||
};
|
||||
use crate::types::{ClosedCaptions, MediaType, ProtocolVersion, RequiredVersion};
|
||||
use crate::Error;
|
||||
use crate::types::{ClosedCaptions, MediaType, ProtocolVersion};
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// Master playlist.
|
||||
#[derive(Debug, Clone, Builder)]
|
||||
#[derive(Debug, Clone, Builder, PartialEq)]
|
||||
#[builder(build_fn(validate = "Self::validate"))]
|
||||
#[builder(setter(into, strip_option))]
|
||||
/// Master playlist.
|
||||
pub struct MasterPlaylist {
|
||||
#[builder(default, setter(name = "version"))]
|
||||
/// Sets the protocol compatibility version of the resulting playlist.
|
||||
///
|
||||
/// If the resulting playlist has tags which requires a compatibility
|
||||
/// version greater than `version`,
|
||||
/// `build()` method will fail with an `ErrorKind::InvalidInput` error.
|
||||
///
|
||||
/// The default is the maximum version among the tags in the playlist.
|
||||
version_tag: ExtXVersion,
|
||||
#[builder(default)]
|
||||
/// Sets the [ExtXIndependentSegments] tag.
|
||||
/// Sets the [`ExtXIndependentSegments`] tag.
|
||||
///
|
||||
/// # Note
|
||||
/// This tag is optional.
|
||||
independent_segments_tag: Option<ExtXIndependentSegments>,
|
||||
#[builder(default)]
|
||||
/// Sets the [ExtXStart] tag.
|
||||
/// Sets the [`ExtXStart`] tag.
|
||||
///
|
||||
/// # Note
|
||||
/// This tag is optional.
|
||||
start_tag: Option<ExtXStart>,
|
||||
/// Sets the [ExtXMedia] tag.
|
||||
#[builder(default)]
|
||||
/// Sets the [`ExtXMedia`] tag.
|
||||
///
|
||||
/// # Note
|
||||
/// This tag is optional.
|
||||
media_tags: Vec<ExtXMedia>,
|
||||
/// Sets all [ExtXStreamInf]s.
|
||||
#[builder(default)]
|
||||
/// Sets all [`ExtXStreamInf`] tags.
|
||||
///
|
||||
/// # Note
|
||||
/// This tag is optional.
|
||||
stream_inf_tags: Vec<ExtXStreamInf>,
|
||||
/// Sets all [ExtXIFrameStreamInf]s.
|
||||
#[builder(default)]
|
||||
/// Sets all [`ExtXIFrameStreamInf`] tags.
|
||||
///
|
||||
/// # Note
|
||||
/// This tag is optional.
|
||||
i_frame_stream_inf_tags: Vec<ExtXIFrameStreamInf>,
|
||||
/// Sets all [ExtXSessionData]s.
|
||||
#[builder(default)]
|
||||
/// Sets all [`ExtXSessionData`] tags.
|
||||
///
|
||||
/// # Note
|
||||
/// This tag is optional.
|
||||
session_data_tags: Vec<ExtXSessionData>,
|
||||
/// Sets all [ExtXSessionKey]s.
|
||||
#[builder(default)]
|
||||
/// Sets all [`ExtXSessionKey`] tags.
|
||||
///
|
||||
/// # Note
|
||||
/// This tag is optional.
|
||||
session_key_tags: Vec<ExtXSessionKey>,
|
||||
}
|
||||
|
||||
impl MasterPlaylist {
|
||||
/// Returns a Builder for a MasterPlaylist.
|
||||
/// Returns a Builder for a [`MasterPlaylist`].
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use hls_m3u8::tags::ExtXStart;
|
||||
/// use hls_m3u8::MasterPlaylist;
|
||||
///
|
||||
/// # fn main() -> Result<(), hls_m3u8::Error> {
|
||||
/// MasterPlaylist::builder()
|
||||
/// .start_tag(ExtXStart::new(20.123456))
|
||||
/// .build()?;
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
pub fn builder() -> MasterPlaylistBuilder { MasterPlaylistBuilder::default() }
|
||||
|
||||
/// Returns the `EXT-X-VERSION` tag contained in the playlist.
|
||||
pub const fn version_tag(&self) -> ExtXVersion { self.version_tag }
|
||||
|
||||
/// Returns the `EXT-X-INDEPENDENT-SEGMENTS` tag contained in the playlist.
|
||||
pub const fn independent_segments_tag(&self) -> Option<ExtXIndependentSegments> {
|
||||
/// Returns the [`ExtXIndependentSegments`] tag contained in the playlist.
|
||||
pub const fn independent_segments(&self) -> Option<ExtXIndependentSegments> {
|
||||
self.independent_segments_tag
|
||||
}
|
||||
|
||||
/// Returns the `EXT-X-START` tag contained in the playlist.
|
||||
pub const fn start_tag(&self) -> Option<ExtXStart> { self.start_tag }
|
||||
/// Sets the [`ExtXIndependentSegments`] tag contained in the playlist.
|
||||
pub fn set_independent_segments<T>(&mut self, value: Option<T>) -> &mut Self
|
||||
where
|
||||
T: Into<ExtXIndependentSegments>,
|
||||
{
|
||||
self.independent_segments_tag = value.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the `EXT-X-MEDIA` tags contained in the playlist.
|
||||
pub fn media_tags(&self) -> &[ExtXMedia] { &self.media_tags }
|
||||
/// Returns the [`ExtXStart`] tag contained in the playlist.
|
||||
pub const fn start(&self) -> Option<ExtXStart> { self.start_tag }
|
||||
|
||||
/// Returns the `EXT-X-STREAM-INF` tags contained in the playlist.
|
||||
pub fn stream_inf_tags(&self) -> &[ExtXStreamInf] { &self.stream_inf_tags }
|
||||
/// Sets the [`ExtXStart`] tag contained in the playlist.
|
||||
pub fn set_start<T>(&mut self, value: Option<T>) -> &mut Self
|
||||
where
|
||||
T: Into<ExtXStart>,
|
||||
{
|
||||
self.start_tag = value.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the `EXT-X-I-FRAME-STREAM-INF` tags contained in the playlist.
|
||||
pub fn i_frame_stream_inf_tags(&self) -> &[ExtXIFrameStreamInf] {
|
||||
/// Returns the [`ExtXMedia`] tags contained in the playlist.
|
||||
pub const fn media_tags(&self) -> &Vec<ExtXMedia> { &self.media_tags }
|
||||
|
||||
/// Appends an [`ExtXMedia`].
|
||||
pub fn push_media_tag(&mut self, value: ExtXMedia) -> &mut Self {
|
||||
self.media_tags.push(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`ExtXMedia`] tags contained in the playlist.
|
||||
pub fn set_media_tags<T>(&mut self, value: Vec<T>) -> &mut Self
|
||||
where
|
||||
T: Into<ExtXMedia>,
|
||||
{
|
||||
self.media_tags = value.into_iter().map(Into::into).collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the [`ExtXStreamInf`] tags contained in the playlist.
|
||||
pub const fn stream_inf_tags(&self) -> &Vec<ExtXStreamInf> { &self.stream_inf_tags }
|
||||
|
||||
/// Appends an [`ExtXStreamInf`].
|
||||
pub fn push_stream_inf(&mut self, value: ExtXStreamInf) -> &mut Self {
|
||||
self.stream_inf_tags.push(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`ExtXStreamInf`] tags contained in the playlist.
|
||||
pub fn set_stream_inf_tags<T>(&mut self, value: Vec<T>) -> &mut Self
|
||||
where
|
||||
T: Into<ExtXStreamInf>,
|
||||
{
|
||||
self.stream_inf_tags = value.into_iter().map(Into::into).collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the [`ExtXIFrameStreamInf`] tags contained in the playlist.
|
||||
pub const fn i_frame_stream_inf_tags(&self) -> &Vec<ExtXIFrameStreamInf> {
|
||||
&self.i_frame_stream_inf_tags
|
||||
}
|
||||
|
||||
/// Returns the `EXT-X-SESSION-DATA` tags contained in the playlist.
|
||||
pub fn session_data_tags(&self) -> &[ExtXSessionData] { &self.session_data_tags }
|
||||
/// Appends an [`ExtXIFrameStreamInf`].
|
||||
pub fn push_i_frame_stream_inf(&mut self, value: ExtXIFrameStreamInf) -> &mut Self {
|
||||
self.i_frame_stream_inf_tags.push(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the `EXT-X-SESSION-KEY` tags contained in the playlist.
|
||||
pub fn session_key_tags(&self) -> &[ExtXSessionKey] { &self.session_key_tags }
|
||||
/// Sets the [`ExtXIFrameStreamInf`] tags contained in the playlist.
|
||||
pub fn set_i_frame_stream_inf_tags<T>(&mut self, value: Vec<T>) -> &mut Self
|
||||
where
|
||||
T: Into<ExtXIFrameStreamInf>,
|
||||
{
|
||||
self.i_frame_stream_inf_tags = value.into_iter().map(Into::into).collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the [`ExtXSessionData`] tags contained in the playlist.
|
||||
pub const fn session_data_tags(&self) -> &Vec<ExtXSessionData> { &self.session_data_tags }
|
||||
|
||||
/// Appends an [`ExtXSessionData`].
|
||||
pub fn push_session_data(&mut self, value: ExtXSessionData) -> &mut Self {
|
||||
self.session_data_tags.push(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`ExtXSessionData`] tags contained in the playlist.
|
||||
pub fn set_session_data_tags<T>(&mut self, value: Vec<T>) -> &mut Self
|
||||
where
|
||||
T: Into<ExtXSessionData>,
|
||||
{
|
||||
self.session_data_tags = value.into_iter().map(Into::into).collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the [`ExtXSessionKey`] tags contained in the playlist.
|
||||
pub const fn session_key_tags(&self) -> &Vec<ExtXSessionKey> { &self.session_key_tags }
|
||||
|
||||
/// Appends an [`ExtXSessionKey`].
|
||||
pub fn push_session_key(&mut self, value: ExtXSessionKey) -> &mut Self {
|
||||
self.session_key_tags.push(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`ExtXSessionKey`] tags contained in the playlist.
|
||||
pub fn set_session_key_tags<T>(&mut self, value: Vec<T>) -> &mut Self
|
||||
where
|
||||
T: Into<ExtXSessionKey>,
|
||||
{
|
||||
self.session_key_tags = value.into_iter().map(Into::into).collect();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RequiredVersion for MasterPlaylist {
|
||||
fn required_version(&self) -> ProtocolVersion { self.version_tag.version() }
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
required_version![
|
||||
self.independent_segments_tag,
|
||||
self.start_tag,
|
||||
self.media_tags,
|
||||
self.stream_inf_tags,
|
||||
self.i_frame_stream_inf_tags,
|
||||
self.session_data_tags,
|
||||
self.session_key_tags
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl MasterPlaylistBuilder {
|
||||
fn validate(&self) -> Result<(), String> {
|
||||
let required_version = self.required_version();
|
||||
let specified_version = self
|
||||
.version_tag
|
||||
.unwrap_or_else(|| required_version.into())
|
||||
.version();
|
||||
|
||||
if required_version > specified_version {
|
||||
return Err(Error::required_version(required_version, specified_version).to_string());
|
||||
}
|
||||
|
||||
self.validate_stream_inf_tags().map_err(|e| e.to_string())?;
|
||||
self.validate_i_frame_stream_inf_tags()
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
@ -103,54 +222,6 @@ impl MasterPlaylistBuilder {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
iter::empty()
|
||||
.chain(
|
||||
self.independent_segments_tag
|
||||
.iter()
|
||||
.map(|t| t.iter().map(|t| t.required_version()))
|
||||
.flatten(),
|
||||
)
|
||||
.chain(
|
||||
self.start_tag
|
||||
.iter()
|
||||
.map(|t| t.iter().map(|t| t.required_version()))
|
||||
.flatten(),
|
||||
)
|
||||
.chain(
|
||||
self.media_tags
|
||||
.iter()
|
||||
.map(|t| t.iter().map(|t| t.required_version()))
|
||||
.flatten(),
|
||||
)
|
||||
.chain(
|
||||
self.stream_inf_tags
|
||||
.iter()
|
||||
.map(|t| t.iter().map(|t| t.required_version()))
|
||||
.flatten(),
|
||||
)
|
||||
.chain(
|
||||
self.i_frame_stream_inf_tags
|
||||
.iter()
|
||||
.map(|t| t.iter().map(|t| t.required_version()))
|
||||
.flatten(),
|
||||
)
|
||||
.chain(
|
||||
self.session_data_tags
|
||||
.iter()
|
||||
.map(|t| t.iter().map(|t| t.required_version()))
|
||||
.flatten(),
|
||||
)
|
||||
.chain(
|
||||
self.session_key_tags
|
||||
.iter()
|
||||
.map(|t| t.iter().map(|t| t.required_version()))
|
||||
.flatten(),
|
||||
)
|
||||
.max()
|
||||
.unwrap_or_else(ProtocolVersion::latest)
|
||||
}
|
||||
|
||||
fn validate_stream_inf_tags(&self) -> crate::Result<()> {
|
||||
if let Some(value) = &self.stream_inf_tags {
|
||||
let mut has_none_closed_captions = false;
|
||||
|
@ -230,11 +301,29 @@ impl MasterPlaylistBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
impl RequiredVersion for MasterPlaylistBuilder {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
// TODO: the .flatten() can be removed as soon as `recursive traits` are
|
||||
// supported. (RequiredVersion is implemented for Option<T>, but
|
||||
// not for Option<Option<T>>)
|
||||
// https://github.com/rust-lang/chalk/issues/12
|
||||
required_version![
|
||||
self.independent_segments_tag.flatten(),
|
||||
self.start_tag.flatten(),
|
||||
self.media_tags,
|
||||
self.stream_inf_tags,
|
||||
self.i_frame_stream_inf_tags,
|
||||
self.session_data_tags,
|
||||
self.session_key_tags
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for MasterPlaylist {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
writeln!(f, "{}", ExtM3u)?;
|
||||
if self.version_tag.version() != ProtocolVersion::V1 {
|
||||
writeln!(f, "{}", self.version_tag)?;
|
||||
if self.required_version() != ProtocolVersion::V1 {
|
||||
writeln!(f, "{}", ExtXVersion::new(self.required_version()))?;
|
||||
}
|
||||
for t in &self.media_tags {
|
||||
writeln!(f, "{}", t)?;
|
||||
|
@ -265,7 +354,7 @@ impl FromStr for MasterPlaylist {
|
|||
type Err = Error;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
let mut builder = MasterPlaylist::builder();
|
||||
let mut builder = Self::builder();
|
||||
|
||||
let mut media_tags = vec![];
|
||||
let mut stream_inf_tags = vec![];
|
||||
|
@ -286,8 +375,10 @@ impl FromStr for MasterPlaylist {
|
|||
Tag::ExtM3u(_) => {
|
||||
return Err(Error::invalid_input());
|
||||
}
|
||||
Tag::ExtXVersion(t) => {
|
||||
builder.version(t.version());
|
||||
Tag::ExtXVersion(_) => {
|
||||
// This tag can be ignored, because the
|
||||
// MasterPlaylist will automatically set the
|
||||
// ExtXVersion tag to correct version!
|
||||
}
|
||||
Tag::ExtInf(_)
|
||||
| Tag::ExtXByteRange(_)
|
||||
|
@ -354,39 +445,39 @@ impl FromStr for MasterPlaylist {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
r#"#EXTM3U
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=150000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234
|
||||
http://example.com/low/index.m3u8
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=240000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234
|
||||
http://example.com/lo_mid/index.m3u8
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=440000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234
|
||||
http://example.com/hi_mid/index.m3u8
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=640000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=640x360
|
||||
http://example.com/high/index.m3u8
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.5"
|
||||
http://example.com/audio/index.m3u8
|
||||
"#
|
||||
.parse::<MasterPlaylist>()
|
||||
.unwrap();
|
||||
"#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();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
let input = r#"#EXTM3U
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=150000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234
|
||||
http://example.com/low/index.m3u8
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=240000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234
|
||||
http://example.com/lo_mid/index.m3u8
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=440000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234
|
||||
http://example.com/hi_mid/index.m3u8
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=640000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=640x360
|
||||
http://example.com/high/index.m3u8
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.5"
|
||||
http://example.com/audio/index.m3u8
|
||||
"#;
|
||||
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);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
use std::fmt;
|
||||
use std::iter;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
|
@ -11,47 +10,38 @@ use crate::tags::{
|
|||
ExtM3u, ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly, ExtXIndependentSegments,
|
||||
ExtXMediaSequence, ExtXPlaylistType, ExtXStart, ExtXTargetDuration, ExtXVersion,
|
||||
};
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::Error;
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::{Encrypted, Error, RequiredVersion};
|
||||
|
||||
/// Media playlist.
|
||||
#[derive(Debug, Clone, Builder)]
|
||||
#[derive(Debug, Clone, Builder, PartialEq, PartialOrd)]
|
||||
#[builder(build_fn(validate = "Self::validate"))]
|
||||
#[builder(setter(into, strip_option))]
|
||||
pub struct MediaPlaylist {
|
||||
/// Sets the protocol compatibility version of the resulting playlist.
|
||||
///
|
||||
/// If the resulting playlist has tags which requires a compatibility
|
||||
/// version greater than `version`,
|
||||
/// `build()` method will fail with an `ErrorKind::InvalidInput` error.
|
||||
///
|
||||
/// The default is the maximum version among the tags in the playlist.
|
||||
#[builder(setter(name = "version"))]
|
||||
version_tag: ExtXVersion,
|
||||
/// Sets the [ExtXTargetDuration] tag.
|
||||
/// Sets the [`ExtXTargetDuration`] tag.
|
||||
target_duration_tag: ExtXTargetDuration,
|
||||
#[builder(default)]
|
||||
/// Sets the [ExtXMediaSequence] tag.
|
||||
/// Sets the [`ExtXMediaSequence`] tag.
|
||||
media_sequence_tag: Option<ExtXMediaSequence>,
|
||||
#[builder(default)]
|
||||
/// Sets the [ExtXDiscontinuitySequence] tag.
|
||||
/// Sets the [`ExtXDiscontinuitySequence`] tag.
|
||||
discontinuity_sequence_tag: Option<ExtXDiscontinuitySequence>,
|
||||
#[builder(default)]
|
||||
/// Sets the [ExtXPlaylistType] tag.
|
||||
/// Sets the [`ExtXPlaylistType`] tag.
|
||||
playlist_type_tag: Option<ExtXPlaylistType>,
|
||||
#[builder(default)]
|
||||
/// Sets the [ExtXIFramesOnly] tag.
|
||||
/// Sets the [`ExtXIFramesOnly`] tag.
|
||||
i_frames_only_tag: Option<ExtXIFramesOnly>,
|
||||
#[builder(default)]
|
||||
/// Sets the [ExtXIndependentSegments] tag.
|
||||
/// Sets the [`ExtXIndependentSegments`] tag.
|
||||
independent_segments_tag: Option<ExtXIndependentSegments>,
|
||||
#[builder(default)]
|
||||
/// Sets the [ExtXStart] tag.
|
||||
/// Sets the [`ExtXStart`] tag.
|
||||
start_tag: Option<ExtXStart>,
|
||||
#[builder(default)]
|
||||
/// Sets the [ExtXEndList] tag.
|
||||
/// Sets the [`ExtXEndList`] tag.
|
||||
end_list_tag: Option<ExtXEndList>,
|
||||
/// Sets all [MediaSegment]s.
|
||||
/// Sets all [`MediaSegment`]s.
|
||||
segments: Vec<MediaSegment>,
|
||||
/// Sets the allowable excess duration of each media segment in the
|
||||
/// associated playlist.
|
||||
|
@ -68,20 +58,6 @@ pub struct MediaPlaylist {
|
|||
|
||||
impl MediaPlaylistBuilder {
|
||||
fn validate(&self) -> Result<(), String> {
|
||||
let required_version = self.required_version();
|
||||
let specified_version = self
|
||||
.version_tag
|
||||
.unwrap_or_else(|| required_version.into())
|
||||
.version();
|
||||
|
||||
if required_version > specified_version {
|
||||
return Err(Error::custom(format!(
|
||||
"required_version: {}, specified_version: {}",
|
||||
required_version, specified_version
|
||||
))
|
||||
.to_string());
|
||||
}
|
||||
|
||||
if let Some(target_duration) = &self.target_duration_tag {
|
||||
self.validate_media_segments(target_duration.duration())
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
@ -96,10 +72,12 @@ impl MediaPlaylistBuilder {
|
|||
for s in segments {
|
||||
// CHECK: `#EXT-X-TARGETDURATION`
|
||||
let segment_duration = s.inf_tag().duration();
|
||||
let rounded_segment_duration = if segment_duration.subsec_nanos() < 500_000_000 {
|
||||
Duration::from_secs(segment_duration.as_secs())
|
||||
} else {
|
||||
Duration::from_secs(segment_duration.as_secs() + 1)
|
||||
let rounded_segment_duration = {
|
||||
if segment_duration.subsec_nanos() < 500_000_000 {
|
||||
Duration::from_secs(segment_duration.as_secs())
|
||||
} else {
|
||||
Duration::from_secs(segment_duration.as_secs() + 1)
|
||||
}
|
||||
};
|
||||
|
||||
let max_segment_duration = {
|
||||
|
@ -138,72 +116,6 @@ impl MediaPlaylistBuilder {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
iter::empty()
|
||||
.chain(
|
||||
self.target_duration_tag
|
||||
.iter()
|
||||
.map(|t| t.required_version()),
|
||||
)
|
||||
.chain(self.media_sequence_tag.iter().map(|t| {
|
||||
if let Some(p) = t {
|
||||
p.required_version()
|
||||
} else {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}))
|
||||
.chain(self.discontinuity_sequence_tag.iter().map(|t| {
|
||||
if let Some(p) = t {
|
||||
p.required_version()
|
||||
} else {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}))
|
||||
.chain(self.playlist_type_tag.iter().map(|t| {
|
||||
if let Some(p) = t {
|
||||
p.required_version()
|
||||
} else {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}))
|
||||
.chain(self.i_frames_only_tag.iter().map(|t| {
|
||||
if let Some(p) = t {
|
||||
p.required_version()
|
||||
} else {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}))
|
||||
.chain(self.independent_segments_tag.iter().map(|t| {
|
||||
if let Some(p) = t {
|
||||
p.required_version()
|
||||
} else {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}))
|
||||
.chain(self.start_tag.iter().map(|t| {
|
||||
if let Some(p) = t {
|
||||
p.required_version()
|
||||
} else {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}))
|
||||
.chain(self.end_list_tag.iter().map(|t| {
|
||||
if let Some(p) = t {
|
||||
p.required_version()
|
||||
} else {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}))
|
||||
.chain(self.segments.iter().map(|t| {
|
||||
t.iter()
|
||||
.map(|s| s.required_version())
|
||||
.max()
|
||||
.unwrap_or(ProtocolVersion::V1)
|
||||
}))
|
||||
.max()
|
||||
.unwrap_or_else(ProtocolVersion::latest)
|
||||
}
|
||||
|
||||
/// Adds a media segment to the resulting playlist.
|
||||
pub fn push_segment<VALUE: Into<MediaSegment>>(&mut self, value: VALUE) -> &mut Self {
|
||||
if let Some(segments) = &mut self.segments {
|
||||
|
@ -214,57 +126,86 @@ impl MediaPlaylistBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
/// Parse the rest of the [MediaPlaylist] from an m3u8 file.
|
||||
/// Parse the rest of the [`MediaPlaylist`] from an m3u8 file.
|
||||
pub fn parse(&mut self, input: &str) -> crate::Result<MediaPlaylist> {
|
||||
parse_media_playlist(input, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl RequiredVersion for MediaPlaylistBuilder {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
required_version![
|
||||
self.target_duration_tag,
|
||||
self.media_sequence_tag,
|
||||
self.discontinuity_sequence_tag,
|
||||
self.playlist_type_tag,
|
||||
self.i_frames_only_tag,
|
||||
self.independent_segments_tag,
|
||||
self.start_tag,
|
||||
self.end_list_tag,
|
||||
self.segments
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl MediaPlaylist {
|
||||
/// Creates a [MediaPlaylistBuilder].
|
||||
/// Returns a builder for [`MediaPlaylist`].
|
||||
pub fn builder() -> MediaPlaylistBuilder { MediaPlaylistBuilder::default() }
|
||||
|
||||
/// Returns the `EXT-X-VERSION` tag contained in the playlist.
|
||||
pub const fn version_tag(&self) -> ExtXVersion { self.version_tag }
|
||||
|
||||
/// Returns the `EXT-X-TARGETDURATION` tag contained in the playlist.
|
||||
/// Returns the [`ExtXTargetDuration`] tag contained in the playlist.
|
||||
pub const fn target_duration_tag(&self) -> ExtXTargetDuration { self.target_duration_tag }
|
||||
|
||||
/// Returns the `EXT-X-MEDIA-SEQUENCE` tag contained in the playlist.
|
||||
pub const fn media_sequence_tag(&self) -> Option<ExtXMediaSequence> { self.media_sequence_tag }
|
||||
|
||||
/// Returns the `EXT-X-DISCONTINUITY-SEQUENCE` tag contained in the
|
||||
/// Returns the [`ExtXDiscontinuitySequence`] tag contained in the
|
||||
/// playlist.
|
||||
pub const fn discontinuity_sequence_tag(&self) -> Option<ExtXDiscontinuitySequence> {
|
||||
self.discontinuity_sequence_tag
|
||||
}
|
||||
|
||||
/// Returns the `EXT-X-PLAYLIST-TYPE` tag contained in the playlist.
|
||||
/// Returns the [`ExtXPlaylistType`] tag contained in the playlist.
|
||||
pub const fn playlist_type_tag(&self) -> Option<ExtXPlaylistType> { self.playlist_type_tag }
|
||||
|
||||
/// Returns the `EXT-X-I-FRAMES-ONLY` tag contained in the playlist.
|
||||
/// Returns the [`ExtXIFramesOnly`] tag contained in the playlist.
|
||||
pub const fn i_frames_only_tag(&self) -> Option<ExtXIFramesOnly> { self.i_frames_only_tag }
|
||||
|
||||
/// Returns the `EXT-X-INDEPENDENT-SEGMENTS` tag contained in the playlist.
|
||||
/// Returns the [`ExtXIndependentSegments`] tag contained in the playlist.
|
||||
pub const fn independent_segments_tag(&self) -> Option<ExtXIndependentSegments> {
|
||||
self.independent_segments_tag
|
||||
}
|
||||
|
||||
/// Returns the `EXT-X-START` tag contained in the playlist.
|
||||
/// Returns the [`ExtXStart`] tag contained in the playlist.
|
||||
pub const fn start_tag(&self) -> Option<ExtXStart> { self.start_tag }
|
||||
|
||||
/// Returns the `EXT-X-ENDLIST` tag contained in the playlist.
|
||||
/// Returns the [`ExtXEndList`] tag contained in the playlist.
|
||||
pub const fn end_list_tag(&self) -> Option<ExtXEndList> { self.end_list_tag }
|
||||
|
||||
/// Returns the media segments contained in the playlist.
|
||||
pub fn segments(&self) -> &[MediaSegment] { &self.segments }
|
||||
/// Returns the [`MediaSegment`]s contained in the playlist.
|
||||
pub const fn segments(&self) -> &Vec<MediaSegment> { &self.segments }
|
||||
}
|
||||
|
||||
impl RequiredVersion for MediaPlaylist {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
required_version![
|
||||
self.target_duration_tag,
|
||||
self.media_sequence_tag,
|
||||
self.discontinuity_sequence_tag,
|
||||
self.playlist_type_tag,
|
||||
self.i_frames_only_tag,
|
||||
self.independent_segments_tag,
|
||||
self.start_tag,
|
||||
self.end_list_tag,
|
||||
self.segments
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for MediaPlaylist {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
writeln!(f, "{}", ExtM3u)?;
|
||||
if self.version_tag.version() != ProtocolVersion::V1 {
|
||||
writeln!(f, "{}", self.version_tag)?;
|
||||
if self.required_version() != ProtocolVersion::V1 {
|
||||
writeln!(f, "{}", ExtXVersion::new(self.required_version()))?;
|
||||
}
|
||||
writeln!(f, "{}", self.target_duration_tag)?;
|
||||
if let Some(value) = &self.media_sequence_tag {
|
||||
|
@ -304,7 +245,8 @@ 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
|
||||
|
||||
let mut available_key_tags: Vec<crate::tags::ExtXKey> = vec![];
|
||||
|
||||
for (i, line) in input.parse::<Lines>()?.into_iter().enumerate() {
|
||||
match line {
|
||||
|
@ -317,10 +259,6 @@ fn parse_media_playlist(
|
|||
}
|
||||
match tag {
|
||||
Tag::ExtM3u(_) => return Err(Error::invalid_input()),
|
||||
Tag::ExtXVersion(t) => {
|
||||
builder.version(t.version());
|
||||
has_version = true;
|
||||
}
|
||||
Tag::ExtInf(t) => {
|
||||
has_partial_segment = true;
|
||||
segment.inf_tag(t);
|
||||
|
@ -336,10 +274,29 @@ fn parse_media_playlist(
|
|||
}
|
||||
Tag::ExtXKey(t) => {
|
||||
has_partial_segment = true;
|
||||
segment.push_key_tag(t);
|
||||
if available_key_tags.is_empty() {
|
||||
// An ExtXKey applies to every MediaSegment and to every Media
|
||||
// Initialization Section declared by an EXT-X-MAP tag, that appears
|
||||
// between it and the next EXT-X-KEY tag in the Playlist file with the
|
||||
// same KEYFORMAT attribute (or the end of the Playlist file).
|
||||
available_key_tags = available_key_tags
|
||||
.into_iter()
|
||||
.map(|k| {
|
||||
if t.key_format() == k.key_format() {
|
||||
t.clone()
|
||||
} else {
|
||||
k
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
} else {
|
||||
available_key_tags.push(t);
|
||||
}
|
||||
}
|
||||
Tag::ExtXMap(t) => {
|
||||
Tag::ExtXMap(mut t) => {
|
||||
has_partial_segment = true;
|
||||
|
||||
t.set_keys(available_key_tags.clone());
|
||||
segment.map_tag(t);
|
||||
}
|
||||
Tag::ExtXProgramDateTime(t) => {
|
||||
|
@ -379,7 +336,7 @@ fn parse_media_playlist(
|
|||
| Tag::ExtXIFrameStreamInf(_)
|
||||
| Tag::ExtXSessionData(_)
|
||||
| Tag::ExtXSessionKey(_) => {
|
||||
return Err(Error::custom(tag));
|
||||
return Err(Error::unexpected_tag(tag));
|
||||
}
|
||||
Tag::ExtXIndependentSegments(t) => {
|
||||
builder.independent_segments_tag(t);
|
||||
|
@ -387,7 +344,7 @@ fn parse_media_playlist(
|
|||
Tag::ExtXStart(t) => {
|
||||
builder.start_tag(t);
|
||||
}
|
||||
Tag::Unknown(_) => {
|
||||
Tag::Unknown(_) | Tag::ExtXVersion(_) => {
|
||||
// [6.3.1. General Client Responsibilities]
|
||||
// > ignore any unrecognized tags.
|
||||
}
|
||||
|
@ -395,18 +352,17 @@ fn parse_media_playlist(
|
|||
}
|
||||
Line::Uri(uri) => {
|
||||
segment.uri(uri);
|
||||
segment.keys(available_key_tags.clone());
|
||||
segments.push(segment.build().map_err(Error::builder_error)?);
|
||||
segment = MediaSegment::builder();
|
||||
has_partial_segment = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
@ -423,6 +379,7 @@ impl FromStr for MediaPlaylist {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn too_large_segment_duration_test() {
|
||||
|
|
|
@ -1,48 +1,143 @@
|
|||
use std::fmt;
|
||||
use std::iter;
|
||||
|
||||
use derive_builder::Builder;
|
||||
|
||||
use crate::tags::{
|
||||
ExtInf, ExtXByteRange, ExtXDateRange, ExtXDiscontinuity, ExtXKey, ExtXMap, ExtXProgramDateTime,
|
||||
};
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::{Encrypted, RequiredVersion};
|
||||
|
||||
/// Media segment.
|
||||
#[derive(Debug, Clone, Builder)]
|
||||
#[derive(Debug, Clone, Builder, PartialEq, PartialOrd)]
|
||||
#[builder(setter(into, strip_option))]
|
||||
/// Media segment.
|
||||
pub struct MediaSegment {
|
||||
#[builder(default)]
|
||||
/// Sets all [ExtXKey] tags.
|
||||
key_tags: Vec<ExtXKey>,
|
||||
/// Sets all [`ExtXKey`] tags.
|
||||
keys: Vec<ExtXKey>,
|
||||
#[builder(default)]
|
||||
/// Sets an [ExtXMap] tag.
|
||||
/// Sets an [`ExtXMap`] tag.
|
||||
map_tag: Option<ExtXMap>,
|
||||
#[builder(default)]
|
||||
/// Sets an [ExtXByteRange] tag.
|
||||
/// Sets an [`ExtXByteRange`] tag.
|
||||
byte_range_tag: Option<ExtXByteRange>,
|
||||
#[builder(default)]
|
||||
/// Sets an [ExtXDateRange] tag.
|
||||
/// Sets an [`ExtXDateRange`] tag.
|
||||
date_range_tag: Option<ExtXDateRange>,
|
||||
#[builder(default)]
|
||||
/// Sets an [ExtXDiscontinuity] tag.
|
||||
/// Sets an [`ExtXDiscontinuity`] tag.
|
||||
discontinuity_tag: Option<ExtXDiscontinuity>,
|
||||
#[builder(default)]
|
||||
/// Sets an [ExtXProgramDateTime] tag.
|
||||
/// Sets an [`ExtXProgramDateTime`] tag.
|
||||
program_date_time_tag: Option<ExtXProgramDateTime>,
|
||||
/// Sets an [ExtInf] tag.
|
||||
/// Sets an [`ExtInf`] tag.
|
||||
inf_tag: ExtInf,
|
||||
/// Sets an Uri.
|
||||
/// Sets an `URI`.
|
||||
uri: String,
|
||||
}
|
||||
|
||||
impl MediaSegment {
|
||||
/// Returns a Builder for a [`MasterPlaylist`].
|
||||
///
|
||||
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
||||
pub fn builder() -> MediaSegmentBuilder { MediaSegmentBuilder::default() }
|
||||
|
||||
/// Returns the `URI` of the media segment.
|
||||
pub const fn uri(&self) -> &String { &self.uri }
|
||||
|
||||
/// Sets the `URI` of the media segment.
|
||||
pub fn set_uri<T>(&mut self, value: T) -> &mut Self
|
||||
where
|
||||
T: Into<String>,
|
||||
{
|
||||
self.uri = value.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the [`ExtInf`] tag associated with the media segment.
|
||||
pub const fn inf_tag(&self) -> &ExtInf { &self.inf_tag }
|
||||
|
||||
/// Sets the [`ExtInf`] tag associated with the media segment.
|
||||
pub fn set_inf_tag<T>(&mut self, value: T) -> &mut Self
|
||||
where
|
||||
T: Into<ExtInf>,
|
||||
{
|
||||
self.inf_tag = value.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the [`ExtXByteRange`] tag associated with the media segment.
|
||||
pub const fn byte_range_tag(&self) -> Option<ExtXByteRange> { self.byte_range_tag }
|
||||
|
||||
/// Sets the [`ExtXByteRange`] tag associated with the media segment.
|
||||
pub fn set_byte_range_tag<T>(&mut self, value: Option<T>) -> &mut Self
|
||||
where
|
||||
T: Into<ExtXByteRange>,
|
||||
{
|
||||
self.byte_range_tag = value.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the [`ExtXDateRange`] tag associated with the media segment.
|
||||
pub const fn date_range_tag(&self) -> &Option<ExtXDateRange> { &self.date_range_tag }
|
||||
|
||||
/// Sets the [`ExtXDateRange`] tag associated with the media segment.
|
||||
pub fn set_date_range_tag<T>(&mut self, value: Option<T>) -> &mut Self
|
||||
where
|
||||
T: Into<ExtXDateRange>,
|
||||
{
|
||||
self.date_range_tag = value.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the [`ExtXDiscontinuity`] tag associated with the media segment.
|
||||
pub const fn discontinuity_tag(&self) -> Option<ExtXDiscontinuity> { self.discontinuity_tag }
|
||||
|
||||
/// Sets the [`ExtXDiscontinuity`] tag associated with the media segment.
|
||||
pub fn set_discontinuity_tag<T>(&mut self, value: Option<T>) -> &mut Self
|
||||
where
|
||||
T: Into<ExtXDiscontinuity>,
|
||||
{
|
||||
self.discontinuity_tag = value.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the [`ExtXProgramDateTime`] tag associated with the media
|
||||
/// segment.
|
||||
pub const fn program_date_time_tag(&self) -> Option<ExtXProgramDateTime> {
|
||||
self.program_date_time_tag
|
||||
}
|
||||
|
||||
/// Sets the [`ExtXProgramDateTime`] tag associated with the media
|
||||
/// segment.
|
||||
pub fn set_program_date_time_tag<T>(&mut self, value: Option<T>) -> &mut Self
|
||||
where
|
||||
T: Into<ExtXProgramDateTime>,
|
||||
{
|
||||
self.program_date_time_tag = value.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the [`ExtXMap`] tag associated with the media segment.
|
||||
pub const fn map_tag(&self) -> &Option<ExtXMap> { &self.map_tag }
|
||||
|
||||
/// Sets the [`ExtXMap`] tag associated with the media segment.
|
||||
pub fn set_map_tag<T>(&mut self, value: Option<T>) -> &mut Self
|
||||
where
|
||||
T: Into<ExtXMap>,
|
||||
{
|
||||
self.map_tag = value.map(Into::into);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl MediaSegmentBuilder {
|
||||
/// Pushes an [ExtXKey] tag.
|
||||
/// Pushes an [`ExtXKey`] tag.
|
||||
pub fn push_key_tag<VALUE: Into<ExtXKey>>(&mut self, value: VALUE) -> &mut Self {
|
||||
if let Some(key_tags) = &mut self.key_tags {
|
||||
if let Some(key_tags) = &mut self.keys {
|
||||
key_tags.push(value.into());
|
||||
} else {
|
||||
self.key_tags = Some(vec![value.into()]);
|
||||
self.keys = Some(vec![value.into()]);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
@ -50,7 +145,7 @@ impl MediaSegmentBuilder {
|
|||
|
||||
impl fmt::Display for MediaSegment {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
for value in &self.key_tags {
|
||||
for value in &self.keys {
|
||||
writeln!(f, "{}", value)?;
|
||||
}
|
||||
if let Some(value) = &self.map_tag {
|
||||
|
@ -68,59 +163,59 @@ impl fmt::Display for MediaSegment {
|
|||
if let Some(value) = &self.program_date_time_tag {
|
||||
writeln!(f, "{}", value)?;
|
||||
}
|
||||
writeln!(f, "{},", self.inf_tag)?;
|
||||
writeln!(f, "{}", self.inf_tag)?; // TODO: there might be a `,` missing
|
||||
writeln!(f, "{}", self.uri)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl MediaSegment {
|
||||
/// Creates a [MediaSegmentBuilder].
|
||||
pub fn builder() -> MediaSegmentBuilder { MediaSegmentBuilder::default() }
|
||||
|
||||
/// Returns the URI of the media segment.
|
||||
pub const fn uri(&self) -> &String { &self.uri }
|
||||
|
||||
/// Returns the `EXT-X-INF` tag associated with the media segment.
|
||||
pub const fn inf_tag(&self) -> &ExtInf { &self.inf_tag }
|
||||
|
||||
/// Returns the `EXT-X-BYTERANGE` tag associated with the media segment.
|
||||
pub const fn byte_range_tag(&self) -> Option<ExtXByteRange> { self.byte_range_tag }
|
||||
|
||||
/// Returns the `EXT-X-DATERANGE` tag associated with the media segment.
|
||||
pub fn date_range_tag(&self) -> Option<&ExtXDateRange> { self.date_range_tag.as_ref() }
|
||||
|
||||
/// Returns the `EXT-X-DISCONTINUITY` tag associated with the media segment.
|
||||
pub const fn discontinuity_tag(&self) -> Option<ExtXDiscontinuity> { self.discontinuity_tag }
|
||||
|
||||
/// Returns the `EXT-X-PROGRAM-DATE-TIME` tag associated with the media
|
||||
/// segment.
|
||||
pub fn program_date_time_tag(&self) -> Option<&ExtXProgramDateTime> {
|
||||
self.program_date_time_tag.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the `EXT-X-MAP` tag associated with the media segment.
|
||||
pub fn map_tag(&self) -> Option<&ExtXMap> { self.map_tag.as_ref() }
|
||||
|
||||
/// Returns the `EXT-X-KEY` tags associated with the media segment.
|
||||
pub fn key_tags(&self) -> &[ExtXKey] { &self.key_tags }
|
||||
}
|
||||
|
||||
impl RequiredVersion for MediaSegment {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
iter::empty()
|
||||
.chain(self.key_tags.iter().map(|t| t.required_version()))
|
||||
.chain(self.map_tag.iter().map(|t| t.required_version()))
|
||||
.chain(self.byte_range_tag.iter().map(|t| t.required_version()))
|
||||
.chain(self.date_range_tag.iter().map(|t| t.required_version()))
|
||||
.chain(self.discontinuity_tag.iter().map(|t| t.required_version()))
|
||||
.chain(
|
||||
self.program_date_time_tag
|
||||
.iter()
|
||||
.map(|t| t.required_version()),
|
||||
)
|
||||
.chain(iter::once(self.inf_tag.required_version()))
|
||||
.max()
|
||||
.unwrap_or(ProtocolVersion::V7)
|
||||
required_version![
|
||||
self.keys,
|
||||
self.map_tag,
|
||||
self.byte_range_tag,
|
||||
self.date_range_tag,
|
||||
self.discontinuity_tag,
|
||||
self.program_date_time_tag,
|
||||
self.inf_tag
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl Encrypted for MediaSegment {
|
||||
fn keys(&self) -> &Vec<ExtXKey> { &self.keys }
|
||||
|
||||
fn keys_mut(&mut self) -> &mut Vec<ExtXKey> { &mut self.keys }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(
|
||||
MediaSegment::builder()
|
||||
.keys(vec![ExtXKey::empty()])
|
||||
.map_tag(ExtXMap::new("https://www.example.com/"))
|
||||
.byte_range_tag(ExtXByteRange::new(20, Some(5)))
|
||||
//.date_range_tag() // TODO!
|
||||
.discontinuity_tag(ExtXDiscontinuity)
|
||||
.inf_tag(ExtInf::new(Duration::from_secs(4)))
|
||||
.uri("http://www.uri.com/")
|
||||
.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"
|
||||
.to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::utils::tag;
|
||||
use crate::Error;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// # [4.4.1.1. EXTM3U]
|
||||
/// # [4.3.1.1. EXTM3U]
|
||||
///
|
||||
/// The [`ExtM3u`] tag indicates that the file is an **Ext**ended **[`M3U`]**
|
||||
/// Playlist file.
|
||||
/// It is the at the start of every [`Media Playlist`] and [`Master Playlist`].
|
||||
|
@ -32,8 +33,7 @@ use crate::Error;
|
|||
/// [`Media Playlist`]: crate::MediaPlaylist
|
||||
/// [`Master Playlist`]: crate::MasterPlaylist
|
||||
/// [`M3U`]: https://en.wikipedia.org/wiki/M3U
|
||||
/// [4.4.1.1. EXTM3U]:
|
||||
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.4.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)]
|
||||
pub struct ExtM3u;
|
||||
|
||||
|
@ -62,6 +62,7 @@ impl FromStr for ExtM3u {
|
|||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
@ -70,7 +71,7 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!("#EXTM3U".parse::<ExtM3u>().ok(), Some(ExtM3u));
|
||||
assert_eq!("#EXTM3U".parse::<ExtM3u>().unwrap(), ExtM3u);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::utils::tag;
|
||||
use crate::Error;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// # [4.4.1.2. EXT-X-VERSION]
|
||||
/// # [4.3.1.2. EXT-X-VERSION]
|
||||
///
|
||||
/// The [`ExtXVersion`] tag indicates the compatibility version of the
|
||||
/// [`Master Playlist`] or [`Media Playlist`] file.
|
||||
/// It applies to the entire Playlist.
|
||||
|
@ -41,8 +42,7 @@ use crate::Error;
|
|||
///
|
||||
/// [`Media Playlist`]: crate::MediaPlaylist
|
||||
/// [`Master Playlist`]: crate::MasterPlaylist
|
||||
/// [4.4.1.2. EXT-X-VERSION]:
|
||||
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.4.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)]
|
||||
pub struct ExtXVersion(ProtocolVersion);
|
||||
|
||||
|
@ -104,6 +104,7 @@ impl FromStr for ExtXVersion {
|
|||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
|
|
@ -3,11 +3,12 @@ use std::ops::{Deref, DerefMut};
|
|||
use std::str::FromStr;
|
||||
|
||||
use crate::attribute::AttributePairs;
|
||||
use crate::types::{ProtocolVersion, RequiredVersion, StreamInf};
|
||||
use crate::types::{HdcpLevel, ProtocolVersion, StreamInf, StreamInfBuilder};
|
||||
use crate::utils::{quote, tag, unquote};
|
||||
use crate::Error;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// # [4.4.5.3. EXT-X-I-FRAME-STREAM-INF]
|
||||
/// # [4.3.5.3. EXT-X-I-FRAME-STREAM-INF]
|
||||
///
|
||||
/// The [`ExtXIFrameStreamInf`] tag identifies a [`Media Playlist`] file,
|
||||
/// containing the I-frames of a multimedia presentation.
|
||||
///
|
||||
|
@ -16,14 +17,79 @@ use crate::Error;
|
|||
///
|
||||
/// [`Master Playlist`]: crate::MasterPlaylist
|
||||
/// [`Media Playlist`]: crate::MediaPlaylist
|
||||
/// [4.4.5.3. EXT-X-I-FRAME-STREAM-INF]:
|
||||
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.4.5.3
|
||||
/// [4.3.5.3. EXT-X-I-FRAME-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5
|
||||
#[derive(PartialOrd, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ExtXIFrameStreamInf {
|
||||
uri: String,
|
||||
stream_inf: StreamInf,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq)]
|
||||
/// Builder for [`ExtXIFrameStreamInf`].
|
||||
pub struct ExtXIFrameStreamInfBuilder {
|
||||
uri: Option<String>,
|
||||
stream_inf: StreamInfBuilder,
|
||||
}
|
||||
|
||||
impl ExtXIFrameStreamInfBuilder {
|
||||
/// An `URI` to the [`MediaPlaylist`] file.
|
||||
///
|
||||
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||
pub fn uri<T: Into<String>>(&mut self, value: T) -> &mut Self {
|
||||
self.uri = Some(value.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// The maximum bandwidth of the stream.
|
||||
pub fn bandwidth(&mut self, value: u64) -> &mut Self {
|
||||
self.stream_inf.bandwidth(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// The average bandwidth of the stream.
|
||||
pub fn average_bandwidth(&mut self, value: u64) -> &mut Self {
|
||||
self.stream_inf.average_bandwidth(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Every media format in any of the renditions specified by the Variant
|
||||
/// Stream.
|
||||
pub fn codecs<T: Into<String>>(&mut self, value: T) -> &mut Self {
|
||||
self.stream_inf.codecs(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// The resolution of the stream.
|
||||
pub fn resolution(&mut self, value: (usize, usize)) -> &mut Self {
|
||||
self.stream_inf.resolution(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// High-bandwidth Digital Content Protection
|
||||
pub fn hdcp_level(&mut self, value: HdcpLevel) -> &mut Self {
|
||||
self.stream_inf.hdcp_level(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// It indicates the set of video renditions, that should be used when
|
||||
/// playing the presentation.
|
||||
pub fn video<T: Into<String>>(&mut self, value: T) -> &mut Self {
|
||||
self.stream_inf.video(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build an [`ExtXIFrameStreamInf`].
|
||||
pub fn build(&self) -> crate::Result<ExtXIFrameStreamInf> {
|
||||
Ok(ExtXIFrameStreamInf {
|
||||
uri: self
|
||||
.uri
|
||||
.clone()
|
||||
.ok_or_else(|| Error::missing_value("frame rate"))?,
|
||||
stream_inf: self.stream_inf.build().map_err(Error::builder_error)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtXIFrameStreamInf {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAME-STREAM-INF:";
|
||||
|
||||
|
@ -35,12 +101,15 @@ impl ExtXIFrameStreamInf {
|
|||
/// let stream = ExtXIFrameStreamInf::new("https://www.example.com", 20);
|
||||
/// ```
|
||||
pub fn new<T: ToString>(uri: T, bandwidth: u64) -> Self {
|
||||
ExtXIFrameStreamInf {
|
||||
Self {
|
||||
uri: uri.to_string(),
|
||||
stream_inf: StreamInf::new(bandwidth),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a builder for [`ExtXIFrameStreamInf`].
|
||||
pub fn builder() -> ExtXIFrameStreamInfBuilder { ExtXIFrameStreamInfBuilder::default() }
|
||||
|
||||
/// Returns the `URI`, that identifies the associated [`media playlist`].
|
||||
///
|
||||
/// # Example
|
||||
|
@ -121,6 +190,34 @@ impl DerefMut for ExtXIFrameStreamInf {
|
|||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_builder() {
|
||||
let mut i_frame_stream_inf =
|
||||
ExtXIFrameStreamInf::new("http://example.com/audio-only.m3u8", 200_000);
|
||||
|
||||
i_frame_stream_inf
|
||||
.set_average_bandwidth(Some(100_000))
|
||||
.set_codecs(Some("mp4a.40.5"))
|
||||
.set_resolution(1920, 1080)
|
||||
.set_hdcp_level(Some(HdcpLevel::None))
|
||||
.set_video(Some("video"));
|
||||
|
||||
assert_eq!(
|
||||
ExtXIFrameStreamInf::builder()
|
||||
.uri("http://example.com/audio-only.m3u8")
|
||||
.bandwidth(200_000)
|
||||
.average_bandwidth(100_000)
|
||||
.codecs("mp4a.40.5")
|
||||
.resolution((1920, 1080))
|
||||
.hdcp_level(HdcpLevel::None)
|
||||
.video("video")
|
||||
.build()
|
||||
.unwrap(),
|
||||
i_frame_stream_inf
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
|
|
@ -4,11 +4,12 @@ use std::str::FromStr;
|
|||
use derive_builder::Builder;
|
||||
|
||||
use crate::attribute::AttributePairs;
|
||||
use crate::types::{Channels, InStreamId, MediaType, ProtocolVersion, RequiredVersion};
|
||||
use crate::types::{Channels, InStreamId, MediaType, ProtocolVersion};
|
||||
use crate::utils::{parse_yes_or_no, quote, tag, unquote};
|
||||
use crate::Error;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// # [4.4.5.1. EXT-X-MEDIA]
|
||||
///
|
||||
/// The [`ExtXMedia`] tag is used to relate [`Media Playlist`]s,
|
||||
/// that contain alternative Renditions of the same content.
|
||||
///
|
||||
|
@ -31,7 +32,7 @@ pub struct ExtXMedia {
|
|||
/// # Note
|
||||
/// This attribute is **required**.
|
||||
media_type: MediaType,
|
||||
#[builder(setter(strip_option, into), default)]
|
||||
#[builder(setter(strip_option), default)]
|
||||
/// Sets the `URI` that identifies the [`Media Playlist`].
|
||||
///
|
||||
/// # Note
|
||||
|
@ -48,7 +49,7 @@ pub struct ExtXMedia {
|
|||
/// # Note
|
||||
/// This attribute is **required**.
|
||||
group_id: String,
|
||||
#[builder(setter(strip_option, into), default)]
|
||||
#[builder(setter(strip_option), default)]
|
||||
/// Sets the name of the primary language used in the rendition.
|
||||
/// The value has to conform to [`RFC5646`].
|
||||
///
|
||||
|
@ -57,7 +58,7 @@ pub struct ExtXMedia {
|
|||
///
|
||||
/// [`RFC5646`]: https://tools.ietf.org/html/rfc5646
|
||||
language: Option<String>,
|
||||
#[builder(setter(strip_option, into), default)]
|
||||
#[builder(setter(strip_option), default)]
|
||||
/// Sets the name of a language associated with the rendition.
|
||||
///
|
||||
/// # Note
|
||||
|
@ -92,14 +93,14 @@ pub struct ExtXMedia {
|
|||
#[builder(default)]
|
||||
/// Sets the value of the `forced` flag.
|
||||
is_forced: bool,
|
||||
#[builder(setter(strip_option, into), default)]
|
||||
#[builder(setter(strip_option), default)]
|
||||
/// Sets the identifier that specifies a rendition within the segments in
|
||||
/// the media playlist.
|
||||
instream_id: Option<InStreamId>,
|
||||
#[builder(setter(strip_option, into), default)]
|
||||
#[builder(setter(strip_option), default)]
|
||||
/// Sets the string that represents uniform type identifiers (UTI).
|
||||
characteristics: Option<String>,
|
||||
#[builder(setter(strip_option, into), default)]
|
||||
#[builder(setter(strip_option), default)]
|
||||
/// Sets the parameters of the rendition.
|
||||
channels: Option<Channels>,
|
||||
}
|
||||
|
@ -309,7 +310,7 @@ impl ExtXMedia {
|
|||
///
|
||||
/// [`Media Playlist`]: crate::MediaPlaylist
|
||||
pub fn set_uri<T: Into<String>>(&mut self, value: Option<T>) -> &mut Self {
|
||||
self.uri = value.map(|v| v.into());
|
||||
self.uri = value.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -347,7 +348,7 @@ impl ExtXMedia {
|
|||
///
|
||||
/// [`RFC5646`]: https://tools.ietf.org/html/rfc5646
|
||||
pub fn set_language<T: Into<String>>(&mut self, value: Option<T>) -> &mut Self {
|
||||
self.language = value.map(|v| v.into());
|
||||
self.language = value.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -387,7 +388,7 @@ impl ExtXMedia {
|
|||
///
|
||||
/// [`language`]: #method.language
|
||||
pub fn set_assoc_language<T: Into<String>>(&mut self, value: Option<T>) -> &mut Self {
|
||||
self.assoc_language = value.map(|v| v.into());
|
||||
self.assoc_language = value.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -589,7 +590,7 @@ impl ExtXMedia {
|
|||
/// [`UTI`]: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#ref-UTI
|
||||
/// [`subtitles`]: crate::types::MediaType::Subtitles
|
||||
pub fn set_characteristics<T: Into<String>>(&mut self, value: Option<T>) -> &mut Self {
|
||||
self.characteristics = value.map(|v| v.into());
|
||||
self.characteristics = value.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -624,7 +625,7 @@ impl ExtXMedia {
|
|||
/// assert_eq!(media.channels(), &Some(Channels::new(6)));
|
||||
/// ```
|
||||
pub fn set_channels<T: Into<Channels>>(&mut self, value: Option<T>) -> &mut Self {
|
||||
self.channels = value.map(|v| v.into());
|
||||
self.channels = value.map(Into::into);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -740,6 +741,7 @@ impl FromStr for ExtXMedia {
|
|||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
|
|
@ -4,18 +4,21 @@ use std::str::FromStr;
|
|||
use derive_builder::Builder;
|
||||
|
||||
use crate::attribute::AttributePairs;
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::utils::{quote, tag, unquote};
|
||||
use crate::Error;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// The data of an [ExtXSessionData] tag.
|
||||
/// The data of an [`ExtXSessionData`] tag.
|
||||
#[derive(Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)]
|
||||
pub enum SessionData {
|
||||
/// A String, that contains the data identified by
|
||||
/// [`data_id`](ExtXSessionData::data_id).
|
||||
/// If a [`language`](ExtXSessionData::language) is specified, the value
|
||||
/// [`data_id`].
|
||||
/// If a [`language`] is specified, the value
|
||||
/// should contain a human-readable string written in the specified
|
||||
/// language.
|
||||
///
|
||||
/// [`data_id`]: ExtXSessionData::data_id
|
||||
/// [`language`]: ExtXSessionData::language
|
||||
Value(String),
|
||||
/// An [`uri`], which points to a [`json`].
|
||||
///
|
||||
|
@ -35,17 +38,20 @@ pub enum SessionData {
|
|||
#[builder(setter(into))]
|
||||
pub struct ExtXSessionData {
|
||||
/// The identifier of the data.
|
||||
/// For more information look [`here`](ExtXSessionData::set_data_id).
|
||||
/// For more information look [`here`].
|
||||
///
|
||||
/// # Note
|
||||
/// This field is required.
|
||||
///
|
||||
/// [`here`]: ExtXSessionData::set_data_id
|
||||
data_id: String,
|
||||
/// The data associated with the
|
||||
/// [`data_id`](ExtXSessionDataBuilder::data_id).
|
||||
/// The data associated with the [`data_id`].
|
||||
/// For more information look [`here`](SessionData).
|
||||
///
|
||||
/// # Note
|
||||
/// This field is required.
|
||||
///
|
||||
/// [`data_id`]: ExtXSessionDataBuilder::data_id
|
||||
data: SessionData,
|
||||
/// The language of the [`data`](ExtXSessionDataBuilder::data).
|
||||
#[builder(setter(into, strip_option), default)]
|
||||
|
@ -318,6 +324,7 @@ impl FromStr for ExtXSessionData {
|
|||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
|
|
@ -2,9 +2,9 @@ use std::fmt;
|
|||
use std::ops::{Deref, DerefMut};
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::{DecryptionKey, EncryptionMethod, ProtocolVersion, RequiredVersion};
|
||||
use crate::types::{DecryptionKey, EncryptionMethod, ProtocolVersion};
|
||||
use crate::utils::tag;
|
||||
use crate::Error;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// # [4.3.4.5. EXT-X-SESSION-KEY]
|
||||
/// The [`ExtXSessionKey`] tag allows encryption keys from [`Media Playlist`]s
|
||||
|
@ -86,6 +86,7 @@ impl DerefMut for ExtXSessionKey {
|
|||
mod test {
|
||||
use super::*;
|
||||
use crate::types::{EncryptionMethod, KeyFormat};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
|
|
@ -4,15 +4,24 @@ use std::str::FromStr;
|
|||
|
||||
use crate::attribute::AttributePairs;
|
||||
use crate::types::{
|
||||
ClosedCaptions, DecimalFloatingPoint, ProtocolVersion, RequiredVersion, StreamInf,
|
||||
ClosedCaptions, DecimalFloatingPoint, HdcpLevel, ProtocolVersion, StreamInf, StreamInfBuilder,
|
||||
};
|
||||
use crate::utils::{quote, tag, unquote};
|
||||
use crate::Error;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// [4.3.4.2. EXT-X-STREAM-INF]
|
||||
/// # [4.3.4.2. EXT-X-STREAM-INF]
|
||||
///
|
||||
/// The [`ExtXStreamInf`] tag specifies a Variant Stream, which is a set
|
||||
/// of Renditions that can be combined to play the presentation. The
|
||||
/// attributes of the tag provide information about the Variant Stream.
|
||||
///
|
||||
/// The URI line that follows the [`ExtXStreamInf`] tag specifies a Media
|
||||
/// Playlist that carries a rendition of the Variant Stream. The URI
|
||||
/// line is REQUIRED. Clients that do not support multiple video
|
||||
/// Renditions SHOULD play this Rendition.
|
||||
///
|
||||
/// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2
|
||||
#[derive(PartialOrd, Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(PartialOrd, Debug, Clone, PartialEq)]
|
||||
pub struct ExtXStreamInf {
|
||||
uri: String,
|
||||
frame_rate: Option<DecimalFloatingPoint>,
|
||||
|
@ -22,6 +31,104 @@ pub struct ExtXStreamInf {
|
|||
stream_inf: StreamInf,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
/// Builder for [`ExtXStreamInf`].
|
||||
pub struct ExtXStreamInfBuilder {
|
||||
uri: Option<String>,
|
||||
frame_rate: Option<DecimalFloatingPoint>,
|
||||
audio: Option<String>,
|
||||
subtitles: Option<String>,
|
||||
closed_captions: Option<ClosedCaptions>,
|
||||
stream_inf: StreamInfBuilder,
|
||||
}
|
||||
|
||||
impl ExtXStreamInfBuilder {
|
||||
/// An `URI` to the [`MediaPlaylist`] file.
|
||||
///
|
||||
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||
pub fn uri<T: Into<String>>(&mut self, value: T) -> &mut Self {
|
||||
self.uri = Some(value.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Maximum frame rate for all the video in the variant stream.
|
||||
pub fn frame_rate(&mut self, value: f64) -> &mut Self {
|
||||
self.frame_rate = Some(value.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// The group identifier for the audio in the variant stream.
|
||||
pub fn audio<T: Into<String>>(&mut self, value: T) -> &mut Self {
|
||||
self.audio = Some(value.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// The group identifier for the subtitles in the variant stream.
|
||||
pub fn subtitles<T: Into<String>>(&mut self, value: T) -> &mut Self {
|
||||
self.subtitles = Some(value.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// The value of [`ClosedCaptions`] attribute.
|
||||
pub fn closed_captions<T: Into<ClosedCaptions>>(&mut self, value: T) -> &mut Self {
|
||||
self.closed_captions = Some(value.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// The maximum bandwidth of the stream.
|
||||
pub fn bandwidth(&mut self, value: u64) -> &mut Self {
|
||||
self.stream_inf.bandwidth(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// The average bandwidth of the stream.
|
||||
pub fn average_bandwidth(&mut self, value: u64) -> &mut Self {
|
||||
self.stream_inf.average_bandwidth(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Every media format in any of the renditions specified by the Variant
|
||||
/// Stream.
|
||||
pub fn codecs<T: Into<String>>(&mut self, value: T) -> &mut Self {
|
||||
self.stream_inf.codecs(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// The resolution of the stream.
|
||||
pub fn resolution(&mut self, value: (usize, usize)) -> &mut Self {
|
||||
self.stream_inf.resolution(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// High-bandwidth Digital Content Protection
|
||||
pub fn hdcp_level(&mut self, value: HdcpLevel) -> &mut Self {
|
||||
self.stream_inf.hdcp_level(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// It indicates the set of video renditions, that should be used when
|
||||
/// playing the presentation.
|
||||
pub fn video<T: Into<String>>(&mut self, value: T) -> &mut Self {
|
||||
self.stream_inf.video(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build an [`ExtXStreamInf`].
|
||||
pub fn build(&self) -> crate::Result<ExtXStreamInf> {
|
||||
Ok(ExtXStreamInf {
|
||||
uri: self
|
||||
.uri
|
||||
.clone()
|
||||
.ok_or_else(|| Error::missing_value("frame rate"))?,
|
||||
frame_rate: self.frame_rate,
|
||||
audio: self.audio.clone(),
|
||||
subtitles: self.subtitles.clone(),
|
||||
closed_captions: self.closed_captions.clone(),
|
||||
stream_inf: self.stream_inf.build().map_err(Error::builder_error)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtXStreamInf {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-STREAM-INF:";
|
||||
|
||||
|
@ -43,6 +150,9 @@ impl ExtXStreamInf {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns a builder for [`ExtXStreamInf`].
|
||||
pub fn builder() -> ExtXStreamInfBuilder { ExtXStreamInfBuilder::default() }
|
||||
|
||||
/// Returns the `URI` that identifies the associated media playlist.
|
||||
///
|
||||
/// # Example
|
||||
|
@ -81,7 +191,7 @@ impl ExtXStreamInf {
|
|||
/// assert_eq!(stream.frame_rate(), Some(59.9));
|
||||
/// ```
|
||||
pub fn set_frame_rate(&mut self, value: Option<f64>) -> &mut Self {
|
||||
self.frame_rate = value.map(|v| v.into());
|
||||
self.frame_rate = value.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -123,7 +233,7 @@ impl ExtXStreamInf {
|
|||
/// assert_eq!(stream.audio(), &Some("audio".to_string()));
|
||||
/// ```
|
||||
pub fn set_audio<T: Into<String>>(&mut self, value: Option<T>) -> &mut Self {
|
||||
self.audio = value.map(|v| v.into());
|
||||
self.audio = value.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -152,7 +262,7 @@ impl ExtXStreamInf {
|
|||
/// assert_eq!(stream.subtitles(), &Some("subs".to_string()));
|
||||
/// ```
|
||||
pub fn set_subtitles<T: Into<String>>(&mut self, value: Option<T>) -> &mut Self {
|
||||
self.subtitles = value.map(|v| v.into());
|
||||
self.subtitles = value.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -171,7 +281,7 @@ impl ExtXStreamInf {
|
|||
/// ```
|
||||
pub const fn closed_captions(&self) -> &Option<ClosedCaptions> { &self.closed_captions }
|
||||
|
||||
/// Returns the value of [`ClosedCaptions`] attribute.
|
||||
/// Sets the value of [`ClosedCaptions`] attribute.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
|
@ -190,6 +300,7 @@ impl ExtXStreamInf {
|
|||
}
|
||||
}
|
||||
|
||||
/// This tag requires [`ProtocolVersion::V1`].
|
||||
impl RequiredVersion for ExtXStreamInf {
|
||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||
}
|
||||
|
@ -265,6 +376,7 @@ impl DerefMut for ExtXStreamInf {
|
|||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::utils::tag;
|
||||
use crate::RequiredVersion;
|
||||
|
||||
/// # [4.4.3.3. EXT-X-DISCONTINUITY-SEQUENCE]
|
||||
///
|
||||
|
@ -83,6 +84,7 @@ impl FromStr for ExtXDiscontinuitySequence {
|
|||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::utils::tag;
|
||||
use crate::Error;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// # [4.4.3.4. EXT-X-ENDLIST]
|
||||
/// The [`ExtXEndList`] tag indicates, that no more [`Media Segment`]s will be
|
||||
|
@ -18,7 +18,7 @@ use crate::Error;
|
|||
/// [`Media Playlist`]: crate::MediaPlaylist
|
||||
/// [4.4.3.4. EXT-X-ENDLIST]:
|
||||
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.4
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct ExtXEndList;
|
||||
|
||||
impl ExtXEndList {
|
||||
|
@ -38,13 +38,14 @@ impl FromStr for ExtXEndList {
|
|||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
tag(input, Self::PREFIX)?;
|
||||
Ok(ExtXEndList)
|
||||
Ok(Self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::utils::tag;
|
||||
use crate::Error;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// # [4.4.3.6. EXT-X-I-FRAMES-ONLY]
|
||||
/// The [`ExtXIFramesOnly`] tag indicates that each [`Media Segment`] in the
|
||||
|
@ -20,7 +20,7 @@ use crate::Error;
|
|||
/// [`Media Segment`]: crate::MediaSegment
|
||||
/// [4.4.3.6. EXT-X-I-FRAMES-ONLY]:
|
||||
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.6
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct ExtXIFramesOnly;
|
||||
|
||||
impl ExtXIFramesOnly {
|
||||
|
@ -40,13 +40,14 @@ impl FromStr for ExtXIFramesOnly {
|
|||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
tag(input, Self::PREFIX)?;
|
||||
Ok(ExtXIFramesOnly)
|
||||
Ok(Self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
|
|
@ -1,20 +1,14 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::utils::tag;
|
||||
use crate::Error;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// # [4.4.3.2. EXT-X-MEDIA-SEQUENCE]
|
||||
/// The [`ExtXMediaSequence`] tag indicates the Media Sequence Number of
|
||||
/// the first [`Media Segment`] that appears in a Playlist file.
|
||||
///
|
||||
/// Its format is:
|
||||
/// ```text
|
||||
/// #EXT-X-MEDIA-SEQUENCE:<number>
|
||||
/// ```
|
||||
/// where `number` is a [`u64`].
|
||||
///
|
||||
/// [Media Segment]: crate::MediaSegment
|
||||
/// [4.4.3.2. EXT-X-MEDIA-SEQUENCE]:
|
||||
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.2
|
||||
|
@ -61,6 +55,7 @@ impl ExtXMediaSequence {
|
|||
}
|
||||
}
|
||||
|
||||
/// This tag requires [`ProtocolVersion::V1`].
|
||||
impl RequiredVersion for ExtXMediaSequence {
|
||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||
}
|
||||
|
@ -74,13 +69,14 @@ impl FromStr for ExtXMediaSequence {
|
|||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
let seq_num = tag(input, Self::PREFIX)?.parse()?;
|
||||
Ok(ExtXMediaSequence::new(seq_num))
|
||||
Ok(Self::new(seq_num))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
|
|
@ -1,30 +1,29 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::utils::tag;
|
||||
use crate::Error;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// # [4.4.3.5. EXT-X-PLAYLIST-TYPE]
|
||||
/// # [4.3.3.5. EXT-X-PLAYLIST-TYPE]
|
||||
///
|
||||
/// The [`ExtXPlaylistType`] tag provides mutability information about the
|
||||
/// [`Media Playlist`]. It applies to the entire [`Media Playlist`].
|
||||
///
|
||||
/// Its format is:
|
||||
/// ```text
|
||||
/// #EXT-X-PLAYLIST-TYPE:<type-enum>
|
||||
/// ```
|
||||
///
|
||||
/// [Media Playlist]: crate::MediaPlaylist
|
||||
/// [4.4.3.5. EXT-X-PLAYLIST-TYPE]:
|
||||
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.5
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
/// [`Media Playlist`]: crate::MediaPlaylist
|
||||
/// [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)]
|
||||
pub enum ExtXPlaylistType {
|
||||
/// If the [`ExtXPlaylistType`] is Event, Media Segments
|
||||
/// can only be added to the end of the Media Playlist.
|
||||
/// If the [`ExtXPlaylistType`] is Event, [`Media Segment`]s
|
||||
/// can only be added to the end of the [`Media Playlist`].
|
||||
///
|
||||
/// [`Media Segment`]: crate::MediaSegment
|
||||
/// [`Media Playlist`]: crate::MediaPlaylist
|
||||
Event,
|
||||
/// If the [`ExtXPlaylistType`] is Video On Demand (Vod),
|
||||
/// the Media Playlist cannot change.
|
||||
/// the [`Media Playlist`] cannot change.
|
||||
///
|
||||
/// [`Media Playlist`]: crate::MediaPlaylist
|
||||
Vod,
|
||||
}
|
||||
|
||||
|
@ -32,6 +31,7 @@ impl ExtXPlaylistType {
|
|||
pub(crate) const PREFIX: &'static str = "#EXT-X-PLAYLIST-TYPE:";
|
||||
}
|
||||
|
||||
/// This tag requires [`ProtocolVersion::V1`].
|
||||
impl RequiredVersion for ExtXPlaylistType {
|
||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||
}
|
||||
|
@ -61,6 +61,7 @@ impl FromStr for ExtXPlaylistType {
|
|||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
|
@ -81,6 +82,8 @@ mod test {
|
|||
assert!("#EXT-X-PLAYLIST-TYPE:H"
|
||||
.parse::<ExtXPlaylistType>()
|
||||
.is_err());
|
||||
|
||||
assert!("garbage".parse::<ExtXPlaylistType>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::utils::tag;
|
||||
use crate::Error;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// # [4.4.3.1. EXT-X-TARGETDURATION]
|
||||
/// The [`ExtXTargetDuration`] tag specifies the maximum [`Media Segment`]
|
||||
/// # [4.3.3.1. EXT-X-TARGETDURATION]
|
||||
/// The [`ExtXTargetDuration`] tag specifies the maximum [`MediaSegment`]
|
||||
/// duration.
|
||||
///
|
||||
/// [`Media Segment`]: crate::MediaSegment
|
||||
/// [4.4.3.1. EXT-X-TARGETDURATION]:
|
||||
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.4.3.1
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||
/// [`MediaSegment`]: crate::MediaSegment
|
||||
/// [4.3.3.1. EXT-X-TARGETDURATION]: https://tools.ietf.org/html/rfc8216#section-4.3.3.1
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
|
||||
pub struct ExtXTargetDuration(Duration);
|
||||
|
||||
impl ExtXTargetDuration {
|
||||
|
@ -31,10 +31,7 @@ impl ExtXTargetDuration {
|
|||
///
|
||||
/// # Note
|
||||
/// The nanoseconds part of the [`Duration`] will be discarded.
|
||||
pub const fn new(duration: Duration) -> Self {
|
||||
// TOOD: round instead of discarding?
|
||||
Self(Duration::from_secs(duration.as_secs()))
|
||||
}
|
||||
pub const fn new(duration: Duration) -> Self { Self(Duration::from_secs(duration.as_secs())) }
|
||||
|
||||
/// Returns the maximum media segment duration.
|
||||
///
|
||||
|
@ -55,6 +52,12 @@ impl RequiredVersion for ExtXTargetDuration {
|
|||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||
}
|
||||
|
||||
impl Deref for ExtXTargetDuration {
|
||||
type Target = Duration;
|
||||
|
||||
fn deref(&self) -> &Self::Target { &self.0 }
|
||||
}
|
||||
|
||||
impl fmt::Display for ExtXTargetDuration {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}{}", Self::PREFIX, self.0.as_secs())
|
||||
|
@ -73,6 +76,7 @@ impl FromStr for ExtXTargetDuration {
|
|||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
@ -97,4 +101,9 @@ mod test {
|
|||
"#EXT-X-TARGETDURATION:5".parse().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deref() {
|
||||
assert_eq!(ExtXTargetDuration::new(Duration::from_secs(5)).as_secs(), 5);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,9 @@ use std::fmt;
|
|||
use std::ops::{Deref, DerefMut};
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::{ByteRange, ProtocolVersion, RequiredVersion};
|
||||
use crate::types::{ByteRange, ProtocolVersion};
|
||||
use crate::utils::tag;
|
||||
use crate::Error;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// # [4.4.2.2. EXT-X-BYTERANGE]
|
||||
///
|
||||
|
@ -23,7 +23,7 @@ use crate::Error;
|
|||
/// [`Media Segment`]: crate::MediaSegment
|
||||
/// [4.4.2.2. EXT-X-BYTERANGE]:
|
||||
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.2
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct ExtXByteRange(ByteRange);
|
||||
|
||||
impl ExtXByteRange {
|
||||
|
@ -97,13 +97,14 @@ impl FromStr for ExtXByteRange {
|
|||
}
|
||||
};
|
||||
|
||||
Ok(ExtXByteRange::new(length, start))
|
||||
Ok(Self::new(length, start))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
|
|
@ -3,63 +3,122 @@ use std::fmt;
|
|||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use chrono::{DateTime, FixedOffset, SecondsFormat};
|
||||
use derive_builder::Builder;
|
||||
|
||||
use crate::attribute::AttributePairs;
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::types::{ProtocolVersion, Value};
|
||||
use crate::utils::{quote, tag, unquote};
|
||||
use crate::Error;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// # [4.3.2.7. EXT-X-DATERANGE]
|
||||
/// The [`ExtXDateRange`] tag associates a date range (i.e., a range of
|
||||
/// time defined by a starting and ending date) with a set of attribute/
|
||||
/// value pairs.
|
||||
///
|
||||
/// [4.3.2.7. EXT-X-DATERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.7
|
||||
///
|
||||
/// TODO: Implement properly
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[derive(Builder, Debug, Clone, PartialEq, PartialOrd)]
|
||||
#[builder(setter(into))]
|
||||
pub struct ExtXDateRange {
|
||||
/// A string that uniquely identifies a [`ExtXDateRange`] in the Playlist.
|
||||
/// A string that uniquely identifies an [`ExtXDateRange`] in the Playlist.
|
||||
///
|
||||
/// # Note
|
||||
/// This attribute is required.
|
||||
id: String,
|
||||
#[builder(setter(strip_option), default)]
|
||||
/// A client-defined string that specifies some set of attributes and their
|
||||
/// associated value semantics. All Date Ranges with the same CLASS
|
||||
/// attribute value MUST adhere to these semantics. This attribute is
|
||||
/// OPTIONAL.
|
||||
/// associated value semantics. All [`ExtXDateRange`]s with the same class
|
||||
/// attribute value must adhere to these semantics.
|
||||
///
|
||||
/// # Note
|
||||
/// This attribute is optional.
|
||||
class: Option<String>,
|
||||
/// The date at which the Date Range begins. This attribute is REQUIRED.
|
||||
/// The date at which the [`ExtXDateRange`] begins.
|
||||
///
|
||||
/// # Note
|
||||
/// This attribute is required.
|
||||
start_date: DateTime<FixedOffset>,
|
||||
/// The date at which the Date Range ends. It MUST be equal to or later than
|
||||
/// the value of the START-DATE attribute. This attribute is OPTIONAL.
|
||||
#[builder(setter(strip_option), default)]
|
||||
/// The date at which the [`ExtXDateRange`] ends. It must be equal to or
|
||||
/// later than the value of the [`start-date`] attribute.
|
||||
///
|
||||
/// # Note
|
||||
/// This attribute is optional.
|
||||
///
|
||||
/// [`start-date`]: #method.start_date
|
||||
end_date: Option<DateTime<FixedOffset>>,
|
||||
/// The duration of the Date Range. It MUST NOT be negative. A single
|
||||
/// instant in time (e.g., crossing a finish line) SHOULD be
|
||||
/// represented with a duration of 0. This attribute is OPTIONAL.
|
||||
#[builder(setter(strip_option), default)]
|
||||
/// The duration of the [`ExtXDateRange`]. A single instant in time (e.g.,
|
||||
/// crossing a finish line) should be represented with a duration of 0.
|
||||
///
|
||||
/// # Note
|
||||
/// This attribute is optional.
|
||||
duration: Option<Duration>,
|
||||
/// The expected duration of the Date Range. It MUST NOT be negative. This
|
||||
/// attribute SHOULD be used to indicate the expected duration of a
|
||||
/// Date Range whose actual duration is not yet known.
|
||||
/// It is OPTIONAL.
|
||||
#[builder(setter(strip_option), default)]
|
||||
/// The expected duration of the [`ExtXDateRange`].
|
||||
/// This attribute should be used to indicate the expected duration of a
|
||||
/// [`ExtXDateRange`] whose actual duration is not yet known.
|
||||
///
|
||||
/// # Note
|
||||
/// This attribute is optional.
|
||||
planned_duration: Option<Duration>,
|
||||
#[builder(setter(strip_option), default)]
|
||||
/// https://tools.ietf.org/html/rfc8216#section-4.3.2.7.1
|
||||
///
|
||||
/// # Note
|
||||
/// This attribute is optional.
|
||||
scte35_cmd: Option<String>,
|
||||
#[builder(setter(strip_option), default)]
|
||||
/// https://tools.ietf.org/html/rfc8216#section-4.3.2.7.1
|
||||
///
|
||||
/// # Note
|
||||
/// This attribute is optional.
|
||||
scte35_out: Option<String>,
|
||||
#[builder(setter(strip_option), default)]
|
||||
/// https://tools.ietf.org/html/rfc8216#section-4.3.2.7.1
|
||||
///
|
||||
/// # Note
|
||||
/// This attribute is optional.
|
||||
scte35_in: Option<String>,
|
||||
#[builder(default)]
|
||||
/// This attribute indicates that the end of the range containing it is
|
||||
/// equal to the START-DATE of its Following Range. The Following Range
|
||||
/// is the Date Range of the same CLASS, that has the earliest
|
||||
/// START-DATE after the START-DATE of the range in question. This
|
||||
/// attribute is OPTIONAL.
|
||||
/// equal to the [`start-date`] of its following range. The following range
|
||||
/// is the [`ExtXDateRange`] of the same class, that has the earliest
|
||||
/// [`start-date`] after the [`start-date`] of the range in question.
|
||||
///
|
||||
/// # Note
|
||||
/// This attribute is optional.
|
||||
end_on_next: bool,
|
||||
/// The "X-" prefix defines a namespace reserved for client-defined
|
||||
/// attributes. The client-attribute MUST be a legal AttributeName.
|
||||
/// Clients SHOULD use a reverse-DNS syntax when defining their own
|
||||
/// attribute names to avoid collisions. The attribute value MUST be
|
||||
/// a quoted-string, a hexadecimal-sequence, or a decimal-floating-
|
||||
/// point. An example of a client-defined attribute is X-COM-EXAMPLE-
|
||||
/// AD-ID="XYZ123". These attributes are OPTIONAL.
|
||||
client_attributes: BTreeMap<String, String>,
|
||||
#[builder(default)]
|
||||
/// The `"X-"` prefix defines a namespace reserved for client-defined
|
||||
/// attributes. The client-attribute must be a uppercase characters.
|
||||
/// Clients should use a reverse-DNS syntax when defining their own
|
||||
/// attribute names to avoid collisions. An example of a client-defined
|
||||
/// attribute is `X-COM-EXAMPLE-AD-ID="XYZ123"`.
|
||||
///
|
||||
/// # Note
|
||||
/// This attribute is optional.
|
||||
client_attributes: BTreeMap<String, Value>,
|
||||
}
|
||||
|
||||
impl ExtXDateRangeBuilder {
|
||||
/// Inserts a key value pair.
|
||||
pub fn insert_client_attribute<K: ToString, V: Into<Value>>(
|
||||
&mut self,
|
||||
key: K,
|
||||
value: V,
|
||||
) -> &mut Self {
|
||||
if self.client_attributes.is_none() {
|
||||
self.client_attributes = Some(BTreeMap::new());
|
||||
}
|
||||
|
||||
if let Some(client_attributes) = &mut self.client_attributes {
|
||||
client_attributes.insert(key.to_string(), value.into());
|
||||
} else {
|
||||
unreachable!();
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtXDateRange {
|
||||
|
@ -97,67 +156,545 @@ impl ExtXDateRange {
|
|||
client_attributes: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a builder for [`ExtXDateRange`].
|
||||
pub fn builder() -> ExtXDateRangeBuilder { ExtXDateRangeBuilder::default() }
|
||||
|
||||
/// A string that uniquely identifies an [`ExtXDateRange`] in the Playlist.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXDateRange;
|
||||
/// use chrono::offset::TimeZone;
|
||||
/// use chrono::{DateTime, FixedOffset};
|
||||
///
|
||||
/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||
///
|
||||
/// let date_range = ExtXDateRange::new(
|
||||
/// "id",
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 2, 19)
|
||||
/// .and_hms_milli(14, 54, 23, 31),
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(date_range.id(), &"id".to_string());
|
||||
/// ```
|
||||
pub const fn id(&self) -> &String { &self.id }
|
||||
|
||||
/// A string that uniquely identifies an [`ExtXDateRange`] in the Playlist.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXDateRange;
|
||||
/// use chrono::offset::TimeZone;
|
||||
/// use chrono::{DateTime, FixedOffset};
|
||||
///
|
||||
/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||
///
|
||||
/// let mut date_range = ExtXDateRange::new(
|
||||
/// "id",
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 2, 19)
|
||||
/// .and_hms_milli(14, 54, 23, 31),
|
||||
/// );
|
||||
///
|
||||
/// date_range.set_id("new_id");
|
||||
/// assert_eq!(date_range.id(), &"new_id".to_string());
|
||||
/// ```
|
||||
pub fn set_id<T: ToString>(&mut self, value: T) -> &mut Self {
|
||||
self.id = value.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// A client-defined string that specifies some set of attributes and their
|
||||
/// associated value semantics. All [`ExtXDateRange`]s with the same class
|
||||
/// attribute value must adhere to these semantics.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXDateRange;
|
||||
/// use chrono::offset::TimeZone;
|
||||
/// use chrono::{DateTime, FixedOffset};
|
||||
///
|
||||
/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||
///
|
||||
/// let mut date_range = ExtXDateRange::new(
|
||||
/// "id",
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 2, 19)
|
||||
/// .and_hms_milli(14, 54, 23, 31),
|
||||
/// );
|
||||
/// # assert_eq!(date_range.class(), &None);
|
||||
///
|
||||
/// date_range.set_class(Some("example_class"));
|
||||
/// assert_eq!(date_range.class(), &Some("example_class".to_string()));
|
||||
/// ```
|
||||
pub const fn class(&self) -> &Option<String> { &self.class }
|
||||
|
||||
/// A client-defined string that specifies some set of attributes and their
|
||||
/// associated value semantics. All [`ExtXDateRange`]s with the same class
|
||||
/// attribute value must adhere to these semantics.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXDateRange;
|
||||
/// use chrono::offset::TimeZone;
|
||||
/// use chrono::{DateTime, FixedOffset};
|
||||
///
|
||||
/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||
///
|
||||
/// let mut date_range = ExtXDateRange::new(
|
||||
/// "id",
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 2, 19)
|
||||
/// .and_hms_milli(14, 54, 23, 31),
|
||||
/// );
|
||||
/// # assert_eq!(date_range.class(), &None);
|
||||
///
|
||||
/// date_range.set_class(Some("example_class"));
|
||||
/// assert_eq!(date_range.class(), &Some("example_class".to_string()));
|
||||
/// ```
|
||||
pub fn set_class<T: ToString>(&mut self, value: Option<T>) -> &mut Self {
|
||||
self.class = value.map(|v| v.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// The date at which the [`ExtXDateRange`] begins.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXDateRange;
|
||||
/// use chrono::offset::TimeZone;
|
||||
/// use chrono::{DateTime, FixedOffset};
|
||||
///
|
||||
/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||
///
|
||||
/// let date_range = ExtXDateRange::new(
|
||||
/// "id",
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 2, 19)
|
||||
/// .and_hms_milli(14, 54, 23, 31),
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// date_range.start_date(),
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 2, 19)
|
||||
/// .and_hms_milli(14, 54, 23, 31)
|
||||
/// );
|
||||
/// ```
|
||||
pub const fn start_date(&self) -> DateTime<FixedOffset> { self.start_date }
|
||||
|
||||
/// The date at which the [`ExtXDateRange`] begins.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXDateRange;
|
||||
/// use chrono::offset::TimeZone;
|
||||
/// use chrono::{DateTime, FixedOffset};
|
||||
///
|
||||
/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||
///
|
||||
/// let mut date_range = ExtXDateRange::new(
|
||||
/// "id",
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 2, 19)
|
||||
/// .and_hms_milli(14, 54, 23, 31),
|
||||
/// );
|
||||
///
|
||||
/// date_range.set_start_date(
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 10, 10)
|
||||
/// .and_hms_milli(10, 10, 10, 10),
|
||||
/// );
|
||||
/// assert_eq!(
|
||||
/// date_range.start_date(),
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 10, 10)
|
||||
/// .and_hms_milli(10, 10, 10, 10)
|
||||
/// );
|
||||
/// ```
|
||||
pub fn set_start_date<T>(&mut self, value: T) -> &mut Self
|
||||
where
|
||||
T: Into<DateTime<FixedOffset>>,
|
||||
{
|
||||
self.start_date = value.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// The date at which the [`ExtXDateRange`] ends. It must be equal to or
|
||||
/// later than the value of the [`start-date`] attribute.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXDateRange;
|
||||
/// use chrono::offset::TimeZone;
|
||||
/// use chrono::{DateTime, FixedOffset};
|
||||
///
|
||||
/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||
///
|
||||
/// let mut date_range = ExtXDateRange::new(
|
||||
/// "id",
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 2, 19)
|
||||
/// .and_hms_milli(14, 54, 23, 31),
|
||||
/// );
|
||||
/// # assert_eq!(date_range.end_date(), None);
|
||||
///
|
||||
/// date_range.set_end_date(Some(
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 10, 10)
|
||||
/// .and_hms_milli(10, 10, 10, 10),
|
||||
/// ));
|
||||
/// assert_eq!(
|
||||
/// date_range.end_date(),
|
||||
/// Some(
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 10, 10)
|
||||
/// .and_hms_milli(10, 10, 10, 10)
|
||||
/// )
|
||||
/// );
|
||||
/// ```
|
||||
pub const fn end_date(&self) -> Option<DateTime<FixedOffset>> { self.end_date }
|
||||
|
||||
/// The date at which the [`ExtXDateRange`] ends. It must be equal to or
|
||||
/// later than the value of the [`start-date`] attribute.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXDateRange;
|
||||
/// use chrono::offset::TimeZone;
|
||||
/// use chrono::{DateTime, FixedOffset};
|
||||
///
|
||||
/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||
///
|
||||
/// let mut date_range = ExtXDateRange::new(
|
||||
/// "id",
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 2, 19)
|
||||
/// .and_hms_milli(14, 54, 23, 31),
|
||||
/// );
|
||||
/// # assert_eq!(date_range.end_date(), None);
|
||||
///
|
||||
/// date_range.set_end_date(Some(
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 10, 10)
|
||||
/// .and_hms_milli(10, 10, 10, 10),
|
||||
/// ));
|
||||
/// assert_eq!(
|
||||
/// date_range.end_date(),
|
||||
/// Some(
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 10, 10)
|
||||
/// .and_hms_milli(10, 10, 10, 10)
|
||||
/// )
|
||||
/// );
|
||||
/// ```
|
||||
pub fn set_end_date<T>(&mut self, value: Option<T>) -> &mut Self
|
||||
where
|
||||
T: Into<DateTime<FixedOffset>>,
|
||||
{
|
||||
self.end_date = value.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
/// The duration of the [`ExtXDateRange`]. A single instant in time (e.g.,
|
||||
/// crossing a finish line) should be represented with a duration of 0.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXDateRange;
|
||||
/// use chrono::offset::TimeZone;
|
||||
/// use chrono::{DateTime, FixedOffset};
|
||||
/// use std::time::Duration;
|
||||
///
|
||||
/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||
///
|
||||
/// let mut date_range = ExtXDateRange::new(
|
||||
/// "id",
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 2, 19)
|
||||
/// .and_hms_milli(14, 54, 23, 31),
|
||||
/// );
|
||||
/// # assert_eq!(date_range.duration(), None);
|
||||
///
|
||||
/// date_range.set_duration(Some(Duration::from_secs_f64(1.234)));
|
||||
/// assert_eq!(date_range.duration(), Some(Duration::from_secs_f64(1.234)));
|
||||
/// ```
|
||||
pub const fn duration(&self) -> Option<Duration> { self.duration }
|
||||
|
||||
/// The duration of the [`ExtXDateRange`]. A single instant in time (e.g.,
|
||||
/// crossing a finish line) should be represented with a duration of 0.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXDateRange;
|
||||
/// use chrono::offset::TimeZone;
|
||||
/// use chrono::{DateTime, FixedOffset};
|
||||
/// use std::time::Duration;
|
||||
///
|
||||
/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||
///
|
||||
/// let mut date_range = ExtXDateRange::new(
|
||||
/// "id",
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 2, 19)
|
||||
/// .and_hms_milli(14, 54, 23, 31),
|
||||
/// );
|
||||
/// # assert_eq!(date_range.duration(), None);
|
||||
///
|
||||
/// date_range.set_duration(Some(Duration::from_secs_f64(1.234)));
|
||||
/// assert_eq!(date_range.duration(), Some(Duration::from_secs_f64(1.234)));
|
||||
/// ```
|
||||
pub fn set_duration(&mut self, value: Option<Duration>) -> &mut Self {
|
||||
self.duration = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// The expected duration of the [`ExtXDateRange`].
|
||||
/// This attribute should be used to indicate the expected duration of a
|
||||
/// [`ExtXDateRange`] whose actual duration is not yet known.
|
||||
/// The date at which the [`ExtXDateRange`] ends. It must be equal to or
|
||||
/// later than the value of the [`start-date`] attribute.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXDateRange;
|
||||
/// use chrono::offset::TimeZone;
|
||||
/// use chrono::{DateTime, FixedOffset};
|
||||
/// use std::time::Duration;
|
||||
///
|
||||
/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||
///
|
||||
/// let mut date_range = ExtXDateRange::new(
|
||||
/// "id",
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 2, 19)
|
||||
/// .and_hms_milli(14, 54, 23, 31),
|
||||
/// );
|
||||
/// # assert_eq!(date_range.planned_duration(), None);
|
||||
///
|
||||
/// date_range.set_planned_duration(Some(Duration::from_secs_f64(1.2345)));
|
||||
/// assert_eq!(
|
||||
/// date_range.planned_duration(),
|
||||
/// Some(Duration::from_secs_f64(1.2345))
|
||||
/// );
|
||||
/// ```
|
||||
pub const fn planned_duration(&self) -> Option<Duration> { self.planned_duration }
|
||||
|
||||
/// The expected duration of the [`ExtXDateRange`].
|
||||
/// This attribute should be used to indicate the expected duration of a
|
||||
/// [`ExtXDateRange`] whose actual duration is not yet known.
|
||||
/// The date at which the [`ExtXDateRange`] ends. It must be equal to or
|
||||
/// later than the value of the [`start-date`] attribute.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXDateRange;
|
||||
/// use chrono::offset::TimeZone;
|
||||
/// use chrono::{DateTime, FixedOffset};
|
||||
/// use std::time::Duration;
|
||||
///
|
||||
/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||
///
|
||||
/// let mut date_range = ExtXDateRange::new(
|
||||
/// "id",
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 2, 19)
|
||||
/// .and_hms_milli(14, 54, 23, 31),
|
||||
/// );
|
||||
/// # assert_eq!(date_range.planned_duration(), None);
|
||||
///
|
||||
/// date_range.set_planned_duration(Some(Duration::from_secs_f64(1.2345)));
|
||||
/// assert_eq!(
|
||||
/// date_range.planned_duration(),
|
||||
/// Some(Duration::from_secs_f64(1.2345))
|
||||
/// );
|
||||
/// ```
|
||||
pub fn set_planned_duration(&mut self, value: Option<Duration>) -> &mut Self {
|
||||
self.planned_duration = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// See here for reference: https://www.scte.org/SCTEDocs/Standards/ANSI_SCTE%2035%202019r1.pdf
|
||||
pub const fn scte35_cmd(&self) -> &Option<String> { &self.scte35_cmd }
|
||||
|
||||
/// See here for reference: https://www.scte.org/SCTEDocs/Standards/ANSI_SCTE%2035%202019r1.pdf
|
||||
pub const fn scte35_in(&self) -> &Option<String> { &self.scte35_in }
|
||||
|
||||
/// See here for reference: https://www.scte.org/SCTEDocs/Standards/ANSI_SCTE%2035%202019r1.pdf
|
||||
pub const fn scte35_out(&self) -> &Option<String> { &self.scte35_out }
|
||||
|
||||
/// This attribute indicates that the end of the range containing it is
|
||||
/// equal to the [`start-date`] of its following range. The following range
|
||||
/// is the [`ExtXDateRange`] of the same class, that has the earliest
|
||||
/// [`start-date`] after the [`start-date`] of the range in question.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXDateRange;
|
||||
/// use chrono::offset::TimeZone;
|
||||
/// use chrono::{DateTime, FixedOffset};
|
||||
/// use std::time::Duration;
|
||||
///
|
||||
/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||
///
|
||||
/// let mut date_range = ExtXDateRange::new(
|
||||
/// "id",
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 2, 19)
|
||||
/// .and_hms_milli(14, 54, 23, 31),
|
||||
/// );
|
||||
/// # assert_eq!(date_range.end_on_next(), false);
|
||||
///
|
||||
/// date_range.set_end_on_next(true);
|
||||
/// assert_eq!(date_range.end_on_next(), true);
|
||||
/// ```
|
||||
pub const fn end_on_next(&self) -> bool { self.end_on_next }
|
||||
|
||||
/// This attribute indicates that the end of the range containing it is
|
||||
/// equal to the [`start-date`] of its following range. The following range
|
||||
/// is the [`ExtXDateRange`] of the same class, that has the earliest
|
||||
/// [`start-date`] after the [`start-date`] of the range in question.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXDateRange;
|
||||
/// use chrono::offset::TimeZone;
|
||||
/// use chrono::{DateTime, FixedOffset};
|
||||
/// use std::time::Duration;
|
||||
///
|
||||
/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||
///
|
||||
/// let mut date_range = ExtXDateRange::new(
|
||||
/// "id",
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 2, 19)
|
||||
/// .and_hms_milli(14, 54, 23, 31),
|
||||
/// );
|
||||
/// # assert_eq!(date_range.end_on_next(), false);
|
||||
///
|
||||
/// date_range.set_end_on_next(true);
|
||||
/// assert_eq!(date_range.end_on_next(), true);
|
||||
/// ```
|
||||
pub fn set_end_on_next(&mut self, value: bool) -> &mut Self {
|
||||
self.end_on_next = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// The "X-" prefix defines a namespace reserved for client-defined
|
||||
/// attributes. The client-attribute must be a uppercase characters.
|
||||
/// Clients should use a reverse-DNS syntax when defining their own
|
||||
/// attribute names to avoid collisions. An example of a client-defined
|
||||
/// attribute is `X-COM-EXAMPLE-AD-ID="XYZ123"`.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXDateRange;
|
||||
/// use std::collections::BTreeMap;
|
||||
///
|
||||
/// use chrono::offset::TimeZone;
|
||||
/// use chrono::{DateTime, FixedOffset};
|
||||
/// use hls_m3u8::types::Value;
|
||||
///
|
||||
/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||
///
|
||||
/// let mut date_range = ExtXDateRange::new(
|
||||
/// "id",
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 2, 19)
|
||||
/// .and_hms_milli(14, 54, 23, 31),
|
||||
/// );
|
||||
/// # assert_eq!(date_range.client_attributes(), &BTreeMap::new());
|
||||
///
|
||||
/// let mut attributes = BTreeMap::new();
|
||||
/// attributes.insert("X-COM-EXAMPLE-FLOAT".to_string(), Value::Float(1.1));
|
||||
///
|
||||
/// date_range.set_client_attributes(attributes.clone());
|
||||
/// assert_eq!(date_range.client_attributes(), &attributes);
|
||||
/// ```
|
||||
pub const fn client_attributes(&self) -> &BTreeMap<String, Value> { &self.client_attributes }
|
||||
|
||||
/// The "X-" prefix defines a namespace reserved for client-defined
|
||||
/// attributes. The client-attribute must be a uppercase characters.
|
||||
/// Clients should use a reverse-DNS syntax when defining their own
|
||||
/// attribute names to avoid collisions. An example of a client-defined
|
||||
/// attribute is `X-COM-EXAMPLE-AD-ID="XYZ123"`.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXDateRange;
|
||||
/// use std::collections::BTreeMap;
|
||||
///
|
||||
/// use chrono::offset::TimeZone;
|
||||
/// use chrono::{DateTime, FixedOffset};
|
||||
/// use hls_m3u8::types::Value;
|
||||
///
|
||||
/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||
///
|
||||
/// let mut date_range = ExtXDateRange::new(
|
||||
/// "id",
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 2, 19)
|
||||
/// .and_hms_milli(14, 54, 23, 31),
|
||||
/// );
|
||||
/// # assert_eq!(date_range.client_attributes(), &BTreeMap::new());
|
||||
///
|
||||
/// let mut attributes = BTreeMap::new();
|
||||
/// attributes.insert("X-COM-EXAMPLE-FLOAT".to_string(), Value::Float(1.1));
|
||||
///
|
||||
/// date_range
|
||||
/// .client_attributes_mut()
|
||||
/// .insert("X-COM-EXAMPLE-FLOAT".to_string(), Value::Float(1.1));
|
||||
///
|
||||
/// assert_eq!(date_range.client_attributes(), &attributes);
|
||||
/// ```
|
||||
pub fn client_attributes_mut(&mut self) -> &mut BTreeMap<String, Value> {
|
||||
&mut self.client_attributes
|
||||
}
|
||||
|
||||
/// The "X-" prefix defines a namespace reserved for client-defined
|
||||
/// attributes. The client-attribute must be a uppercase characters.
|
||||
/// Clients should use a reverse-DNS syntax when defining their own
|
||||
/// attribute names to avoid collisions. An example of a client-defined
|
||||
/// attribute is `X-COM-EXAMPLE-AD-ID="XYZ123"`.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXDateRange;
|
||||
/// use std::collections::BTreeMap;
|
||||
///
|
||||
/// use chrono::offset::TimeZone;
|
||||
/// use chrono::{DateTime, FixedOffset};
|
||||
/// use hls_m3u8::types::Value;
|
||||
///
|
||||
/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||
///
|
||||
/// let mut date_range = ExtXDateRange::new(
|
||||
/// "id",
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 2, 19)
|
||||
/// .and_hms_milli(14, 54, 23, 31),
|
||||
/// );
|
||||
/// # assert_eq!(date_range.client_attributes(), &BTreeMap::new());
|
||||
///
|
||||
/// let mut attributes = BTreeMap::new();
|
||||
/// attributes.insert("X-COM-EXAMPLE-FLOAT".to_string(), Value::Float(1.1));
|
||||
///
|
||||
/// date_range.set_client_attributes(attributes.clone());
|
||||
/// assert_eq!(date_range.client_attributes(), &attributes);
|
||||
/// ```
|
||||
pub fn set_client_attributes(&mut self, value: BTreeMap<String, Value>) -> &mut Self {
|
||||
self.client_attributes = value;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// This tag requires [`ProtocolVersion::V1`].
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXDateRange;
|
||||
/// use chrono::offset::TimeZone;
|
||||
/// use chrono::{DateTime, FixedOffset};
|
||||
/// use hls_m3u8::types::{ProtocolVersion, RequiredVersion};
|
||||
///
|
||||
/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||
///
|
||||
/// let date_range = ExtXDateRange::new(
|
||||
/// "id",
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 2, 19)
|
||||
/// .and_hms_milli(14, 54, 23, 31),
|
||||
/// );
|
||||
/// assert_eq!(date_range.required_version(), ProtocolVersion::V1);
|
||||
/// ```
|
||||
impl RequiredVersion for ExtXDateRange {
|
||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||
}
|
||||
|
||||
impl fmt::Display for ExtXDateRange {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", Self::PREFIX)?;
|
||||
write!(f, "ID={}", quote(&self.id))?;
|
||||
if let Some(value) = &self.class {
|
||||
write!(f, ",CLASS={}", quote(value))?;
|
||||
}
|
||||
write!(f, ",START-DATE={}", quote(&self.start_date))?;
|
||||
if let Some(value) = &self.end_date {
|
||||
write!(f, ",END-DATE={}", quote(value))?;
|
||||
}
|
||||
if let Some(value) = &self.duration {
|
||||
write!(f, ",DURATION={}", value.as_secs_f64())?;
|
||||
}
|
||||
if let Some(value) = &self.planned_duration {
|
||||
write!(f, ",PLANNED-DURATION={}", value.as_secs_f64())?;
|
||||
}
|
||||
if let Some(value) = &self.scte35_cmd {
|
||||
write!(f, ",SCTE35-CMD={}", quote(value))?;
|
||||
}
|
||||
if let Some(value) = &self.scte35_out {
|
||||
write!(f, ",SCTE35-OUT={}", quote(value))?;
|
||||
}
|
||||
if let Some(value) = &self.scte35_in {
|
||||
write!(f, ",SCTE35-IN={}", quote(value))?;
|
||||
}
|
||||
if self.end_on_next {
|
||||
write!(f, ",END-ON-NEXT=YES",)?;
|
||||
}
|
||||
for (k, v) in &self.client_attributes {
|
||||
write!(f, ",{}={}", k, v)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ExtXDateRange {
|
||||
type Err = Error;
|
||||
|
||||
|
@ -194,13 +731,13 @@ impl FromStr for ExtXDateRange {
|
|||
"SCTE35-IN" => scte35_in = Some(unquote(value)),
|
||||
"END-ON-NEXT" => {
|
||||
if value != "YES" {
|
||||
return Err(Error::invalid_input());
|
||||
return Err(Error::custom("The value of `END-ON-NEXT` has to be `YES`!"));
|
||||
}
|
||||
end_on_next = true;
|
||||
}
|
||||
_ => {
|
||||
if key.starts_with("X-") {
|
||||
client_attributes.insert(key.split_at(2).1.to_owned(), value.to_owned());
|
||||
client_attributes.insert(key.to_ascii_uppercase(), value.parse()?);
|
||||
} else {
|
||||
// [6.3.1. General Client Responsibilities]
|
||||
// > ignore any attribute/value pair with an
|
||||
|
@ -234,13 +771,174 @@ impl FromStr for ExtXDateRange {
|
|||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ExtXDateRange {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", Self::PREFIX)?;
|
||||
write!(f, "ID={}", quote(&self.id))?;
|
||||
if let Some(value) = &self.class {
|
||||
write!(f, ",CLASS={}", quote(value))?;
|
||||
}
|
||||
|
||||
write!(
|
||||
f,
|
||||
",START-DATE={}",
|
||||
quote(&self.start_date.to_rfc3339_opts(SecondsFormat::AutoSi, true))
|
||||
)?;
|
||||
|
||||
if let Some(value) = &self.end_date {
|
||||
write!(
|
||||
f,
|
||||
",END-DATE={}",
|
||||
quote(value.to_rfc3339_opts(SecondsFormat::AutoSi, true))
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(value) = &self.duration {
|
||||
write!(f, ",DURATION={}", value.as_secs_f64())?;
|
||||
}
|
||||
|
||||
if let Some(value) = &self.planned_duration {
|
||||
write!(f, ",PLANNED-DURATION={}", value.as_secs_f64())?;
|
||||
}
|
||||
|
||||
if let Some(value) = &self.scte35_cmd {
|
||||
write!(f, ",SCTE35-CMD={}", value)?;
|
||||
}
|
||||
|
||||
if let Some(value) = &self.scte35_out {
|
||||
write!(f, ",SCTE35-OUT={}", value)?;
|
||||
}
|
||||
|
||||
if let Some(value) = &self.scte35_in {
|
||||
write!(f, ",SCTE35-IN={}", value)?;
|
||||
}
|
||||
|
||||
for (k, v) in &self.client_attributes {
|
||||
write!(f, ",{}={}", k, v)?;
|
||||
}
|
||||
|
||||
if self.end_on_next {
|
||||
write!(f, ",END-ON-NEXT=YES",)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use chrono::offset::TimeZone;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(
|
||||
"#EXT-X-DATERANGE:\
|
||||
ID=\"splice-6FFFFFF0\",\
|
||||
START-DATE=\"2014-03-05T11:15:00Z\",\
|
||||
PLANNED-DURATION=59.993,\
|
||||
SCTE35-OUT=0xFC002F0000000000FF000014056F\
|
||||
FFFFF000E011622DCAFF000052636200000000000\
|
||||
A0008029896F50000008700000000"
|
||||
.parse::<ExtXDateRange>()
|
||||
.unwrap(),
|
||||
ExtXDateRange::builder()
|
||||
.id("splice-6FFFFFF0")
|
||||
.start_date(FixedOffset::east(0).ymd(2014, 3, 5).and_hms(11, 15, 0))
|
||||
.planned_duration(Duration::from_secs_f64(59.993))
|
||||
.scte35_out(
|
||||
"0xFC002F0000000000FF00001\
|
||||
4056FFFFFF000E011622DCAFF0\
|
||||
00052636200000000000A00080\
|
||||
29896F50000008700000000"
|
||||
)
|
||||
.build()
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
"#EXT-X-DATERANGE:\
|
||||
ID=\"test_id\",\
|
||||
CLASS=\"test_class\",\
|
||||
START-DATE=\"2014-03-05T11:15:00Z\",\
|
||||
END-DATE=\"2014-03-05T11:16:00Z\",\
|
||||
DURATION=60.1,\
|
||||
PLANNED-DURATION=59.993,\
|
||||
X-CUSTOM=45.3,\
|
||||
SCTE35-CMD=0xFC002F0000000000FF2,\
|
||||
SCTE35-OUT=0xFC002F0000000000FF0,\
|
||||
SCTE35-IN=0xFC002F0000000000FF1,\
|
||||
END-ON-NEXT=YES,\
|
||||
UNKNOWN=PHANTOM"
|
||||
.parse::<ExtXDateRange>()
|
||||
.unwrap(),
|
||||
ExtXDateRange::builder()
|
||||
.id("test_id")
|
||||
.class("test_class")
|
||||
.start_date(FixedOffset::east(0).ymd(2014, 3, 5).and_hms(11, 15, 0))
|
||||
.end_date(FixedOffset::east(0).ymd(2014, 3, 5).and_hms(11, 16, 0))
|
||||
.duration(Duration::from_secs_f64(60.1))
|
||||
.planned_duration(Duration::from_secs_f64(59.993))
|
||||
.insert_client_attribute("X-CUSTOM", 45.3)
|
||||
.scte35_cmd("0xFC002F0000000000FF2")
|
||||
.scte35_out("0xFC002F0000000000FF0")
|
||||
.scte35_in("0xFC002F0000000000FF1")
|
||||
.end_on_next(true)
|
||||
.build()
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
assert!("#EXT-X-DATERANGE:END-ON-NEXT=NO"
|
||||
.parse::<ExtXDateRange>()
|
||||
.is_err());
|
||||
|
||||
assert!("garbage".parse::<ExtXDateRange>().is_err());
|
||||
assert!("".parse::<ExtXDateRange>().is_err());
|
||||
|
||||
assert!("#EXT-X-DATERANGE:\
|
||||
ID=\"test_id\",\
|
||||
START-DATE=\"2014-03-05T11:15:00Z\",\
|
||||
END-ON-NEXT=YES"
|
||||
.parse::<ExtXDateRange>()
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(
|
||||
ExtXDateRange::builder()
|
||||
.id("test_id")
|
||||
.class("test_class")
|
||||
.start_date(FixedOffset::east(0).ymd(2014, 3, 5).and_hms(11, 15, 0))
|
||||
.end_date(FixedOffset::east(0).ymd(2014, 3, 5).and_hms(11, 16, 0))
|
||||
.duration(Duration::from_secs_f64(60.1))
|
||||
.planned_duration(Duration::from_secs_f64(59.993))
|
||||
.insert_client_attribute("X-CUSTOM", 45.3)
|
||||
.scte35_cmd("0xFC002F0000000000FF2")
|
||||
.scte35_out("0xFC002F0000000000FF0")
|
||||
.scte35_in("0xFC002F0000000000FF1")
|
||||
.end_on_next(true)
|
||||
.build()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
"#EXT-X-DATERANGE:\
|
||||
ID=\"test_id\",\
|
||||
CLASS=\"test_class\",\
|
||||
START-DATE=\"2014-03-05T11:15:00Z\",\
|
||||
END-DATE=\"2014-03-05T11:16:00Z\",\
|
||||
DURATION=60.1,\
|
||||
PLANNED-DURATION=59.993,\
|
||||
SCTE35-CMD=0xFC002F0000000000FF2,\
|
||||
SCTE35-OUT=0xFC002F0000000000FF0,\
|
||||
SCTE35-IN=0xFC002F0000000000FF1,\
|
||||
X-CUSTOM=45.3,\
|
||||
END-ON-NEXT=YES"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::utils::tag;
|
||||
use crate::Error;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// # [4.4.2.3. EXT-X-DISCONTINUITY]
|
||||
/// The [`ExtXDiscontinuity`] tag indicates a discontinuity between the
|
||||
|
@ -17,7 +17,7 @@ use crate::Error;
|
|||
/// [`Media Segment`]: crate::MediaSegment
|
||||
/// [4.4.2.3. EXT-X-DISCONTINUITY]:
|
||||
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.3
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct ExtXDiscontinuity;
|
||||
|
||||
impl ExtXDiscontinuity {
|
||||
|
@ -37,13 +37,14 @@ impl FromStr for ExtXDiscontinuity {
|
|||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
tag(input, Self::PREFIX)?;
|
||||
Ok(ExtXDiscontinuity)
|
||||
Ok(Self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
|
|
@ -2,24 +2,17 @@ use std::fmt;
|
|||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::utils::tag;
|
||||
use crate::Error;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// # [4.4.2.1. EXTINF]
|
||||
/// # [4.3.2.1. EXTINF]
|
||||
///
|
||||
/// The [`ExtInf`] tag specifies the duration of a [`Media Segment`]. It applies
|
||||
/// only to the next [`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
|
||||
/// [4.4.2.1. EXTINF]:
|
||||
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.1
|
||||
/// [4.3.2.1. EXTINF]: https://tools.ietf.org/html/rfc8216#section-4.3.2.1
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct ExtInf {
|
||||
duration: Duration,
|
||||
|
@ -137,10 +130,7 @@ impl RequiredVersion for ExtInf {
|
|||
impl fmt::Display for ExtInf {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", Self::PREFIX)?;
|
||||
|
||||
let duration = (self.duration.as_secs() as f64)
|
||||
+ (f64::from(self.duration.subsec_nanos()) / 1_000_000_000.0);
|
||||
write!(f, "{},", duration)?;
|
||||
write!(f, "{},", self.duration.as_secs_f64())?;
|
||||
|
||||
if let Some(value) = &self.title {
|
||||
write!(f, "{}", value)?;
|
||||
|
@ -188,6 +178,7 @@ impl From<Duration> for ExtInf {
|
|||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
@ -236,6 +227,9 @@ mod test {
|
|||
"#EXTINF:5,title".parse::<ExtInf>().unwrap(),
|
||||
ExtInf::with_title(Duration::from_secs(5), "title")
|
||||
);
|
||||
|
||||
assert!("#EXTINF:".parse::<ExtInf>().is_err());
|
||||
assert!("#EXTINF:garbage".parse::<ExtInf>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -258,4 +252,12 @@ mod test {
|
|||
ProtocolVersion::V3
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from() {
|
||||
assert_eq!(
|
||||
ExtInf::from(Duration::from_secs(1)),
|
||||
ExtInf::new(Duration::from_secs(1))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,32 +2,27 @@ use std::fmt;
|
|||
use std::ops::{Deref, DerefMut};
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::{DecryptionKey, EncryptionMethod};
|
||||
use crate::types::{DecryptionKey, EncryptionMethod, ProtocolVersion};
|
||||
use crate::utils::tag;
|
||||
use crate::Error;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// # [4.4.2.4. EXT-X-KEY]
|
||||
/// # [4.3.2.4. EXT-X-KEY]
|
||||
///
|
||||
/// [`Media Segment`]s may be encrypted. The [`ExtXKey`] tag specifies how to
|
||||
/// decrypt them. It applies to every [`Media Segment`] and to every Media
|
||||
/// Initialization Section declared by an [`ExtXMap`] tag, that appears
|
||||
/// between it and the next [`ExtXKey`] tag in the Playlist file with the
|
||||
/// same [`KeyFormat`] attribute (or the end of the Playlist file).
|
||||
///
|
||||
/// The format is:
|
||||
/// ```text
|
||||
/// #EXT-X-KEY:<attribute-list>
|
||||
/// ```
|
||||
///
|
||||
/// # Note
|
||||
/// In case of an empty key (`EncryptionMethod::None`),
|
||||
/// In case of an empty key ([`EncryptionMethod::None`]),
|
||||
/// all attributes will be ignored.
|
||||
///
|
||||
/// [`KeyFormat`]: crate::types::KeyFormat
|
||||
/// [`ExtXMap`]: crate::tags::ExtXMap
|
||||
/// [`Media Segment`]: crate::MediaSegment
|
||||
/// [4.4.2.4. EXT-X-KEY]:
|
||||
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.4
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct ExtXKey(DecryptionKey);
|
||||
|
||||
impl ExtXKey {
|
||||
|
@ -37,7 +32,7 @@ impl ExtXKey {
|
|||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use hls_m3u8::tags::ExtXKey;
|
||||
/// # use hls_m3u8::tags::ExtXKey;
|
||||
/// use hls_m3u8::types::EncryptionMethod;
|
||||
///
|
||||
/// let key = ExtXKey::new(EncryptionMethod::Aes128, "https://www.example.com/");
|
||||
|
@ -55,8 +50,7 @@ impl ExtXKey {
|
|||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use hls_m3u8::tags::ExtXKey;
|
||||
///
|
||||
/// # use hls_m3u8::tags::ExtXKey;
|
||||
/// let key = ExtXKey::empty();
|
||||
///
|
||||
/// assert_eq!(key.to_string(), "#EXT-X-KEY:METHOD=NONE");
|
||||
|
@ -72,20 +66,26 @@ impl ExtXKey {
|
|||
}
|
||||
|
||||
/// Returns whether the [`EncryptionMethod`] is
|
||||
/// [`None`](EncryptionMethod::None).
|
||||
/// [`None`].
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use hls_m3u8::tags::ExtXKey;
|
||||
/// # use hls_m3u8::tags::ExtXKey;
|
||||
/// use hls_m3u8::types::EncryptionMethod;
|
||||
///
|
||||
/// let key = ExtXKey::empty();
|
||||
///
|
||||
/// assert_eq!(key.method() == EncryptionMethod::None, key.is_empty());
|
||||
/// ```
|
||||
///
|
||||
/// [`None`]: EncryptionMethod::None
|
||||
pub fn is_empty(&self) -> bool { self.0.method() == EncryptionMethod::None }
|
||||
}
|
||||
|
||||
impl RequiredVersion for ExtXKey {
|
||||
fn required_version(&self) -> ProtocolVersion { self.0.required_version() }
|
||||
}
|
||||
|
||||
impl FromStr for ExtXKey {
|
||||
type Err = Error;
|
||||
|
||||
|
@ -113,6 +113,7 @@ impl DerefMut for ExtXKey {
|
|||
mod test {
|
||||
use super::*;
|
||||
use crate::types::{EncryptionMethod, KeyFormat};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
|
|
@ -2,66 +2,159 @@ use std::fmt;
|
|||
use std::str::FromStr;
|
||||
|
||||
use crate::attribute::AttributePairs;
|
||||
use crate::types::{ByteRange, ProtocolVersion, RequiredVersion};
|
||||
use crate::tags::ExtXKey;
|
||||
use crate::types::{ByteRange, ProtocolVersion};
|
||||
use crate::utils::{quote, tag, unquote};
|
||||
use crate::Error;
|
||||
use crate::{Encrypted, Error, RequiredVersion};
|
||||
|
||||
/// # [4.4.2.5. EXT-X-MAP]
|
||||
/// # [4.3.2.5. EXT-X-MAP]
|
||||
///
|
||||
/// The [`ExtXMap`] tag specifies how to obtain the Media Initialization
|
||||
/// Section, required to parse the applicable [Media Segment]s.
|
||||
/// Section, required to parse the applicable [`MediaSegment`]s.
|
||||
///
|
||||
/// Its format is:
|
||||
/// ```text
|
||||
/// #EXT-X-MAP:<attribute-list>
|
||||
/// ```
|
||||
///
|
||||
/// [Media Segment]: crate::MediaSegment
|
||||
/// [4.4.2.5. EXT-X-MAP]:
|
||||
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.5
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
/// [`MediaSegment`]: crate::MediaSegment
|
||||
/// [4.3.2.5. EXT-X-MAP]: https://tools.ietf.org/html/rfc8216#section-4.3.2.5
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct ExtXMap {
|
||||
uri: String,
|
||||
range: Option<ByteRange>,
|
||||
keys: Vec<ExtXKey>,
|
||||
}
|
||||
|
||||
impl ExtXMap {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-MAP:";
|
||||
|
||||
/// Makes a new [`ExtXMap`] tag.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMap;
|
||||
/// let map = ExtXMap::new("https://prod.mediaspace.com/init.bin");
|
||||
/// ```
|
||||
pub fn new<T: ToString>(uri: T) -> Self {
|
||||
ExtXMap {
|
||||
Self {
|
||||
uri: uri.to_string(),
|
||||
range: None,
|
||||
keys: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes a new [`ExtXMap`] tag with the given range.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMap;
|
||||
/// use hls_m3u8::types::ByteRange;
|
||||
///
|
||||
/// let map = ExtXMap::with_range(
|
||||
/// "https://prod.mediaspace.com/init.bin",
|
||||
/// ByteRange::new(9, Some(2)),
|
||||
/// );
|
||||
/// ```
|
||||
pub fn with_range<T: ToString>(uri: T, range: ByteRange) -> Self {
|
||||
ExtXMap {
|
||||
Self {
|
||||
uri: uri.to_string(),
|
||||
range: Some(range),
|
||||
keys: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the `URI` that identifies a resource,
|
||||
/// that contains the media initialization section.
|
||||
/// Returns the `URI` that identifies a resource, that contains the media
|
||||
/// initialization section.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMap;
|
||||
/// let map = ExtXMap::new("https://prod.mediaspace.com/init.bin");
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// map.uri(),
|
||||
/// &"https://prod.mediaspace.com/init.bin".to_string()
|
||||
/// );
|
||||
/// ```
|
||||
pub const fn uri(&self) -> &String { &self.uri }
|
||||
|
||||
/// Sets the `URI` that identifies a resource, that contains the media
|
||||
/// initialization section.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMap;
|
||||
/// let mut map = ExtXMap::new("https://prod.mediaspace.com/init.bin");
|
||||
///
|
||||
/// map.set_uri("https://dev.mediaspace.com/init.bin");
|
||||
/// assert_eq!(
|
||||
/// map.uri(),
|
||||
/// &"https://dev.mediaspace.com/init.bin".to_string()
|
||||
/// );
|
||||
/// ```
|
||||
pub fn set_uri<T: ToString>(&mut self, value: T) -> &mut Self {
|
||||
self.uri = value.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the range of the media initialization section.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMap;
|
||||
/// use hls_m3u8::types::ByteRange;
|
||||
///
|
||||
/// let map = ExtXMap::with_range(
|
||||
/// "https://prod.mediaspace.com/init.bin",
|
||||
/// ByteRange::new(9, Some(2)),
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(map.range(), Some(ByteRange::new(9, Some(2))));
|
||||
/// ```
|
||||
pub const fn range(&self) -> Option<ByteRange> { self.range }
|
||||
|
||||
/// Sets the range of the media initialization section.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMap;
|
||||
/// use hls_m3u8::types::ByteRange;
|
||||
///
|
||||
/// let mut map = ExtXMap::with_range(
|
||||
/// "https://prod.mediaspace.com/init.bin",
|
||||
/// ByteRange::new(9, Some(2)),
|
||||
/// );
|
||||
///
|
||||
/// map.set_range(Some(ByteRange::new(1, None)));
|
||||
/// assert_eq!(map.range(), Some(ByteRange::new(1, None)));
|
||||
/// ```
|
||||
pub fn set_range(&mut self, value: Option<ByteRange>) -> &mut Self {
|
||||
self.range = value;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Encrypted for ExtXMap {
|
||||
fn keys(&self) -> &Vec<ExtXKey> { &self.keys }
|
||||
|
||||
fn keys_mut(&mut self) -> &mut Vec<ExtXKey> { &mut self.keys }
|
||||
}
|
||||
|
||||
/// This tag requires [`ProtocolVersion::V6`].
|
||||
impl RequiredVersion for ExtXMap {
|
||||
// this should return ProtocolVersion::V5, if it does not contain an
|
||||
// EXT-X-I-FRAMES-ONLY!
|
||||
// http://alexzambelli.com/blog/2016/05/04/understanding-hls-versions-and-client-compatibility/
|
||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V6 }
|
||||
|
||||
fn introduced_version(&self) -> ProtocolVersion { ProtocolVersion::V5 }
|
||||
}
|
||||
|
||||
impl fmt::Display for ExtXMap {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", Self::PREFIX)?;
|
||||
write!(f, "URI={}", quote(&self.uri))?;
|
||||
|
||||
if let Some(value) = &self.range {
|
||||
write!(f, ",BYTERANGE={}", quote(value))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -79,7 +172,7 @@ impl FromStr for ExtXMap {
|
|||
match key.as_str() {
|
||||
"URI" => uri = Some(unquote(value)),
|
||||
"BYTERANGE" => {
|
||||
range = Some((unquote(value).parse())?);
|
||||
range = Some(unquote(value).parse()?);
|
||||
}
|
||||
_ => {
|
||||
// [6.3.1. General Client Responsibilities]
|
||||
|
@ -90,13 +183,18 @@ impl FromStr for ExtXMap {
|
|||
}
|
||||
|
||||
let uri = uri.ok_or_else(|| Error::missing_value("EXT-X-URI"))?;
|
||||
Ok(ExtXMap { uri, range })
|
||||
Ok(Self {
|
||||
uri,
|
||||
range,
|
||||
keys: vec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
@ -122,6 +220,12 @@ mod test {
|
|||
ExtXMap::with_range("foo", ByteRange::new(9, Some(2))),
|
||||
"#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\"".parse().unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
ExtXMap::with_range("foo", ByteRange::new(9, Some(2))),
|
||||
"#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\",UNKNOWN=IGNORED"
|
||||
.parse()
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -132,4 +236,10 @@ mod test {
|
|||
ProtocolVersion::V6
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypted() {
|
||||
assert_eq!(ExtXMap::new("foo").keys(), &vec![]);
|
||||
assert_eq!(ExtXMap::new("foo").keys_mut(), &mut vec![]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,11 +2,11 @@ use std::fmt;
|
|||
use std::ops::{Deref, DerefMut};
|
||||
use std::str::FromStr;
|
||||
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
use chrono::{DateTime, FixedOffset, SecondsFormat};
|
||||
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::utils::tag;
|
||||
use crate::Error;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// # [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]
|
||||
/// The [`ExtXProgramDateTime`] tag associates the first sample of a
|
||||
|
@ -24,8 +24,8 @@ impl ExtXProgramDateTime {
|
|||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXProgramDateTime;
|
||||
/// use chrono::{FixedOffset, TimeZone};
|
||||
/// use hls_m3u8::tags::ExtXProgramDateTime;
|
||||
///
|
||||
/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||
///
|
||||
|
@ -39,22 +39,71 @@ impl ExtXProgramDateTime {
|
|||
|
||||
/// Returns the date-time of the first sample of the associated media
|
||||
/// segment.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXProgramDateTime;
|
||||
/// use chrono::{FixedOffset, TimeZone};
|
||||
///
|
||||
/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||
///
|
||||
/// let program_date_time = ExtXProgramDateTime::new(
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 2, 19)
|
||||
/// .and_hms_milli(14, 54, 23, 31),
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// program_date_time.date_time(),
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 2, 19)
|
||||
/// .and_hms_milli(14, 54, 23, 31)
|
||||
/// );
|
||||
/// ```
|
||||
pub const fn date_time(&self) -> DateTime<FixedOffset> { self.0 }
|
||||
|
||||
/// Sets the date-time of the first sample of the associated media segment.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXProgramDateTime;
|
||||
/// use chrono::{FixedOffset, TimeZone};
|
||||
///
|
||||
/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||
///
|
||||
/// let mut program_date_time = ExtXProgramDateTime::new(
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 2, 19)
|
||||
/// .and_hms_milli(14, 54, 23, 31),
|
||||
/// );
|
||||
///
|
||||
/// program_date_time.set_date_time(
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 10, 10)
|
||||
/// .and_hms_milli(10, 10, 10, 10),
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// program_date_time.date_time(),
|
||||
/// FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 10, 10)
|
||||
/// .and_hms_milli(10, 10, 10, 10)
|
||||
/// );
|
||||
/// ```
|
||||
pub fn set_date_time(&mut self, value: DateTime<FixedOffset>) -> &mut Self {
|
||||
self.0 = value;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// This tag requires [`ProtocolVersion::V1`].
|
||||
impl RequiredVersion for ExtXProgramDateTime {
|
||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||
}
|
||||
|
||||
impl fmt::Display for ExtXProgramDateTime {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let date_time = self.0.to_rfc3339();
|
||||
let date_time = self.0.to_rfc3339_opts(SecondsFormat::Millis, true);
|
||||
write!(f, "{}{}", Self::PREFIX, date_time)
|
||||
}
|
||||
}
|
||||
|
@ -83,7 +132,8 @@ impl DerefMut for ExtXProgramDateTime {
|
|||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use chrono::TimeZone;
|
||||
use chrono::{Datelike, TimeZone};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||
|
||||
|
@ -126,4 +176,32 @@ mod test {
|
|||
ProtocolVersion::V1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deref() {
|
||||
assert_eq!(
|
||||
ExtXProgramDateTime::new(
|
||||
FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
.ymd(2010, 2, 19)
|
||||
.and_hms_milli(14, 54, 23, 31),
|
||||
)
|
||||
.year(),
|
||||
2010
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deref_mut() {
|
||||
assert_eq!(
|
||||
ExtXProgramDateTime::new(
|
||||
FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
.ymd(2010, 2, 19)
|
||||
.and_hms_milli(14, 54, 23, 31),
|
||||
)
|
||||
.deref_mut(),
|
||||
&mut FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
.ymd(2010, 2, 19)
|
||||
.and_hms_milli(14, 54, 23, 31),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::utils::tag;
|
||||
use crate::Error;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS]
|
||||
///
|
||||
|
@ -28,13 +28,14 @@ impl FromStr for ExtXIndependentSegments {
|
|||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
tag(input, Self::PREFIX)?;
|
||||
Ok(ExtXIndependentSegments)
|
||||
Ok(Self)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
|
|
@ -2,14 +2,14 @@ use std::fmt;
|
|||
use std::str::FromStr;
|
||||
|
||||
use crate::attribute::AttributePairs;
|
||||
use crate::types::{ProtocolVersion, RequiredVersion, SignedDecimalFloatingPoint};
|
||||
use crate::types::{ProtocolVersion, SignedDecimalFloatingPoint};
|
||||
use crate::utils::{parse_yes_or_no, tag};
|
||||
use crate::Error;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// [4.3.5.2. EXT-X-START]
|
||||
///
|
||||
/// [4.3.5.2. EXT-X-START]: https://tools.ietf.org/html/rfc8216#section-4.3.5.2
|
||||
#[derive(PartialOrd, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(PartialOrd, Debug, Clone, Copy, PartialEq)]
|
||||
pub struct ExtXStart {
|
||||
time_offset: SignedDecimalFloatingPoint,
|
||||
precise: bool,
|
||||
|
@ -26,7 +26,7 @@ impl ExtXStart {
|
|||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXStart;
|
||||
/// ExtXStart::new(20.123456);
|
||||
/// let start = ExtXStart::new(20.123456);
|
||||
/// ```
|
||||
pub fn new(time_offset: f64) -> Self {
|
||||
Self {
|
||||
|
@ -157,6 +157,7 @@ impl FromStr for ExtXStart {
|
|||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
|
150
src/traits.rs
Normal file
150
src/traits.rs
Normal file
|
@ -0,0 +1,150 @@
|
|||
use crate::tags::ExtXKey;
|
||||
use crate::types::{EncryptionMethod, ProtocolVersion};
|
||||
|
||||
/// A trait, that is implemented on all tags, that could be encrypted.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use hls_m3u8::tags::ExtXKey;
|
||||
/// use hls_m3u8::types::EncryptionMethod;
|
||||
/// use hls_m3u8::Encrypted;
|
||||
///
|
||||
/// struct ExampleTag {
|
||||
/// keys: Vec<ExtXKey>,
|
||||
/// }
|
||||
///
|
||||
/// // Implementing the trait is very simple:
|
||||
/// // Simply expose the internal buffer, that contains all the keys.
|
||||
/// impl Encrypted for ExampleTag {
|
||||
/// fn keys(&self) -> &Vec<ExtXKey> { &self.keys }
|
||||
///
|
||||
/// fn keys_mut(&mut self) -> &mut Vec<ExtXKey> { &mut self.keys }
|
||||
/// }
|
||||
///
|
||||
/// let mut example_tag = ExampleTag { keys: vec![] };
|
||||
///
|
||||
/// // adding new keys:
|
||||
/// example_tag.set_keys(vec![ExtXKey::empty()]);
|
||||
/// example_tag.push_key(ExtXKey::new(
|
||||
/// EncryptionMethod::Aes128,
|
||||
/// "http://www.example.com/data.bin",
|
||||
/// ));
|
||||
///
|
||||
/// // getting the keys:
|
||||
/// assert_eq!(
|
||||
/// example_tag.keys(),
|
||||
/// &vec![
|
||||
/// ExtXKey::empty(),
|
||||
/// ExtXKey::new(EncryptionMethod::Aes128, "http://www.example.com/data.bin",)
|
||||
/// ]
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// example_tag.keys_mut(),
|
||||
/// &mut vec![
|
||||
/// ExtXKey::empty(),
|
||||
/// ExtXKey::new(EncryptionMethod::Aes128, "http://www.example.com/data.bin",)
|
||||
/// ]
|
||||
/// );
|
||||
///
|
||||
/// assert!(example_tag.is_encrypted());
|
||||
/// assert!(!example_tag.is_not_encrypted());
|
||||
/// ```
|
||||
pub trait Encrypted {
|
||||
/// Returns a shared reference to all keys, that can be used to decrypt this
|
||||
/// tag.
|
||||
fn keys(&self) -> &Vec<ExtXKey>;
|
||||
|
||||
/// Returns an exclusive reference to all keys, that can be used to decrypt
|
||||
/// this tag.
|
||||
fn keys_mut(&mut self) -> &mut Vec<ExtXKey>;
|
||||
|
||||
/// Sets all keys, that can be used to decrypt this tag.
|
||||
fn set_keys(&mut self, value: Vec<ExtXKey>) -> &mut Self {
|
||||
let keys = self.keys_mut();
|
||||
*keys = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a single key to the list of keys, that can be used to decrypt this
|
||||
/// tag.
|
||||
fn push_key(&mut self, value: ExtXKey) -> &mut Self {
|
||||
self.keys_mut().push(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns `true`, if the tag is encrypted.
|
||||
///
|
||||
/// # Note
|
||||
/// This will return `true`, if any of the keys satisfies
|
||||
/// ```text
|
||||
/// key.method() != EncryptionMethod::None
|
||||
/// ```
|
||||
fn is_encrypted(&self) -> bool {
|
||||
if self.keys().is_empty() {
|
||||
return false;
|
||||
}
|
||||
self.keys()
|
||||
.iter()
|
||||
.any(|k| k.method() != EncryptionMethod::None)
|
||||
}
|
||||
|
||||
/// Returns `false`, if the tag is not encrypted.
|
||||
///
|
||||
/// # Note
|
||||
/// This is the inverse of [`is_encrypted`].
|
||||
///
|
||||
/// [`is_encrypted`]: #method.is_encrypted
|
||||
fn is_not_encrypted(&self) -> bool { !self.is_encrypted() }
|
||||
}
|
||||
|
||||
/// # Example
|
||||
/// Implementing it:
|
||||
/// ```
|
||||
/// # use hls_m3u8::RequiredVersion;
|
||||
/// use hls_m3u8::types::ProtocolVersion;
|
||||
///
|
||||
/// struct ExampleTag(u64);
|
||||
///
|
||||
/// impl RequiredVersion for ExampleTag {
|
||||
/// fn required_version(&self) -> ProtocolVersion {
|
||||
/// if self.0 == 5 {
|
||||
/// ProtocolVersion::V4
|
||||
/// } else {
|
||||
/// ProtocolVersion::V1
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// assert_eq!(ExampleTag(5).required_version(), ProtocolVersion::V4);
|
||||
/// assert_eq!(ExampleTag(2).required_version(), ProtocolVersion::V1);
|
||||
/// ```
|
||||
pub trait RequiredVersion {
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
///
|
||||
/// # Note
|
||||
/// This is for the latest working [`ProtocolVersion`] and a client, that
|
||||
/// only supports an older version would break.
|
||||
fn required_version(&self) -> ProtocolVersion;
|
||||
|
||||
/// The protocol version, in which the tag has been introduced.
|
||||
fn introduced_version(&self) -> ProtocolVersion { self.required_version() }
|
||||
}
|
||||
|
||||
impl<T: RequiredVersion> RequiredVersion for Vec<T> {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
self.iter()
|
||||
.map(|v| v.required_version())
|
||||
.max()
|
||||
// return ProtocolVersion::V1, if the iterator is empty:
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: RequiredVersion> RequiredVersion for Option<T> {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
self.iter()
|
||||
.map(|v| v.required_version())
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
|
@ -114,6 +114,7 @@ impl FromStr for ByteRange {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
|
|
@ -11,14 +11,6 @@ use crate::Error;
|
|||
/// present in any [`MediaSegment`] in the rendition. For example, an
|
||||
/// `AC-3 5.1` rendition would have a `CHANNELS="6"` attribute.
|
||||
///
|
||||
/// The second parameter identifies the encoding of object-based audio used by
|
||||
/// the rendition. This parameter is a comma-separated list of Audio
|
||||
/// Object Coding Identifiers. It is optional. An Audio Object
|
||||
/// Coding Identifier is a string containing characters from the set
|
||||
/// `[A..Z]`, `[0..9]`, and `'-'`. They are codec-specific. A parameter
|
||||
/// value of consisting solely of the dash character (`'-'`) indicates
|
||||
/// that the audio is not object-based.
|
||||
///
|
||||
/// # Example
|
||||
/// Creating a `CHANNELS="6"` attribute
|
||||
/// ```
|
||||
|
@ -31,16 +23,11 @@ use crate::Error;
|
|||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// # Note
|
||||
/// Currently there are no example playlists in the documentation,
|
||||
/// or in popular m3u8 libraries, showing a usage for the second parameter
|
||||
/// of [`Channels`], so if you have one please open an issue on github!
|
||||
///
|
||||
/// [`MediaSegment`]: crate::MediaSegment
|
||||
#[derive(Debug, Clone, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
pub struct Channels {
|
||||
first_parameter: u64,
|
||||
second_parameter: Option<Vec<String>>,
|
||||
channel_number: u64,
|
||||
unknown: Vec<String>,
|
||||
}
|
||||
|
||||
impl Channels {
|
||||
|
@ -51,77 +38,36 @@ impl Channels {
|
|||
/// # use hls_m3u8::types::Channels;
|
||||
/// let mut channels = Channels::new(6);
|
||||
/// ```
|
||||
pub const fn new(value: u64) -> Self {
|
||||
pub fn new(value: u64) -> Self {
|
||||
Self {
|
||||
first_parameter: value,
|
||||
second_parameter: None,
|
||||
channel_number: value,
|
||||
unknown: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the first parameter.
|
||||
/// Returns the channel number.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::types::Channels;
|
||||
/// let mut channels = Channels::new(6);
|
||||
///
|
||||
/// assert_eq!(channels.first_parameter(), 6);
|
||||
/// assert_eq!(channels.channel_number(), 6);
|
||||
/// ```
|
||||
pub const fn first_parameter(&self) -> u64 { self.first_parameter }
|
||||
pub const fn channel_number(&self) -> u64 { self.channel_number }
|
||||
|
||||
/// Sets the first parameter.
|
||||
/// Sets the channel number.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::types::Channels;
|
||||
/// let mut channels = Channels::new(3);
|
||||
///
|
||||
/// channels.set_first_parameter(6);
|
||||
/// assert_eq!(channels.first_parameter(), 6)
|
||||
/// channels.set_channel_number(6);
|
||||
/// assert_eq!(channels.channel_number(), 6)
|
||||
/// ```
|
||||
pub fn set_first_parameter(&mut self, value: u64) -> &mut Self {
|
||||
self.first_parameter = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the second parameter, if there is any!
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::types::Channels;
|
||||
/// let mut channels = Channels::new(3);
|
||||
/// # assert_eq!(channels.second_parameter(), &None);
|
||||
///
|
||||
/// channels.set_second_parameter(Some(vec!["AAC", "MP3"]));
|
||||
/// assert_eq!(
|
||||
/// channels.second_parameter(),
|
||||
/// &Some(vec!["AAC".to_string(), "MP3".to_string()])
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// # Note
|
||||
/// Currently there is no use for this parameter.
|
||||
pub const fn second_parameter(&self) -> &Option<Vec<String>> { &self.second_parameter }
|
||||
|
||||
/// Sets the second parameter.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::types::Channels;
|
||||
/// let mut channels = Channels::new(3);
|
||||
/// # assert_eq!(channels.second_parameter(), &None);
|
||||
///
|
||||
/// channels.set_second_parameter(Some(vec!["AAC", "MP3"]));
|
||||
/// assert_eq!(
|
||||
/// channels.second_parameter(),
|
||||
/// &Some(vec!["AAC".to_string(), "MP3".to_string()])
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// # Note
|
||||
/// Currently there is no use for this parameter.
|
||||
pub fn set_second_parameter<T: ToString>(&mut self, value: Option<Vec<T>>) -> &mut Self {
|
||||
self.second_parameter = value.map(|v| v.into_iter().map(|s| s.to_string()).collect());
|
||||
pub fn set_channel_number(&mut self, value: u64) -> &mut Self {
|
||||
self.channel_number = value;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -131,28 +77,23 @@ impl FromStr for Channels {
|
|||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
let parameters = input.split('/').collect::<Vec<_>>();
|
||||
let first_parameter = parameters
|
||||
let channel_number = parameters
|
||||
.first()
|
||||
.ok_or_else(|| Error::missing_attribute("First parameter of channels!"))?
|
||||
.parse()?;
|
||||
|
||||
let second_parameter = parameters
|
||||
.get(1)
|
||||
.map(|v| v.split(',').map(|v| v.to_string()).collect());
|
||||
|
||||
Ok(Self {
|
||||
first_parameter,
|
||||
second_parameter,
|
||||
channel_number,
|
||||
unknown: parameters[1..].iter().map(|v| v.to_string()).collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Channels {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", self.first_parameter)?;
|
||||
|
||||
if let Some(second) = &self.second_parameter {
|
||||
write!(f, "/{}", second.join(","))?;
|
||||
write!(f, "{}", self.channel_number)?;
|
||||
if !self.unknown.is_empty() {
|
||||
write!(f, "{}", self.unknown.join(","))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -162,31 +103,20 @@ impl fmt::Display for Channels {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
let mut channels = Channels::new(6);
|
||||
assert_eq!(channels.to_string(), "6".to_string());
|
||||
|
||||
channels.set_first_parameter(7);
|
||||
channels.set_channel_number(7);
|
||||
assert_eq!(channels.to_string(), "7".to_string());
|
||||
|
||||
assert_eq!(
|
||||
"6/P,K,J".to_string(),
|
||||
Channels::new(6)
|
||||
.set_second_parameter(Some(vec!["P", "K", "J"]))
|
||||
.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!("6".parse::<Channels>().unwrap(), Channels::new(6));
|
||||
let mut result = Channels::new(6);
|
||||
result.set_second_parameter(Some(vec!["P", "K", "J"]));
|
||||
|
||||
assert_eq!("6/P,K,J".parse::<Channels>().unwrap(), result);
|
||||
|
||||
assert!("garbage".parse::<Channels>().is_err());
|
||||
assert!("".parse::<Channels>().is_err());
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use core::convert::Infallible;
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::utils::{quote, unquote};
|
||||
use crate::Error;
|
||||
|
||||
/// The identifier of a closed captions group or its absence.
|
||||
///
|
||||
|
@ -26,7 +26,7 @@ impl fmt::Display for ClosedCaptions {
|
|||
}
|
||||
|
||||
impl FromStr for ClosedCaptions {
|
||||
type Err = Error;
|
||||
type Err = Infallible;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
if input.trim() == "NONE" {
|
||||
|
@ -40,6 +40,7 @@ impl FromStr for ClosedCaptions {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
|
|
@ -23,7 +23,7 @@ impl DecimalFloatingPoint {
|
|||
/// otherwise this function will return an error that has the kind
|
||||
/// `ErrorKind::InvalidInput`.
|
||||
pub fn new(value: f64) -> crate::Result<Self> {
|
||||
if value.is_sign_negative() || value.is_infinite() {
|
||||
if value.is_sign_negative() || value.is_infinite() || value.is_nan() {
|
||||
return Err(Error::invalid_input());
|
||||
}
|
||||
Ok(Self(value))
|
||||
|
@ -35,8 +35,6 @@ impl DecimalFloatingPoint {
|
|||
pub const fn as_f64(self) -> f64 { self.0 }
|
||||
}
|
||||
|
||||
impl Eq for DecimalFloatingPoint {}
|
||||
|
||||
// this trait is implemented manually, so it doesn't construct a
|
||||
// [`DecimalFloatingPoint`], with a negative value.
|
||||
impl FromStr for DecimalFloatingPoint {
|
||||
|
@ -56,7 +54,7 @@ impl From<f64> for DecimalFloatingPoint {
|
|||
let mut result = value;
|
||||
|
||||
// guard against the unlikely case of an infinite value...
|
||||
if result.is_infinite() {
|
||||
if result.is_infinite() || result.is_nan() {
|
||||
result = 0.0;
|
||||
}
|
||||
|
||||
|
@ -65,12 +63,13 @@ impl From<f64> for DecimalFloatingPoint {
|
|||
}
|
||||
|
||||
impl From<f32> for DecimalFloatingPoint {
|
||||
fn from(value: f32) -> Self { (value as f64).into() }
|
||||
fn from(value: f32) -> Self { f64::from(value).into() }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
macro_rules! test_from {
|
||||
( $($input:expr),* ) => {
|
||||
|
@ -88,7 +87,7 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
test_from![1u8, 1u16, 1u32, 1.0f32, -1.0f32, 1.0f64, -1.0f64];
|
||||
test_from![1_u8, 1_u16, 1_u32, 1.0_f32, -1.0_f32, 1.0_f64, -1.0_f64];
|
||||
|
||||
#[test]
|
||||
pub fn test_display() {
|
||||
|
|
|
@ -4,14 +4,15 @@ use derive_more::Display;
|
|||
|
||||
use crate::Error;
|
||||
|
||||
/// Decimal resolution.
|
||||
/// This is a simple wrapper type for the display resolution. (1920x1080,
|
||||
/// 1280x720, ...).
|
||||
///
|
||||
/// See: [4.2. Attribute Lists]
|
||||
///
|
||||
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
||||
#[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display)]
|
||||
#[display(fmt = "{}x{}", width, height)]
|
||||
pub(crate) struct DecimalResolution {
|
||||
pub struct DecimalResolution {
|
||||
width: usize,
|
||||
height: usize,
|
||||
}
|
||||
|
@ -41,7 +42,7 @@ impl DecimalResolution {
|
|||
|
||||
/// [`DecimalResolution`] can be constructed from a tuple; `(width, height)`.
|
||||
impl From<(usize, usize)> for DecimalResolution {
|
||||
fn from(value: (usize, usize)) -> Self { DecimalResolution::new(value.0, value.1) }
|
||||
fn from(value: (usize, usize)) -> Self { Self::new(value.0, value.1) }
|
||||
}
|
||||
|
||||
impl FromStr for DecimalResolution {
|
||||
|
@ -67,6 +68,7 @@ impl FromStr for DecimalResolution {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
|
|
@ -6,12 +6,11 @@ use derive_builder::Builder;
|
|||
use crate::attribute::AttributePairs;
|
||||
use crate::types::{
|
||||
EncryptionMethod, InitializationVector, KeyFormat, KeyFormatVersions, ProtocolVersion,
|
||||
RequiredVersion,
|
||||
};
|
||||
use crate::utils::{quote, unquote};
|
||||
use crate::Error;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
#[derive(Builder, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[derive(Builder, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
#[builder(setter(into), build_fn(validate = "Self::validate"))]
|
||||
/// [`DecryptionKey`] contains data, that is shared between [`ExtXSessionKey`]
|
||||
/// and [`ExtXKey`].
|
||||
|
@ -46,7 +45,7 @@ impl DecryptionKeyBuilder {
|
|||
}
|
||||
|
||||
impl DecryptionKey {
|
||||
/// Makes a new [DecryptionKey].
|
||||
/// Makes a new [`DecryptionKey`].
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
|
@ -65,7 +64,7 @@ impl DecryptionKey {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns the [EncryptionMethod].
|
||||
/// Returns the [`EncryptionMethod`].
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
|
@ -81,7 +80,7 @@ impl DecryptionKey {
|
|||
/// Returns a Builder to build a [DecryptionKey].
|
||||
pub fn builder() -> DecryptionKeyBuilder { DecryptionKeyBuilder::default() }
|
||||
|
||||
/// Sets the [EncryptionMethod].
|
||||
/// Sets the [`EncryptionMethod`].
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
|
@ -121,7 +120,7 @@ impl DecryptionKey {
|
|||
/// Sets the `URI` attribute.
|
||||
///
|
||||
/// # Note
|
||||
/// This attribute is required, if the [EncryptionMethod] is not `None`.
|
||||
/// This attribute is required, if the [`EncryptionMethod`] is not `None`.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
|
@ -208,7 +207,7 @@ impl DecryptionKey {
|
|||
/// ```
|
||||
pub const fn key_format(&self) -> Option<KeyFormat> { self.key_format }
|
||||
|
||||
/// Sets the [KeyFormat] attribute.
|
||||
/// Sets the [`KeyFormat`] attribute.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
|
@ -222,11 +221,11 @@ impl DecryptionKey {
|
|||
/// assert_eq!(key.key_format(), Some(KeyFormat::Identity));
|
||||
/// ```
|
||||
pub fn set_key_format<T: Into<KeyFormat>>(&mut self, value: Option<T>) -> &mut Self {
|
||||
self.key_format = value.map(|v| v.into());
|
||||
self.key_format = value.map(Into::into);
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the [KeyFormatVersions] attribute.
|
||||
/// Returns the [`KeyFormatVersions`] attribute.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
|
@ -246,7 +245,7 @@ impl DecryptionKey {
|
|||
&self.key_format_versions
|
||||
}
|
||||
|
||||
/// Sets the [KeyFormatVersions] attribute.
|
||||
/// Sets the [`KeyFormatVersions`] attribute.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
|
@ -267,7 +266,7 @@ impl DecryptionKey {
|
|||
&mut self,
|
||||
value: Option<T>,
|
||||
) -> &mut Self {
|
||||
self.key_format_versions = value.map(|v| v.into());
|
||||
self.key_format_versions = value.map(Into::into);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -354,6 +353,7 @@ impl fmt::Display for DecryptionKey {
|
|||
mod test {
|
||||
use super::*;
|
||||
use crate::types::EncryptionMethod;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_builder() {
|
||||
|
|
|
@ -50,6 +50,7 @@ pub enum EncryptionMethod {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
|
|
@ -17,6 +17,7 @@ pub enum HdcpLevel {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
|
|
@ -81,6 +81,7 @@ pub enum InStreamId {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
macro_rules! gen_tests {
|
||||
( $($string:expr => $enum:expr),* ) => {
|
||||
|
|
|
@ -66,6 +66,7 @@ impl FromStr for InitializationVector {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::utils::{quote, tag, unquote};
|
||||
use crate::Error;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||
/// [`KeyFormat`] specifies, how the key is represented in the
|
||||
|
@ -31,6 +31,7 @@ impl fmt::Display for KeyFormat {
|
|||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", quote("identity")) }
|
||||
}
|
||||
|
||||
/// This tag requires [`ProtocolVersion::V5`].
|
||||
impl RequiredVersion for KeyFormat {
|
||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V5 }
|
||||
}
|
||||
|
@ -38,6 +39,7 @@ impl RequiredVersion for KeyFormat {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
use std::convert::Infallible;
|
||||
use std::fmt;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::utils::{quote, unquote};
|
||||
use crate::Error;
|
||||
use crate::RequiredVersion;
|
||||
|
||||
/// A list of [usize], that can be used to indicate which version(s)
|
||||
/// A list of [`usize`], that can be used to indicate which version(s)
|
||||
/// this instance complies with, if more than one version of a particular
|
||||
/// [`KeyFormat`] is defined.
|
||||
///
|
||||
|
@ -14,10 +15,6 @@ use crate::Error;
|
|||
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||
pub struct KeyFormatVersions(Vec<usize>);
|
||||
|
||||
impl Default for KeyFormatVersions {
|
||||
fn default() -> Self { Self(vec![1]) }
|
||||
}
|
||||
|
||||
impl KeyFormatVersions {
|
||||
/// Makes a new [`KeyFormatVersions`].
|
||||
pub fn new() -> Self { Self::default() }
|
||||
|
@ -36,6 +33,10 @@ impl KeyFormatVersions {
|
|||
pub fn is_default(&self) -> bool { self.0 == vec![1] && self.0.len() == 1 || self.0.is_empty() }
|
||||
}
|
||||
|
||||
impl Default for KeyFormatVersions {
|
||||
fn default() -> Self { Self(vec![1]) }
|
||||
}
|
||||
|
||||
impl Deref for KeyFormatVersions {
|
||||
type Target = Vec<usize>;
|
||||
|
||||
|
@ -46,12 +47,13 @@ impl DerefMut for KeyFormatVersions {
|
|||
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 }
|
||||
}
|
||||
|
||||
/// This tag requires [`ProtocolVersion::V5`].
|
||||
impl RequiredVersion for KeyFormatVersions {
|
||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V5 }
|
||||
}
|
||||
|
||||
impl FromStr for KeyFormatVersions {
|
||||
type Err = Error;
|
||||
type Err = Infallible;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
let mut result = unquote(input)
|
||||
|
@ -95,6 +97,7 @@ impl<T: Into<Vec<usize>>> From<T> for KeyFormatVersions {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
|
|
@ -14,6 +14,7 @@ pub enum MediaType {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
|
|
|
@ -15,6 +15,7 @@ mod media_type;
|
|||
mod protocol_version;
|
||||
mod signed_decimal_floating_point;
|
||||
mod stream_inf;
|
||||
mod value;
|
||||
|
||||
pub use byte_range::*;
|
||||
pub use channels::*;
|
||||
|
@ -32,3 +33,4 @@ pub use media_type::*;
|
|||
pub use protocol_version::*;
|
||||
pub(crate) use signed_decimal_floating_point::*;
|
||||
pub use stream_inf::*;
|
||||
pub use value::*;
|
||||
|
|
|
@ -3,30 +3,6 @@ use std::str::FromStr;
|
|||
|
||||
use crate::Error;
|
||||
|
||||
/// # Example
|
||||
/// Implementing it:
|
||||
/// ```
|
||||
/// # use hls_m3u8::types::{ProtocolVersion, RequiredVersion};
|
||||
/// #
|
||||
/// struct NewTag(u64);
|
||||
///
|
||||
/// impl RequiredVersion for NewTag {
|
||||
/// fn required_version(&self) -> ProtocolVersion {
|
||||
/// if self.0 == 5 {
|
||||
/// ProtocolVersion::V4
|
||||
/// } else {
|
||||
/// ProtocolVersion::V1
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// assert_eq!(NewTag(5).required_version(), ProtocolVersion::V4);
|
||||
/// assert_eq!(NewTag(2).required_version(), ProtocolVersion::V1);
|
||||
/// ```
|
||||
pub trait RequiredVersion {
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
fn required_version(&self) -> ProtocolVersion;
|
||||
}
|
||||
|
||||
/// # [7. Protocol Version Compatibility]
|
||||
/// The [`ProtocolVersion`] specifies, which m3u8 revision is required, to parse
|
||||
/// a certain tag correctly.
|
||||
|
@ -97,6 +73,7 @@ impl Default for ProtocolVersion {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
|
|
@ -10,20 +10,20 @@ use derive_more::{Display, FromStr};
|
|||
pub(crate) struct SignedDecimalFloatingPoint(f64);
|
||||
|
||||
impl SignedDecimalFloatingPoint {
|
||||
/// Makes a new [SignedDecimalFloatingPoint] instance.
|
||||
/// Makes a new [`SignedDecimalFloatingPoint`] instance.
|
||||
///
|
||||
/// # Panics
|
||||
/// The given value must be finite, otherwise this function will panic!
|
||||
pub fn new(value: f64) -> Self {
|
||||
if value.is_infinite() {
|
||||
panic!("Floating point value must be finite!");
|
||||
if value.is_infinite() || value.is_nan() {
|
||||
panic!("Floating point value must be finite and not NaN!");
|
||||
}
|
||||
Self(value)
|
||||
}
|
||||
|
||||
pub(crate) const fn from_f64_unchecked(value: f64) -> Self { Self(value) }
|
||||
|
||||
/// Converts [DecimalFloatingPoint] to [f64].
|
||||
/// Converts [`DecimalFloatingPoint`] to [`f64`].
|
||||
pub const fn as_f64(self) -> f64 { self.0 }
|
||||
}
|
||||
|
||||
|
@ -33,11 +33,10 @@ impl Deref for SignedDecimalFloatingPoint {
|
|||
fn deref(&self) -> &Self::Target { &self.0 }
|
||||
}
|
||||
|
||||
impl Eq for SignedDecimalFloatingPoint {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
macro_rules! test_from {
|
||||
( $( $input:expr => $output:expr ),* ) => {
|
||||
|
@ -56,21 +55,21 @@ mod tests {
|
|||
}
|
||||
|
||||
test_from![
|
||||
SignedDecimalFloatingPoint::from(1u8) => SignedDecimalFloatingPoint::new(1.0),
|
||||
SignedDecimalFloatingPoint::from(1i8) => SignedDecimalFloatingPoint::new(1.0),
|
||||
SignedDecimalFloatingPoint::from(1u16) => SignedDecimalFloatingPoint::new(1.0),
|
||||
SignedDecimalFloatingPoint::from(1i16) => SignedDecimalFloatingPoint::new(1.0),
|
||||
SignedDecimalFloatingPoint::from(1u32) => SignedDecimalFloatingPoint::new(1.0),
|
||||
SignedDecimalFloatingPoint::from(1i32) => SignedDecimalFloatingPoint::new(1.0),
|
||||
SignedDecimalFloatingPoint::from(1.0f32) => SignedDecimalFloatingPoint::new(1.0),
|
||||
SignedDecimalFloatingPoint::from(1.0f64) => SignedDecimalFloatingPoint::new(1.0)
|
||||
SignedDecimalFloatingPoint::from(1_u8) => SignedDecimalFloatingPoint::new(1.0),
|
||||
SignedDecimalFloatingPoint::from(1_i8) => SignedDecimalFloatingPoint::new(1.0),
|
||||
SignedDecimalFloatingPoint::from(1_u16) => SignedDecimalFloatingPoint::new(1.0),
|
||||
SignedDecimalFloatingPoint::from(1_i16) => SignedDecimalFloatingPoint::new(1.0),
|
||||
SignedDecimalFloatingPoint::from(1_u32) => SignedDecimalFloatingPoint::new(1.0),
|
||||
SignedDecimalFloatingPoint::from(1_i32) => SignedDecimalFloatingPoint::new(1.0),
|
||||
SignedDecimalFloatingPoint::from(1.0_f32) => SignedDecimalFloatingPoint::new(1.0),
|
||||
SignedDecimalFloatingPoint::from(1.0_f64) => SignedDecimalFloatingPoint::new(1.0)
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(
|
||||
SignedDecimalFloatingPoint::new(1.0).to_string(),
|
||||
1.0f64.to_string()
|
||||
1.0_f64.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,26 +1,44 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use derive_builder::Builder;
|
||||
|
||||
use crate::attribute::AttributePairs;
|
||||
use crate::types::{DecimalResolution, HdcpLevel};
|
||||
use crate::utils::{quote, unquote};
|
||||
use crate::Error;
|
||||
|
||||
/// [4.3.4.2. EXT-X-STREAM-INF]
|
||||
/// # [4.3.4.2. EXT-X-STREAM-INF]
|
||||
///
|
||||
/// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2
|
||||
#[derive(PartialOrd, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[derive(Builder, PartialOrd, Debug, Clone, PartialEq, Eq, Hash, Ord)]
|
||||
#[builder(setter(into, strip_option))]
|
||||
#[builder(derive(Debug, PartialEq))]
|
||||
pub struct StreamInf {
|
||||
/// The maximum bandwidth of the stream.
|
||||
bandwidth: u64,
|
||||
#[builder(default)]
|
||||
/// The average bandwidth of the stream.
|
||||
average_bandwidth: Option<u64>,
|
||||
#[builder(default)]
|
||||
/// Every media format in any of the renditions specified by the Variant
|
||||
/// Stream.
|
||||
codecs: Option<String>,
|
||||
#[builder(default)]
|
||||
/// The resolution of the stream.
|
||||
resolution: Option<DecimalResolution>,
|
||||
#[builder(default)]
|
||||
/// High-bandwidth Digital Content Protection
|
||||
hdcp_level: Option<HdcpLevel>,
|
||||
#[builder(default)]
|
||||
/// It indicates the set of video renditions, that should be used when
|
||||
/// playing the presentation.
|
||||
video: Option<String>,
|
||||
}
|
||||
|
||||
impl StreamInf {
|
||||
/// Creates a new [StreamInf].
|
||||
/// Creates a new [`StreamInf`].
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use hls_m3u8::types::StreamInf;
|
||||
|
@ -211,7 +229,7 @@ impl StreamInf {
|
|||
/// assert_eq!(stream.hdcp_level(), Some(HdcpLevel::None));
|
||||
/// ```
|
||||
pub fn set_hdcp_level<T: Into<HdcpLevel>>(&mut self, value: Option<T>) -> &mut Self {
|
||||
self.hdcp_level = value.map(|v| v.into());
|
||||
self.hdcp_level = value.map(Into::into);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -282,6 +300,7 @@ impl FromStr for StreamInf {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
|
107
src/types/value.rs
Normal file
107
src/types/value.rs
Normal file
|
@ -0,0 +1,107 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use hex;
|
||||
|
||||
use crate::utils::{quote, unquote};
|
||||
use crate::Error;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, PartialOrd)]
|
||||
/// A [`Value`].
|
||||
pub enum Value {
|
||||
/// A [`String`].
|
||||
String(String),
|
||||
/// A sequence of bytes.
|
||||
Hex(Vec<u8>),
|
||||
/// A floating point number, that's neither NaN nor infinite!
|
||||
Float(f64),
|
||||
}
|
||||
|
||||
impl fmt::Display for Value {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match &self {
|
||||
Self::String(value) => write!(f, "{}", quote(value)),
|
||||
Self::Hex(value) => write!(f, "0x{}", hex::encode_upper(value)),
|
||||
Self::Float(value) => write!(f, "{}", value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Value {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
if input.starts_with("0x") || input.starts_with("0X") {
|
||||
Ok(Self::Hex(hex::decode(
|
||||
input.trim_start_matches("0x").trim_start_matches("0X"),
|
||||
)?))
|
||||
} else {
|
||||
match input.parse() {
|
||||
Ok(value) => Ok(Self::Float(value)),
|
||||
Err(_) => Ok(Self::String(unquote(input))),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for Value {
|
||||
fn from(value: f64) -> Self { Self::Float(value) }
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for Value {
|
||||
fn from(value: Vec<u8>) -> Self { Self::Hex(value) }
|
||||
}
|
||||
|
||||
impl From<String> for Value {
|
||||
fn from(value: String) -> Self { Self::String(unquote(value)) }
|
||||
}
|
||||
|
||||
impl From<&str> for Value {
|
||||
fn from(value: &str) -> Self { Self::String(unquote(value)) }
|
||||
}
|
||||
|
||||
// impl<T: AsRef<[u8]>> From<T> for Value {
|
||||
// fn from(value: T) -> Self { Self::Hex(value.as_ref().into()) }
|
||||
// }
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(Value::Float(1.1).to_string(), "1.1".to_string());
|
||||
assert_eq!(
|
||||
Value::String("&str".to_string()).to_string(),
|
||||
"\"&str\"".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
Value::Hex(vec![1, 2, 3]).to_string(),
|
||||
"0x010203".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(Value::Float(1.1), "1.1".parse().unwrap());
|
||||
assert_eq!(
|
||||
Value::String("&str".to_string()),
|
||||
"\"&str\"".parse().unwrap()
|
||||
);
|
||||
assert_eq!(Value::Hex(vec![1, 2, 3]), "0x010203".parse().unwrap());
|
||||
assert_eq!(Value::Hex(vec![1, 2, 3]), "0X010203".parse().unwrap());
|
||||
assert!("0x010203Z".parse::<Value>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from() {
|
||||
assert_eq!(Value::from(1.0_f64), Value::Float(1.0));
|
||||
assert_eq!(Value::from("\"&str\""), Value::String("&str".to_string()));
|
||||
assert_eq!(
|
||||
Value::from("&str".to_string()),
|
||||
Value::String("&str".to_string())
|
||||
);
|
||||
assert_eq!(Value::from(vec![1, 2, 3]), Value::Hex(vec![1, 2, 3]));
|
||||
}
|
||||
}
|
12
src/utils.rs
12
src/utils.rs
|
@ -1,5 +1,16 @@
|
|||
use crate::Error;
|
||||
|
||||
macro_rules! required_version {
|
||||
( $( $tag:expr ),* ) => {
|
||||
::core::iter::empty()
|
||||
$(
|
||||
.chain(::core::iter::once($tag.required_version()))
|
||||
)*
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_from {
|
||||
( $($( $type:tt ),* => $target:path ),* ) => {
|
||||
use ::core::convert::From;
|
||||
|
@ -72,6 +83,7 @@ where
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_parse_yes_or_no() {
|
||||
|
|
371
tests/master_playlist.rs
Normal file
371
tests/master_playlist.rs
Normal file
|
@ -0,0 +1,371 @@
|
|||
use hls_m3u8::tags::{ExtXIFrameStreamInf, ExtXMedia, ExtXStreamInf};
|
||||
use hls_m3u8::types::MediaType;
|
||||
use hls_m3u8::MasterPlaylist;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_master_playlist() {
|
||||
// https://tools.ietf.org/html/rfc8216#section-8.4
|
||||
let master_playlist = "#EXTM3U\n\
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1000000\n\
|
||||
http://example.com/low.m3u8\n\
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=2560000,AVERAGE-BANDWIDTH=2000000\n\
|
||||
http://example.com/mid.m3u8\n\
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=7680000,AVERAGE-BANDWIDTH=6000000\n\
|
||||
http://example.com/hi.m3u8\n\
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n\
|
||||
http://example.com/audio-only.m3u8"
|
||||
.parse::<MasterPlaylist>()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
MasterPlaylist::builder()
|
||||
.stream_inf_tags(vec![
|
||||
ExtXStreamInf::builder()
|
||||
.bandwidth(1280000)
|
||||
.average_bandwidth(1000000)
|
||||
.uri("http://example.com/low.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
ExtXStreamInf::builder()
|
||||
.bandwidth(2560000)
|
||||
.average_bandwidth(2000000)
|
||||
.uri("http://example.com/mid.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
ExtXStreamInf::builder()
|
||||
.bandwidth(7680000)
|
||||
.average_bandwidth(6000000)
|
||||
.uri("http://example.com/hi.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
ExtXStreamInf::builder()
|
||||
.bandwidth(65000)
|
||||
.codecs("mp4a.40.5")
|
||||
.uri("http://example.com/audio-only.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
])
|
||||
.build()
|
||||
.unwrap(),
|
||||
master_playlist
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_master_playlist_with_i_frames() {
|
||||
// https://tools.ietf.org/html/rfc8216#section-8.5
|
||||
let master_playlist = "#EXTM3U\n\
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=1280000\n\
|
||||
low/audio-video.m3u8\n\
|
||||
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=86000,URI=\"low/iframe.m3u8\"\n\
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=2560000\n\
|
||||
mid/audio-video.m3u8\n\
|
||||
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=150000,URI=\"mid/iframe.m3u8\"\n\
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=7680000\n\
|
||||
hi/audio-video.m3u8\n\
|
||||
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=550000,URI=\"hi/iframe.m3u8\"\n\
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n\
|
||||
audio-only.m3u8"
|
||||
.parse::<MasterPlaylist>()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
MasterPlaylist::builder()
|
||||
.stream_inf_tags(vec![
|
||||
ExtXStreamInf::builder()
|
||||
.bandwidth(1280000)
|
||||
.uri("low/audio-video.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
ExtXStreamInf::builder()
|
||||
.bandwidth(2560000)
|
||||
.uri("mid/audio-video.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
ExtXStreamInf::builder()
|
||||
.bandwidth(7680000)
|
||||
.uri("hi/audio-video.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
ExtXStreamInf::builder()
|
||||
.bandwidth(65000)
|
||||
.codecs("mp4a.40.5")
|
||||
.uri("audio-only.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
])
|
||||
.i_frame_stream_inf_tags(vec![
|
||||
ExtXIFrameStreamInf::builder()
|
||||
.bandwidth(86000)
|
||||
.uri("low/iframe.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
ExtXIFrameStreamInf::builder()
|
||||
.bandwidth(150000)
|
||||
.uri("mid/iframe.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
ExtXIFrameStreamInf::builder()
|
||||
.bandwidth(550000)
|
||||
.uri("hi/iframe.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
])
|
||||
.build()
|
||||
.unwrap(),
|
||||
master_playlist
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_master_playlist_with_alternative_audio() {
|
||||
// https://tools.ietf.org/html/rfc8216#section-8.6
|
||||
// TODO: I think the CODECS=\"..." have to be replaced.
|
||||
let master_playlist = "#EXTM3U\n\
|
||||
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"English\", \
|
||||
DEFAULT=YES,AUTOSELECT=YES,LANGUAGE=\"en\", \
|
||||
URI=\"main/english-audio.m3u8\"\n\
|
||||
|
||||
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"Deutsch\", \
|
||||
DEFAULT=NO,AUTOSELECT=YES,LANGUAGE=\"de\", \
|
||||
URI=\"main/german-audio.m3u8\"\n\
|
||||
|
||||
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"Commentary\", \
|
||||
DEFAULT=NO,AUTOSELECT=NO,LANGUAGE=\"en\", \
|
||||
URI=\"commentary/audio-only.m3u8\"\n\
|
||||
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"...\",AUDIO=\"aac\"\n\
|
||||
low/video-only.m3u8\n\
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS=\"...\",AUDIO=\"aac\"\n\
|
||||
mid/video-only.m3u8\n\
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS=\"...\",AUDIO=\"aac\"\n\
|
||||
hi/video-only.m3u8\n\
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\",AUDIO=\"aac\"\n\
|
||||
main/english-audio.m3u8"
|
||||
.parse::<MasterPlaylist>()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
MasterPlaylist::builder()
|
||||
.media_tags(vec![
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("aac")
|
||||
.name("English")
|
||||
.is_default(true)
|
||||
.is_autoselect(true)
|
||||
.language("en")
|
||||
.uri("main/english-audio.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("aac")
|
||||
.name("Deutsch")
|
||||
.is_default(false)
|
||||
.is_autoselect(true)
|
||||
.language("de")
|
||||
.uri("main/german-audio.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("aac")
|
||||
.name("Commentary")
|
||||
.is_default(false)
|
||||
.is_autoselect(false)
|
||||
.language("en")
|
||||
.uri("commentary/audio-only.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
])
|
||||
.stream_inf_tags(vec![
|
||||
ExtXStreamInf::builder()
|
||||
.bandwidth(1280000)
|
||||
.codecs("...")
|
||||
.audio("aac")
|
||||
.uri("low/video-only.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
ExtXStreamInf::builder()
|
||||
.bandwidth(2560000)
|
||||
.codecs("...")
|
||||
.audio("aac")
|
||||
.uri("mid/video-only.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
ExtXStreamInf::builder()
|
||||
.bandwidth(7680000)
|
||||
.codecs("...")
|
||||
.audio("aac")
|
||||
.uri("hi/video-only.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
ExtXStreamInf::builder()
|
||||
.bandwidth(65000)
|
||||
.codecs("mp4a.40.5")
|
||||
.audio("aac")
|
||||
.uri("main/english-audio.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
])
|
||||
.build()
|
||||
.unwrap(),
|
||||
master_playlist
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_master_playlist_with_alternative_video() {
|
||||
// https://tools.ietf.org/html/rfc8216#section-8.7
|
||||
let master_playlist = "#EXTM3U\n\
|
||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"low\",NAME=\"Main\", \
|
||||
AUTOSELECT=YES,DEFAULT=YES,URI=\"low/main/audio-video.m3u8\"\n\
|
||||
|
||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"low\",NAME=\"Centerfield\", \
|
||||
DEFAULT=NO,URI=\"low/centerfield/audio-video.m3u8\"\n\
|
||||
|
||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"low\",NAME=\"Dugout\", \
|
||||
DEFAULT=NO,URI=\"low/dugout/audio-video.m3u8\"\n\
|
||||
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"...\",VIDEO=\"low\"\n\
|
||||
low/main/audio-video.m3u8\n\
|
||||
|
||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"mid\",NAME=\"Main\", \
|
||||
AUTOSELECT=YES,DEFAULT=YES,URI=\"mid/main/audio-video.m3u8\"\n\
|
||||
|
||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"mid\",NAME=\"Centerfield\", \
|
||||
DEFAULT=NO,URI=\"mid/centerfield/audio-video.m3u8\"\n\
|
||||
|
||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"mid\",NAME=\"Dugout\", \
|
||||
DEFAULT=NO,URI=\"mid/dugout/audio-video.m3u8\"\n\
|
||||
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS=\"...\",VIDEO=\"mid\"\n\
|
||||
mid/main/audio-video.m3u8\n\
|
||||
|
||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"hi\",NAME=\"Main\", \
|
||||
AUTOSELECT=YES,DEFAULT=YES,URI=\"hi/main/audio-video.m3u8\"\n\
|
||||
|
||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"hi\",NAME=\"Centerfield\", \
|
||||
DEFAULT=NO,URI=\"hi/centerfield/audio-video.m3u8\"\n\
|
||||
|
||||
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"hi\",NAME=\"Dugout\", \
|
||||
DEFAULT=NO,URI=\"hi/dugout/audio-video.m3u8\"\n\
|
||||
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS=\"...\",VIDEO=\"hi\"
|
||||
hi/main/audio-video.m3u8"
|
||||
.parse::<MasterPlaylist>()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
MasterPlaylist::builder()
|
||||
.media_tags(vec![
|
||||
// low
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Video)
|
||||
.group_id("low")
|
||||
.name("Main")
|
||||
.is_default(true)
|
||||
.is_autoselect(true)
|
||||
.uri("low/main/audio-video.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Video)
|
||||
.group_id("low")
|
||||
.name("Centerfield")
|
||||
.is_default(false)
|
||||
.uri("low/centerfield/audio-video.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Video)
|
||||
.group_id("low")
|
||||
.name("Dugout")
|
||||
.is_default(false)
|
||||
.uri("low/dugout/audio-video.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
// mid
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Video)
|
||||
.group_id("mid")
|
||||
.name("Main")
|
||||
.is_default(true)
|
||||
.is_autoselect(true)
|
||||
.uri("mid/main/audio-video.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Video)
|
||||
.group_id("mid")
|
||||
.name("Centerfield")
|
||||
.is_default(false)
|
||||
.uri("mid/centerfield/audio-video.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Video)
|
||||
.group_id("mid")
|
||||
.name("Dugout")
|
||||
.is_default(false)
|
||||
.uri("mid/dugout/audio-video.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
// hi
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Video)
|
||||
.group_id("hi")
|
||||
.name("Main")
|
||||
.is_default(true)
|
||||
.is_autoselect(true)
|
||||
.uri("hi/main/audio-video.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Video)
|
||||
.group_id("hi")
|
||||
.name("Centerfield")
|
||||
.is_default(false)
|
||||
.uri("hi/centerfield/audio-video.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Video)
|
||||
.group_id("hi")
|
||||
.name("Dugout")
|
||||
.is_default(false)
|
||||
.uri("hi/dugout/audio-video.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
])
|
||||
.stream_inf_tags(vec![
|
||||
ExtXStreamInf::builder()
|
||||
.bandwidth(1280000)
|
||||
.codecs("...")
|
||||
.video("low")
|
||||
.uri("low/main/audio-video.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
ExtXStreamInf::builder()
|
||||
.bandwidth(2560000)
|
||||
.codecs("...")
|
||||
.video("mid")
|
||||
.uri("mid/main/audio-video.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
ExtXStreamInf::builder()
|
||||
.bandwidth(7680000)
|
||||
.codecs("...")
|
||||
.video("hi")
|
||||
.uri("hi/main/audio-video.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
])
|
||||
.build()
|
||||
.unwrap(),
|
||||
master_playlist
|
||||
);
|
||||
}
|
53
tests/media_playlist.rs
Normal file
53
tests/media_playlist.rs
Normal file
|
@ -0,0 +1,53 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use hls_m3u8::tags::{ExtInf, ExtXByteRange, ExtXMediaSequence, ExtXTargetDuration};
|
||||
use hls_m3u8::{MediaPlaylist, MediaSegment};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_media_playlist_with_byterange() {
|
||||
let media_playlist = "#EXTM3U\n\
|
||||
#EXT-X-TARGETDURATION:10\n\
|
||||
#EXT-X-VERSION:4\n\
|
||||
#EXT-X-MEDIA-SEQUENCE:0\n\
|
||||
#EXTINF:10.0,\n\
|
||||
#EXT-X-BYTERANGE:75232@0\n\
|
||||
video.ts\n\
|
||||
#EXT-X-BYTERANGE:82112@752321\n\
|
||||
#EXTINF:10.0,\n\
|
||||
video.ts\n\
|
||||
#EXTINF:10.0,\n\
|
||||
#EXT-X-BYTERANGE:69864\n\
|
||||
video.ts"
|
||||
.parse::<MediaPlaylist>()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
MediaPlaylist::builder()
|
||||
.target_duration_tag(ExtXTargetDuration::new(Duration::from_secs(10)))
|
||||
.media_sequence_tag(ExtXMediaSequence::new(0))
|
||||
.segments(vec![
|
||||
MediaSegment::builder()
|
||||
.inf_tag(ExtInf::new(Duration::from_secs_f64(10.0)))
|
||||
.byte_range_tag(ExtXByteRange::new(75232, Some(0)))
|
||||
.uri("video.ts")
|
||||
.build()
|
||||
.unwrap(),
|
||||
MediaSegment::builder()
|
||||
.inf_tag(ExtInf::new(Duration::from_secs_f64(10.0)))
|
||||
.byte_range_tag(ExtXByteRange::new(82112, Some(752321)))
|
||||
.uri("video.ts")
|
||||
.build()
|
||||
.unwrap(),
|
||||
MediaSegment::builder()
|
||||
.inf_tag(ExtInf::new(Duration::from_secs_f64(10.0)))
|
||||
.byte_range_tag(ExtXByteRange::new(69864, None))
|
||||
.uri("video.ts")
|
||||
.build()
|
||||
.unwrap(),
|
||||
])
|
||||
.build()
|
||||
.unwrap(),
|
||||
media_playlist
|
||||
)
|
||||
}
|
Loading…
Reference in a new issue