mirror of
https://github.com/sile/hls_m3u8.git
synced 2024-11-22 07:10:59 +00:00
commit
fb44c8803d
52 changed files with 3356 additions and 2155 deletions
|
@ -9,13 +9,18 @@ readme = "README.md"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
keywords = ["hls", "m3u8"]
|
keywords = ["hls", "m3u8"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
categories = ["parser"]
|
||||||
|
|
||||||
[badges]
|
[badges]
|
||||||
travis-ci = {repository = "sile/hls_m3u8"}
|
travis-ci = {repository = "sile/hls_m3u8"}
|
||||||
codecov = {repository = "sile/hls_m3u8"}
|
codecov = {repository = "sile/hls_m3u8"}
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
trackable = "0.2"
|
getset = "0.0.8"
|
||||||
|
failure = "0.1.5"
|
||||||
|
derive_builder = "0.7.2"
|
||||||
|
url = "2.1.0"
|
||||||
|
chrono = "0.4.9"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
clap = "2"
|
clap = "2"
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
extern crate clap;
|
extern crate clap;
|
||||||
extern crate hls_m3u8;
|
extern crate hls_m3u8;
|
||||||
#[macro_use]
|
|
||||||
extern crate trackable;
|
|
||||||
|
|
||||||
use clap::{App, Arg};
|
use clap::{App, Arg};
|
||||||
use hls_m3u8::{MasterPlaylist, MediaPlaylist};
|
use hls_m3u8::{MasterPlaylist, MediaPlaylist};
|
||||||
use std::io::{self, Read};
|
use std::io::{self, Read};
|
||||||
use trackable::error::Failure;
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let matches = App::new("parse")
|
let matches = App::new("parse")
|
||||||
|
@ -19,17 +16,15 @@ fn main() {
|
||||||
)
|
)
|
||||||
.get_matches();
|
.get_matches();
|
||||||
let mut m3u8 = String::new();
|
let mut m3u8 = String::new();
|
||||||
track_try_unwrap!(io::stdin()
|
io::stdin().read_to_string(&mut m3u8).unwrap();
|
||||||
.read_to_string(&mut m3u8)
|
|
||||||
.map_err(Failure::from_error));
|
|
||||||
|
|
||||||
match matches.value_of("M3U8_TYPE").unwrap() {
|
match matches.value_of("M3U8_TYPE").unwrap() {
|
||||||
"media" => {
|
"media" => {
|
||||||
let playlist: MediaPlaylist = track_try_unwrap!(m3u8.parse());
|
let playlist: MediaPlaylist = m3u8.parse().unwrap();
|
||||||
println!("{}", playlist);
|
println!("{}", playlist);
|
||||||
}
|
}
|
||||||
"master" => {
|
"master" => {
|
||||||
let playlist: MasterPlaylist = track_try_unwrap!(m3u8.parse());
|
let playlist: MasterPlaylist = m3u8.parse().unwrap();
|
||||||
println!("{}", playlist);
|
println!("{}", playlist);
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
|
|
207
src/attribute.rs
207
src/attribute.rs
|
@ -1,103 +1,138 @@
|
||||||
use crate::{ErrorKind, Result};
|
use std::collections::HashMap;
|
||||||
use std::collections::HashSet;
|
use std::ops::{Deref, DerefMut};
|
||||||
use std::str;
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[derive(Debug)]
|
use crate::Error;
|
||||||
pub struct AttributePairs<'a> {
|
|
||||||
input: &'a str,
|
|
||||||
visited_keys: HashSet<&'a str>,
|
|
||||||
}
|
|
||||||
impl<'a> AttributePairs<'a> {
|
|
||||||
pub fn parse(input: &'a str) -> Self {
|
|
||||||
AttributePairs {
|
|
||||||
input,
|
|
||||||
visited_keys: HashSet::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_name(&mut self) -> Result<&'a str> {
|
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||||
for i in 0..self.input.len() {
|
pub struct AttributePairs(HashMap<String, String>);
|
||||||
match self.input.as_bytes()[i] {
|
|
||||||
b'=' => {
|
|
||||||
let (key, _) = self.input.split_at(i);
|
|
||||||
let (_, rest) = self.input.split_at(i + 1);
|
|
||||||
self.input = rest;
|
|
||||||
return Ok(key);
|
|
||||||
}
|
|
||||||
b'A'..=b'Z' | b'0'..=b'9' | b'-' => {}
|
|
||||||
_ => track_panic!(
|
|
||||||
ErrorKind::InvalidInput,
|
|
||||||
"Malformed attribute name: {:?}",
|
|
||||||
self.input
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
track_panic!(
|
|
||||||
ErrorKind::InvalidInput,
|
|
||||||
"No attribute value: {:?}",
|
|
||||||
self.input
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_raw_value(&mut self) -> &'a str {
|
impl AttributePairs {
|
||||||
let mut in_quote = false;
|
pub fn new() -> Self {
|
||||||
let mut value_end = self.input.len();
|
Self::default()
|
||||||
let mut next = self.input.len();
|
|
||||||
for (i, c) in self.input.bytes().enumerate() {
|
|
||||||
match c {
|
|
||||||
b'"' => {
|
|
||||||
in_quote = !in_quote;
|
|
||||||
}
|
|
||||||
b',' if !in_quote => {
|
|
||||||
value_end = i;
|
|
||||||
next = i + 1;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let (value, _) = self.input.split_at(value_end);
|
|
||||||
let (_, rest) = self.input.split_at(next);
|
|
||||||
self.input = rest;
|
|
||||||
value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl<'a> Iterator for AttributePairs<'a> {
|
|
||||||
type Item = Result<(&'a str, &'a str)>;
|
impl Deref for AttributePairs {
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
type Target = HashMap<String, String>;
|
||||||
if self.input.is_empty() {
|
|
||||||
return None;
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DerefMut for AttributePairs {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoIterator for AttributePairs {
|
||||||
|
type Item = (String, String);
|
||||||
|
type IntoIter = ::std::collections::hash_map::IntoIter<String, String>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.0.into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> IntoIterator for &'a AttributePairs {
|
||||||
|
type Item = (&'a String, &'a String);
|
||||||
|
type IntoIter = ::std::collections::hash_map::Iter<'a, String, String>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.0.iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for AttributePairs {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
|
let mut result = AttributePairs::new();
|
||||||
|
|
||||||
|
for line in split(input, ',') {
|
||||||
|
let pair = split(line.trim(), '=');
|
||||||
|
|
||||||
|
if pair.len() < 2 {
|
||||||
|
return Err(Error::invalid_input());
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = pair[0].to_uppercase();
|
||||||
|
let value = pair[1].to_string();
|
||||||
|
|
||||||
|
result.insert(key.to_string(), value.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = || -> Result<(&'a str, &'a str)> {
|
dbg!(&result);
|
||||||
let key = track!(self.parse_name())?;
|
Ok(result)
|
||||||
track_assert!(
|
|
||||||
self.visited_keys.insert(key),
|
|
||||||
ErrorKind::InvalidInput,
|
|
||||||
"Duplicate attribute key: {:?}",
|
|
||||||
key
|
|
||||||
);
|
|
||||||
|
|
||||||
let value = self.parse_raw_value();
|
|
||||||
Ok((key, value))
|
|
||||||
}();
|
|
||||||
Some(result)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn split(value: &str, terminator: char) -> Vec<String> {
|
||||||
|
let mut result = vec![];
|
||||||
|
|
||||||
|
let mut inside_quotes = false;
|
||||||
|
let mut temp_string = String::new();
|
||||||
|
|
||||||
|
for c in value.chars() {
|
||||||
|
match c {
|
||||||
|
'"' => {
|
||||||
|
if inside_quotes {
|
||||||
|
inside_quotes = false;
|
||||||
|
} else {
|
||||||
|
inside_quotes = true;
|
||||||
|
}
|
||||||
|
temp_string.push(c);
|
||||||
|
}
|
||||||
|
k if (k == terminator) => {
|
||||||
|
if !inside_quotes {
|
||||||
|
result.push(temp_string);
|
||||||
|
temp_string = String::new();
|
||||||
|
} else {
|
||||||
|
temp_string.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
temp_string.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.push(temp_string);
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_works() {
|
fn test_parser() {
|
||||||
let mut pairs = AttributePairs::parse("FOO=BAR,BAR=\"baz,qux\",ABC=12.3");
|
let pairs = ("FOO=BAR,BAR=\"baz,qux\",ABC=12.3")
|
||||||
assert_eq!(pairs.next().map(|x| x.ok()), Some(Some(("FOO", "BAR"))));
|
.parse::<AttributePairs>()
|
||||||
assert_eq!(
|
.unwrap();
|
||||||
pairs.next().map(|x| x.ok()),
|
|
||||||
Some(Some(("BAR", "\"baz,qux\"")))
|
let mut iterator = pairs.iter();
|
||||||
);
|
assert!(iterator.any(|(k, v)| k == "FOO" && "BAR" == v));
|
||||||
assert_eq!(pairs.next().map(|x| x.ok()), Some(Some(("ABC", "12.3"))));
|
|
||||||
assert_eq!(pairs.next().map(|x| x.ok()), None)
|
let mut iterator = pairs.iter();
|
||||||
|
assert!(iterator.any(|(k, v)| k == "BAR" && v == "\"baz,qux\""));
|
||||||
|
|
||||||
|
let mut iterator = pairs.iter();
|
||||||
|
assert!(iterator.any(|(k, v)| k == "ABC" && v == "12.3"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_iterator() {
|
||||||
|
let mut attrs = AttributePairs::new();
|
||||||
|
attrs.insert("key_01".to_string(), "value_01".to_string());
|
||||||
|
attrs.insert("key_02".to_string(), "value_02".to_string());
|
||||||
|
|
||||||
|
let mut iterator = attrs.iter();
|
||||||
|
assert!(iterator.any(|(k, v)| k == "key_01" && v == "value_01"));
|
||||||
|
|
||||||
|
let mut iterator = attrs.iter();
|
||||||
|
assert!(iterator.any(|(k, v)| k == "key_02" && v == "value_02"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
227
src/error.rs
227
src/error.rs
|
@ -1,13 +1,224 @@
|
||||||
use trackable::error::{ErrorKind as TrackableErrorKind, TrackableError};
|
use std::error;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
/// This crate specific `Error` type.
|
use failure::{Backtrace, Context, Fail};
|
||||||
#[derive(Debug, Clone, TrackableError)]
|
|
||||||
pub struct Error(TrackableError<ErrorKind>);
|
|
||||||
|
|
||||||
/// Possible error kinds.
|
/// This crate specific `Result` type.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
#[allow(missing_docs)]
|
|
||||||
|
/// The ErrorKind.
|
||||||
|
#[derive(Debug, Fail, Clone, PartialEq, Eq)]
|
||||||
pub enum ErrorKind {
|
pub enum ErrorKind {
|
||||||
|
#[fail(display = "ChronoParseError: {}", _0)]
|
||||||
|
/// An error from the [Chrono](chrono) crate.
|
||||||
|
ChronoParseError(String),
|
||||||
|
#[fail(display = "UrlParseError: {}", _0)]
|
||||||
|
/// An error from the [Url](url) crate.
|
||||||
|
UrlParseError(String),
|
||||||
|
#[fail(display = "UnknownError: {}", _0)]
|
||||||
|
/// An unknown error occured.
|
||||||
|
UnknownError(String),
|
||||||
|
|
||||||
|
#[fail(display = "A value is missing for the attribute {}", _0)]
|
||||||
|
/// A required value is missing.
|
||||||
|
MissingValue(String),
|
||||||
|
|
||||||
|
#[fail(display = "Invalid Input")]
|
||||||
|
/// Error for anything.
|
||||||
InvalidInput,
|
InvalidInput,
|
||||||
|
|
||||||
|
#[fail(display = "ParseIntError: {}", _0)]
|
||||||
|
/// Failed to parse a String to int.
|
||||||
|
ParseIntError(String),
|
||||||
|
|
||||||
|
#[fail(display = "ParseFloatError: {}", _0)]
|
||||||
|
/// Failed to parse a String to float.
|
||||||
|
ParseFloatError(String),
|
||||||
|
|
||||||
|
#[fail(display = "MissingTag: Expected {} at the start of {:?}", tag, input)]
|
||||||
|
/// A tag is missing, that is required at the start of the input.
|
||||||
|
MissingTag {
|
||||||
|
/// The required tag.
|
||||||
|
tag: String,
|
||||||
|
/// The unparsed input data.
|
||||||
|
input: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[fail(display = "CustomError: {}", _0)]
|
||||||
|
/// A custom error.
|
||||||
|
Custom(String),
|
||||||
|
|
||||||
|
#[fail(display = "Unmatched Group: {:?}", _0)]
|
||||||
|
/// Unmatched Group
|
||||||
|
UnmatchedGroup(String),
|
||||||
|
|
||||||
|
#[fail(display = "Unknown Protocol version: {:?}", _0)]
|
||||||
|
/// Unknown m3u8 version. This library supports up to ProtocolVersion 7.
|
||||||
|
UnknownProtocolVersion(String),
|
||||||
|
|
||||||
|
#[fail(display = "IoError: {}", _0)]
|
||||||
|
/// Some io error
|
||||||
|
Io(String),
|
||||||
|
|
||||||
|
#[fail(
|
||||||
|
display = "VersionError: required_version: {:?}, specified_version: {:?}",
|
||||||
|
_0, _1
|
||||||
|
)]
|
||||||
|
/// This error occurs, if there is a ProtocolVersion mismatch.
|
||||||
|
VersionError(String, String),
|
||||||
|
|
||||||
|
#[fail(display = "BuilderError: {}", _0)]
|
||||||
|
/// An Error from a Builder.
|
||||||
|
BuilderError(String),
|
||||||
|
|
||||||
|
/// Hints that destructuring should not be exhaustive.
|
||||||
|
///
|
||||||
|
/// This enum may grow additional variants, so this makes sure clients
|
||||||
|
/// don't count on exhaustive matching. (Otherwise, adding a new variant
|
||||||
|
/// could break existing code.)
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[fail(display = "Invalid error")]
|
||||||
|
__Nonexhaustive,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
/// The Error type of this library.
|
||||||
|
pub struct Error {
|
||||||
|
inner: Context<ErrorKind>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fail for Error {
|
||||||
|
fn cause(&self) -> Option<&dyn Fail> {
|
||||||
|
self.inner.cause()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn backtrace(&self) -> Option<&Backtrace> {
|
||||||
|
self.inner.backtrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
self.inner.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ErrorKind> for Error {
|
||||||
|
fn from(kind: ErrorKind) -> Error {
|
||||||
|
Error::from(Context::new(kind))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Context<ErrorKind>> for Error {
|
||||||
|
fn from(inner: Context<ErrorKind>) -> Error {
|
||||||
|
Error { inner }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error {
|
||||||
|
pub(crate) fn unknown<T>(value: T) -> Self
|
||||||
|
where
|
||||||
|
T: error::Error,
|
||||||
|
{
|
||||||
|
Self::from(ErrorKind::UnknownError(value.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn missing_value<T: ToString>(value: T) -> Self {
|
||||||
|
Self::from(ErrorKind::MissingValue(value.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn invalid_input() -> Self {
|
||||||
|
Self::from(ErrorKind::InvalidInput)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parse_int_error<T: ToString>(value: T) -> Self {
|
||||||
|
Self::from(ErrorKind::ParseIntError(value.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn parse_float_error<T: ToString>(value: T) -> Self {
|
||||||
|
Self::from(ErrorKind::ParseFloatError(value.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn missing_tag<T, U>(tag: T, input: U) -> Self
|
||||||
|
where
|
||||||
|
T: ToString,
|
||||||
|
U: ToString,
|
||||||
|
{
|
||||||
|
Self::from(ErrorKind::MissingTag {
|
||||||
|
tag: tag.to_string(),
|
||||||
|
input: input.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn unmatched_group<T: ToString>(value: T) -> Self {
|
||||||
|
Self::from(ErrorKind::UnmatchedGroup(value.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn custom<T>(value: T) -> Self
|
||||||
|
where
|
||||||
|
T: fmt::Display,
|
||||||
|
{
|
||||||
|
Self::from(ErrorKind::Custom(value.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn unknown_protocol_version<T: ToString>(value: T) -> Self {
|
||||||
|
Self::from(ErrorKind::UnknownProtocolVersion(value.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
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()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn url<T: ToString>(value: T) -> Self {
|
||||||
|
Self::from(ErrorKind::UrlParseError(value.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn chrono<T: ToString>(value: T) -> Self {
|
||||||
|
Self::from(ErrorKind::ChronoParseError(value.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<::std::num::ParseIntError> for Error {
|
||||||
|
fn from(value: ::std::num::ParseIntError) -> Self {
|
||||||
|
Error::parse_int_error(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<::std::num::ParseFloatError> for Error {
|
||||||
|
fn from(value: ::std::num::ParseFloatError) -> Self {
|
||||||
|
Error::parse_float_error(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<::std::io::Error> for Error {
|
||||||
|
fn from(value: ::std::io::Error) -> Self {
|
||||||
|
Error::io(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<::url::ParseError> for Error {
|
||||||
|
fn from(value: ::url::ParseError) -> Self {
|
||||||
|
Error::url(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<::chrono::ParseError> for Error {
|
||||||
|
fn from(value: ::chrono::ParseError) -> Self {
|
||||||
|
Error::chrono(value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
impl TrackableErrorKind for ErrorKind {}
|
|
||||||
|
|
14
src/lib.rs
14
src/lib.rs
|
@ -1,3 +1,9 @@
|
||||||
|
#![warn(
|
||||||
|
//clippy::pedantic,
|
||||||
|
clippy::nursery,
|
||||||
|
clippy::cargo
|
||||||
|
)]
|
||||||
|
#![warn(missing_docs)]
|
||||||
//! [HLS] m3u8 parser/generator.
|
//! [HLS] m3u8 parser/generator.
|
||||||
//!
|
//!
|
||||||
//! [HLS]: https://tools.ietf.org/html/rfc8216
|
//! [HLS]: https://tools.ietf.org/html/rfc8216
|
||||||
|
@ -20,13 +26,10 @@
|
||||||
//!
|
//!
|
||||||
//! assert!(m3u8.parse::<MediaPlaylist>().is_ok());
|
//! assert!(m3u8.parse::<MediaPlaylist>().is_ok());
|
||||||
//! ```
|
//! ```
|
||||||
#![warn(missing_docs)]
|
|
||||||
#[macro_use]
|
|
||||||
extern crate trackable;
|
|
||||||
|
|
||||||
pub use error::{Error, ErrorKind};
|
pub use error::{Error, ErrorKind};
|
||||||
pub use master_playlist::{MasterPlaylist, MasterPlaylistBuilder};
|
pub use master_playlist::{MasterPlaylist, MasterPlaylistBuilder};
|
||||||
pub use media_playlist::{MediaPlaylist, MediaPlaylistBuilder, MediaPlaylistOptions};
|
pub use media_playlist::{MediaPlaylist, MediaPlaylistBuilder};
|
||||||
pub use media_segment::{MediaSegment, MediaSegmentBuilder};
|
pub use media_segment::{MediaSegment, MediaSegmentBuilder};
|
||||||
|
|
||||||
pub mod tags;
|
pub mod tags;
|
||||||
|
@ -40,5 +43,4 @@ mod media_playlist;
|
||||||
mod media_segment;
|
mod media_segment;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
/// This crate specific `Result` type.
|
pub use error::Result;
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
|
||||||
|
|
253
src/line.rs
253
src/line.rs
|
@ -1,81 +1,99 @@
|
||||||
use crate::tags;
|
|
||||||
use crate::types::SingleLineString;
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::ops::{Deref, DerefMut};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[derive(Debug)]
|
use url::Url;
|
||||||
pub struct Lines<'a> {
|
|
||||||
input: &'a str,
|
|
||||||
}
|
|
||||||
impl<'a> Lines<'a> {
|
|
||||||
pub fn new(input: &'a str) -> Self {
|
|
||||||
Lines { input }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_line(&mut self) -> Result<Line<'a>> {
|
use crate::tags;
|
||||||
let mut end = self.input.len();
|
use crate::Error;
|
||||||
let mut next_start = self.input.len();
|
|
||||||
let mut adjust = 0;
|
#[derive(Debug, Default)]
|
||||||
let mut next_line_of_ext_x_stream_inf = false;
|
pub struct Lines(Vec<Line>);
|
||||||
for (i, c) in self.input.char_indices() {
|
|
||||||
match c {
|
impl Lines {
|
||||||
'\n' => {
|
pub fn new() -> Self {
|
||||||
if !next_line_of_ext_x_stream_inf
|
Self::default()
|
||||||
&& self.input.starts_with(tags::ExtXStreamInf::PREFIX)
|
}
|
||||||
{
|
}
|
||||||
next_line_of_ext_x_stream_inf = true;
|
|
||||||
adjust = 0;
|
impl FromStr for Lines {
|
||||||
continue;
|
type Err = Error;
|
||||||
}
|
|
||||||
next_start = i + 1;
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
end = i - adjust;
|
let mut result = Lines::new();
|
||||||
break;
|
|
||||||
}
|
let mut stream_inf = false;
|
||||||
'\r' => {
|
let mut stream_inf_line = None;
|
||||||
adjust = 1;
|
|
||||||
}
|
for l in input.lines() {
|
||||||
_ => {
|
let line = l.trim();
|
||||||
track_assert!(!c.is_control(), ErrorKind::InvalidInput);
|
|
||||||
adjust = 0;
|
// ignore empty lines
|
||||||
}
|
if line.len() == 0 {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pline = {
|
||||||
|
if line.starts_with(tags::ExtXStreamInf::PREFIX) {
|
||||||
|
stream_inf = true;
|
||||||
|
stream_inf_line = Some(line);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
} else if line.starts_with("#EXT") {
|
||||||
|
Line::Tag(line.parse()?)
|
||||||
|
} else if line.starts_with("#") {
|
||||||
|
continue; // ignore comments
|
||||||
|
} else {
|
||||||
|
// stream inf line needs special treatment
|
||||||
|
if stream_inf {
|
||||||
|
stream_inf = false;
|
||||||
|
if let Some(first_line) = stream_inf_line {
|
||||||
|
let res = Line::Tag(format!("{}\n{}", first_line, line).parse()?);
|
||||||
|
stream_inf_line = None;
|
||||||
|
res
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Line::Uri(line.trim().parse()?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
result.push(pline);
|
||||||
}
|
}
|
||||||
let raw_line = &self.input[..end];
|
|
||||||
let line = if raw_line.is_empty() {
|
Ok(result)
|
||||||
Line::Blank
|
}
|
||||||
} else if raw_line.starts_with("#EXT") {
|
}
|
||||||
Line::Tag(track!(raw_line.parse())?)
|
|
||||||
} else if raw_line.starts_with('#') {
|
impl IntoIterator for Lines {
|
||||||
Line::Comment(raw_line)
|
type Item = Line;
|
||||||
} else {
|
type IntoIter = ::std::vec::IntoIter<Line>;
|
||||||
let uri = track!(SingleLineString::new(raw_line))?;
|
|
||||||
Line::Uri(uri)
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
};
|
self.0.into_iter()
|
||||||
self.input = &self.input[next_start..];
|
}
|
||||||
Ok(line)
|
}
|
||||||
}
|
|
||||||
}
|
impl Deref for Lines {
|
||||||
impl<'a> Iterator for Lines<'a> {
|
type Target = Vec<Line>;
|
||||||
type Item = Result<Line<'a>>;
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn deref(&self) -> &Self::Target {
|
||||||
if self.input.is_empty() {
|
&self.0
|
||||||
return None;
|
}
|
||||||
}
|
}
|
||||||
match track!(self.read_line()) {
|
|
||||||
Err(e) => Some(Err(e)),
|
impl DerefMut for Lines {
|
||||||
Ok(line) => Some(Ok(line)),
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
}
|
&mut self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum Line<'a> {
|
pub enum Line {
|
||||||
Blank,
|
|
||||||
Comment(&'a str),
|
|
||||||
Tag(Tag),
|
Tag(Tag),
|
||||||
Uri(SingleLineString),
|
Uri(Url),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
@ -103,86 +121,89 @@ pub enum Tag {
|
||||||
ExtXSessionKey(tags::ExtXSessionKey),
|
ExtXSessionKey(tags::ExtXSessionKey),
|
||||||
ExtXIndependentSegments(tags::ExtXIndependentSegments),
|
ExtXIndependentSegments(tags::ExtXIndependentSegments),
|
||||||
ExtXStart(tags::ExtXStart),
|
ExtXStart(tags::ExtXStart),
|
||||||
Unknown(SingleLineString),
|
Unknown(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Tag {
|
impl fmt::Display for Tag {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
match *self {
|
match &self {
|
||||||
Tag::ExtM3u(ref t) => t.fmt(f),
|
Tag::ExtM3u(value) => value.fmt(f),
|
||||||
Tag::ExtXVersion(ref t) => t.fmt(f),
|
Tag::ExtXVersion(value) => value.fmt(f),
|
||||||
Tag::ExtInf(ref t) => t.fmt(f),
|
Tag::ExtInf(value) => value.fmt(f),
|
||||||
Tag::ExtXByteRange(ref t) => t.fmt(f),
|
Tag::ExtXByteRange(value) => value.fmt(f),
|
||||||
Tag::ExtXDiscontinuity(ref t) => t.fmt(f),
|
Tag::ExtXDiscontinuity(value) => value.fmt(f),
|
||||||
Tag::ExtXKey(ref t) => t.fmt(f),
|
Tag::ExtXKey(value) => value.fmt(f),
|
||||||
Tag::ExtXMap(ref t) => t.fmt(f),
|
Tag::ExtXMap(value) => value.fmt(f),
|
||||||
Tag::ExtXProgramDateTime(ref t) => t.fmt(f),
|
Tag::ExtXProgramDateTime(value) => value.fmt(f),
|
||||||
Tag::ExtXDateRange(ref t) => t.fmt(f),
|
Tag::ExtXDateRange(value) => value.fmt(f),
|
||||||
Tag::ExtXTargetDuration(ref t) => t.fmt(f),
|
Tag::ExtXTargetDuration(value) => value.fmt(f),
|
||||||
Tag::ExtXMediaSequence(ref t) => t.fmt(f),
|
Tag::ExtXMediaSequence(value) => value.fmt(f),
|
||||||
Tag::ExtXDiscontinuitySequence(ref t) => t.fmt(f),
|
Tag::ExtXDiscontinuitySequence(value) => value.fmt(f),
|
||||||
Tag::ExtXEndList(ref t) => t.fmt(f),
|
Tag::ExtXEndList(value) => value.fmt(f),
|
||||||
Tag::ExtXPlaylistType(ref t) => t.fmt(f),
|
Tag::ExtXPlaylistType(value) => value.fmt(f),
|
||||||
Tag::ExtXIFramesOnly(ref t) => t.fmt(f),
|
Tag::ExtXIFramesOnly(value) => value.fmt(f),
|
||||||
Tag::ExtXMedia(ref t) => t.fmt(f),
|
Tag::ExtXMedia(value) => value.fmt(f),
|
||||||
Tag::ExtXStreamInf(ref t) => t.fmt(f),
|
Tag::ExtXStreamInf(value) => value.fmt(f),
|
||||||
Tag::ExtXIFrameStreamInf(ref t) => t.fmt(f),
|
Tag::ExtXIFrameStreamInf(value) => value.fmt(f),
|
||||||
Tag::ExtXSessionData(ref t) => t.fmt(f),
|
Tag::ExtXSessionData(value) => value.fmt(f),
|
||||||
Tag::ExtXSessionKey(ref t) => t.fmt(f),
|
Tag::ExtXSessionKey(value) => value.fmt(f),
|
||||||
Tag::ExtXIndependentSegments(ref t) => t.fmt(f),
|
Tag::ExtXIndependentSegments(value) => value.fmt(f),
|
||||||
Tag::ExtXStart(ref t) => t.fmt(f),
|
Tag::ExtXStart(value) => value.fmt(f),
|
||||||
Tag::Unknown(ref t) => t.fmt(f),
|
Tag::Unknown(value) => value.fmt(f),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for Tag {
|
impl FromStr for Tag {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
if s.starts_with(tags::ExtM3u::PREFIX) {
|
if s.starts_with(tags::ExtM3u::PREFIX) {
|
||||||
track!(s.parse().map(Tag::ExtM3u))
|
s.parse().map(Tag::ExtM3u)
|
||||||
} else if s.starts_with(tags::ExtXVersion::PREFIX) {
|
} else if s.starts_with(tags::ExtXVersion::PREFIX) {
|
||||||
track!(s.parse().map(Tag::ExtXVersion))
|
s.parse().map(Tag::ExtXVersion)
|
||||||
} else if s.starts_with(tags::ExtInf::PREFIX) {
|
} else if s.starts_with(tags::ExtInf::PREFIX) {
|
||||||
track!(s.parse().map(Tag::ExtInf))
|
s.parse().map(Tag::ExtInf)
|
||||||
} else if s.starts_with(tags::ExtXByteRange::PREFIX) {
|
} else if s.starts_with(tags::ExtXByteRange::PREFIX) {
|
||||||
track!(s.parse().map(Tag::ExtXByteRange))
|
s.parse().map(Tag::ExtXByteRange)
|
||||||
} else if s.starts_with(tags::ExtXDiscontinuity::PREFIX) {
|
} else if s.starts_with(tags::ExtXDiscontinuity::PREFIX) {
|
||||||
track!(s.parse().map(Tag::ExtXDiscontinuity))
|
s.parse().map(Tag::ExtXDiscontinuity)
|
||||||
} else if s.starts_with(tags::ExtXKey::PREFIX) {
|
} else if s.starts_with(tags::ExtXKey::PREFIX) {
|
||||||
track!(s.parse().map(Tag::ExtXKey))
|
s.parse().map(Tag::ExtXKey)
|
||||||
} else if s.starts_with(tags::ExtXMap::PREFIX) {
|
} else if s.starts_with(tags::ExtXMap::PREFIX) {
|
||||||
track!(s.parse().map(Tag::ExtXMap))
|
s.parse().map(Tag::ExtXMap)
|
||||||
} else if s.starts_with(tags::ExtXProgramDateTime::PREFIX) {
|
} else if s.starts_with(tags::ExtXProgramDateTime::PREFIX) {
|
||||||
track!(s.parse().map(Tag::ExtXProgramDateTime))
|
s.parse().map(Tag::ExtXProgramDateTime)
|
||||||
} else if s.starts_with(tags::ExtXTargetDuration::PREFIX) {
|
} else if s.starts_with(tags::ExtXTargetDuration::PREFIX) {
|
||||||
track!(s.parse().map(Tag::ExtXTargetDuration))
|
s.parse().map(Tag::ExtXTargetDuration)
|
||||||
} else if s.starts_with(tags::ExtXDateRange::PREFIX) {
|
} else if s.starts_with(tags::ExtXDateRange::PREFIX) {
|
||||||
track!(s.parse().map(Tag::ExtXDateRange))
|
s.parse().map(Tag::ExtXDateRange)
|
||||||
} else if s.starts_with(tags::ExtXMediaSequence::PREFIX) {
|
} else if s.starts_with(tags::ExtXMediaSequence::PREFIX) {
|
||||||
track!(s.parse().map(Tag::ExtXMediaSequence))
|
s.parse().map(Tag::ExtXMediaSequence)
|
||||||
} else if s.starts_with(tags::ExtXDiscontinuitySequence::PREFIX) {
|
} else if s.starts_with(tags::ExtXDiscontinuitySequence::PREFIX) {
|
||||||
track!(s.parse().map(Tag::ExtXDiscontinuitySequence))
|
s.parse().map(Tag::ExtXDiscontinuitySequence)
|
||||||
} else if s.starts_with(tags::ExtXEndList::PREFIX) {
|
} else if s.starts_with(tags::ExtXEndList::PREFIX) {
|
||||||
track!(s.parse().map(Tag::ExtXEndList))
|
s.parse().map(Tag::ExtXEndList)
|
||||||
} else if s.starts_with(tags::ExtXPlaylistType::PREFIX) {
|
} else if s.starts_with(tags::ExtXPlaylistType::PREFIX) {
|
||||||
track!(s.parse().map(Tag::ExtXPlaylistType))
|
s.parse().map(Tag::ExtXPlaylistType)
|
||||||
} else if s.starts_with(tags::ExtXIFramesOnly::PREFIX) {
|
} else if s.starts_with(tags::ExtXIFramesOnly::PREFIX) {
|
||||||
track!(s.parse().map(Tag::ExtXIFramesOnly))
|
s.parse().map(Tag::ExtXIFramesOnly)
|
||||||
} else if s.starts_with(tags::ExtXMedia::PREFIX) {
|
} else if s.starts_with(tags::ExtXMedia::PREFIX) {
|
||||||
track!(s.parse().map(Tag::ExtXMedia))
|
s.parse().map(Tag::ExtXMedia)
|
||||||
} else if s.starts_with(tags::ExtXStreamInf::PREFIX) {
|
} else if s.starts_with(tags::ExtXStreamInf::PREFIX) {
|
||||||
track!(s.parse().map(Tag::ExtXStreamInf))
|
s.parse().map(Tag::ExtXStreamInf)
|
||||||
} else if s.starts_with(tags::ExtXIFrameStreamInf::PREFIX) {
|
} else if s.starts_with(tags::ExtXIFrameStreamInf::PREFIX) {
|
||||||
track!(s.parse().map(Tag::ExtXIFrameStreamInf))
|
s.parse().map(Tag::ExtXIFrameStreamInf)
|
||||||
} else if s.starts_with(tags::ExtXSessionData::PREFIX) {
|
} else if s.starts_with(tags::ExtXSessionData::PREFIX) {
|
||||||
track!(s.parse().map(Tag::ExtXSessionData))
|
s.parse().map(Tag::ExtXSessionData)
|
||||||
} else if s.starts_with(tags::ExtXSessionKey::PREFIX) {
|
} else if s.starts_with(tags::ExtXSessionKey::PREFIX) {
|
||||||
track!(s.parse().map(Tag::ExtXSessionKey))
|
s.parse().map(Tag::ExtXSessionKey)
|
||||||
} else if s.starts_with(tags::ExtXIndependentSegments::PREFIX) {
|
} else if s.starts_with(tags::ExtXIndependentSegments::PREFIX) {
|
||||||
track!(s.parse().map(Tag::ExtXIndependentSegments))
|
s.parse().map(Tag::ExtXIndependentSegments)
|
||||||
} else if s.starts_with(tags::ExtXStart::PREFIX) {
|
} else if s.starts_with(tags::ExtXStart::PREFIX) {
|
||||||
track!(s.parse().map(Tag::ExtXStart))
|
s.parse().map(Tag::ExtXStart)
|
||||||
} else {
|
} else {
|
||||||
track!(SingleLineString::new(s)).map(Tag::Unknown)
|
Ok(Tag::Unknown(s.to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,251 +1,68 @@
|
||||||
use crate::line::{Line, Lines, Tag};
|
|
||||||
use crate::tags::{
|
|
||||||
ExtM3u, ExtXIFrameStreamInf, ExtXIndependentSegments, ExtXMedia, ExtXSessionData,
|
|
||||||
ExtXSessionKey, ExtXStart, ExtXStreamInf, ExtXVersion, MasterPlaylistTag,
|
|
||||||
};
|
|
||||||
use crate::types::{ClosedCaptions, MediaType, ProtocolVersion, QuotedString};
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::iter;
|
use std::iter;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
/// Master playlist builder.
|
use derive_builder::Builder;
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct MasterPlaylistBuilder {
|
|
||||||
version: Option<ProtocolVersion>,
|
|
||||||
independent_segments_tag: Option<ExtXIndependentSegments>,
|
|
||||||
start_tag: Option<ExtXStart>,
|
|
||||||
media_tags: Vec<ExtXMedia>,
|
|
||||||
stream_inf_tags: Vec<ExtXStreamInf>,
|
|
||||||
i_frame_stream_inf_tags: Vec<ExtXIFrameStreamInf>,
|
|
||||||
session_data_tags: Vec<ExtXSessionData>,
|
|
||||||
session_key_tags: Vec<ExtXSessionKey>,
|
|
||||||
}
|
|
||||||
impl MasterPlaylistBuilder {
|
|
||||||
/// Makes a new `MasterPlaylistBuilder` instance.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
MasterPlaylistBuilder {
|
|
||||||
version: None,
|
|
||||||
independent_segments_tag: None,
|
|
||||||
start_tag: None,
|
|
||||||
media_tags: Vec::new(),
|
|
||||||
stream_inf_tags: Vec::new(),
|
|
||||||
i_frame_stream_inf_tags: Vec::new(),
|
|
||||||
session_data_tags: Vec::new(),
|
|
||||||
session_key_tags: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the protocol compatibility version of the resulting playlist.
|
use crate::line::{Line, Lines, Tag};
|
||||||
///
|
use crate::tags::{
|
||||||
/// If the resulting playlist has tags which requires a compatibility version greater than `version`,
|
ExtM3u, ExtXIFrameStreamInf, ExtXIndependentSegments, ExtXMedia, ExtXSessionData,
|
||||||
/// `finish()` method will fail with an `ErrorKind::InvalidInput` error.
|
ExtXSessionKey, ExtXStart, ExtXStreamInf, ExtXVersion,
|
||||||
///
|
};
|
||||||
/// The default is the maximum version among the tags in the playlist.
|
use crate::types::{ClosedCaptions, MediaType, ProtocolVersion};
|
||||||
pub fn version(&mut self, version: ProtocolVersion) -> &mut Self {
|
use crate::Error;
|
||||||
self.version = Some(version);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds the given tag to the resulting playlist.
|
|
||||||
///
|
|
||||||
/// If it is forbidden to have multiple instance of the tag, the existing one will be overwritten.
|
|
||||||
pub fn tag<T: Into<MasterPlaylistTag>>(&mut self, tag: T) -> &mut Self {
|
|
||||||
match tag.into() {
|
|
||||||
MasterPlaylistTag::ExtXIndependentSegments(t) => {
|
|
||||||
self.independent_segments_tag = Some(t);
|
|
||||||
}
|
|
||||||
MasterPlaylistTag::ExtXStart(t) => self.start_tag = Some(t),
|
|
||||||
MasterPlaylistTag::ExtXMedia(t) => self.media_tags.push(t),
|
|
||||||
MasterPlaylistTag::ExtXStreamInf(t) => self.stream_inf_tags.push(t),
|
|
||||||
MasterPlaylistTag::ExtXIFrameStreamInf(t) => self.i_frame_stream_inf_tags.push(t),
|
|
||||||
MasterPlaylistTag::ExtXSessionData(t) => self.session_data_tags.push(t),
|
|
||||||
MasterPlaylistTag::ExtXSessionKey(t) => self.session_key_tags.push(t),
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds a `MasterPlaylist` instance.
|
|
||||||
pub fn finish(self) -> Result<MasterPlaylist> {
|
|
||||||
let required_version = self.required_version();
|
|
||||||
let specified_version = self.version.unwrap_or(required_version);
|
|
||||||
track_assert!(
|
|
||||||
required_version <= specified_version,
|
|
||||||
ErrorKind::InvalidInput,
|
|
||||||
"required_version:{}, specified_version:{}",
|
|
||||||
required_version,
|
|
||||||
specified_version,
|
|
||||||
);
|
|
||||||
|
|
||||||
track!(self.validate_stream_inf_tags())?;
|
|
||||||
track!(self.validate_i_frame_stream_inf_tags())?;
|
|
||||||
track!(self.validate_session_data_tags())?;
|
|
||||||
track!(self.validate_session_key_tags())?;
|
|
||||||
|
|
||||||
Ok(MasterPlaylist {
|
|
||||||
version_tag: ExtXVersion::new(specified_version),
|
|
||||||
independent_segments_tag: self.independent_segments_tag,
|
|
||||||
start_tag: self.start_tag,
|
|
||||||
media_tags: self.media_tags,
|
|
||||||
stream_inf_tags: self.stream_inf_tags,
|
|
||||||
i_frame_stream_inf_tags: self.i_frame_stream_inf_tags,
|
|
||||||
session_data_tags: self.session_data_tags,
|
|
||||||
session_key_tags: self.session_key_tags,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn required_version(&self) -> ProtocolVersion {
|
|
||||||
iter::empty()
|
|
||||||
.chain(
|
|
||||||
self.independent_segments_tag
|
|
||||||
.iter()
|
|
||||||
.map(|t| t.requires_version()),
|
|
||||||
)
|
|
||||||
.chain(self.start_tag.iter().map(|t| t.requires_version()))
|
|
||||||
.chain(self.media_tags.iter().map(|t| t.requires_version()))
|
|
||||||
.chain(self.stream_inf_tags.iter().map(|t| t.requires_version()))
|
|
||||||
.chain(
|
|
||||||
self.i_frame_stream_inf_tags
|
|
||||||
.iter()
|
|
||||||
.map(|t| t.requires_version()),
|
|
||||||
)
|
|
||||||
.chain(self.session_data_tags.iter().map(|t| t.requires_version()))
|
|
||||||
.chain(self.session_key_tags.iter().map(|t| t.requires_version()))
|
|
||||||
.max()
|
|
||||||
.expect("Never fails")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_stream_inf_tags(&self) -> Result<()> {
|
|
||||||
let mut has_none_closed_captions = false;
|
|
||||||
for t in &self.stream_inf_tags {
|
|
||||||
if let Some(group_id) = t.audio() {
|
|
||||||
track_assert!(
|
|
||||||
self.check_media_group(MediaType::Audio, group_id),
|
|
||||||
ErrorKind::InvalidInput,
|
|
||||||
"Unmatched audio group: {:?}",
|
|
||||||
group_id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Some(group_id) = t.video() {
|
|
||||||
track_assert!(
|
|
||||||
self.check_media_group(MediaType::Video, group_id),
|
|
||||||
ErrorKind::InvalidInput,
|
|
||||||
"Unmatched video group: {:?}",
|
|
||||||
group_id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Some(group_id) = t.subtitles() {
|
|
||||||
track_assert!(
|
|
||||||
self.check_media_group(MediaType::Subtitles, group_id),
|
|
||||||
ErrorKind::InvalidInput,
|
|
||||||
"Unmatched subtitles group: {:?}",
|
|
||||||
group_id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
match t.closed_captions() {
|
|
||||||
Some(&ClosedCaptions::GroupId(ref group_id)) => {
|
|
||||||
track_assert!(
|
|
||||||
self.check_media_group(MediaType::ClosedCaptions, group_id),
|
|
||||||
ErrorKind::InvalidInput,
|
|
||||||
"Unmatched closed-captions group: {:?}",
|
|
||||||
group_id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Some(&ClosedCaptions::None) => {
|
|
||||||
has_none_closed_captions = true;
|
|
||||||
}
|
|
||||||
None => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if has_none_closed_captions {
|
|
||||||
track_assert!(
|
|
||||||
self.stream_inf_tags
|
|
||||||
.iter()
|
|
||||||
.all(|t| t.closed_captions() == Some(&ClosedCaptions::None)),
|
|
||||||
ErrorKind::InvalidInput
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_i_frame_stream_inf_tags(&self) -> Result<()> {
|
|
||||||
for t in &self.i_frame_stream_inf_tags {
|
|
||||||
if let Some(group_id) = t.video() {
|
|
||||||
track_assert!(
|
|
||||||
self.check_media_group(MediaType::Video, group_id),
|
|
||||||
ErrorKind::InvalidInput,
|
|
||||||
"Unmatched video group: {:?}",
|
|
||||||
group_id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_session_data_tags(&self) -> Result<()> {
|
|
||||||
let mut set = HashSet::new();
|
|
||||||
for t in &self.session_data_tags {
|
|
||||||
track_assert!(
|
|
||||||
set.insert((t.data_id(), t.language())),
|
|
||||||
ErrorKind::InvalidInput,
|
|
||||||
"Conflict: {}",
|
|
||||||
t
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_session_key_tags(&self) -> Result<()> {
|
|
||||||
let mut set = HashSet::new();
|
|
||||||
for t in &self.session_key_tags {
|
|
||||||
track_assert!(
|
|
||||||
set.insert(t.key()),
|
|
||||||
ErrorKind::InvalidInput,
|
|
||||||
"Conflict: {}",
|
|
||||||
t
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_media_group(&self, media_type: MediaType, group_id: &QuotedString) -> bool {
|
|
||||||
self.media_tags
|
|
||||||
.iter()
|
|
||||||
.any(|t| t.media_type() == media_type && t.group_id() == group_id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Default for MasterPlaylistBuilder {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Master playlist.
|
/// Master playlist.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Builder)]
|
||||||
|
#[builder(build_fn(validate = "Self::validate"))]
|
||||||
|
#[builder(setter(into, strip_option))]
|
||||||
pub struct MasterPlaylist {
|
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,
|
version_tag: ExtXVersion,
|
||||||
|
#[builder(default)]
|
||||||
|
/// Sets the [ExtXIndependentSegments] tag.
|
||||||
independent_segments_tag: Option<ExtXIndependentSegments>,
|
independent_segments_tag: Option<ExtXIndependentSegments>,
|
||||||
|
#[builder(default)]
|
||||||
|
/// Sets the [ExtXStart] tag.
|
||||||
start_tag: Option<ExtXStart>,
|
start_tag: Option<ExtXStart>,
|
||||||
|
/// Sets the [ExtXMedia] tag.
|
||||||
media_tags: Vec<ExtXMedia>,
|
media_tags: Vec<ExtXMedia>,
|
||||||
|
/// Sets all [ExtXStreamInf]s.
|
||||||
stream_inf_tags: Vec<ExtXStreamInf>,
|
stream_inf_tags: Vec<ExtXStreamInf>,
|
||||||
|
/// Sets all [ExtXIFrameStreamInf]s.
|
||||||
i_frame_stream_inf_tags: Vec<ExtXIFrameStreamInf>,
|
i_frame_stream_inf_tags: Vec<ExtXIFrameStreamInf>,
|
||||||
|
/// Sets all [ExtXSessionData]s.
|
||||||
session_data_tags: Vec<ExtXSessionData>,
|
session_data_tags: Vec<ExtXSessionData>,
|
||||||
|
/// Sets all [ExtXSessionKey]s.
|
||||||
session_key_tags: Vec<ExtXSessionKey>,
|
session_key_tags: Vec<ExtXSessionKey>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MasterPlaylist {
|
impl MasterPlaylist {
|
||||||
|
/// Returns a Builder for a MasterPlaylist.
|
||||||
|
pub fn builder() -> MasterPlaylistBuilder {
|
||||||
|
MasterPlaylistBuilder::default()
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the `EXT-X-VERSION` tag contained in the playlist.
|
/// Returns the `EXT-X-VERSION` tag contained in the playlist.
|
||||||
pub fn version_tag(&self) -> ExtXVersion {
|
pub const fn version_tag(&self) -> ExtXVersion {
|
||||||
self.version_tag
|
self.version_tag
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the `EXT-X-INDEPENDENT-SEGMENTS` tag contained in the playlist.
|
/// Returns the `EXT-X-INDEPENDENT-SEGMENTS` tag contained in the playlist.
|
||||||
pub fn independent_segments_tag(&self) -> Option<ExtXIndependentSegments> {
|
pub const fn independent_segments_tag(&self) -> Option<ExtXIndependentSegments> {
|
||||||
self.independent_segments_tag
|
self.independent_segments_tag
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the `EXT-X-START` tag contained in the playlist.
|
/// Returns the `EXT-X-START` tag contained in the playlist.
|
||||||
pub fn start_tag(&self) -> Option<ExtXStart> {
|
pub const fn start_tag(&self) -> Option<ExtXStart> {
|
||||||
self.start_tag
|
self.start_tag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -274,6 +91,156 @@ impl MasterPlaylist {
|
||||||
&self.session_key_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(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())?;
|
||||||
|
self.validate_session_data_tags()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn required_version(&self) -> ProtocolVersion {
|
||||||
|
iter::empty()
|
||||||
|
.chain(
|
||||||
|
self.independent_segments_tag
|
||||||
|
.iter()
|
||||||
|
.map(|t| t.iter().map(|t| t.requires_version()))
|
||||||
|
.flatten(),
|
||||||
|
)
|
||||||
|
.chain(
|
||||||
|
self.start_tag
|
||||||
|
.iter()
|
||||||
|
.map(|t| t.iter().map(|t| t.requires_version()))
|
||||||
|
.flatten(),
|
||||||
|
)
|
||||||
|
.chain(
|
||||||
|
self.media_tags
|
||||||
|
.iter()
|
||||||
|
.map(|t| t.iter().map(|t| t.requires_version()))
|
||||||
|
.flatten(),
|
||||||
|
)
|
||||||
|
.chain(
|
||||||
|
self.stream_inf_tags
|
||||||
|
.iter()
|
||||||
|
.map(|t| t.iter().map(|t| t.requires_version()))
|
||||||
|
.flatten(),
|
||||||
|
)
|
||||||
|
.chain(
|
||||||
|
self.i_frame_stream_inf_tags
|
||||||
|
.iter()
|
||||||
|
.map(|t| t.iter().map(|t| t.requires_version()))
|
||||||
|
.flatten(),
|
||||||
|
)
|
||||||
|
.chain(
|
||||||
|
self.session_data_tags
|
||||||
|
.iter()
|
||||||
|
.map(|t| t.iter().map(|t| t.requires_version()))
|
||||||
|
.flatten(),
|
||||||
|
)
|
||||||
|
.chain(
|
||||||
|
self.session_key_tags
|
||||||
|
.iter()
|
||||||
|
.map(|t| t.iter().map(|t| t.requires_version()))
|
||||||
|
.flatten(),
|
||||||
|
)
|
||||||
|
.max()
|
||||||
|
.unwrap_or(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;
|
||||||
|
|
||||||
|
for t in value {
|
||||||
|
if let Some(group_id) = t.audio() {
|
||||||
|
if !self.check_media_group(MediaType::Audio, group_id) {
|
||||||
|
return Err(Error::unmatched_group(group_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(group_id) = t.video() {
|
||||||
|
if !self.check_media_group(MediaType::Video, group_id) {
|
||||||
|
return Err(Error::unmatched_group(group_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(group_id) = t.subtitles() {
|
||||||
|
if !self.check_media_group(MediaType::Subtitles, group_id) {
|
||||||
|
return Err(Error::unmatched_group(group_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match t.closed_captions() {
|
||||||
|
Some(&ClosedCaptions::GroupId(ref group_id)) => {
|
||||||
|
if !self.check_media_group(MediaType::ClosedCaptions, group_id) {
|
||||||
|
return Err(Error::unmatched_group(group_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(&ClosedCaptions::None) => {
|
||||||
|
has_none_closed_captions = true;
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if has_none_closed_captions {
|
||||||
|
if !value
|
||||||
|
.iter()
|
||||||
|
.all(|t| t.closed_captions() == Some(&ClosedCaptions::None))
|
||||||
|
{
|
||||||
|
return Err(Error::invalid_input());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_i_frame_stream_inf_tags(&self) -> crate::Result<()> {
|
||||||
|
if let Some(value) = &self.i_frame_stream_inf_tags {
|
||||||
|
for t in value {
|
||||||
|
if let Some(group_id) = t.video() {
|
||||||
|
if !self.check_media_group(MediaType::Video, group_id) {
|
||||||
|
return Err(Error::unmatched_group(group_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_session_data_tags(&self) -> crate::Result<()> {
|
||||||
|
let mut set = HashSet::new();
|
||||||
|
if let Some(value) = &self.session_data_tags {
|
||||||
|
for t in value {
|
||||||
|
if !set.insert((t.data_id(), t.language())) {
|
||||||
|
return Err(Error::custom(format!("Conflict: {}", t)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_media_group<T: ToString>(&self, media_type: MediaType, group_id: T) -> bool {
|
||||||
|
if let Some(value) = &self.media_tags {
|
||||||
|
value
|
||||||
|
.iter()
|
||||||
|
.any(|t| t.media_type() == media_type && t.group_id() == &group_id.to_string())
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Display for MasterPlaylist {
|
impl fmt::Display for MasterPlaylist {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
writeln!(f, "{}", ExtM3u)?;
|
writeln!(f, "{}", ExtM3u)?;
|
||||||
|
@ -295,33 +262,42 @@ impl fmt::Display for MasterPlaylist {
|
||||||
for t in &self.session_key_tags {
|
for t in &self.session_key_tags {
|
||||||
writeln!(f, "{}", t)?;
|
writeln!(f, "{}", t)?;
|
||||||
}
|
}
|
||||||
if let Some(ref t) = self.independent_segments_tag {
|
if let Some(value) = &self.independent_segments_tag {
|
||||||
writeln!(f, "{}", t)?;
|
writeln!(f, "{}", value)?;
|
||||||
}
|
}
|
||||||
if let Some(ref t) = self.start_tag {
|
if let Some(value) = &self.start_tag {
|
||||||
writeln!(f, "{}", t)?;
|
writeln!(f, "{}", value)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for MasterPlaylist {
|
impl FromStr for MasterPlaylist {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
let mut builder = MasterPlaylistBuilder::new();
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
for (i, line) in Lines::new(s).enumerate() {
|
let mut builder = MasterPlaylist::builder();
|
||||||
match track!(line)? {
|
|
||||||
Line::Blank | Line::Comment(_) => {}
|
let mut media_tags = vec![];
|
||||||
|
let mut stream_inf_tags = vec![];
|
||||||
|
let mut i_frame_stream_inf_tags = vec![];
|
||||||
|
let mut session_data_tags = vec![];
|
||||||
|
let mut session_key_tags = vec![];
|
||||||
|
|
||||||
|
for (i, line) in input.parse::<Lines>()?.into_iter().enumerate() {
|
||||||
|
match line {
|
||||||
Line::Tag(tag) => {
|
Line::Tag(tag) => {
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
track_assert_eq!(tag, Tag::ExtM3u(ExtM3u), ErrorKind::InvalidInput);
|
if tag != Tag::ExtM3u(ExtM3u) {
|
||||||
|
return Err(Error::invalid_input());
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
match tag {
|
match tag {
|
||||||
Tag::ExtM3u(_) => {
|
Tag::ExtM3u(_) => {
|
||||||
track_panic!(ErrorKind::InvalidInput);
|
return Err(Error::invalid_input());
|
||||||
}
|
}
|
||||||
Tag::ExtXVersion(t) => {
|
Tag::ExtXVersion(t) => {
|
||||||
track_assert_eq!(builder.version, None, ErrorKind::InvalidInput);
|
|
||||||
builder.version(t.version());
|
builder.version(t.version());
|
||||||
}
|
}
|
||||||
Tag::ExtInf(_)
|
Tag::ExtInf(_)
|
||||||
|
@ -337,46 +313,92 @@ impl FromStr for MasterPlaylist {
|
||||||
| Tag::ExtXEndList(_)
|
| Tag::ExtXEndList(_)
|
||||||
| Tag::ExtXPlaylistType(_)
|
| Tag::ExtXPlaylistType(_)
|
||||||
| Tag::ExtXIFramesOnly(_) => {
|
| Tag::ExtXIFramesOnly(_) => {
|
||||||
track_panic!(ErrorKind::InvalidInput, "{}", tag)
|
return Err(Error::custom(format!(
|
||||||
|
"This tag isn't allowed in a master playlist: {}",
|
||||||
|
tag
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
Tag::ExtXMedia(t) => {
|
Tag::ExtXMedia(t) => {
|
||||||
builder.tag(t);
|
media_tags.push(t);
|
||||||
}
|
}
|
||||||
Tag::ExtXStreamInf(t) => {
|
Tag::ExtXStreamInf(t) => {
|
||||||
builder.tag(t);
|
stream_inf_tags.push(t);
|
||||||
}
|
}
|
||||||
Tag::ExtXIFrameStreamInf(t) => {
|
Tag::ExtXIFrameStreamInf(t) => {
|
||||||
builder.tag(t);
|
i_frame_stream_inf_tags.push(t);
|
||||||
}
|
}
|
||||||
Tag::ExtXSessionData(t) => {
|
Tag::ExtXSessionData(t) => {
|
||||||
builder.tag(t);
|
session_data_tags.push(t);
|
||||||
}
|
}
|
||||||
Tag::ExtXSessionKey(t) => {
|
Tag::ExtXSessionKey(t) => {
|
||||||
builder.tag(t);
|
session_key_tags.push(t);
|
||||||
}
|
}
|
||||||
Tag::ExtXIndependentSegments(t) => {
|
Tag::ExtXIndependentSegments(t) => {
|
||||||
track_assert_eq!(
|
builder.independent_segments_tag(t);
|
||||||
builder.independent_segments_tag,
|
|
||||||
None,
|
|
||||||
ErrorKind::InvalidInput
|
|
||||||
);
|
|
||||||
builder.tag(t);
|
|
||||||
}
|
}
|
||||||
Tag::ExtXStart(t) => {
|
Tag::ExtXStart(t) => {
|
||||||
track_assert_eq!(builder.start_tag, None, ErrorKind::InvalidInput);
|
builder.start_tag(t);
|
||||||
builder.tag(t);
|
|
||||||
}
|
}
|
||||||
Tag::Unknown(_) => {
|
_ => {
|
||||||
// [6.3.1. General Client Responsibilities]
|
// [6.3.1. General Client Responsibilities]
|
||||||
// > ignore any unrecognized tags.
|
// > ignore any unrecognized tags.
|
||||||
|
// TODO: collect custom tags
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Line::Uri(uri) => {
|
Line::Uri(uri) => {
|
||||||
track_panic!(ErrorKind::InvalidInput, "Unexpected URI: {:?}", uri);
|
return Err(Error::custom(format!("Unexpected URI: {:?}", uri)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
track!(builder.finish())
|
|
||||||
|
builder.media_tags(media_tags);
|
||||||
|
builder.stream_inf_tags(stream_inf_tags);
|
||||||
|
builder.i_frame_stream_inf_tags(i_frame_stream_inf_tags);
|
||||||
|
builder.session_data_tags(session_data_tags);
|
||||||
|
builder.session_key_tags(session_key_tags);
|
||||||
|
|
||||||
|
builder.build().map_err(Error::builder_error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parser() {
|
||||||
|
r#"#EXTM3U
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=150000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
|
||||||
|
http://example.com/low/index.m3u8
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=240000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
|
||||||
|
http://example.com/lo_mid/index.m3u8
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=440000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
|
||||||
|
http://example.com/hi_mid/index.m3u8
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=640000,RESOLUTION=640x360,CODECS="avc1.42e00a,mp4a.40.2"
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_display() {
|
||||||
|
let input = r#"#EXTM3U
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=150000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
|
||||||
|
http://example.com/low/index.m3u8
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=240000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
|
||||||
|
http://example.com/lo_mid/index.m3u8
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=440000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
|
||||||
|
http://example.com/hi_mid/index.m3u8
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=640000,RESOLUTION=640x360,CODECS="avc1.42e00a,mp4a.40.2"
|
||||||
|
http://example.com/high/index.m3u8
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.5"
|
||||||
|
http://example.com/audio/index.m3u8
|
||||||
|
"#;
|
||||||
|
let playlist = input.parse::<MasterPlaylist>().unwrap();
|
||||||
|
assert_eq!(playlist.to_string(), input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,153 +1,137 @@
|
||||||
use crate::line::{Line, Lines, Tag};
|
|
||||||
use crate::media_segment::{MediaSegment, MediaSegmentBuilder};
|
|
||||||
use crate::tags::{
|
|
||||||
ExtM3u, ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly, ExtXIndependentSegments,
|
|
||||||
ExtXMediaSequence, ExtXPlaylistType, ExtXStart, ExtXTargetDuration, ExtXVersion,
|
|
||||||
MediaPlaylistTag,
|
|
||||||
};
|
|
||||||
use crate::types::ProtocolVersion;
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::iter;
|
use std::iter;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
/// Media playlist builder.
|
use derive_builder::Builder;
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct MediaPlaylistBuilder {
|
|
||||||
version: Option<ProtocolVersion>,
|
|
||||||
target_duration_tag: Option<ExtXTargetDuration>,
|
|
||||||
media_sequence_tag: Option<ExtXMediaSequence>,
|
|
||||||
discontinuity_sequence_tag: Option<ExtXDiscontinuitySequence>,
|
|
||||||
playlist_type_tag: Option<ExtXPlaylistType>,
|
|
||||||
i_frames_only_tag: Option<ExtXIFramesOnly>,
|
|
||||||
independent_segments_tag: Option<ExtXIndependentSegments>,
|
|
||||||
start_tag: Option<ExtXStart>,
|
|
||||||
end_list_tag: Option<ExtXEndList>,
|
|
||||||
segments: Vec<MediaSegment>,
|
|
||||||
options: MediaPlaylistOptions,
|
|
||||||
}
|
|
||||||
impl MediaPlaylistBuilder {
|
|
||||||
/// Makes a new `MediaPlaylistBuilder` instance.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
MediaPlaylistBuilder {
|
|
||||||
version: None,
|
|
||||||
target_duration_tag: None,
|
|
||||||
media_sequence_tag: None,
|
|
||||||
discontinuity_sequence_tag: None,
|
|
||||||
playlist_type_tag: None,
|
|
||||||
i_frames_only_tag: None,
|
|
||||||
independent_segments_tag: None,
|
|
||||||
start_tag: None,
|
|
||||||
end_list_tag: None,
|
|
||||||
segments: Vec::new(),
|
|
||||||
options: MediaPlaylistOptions::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
use crate::line::{Line, Lines, Tag};
|
||||||
|
use crate::media_segment::MediaSegment;
|
||||||
|
use crate::tags::{
|
||||||
|
ExtM3u, ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly, ExtXIndependentSegments,
|
||||||
|
ExtXMediaSequence, ExtXPlaylistType, ExtXStart, ExtXTargetDuration, ExtXVersion,
|
||||||
|
};
|
||||||
|
use crate::types::ProtocolVersion;
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
|
/// Media playlist.
|
||||||
|
#[derive(Debug, Clone, Builder)]
|
||||||
|
#[builder(build_fn(validate = "Self::validate"))]
|
||||||
|
#[builder(setter(into, strip_option))]
|
||||||
|
pub struct MediaPlaylist {
|
||||||
/// Sets the protocol compatibility version of the resulting playlist.
|
/// Sets the protocol compatibility version of the resulting playlist.
|
||||||
///
|
///
|
||||||
/// If the resulting playlist has tags which requires a compatibility version greater than `version`,
|
/// If the resulting playlist has tags which requires a compatibility
|
||||||
/// `finish()` method will fail with an `ErrorKind::InvalidInput` error.
|
/// 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.
|
/// The default is the maximum version among the tags in the playlist.
|
||||||
pub fn version(&mut self, version: ProtocolVersion) -> &mut Self {
|
#[builder(setter(name = "version"))]
|
||||||
self.version = Some(version);
|
version_tag: ExtXVersion,
|
||||||
self
|
/// Sets the [ExtXTargetDuration] tag.
|
||||||
}
|
target_duration_tag: ExtXTargetDuration,
|
||||||
|
#[builder(default)]
|
||||||
/// Sets the given tag to the resulting playlist.
|
/// Sets the [ExtXMediaSequence] tag.
|
||||||
pub fn tag<T: Into<MediaPlaylistTag>>(&mut self, tag: T) -> &mut Self {
|
media_sequence_tag: Option<ExtXMediaSequence>,
|
||||||
match tag.into() {
|
#[builder(default)]
|
||||||
MediaPlaylistTag::ExtXTargetDuration(t) => self.target_duration_tag = Some(t),
|
/// Sets the [ExtXDiscontinuitySequence] tag.
|
||||||
MediaPlaylistTag::ExtXMediaSequence(t) => self.media_sequence_tag = Some(t),
|
discontinuity_sequence_tag: Option<ExtXDiscontinuitySequence>,
|
||||||
MediaPlaylistTag::ExtXDiscontinuitySequence(t) => {
|
#[builder(default)]
|
||||||
self.discontinuity_sequence_tag = Some(t)
|
/// Sets the [ExtXPlaylistType] tag.
|
||||||
}
|
playlist_type_tag: Option<ExtXPlaylistType>,
|
||||||
MediaPlaylistTag::ExtXPlaylistType(t) => self.playlist_type_tag = Some(t),
|
#[builder(default)]
|
||||||
MediaPlaylistTag::ExtXIFramesOnly(t) => self.i_frames_only_tag = Some(t),
|
/// Sets the [ExtXIFramesOnly] tag.
|
||||||
MediaPlaylistTag::ExtXIndependentSegments(t) => self.independent_segments_tag = Some(t),
|
i_frames_only_tag: Option<ExtXIFramesOnly>,
|
||||||
MediaPlaylistTag::ExtXStart(t) => self.start_tag = Some(t),
|
#[builder(default)]
|
||||||
MediaPlaylistTag::ExtXEndList(t) => self.end_list_tag = Some(t),
|
/// Sets the [ExtXIndependentSegments] tag.
|
||||||
}
|
independent_segments_tag: Option<ExtXIndependentSegments>,
|
||||||
self
|
#[builder(default)]
|
||||||
}
|
/// Sets the [ExtXStart] tag.
|
||||||
|
start_tag: Option<ExtXStart>,
|
||||||
/// Adds a media segment to the resulting playlist.
|
#[builder(default)]
|
||||||
pub fn segment(&mut self, segment: MediaSegment) -> &mut Self {
|
/// Sets the [ExtXEndList] tag.
|
||||||
self.segments.push(segment);
|
end_list_tag: Option<ExtXEndList>,
|
||||||
self
|
/// Sets all [MediaSegment]s.
|
||||||
}
|
segments: Vec<MediaSegment>,
|
||||||
|
/// Sets the allowable excess duration of each media segment in the associated playlist.
|
||||||
/// Sets the options that will be associated to the resulting playlist.
|
|
||||||
///
|
///
|
||||||
/// The default value is `MediaPlaylistOptions::default()`.
|
/// # Error
|
||||||
pub fn options(&mut self, options: MediaPlaylistOptions) -> &mut Self {
|
/// If there is a media segment of which duration exceeds
|
||||||
self.options = options;
|
/// `#EXT-X-TARGETDURATION + allowable_excess_duration`,
|
||||||
self
|
/// the invocation of `MediaPlaylistBuilder::build()` method will fail.
|
||||||
}
|
///
|
||||||
|
/// The default value is `Duration::from_secs(0)`.
|
||||||
|
#[builder(default = "Duration::from_secs(0)")]
|
||||||
|
allowable_excess_duration: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
/// Builds a `MediaPlaylist` instance.
|
impl MediaPlaylistBuilder {
|
||||||
pub fn finish(self) -> Result<MediaPlaylist> {
|
fn validate(&self) -> Result<(), String> {
|
||||||
let required_version = self.required_version();
|
let required_version = self.required_version();
|
||||||
let specified_version = self.version.unwrap_or(required_version);
|
let specified_version = self
|
||||||
track_assert!(
|
.version_tag
|
||||||
required_version <= specified_version,
|
.unwrap_or(required_version.into())
|
||||||
ErrorKind::InvalidInput,
|
.version();
|
||||||
"required_version:{}, specified_version:{}",
|
|
||||||
required_version,
|
|
||||||
specified_version,
|
|
||||||
);
|
|
||||||
|
|
||||||
let target_duration_tag =
|
if required_version > specified_version {
|
||||||
track_assert_some!(self.target_duration_tag, ErrorKind::InvalidInput);
|
return Err(Error::custom(format!(
|
||||||
track!(self.validate_media_segments(target_duration_tag.duration()))?;
|
"required_version: {}, specified_version: {}",
|
||||||
|
required_version, specified_version
|
||||||
|
))
|
||||||
|
.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
Ok(MediaPlaylist {
|
if let Some(target_duration) = &self.target_duration_tag {
|
||||||
version_tag: ExtXVersion::new(specified_version),
|
self.validate_media_segments(target_duration.duration())
|
||||||
target_duration_tag,
|
.map_err(|e| e.to_string())?;
|
||||||
media_sequence_tag: self.media_sequence_tag,
|
}
|
||||||
discontinuity_sequence_tag: self.discontinuity_sequence_tag,
|
|
||||||
playlist_type_tag: self.playlist_type_tag,
|
Ok(())
|
||||||
i_frames_only_tag: self.i_frames_only_tag,
|
|
||||||
independent_segments_tag: self.independent_segments_tag,
|
|
||||||
start_tag: self.start_tag,
|
|
||||||
end_list_tag: self.end_list_tag,
|
|
||||||
segments: self.segments,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_media_segments(&self, target_duration: Duration) -> Result<()> {
|
fn validate_media_segments(&self, target_duration: Duration) -> crate::Result<()> {
|
||||||
let mut last_range_uri = None;
|
let mut last_range_uri = None;
|
||||||
for s in &self.segments {
|
if let Some(segments) = &self.segments {
|
||||||
// CHECK: `#EXT-X-TARGETDURATION`
|
for s in segments {
|
||||||
let segment_duration = s.inf_tag().duration();
|
// CHECK: `#EXT-X-TARGETDURATION`
|
||||||
let rounded_segment_duration = if segment_duration.subsec_nanos() < 500_000_000 {
|
let segment_duration = s.inf_tag().duration();
|
||||||
Duration::from_secs(segment_duration.as_secs())
|
let rounded_segment_duration = if segment_duration.subsec_nanos() < 500_000_000 {
|
||||||
} else {
|
Duration::from_secs(segment_duration.as_secs())
|
||||||
Duration::from_secs(segment_duration.as_secs() + 1)
|
|
||||||
};
|
|
||||||
let max_segment_duration = target_duration + self.options.allowable_excess_duration;
|
|
||||||
track_assert!(
|
|
||||||
rounded_segment_duration <= max_segment_duration,
|
|
||||||
ErrorKind::InvalidInput,
|
|
||||||
"Too large segment duration: actual={:?}, max={:?}, target_duration={:?}, uri={:?}",
|
|
||||||
segment_duration,
|
|
||||||
max_segment_duration,
|
|
||||||
target_duration,
|
|
||||||
s.uri()
|
|
||||||
);
|
|
||||||
|
|
||||||
// CHECK: `#EXT-X-BYTE-RANGE`
|
|
||||||
if let Some(tag) = s.byte_range_tag() {
|
|
||||||
if tag.range().start.is_none() {
|
|
||||||
let last_uri = track_assert_some!(last_range_uri, ErrorKind::InvalidInput);
|
|
||||||
track_assert_eq!(last_uri, s.uri(), ErrorKind::InvalidInput);
|
|
||||||
} else {
|
} else {
|
||||||
last_range_uri = Some(s.uri());
|
Duration::from_secs(segment_duration.as_secs() + 1)
|
||||||
|
};
|
||||||
|
|
||||||
|
let max_segment_duration = {
|
||||||
|
if let Some(value) = &self.allowable_excess_duration {
|
||||||
|
target_duration + *value
|
||||||
|
} else {
|
||||||
|
target_duration
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !(rounded_segment_duration <= max_segment_duration) {
|
||||||
|
return Err(Error::custom(format!(
|
||||||
|
"Too large segment duration: actual={:?}, max={:?}, target_duration={:?}, uri={:?}",
|
||||||
|
segment_duration,
|
||||||
|
max_segment_duration,
|
||||||
|
target_duration,
|
||||||
|
s.uri()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// CHECK: `#EXT-X-BYTE-RANGE`
|
||||||
|
if let Some(tag) = s.byte_range_tag() {
|
||||||
|
if tag.to_range().start().is_none() {
|
||||||
|
let last_uri = last_range_uri.ok_or(Error::invalid_input())?;
|
||||||
|
if last_uri != s.uri() {
|
||||||
|
return Err(Error::invalid_input());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
last_range_uri = Some(s.uri());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
last_range_uri = None;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
last_range_uri = None;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -160,89 +144,128 @@ impl MediaPlaylistBuilder {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|t| t.requires_version()),
|
.map(|t| t.requires_version()),
|
||||||
)
|
)
|
||||||
.chain(self.media_sequence_tag.iter().map(|t| t.requires_version()))
|
.chain(self.media_sequence_tag.iter().map(|t| {
|
||||||
.chain(
|
if let Some(p) = t {
|
||||||
self.discontinuity_sequence_tag
|
p.requires_version()
|
||||||
.iter()
|
} else {
|
||||||
.map(|t| t.requires_version()),
|
ProtocolVersion::V1
|
||||||
)
|
}
|
||||||
.chain(self.playlist_type_tag.iter().map(|t| t.requires_version()))
|
}))
|
||||||
.chain(self.i_frames_only_tag.iter().map(|t| t.requires_version()))
|
.chain(self.discontinuity_sequence_tag.iter().map(|t| {
|
||||||
.chain(
|
if let Some(p) = t {
|
||||||
self.independent_segments_tag
|
p.requires_version()
|
||||||
.iter()
|
} else {
|
||||||
.map(|t| t.requires_version()),
|
ProtocolVersion::V1
|
||||||
)
|
}
|
||||||
.chain(self.start_tag.iter().map(|t| t.requires_version()))
|
}))
|
||||||
.chain(self.end_list_tag.iter().map(|t| t.requires_version()))
|
.chain(self.playlist_type_tag.iter().map(|t| {
|
||||||
.chain(self.segments.iter().map(|s| s.requires_version()))
|
if let Some(p) = t {
|
||||||
|
p.requires_version()
|
||||||
|
} else {
|
||||||
|
ProtocolVersion::V1
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.chain(self.i_frames_only_tag.iter().map(|t| {
|
||||||
|
if let Some(p) = t {
|
||||||
|
p.requires_version()
|
||||||
|
} else {
|
||||||
|
ProtocolVersion::V1
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.chain(self.independent_segments_tag.iter().map(|t| {
|
||||||
|
if let Some(p) = t {
|
||||||
|
p.requires_version()
|
||||||
|
} else {
|
||||||
|
ProtocolVersion::V1
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.chain(self.start_tag.iter().map(|t| {
|
||||||
|
if let Some(p) = t {
|
||||||
|
p.requires_version()
|
||||||
|
} else {
|
||||||
|
ProtocolVersion::V1
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.chain(self.end_list_tag.iter().map(|t| {
|
||||||
|
if let Some(p) = t {
|
||||||
|
p.requires_version()
|
||||||
|
} else {
|
||||||
|
ProtocolVersion::V1
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.chain(self.segments.iter().map(|t| {
|
||||||
|
t.iter()
|
||||||
|
.map(|s| s.requires_version())
|
||||||
|
.max()
|
||||||
|
.unwrap_or(ProtocolVersion::V1)
|
||||||
|
}))
|
||||||
.max()
|
.max()
|
||||||
.unwrap_or(ProtocolVersion::V1)
|
.unwrap_or(ProtocolVersion::latest())
|
||||||
}
|
}
|
||||||
}
|
|
||||||
impl Default for MediaPlaylistBuilder {
|
/// Adds a media segment to the resulting playlist.
|
||||||
fn default() -> Self {
|
pub fn push_segment<VALUE: Into<MediaSegment>>(&mut self, value: VALUE) -> &mut Self {
|
||||||
Self::new()
|
if let Some(segments) = &mut self.segments {
|
||||||
|
segments.push(value.into());
|
||||||
|
} else {
|
||||||
|
self.segments = Some(vec![value.into()]);
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Media playlist.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct MediaPlaylist {
|
|
||||||
version_tag: ExtXVersion,
|
|
||||||
target_duration_tag: ExtXTargetDuration,
|
|
||||||
media_sequence_tag: Option<ExtXMediaSequence>,
|
|
||||||
discontinuity_sequence_tag: Option<ExtXDiscontinuitySequence>,
|
|
||||||
playlist_type_tag: Option<ExtXPlaylistType>,
|
|
||||||
i_frames_only_tag: Option<ExtXIFramesOnly>,
|
|
||||||
independent_segments_tag: Option<ExtXIndependentSegments>,
|
|
||||||
start_tag: Option<ExtXStart>,
|
|
||||||
end_list_tag: Option<ExtXEndList>,
|
|
||||||
segments: Vec<MediaSegment>,
|
|
||||||
}
|
|
||||||
impl MediaPlaylist {
|
impl MediaPlaylist {
|
||||||
|
/// Creates a [MediaPlaylistBuilder].
|
||||||
|
pub fn builder() -> MediaPlaylistBuilder {
|
||||||
|
MediaPlaylistBuilder::default()
|
||||||
|
}
|
||||||
/// Returns the `EXT-X-VERSION` tag contained in the playlist.
|
/// Returns the `EXT-X-VERSION` tag contained in the playlist.
|
||||||
pub fn version_tag(&self) -> ExtXVersion {
|
pub const fn version_tag(&self) -> ExtXVersion {
|
||||||
self.version_tag
|
self.version_tag
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the `EXT-X-TARGETDURATION` tag contained in the playlist.
|
/// Returns the `EXT-X-TARGETDURATION` tag contained in the playlist.
|
||||||
pub fn target_duration_tag(&self) -> ExtXTargetDuration {
|
pub const fn target_duration_tag(&self) -> ExtXTargetDuration {
|
||||||
self.target_duration_tag
|
self.target_duration_tag
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the `EXT-X-MEDIA-SEQUENCE` tag contained in the playlist.
|
/// Returns the `EXT-X-MEDIA-SEQUENCE` tag contained in the playlist.
|
||||||
pub fn media_sequence_tag(&self) -> Option<ExtXMediaSequence> {
|
pub const fn media_sequence_tag(&self) -> Option<ExtXMediaSequence> {
|
||||||
self.media_sequence_tag
|
self.media_sequence_tag
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the `EXT-X-DISCONTINUITY-SEQUENCE` tag contained in the playlist.
|
/// Returns the `EXT-X-DISCONTINUITY-SEQUENCE` tag contained in the playlist.
|
||||||
pub fn discontinuity_sequence_tag(&self) -> Option<ExtXDiscontinuitySequence> {
|
pub const fn discontinuity_sequence_tag(&self) -> Option<ExtXDiscontinuitySequence> {
|
||||||
self.discontinuity_sequence_tag
|
self.discontinuity_sequence_tag
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the `EXT-X-PLAYLIST-TYPE` tag contained in the playlist.
|
/// Returns the `EXT-X-PLAYLIST-TYPE` tag contained in the playlist.
|
||||||
pub fn playlist_type_tag(&self) -> Option<ExtXPlaylistType> {
|
pub const fn playlist_type_tag(&self) -> Option<ExtXPlaylistType> {
|
||||||
self.playlist_type_tag
|
self.playlist_type_tag
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the `EXT-X-I-FRAMES-ONLY` tag contained in the playlist.
|
/// Returns the `EXT-X-I-FRAMES-ONLY` tag contained in the playlist.
|
||||||
pub fn i_frames_only_tag(&self) -> Option<ExtXIFramesOnly> {
|
pub const fn i_frames_only_tag(&self) -> Option<ExtXIFramesOnly> {
|
||||||
self.i_frames_only_tag
|
self.i_frames_only_tag
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the `EXT-X-INDEPENDENT-SEGMENTS` tag contained in the playlist.
|
/// Returns the `EXT-X-INDEPENDENT-SEGMENTS` tag contained in the playlist.
|
||||||
pub fn independent_segments_tag(&self) -> Option<ExtXIndependentSegments> {
|
pub const fn independent_segments_tag(&self) -> Option<ExtXIndependentSegments> {
|
||||||
self.independent_segments_tag
|
self.independent_segments_tag
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the `EXT-X-START` tag contained in the playlist.
|
/// Returns the `EXT-X-START` tag contained in the playlist.
|
||||||
pub fn start_tag(&self) -> Option<ExtXStart> {
|
pub const fn start_tag(&self) -> Option<ExtXStart> {
|
||||||
self.start_tag
|
self.start_tag
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the `EXT-X-ENDLIST` tag contained in the playlist.
|
/// Returns the `EXT-X-ENDLIST` tag contained in the playlist.
|
||||||
pub fn end_list_tag(&self) -> Option<ExtXEndList> {
|
pub const fn end_list_tag(&self) -> Option<ExtXEndList> {
|
||||||
self.end_list_tag
|
self.end_list_tag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,6 +274,7 @@ impl MediaPlaylist {
|
||||||
&self.segments
|
&self.segments
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for MediaPlaylist {
|
impl fmt::Display for MediaPlaylist {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
writeln!(f, "{}", ExtM3u)?;
|
writeln!(f, "{}", ExtM3u)?;
|
||||||
|
@ -258,201 +282,156 @@ impl fmt::Display for MediaPlaylist {
|
||||||
writeln!(f, "{}", self.version_tag)?;
|
writeln!(f, "{}", self.version_tag)?;
|
||||||
}
|
}
|
||||||
writeln!(f, "{}", self.target_duration_tag)?;
|
writeln!(f, "{}", self.target_duration_tag)?;
|
||||||
if let Some(ref t) = self.media_sequence_tag {
|
if let Some(value) = &self.media_sequence_tag {
|
||||||
writeln!(f, "{}", t)?;
|
writeln!(f, "{}", value)?;
|
||||||
}
|
}
|
||||||
if let Some(ref t) = self.discontinuity_sequence_tag {
|
if let Some(value) = &self.discontinuity_sequence_tag {
|
||||||
writeln!(f, "{}", t)?;
|
writeln!(f, "{}", value)?;
|
||||||
}
|
}
|
||||||
if let Some(ref t) = self.playlist_type_tag {
|
if let Some(value) = &self.playlist_type_tag {
|
||||||
writeln!(f, "{}", t)?;
|
writeln!(f, "{}", value)?;
|
||||||
}
|
}
|
||||||
if let Some(ref t) = self.i_frames_only_tag {
|
if let Some(value) = &self.i_frames_only_tag {
|
||||||
writeln!(f, "{}", t)?;
|
writeln!(f, "{}", value)?;
|
||||||
}
|
}
|
||||||
if let Some(ref t) = self.independent_segments_tag {
|
if let Some(value) = &self.independent_segments_tag {
|
||||||
writeln!(f, "{}", t)?;
|
writeln!(f, "{}", value)?;
|
||||||
}
|
}
|
||||||
if let Some(ref t) = self.start_tag {
|
if let Some(value) = &self.start_tag {
|
||||||
writeln!(f, "{}", t)?;
|
writeln!(f, "{}", value)?;
|
||||||
}
|
}
|
||||||
for segment in &self.segments {
|
for segment in &self.segments {
|
||||||
write!(f, "{}", segment)?;
|
write!(f, "{}", segment)?;
|
||||||
}
|
}
|
||||||
if let Some(ref t) = self.end_list_tag {
|
if let Some(value) = &self.end_list_tag {
|
||||||
writeln!(f, "{}", t)?;
|
writeln!(f, "{}", value)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl FromStr for MediaPlaylist {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track!(MediaPlaylistOptions::new().parse(s))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Media playlist options.
|
fn parse_media_playlist(
|
||||||
#[derive(Debug, Clone)]
|
input: &str,
|
||||||
pub struct MediaPlaylistOptions {
|
builder: &mut MediaPlaylistBuilder,
|
||||||
allowable_excess_duration: Duration,
|
) -> crate::Result<MediaPlaylist> {
|
||||||
}
|
let mut segment = MediaSegment::builder();
|
||||||
impl MediaPlaylistOptions {
|
let mut segments = vec![];
|
||||||
/// Makes a new `MediaPlaylistOptions` with the default settings.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
MediaPlaylistOptions {
|
|
||||||
allowable_excess_duration: Duration::from_secs(0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the allowable excess duration of each media segment in the associated playlist.
|
let mut has_partial_segment = false;
|
||||||
///
|
let mut has_discontinuity_tag = false;
|
||||||
/// If there is a media segment of which duration exceeds
|
let mut has_version = false; // m3u8 files without ExtXVersion tags are ProtocolVersion::V1
|
||||||
/// `#EXT-X-TARGETDURATION + allowable_excess_duration`,
|
|
||||||
/// the invocation of `MediaPlaylistBuilder::finish()` method will fail.
|
|
||||||
///
|
|
||||||
/// The default value is `Duration::from_secs(0)`.
|
|
||||||
pub fn allowable_excess_segment_duration(
|
|
||||||
&mut self,
|
|
||||||
allowable_excess_duration: Duration,
|
|
||||||
) -> &mut Self {
|
|
||||||
self.allowable_excess_duration = allowable_excess_duration;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parses the given M3U8 text with the specified settings.
|
for (i, line) in input.parse::<Lines>()?.into_iter().enumerate() {
|
||||||
pub fn parse(&self, m3u8: &str) -> Result<MediaPlaylist> {
|
match line {
|
||||||
let mut builder = MediaPlaylistBuilder::new();
|
Line::Tag(tag) => {
|
||||||
builder.options(self.clone());
|
if i == 0 {
|
||||||
|
if tag != Tag::ExtM3u(ExtM3u) {
|
||||||
let mut segment = MediaSegmentBuilder::new();
|
return Err(Error::custom("m3u8 doesn't start with #EXTM3U"));
|
||||||
let mut has_partial_segment = false;
|
|
||||||
let mut has_discontinuity_tag = false;
|
|
||||||
for (i, line) in Lines::new(m3u8).enumerate() {
|
|
||||||
match track!(line)? {
|
|
||||||
Line::Blank | Line::Comment(_) => {}
|
|
||||||
Line::Tag(tag) => {
|
|
||||||
if i == 0 {
|
|
||||||
track_assert_eq!(tag, Tag::ExtM3u(ExtM3u), ErrorKind::InvalidInput);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
match tag {
|
|
||||||
Tag::ExtM3u(_) => track_panic!(ErrorKind::InvalidInput),
|
|
||||||
Tag::ExtXVersion(t) => {
|
|
||||||
track_assert_eq!(builder.version, None, ErrorKind::InvalidInput);
|
|
||||||
builder.version(t.version());
|
|
||||||
}
|
|
||||||
Tag::ExtInf(t) => {
|
|
||||||
has_partial_segment = true;
|
|
||||||
segment.tag(t);
|
|
||||||
}
|
|
||||||
Tag::ExtXByteRange(t) => {
|
|
||||||
has_partial_segment = true;
|
|
||||||
segment.tag(t);
|
|
||||||
}
|
|
||||||
Tag::ExtXDiscontinuity(t) => {
|
|
||||||
has_discontinuity_tag = true;
|
|
||||||
has_partial_segment = true;
|
|
||||||
segment.tag(t);
|
|
||||||
}
|
|
||||||
Tag::ExtXKey(t) => {
|
|
||||||
has_partial_segment = true;
|
|
||||||
segment.tag(t);
|
|
||||||
}
|
|
||||||
Tag::ExtXMap(t) => {
|
|
||||||
has_partial_segment = true;
|
|
||||||
segment.tag(t);
|
|
||||||
}
|
|
||||||
Tag::ExtXProgramDateTime(t) => {
|
|
||||||
has_partial_segment = true;
|
|
||||||
segment.tag(t);
|
|
||||||
}
|
|
||||||
Tag::ExtXDateRange(t) => {
|
|
||||||
has_partial_segment = true;
|
|
||||||
segment.tag(t);
|
|
||||||
}
|
|
||||||
Tag::ExtXTargetDuration(t) => {
|
|
||||||
track_assert_eq!(
|
|
||||||
builder.target_duration_tag,
|
|
||||||
None,
|
|
||||||
ErrorKind::InvalidInput
|
|
||||||
);
|
|
||||||
builder.tag(t);
|
|
||||||
}
|
|
||||||
Tag::ExtXMediaSequence(t) => {
|
|
||||||
track_assert_eq!(
|
|
||||||
builder.media_sequence_tag,
|
|
||||||
None,
|
|
||||||
ErrorKind::InvalidInput
|
|
||||||
);
|
|
||||||
track_assert!(builder.segments.is_empty(), ErrorKind::InvalidInput);
|
|
||||||
builder.tag(t);
|
|
||||||
}
|
|
||||||
Tag::ExtXDiscontinuitySequence(t) => {
|
|
||||||
track_assert!(builder.segments.is_empty(), ErrorKind::InvalidInput);
|
|
||||||
track_assert!(!has_discontinuity_tag, ErrorKind::InvalidInput);
|
|
||||||
builder.tag(t);
|
|
||||||
}
|
|
||||||
Tag::ExtXEndList(t) => {
|
|
||||||
track_assert_eq!(builder.end_list_tag, None, ErrorKind::InvalidInput);
|
|
||||||
builder.tag(t);
|
|
||||||
}
|
|
||||||
Tag::ExtXPlaylistType(t) => {
|
|
||||||
track_assert_eq!(
|
|
||||||
builder.playlist_type_tag,
|
|
||||||
None,
|
|
||||||
ErrorKind::InvalidInput
|
|
||||||
);
|
|
||||||
builder.tag(t);
|
|
||||||
}
|
|
||||||
Tag::ExtXIFramesOnly(t) => {
|
|
||||||
track_assert_eq!(
|
|
||||||
builder.i_frames_only_tag,
|
|
||||||
None,
|
|
||||||
ErrorKind::InvalidInput
|
|
||||||
);
|
|
||||||
builder.tag(t);
|
|
||||||
}
|
|
||||||
Tag::ExtXMedia(_)
|
|
||||||
| Tag::ExtXStreamInf(_)
|
|
||||||
| Tag::ExtXIFrameStreamInf(_)
|
|
||||||
| Tag::ExtXSessionData(_)
|
|
||||||
| Tag::ExtXSessionKey(_) => {
|
|
||||||
track_panic!(ErrorKind::InvalidInput, "{}", tag)
|
|
||||||
}
|
|
||||||
Tag::ExtXIndependentSegments(t) => {
|
|
||||||
track_assert_eq!(
|
|
||||||
builder.independent_segments_tag,
|
|
||||||
None,
|
|
||||||
ErrorKind::InvalidInput
|
|
||||||
);
|
|
||||||
builder.tag(t);
|
|
||||||
}
|
|
||||||
Tag::ExtXStart(t) => {
|
|
||||||
track_assert_eq!(builder.start_tag, None, ErrorKind::InvalidInput);
|
|
||||||
builder.tag(t);
|
|
||||||
}
|
|
||||||
Tag::Unknown(_) => {
|
|
||||||
// [6.3.1. General Client Responsibilities]
|
|
||||||
// > ignore any unrecognized tags.
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
Line::Uri(uri) => {
|
match tag {
|
||||||
segment.uri(uri);
|
Tag::ExtM3u(_) => return Err(Error::invalid_input()),
|
||||||
builder.segment(track!(segment.finish())?);
|
Tag::ExtXVersion(t) => {
|
||||||
segment = MediaSegmentBuilder::new();
|
builder.version(t.version());
|
||||||
has_partial_segment = false;
|
has_version = true;
|
||||||
|
}
|
||||||
|
Tag::ExtInf(t) => {
|
||||||
|
has_partial_segment = true;
|
||||||
|
segment.inf_tag(t);
|
||||||
|
}
|
||||||
|
Tag::ExtXByteRange(t) => {
|
||||||
|
has_partial_segment = true;
|
||||||
|
segment.byte_range_tag(t);
|
||||||
|
}
|
||||||
|
Tag::ExtXDiscontinuity(t) => {
|
||||||
|
has_discontinuity_tag = true;
|
||||||
|
has_partial_segment = true;
|
||||||
|
segment.discontinuity_tag(t);
|
||||||
|
}
|
||||||
|
Tag::ExtXKey(t) => {
|
||||||
|
has_partial_segment = true;
|
||||||
|
segment.push_key_tag(t);
|
||||||
|
}
|
||||||
|
Tag::ExtXMap(t) => {
|
||||||
|
has_partial_segment = true;
|
||||||
|
segment.map_tag(t);
|
||||||
|
}
|
||||||
|
Tag::ExtXProgramDateTime(t) => {
|
||||||
|
has_partial_segment = true;
|
||||||
|
segment.program_date_time_tag(t);
|
||||||
|
}
|
||||||
|
Tag::ExtXDateRange(t) => {
|
||||||
|
has_partial_segment = true;
|
||||||
|
segment.date_range_tag(t);
|
||||||
|
}
|
||||||
|
Tag::ExtXTargetDuration(t) => {
|
||||||
|
builder.target_duration_tag(t);
|
||||||
|
}
|
||||||
|
Tag::ExtXMediaSequence(t) => {
|
||||||
|
builder.media_sequence_tag(t);
|
||||||
|
}
|
||||||
|
Tag::ExtXDiscontinuitySequence(t) => {
|
||||||
|
if segments.is_empty() {
|
||||||
|
return Err(Error::invalid_input());
|
||||||
|
}
|
||||||
|
if has_discontinuity_tag {
|
||||||
|
return Err(Error::invalid_input());
|
||||||
|
}
|
||||||
|
builder.discontinuity_sequence_tag(t);
|
||||||
|
}
|
||||||
|
Tag::ExtXEndList(t) => {
|
||||||
|
builder.end_list_tag(t);
|
||||||
|
}
|
||||||
|
Tag::ExtXPlaylistType(t) => {
|
||||||
|
builder.playlist_type_tag(t);
|
||||||
|
}
|
||||||
|
Tag::ExtXIFramesOnly(t) => {
|
||||||
|
builder.i_frames_only_tag(t);
|
||||||
|
}
|
||||||
|
Tag::ExtXMedia(_)
|
||||||
|
| Tag::ExtXStreamInf(_)
|
||||||
|
| Tag::ExtXIFrameStreamInf(_)
|
||||||
|
| Tag::ExtXSessionData(_)
|
||||||
|
| Tag::ExtXSessionKey(_) => {
|
||||||
|
return Err(Error::custom(tag));
|
||||||
|
}
|
||||||
|
Tag::ExtXIndependentSegments(t) => {
|
||||||
|
builder.independent_segments_tag(t);
|
||||||
|
}
|
||||||
|
Tag::ExtXStart(t) => {
|
||||||
|
builder.start_tag(t);
|
||||||
|
}
|
||||||
|
Tag::Unknown(_) => {
|
||||||
|
// [6.3.1. General Client Responsibilities]
|
||||||
|
// > ignore any unrecognized tags.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Line::Uri(uri) => {
|
||||||
|
segment.uri(uri);
|
||||||
|
segments.push(segment.build().map_err(Error::builder_error)?);
|
||||||
|
segment = MediaSegment::builder();
|
||||||
|
has_partial_segment = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
track_assert!(!has_partial_segment, ErrorKind::InvalidInput);
|
|
||||||
track!(builder.finish())
|
|
||||||
}
|
}
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
impl Default for MediaPlaylistOptions {
|
|
||||||
fn default() -> Self {
|
impl FromStr for MediaPlaylist {
|
||||||
Self::new()
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
|
parse_media_playlist(input, &mut Self::builder())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -462,36 +441,37 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn too_large_segment_duration_test() {
|
fn too_large_segment_duration_test() {
|
||||||
let m3u8 = "#EXTM3U\n\
|
let playlist = r#"
|
||||||
#EXT-X-TARGETDURATION:8\n\
|
#EXTM3U
|
||||||
#EXT-X-VERSION:3\n\
|
#EXT-X-TARGETDURATION:8
|
||||||
#EXTINF:9.009,\n\
|
#EXT-X-VERSION:3
|
||||||
http://media.example.com/first.ts\n\
|
#EXTINF:9.009,
|
||||||
#EXTINF:9.509,\n\
|
http://media.example.com/first.ts
|
||||||
http://media.example.com/second.ts\n\
|
#EXTINF:9.509,
|
||||||
#EXTINF:3.003,\n\
|
http://media.example.com/second.ts
|
||||||
http://media.example.com/third.ts\n\
|
#EXTINF:3.003,
|
||||||
#EXT-X-ENDLIST";
|
http://media.example.com/third.ts
|
||||||
|
#EXT-X-ENDLIST"#;
|
||||||
|
|
||||||
// Error (allowable segment duration = target duration = 8)
|
// Error (allowable segment duration = target duration = 8)
|
||||||
assert!(m3u8.parse::<MediaPlaylist>().is_err());
|
assert!(playlist.parse::<MediaPlaylist>().is_err());
|
||||||
|
|
||||||
// Error (allowable segment duration = 9)
|
// Error (allowable segment duration = 9)
|
||||||
assert!(MediaPlaylistOptions::new()
|
assert!(MediaPlaylist::builder()
|
||||||
.allowable_excess_segment_duration(Duration::from_secs(1))
|
.allowable_excess_duration(Duration::from_secs(1))
|
||||||
.parse(m3u8)
|
.parse(playlist)
|
||||||
.is_err());
|
.is_err());
|
||||||
|
|
||||||
// Ok (allowable segment duration = 10)
|
// Ok (allowable segment duration = 10)
|
||||||
assert!(MediaPlaylistOptions::new()
|
MediaPlaylist::builder()
|
||||||
.allowable_excess_segment_duration(Duration::from_secs(2))
|
.allowable_excess_duration(Duration::from_secs(2))
|
||||||
.parse(m3u8)
|
.parse(playlist)
|
||||||
.is_ok());
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn empty_m3u8_parse_test() {
|
fn test_empty_playlist() {
|
||||||
let m3u8 = "";
|
let playlist = "";
|
||||||
assert!(m3u8.parse::<MediaPlaylist>().is_err());
|
assert!(playlist.parse::<MediaPlaylist>().is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,131 +1,97 @@
|
||||||
use crate::tags::{
|
|
||||||
ExtInf, ExtXByteRange, ExtXDateRange, ExtXDiscontinuity, ExtXKey, ExtXMap, ExtXProgramDateTime,
|
|
||||||
MediaSegmentTag,
|
|
||||||
};
|
|
||||||
use crate::types::{ProtocolVersion, SingleLineString};
|
|
||||||
use crate::{ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::iter;
|
use std::iter;
|
||||||
|
|
||||||
/// Media segment builder.
|
use derive_builder::Builder;
|
||||||
#[derive(Debug, Clone)]
|
use url::Url;
|
||||||
pub struct MediaSegmentBuilder {
|
|
||||||
key_tags: Vec<ExtXKey>,
|
|
||||||
map_tag: Option<ExtXMap>,
|
|
||||||
byte_range_tag: Option<ExtXByteRange>,
|
|
||||||
date_range_tag: Option<ExtXDateRange>,
|
|
||||||
discontinuity_tag: Option<ExtXDiscontinuity>,
|
|
||||||
program_date_time_tag: Option<ExtXProgramDateTime>,
|
|
||||||
inf_tag: Option<ExtInf>,
|
|
||||||
uri: Option<SingleLineString>,
|
|
||||||
}
|
|
||||||
impl MediaSegmentBuilder {
|
|
||||||
/// Makes a new `MediaSegmentBuilder` instance.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
MediaSegmentBuilder {
|
|
||||||
key_tags: Vec::new(),
|
|
||||||
map_tag: None,
|
|
||||||
byte_range_tag: None,
|
|
||||||
date_range_tag: None,
|
|
||||||
discontinuity_tag: None,
|
|
||||||
program_date_time_tag: None,
|
|
||||||
inf_tag: None,
|
|
||||||
uri: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the URI of the resulting media segment.
|
use crate::tags::{
|
||||||
pub fn uri(&mut self, uri: SingleLineString) -> &mut Self {
|
ExtInf, ExtXByteRange, ExtXDateRange, ExtXDiscontinuity, ExtXKey, ExtXMap, ExtXProgramDateTime,
|
||||||
self.uri = Some(uri);
|
};
|
||||||
self
|
use crate::types::ProtocolVersion;
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the given tag to the resulting media segment.
|
|
||||||
pub fn tag<T: Into<MediaSegmentTag>>(&mut self, tag: T) -> &mut Self {
|
|
||||||
match tag.into() {
|
|
||||||
MediaSegmentTag::ExtInf(t) => self.inf_tag = Some(t),
|
|
||||||
MediaSegmentTag::ExtXByteRange(t) => self.byte_range_tag = Some(t),
|
|
||||||
MediaSegmentTag::ExtXDateRange(t) => self.date_range_tag = Some(t),
|
|
||||||
MediaSegmentTag::ExtXDiscontinuity(t) => self.discontinuity_tag = Some(t),
|
|
||||||
MediaSegmentTag::ExtXKey(t) => self.key_tags.push(t),
|
|
||||||
MediaSegmentTag::ExtXMap(t) => self.map_tag = Some(t),
|
|
||||||
MediaSegmentTag::ExtXProgramDateTime(t) => self.program_date_time_tag = Some(t),
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds a `MediaSegment` instance.
|
|
||||||
pub fn finish(self) -> Result<MediaSegment> {
|
|
||||||
let uri = track_assert_some!(self.uri, ErrorKind::InvalidInput);
|
|
||||||
let inf_tag = track_assert_some!(self.inf_tag, ErrorKind::InvalidInput);
|
|
||||||
Ok(MediaSegment {
|
|
||||||
key_tags: self.key_tags,
|
|
||||||
map_tag: self.map_tag,
|
|
||||||
byte_range_tag: self.byte_range_tag,
|
|
||||||
date_range_tag: self.date_range_tag,
|
|
||||||
discontinuity_tag: self.discontinuity_tag,
|
|
||||||
program_date_time_tag: self.program_date_time_tag,
|
|
||||||
inf_tag,
|
|
||||||
uri,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Default for MediaSegmentBuilder {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Media segment.
|
/// Media segment.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Builder)]
|
||||||
|
#[builder(setter(into, strip_option))]
|
||||||
pub struct MediaSegment {
|
pub struct MediaSegment {
|
||||||
|
#[builder(default)]
|
||||||
|
/// Sets all [ExtXKey] tags.
|
||||||
key_tags: Vec<ExtXKey>,
|
key_tags: Vec<ExtXKey>,
|
||||||
|
#[builder(default)]
|
||||||
|
/// Sets an [ExtXMap] tag.
|
||||||
map_tag: Option<ExtXMap>,
|
map_tag: Option<ExtXMap>,
|
||||||
|
#[builder(default)]
|
||||||
|
/// Sets an [ExtXByteRange] tag.
|
||||||
byte_range_tag: Option<ExtXByteRange>,
|
byte_range_tag: Option<ExtXByteRange>,
|
||||||
|
#[builder(default)]
|
||||||
|
/// Sets an [ExtXDateRange] tag.
|
||||||
date_range_tag: Option<ExtXDateRange>,
|
date_range_tag: Option<ExtXDateRange>,
|
||||||
|
#[builder(default)]
|
||||||
|
/// Sets an [ExtXDiscontinuity] tag.
|
||||||
discontinuity_tag: Option<ExtXDiscontinuity>,
|
discontinuity_tag: Option<ExtXDiscontinuity>,
|
||||||
|
#[builder(default)]
|
||||||
|
/// Sets an [ExtXProgramDateTime] tag.
|
||||||
program_date_time_tag: Option<ExtXProgramDateTime>,
|
program_date_time_tag: Option<ExtXProgramDateTime>,
|
||||||
|
/// Sets an [ExtInf] tag.
|
||||||
inf_tag: ExtInf,
|
inf_tag: ExtInf,
|
||||||
uri: SingleLineString,
|
/// Sets an Uri.
|
||||||
|
uri: Url,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl MediaSegmentBuilder {
|
||||||
|
/// 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 {
|
||||||
|
key_tags.push(value.into());
|
||||||
|
} else {
|
||||||
|
self.key_tags = Some(vec![value.into()]);
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Display for MediaSegment {
|
impl fmt::Display for MediaSegment {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
for t in &self.key_tags {
|
for value in &self.key_tags {
|
||||||
writeln!(f, "{}", t)?;
|
writeln!(f, "{}", value)?;
|
||||||
}
|
}
|
||||||
if let Some(ref t) = self.map_tag {
|
if let Some(value) = &self.map_tag {
|
||||||
writeln!(f, "{}", t)?;
|
writeln!(f, "{}", value)?;
|
||||||
}
|
}
|
||||||
if let Some(ref t) = self.byte_range_tag {
|
if let Some(value) = &self.byte_range_tag {
|
||||||
writeln!(f, "{}", t)?;
|
writeln!(f, "{}", value)?;
|
||||||
}
|
}
|
||||||
if let Some(ref t) = self.date_range_tag {
|
if let Some(value) = &self.date_range_tag {
|
||||||
writeln!(f, "{}", t)?;
|
writeln!(f, "{}", value)?;
|
||||||
}
|
}
|
||||||
if let Some(ref t) = self.discontinuity_tag {
|
if let Some(value) = &self.discontinuity_tag {
|
||||||
writeln!(f, "{}", t)?;
|
writeln!(f, "{}", value)?;
|
||||||
}
|
}
|
||||||
if let Some(ref t) = self.program_date_time_tag {
|
if let Some(value) = &self.program_date_time_tag {
|
||||||
writeln!(f, "{}", t)?;
|
writeln!(f, "{}", value)?;
|
||||||
}
|
}
|
||||||
writeln!(f, "{},", self.inf_tag)?;
|
writeln!(f, "{},", self.inf_tag)?;
|
||||||
writeln!(f, "{}", self.uri)?;
|
writeln!(f, "{}", self.uri)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MediaSegment {
|
impl MediaSegment {
|
||||||
|
/// Creates a [MediaSegmentBuilder].
|
||||||
|
pub fn builder() -> MediaSegmentBuilder {
|
||||||
|
MediaSegmentBuilder::default()
|
||||||
|
}
|
||||||
/// Returns the URI of the media segment.
|
/// Returns the URI of the media segment.
|
||||||
pub fn uri(&self) -> &SingleLineString {
|
pub const fn uri(&self) -> &Url {
|
||||||
&self.uri
|
&self.uri
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the `EXT-X-INF` tag associated with the media segment.
|
/// Returns the `EXT-X-INF` tag associated with the media segment.
|
||||||
pub fn inf_tag(&self) -> &ExtInf {
|
pub const fn inf_tag(&self) -> &ExtInf {
|
||||||
&self.inf_tag
|
&self.inf_tag
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the `EXT-X-BYTERANGE` tag associated with the media segment.
|
/// Returns the `EXT-X-BYTERANGE` tag associated with the media segment.
|
||||||
pub fn byte_range_tag(&self) -> Option<ExtXByteRange> {
|
pub const fn byte_range_tag(&self) -> Option<ExtXByteRange> {
|
||||||
self.byte_range_tag
|
self.byte_range_tag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,7 +101,7 @@ impl MediaSegment {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the `EXT-X-DISCONTINUITY` tag associated with the media segment.
|
/// Returns the `EXT-X-DISCONTINUITY` tag associated with the media segment.
|
||||||
pub fn discontinuity_tag(&self) -> Option<ExtXDiscontinuity> {
|
pub const fn discontinuity_tag(&self) -> Option<ExtXDiscontinuity> {
|
||||||
self.discontinuity_tag
|
self.discontinuity_tag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,6 +135,6 @@ impl MediaSegment {
|
||||||
)
|
)
|
||||||
.chain(iter::once(self.inf_tag.requires_version()))
|
.chain(iter::once(self.inf_tag.requires_version()))
|
||||||
.max()
|
.max()
|
||||||
.expect("Never fails")
|
.unwrap_or(ProtocolVersion::V7)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,36 @@
|
||||||
use crate::types::ProtocolVersion;
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::types::ProtocolVersion;
|
||||||
|
use crate::utils::tag;
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// [4.3.1.1. EXTM3U]
|
/// [4.3.1.1. EXTM3U]
|
||||||
///
|
///
|
||||||
/// [4.3.1.1. EXTM3U]: https://tools.ietf.org/html/rfc8216#section-4.3.1.1
|
/// [4.3.1.1. EXTM3U]: https://tools.ietf.org/html/rfc8216#section-4.3.1.1
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct ExtM3u;
|
pub struct ExtM3u;
|
||||||
|
|
||||||
impl ExtM3u {
|
impl ExtM3u {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXTM3U";
|
pub(crate) const PREFIX: &'static str = "#EXTM3U";
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
pub fn requires_version(self) -> ProtocolVersion {
|
pub const fn requires_version(&self) -> ProtocolVersion {
|
||||||
ProtocolVersion::V1
|
ProtocolVersion::V1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtM3u {
|
impl fmt::Display for ExtM3u {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
Self::PREFIX.fmt(f)
|
Self::PREFIX.fmt(f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtM3u {
|
impl FromStr for ExtM3u {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput);
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
|
tag(input, Self::PREFIX)?;
|
||||||
Ok(ExtM3u)
|
Ok(ExtM3u)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,9 +40,17 @@ mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn extm3u() {
|
fn test_display() {
|
||||||
|
assert_eq!(ExtM3u.to_string(), "#EXTM3U".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parser() {
|
||||||
assert_eq!("#EXTM3U".parse::<ExtM3u>().ok(), Some(ExtM3u));
|
assert_eq!("#EXTM3U".parse::<ExtM3u>().ok(), Some(ExtM3u));
|
||||||
assert_eq!(ExtM3u.to_string(), "#EXTM3U");
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_requires_version() {
|
||||||
assert_eq!(ExtM3u.requires_version(), ProtocolVersion::V1);
|
assert_eq!(ExtM3u.requires_version(), ProtocolVersion::V1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,48 +1,59 @@
|
||||||
use crate::types::ProtocolVersion;
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::types::ProtocolVersion;
|
||||||
|
use crate::utils::tag;
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// [4.3.1.2. EXT-X-VERSION]
|
/// [4.3.1.2. EXT-X-VERSION]
|
||||||
///
|
///
|
||||||
/// [4.3.1.2. EXT-X-VERSION]: https://tools.ietf.org/html/rfc8216#section-4.3.1.2
|
/// [4.3.1.2. EXT-X-VERSION]: https://tools.ietf.org/html/rfc8216#section-4.3.1.2
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct ExtXVersion {
|
pub struct ExtXVersion(ProtocolVersion);
|
||||||
version: ProtocolVersion,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ExtXVersion {
|
impl ExtXVersion {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-VERSION:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-VERSION:";
|
||||||
|
|
||||||
/// Makes a new `ExtXVersion` tag.
|
/// Makes a new `ExtXVersion` tag.
|
||||||
pub fn new(version: ProtocolVersion) -> Self {
|
pub const fn new(version: ProtocolVersion) -> Self {
|
||||||
ExtXVersion { version }
|
Self(version)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the protocol compatibility version of the playlist containing this tag.
|
/// Returns the protocol compatibility version of the playlist containing this tag.
|
||||||
pub fn version(self) -> ProtocolVersion {
|
pub const fn version(&self) -> ProtocolVersion {
|
||||||
self.version
|
self.0
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
pub fn requires_version(self) -> ProtocolVersion {
|
pub const fn requires_version(&self) -> ProtocolVersion {
|
||||||
ProtocolVersion::V1
|
ProtocolVersion::V1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXVersion {
|
impl fmt::Display for ExtXVersion {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(f, "{}{}", Self::PREFIX, self.version)
|
write!(f, "{}{}", Self::PREFIX, self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ExtXVersion {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(ProtocolVersion::V1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ProtocolVersion> for ExtXVersion {
|
||||||
|
fn from(value: ProtocolVersion) -> Self {
|
||||||
|
Self(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXVersion {
|
impl FromStr for ExtXVersion {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
let suffix = s.split_at(Self::PREFIX.len()).1;
|
let version = tag(input, Self::PREFIX)?.parse()?;
|
||||||
let version = track!(suffix.parse())?;
|
Ok(ExtXVersion::new(version))
|
||||||
Ok(ExtXVersion { version })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,11 +62,34 @@ mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ext_x_version() {
|
fn test_display() {
|
||||||
let tag = ExtXVersion::new(ProtocolVersion::V6);
|
assert_eq!(
|
||||||
assert_eq!("#EXT-X-VERSION:6".parse().ok(), Some(tag));
|
ExtXVersion::new(ProtocolVersion::V6).to_string(),
|
||||||
assert_eq!(tag.to_string(), "#EXT-X-VERSION:6");
|
"#EXT-X-VERSION:6"
|
||||||
assert_eq!(tag.version(), ProtocolVersion::V6);
|
);
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parser() {
|
||||||
|
assert_eq!(
|
||||||
|
"#EXT-X-VERSION:6".parse().ok(),
|
||||||
|
Some(ExtXVersion::new(ProtocolVersion::V6))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_requires_version() {
|
||||||
|
assert_eq!(
|
||||||
|
ExtXVersion::new(ProtocolVersion::V6).requires_version(),
|
||||||
|
ProtocolVersion::V1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_version() {
|
||||||
|
assert_eq!(
|
||||||
|
ExtXVersion::new(ProtocolVersion::V6).version(),
|
||||||
|
ProtocolVersion::V6
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +1,60 @@
|
||||||
use crate::attribute::AttributePairs;
|
|
||||||
use crate::types::{DecimalResolution, HdcpLevel, ProtocolVersion, QuotedString};
|
|
||||||
use crate::utils::parse_u64;
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use getset::{Getters, MutGetters, Setters};
|
||||||
|
|
||||||
|
use crate::attribute::AttributePairs;
|
||||||
|
use crate::types::{DecimalResolution, HdcpLevel, ProtocolVersion};
|
||||||
|
use crate::utils::parse_u64;
|
||||||
|
use crate::utils::{quote, tag, unquote};
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// [4.3.4.3. EXT-X-I-FRAME-STREAM-INF]
|
/// [4.3.4.3. EXT-X-I-FRAME-STREAM-INF]
|
||||||
///
|
///
|
||||||
/// [4.3.4.3. EXT-X-I-FRAME-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.3
|
/// [4.3.4.3. EXT-X-I-FRAME-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.3
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Getters, Setters, MutGetters, Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct ExtXIFrameStreamInf {
|
pub struct ExtXIFrameStreamInf {
|
||||||
uri: QuotedString,
|
#[get = "pub"]
|
||||||
|
#[set = "pub"]
|
||||||
|
#[get_mut = "pub"]
|
||||||
|
/// The URI, that identifies the associated media playlist.
|
||||||
|
uri: String,
|
||||||
|
#[get = "pub"]
|
||||||
|
#[set = "pub"]
|
||||||
|
#[get_mut = "pub"]
|
||||||
|
/// The peak segment bit rate of the variant stream.
|
||||||
bandwidth: u64,
|
bandwidth: u64,
|
||||||
|
#[get = "pub"]
|
||||||
|
#[set = "pub"]
|
||||||
|
#[get_mut = "pub"]
|
||||||
|
/// The average segment bit rate of the variant stream.
|
||||||
average_bandwidth: Option<u64>,
|
average_bandwidth: Option<u64>,
|
||||||
codecs: Option<QuotedString>,
|
#[get = "pub"]
|
||||||
|
#[set = "pub"]
|
||||||
|
#[get_mut = "pub"]
|
||||||
|
/// A string that represents the list of codec types contained the variant stream.
|
||||||
|
codecs: Option<String>,
|
||||||
|
/// The optimal pixel resolution at which to display all the video in the variant stream.
|
||||||
resolution: Option<DecimalResolution>,
|
resolution: Option<DecimalResolution>,
|
||||||
|
#[get = "pub"]
|
||||||
|
#[set = "pub"]
|
||||||
|
#[get_mut = "pub"]
|
||||||
|
/// The HDCP level of the variant stream.
|
||||||
hdcp_level: Option<HdcpLevel>,
|
hdcp_level: Option<HdcpLevel>,
|
||||||
video: Option<QuotedString>,
|
#[get = "pub"]
|
||||||
|
#[set = "pub"]
|
||||||
|
#[get_mut = "pub"]
|
||||||
|
/// The group identifier for the video in the variant stream.
|
||||||
|
video: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExtXIFrameStreamInf {
|
impl ExtXIFrameStreamInf {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAME-STREAM-INF:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAME-STREAM-INF:";
|
||||||
|
|
||||||
/// Makes a new `ExtXIFrameStreamInf` tag.
|
/// Makes a new `ExtXIFrameStreamInf` tag.
|
||||||
pub fn new(uri: QuotedString, bandwidth: u64) -> Self {
|
pub fn new<T: ToString>(uri: T, bandwidth: u64) -> Self {
|
||||||
ExtXIFrameStreamInf {
|
ExtXIFrameStreamInf {
|
||||||
uri,
|
uri: uri.to_string(),
|
||||||
bandwidth,
|
bandwidth,
|
||||||
average_bandwidth: None,
|
average_bandwidth: None,
|
||||||
codecs: None,
|
codecs: None,
|
||||||
|
@ -35,43 +64,27 @@ impl ExtXIFrameStreamInf {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the URI that identifies the associated media playlist.
|
/// The optimal pixel resolution at which to display all the video in the variant stream.
|
||||||
pub fn uri(&self) -> &QuotedString {
|
pub fn resolution(&self) -> Option<(usize, usize)> {
|
||||||
&self.uri
|
if let Some(res) = &self.resolution {
|
||||||
|
Some((res.width(), res.height()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the peak segment bit rate of the variant stream.
|
/// Sets the optimal pixel resolution at which to display all the video in the variant stream.
|
||||||
pub fn bandwidth(&self) -> u64 {
|
pub fn set_resolution(&mut self, width: usize, height: usize) -> &mut Self {
|
||||||
self.bandwidth
|
if let Some(res) = &mut self.resolution {
|
||||||
|
res.set_width(width);
|
||||||
|
res.set_height(height);
|
||||||
|
} else {
|
||||||
|
self.resolution = Some(DecimalResolution::new(width, height));
|
||||||
|
}
|
||||||
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the average segment bit rate of the variant stream.
|
|
||||||
pub fn average_bandwidth(&self) -> Option<u64> {
|
|
||||||
self.average_bandwidth
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a string that represents the list of codec types contained the variant stream.
|
|
||||||
pub fn codecs(&self) -> Option<&QuotedString> {
|
|
||||||
self.codecs.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the optimal pixel resolution at which to display all the video in the variant stream.
|
|
||||||
pub fn resolution(&self) -> Option<DecimalResolution> {
|
|
||||||
self.resolution
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the HDCP level of the variant stream.
|
|
||||||
pub fn hdcp_level(&self) -> Option<HdcpLevel> {
|
|
||||||
self.hdcp_level
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the group identifier for the video in the variant stream.
|
|
||||||
pub fn video(&self) -> Option<&QuotedString> {
|
|
||||||
self.video.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
pub fn requires_version(&self) -> ProtocolVersion {
|
pub const fn requires_version(&self) -> ProtocolVersion {
|
||||||
ProtocolVersion::V1
|
ProtocolVersion::V1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,22 +92,23 @@ impl ExtXIFrameStreamInf {
|
||||||
impl fmt::Display for ExtXIFrameStreamInf {
|
impl fmt::Display for ExtXIFrameStreamInf {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
write!(f, "URI={}", self.uri)?;
|
write!(f, "URI={}", quote(&self.uri))?;
|
||||||
write!(f, ",BANDWIDTH={}", self.bandwidth)?;
|
write!(f, ",BANDWIDTH={}", self.bandwidth)?;
|
||||||
if let Some(ref x) = self.average_bandwidth {
|
|
||||||
write!(f, ",AVERAGE-BANDWIDTH={}", x)?;
|
if let Some(value) = &self.average_bandwidth {
|
||||||
|
write!(f, ",AVERAGE-BANDWIDTH={}", value)?;
|
||||||
}
|
}
|
||||||
if let Some(ref x) = self.codecs {
|
if let Some(value) = &self.codecs {
|
||||||
write!(f, ",CODECS={}", x)?;
|
write!(f, ",CODECS={}", quote(value))?;
|
||||||
}
|
}
|
||||||
if let Some(ref x) = self.resolution {
|
if let Some(value) = &self.resolution {
|
||||||
write!(f, ",RESOLUTION={}", x)?;
|
write!(f, ",RESOLUTION={}", value)?;
|
||||||
}
|
}
|
||||||
if let Some(ref x) = self.hdcp_level {
|
if let Some(value) = &self.hdcp_level {
|
||||||
write!(f, ",HDCP-LEVEL={}", x)?;
|
write!(f, ",HDCP-LEVEL={}", value)?;
|
||||||
}
|
}
|
||||||
if let Some(ref x) = self.video {
|
if let Some(value) = &self.video {
|
||||||
write!(f, ",VIDEO={}", x)?;
|
write!(f, ",VIDEO={}", quote(value))?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -102,8 +116,9 @@ impl fmt::Display for ExtXIFrameStreamInf {
|
||||||
|
|
||||||
impl FromStr for ExtXIFrameStreamInf {
|
impl FromStr for ExtXIFrameStreamInf {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
|
let input = tag(input, Self::PREFIX)?;
|
||||||
|
|
||||||
let mut uri = None;
|
let mut uri = None;
|
||||||
let mut bandwidth = None;
|
let mut bandwidth = None;
|
||||||
|
@ -112,17 +127,16 @@ impl FromStr for ExtXIFrameStreamInf {
|
||||||
let mut resolution = None;
|
let mut resolution = None;
|
||||||
let mut hdcp_level = None;
|
let mut hdcp_level = None;
|
||||||
let mut video = None;
|
let mut video = None;
|
||||||
let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1);
|
|
||||||
for attr in attrs {
|
for (key, value) in input.parse::<AttributePairs>()? {
|
||||||
let (key, value) = track!(attr)?;
|
match key.as_str() {
|
||||||
match key {
|
"URI" => uri = Some(unquote(value)),
|
||||||
"URI" => uri = Some(track!(value.parse())?),
|
"BANDWIDTH" => bandwidth = Some(parse_u64(value)?),
|
||||||
"BANDWIDTH" => bandwidth = Some(track!(parse_u64(value))?),
|
"AVERAGE-BANDWIDTH" => average_bandwidth = Some(parse_u64(value)?),
|
||||||
"AVERAGE-BANDWIDTH" => average_bandwidth = Some(track!(parse_u64(value))?),
|
"CODECS" => codecs = Some(unquote(value)),
|
||||||
"CODECS" => codecs = Some(track!(value.parse())?),
|
"RESOLUTION" => resolution = Some(value.parse()?),
|
||||||
"RESOLUTION" => resolution = Some(track!(value.parse())?),
|
"HDCP-LEVEL" => hdcp_level = Some(value.parse()?),
|
||||||
"HDCP-LEVEL" => hdcp_level = Some(track!(value.parse())?),
|
"VIDEO" => video = Some(unquote(value)),
|
||||||
"VIDEO" => video = Some(track!(value.parse())?),
|
|
||||||
_ => {
|
_ => {
|
||||||
// [6.3.1. General Client Responsibilities]
|
// [6.3.1. General Client Responsibilities]
|
||||||
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
||||||
|
@ -130,8 +144,9 @@ impl FromStr for ExtXIFrameStreamInf {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let uri = track_assert_some!(uri, ErrorKind::InvalidInput);
|
let uri = uri.ok_or(Error::missing_value("URI"))?;
|
||||||
let bandwidth = track_assert_some!(bandwidth, ErrorKind::InvalidInput);
|
let bandwidth = bandwidth.ok_or(Error::missing_value("BANDWIDTH"))?;
|
||||||
|
|
||||||
Ok(ExtXIFrameStreamInf {
|
Ok(ExtXIFrameStreamInf {
|
||||||
uri,
|
uri,
|
||||||
bandwidth,
|
bandwidth,
|
||||||
|
@ -149,15 +164,30 @@ mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ext_x_i_frame_stream_inf() {
|
fn test_display() {
|
||||||
let tag = ExtXIFrameStreamInf::new(quoted_string("foo"), 1000);
|
|
||||||
let text = r#"#EXT-X-I-FRAME-STREAM-INF:URI="foo",BANDWIDTH=1000"#;
|
let text = r#"#EXT-X-I-FRAME-STREAM-INF:URI="foo",BANDWIDTH=1000"#;
|
||||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
assert_eq!(ExtXIFrameStreamInf::new("foo", 1000).to_string(), text);
|
||||||
assert_eq!(tag.to_string(), text);
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn quoted_string(s: &str) -> QuotedString {
|
#[test]
|
||||||
QuotedString::new(s).unwrap()
|
fn test_parser() {
|
||||||
|
let text = r#"#EXT-X-I-FRAME-STREAM-INF:URI="foo",BANDWIDTH=1000"#;
|
||||||
|
let i_frame_stream_inf = ExtXIFrameStreamInf::new("foo", 1000);
|
||||||
|
assert_eq!(
|
||||||
|
text.parse::<ExtXIFrameStreamInf>().unwrap(),
|
||||||
|
i_frame_stream_inf.clone()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(i_frame_stream_inf.uri(), "foo");
|
||||||
|
assert_eq!(*i_frame_stream_inf.bandwidth(), 1000);
|
||||||
|
// TODO: test all the optional fields
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_requires_version() {
|
||||||
|
assert_eq!(
|
||||||
|
ExtXIFrameStreamInf::new("foo", 1000).requires_version(),
|
||||||
|
ProtocolVersion::V1
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,31 @@
|
||||||
use crate::attribute::AttributePairs;
|
|
||||||
use crate::types::{InStreamId, MediaType, ProtocolVersion, QuotedString};
|
|
||||||
use crate::utils::parse_yes_or_no;
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::attribute::AttributePairs;
|
||||||
|
use crate::types::{InStreamId, MediaType, ProtocolVersion};
|
||||||
|
use crate::utils::{parse_yes_or_no, quote, tag, unquote};
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// `ExtXMedia` builder.
|
/// `ExtXMedia` builder.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ExtXMediaBuilder {
|
pub struct ExtXMediaBuilder {
|
||||||
media_type: Option<MediaType>,
|
media_type: Option<MediaType>,
|
||||||
uri: Option<QuotedString>,
|
uri: Option<String>,
|
||||||
group_id: Option<QuotedString>,
|
group_id: Option<String>,
|
||||||
language: Option<QuotedString>,
|
language: Option<String>,
|
||||||
assoc_language: Option<QuotedString>,
|
assoc_language: Option<String>,
|
||||||
name: Option<QuotedString>,
|
name: Option<String>,
|
||||||
default: bool,
|
default: bool,
|
||||||
autoselect: Option<bool>,
|
autoselect: Option<bool>,
|
||||||
forced: Option<bool>,
|
forced: Option<bool>,
|
||||||
instream_id: Option<InStreamId>,
|
instream_id: Option<InStreamId>,
|
||||||
characteristics: Option<QuotedString>,
|
characteristics: Option<String>,
|
||||||
channels: Option<QuotedString>,
|
channels: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExtXMediaBuilder {
|
impl ExtXMediaBuilder {
|
||||||
/// Makes a `ExtXMediaBuilder` instance.
|
/// Makes a `ExtXMediaBuilder` instance.
|
||||||
pub fn new() -> Self {
|
pub const fn new() -> Self {
|
||||||
ExtXMediaBuilder {
|
ExtXMediaBuilder {
|
||||||
media_type: None,
|
media_type: None,
|
||||||
uri: None,
|
uri: None,
|
||||||
|
@ -48,32 +49,32 @@ impl ExtXMediaBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the identifier that specifies the group to which the rendition belongs.
|
/// Sets the identifier that specifies the group to which the rendition belongs.
|
||||||
pub fn group_id(&mut self, group_id: QuotedString) -> &mut Self {
|
pub fn group_id<T: ToString>(&mut self, group_id: T) -> &mut Self {
|
||||||
self.group_id = Some(group_id);
|
self.group_id = Some(group_id.to_string());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets a human-readable description of the rendition.
|
/// Sets a human-readable description of the rendition.
|
||||||
pub fn name(&mut self, name: QuotedString) -> &mut Self {
|
pub fn name<T: ToString>(&mut self, name: T) -> &mut Self {
|
||||||
self.name = Some(name);
|
self.name = Some(name.to_string());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the URI that identifies the media playlist.
|
/// Sets the URI that identifies the media playlist.
|
||||||
pub fn uri(&mut self, uri: QuotedString) -> &mut Self {
|
pub fn uri<T: ToString>(&mut self, uri: T) -> &mut Self {
|
||||||
self.uri = Some(uri);
|
self.uri = Some(uri.to_string());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the name of the primary language used in the rendition.
|
/// Sets the name of the primary language used in the rendition.
|
||||||
pub fn language(&mut self, language: QuotedString) -> &mut Self {
|
pub fn language<T: ToString>(&mut self, language: T) -> &mut Self {
|
||||||
self.language = Some(language);
|
self.language = Some(language.to_string());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the name of a language associated with the rendition.
|
/// Sets the name of a language associated with the rendition.
|
||||||
pub fn assoc_language(&mut self, language: QuotedString) -> &mut Self {
|
pub fn assoc_language<T: ToString>(&mut self, language: T) -> &mut Self {
|
||||||
self.assoc_language = Some(language);
|
self.assoc_language = Some(language.to_string());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,34 +103,51 @@ impl ExtXMediaBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the string that represents uniform type identifiers (UTI).
|
/// Sets the string that represents uniform type identifiers (UTI).
|
||||||
pub fn characteristics(&mut self, characteristics: QuotedString) -> &mut Self {
|
pub fn characteristics<T: ToString>(&mut self, characteristics: T) -> &mut Self {
|
||||||
self.characteristics = Some(characteristics);
|
self.characteristics = Some(characteristics.to_string());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the string that represents the parameters of the rendition.
|
/// Sets the string that represents the parameters of the rendition.
|
||||||
pub fn channels(&mut self, channels: QuotedString) -> &mut Self {
|
pub fn channels<T: ToString>(&mut self, channels: T) -> &mut Self {
|
||||||
self.channels = Some(channels);
|
self.channels = Some(channels.to_string());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Builds a `ExtXMedia` instance.
|
/// Builds a `ExtXMedia` instance.
|
||||||
pub fn finish(self) -> Result<ExtXMedia> {
|
pub fn finish(self) -> crate::Result<ExtXMedia> {
|
||||||
let media_type = track_assert_some!(self.media_type, ErrorKind::InvalidInput);
|
let media_type = self
|
||||||
let group_id = track_assert_some!(self.group_id, ErrorKind::InvalidInput);
|
.media_type
|
||||||
let name = track_assert_some!(self.name, ErrorKind::InvalidInput);
|
.ok_or(Error::missing_value("self.media_type"))?;
|
||||||
|
let group_id = self.group_id.ok_or(Error::missing_value("self.group_id"))?;
|
||||||
|
let name = self.name.ok_or(Error::missing_value("self.name"))?;
|
||||||
|
|
||||||
if MediaType::ClosedCaptions == media_type {
|
if MediaType::ClosedCaptions == media_type {
|
||||||
track_assert_ne!(self.uri, None, ErrorKind::InvalidInput);
|
if let None = self.uri {
|
||||||
track_assert!(self.instream_id.is_some(), ErrorKind::InvalidInput);
|
return Err(Error::missing_value("self.uri"));
|
||||||
|
}
|
||||||
|
self.instream_id
|
||||||
|
.ok_or(Error::missing_value("self.instream_id"))?;
|
||||||
} else {
|
} else {
|
||||||
track_assert!(self.instream_id.is_none(), ErrorKind::InvalidInput);
|
if let Some(_) = &self.instream_id {
|
||||||
|
Err(Error::invalid_input())?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.default && self.autoselect.is_some() {
|
if self.default && self.autoselect.is_some() {
|
||||||
track_assert_eq!(self.autoselect, Some(true), ErrorKind::InvalidInput);
|
if let Some(value) = &self.autoselect {
|
||||||
|
if *value {
|
||||||
|
Err(Error::invalid_input())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if MediaType::Subtitles != media_type {
|
if MediaType::Subtitles != media_type {
|
||||||
track_assert_eq!(self.forced, None, ErrorKind::InvalidInput);
|
if self.forced.is_some() {
|
||||||
|
Err(Error::invalid_input())?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ExtXMedia {
|
Ok(ExtXMedia {
|
||||||
media_type,
|
media_type,
|
||||||
uri: self.uri,
|
uri: self.uri,
|
||||||
|
@ -159,31 +177,31 @@ impl Default for ExtXMediaBuilder {
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct ExtXMedia {
|
pub struct ExtXMedia {
|
||||||
media_type: MediaType,
|
media_type: MediaType,
|
||||||
uri: Option<QuotedString>,
|
uri: Option<String>,
|
||||||
group_id: QuotedString,
|
group_id: String,
|
||||||
language: Option<QuotedString>,
|
language: Option<String>,
|
||||||
assoc_language: Option<QuotedString>,
|
assoc_language: Option<String>,
|
||||||
name: QuotedString,
|
name: String,
|
||||||
default: bool,
|
default: bool,
|
||||||
autoselect: bool,
|
autoselect: bool,
|
||||||
forced: bool,
|
forced: bool,
|
||||||
instream_id: Option<InStreamId>,
|
instream_id: Option<InStreamId>,
|
||||||
characteristics: Option<QuotedString>,
|
characteristics: Option<String>,
|
||||||
channels: Option<QuotedString>,
|
channels: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExtXMedia {
|
impl ExtXMedia {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA:";
|
||||||
|
|
||||||
/// Makes a new `ExtXMedia` tag.
|
/// Makes a new `ExtXMedia` tag.
|
||||||
pub fn new(media_type: MediaType, group_id: QuotedString, name: QuotedString) -> Self {
|
pub fn new<T: ToString>(media_type: MediaType, group_id: T, name: T) -> Self {
|
||||||
ExtXMedia {
|
ExtXMedia {
|
||||||
media_type,
|
media_type,
|
||||||
uri: None,
|
uri: None,
|
||||||
group_id,
|
group_id: group_id.to_string(),
|
||||||
language: None,
|
language: None,
|
||||||
assoc_language: None,
|
assoc_language: None,
|
||||||
name,
|
name: name.to_string(),
|
||||||
default: false,
|
default: false,
|
||||||
autoselect: false,
|
autoselect: false,
|
||||||
forced: false,
|
forced: false,
|
||||||
|
@ -194,65 +212,65 @@ impl ExtXMedia {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the type of the media associated with this tag.
|
/// Returns the type of the media associated with this tag.
|
||||||
pub fn media_type(&self) -> MediaType {
|
pub const fn media_type(&self) -> MediaType {
|
||||||
self.media_type
|
self.media_type
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the identifier that specifies the group to which the rendition belongs.
|
/// Returns the identifier that specifies the group to which the rendition belongs.
|
||||||
pub fn group_id(&self) -> &QuotedString {
|
pub const fn group_id(&self) -> &String {
|
||||||
&self.group_id
|
&self.group_id
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a human-readable description of the rendition.
|
/// Returns a human-readable description of the rendition.
|
||||||
pub fn name(&self) -> &QuotedString {
|
pub const fn name(&self) -> &String {
|
||||||
&self.name
|
&self.name
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the URI that identifies the media playlist.
|
/// Returns the URI that identifies the media playlist.
|
||||||
pub fn uri(&self) -> Option<&QuotedString> {
|
pub fn uri(&self) -> Option<&String> {
|
||||||
self.uri.as_ref()
|
self.uri.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the name of the primary language used in the rendition.
|
/// Returns the name of the primary language used in the rendition.
|
||||||
pub fn language(&self) -> Option<&QuotedString> {
|
pub fn language(&self) -> Option<&String> {
|
||||||
self.language.as_ref()
|
self.language.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the name of a language associated with the rendition.
|
/// Returns the name of a language associated with the rendition.
|
||||||
pub fn assoc_language(&self) -> Option<&QuotedString> {
|
pub fn assoc_language(&self) -> Option<&String> {
|
||||||
self.assoc_language.as_ref()
|
self.assoc_language.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether this is the default rendition.
|
/// Returns whether this is the default rendition.
|
||||||
pub fn default(&self) -> bool {
|
pub const fn default(&self) -> bool {
|
||||||
self.default
|
self.default
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether the client may choose to
|
/// Returns whether the client may choose to
|
||||||
/// play this rendition in the absence of explicit user preference.
|
/// play this rendition in the absence of explicit user preference.
|
||||||
pub fn autoselect(&self) -> bool {
|
pub const fn autoselect(&self) -> bool {
|
||||||
self.autoselect
|
self.autoselect
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether the rendition contains content that is considered essential to play.
|
/// Returns whether the rendition contains content that is considered essential to play.
|
||||||
pub fn forced(&self) -> bool {
|
pub const fn forced(&self) -> bool {
|
||||||
self.forced
|
self.forced
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the identifier that specifies a rendition within the segments in the media playlist.
|
/// Returns the identifier that specifies a rendition within the segments in the media playlist.
|
||||||
pub fn instream_id(&self) -> Option<InStreamId> {
|
pub const fn instream_id(&self) -> Option<InStreamId> {
|
||||||
self.instream_id
|
self.instream_id
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a string that represents uniform type identifiers (UTI).
|
/// Returns a string that represents uniform type identifiers (UTI).
|
||||||
///
|
///
|
||||||
/// Each UTI indicates an individual characteristic of the rendition.
|
/// Each UTI indicates an individual characteristic of the rendition.
|
||||||
pub fn characteristics(&self) -> Option<&QuotedString> {
|
pub fn characteristics(&self) -> Option<&String> {
|
||||||
self.characteristics.as_ref()
|
self.characteristics.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a string that represents the parameters of the rendition.
|
/// Returns a string that represents the parameters of the rendition.
|
||||||
pub fn channels(&self) -> Option<&QuotedString> {
|
pub fn channels(&self) -> Option<&String> {
|
||||||
self.channels.as_ref()
|
self.channels.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -273,17 +291,17 @@ impl fmt::Display for ExtXMedia {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
write!(f, "TYPE={}", self.media_type)?;
|
write!(f, "TYPE={}", self.media_type)?;
|
||||||
if let Some(ref x) = self.uri {
|
if let Some(value) = &self.uri {
|
||||||
write!(f, ",URI={}", x)?;
|
write!(f, ",URI={}", quote(value))?;
|
||||||
}
|
}
|
||||||
write!(f, ",GROUP-ID={}", self.group_id)?;
|
write!(f, ",GROUP-ID={}", quote(&self.group_id))?;
|
||||||
if let Some(ref x) = self.language {
|
if let Some(value) = &self.language {
|
||||||
write!(f, ",LANGUAGE={}", x)?;
|
write!(f, ",LANGUAGE={}", quote(value))?;
|
||||||
}
|
}
|
||||||
if let Some(ref x) = self.assoc_language {
|
if let Some(value) = &self.assoc_language {
|
||||||
write!(f, ",ASSOC-LANGUAGE={}", x)?;
|
write!(f, ",ASSOC-LANGUAGE={}", quote(value))?;
|
||||||
}
|
}
|
||||||
write!(f, ",NAME={}", self.name)?;
|
write!(f, ",NAME={}", quote(&self.name))?;
|
||||||
if self.default {
|
if self.default {
|
||||||
write!(f, ",DEFAULT=YES")?;
|
write!(f, ",DEFAULT=YES")?;
|
||||||
}
|
}
|
||||||
|
@ -293,14 +311,14 @@ impl fmt::Display for ExtXMedia {
|
||||||
if self.forced {
|
if self.forced {
|
||||||
write!(f, ",FORCED=YES")?;
|
write!(f, ",FORCED=YES")?;
|
||||||
}
|
}
|
||||||
if let Some(ref x) = self.instream_id {
|
if let Some(value) = &self.instream_id {
|
||||||
write!(f, ",INSTREAM-ID=\"{}\"", x)?;
|
write!(f, ",INSTREAM-ID={}", quote(value))?;
|
||||||
}
|
}
|
||||||
if let Some(ref x) = self.characteristics {
|
if let Some(value) = &self.characteristics {
|
||||||
write!(f, ",CHARACTERISTICS={}", x)?;
|
write!(f, ",CHARACTERISTICS={}", quote(value))?;
|
||||||
}
|
}
|
||||||
if let Some(ref x) = self.channels {
|
if let Some(value) = &self.channels {
|
||||||
write!(f, ",CHANNELS={}", x)?;
|
write!(f, ",CHANNELS={}", quote(value))?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -308,50 +326,49 @@ impl fmt::Display for ExtXMedia {
|
||||||
|
|
||||||
impl FromStr for ExtXMedia {
|
impl FromStr for ExtXMedia {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
|
let input = tag(input, Self::PREFIX)?;
|
||||||
|
|
||||||
let mut builder = ExtXMediaBuilder::new();
|
let mut builder = ExtXMediaBuilder::new();
|
||||||
let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1);
|
|
||||||
for attr in attrs {
|
for (key, value) in input.parse::<AttributePairs>()? {
|
||||||
let (key, value) = track!(attr)?;
|
match key.as_str() {
|
||||||
match key {
|
|
||||||
"TYPE" => {
|
"TYPE" => {
|
||||||
builder.media_type(track!(value.parse())?);
|
builder.media_type(value.parse()?);
|
||||||
}
|
}
|
||||||
"URI" => {
|
"URI" => {
|
||||||
builder.uri(track!(value.parse())?);
|
builder.uri(unquote(value));
|
||||||
}
|
}
|
||||||
"GROUP-ID" => {
|
"GROUP-ID" => {
|
||||||
builder.group_id(track!(value.parse())?);
|
builder.group_id(unquote(value));
|
||||||
}
|
}
|
||||||
"LANGUAGE" => {
|
"LANGUAGE" => {
|
||||||
builder.language(track!(value.parse())?);
|
builder.language(unquote(value));
|
||||||
}
|
}
|
||||||
"ASSOC-LANGUAGE" => {
|
"ASSOC-LANGUAGE" => {
|
||||||
builder.assoc_language(track!(value.parse())?);
|
builder.assoc_language(unquote(value));
|
||||||
}
|
}
|
||||||
"NAME" => {
|
"NAME" => {
|
||||||
builder.name(track!(value.parse())?);
|
builder.name(unquote(value));
|
||||||
}
|
}
|
||||||
"DEFAULT" => {
|
"DEFAULT" => {
|
||||||
builder.default(track!(parse_yes_or_no(value))?);
|
builder.default((parse_yes_or_no(value))?);
|
||||||
}
|
}
|
||||||
"AUTOSELECT" => {
|
"AUTOSELECT" => {
|
||||||
builder.autoselect(track!(parse_yes_or_no(value))?);
|
builder.autoselect((parse_yes_or_no(value))?);
|
||||||
}
|
}
|
||||||
"FORCED" => {
|
"FORCED" => {
|
||||||
builder.forced(track!(parse_yes_or_no(value))?);
|
builder.forced((parse_yes_or_no(value))?);
|
||||||
}
|
}
|
||||||
"INSTREAM-ID" => {
|
"INSTREAM-ID" => {
|
||||||
let s: QuotedString = track!(value.parse())?;
|
builder.instream_id(unquote(value).parse()?);
|
||||||
builder.instream_id(track!(s.parse())?);
|
|
||||||
}
|
}
|
||||||
"CHARACTERISTICS" => {
|
"CHARACTERISTICS" => {
|
||||||
builder.characteristics(track!(value.parse())?);
|
builder.characteristics(unquote(value));
|
||||||
}
|
}
|
||||||
"CHANNELS" => {
|
"CHANNELS" => {
|
||||||
builder.channels(track!(value.parse())?);
|
builder.channels(unquote(value));
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// [6.3.1. General Client Responsibilities]
|
// [6.3.1. General Client Responsibilities]
|
||||||
|
@ -359,7 +376,7 @@ impl FromStr for ExtXMedia {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
track!(builder.finish())
|
(builder.finish())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -369,14 +386,10 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ext_x_media() {
|
fn ext_x_media() {
|
||||||
let tag = ExtXMedia::new(MediaType::Audio, quoted_string("foo"), quoted_string("bar"));
|
let tag = ExtXMedia::new(MediaType::Audio, "foo", "bar");
|
||||||
let text = r#"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="foo",NAME="bar""#;
|
let text = r#"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="foo",NAME="bar""#;
|
||||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||||
assert_eq!(tag.to_string(), text);
|
assert_eq!(tag.to_string(), text);
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn quoted_string(s: &str) -> QuotedString {
|
|
||||||
QuotedString::new(s).unwrap()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,57 +1,64 @@
|
||||||
use crate::attribute::AttributePairs;
|
|
||||||
use crate::types::{ProtocolVersion, QuotedString, SessionData};
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use getset::{Getters, MutGetters, Setters};
|
||||||
|
|
||||||
|
use crate::attribute::AttributePairs;
|
||||||
|
use crate::types::ProtocolVersion;
|
||||||
|
use crate::utils::{quote, tag, unquote};
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
|
/// Session data.
|
||||||
|
///
|
||||||
|
/// See: [4.3.4.4. EXT-X-SESSION-DATA]
|
||||||
|
///
|
||||||
|
/// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub enum SessionData {
|
||||||
|
Value(String),
|
||||||
|
Uri(String),
|
||||||
|
}
|
||||||
|
|
||||||
/// [4.3.4.4. EXT-X-SESSION-DATA]
|
/// [4.3.4.4. EXT-X-SESSION-DATA]
|
||||||
///
|
///
|
||||||
/// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4
|
/// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Getters, MutGetters, Setters, Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
#[get = "pub"]
|
||||||
|
#[set = "pub"]
|
||||||
|
#[get_mut = "pub"]
|
||||||
pub struct ExtXSessionData {
|
pub struct ExtXSessionData {
|
||||||
data_id: QuotedString,
|
/// The identifier of the data.
|
||||||
|
data_id: String,
|
||||||
|
/// The session data.
|
||||||
data: SessionData,
|
data: SessionData,
|
||||||
language: Option<QuotedString>,
|
/// The language of the data.
|
||||||
|
language: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExtXSessionData {
|
impl ExtXSessionData {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-DATA:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-DATA:";
|
||||||
|
|
||||||
/// Makes a new `ExtXSessionData` tag.
|
/// Makes a new `ExtXSessionData` tag.
|
||||||
pub fn new(data_id: QuotedString, data: SessionData) -> Self {
|
pub fn new<T: ToString>(data_id: T, data: SessionData) -> Self {
|
||||||
ExtXSessionData {
|
ExtXSessionData {
|
||||||
data_id,
|
data_id: data_id.to_string(),
|
||||||
data,
|
data,
|
||||||
language: None,
|
language: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Makes a new `ExtXSessionData` with the given language.
|
/// Makes a new `ExtXSessionData` with the given language.
|
||||||
pub fn with_language(data_id: QuotedString, data: SessionData, language: QuotedString) -> Self {
|
pub fn with_language<T: ToString>(data_id: T, data: SessionData, language: T) -> Self {
|
||||||
ExtXSessionData {
|
ExtXSessionData {
|
||||||
data_id,
|
data_id: data_id.to_string(),
|
||||||
data,
|
data,
|
||||||
language: Some(language),
|
language: Some(language.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the identifier of the data.
|
|
||||||
pub fn data_id(&self) -> &QuotedString {
|
|
||||||
&self.data_id
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the session data.
|
|
||||||
pub fn data(&self) -> &SessionData {
|
|
||||||
&self.data
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the language of the data.
|
|
||||||
pub fn language(&self) -> Option<&QuotedString> {
|
|
||||||
self.language.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
pub fn requires_version(&self) -> ProtocolVersion {
|
pub const fn requires_version(&self) -> ProtocolVersion {
|
||||||
ProtocolVersion::V1
|
ProtocolVersion::V1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,13 +66,13 @@ impl ExtXSessionData {
|
||||||
impl fmt::Display for ExtXSessionData {
|
impl fmt::Display for ExtXSessionData {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
write!(f, "DATA-ID={}", self.data_id)?;
|
write!(f, "DATA-ID={}", quote(&self.data_id))?;
|
||||||
match self.data {
|
match &self.data {
|
||||||
SessionData::Value(ref x) => write!(f, ",VALUE={}", x)?,
|
SessionData::Value(value) => write!(f, ",VALUE={}", quote(value))?,
|
||||||
SessionData::Uri(ref x) => write!(f, ",URI={}", x)?,
|
SessionData::Uri(value) => write!(f, ",URI={}", quote(value))?,
|
||||||
}
|
}
|
||||||
if let Some(ref x) = self.language {
|
if let Some(value) = &self.language {
|
||||||
write!(f, ",LANGUAGE={}", x)?;
|
write!(f, ",LANGUAGE={}", quote(value))?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -73,21 +80,21 @@ impl fmt::Display for ExtXSessionData {
|
||||||
|
|
||||||
impl FromStr for ExtXSessionData {
|
impl FromStr for ExtXSessionData {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
|
let input = tag(input, Self::PREFIX)?;
|
||||||
|
|
||||||
let mut data_id = None;
|
let mut data_id = None;
|
||||||
let mut session_value = None;
|
let mut session_value = None;
|
||||||
let mut uri = None;
|
let mut uri = None;
|
||||||
let mut language = None;
|
let mut language = None;
|
||||||
let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1);
|
|
||||||
for attr in attrs {
|
for (key, value) in input.parse::<AttributePairs>()? {
|
||||||
let (key, value) = track!(attr)?;
|
match key.as_str() {
|
||||||
match key {
|
"DATA-ID" => data_id = Some(unquote(value)),
|
||||||
"DATA-ID" => data_id = Some(track!(value.parse())?),
|
"VALUE" => session_value = Some(unquote(value)),
|
||||||
"VALUE" => session_value = Some(track!(value.parse())?),
|
"URI" => uri = Some(unquote(value)),
|
||||||
"URI" => uri = Some(track!(value.parse())?),
|
"LANGUAGE" => language = Some(unquote(value)),
|
||||||
"LANGUAGE" => language = Some(track!(value.parse())?),
|
|
||||||
_ => {
|
_ => {
|
||||||
// [6.3.1. General Client Responsibilities]
|
// [6.3.1. General Client Responsibilities]
|
||||||
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
||||||
|
@ -95,15 +102,21 @@ impl FromStr for ExtXSessionData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let data_id = track_assert_some!(data_id, ErrorKind::InvalidInput);
|
let data_id = data_id.ok_or(Error::missing_value("EXT-X-DATA-ID"))?;
|
||||||
let data = if let Some(value) = session_value {
|
let data = {
|
||||||
track_assert_eq!(uri, None, ErrorKind::InvalidInput);
|
if let Some(value) = session_value {
|
||||||
SessionData::Value(value)
|
if uri.is_some() {
|
||||||
} else if let Some(uri) = uri {
|
return Err(Error::invalid_input());
|
||||||
SessionData::Uri(uri)
|
} else {
|
||||||
} else {
|
SessionData::Value(value)
|
||||||
track_panic!(ErrorKind::InvalidInput);
|
}
|
||||||
|
} else if let Some(uri) = uri {
|
||||||
|
SessionData::Uri(uri)
|
||||||
|
} else {
|
||||||
|
return Err(Error::invalid_input());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(ExtXSessionData {
|
Ok(ExtXSessionData {
|
||||||
data_id,
|
data_id,
|
||||||
data,
|
data,
|
||||||
|
@ -117,35 +130,38 @@ mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ext_x_session_data() {
|
fn test_display() {
|
||||||
let tag = ExtXSessionData::new(
|
let tag = ExtXSessionData::new("foo", SessionData::Value("bar".into()));
|
||||||
quoted_string("foo"),
|
|
||||||
SessionData::Value(quoted_string("bar")),
|
|
||||||
);
|
|
||||||
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar""#;
|
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar""#;
|
||||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
|
||||||
assert_eq!(tag.to_string(), text);
|
assert_eq!(tag.to_string(), text);
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
|
||||||
|
|
||||||
let tag =
|
let tag = ExtXSessionData::new("foo", SessionData::Uri("bar".into()));
|
||||||
ExtXSessionData::new(quoted_string("foo"), SessionData::Uri(quoted_string("bar")));
|
|
||||||
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",URI="bar""#;
|
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",URI="bar""#;
|
||||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
|
||||||
assert_eq!(tag.to_string(), text);
|
assert_eq!(tag.to_string(), text);
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
|
||||||
|
|
||||||
let tag = ExtXSessionData::with_language(
|
let tag = ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz");
|
||||||
quoted_string("foo"),
|
|
||||||
SessionData::Value(quoted_string("bar")),
|
|
||||||
quoted_string("baz"),
|
|
||||||
);
|
|
||||||
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar",LANGUAGE="baz""#;
|
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar",LANGUAGE="baz""#;
|
||||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
|
||||||
assert_eq!(tag.to_string(), text);
|
assert_eq!(tag.to_string(), text);
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn quoted_string(s: &str) -> QuotedString {
|
#[test]
|
||||||
QuotedString::new(s).unwrap()
|
fn test_parser() {
|
||||||
|
let tag = ExtXSessionData::new("foo", SessionData::Value("bar".into()));
|
||||||
|
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar""#;
|
||||||
|
assert_eq!(text.parse::<ExtXSessionData>().unwrap(), tag);
|
||||||
|
|
||||||
|
let tag = ExtXSessionData::new("foo", SessionData::Uri("bar".into()));
|
||||||
|
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",URI="bar""#;
|
||||||
|
assert_eq!(text.parse::<ExtXSessionData>().unwrap(), tag);
|
||||||
|
|
||||||
|
let tag = ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz");
|
||||||
|
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar",LANGUAGE="baz""#;
|
||||||
|
assert_eq!(text.parse::<ExtXSessionData>().unwrap(), tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_requires_version() {
|
||||||
|
let tag = ExtXSessionData::new("foo", SessionData::Value("bar".into()));
|
||||||
|
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,75 +1,148 @@
|
||||||
use crate::types::{DecryptionKey, ProtocolVersion};
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::ops::{Deref, DerefMut};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::types::{DecryptionKey, EncryptionMethod, ProtocolVersion};
|
||||||
|
use crate::utils::tag;
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// [4.3.4.5. EXT-X-SESSION-KEY]
|
/// [4.3.4.5. EXT-X-SESSION-KEY]
|
||||||
///
|
///
|
||||||
/// [4.3.4.5. EXT-X-SESSION-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5
|
/// [4.3.4.5. EXT-X-SESSION-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct ExtXSessionKey {
|
pub struct ExtXSessionKey(DecryptionKey);
|
||||||
key: DecryptionKey,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ExtXSessionKey {
|
impl ExtXSessionKey {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-KEY:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-KEY:";
|
||||||
|
|
||||||
/// Makes a new `ExtXSessionKey` tag.
|
/// Makes a new [ExtXSessionKey] tag.
|
||||||
pub fn new(key: DecryptionKey) -> Self {
|
/// # Panic
|
||||||
ExtXSessionKey { key }
|
/// This method will panic, if the [EncryptionMethod] is None.
|
||||||
}
|
pub fn new(method: EncryptionMethod, uri: Url) -> Self {
|
||||||
|
if method == EncryptionMethod::None {
|
||||||
|
panic!("The EncryptionMethod is not allowed to be None");
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns a decryption key for the playlist.
|
Self(DecryptionKey::new(method, uri))
|
||||||
pub fn key(&self) -> &DecryptionKey {
|
|
||||||
&self.key
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// use hls_m3u8::tags::ExtXSessionKey;
|
||||||
|
/// use hls_m3u8::types::{EncryptionMethod, ProtocolVersion};
|
||||||
|
///
|
||||||
|
/// let mut key = ExtXSessionKey::new(
|
||||||
|
/// EncryptionMethod::Aes128,
|
||||||
|
/// "https://www.example.com".parse().unwrap()
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// assert_eq!(
|
||||||
|
/// key.requires_version(),
|
||||||
|
/// ProtocolVersion::V1
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
pub fn requires_version(&self) -> ProtocolVersion {
|
pub fn requires_version(&self) -> ProtocolVersion {
|
||||||
self.key.requires_version()
|
if self.0.key_format.is_some() | self.0.key_format_versions.is_some() {
|
||||||
|
ProtocolVersion::V5
|
||||||
|
} else if self.0.iv.is_some() {
|
||||||
|
ProtocolVersion::V2
|
||||||
|
} else {
|
||||||
|
ProtocolVersion::V1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXSessionKey {
|
impl fmt::Display for ExtXSessionKey {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(f, "{}{}", Self::PREFIX, self.key)
|
write!(f, "{}{}", Self::PREFIX, self.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXSessionKey {
|
impl FromStr for ExtXSessionKey {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
let suffix = s.split_at(Self::PREFIX.len()).1;
|
let input = tag(input, Self::PREFIX)?;
|
||||||
let key = track!(suffix.parse())?;
|
Ok(Self(input.parse()?))
|
||||||
Ok(ExtXSessionKey { key })
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for ExtXSessionKey {
|
||||||
|
type Target = DecryptionKey;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DerefMut for ExtXSessionKey {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::types::{EncryptionMethod, InitializationVector, QuotedString};
|
use crate::types::EncryptionMethod;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ext_x_session_key() {
|
fn test_display() {
|
||||||
let tag = ExtXSessionKey::new(DecryptionKey {
|
let mut key = ExtXSessionKey::new(
|
||||||
method: EncryptionMethod::Aes128,
|
EncryptionMethod::Aes128,
|
||||||
uri: quoted_string("foo"),
|
"https://www.example.com/hls-key/key.bin".parse().unwrap(),
|
||||||
iv: Some(InitializationVector([
|
);
|
||||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
|
key.set_iv([
|
||||||
])),
|
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
||||||
key_format: None,
|
]);
|
||||||
key_format_versions: None,
|
|
||||||
});
|
assert_eq!(
|
||||||
let text =
|
key.to_string(),
|
||||||
r#"#EXT-X-SESSION-KEY:METHOD=AES-128,URI="foo",IV=0x000102030405060708090a0b0c0d0e0f"#;
|
"#EXT-X-SESSION-KEY:METHOD=AES-128,\
|
||||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
URI=\"https://www.example.com/hls-key/key.bin\",\
|
||||||
assert_eq!(tag.to_string(), text);
|
IV=0x10ef8f758ca555115584bb5b3c687f52"
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V2);
|
.to_string()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn quoted_string(s: &str) -> QuotedString {
|
#[test]
|
||||||
QuotedString::new(s).unwrap()
|
fn test_parser() {
|
||||||
|
assert_eq!(
|
||||||
|
r#"#EXT-X-SESSION-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52""#
|
||||||
|
.parse::<ExtXSessionKey>()
|
||||||
|
.unwrap(),
|
||||||
|
ExtXSessionKey::new(
|
||||||
|
EncryptionMethod::Aes128,
|
||||||
|
"https://priv.example.com/key.php?r=52".parse().unwrap()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut key = ExtXSessionKey::new(
|
||||||
|
EncryptionMethod::Aes128,
|
||||||
|
"https://www.example.com/hls-key/key.bin".parse().unwrap(),
|
||||||
|
);
|
||||||
|
key.set_iv([
|
||||||
|
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
"#EXT-X-SESSION-KEY:METHOD=AES-128,\
|
||||||
|
URI=\"https://www.example.com/hls-key/key.bin\",\
|
||||||
|
IV=0X10ef8f758ca555115584bb5b3c687f52"
|
||||||
|
.parse::<ExtXSessionKey>()
|
||||||
|
.unwrap(),
|
||||||
|
key
|
||||||
|
);
|
||||||
|
|
||||||
|
key.set_key_format("baz");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
r#"#EXT-X-SESSION-KEY:METHOD=AES-128,URI="https://www.example.com/hls-key/key.bin",IV=0x10ef8f758ca555115584bb5b3c687f52,KEYFORMAT="baz""#
|
||||||
|
.parse::<ExtXSessionKey>().unwrap(),
|
||||||
|
key
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,30 @@
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use crate::attribute::AttributePairs;
|
use crate::attribute::AttributePairs;
|
||||||
use crate::types::{
|
use crate::types::{
|
||||||
ClosedCaptions, DecimalFloatingPoint, DecimalResolution, HdcpLevel, ProtocolVersion,
|
ClosedCaptions, DecimalFloatingPoint, DecimalResolution, HdcpLevel, ProtocolVersion,
|
||||||
QuotedString, SingleLineString,
|
|
||||||
};
|
};
|
||||||
use crate::utils::parse_u64;
|
use crate::utils::{parse_u64, quote, tag, unquote};
|
||||||
use crate::{Error, ErrorKind, Result};
|
use crate::Error;
|
||||||
use std::fmt;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
/// [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
|
/// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct ExtXStreamInf {
|
pub struct ExtXStreamInf {
|
||||||
uri: SingleLineString,
|
uri: Url,
|
||||||
bandwidth: u64,
|
bandwidth: u64,
|
||||||
average_bandwidth: Option<u64>,
|
average_bandwidth: Option<u64>,
|
||||||
codecs: Option<QuotedString>,
|
codecs: Option<String>,
|
||||||
resolution: Option<DecimalResolution>,
|
resolution: Option<DecimalResolution>,
|
||||||
frame_rate: Option<DecimalFloatingPoint>,
|
frame_rate: Option<DecimalFloatingPoint>,
|
||||||
hdcp_level: Option<HdcpLevel>,
|
hdcp_level: Option<HdcpLevel>,
|
||||||
audio: Option<QuotedString>,
|
audio: Option<String>,
|
||||||
video: Option<QuotedString>,
|
video: Option<String>,
|
||||||
subtitles: Option<QuotedString>,
|
subtitles: Option<String>,
|
||||||
closed_captions: Option<ClosedCaptions>,
|
closed_captions: Option<ClosedCaptions>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,7 +32,7 @@ impl ExtXStreamInf {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-STREAM-INF:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-STREAM-INF:";
|
||||||
|
|
||||||
/// Makes a new `ExtXStreamInf` tag.
|
/// Makes a new `ExtXStreamInf` tag.
|
||||||
pub fn new(uri: SingleLineString, bandwidth: u64) -> Self {
|
pub const fn new(uri: Url, bandwidth: u64) -> Self {
|
||||||
ExtXStreamInf {
|
ExtXStreamInf {
|
||||||
uri,
|
uri,
|
||||||
bandwidth,
|
bandwidth,
|
||||||
|
@ -47,52 +49,57 @@ impl ExtXStreamInf {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the URI that identifies the associated media playlist.
|
/// Returns the URI that identifies the associated media playlist.
|
||||||
pub fn uri(&self) -> &SingleLineString {
|
pub const fn uri(&self) -> &Url {
|
||||||
&self.uri
|
&self.uri
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the peak segment bit rate of the variant stream.
|
/// Returns the peak segment bit rate of the variant stream.
|
||||||
pub fn bandwidth(&self) -> u64 {
|
pub const fn bandwidth(&self) -> u64 {
|
||||||
self.bandwidth
|
self.bandwidth
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the average segment bit rate of the variant stream.
|
/// Returns the average segment bit rate of the variant stream.
|
||||||
pub fn average_bandwidth(&self) -> Option<u64> {
|
pub const fn average_bandwidth(&self) -> Option<u64> {
|
||||||
self.average_bandwidth
|
self.average_bandwidth
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a string that represents the list of codec types contained the variant stream.
|
/// Returns a string that represents the list of codec types contained the variant stream.
|
||||||
pub fn codecs(&self) -> Option<&QuotedString> {
|
pub fn codecs(&self) -> Option<&String> {
|
||||||
self.codecs.as_ref()
|
self.codecs.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the optimal pixel resolution at which to display all the video in the variant stream.
|
/// Returns the optimal pixel resolution at which to display all the video in the variant
|
||||||
pub fn resolution(&self) -> Option<DecimalResolution> {
|
/// stream.
|
||||||
self.resolution
|
pub fn resolution(&self) -> Option<(usize, usize)> {
|
||||||
|
if let Some(res) = &self.resolution {
|
||||||
|
Some((res.width(), res.height()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the maximum frame rate for all the video in the variant stream.
|
/// Returns the maximum frame rate for all the video in the variant stream.
|
||||||
pub fn frame_rate(&self) -> Option<DecimalFloatingPoint> {
|
pub fn frame_rate(&self) -> Option<f64> {
|
||||||
self.frame_rate
|
self.frame_rate.map_or(None, |v| Some(v.as_f64()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the HDCP level of the variant stream.
|
/// Returns the HDCP level of the variant stream.
|
||||||
pub fn hdcp_level(&self) -> Option<HdcpLevel> {
|
pub const fn hdcp_level(&self) -> Option<HdcpLevel> {
|
||||||
self.hdcp_level
|
self.hdcp_level
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the group identifier for the audio in the variant stream.
|
/// Returns the group identifier for the audio in the variant stream.
|
||||||
pub fn audio(&self) -> Option<&QuotedString> {
|
pub fn audio(&self) -> Option<&String> {
|
||||||
self.audio.as_ref()
|
self.audio.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the group identifier for the video in the variant stream.
|
/// Returns the group identifier for the video in the variant stream.
|
||||||
pub fn video(&self) -> Option<&QuotedString> {
|
pub fn video(&self) -> Option<&String> {
|
||||||
self.video.as_ref()
|
self.video.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the group identifier for the subtitles in the variant stream.
|
/// Returns the group identifier for the subtitles in the variant stream.
|
||||||
pub fn subtitles(&self) -> Option<&QuotedString> {
|
pub fn subtitles(&self) -> Option<&String> {
|
||||||
self.subtitles.as_ref()
|
self.subtitles.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,7 +109,7 @@ impl ExtXStreamInf {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
pub fn requires_version(&self) -> ProtocolVersion {
|
pub const fn requires_version(&self) -> ProtocolVersion {
|
||||||
ProtocolVersion::V1
|
ProtocolVersion::V1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,32 +118,32 @@ impl fmt::Display for ExtXStreamInf {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
write!(f, "BANDWIDTH={}", self.bandwidth)?;
|
write!(f, "BANDWIDTH={}", self.bandwidth)?;
|
||||||
if let Some(ref x) = self.average_bandwidth {
|
if let Some(value) = &self.average_bandwidth {
|
||||||
write!(f, ",AVERAGE-BANDWIDTH={}", x)?;
|
write!(f, ",AVERAGE-BANDWIDTH={}", value)?;
|
||||||
}
|
}
|
||||||
if let Some(ref x) = self.codecs {
|
if let Some(value) = &self.resolution {
|
||||||
write!(f, ",CODECS={}", x)?;
|
write!(f, ",RESOLUTION={}", value)?;
|
||||||
}
|
}
|
||||||
if let Some(ref x) = self.resolution {
|
if let Some(value) = &self.codecs {
|
||||||
write!(f, ",RESOLUTION={}", x)?;
|
write!(f, ",CODECS={}", quote(value))?;
|
||||||
}
|
}
|
||||||
if let Some(ref x) = self.frame_rate {
|
if let Some(value) = &self.frame_rate {
|
||||||
write!(f, ",FRAME-RATE={:.3}", x.as_f64())?;
|
write!(f, ",FRAME-RATE={:.3}", value.as_f64())?;
|
||||||
}
|
}
|
||||||
if let Some(ref x) = self.hdcp_level {
|
if let Some(value) = &self.hdcp_level {
|
||||||
write!(f, ",HDCP-LEVEL={}", x)?;
|
write!(f, ",HDCP-LEVEL={}", value)?;
|
||||||
}
|
}
|
||||||
if let Some(ref x) = self.audio {
|
if let Some(value) = &self.audio {
|
||||||
write!(f, ",AUDIO={}", x)?;
|
write!(f, ",AUDIO={}", quote(value))?;
|
||||||
}
|
}
|
||||||
if let Some(ref x) = self.video {
|
if let Some(value) = &self.video {
|
||||||
write!(f, ",VIDEO={}", x)?;
|
write!(f, ",VIDEO={}", quote(value))?;
|
||||||
}
|
}
|
||||||
if let Some(ref x) = self.subtitles {
|
if let Some(value) = &self.subtitles {
|
||||||
write!(f, ",SUBTITLES={}", x)?;
|
write!(f, ",SUBTITLES={}", quote(value))?;
|
||||||
}
|
}
|
||||||
if let Some(ref x) = self.closed_captions {
|
if let Some(value) = &self.closed_captions {
|
||||||
write!(f, ",CLOSED-CAPTIONS={}", x)?;
|
write!(f, ",CLOSED-CAPTIONS={}", value)?;
|
||||||
}
|
}
|
||||||
write!(f, "\n{}", self.uri)?;
|
write!(f, "\n{}", self.uri)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -145,16 +152,14 @@ impl fmt::Display for ExtXStreamInf {
|
||||||
|
|
||||||
impl FromStr for ExtXStreamInf {
|
impl FromStr for ExtXStreamInf {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
let mut lines = s.splitn(2, '\n');
|
|
||||||
let first_line = lines.next().expect("Never fails").trim_end_matches('\r');
|
|
||||||
let second_line = track_assert_some!(lines.next(), ErrorKind::InvalidInput);
|
|
||||||
|
|
||||||
track_assert!(
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
first_line.starts_with(Self::PREFIX),
|
let mut lines = input.lines();
|
||||||
ErrorKind::InvalidInput
|
let first_line = lines.next().ok_or(Error::missing_value("first_line"))?;
|
||||||
);
|
let uri = lines.next().ok_or(Error::missing_value("second_line"))?;
|
||||||
let uri = track!(SingleLineString::new(second_line))?;
|
|
||||||
|
let first_line = tag(first_line, Self::PREFIX)?;
|
||||||
|
|
||||||
let mut bandwidth = None;
|
let mut bandwidth = None;
|
||||||
let mut average_bandwidth = None;
|
let mut average_bandwidth = None;
|
||||||
let mut codecs = None;
|
let mut codecs = None;
|
||||||
|
@ -165,29 +170,30 @@ impl FromStr for ExtXStreamInf {
|
||||||
let mut video = None;
|
let mut video = None;
|
||||||
let mut subtitles = None;
|
let mut subtitles = None;
|
||||||
let mut closed_captions = None;
|
let mut closed_captions = None;
|
||||||
let attrs = AttributePairs::parse(first_line.split_at(Self::PREFIX.len()).1);
|
|
||||||
for attr in attrs {
|
for (key, value) in first_line.parse::<AttributePairs>()? {
|
||||||
let (key, value) = track!(attr)?;
|
match key.as_str() {
|
||||||
match key {
|
"BANDWIDTH" => bandwidth = Some((parse_u64(value))?),
|
||||||
"BANDWIDTH" => bandwidth = Some(track!(parse_u64(value))?),
|
"AVERAGE-BANDWIDTH" => average_bandwidth = Some((parse_u64(value))?),
|
||||||
"AVERAGE-BANDWIDTH" => average_bandwidth = Some(track!(parse_u64(value))?),
|
"CODECS" => codecs = Some(unquote(value)),
|
||||||
"CODECS" => codecs = Some(track!(value.parse())?),
|
"RESOLUTION" => resolution = Some((value.parse())?),
|
||||||
"RESOLUTION" => resolution = Some(track!(value.parse())?),
|
"FRAME-RATE" => frame_rate = Some((value.parse())?),
|
||||||
"FRAME-RATE" => frame_rate = Some(track!(value.parse())?),
|
"HDCP-LEVEL" => hdcp_level = Some((value.parse())?),
|
||||||
"HDCP-LEVEL" => hdcp_level = Some(track!(value.parse())?),
|
"AUDIO" => audio = Some(unquote(value)),
|
||||||
"AUDIO" => audio = Some(track!(value.parse())?),
|
"VIDEO" => video = Some(unquote(value)),
|
||||||
"VIDEO" => video = Some(track!(value.parse())?),
|
"SUBTITLES" => subtitles = Some(unquote(value)),
|
||||||
"SUBTITLES" => subtitles = Some(track!(value.parse())?),
|
"CLOSED-CAPTIONS" => closed_captions = Some((value.parse())?),
|
||||||
"CLOSED-CAPTIONS" => closed_captions = Some(track!(value.parse())?),
|
|
||||||
_ => {
|
_ => {
|
||||||
// [6.3.1. General Client Responsibilities]
|
// [6.3.1. General Client Responsibilities]
|
||||||
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let bandwidth = track_assert_some!(bandwidth, ErrorKind::InvalidInput);
|
|
||||||
|
let bandwidth = bandwidth.ok_or(Error::missing_value("EXT-X-BANDWIDTH"))?;
|
||||||
|
|
||||||
Ok(ExtXStreamInf {
|
Ok(ExtXStreamInf {
|
||||||
uri,
|
uri: uri.parse()?,
|
||||||
bandwidth,
|
bandwidth,
|
||||||
average_bandwidth,
|
average_bandwidth,
|
||||||
codecs,
|
codecs,
|
||||||
|
@ -207,11 +213,30 @@ mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ext_x_stream_inf() {
|
fn test_parser() {
|
||||||
let tag = ExtXStreamInf::new(SingleLineString::new("foo").unwrap(), 1000);
|
let stream_inf = "#EXT-X-STREAM-INF:BANDWIDTH=1000\nhttp://www.example.com"
|
||||||
let text = "#EXT-X-STREAM-INF:BANDWIDTH=1000\nfoo";
|
.parse::<ExtXStreamInf>()
|
||||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
.unwrap();
|
||||||
assert_eq!(tag.to_string(), text);
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
assert_eq!(
|
||||||
|
stream_inf,
|
||||||
|
ExtXStreamInf::new("http://www.example.com".parse().unwrap(), 1000)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_requires_version() {
|
||||||
|
assert_eq!(
|
||||||
|
ProtocolVersion::V1,
|
||||||
|
ExtXStreamInf::new("http://www.example.com".parse().unwrap(), 1000).requires_version()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_display() {
|
||||||
|
assert_eq!(
|
||||||
|
ExtXStreamInf::new("http://www.example.com".parse().unwrap(), 1000).to_string(),
|
||||||
|
"#EXT-X-STREAM-INF:BANDWIDTH=1000\nhttp://www.example.com/".to_string()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use crate::types::ProtocolVersion;
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use trackable::error::ErrorKindExt;
|
|
||||||
|
use crate::types::ProtocolVersion;
|
||||||
|
use crate::utils::tag;
|
||||||
|
|
||||||
/// [4.3.3.3. EXT-X-DISCONTINUITY-SEQUENCE]
|
/// [4.3.3.3. EXT-X-DISCONTINUITY-SEQUENCE]
|
||||||
///
|
///
|
||||||
|
@ -16,18 +16,18 @@ impl ExtXDiscontinuitySequence {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY-SEQUENCE:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY-SEQUENCE:";
|
||||||
|
|
||||||
/// Makes a new `ExtXDiscontinuitySequence` tag.
|
/// Makes a new `ExtXDiscontinuitySequence` tag.
|
||||||
pub fn new(seq_num: u64) -> Self {
|
pub const fn new(seq_num: u64) -> Self {
|
||||||
ExtXDiscontinuitySequence { seq_num }
|
ExtXDiscontinuitySequence { seq_num }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the discontinuity sequence number of
|
/// Returns the discontinuity sequence number of
|
||||||
/// the first media segment that appears in the associated playlist.
|
/// the first media segment that appears in the associated playlist.
|
||||||
pub fn seq_num(self) -> u64 {
|
pub const fn seq_num(self) -> u64 {
|
||||||
self.seq_num
|
self.seq_num
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
pub fn requires_version(self) -> ProtocolVersion {
|
pub const fn requires_version(self) -> ProtocolVersion {
|
||||||
ProtocolVersion::V1
|
ProtocolVersion::V1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,11 +39,11 @@ impl fmt::Display for ExtXDiscontinuitySequence {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXDiscontinuitySequence {
|
impl FromStr for ExtXDiscontinuitySequence {
|
||||||
type Err = Error;
|
type Err = crate::Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
let seq_num = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?;
|
let seq_num = tag(input, Self::PREFIX)?.parse().unwrap(); // TODO!
|
||||||
Ok(ExtXDiscontinuitySequence { seq_num })
|
Ok(Self::new(seq_num))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
use crate::types::ProtocolVersion;
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::types::ProtocolVersion;
|
||||||
|
use crate::utils::tag;
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// [4.3.3.4. EXT-X-ENDLIST]
|
/// [4.3.3.4. EXT-X-ENDLIST]
|
||||||
///
|
///
|
||||||
/// [4.3.3.4. EXT-X-ENDLIST]: https://tools.ietf.org/html/rfc8216#section-4.3.3.4
|
/// [4.3.3.4. EXT-X-ENDLIST]: https://tools.ietf.org/html/rfc8216#section-4.3.3.4
|
||||||
|
@ -12,19 +14,22 @@ impl ExtXEndList {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-ENDLIST";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-ENDLIST";
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
pub fn requires_version(self) -> ProtocolVersion {
|
pub const fn requires_version(self) -> ProtocolVersion {
|
||||||
ProtocolVersion::V1
|
ProtocolVersion::V1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXEndList {
|
impl fmt::Display for ExtXEndList {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
Self::PREFIX.fmt(f)
|
Self::PREFIX.fmt(f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXEndList {
|
impl FromStr for ExtXEndList {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput);
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
|
tag(input, Self::PREFIX)?;
|
||||||
Ok(ExtXEndList)
|
Ok(ExtXEndList)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
use crate::types::ProtocolVersion;
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::types::ProtocolVersion;
|
||||||
|
use crate::utils::tag;
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// [4.3.3.6. EXT-X-I-FRAMES-ONLY]
|
/// [4.3.3.6. EXT-X-I-FRAMES-ONLY]
|
||||||
///
|
///
|
||||||
/// [4.3.3.6. EXT-X-I-FRAMES-ONLY]: https://tools.ietf.org/html/rfc8216#section-4.3.3.6
|
/// [4.3.3.6. EXT-X-I-FRAMES-ONLY]: https://tools.ietf.org/html/rfc8216#section-4.3.3.6
|
||||||
|
@ -13,7 +15,7 @@ impl ExtXIFramesOnly {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAMES-ONLY";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAMES-ONLY";
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
pub fn requires_version(self) -> ProtocolVersion {
|
pub const fn requires_version(self) -> ProtocolVersion {
|
||||||
ProtocolVersion::V4
|
ProtocolVersion::V4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,8 +28,9 @@ impl fmt::Display for ExtXIFramesOnly {
|
||||||
|
|
||||||
impl FromStr for ExtXIFramesOnly {
|
impl FromStr for ExtXIFramesOnly {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput);
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
|
tag(input, Self::PREFIX)?;
|
||||||
Ok(ExtXIFramesOnly)
|
Ok(ExtXIFramesOnly)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
use crate::types::ProtocolVersion;
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use trackable::error::ErrorKindExt;
|
|
||||||
|
use crate::types::ProtocolVersion;
|
||||||
|
use crate::utils::tag;
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// [4.3.3.2. EXT-X-MEDIA-SEQUENCE]
|
/// [4.3.3.2. EXT-X-MEDIA-SEQUENCE]
|
||||||
///
|
///
|
||||||
|
@ -16,17 +17,17 @@ impl ExtXMediaSequence {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA-SEQUENCE:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA-SEQUENCE:";
|
||||||
|
|
||||||
/// Makes a new `ExtXMediaSequence` tag.
|
/// Makes a new `ExtXMediaSequence` tag.
|
||||||
pub fn new(seq_num: u64) -> Self {
|
pub const fn new(seq_num: u64) -> Self {
|
||||||
ExtXMediaSequence { seq_num }
|
ExtXMediaSequence { seq_num }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the sequence number of the first media segment that appears in the associated playlist.
|
/// Returns the sequence number of the first media segment that appears in the associated playlist.
|
||||||
pub fn seq_num(self) -> u64 {
|
pub const fn seq_num(self) -> u64 {
|
||||||
self.seq_num
|
self.seq_num
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
pub fn requires_version(self) -> ProtocolVersion {
|
pub const fn requires_version(self) -> ProtocolVersion {
|
||||||
ProtocolVersion::V1
|
ProtocolVersion::V1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,10 +40,11 @@ impl fmt::Display for ExtXMediaSequence {
|
||||||
|
|
||||||
impl FromStr for ExtXMediaSequence {
|
impl FromStr for ExtXMediaSequence {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
let seq_num = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?;
|
let seq_num = tag(input, Self::PREFIX)?.parse()?;
|
||||||
Ok(ExtXMediaSequence { seq_num })
|
|
||||||
|
Ok(ExtXMediaSequence::new(seq_num))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,48 +1,62 @@
|
||||||
use crate::types::{PlaylistType, ProtocolVersion};
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use trackable::error::ErrorKindExt;
|
|
||||||
|
|
||||||
/// [4.3.3.5. EXT-X-PLAYLIST-TYPE]
|
use crate::types::ProtocolVersion;
|
||||||
|
use crate::utils::tag;
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
|
/// [4.3.3.5. EXT-X-PLAYLIST-TYPE](https://tools.ietf.org/html/rfc8216#section-4.3.3.5)
|
||||||
///
|
///
|
||||||
/// [4.3.3.5. EXT-X-PLAYLIST-TYPE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.5
|
/// The EXT-X-PLAYLIST-TYPE tag provides mutability information about the
|
||||||
|
/// Media Playlist. It applies to the entire Media Playlist.
|
||||||
|
/// It is OPTIONAL. Its format is:
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// #EXT-X-PLAYLIST-TYPE:<type-enum>
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
/// If the EXT-X-PLAYLIST-TYPE tag is omitted from a Media Playlist, the
|
||||||
|
/// Playlist can be updated according to the rules in Section 6.2.1 with
|
||||||
|
/// no additional restrictions.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct ExtXPlaylistType {
|
pub enum ExtXPlaylistType {
|
||||||
playlist_type: PlaylistType,
|
/// If the ExtXPlaylistType is Event, Media Segments can only be added to
|
||||||
|
/// the end of the Media Playlist.
|
||||||
|
Event,
|
||||||
|
/// If the ExtXPlaylistType is Video On Demand (Vod),
|
||||||
|
/// the Media Playlist cannot change.
|
||||||
|
Vod,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExtXPlaylistType {
|
impl ExtXPlaylistType {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-PLAYLIST-TYPE:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-PLAYLIST-TYPE:";
|
||||||
|
|
||||||
/// Makes a new `ExtXPlaylistType` tag.
|
|
||||||
pub fn new(playlist_type: PlaylistType) -> Self {
|
|
||||||
ExtXPlaylistType { playlist_type }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the type of the associated media playlist.
|
|
||||||
pub fn playlist_type(self) -> PlaylistType {
|
|
||||||
self.playlist_type
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
pub fn requires_version(self) -> ProtocolVersion {
|
pub const fn requires_version(&self) -> ProtocolVersion {
|
||||||
ProtocolVersion::V1
|
ProtocolVersion::V1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXPlaylistType {
|
impl fmt::Display for ExtXPlaylistType {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(f, "{}{}", Self::PREFIX, self.playlist_type)
|
match &self {
|
||||||
|
Self::Event => write!(f, "{}EVENT", Self::PREFIX),
|
||||||
|
Self::Vod => write!(f, "{}VOD", Self::PREFIX),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXPlaylistType {
|
impl FromStr for ExtXPlaylistType {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
let playlist_type = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?;
|
let input = tag(input, Self::PREFIX)?;
|
||||||
Ok(ExtXPlaylistType { playlist_type })
|
match input {
|
||||||
|
"EVENT" => Ok(Self::Event),
|
||||||
|
"VOD" => Ok(Self::Vod),
|
||||||
|
_ => Err(Error::custom(format!("Unknown playlist type: {:?}", input))),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,11 +65,48 @@ mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ext_x_playlist_type() {
|
fn test_parser() {
|
||||||
let tag = ExtXPlaylistType::new(PlaylistType::Vod);
|
assert_eq!(
|
||||||
let text = "#EXT-X-PLAYLIST-TYPE:VOD";
|
"#EXT-X-PLAYLIST-TYPE:VOD"
|
||||||
assert_eq!(text.parse().ok(), Some(tag));
|
.parse::<ExtXPlaylistType>()
|
||||||
assert_eq!(tag.to_string(), text);
|
.unwrap(),
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
ExtXPlaylistType::Vod,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
"#EXT-X-PLAYLIST-TYPE:EVENT"
|
||||||
|
.parse::<ExtXPlaylistType>()
|
||||||
|
.unwrap(),
|
||||||
|
ExtXPlaylistType::Event,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!("#EXT-X-PLAYLIST-TYPE:H"
|
||||||
|
.parse::<ExtXPlaylistType>()
|
||||||
|
.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_display() {
|
||||||
|
assert_eq!(
|
||||||
|
"#EXT-X-PLAYLIST-TYPE:VOD".to_string(),
|
||||||
|
ExtXPlaylistType::Vod.to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
"#EXT-X-PLAYLIST-TYPE:EVENT".to_string(),
|
||||||
|
ExtXPlaylistType::Event.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_requires_version() {
|
||||||
|
assert_eq!(
|
||||||
|
ExtXPlaylistType::Vod.requires_version(),
|
||||||
|
ProtocolVersion::V1
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ExtXPlaylistType::Event.requires_version(),
|
||||||
|
ProtocolVersion::V1
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
use crate::types::ProtocolVersion;
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use trackable::error::ErrorKindExt;
|
|
||||||
|
use crate::types::ProtocolVersion;
|
||||||
|
use crate::utils::tag;
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// [4.3.3.1. EXT-X-TARGETDURATION]
|
/// [4.3.3.1. EXT-X-TARGETDURATION]
|
||||||
///
|
///
|
||||||
/// [4.3.3.1. EXT-X-TARGETDURATION]: https://tools.ietf.org/html/rfc8216#section-4.3.3.1
|
/// [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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||||
pub struct ExtXTargetDuration {
|
pub struct ExtXTargetDuration {
|
||||||
duration: Duration,
|
duration: Duration,
|
||||||
}
|
}
|
||||||
|
@ -19,18 +20,18 @@ impl ExtXTargetDuration {
|
||||||
/// Makes a new `ExtXTargetduration` tag.
|
/// Makes a new `ExtXTargetduration` tag.
|
||||||
///
|
///
|
||||||
/// Note that the nanoseconds part of the `duration` will be discarded.
|
/// Note that the nanoseconds part of the `duration` will be discarded.
|
||||||
pub fn new(duration: Duration) -> Self {
|
pub const fn new(duration: Duration) -> Self {
|
||||||
let duration = Duration::from_secs(duration.as_secs());
|
let duration = Duration::from_secs(duration.as_secs());
|
||||||
ExtXTargetDuration { duration }
|
ExtXTargetDuration { duration }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the maximum media segment duration in the associated playlist.
|
/// Returns the maximum media segment duration in the associated playlist.
|
||||||
pub fn duration(&self) -> Duration {
|
pub const fn duration(&self) -> Duration {
|
||||||
self.duration
|
self.duration
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
pub fn requires_version(&self) -> ProtocolVersion {
|
pub const fn requires_version(&self) -> ProtocolVersion {
|
||||||
ProtocolVersion::V1
|
ProtocolVersion::V1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,11 +44,11 @@ impl fmt::Display for ExtXTargetDuration {
|
||||||
|
|
||||||
impl FromStr for ExtXTargetDuration {
|
impl FromStr for ExtXTargetDuration {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
let duration = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?;
|
let input = tag(input, Self::PREFIX)?.parse()?;
|
||||||
Ok(ExtXTargetDuration {
|
Ok(ExtXTargetDuration {
|
||||||
duration: Duration::from_secs(duration),
|
duration: Duration::from_secs(input),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,48 +1,96 @@
|
||||||
use crate::types::{ByteRange, ProtocolVersion};
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::ops::Deref;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use trackable::error::ErrorKindExt;
|
|
||||||
|
use crate::types::{ByteRange, ProtocolVersion};
|
||||||
|
use crate::utils::tag;
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// [4.3.2.2. EXT-X-BYTERANGE]
|
/// [4.3.2.2. EXT-X-BYTERANGE]
|
||||||
///
|
///
|
||||||
/// [4.3.2.2. EXT-X-BYTERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.2
|
/// [4.3.2.2. EXT-X-BYTERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.2
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct ExtXByteRange {
|
pub struct ExtXByteRange(ByteRange);
|
||||||
range: ByteRange,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ExtXByteRange {
|
impl ExtXByteRange {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-BYTERANGE:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-BYTERANGE:";
|
||||||
|
|
||||||
/// Makes a new `ExtXByteRange` tag.
|
/// Makes a new `ExtXByteRange` tag.
|
||||||
pub fn new(range: ByteRange) -> Self {
|
/// # Example
|
||||||
ExtXByteRange { range }
|
/// ```
|
||||||
|
/// use hls_m3u8::tags::ExtXByteRange;
|
||||||
|
///
|
||||||
|
/// let byte_range = ExtXByteRange::new(20, Some(5));
|
||||||
|
/// ```
|
||||||
|
pub const fn new(length: usize, start: Option<usize>) -> Self {
|
||||||
|
Self(ByteRange::new(length, start))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the range of the associated media segment.
|
/// Converts the [ExtXByteRange] to a [ByteRange].
|
||||||
pub fn range(&self) -> ByteRange {
|
/// # Example
|
||||||
self.range
|
/// ```
|
||||||
|
/// use hls_m3u8::tags::ExtXByteRange;
|
||||||
|
/// use hls_m3u8::types::ByteRange;
|
||||||
|
///
|
||||||
|
/// let byte_range = ExtXByteRange::new(20, Some(5));
|
||||||
|
/// let range: ByteRange = byte_range.to_range();
|
||||||
|
/// ```
|
||||||
|
pub const fn to_range(&self) -> ByteRange {
|
||||||
|
self.0
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
pub fn requires_version(&self) -> ProtocolVersion {
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// use hls_m3u8::tags::ExtXByteRange;
|
||||||
|
/// use hls_m3u8::types::ProtocolVersion;
|
||||||
|
///
|
||||||
|
/// let byte_range = ExtXByteRange::new(20, Some(5));
|
||||||
|
/// assert_eq!(byte_range.requires_version(), ProtocolVersion::V4);
|
||||||
|
/// ```
|
||||||
|
pub const fn requires_version(&self) -> ProtocolVersion {
|
||||||
ProtocolVersion::V4
|
ProtocolVersion::V4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Deref for ExtXByteRange {
|
||||||
|
type Target = ByteRange;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXByteRange {
|
impl fmt::Display for ExtXByteRange {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(f, "{}{}", Self::PREFIX, self.range)
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
|
write!(f, "{}", self.0)?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXByteRange {
|
impl FromStr for ExtXByteRange {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
let range = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?;
|
let input = tag(input, Self::PREFIX)?;
|
||||||
Ok(ExtXByteRange { range })
|
|
||||||
|
let tokens = input.splitn(2, '@').collect::<Vec<_>>();
|
||||||
|
if tokens.is_empty() {
|
||||||
|
return Err(Error::invalid_input());
|
||||||
|
}
|
||||||
|
|
||||||
|
let length = tokens[0].parse()?;
|
||||||
|
|
||||||
|
let start = {
|
||||||
|
let mut result = None;
|
||||||
|
if tokens.len() == 2 {
|
||||||
|
result = Some(tokens[1].parse()?);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ExtXByteRange::new(length, start))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,21 +99,40 @@ mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ext_x_byterange() {
|
fn test_display() {
|
||||||
let tag = ExtXByteRange::new(ByteRange {
|
let byte_range = ExtXByteRange::new(0, Some(5));
|
||||||
length: 3,
|
assert_eq!(byte_range.to_string(), "#EXT-X-BYTERANGE:0@5".to_string());
|
||||||
start: None,
|
|
||||||
});
|
|
||||||
assert_eq!("#EXT-X-BYTERANGE:3".parse().ok(), Some(tag));
|
|
||||||
assert_eq!(tag.to_string(), "#EXT-X-BYTERANGE:3");
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V4);
|
|
||||||
|
|
||||||
let tag = ExtXByteRange::new(ByteRange {
|
let byte_range = ExtXByteRange::new(99999, Some(2));
|
||||||
length: 3,
|
assert_eq!(
|
||||||
start: Some(5),
|
byte_range.to_string(),
|
||||||
});
|
"#EXT-X-BYTERANGE:99999@2".to_string()
|
||||||
assert_eq!("#EXT-X-BYTERANGE:3@5".parse().ok(), Some(tag));
|
);
|
||||||
assert_eq!(tag.to_string(), "#EXT-X-BYTERANGE:3@5");
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V4);
|
let byte_range = ExtXByteRange::new(99999, None);
|
||||||
|
assert_eq!(byte_range.to_string(), "#EXT-X-BYTERANGE:99999".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse() {
|
||||||
|
let byte_range = ExtXByteRange::new(99999, Some(2));
|
||||||
|
assert_eq!(
|
||||||
|
byte_range,
|
||||||
|
"#EXT-X-BYTERANGE:99999@2".parse::<ExtXByteRange>().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
let byte_range = ExtXByteRange::new(99999, None);
|
||||||
|
assert_eq!(
|
||||||
|
byte_range,
|
||||||
|
"#EXT-X-BYTERANGE:99999".parse::<ExtXByteRange>().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deref() {
|
||||||
|
let byte_range = ExtXByteRange::new(0, Some(22));
|
||||||
|
|
||||||
|
assert_eq!(*byte_range.length(), 0);
|
||||||
|
assert_eq!(*byte_range.start(), Some(22));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,37 +1,75 @@
|
||||||
use crate::attribute::AttributePairs;
|
|
||||||
use crate::types::{DecimalFloatingPoint, ProtocolVersion, QuotedString};
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use chrono::{DateTime, FixedOffset};
|
||||||
|
use getset::{Getters, MutGetters, Setters};
|
||||||
|
|
||||||
|
use crate::attribute::AttributePairs;
|
||||||
|
use crate::types::{DecimalFloatingPoint, ProtocolVersion};
|
||||||
|
use crate::utils::{quote, tag, unquote};
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// [4.3.2.7. EXT-X-DATERANGE]
|
/// [4.3.2.7. EXT-X-DATERANGE]
|
||||||
///
|
///
|
||||||
/// [4.3.2.7. EXT-X-DATERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.7
|
/// [4.3.2.7. EXT-X-DATERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.7
|
||||||
///
|
///
|
||||||
/// TODO: Implement properly
|
/// TODO: Implement properly
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Getters, MutGetters, Setters)]
|
||||||
|
#[get = "pub"]
|
||||||
|
#[set = "pub"]
|
||||||
|
#[get_mut = "pub"]
|
||||||
pub struct ExtXDateRange {
|
pub struct ExtXDateRange {
|
||||||
pub id: QuotedString,
|
/// A string that uniquely identifies a Date Range in the Playlist.
|
||||||
pub class: Option<QuotedString>,
|
/// This attribute is REQUIRED.
|
||||||
pub start_date: QuotedString,
|
id: String,
|
||||||
pub end_date: Option<QuotedString>,
|
/// A client-defined string that specifies some set of attributes and their associated value
|
||||||
pub duration: Option<Duration>,
|
/// semantics. All Date Ranges with the same CLASS attribute value MUST adhere to these
|
||||||
pub planned_duration: Option<Duration>,
|
/// semantics. This attribute is OPTIONAL.
|
||||||
pub scte35_cmd: Option<QuotedString>,
|
class: Option<String>,
|
||||||
pub scte35_out: Option<QuotedString>,
|
/// The date at which the Date Range begins. This attribute is REQUIRED.
|
||||||
pub scte35_in: Option<QuotedString>,
|
start_date: DateTime<FixedOffset>,
|
||||||
pub end_on_next: bool,
|
/// The date at which the Date Range ends. It MUST be equal to or later than the value of the
|
||||||
pub client_attributes: BTreeMap<String, String>,
|
/// START-DATE attribute. This attribute is OPTIONAL.
|
||||||
|
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.
|
||||||
|
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.
|
||||||
|
planned_duration: Option<Duration>,
|
||||||
|
///
|
||||||
|
scte35_cmd: Option<String>,
|
||||||
|
///
|
||||||
|
scte35_out: Option<String>,
|
||||||
|
///
|
||||||
|
scte35_in: Option<String>,
|
||||||
|
/// 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.
|
||||||
|
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>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExtXDateRange {
|
impl ExtXDateRange {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-DATERANGE:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-DATERANGE:";
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
pub fn requires_version(&self) -> ProtocolVersion {
|
pub const fn requires_version(&self) -> ProtocolVersion {
|
||||||
ProtocolVersion::V1
|
ProtocolVersion::V1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,13 +77,13 @@ impl ExtXDateRange {
|
||||||
impl fmt::Display for ExtXDateRange {
|
impl fmt::Display for ExtXDateRange {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
write!(f, "ID={}", self.id)?;
|
write!(f, "ID={}", quote(&self.id))?;
|
||||||
if let Some(ref x) = self.class {
|
if let Some(value) = &self.class {
|
||||||
write!(f, ",CLASS={}", x)?;
|
write!(f, ",CLASS={}", quote(value))?;
|
||||||
}
|
}
|
||||||
write!(f, ",START-DATE={}", self.start_date)?;
|
write!(f, ",START-DATE={}", quote(&self.start_date))?;
|
||||||
if let Some(ref x) = self.end_date {
|
if let Some(value) = &self.end_date {
|
||||||
write!(f, ",END-DATE={}", x)?;
|
write!(f, ",END-DATE={}", quote(value))?;
|
||||||
}
|
}
|
||||||
if let Some(x) = self.duration {
|
if let Some(x) = self.duration {
|
||||||
write!(f, ",DURATION={}", DecimalFloatingPoint::from_duration(x))?;
|
write!(f, ",DURATION={}", DecimalFloatingPoint::from_duration(x))?;
|
||||||
|
@ -57,14 +95,14 @@ impl fmt::Display for ExtXDateRange {
|
||||||
DecimalFloatingPoint::from_duration(x)
|
DecimalFloatingPoint::from_duration(x)
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
if let Some(ref x) = self.scte35_cmd {
|
if let Some(value) = &self.scte35_cmd {
|
||||||
write!(f, ",SCTE35-CMD={}", x)?;
|
write!(f, ",SCTE35-CMD={}", quote(value))?;
|
||||||
}
|
}
|
||||||
if let Some(ref x) = self.scte35_out {
|
if let Some(value) = &self.scte35_out {
|
||||||
write!(f, ",SCTE35-OUT={}", x)?;
|
write!(f, ",SCTE35-OUT={}", quote(value))?;
|
||||||
}
|
}
|
||||||
if let Some(ref x) = self.scte35_in {
|
if let Some(value) = &self.scte35_in {
|
||||||
write!(f, ",SCTE35-IN={}", x)?;
|
write!(f, ",SCTE35-IN={}", quote(value))?;
|
||||||
}
|
}
|
||||||
if self.end_on_next {
|
if self.end_on_next {
|
||||||
write!(f, ",END-ON-NEXT=YES",)?;
|
write!(f, ",END-ON-NEXT=YES",)?;
|
||||||
|
@ -78,8 +116,9 @@ impl fmt::Display for ExtXDateRange {
|
||||||
|
|
||||||
impl FromStr for ExtXDateRange {
|
impl FromStr for ExtXDateRange {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
|
let input = tag(input, Self::PREFIX)?;
|
||||||
|
|
||||||
let mut id = None;
|
let mut id = None;
|
||||||
let mut class = None;
|
let mut class = None;
|
||||||
|
@ -91,28 +130,30 @@ impl FromStr for ExtXDateRange {
|
||||||
let mut scte35_out = None;
|
let mut scte35_out = None;
|
||||||
let mut scte35_in = None;
|
let mut scte35_in = None;
|
||||||
let mut end_on_next = false;
|
let mut end_on_next = false;
|
||||||
|
|
||||||
let mut client_attributes = BTreeMap::new();
|
let mut client_attributes = BTreeMap::new();
|
||||||
let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1);
|
|
||||||
for attr in attrs {
|
for (key, value) in input.parse::<AttributePairs>()? {
|
||||||
let (key, value) = track!(attr)?;
|
match key.as_str() {
|
||||||
match key {
|
"ID" => id = Some(unquote(value)),
|
||||||
"ID" => id = Some(track!(value.parse())?),
|
"CLASS" => class = Some(unquote(value)),
|
||||||
"CLASS" => class = Some(track!(value.parse())?),
|
"START-DATE" => start_date = Some(unquote(value)),
|
||||||
"START-DATE" => start_date = Some(track!(value.parse())?),
|
"END-DATE" => end_date = Some(unquote(value).parse()?),
|
||||||
"END-DATE" => end_date = Some(track!(value.parse())?),
|
|
||||||
"DURATION" => {
|
"DURATION" => {
|
||||||
let seconds: DecimalFloatingPoint = track!(value.parse())?;
|
let seconds: DecimalFloatingPoint = (value.parse())?;
|
||||||
duration = Some(seconds.to_duration());
|
duration = Some(seconds.to_duration());
|
||||||
}
|
}
|
||||||
"PLANNED-DURATION" => {
|
"PLANNED-DURATION" => {
|
||||||
let seconds: DecimalFloatingPoint = track!(value.parse())?;
|
let seconds: DecimalFloatingPoint = (value.parse())?;
|
||||||
planned_duration = Some(seconds.to_duration());
|
planned_duration = Some(seconds.to_duration());
|
||||||
}
|
}
|
||||||
"SCTE35-CMD" => scte35_cmd = Some(track!(value.parse())?),
|
"SCTE35-CMD" => scte35_cmd = Some(unquote(value)),
|
||||||
"SCTE35-OUT" => scte35_out = Some(track!(value.parse())?),
|
"SCTE35-OUT" => scte35_out = Some(unquote(value)),
|
||||||
"SCTE35-IN" => scte35_in = Some(track!(value.parse())?),
|
"SCTE35-IN" => scte35_in = Some(unquote(value)),
|
||||||
"END-ON-NEXT" => {
|
"END-ON-NEXT" => {
|
||||||
track_assert_eq!(value, "YES", ErrorKind::InvalidInput);
|
if value != "YES" {
|
||||||
|
return Err(Error::invalid_input());
|
||||||
|
}
|
||||||
end_on_next = true;
|
end_on_next = true;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
@ -126,10 +167,15 @@ impl FromStr for ExtXDateRange {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let id = track_assert_some!(id, ErrorKind::InvalidInput);
|
let id = id.ok_or(Error::missing_value("EXT-X-ID"))?;
|
||||||
let start_date = track_assert_some!(start_date, ErrorKind::InvalidInput);
|
let start_date = start_date
|
||||||
|
.ok_or(Error::missing_value("EXT-X-START-DATE"))?
|
||||||
|
.parse()?;
|
||||||
|
|
||||||
if end_on_next {
|
if end_on_next {
|
||||||
track_assert!(class.is_some(), ErrorKind::InvalidInput);
|
if class.is_none() {
|
||||||
|
return Err(Error::invalid_input());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(ExtXDateRange {
|
Ok(ExtXDateRange {
|
||||||
id,
|
id,
|
||||||
|
|
|
@ -1,30 +1,36 @@
|
||||||
use crate::types::ProtocolVersion;
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::types::ProtocolVersion;
|
||||||
|
use crate::utils::tag;
|
||||||
|
use crate::{Error, Result};
|
||||||
|
|
||||||
/// [4.3.2.3. EXT-X-DISCONTINUITY]
|
/// [4.3.2.3. EXT-X-DISCONTINUITY]
|
||||||
///
|
///
|
||||||
/// [4.3.2.3. EXT-X-DISCONTINUITY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.3
|
/// [4.3.2.3. EXT-X-DISCONTINUITY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.3
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct ExtXDiscontinuity;
|
pub struct ExtXDiscontinuity;
|
||||||
|
|
||||||
impl ExtXDiscontinuity {
|
impl ExtXDiscontinuity {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY";
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
pub fn requires_version(self) -> ProtocolVersion {
|
pub const fn requires_version(self) -> ProtocolVersion {
|
||||||
ProtocolVersion::V1
|
ProtocolVersion::V1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXDiscontinuity {
|
impl fmt::Display for ExtXDiscontinuity {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
Self::PREFIX.fmt(f)
|
Self::PREFIX.fmt(f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXDiscontinuity {
|
impl FromStr for ExtXDiscontinuity {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput);
|
fn from_str(input: &str) -> Result<Self> {
|
||||||
|
tag(input, Self::PREFIX)?;
|
||||||
Ok(ExtXDiscontinuity)
|
Ok(ExtXDiscontinuity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,60 @@
|
||||||
use crate::types::{DecimalFloatingPoint, ProtocolVersion, SingleLineString};
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use trackable::error::ErrorKindExt;
|
|
||||||
|
|
||||||
/// [4.3.2.1. EXTINF]
|
use crate::types::{DecimalFloatingPoint, ProtocolVersion};
|
||||||
|
use crate::utils::tag;
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
|
/// [4.3.2.1. EXTINF](https://tools.ietf.org/html/rfc8216#section-4.3.2.1)
|
||||||
///
|
///
|
||||||
/// [4.3.2.1. EXTINF]: https://tools.ietf.org/html/rfc8216#section-4.3.2.1
|
/// The [ExtInf] tag specifies the duration of a [Media Segment]. It applies
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
/// only to the next [Media Segment]. This tag is REQUIRED for each [Media Segment].
|
||||||
|
///
|
||||||
|
/// Its format is:
|
||||||
|
/// ```text
|
||||||
|
/// #EXTINF:<duration>,[<title>]
|
||||||
|
/// ```
|
||||||
|
/// The title is an optional informative title about the [Media Segment].
|
||||||
|
///
|
||||||
|
/// [Media Segment]: crate::media_segment::MediaSegment
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// Parsing from a String:
|
||||||
|
/// ```
|
||||||
|
/// use std::time::Duration;
|
||||||
|
/// use hls_m3u8::tags::ExtInf;
|
||||||
|
///
|
||||||
|
/// let ext_inf = "#EXTINF:8,".parse::<ExtInf>().expect("Failed to parse tag!");
|
||||||
|
///
|
||||||
|
/// assert_eq!(ext_inf.duration(), Duration::from_secs(8));
|
||||||
|
/// assert_eq!(ext_inf.title(), None);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Converting to a String:
|
||||||
|
/// ```
|
||||||
|
/// use std::time::Duration;
|
||||||
|
/// use hls_m3u8::tags::ExtInf;
|
||||||
|
///
|
||||||
|
/// let ext_inf = ExtInf::with_title(
|
||||||
|
/// Duration::from_millis(88),
|
||||||
|
/// "title"
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// assert_eq!(ext_inf.duration(), Duration::from_millis(88));
|
||||||
|
/// assert_eq!(ext_inf.to_string(), "#EXTINF:0.088,title".to_string());
|
||||||
|
/// ```
|
||||||
|
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
pub struct ExtInf {
|
pub struct ExtInf {
|
||||||
duration: Duration,
|
duration: Duration,
|
||||||
title: Option<SingleLineString>,
|
title: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExtInf {
|
impl ExtInf {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXTINF:";
|
pub(crate) const PREFIX: &'static str = "#EXTINF:";
|
||||||
|
|
||||||
/// Makes a new `ExtInf` tag.
|
/// Makes a new `ExtInf` tag.
|
||||||
pub fn new(duration: Duration) -> Self {
|
pub const fn new(duration: Duration) -> Self {
|
||||||
ExtInf {
|
ExtInf {
|
||||||
duration,
|
duration,
|
||||||
title: None,
|
title: None,
|
||||||
|
@ -26,20 +62,20 @@ impl ExtInf {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Makes a new `ExtInf` tag with the given title.
|
/// Makes a new `ExtInf` tag with the given title.
|
||||||
pub fn with_title(duration: Duration, title: SingleLineString) -> Self {
|
pub fn with_title<T: ToString>(duration: Duration, title: T) -> Self {
|
||||||
ExtInf {
|
ExtInf {
|
||||||
duration,
|
duration,
|
||||||
title: Some(title),
|
title: Some(title.to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the duration of the associated media segment.
|
/// Returns the duration of the associated media segment.
|
||||||
pub fn duration(&self) -> Duration {
|
pub const fn duration(&self) -> Duration {
|
||||||
self.duration
|
self.duration
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the title of the associated media segment.
|
/// Returns the title of the associated media segment.
|
||||||
pub fn title(&self) -> Option<&SingleLineString> {
|
pub fn title(&self) -> Option<&String> {
|
||||||
self.title.as_ref()
|
self.title.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,10 +95,10 @@ impl fmt::Display for ExtInf {
|
||||||
|
|
||||||
let duration = (self.duration.as_secs() as f64)
|
let duration = (self.duration.as_secs() as f64)
|
||||||
+ (f64::from(self.duration.subsec_nanos()) / 1_000_000_000.0);
|
+ (f64::from(self.duration.subsec_nanos()) / 1_000_000_000.0);
|
||||||
write!(f, "{}", duration)?;
|
write!(f, "{},", duration)?;
|
||||||
|
|
||||||
if let Some(ref title) = self.title {
|
if let Some(value) = &self.title {
|
||||||
write!(f, ",{}", title)?;
|
write!(f, "{}", value)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -70,45 +106,114 @@ impl fmt::Display for ExtInf {
|
||||||
|
|
||||||
impl FromStr for ExtInf {
|
impl FromStr for ExtInf {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
|
||||||
let mut tokens = s.split_at(Self::PREFIX.len()).1.splitn(2, ',');
|
|
||||||
|
|
||||||
let seconds: DecimalFloatingPoint =
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
may_invalid!(tokens.next().expect("Never fails").parse())?;
|
let input = tag(input, Self::PREFIX)?;
|
||||||
let duration = seconds.to_duration();
|
dbg!(&input);
|
||||||
|
let tokens = input.splitn(2, ',').collect::<Vec<_>>();
|
||||||
|
|
||||||
let title = if let Some(title) = tokens.next() {
|
if tokens.len() == 0 {
|
||||||
Some(track!(SingleLineString::new(title))?)
|
return Err(Error::custom(format!(
|
||||||
} else {
|
"failed to parse #EXTINF tag, couldn't split input: {:?}",
|
||||||
None
|
input
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let duration = tokens[0].parse::<DecimalFloatingPoint>()?.to_duration();
|
||||||
|
|
||||||
|
let title = {
|
||||||
|
if tokens.len() >= 2 {
|
||||||
|
if tokens[1].trim().is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(tokens[1].to_string())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(ExtInf { duration, title })
|
Ok(ExtInf { duration, title })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Duration> for ExtInf {
|
||||||
|
fn from(value: Duration) -> Self {
|
||||||
|
Self::new(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn extinf() {
|
fn test_display() {
|
||||||
let tag = ExtInf::new(Duration::from_secs(5));
|
assert_eq!(
|
||||||
assert_eq!("#EXTINF:5".parse().ok(), Some(tag.clone()));
|
"#EXTINF:5,".to_string(),
|
||||||
assert_eq!(tag.to_string(), "#EXTINF:5");
|
ExtInf::new(Duration::from_secs(5)).to_string()
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
|
||||||
|
|
||||||
let tag = ExtInf::with_title(
|
|
||||||
Duration::from_secs(5),
|
|
||||||
SingleLineString::new("foo").unwrap(),
|
|
||||||
);
|
);
|
||||||
assert_eq!("#EXTINF:5,foo".parse().ok(), Some(tag.clone()));
|
assert_eq!(
|
||||||
assert_eq!(tag.to_string(), "#EXTINF:5,foo");
|
"#EXTINF:5.5,".to_string(),
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
ExtInf::new(Duration::from_millis(5500)).to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
"#EXTINF:5.5,title".to_string(),
|
||||||
|
ExtInf::with_title(Duration::from_millis(5500), "title").to_string()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
"#EXTINF:5,title".to_string(),
|
||||||
|
ExtInf::with_title(Duration::from_secs(5), "title").to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let tag = ExtInf::new(Duration::from_millis(1234));
|
#[test]
|
||||||
assert_eq!("#EXTINF:1.234".parse().ok(), Some(tag.clone()));
|
fn test_parser() {
|
||||||
assert_eq!(tag.to_string(), "#EXTINF:1.234");
|
// #EXTINF:<duration>,[<title>]
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V3);
|
assert_eq!(
|
||||||
|
"#EXTINF:5".parse::<ExtInf>().unwrap(),
|
||||||
|
ExtInf::new(Duration::from_secs(5))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
"#EXTINF:5,".parse::<ExtInf>().unwrap(),
|
||||||
|
ExtInf::new(Duration::from_secs(5))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
"#EXTINF:5.5".parse::<ExtInf>().unwrap(),
|
||||||
|
ExtInf::new(Duration::from_millis(5500))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
"#EXTINF:5.5,".parse::<ExtInf>().unwrap(),
|
||||||
|
ExtInf::new(Duration::from_millis(5500))
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
"#EXTINF:5.5,title".parse::<ExtInf>().unwrap(),
|
||||||
|
ExtInf::with_title(Duration::from_millis(5500), "title")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
"#EXTINF:5,title".parse::<ExtInf>().unwrap(),
|
||||||
|
ExtInf::with_title(Duration::from_secs(5), "title")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_title() {
|
||||||
|
assert_eq!(ExtInf::new(Duration::from_secs(5)).title(), None);
|
||||||
|
assert_eq!(
|
||||||
|
ExtInf::with_title(Duration::from_secs(5), "title").title(),
|
||||||
|
Some(&"title".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_requires_version() {
|
||||||
|
assert_eq!(
|
||||||
|
ExtInf::new(Duration::from_secs(4)).requires_version(),
|
||||||
|
ProtocolVersion::V1
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ExtInf::new(Duration::from_millis(4400)).requires_version(),
|
||||||
|
ProtocolVersion::V3
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,130 +1,157 @@
|
||||||
use crate::attribute::AttributePairs;
|
|
||||||
use crate::types::{DecryptionKey, ProtocolVersion};
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::ops::{Deref, DerefMut};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
use crate::types::{DecryptionKey, EncryptionMethod};
|
||||||
|
use crate::utils::tag;
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// [4.3.2.4. EXT-X-KEY]
|
/// [4.3.2.4. EXT-X-KEY]
|
||||||
///
|
///
|
||||||
/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4
|
/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4
|
||||||
|
/// # Note
|
||||||
|
/// In case of an empty key (`EncryptionMethod::None`), all attributes will be ignored.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct ExtXKey {
|
pub struct ExtXKey(DecryptionKey);
|
||||||
key: Option<DecryptionKey>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ExtXKey {
|
impl ExtXKey {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-KEY:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-KEY:";
|
||||||
|
|
||||||
/// Makes a new `ExtXKey` tag.
|
/// Makes a new `ExtXKey` tag.
|
||||||
pub fn new(key: DecryptionKey) -> Self {
|
/// # Example
|
||||||
ExtXKey { key: Some(key) }
|
/// ```
|
||||||
|
/// use url::Url;
|
||||||
|
///
|
||||||
|
/// use hls_m3u8::tags::ExtXKey;
|
||||||
|
/// use hls_m3u8::types::EncryptionMethod;
|
||||||
|
///
|
||||||
|
/// let key = ExtXKey::new(
|
||||||
|
/// EncryptionMethod::Aes128,
|
||||||
|
/// "https://www.example.com".parse().unwrap()
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// assert_eq!(
|
||||||
|
/// key.to_string(),
|
||||||
|
/// "#EXT-X-KEY:METHOD=AES-128,URI=\"https://www.example.com/\""
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
pub const fn new(method: EncryptionMethod, uri: Url) -> Self {
|
||||||
|
Self(DecryptionKey::new(method, uri))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Makes a new `ExtXKey` tag without a decryption key.
|
/// Makes a new `ExtXKey` tag without a decryption key.
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// use hls_m3u8::tags::ExtXKey;
|
||||||
///
|
///
|
||||||
/// This tag has the `METHDO=NONE` attribute.
|
/// let key = ExtXKey::empty();
|
||||||
pub fn new_without_key() -> Self {
|
///
|
||||||
ExtXKey { key: None }
|
/// assert_eq!(
|
||||||
|
/// key.to_string(),
|
||||||
|
/// "#EXT-X-KEY:METHOD=NONE"
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
pub const fn empty() -> Self {
|
||||||
|
Self(DecryptionKey {
|
||||||
|
method: EncryptionMethod::None,
|
||||||
|
uri: None,
|
||||||
|
iv: None,
|
||||||
|
key_format: None,
|
||||||
|
key_format_versions: None,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the decryption key for the following media segments and media initialization sections.
|
/// Returns whether the [EncryptionMethod] is [None](EncryptionMethod::None).
|
||||||
pub fn key(&self) -> Option<&DecryptionKey> {
|
/// # Example
|
||||||
self.key.as_ref()
|
/// ```
|
||||||
}
|
/// use hls_m3u8::tags::ExtXKey;
|
||||||
|
/// use hls_m3u8::types::EncryptionMethod;
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
///
|
||||||
pub fn requires_version(&self) -> ProtocolVersion {
|
/// let key = ExtXKey::empty();
|
||||||
self.key
|
///
|
||||||
.as_ref()
|
/// assert_eq!(
|
||||||
.map_or(ProtocolVersion::V1, |k| k.requires_version())
|
/// key.method() == EncryptionMethod::None,
|
||||||
}
|
/// key.is_empty()
|
||||||
}
|
/// );
|
||||||
|
/// ```
|
||||||
impl fmt::Display for ExtXKey {
|
pub fn is_empty(&self) -> bool {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
self.0.method() == EncryptionMethod::None
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
|
||||||
if let Some(ref key) = self.key {
|
|
||||||
write!(f, "{}", key)?;
|
|
||||||
} else {
|
|
||||||
write!(f, "METHOD=NONE")?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXKey {
|
impl FromStr for ExtXKey {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
|
||||||
let suffix = s.split_at(Self::PREFIX.len()).1;
|
|
||||||
|
|
||||||
if AttributePairs::parse(suffix).any(|a| a.as_ref().ok() == Some(&("METHOD", "NONE"))) {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
for attr in AttributePairs::parse(suffix) {
|
let input = tag(input, Self::PREFIX)?;
|
||||||
let (key, _) = track!(attr)?;
|
Ok(Self(input.parse()?))
|
||||||
track_assert_ne!(key, "URI", ErrorKind::InvalidInput);
|
}
|
||||||
track_assert_ne!(key, "IV", ErrorKind::InvalidInput);
|
}
|
||||||
track_assert_ne!(key, "KEYFORMAT", ErrorKind::InvalidInput);
|
|
||||||
track_assert_ne!(key, "KEYFORMATVERSIONS", ErrorKind::InvalidInput);
|
impl fmt::Display for ExtXKey {
|
||||||
}
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
Ok(ExtXKey { key: None })
|
write!(f, "{}{}", Self::PREFIX, self.0)
|
||||||
} else {
|
}
|
||||||
let key = track!(suffix.parse())?;
|
}
|
||||||
Ok(ExtXKey { key: Some(key) })
|
|
||||||
}
|
impl Deref for ExtXKey {
|
||||||
|
type Target = DecryptionKey;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DerefMut for ExtXKey {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::types::{EncryptionMethod, InitializationVector, QuotedString};
|
use crate::types::EncryptionMethod;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ext_x_key() {
|
fn test_display() {
|
||||||
let tag = ExtXKey::new_without_key();
|
assert_eq!(
|
||||||
let text = "#EXT-X-KEY:METHOD=NONE";
|
ExtXKey::empty().to_string(),
|
||||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
"#EXT-X-KEY:METHOD=NONE".to_string()
|
||||||
assert_eq!(tag.to_string(), text);
|
);
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
|
||||||
|
|
||||||
let tag = ExtXKey::new(DecryptionKey {
|
let mut key = ExtXKey::empty();
|
||||||
method: EncryptionMethod::Aes128,
|
// it is expected, that all attributes will be ignored in an empty key!
|
||||||
uri: QuotedString::new("foo").unwrap(),
|
key.set_key_format("hi");
|
||||||
iv: None,
|
key.set_iv([
|
||||||
key_format: None,
|
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
||||||
key_format_versions: None,
|
]);
|
||||||
});
|
key.set_uri("https://www.example.com".parse().unwrap());
|
||||||
let text = r#"#EXT-X-KEY:METHOD=AES-128,URI="foo""#;
|
key.set_key_format_versions("1/2/3");
|
||||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
|
||||||
assert_eq!(tag.to_string(), text);
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
|
||||||
|
|
||||||
let tag = ExtXKey::new(DecryptionKey {
|
assert_eq!(key.to_string(), "#EXT-X-KEY:METHOD=NONE".to_string());
|
||||||
method: EncryptionMethod::Aes128,
|
}
|
||||||
uri: QuotedString::new("foo").unwrap(),
|
|
||||||
iv: Some(InitializationVector([
|
|
||||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
|
|
||||||
])),
|
|
||||||
key_format: None,
|
|
||||||
key_format_versions: None,
|
|
||||||
});
|
|
||||||
let text = r#"#EXT-X-KEY:METHOD=AES-128,URI="foo",IV=0x000102030405060708090a0b0c0d0e0f"#;
|
|
||||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
|
||||||
assert_eq!(tag.to_string(), text);
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V2);
|
|
||||||
|
|
||||||
let tag = ExtXKey::new(DecryptionKey {
|
#[test]
|
||||||
method: EncryptionMethod::Aes128,
|
fn test_parser() {
|
||||||
uri: QuotedString::new("foo").unwrap(),
|
assert_eq!(
|
||||||
iv: Some(InitializationVector([
|
r#"#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52""#
|
||||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
|
.parse::<ExtXKey>()
|
||||||
])),
|
.unwrap(),
|
||||||
key_format: Some(QuotedString::new("baz").unwrap()),
|
ExtXKey::new(
|
||||||
key_format_versions: None,
|
EncryptionMethod::Aes128,
|
||||||
});
|
"https://priv.example.com/key.php?r=52".parse().unwrap()
|
||||||
let text = r#"#EXT-X-KEY:METHOD=AES-128,URI="foo",IV=0x000102030405060708090a0b0c0d0e0f,KEYFORMAT="baz""#;
|
)
|
||||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
);
|
||||||
assert_eq!(tag.to_string(), text);
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V5);
|
let mut key = ExtXKey::new(
|
||||||
|
EncryptionMethod::Aes128,
|
||||||
|
"https://www.example.com/hls-key/key.bin".parse().unwrap(),
|
||||||
|
);
|
||||||
|
key.set_iv([
|
||||||
|
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
use crate::attribute::AttributePairs;
|
|
||||||
use crate::types::{ByteRange, ProtocolVersion, QuotedString};
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::attribute::AttributePairs;
|
||||||
|
use crate::types::{ByteRange, ProtocolVersion};
|
||||||
|
use crate::utils::{quote, tag, unquote};
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// [4.3.2.5. EXT-X-MAP]
|
/// [4.3.2.5. EXT-X-MAP]
|
||||||
///
|
///
|
||||||
/// [4.3.2.5. EXT-X-MAP]: https://tools.ietf.org/html/rfc8216#section-4.3.2.5
|
/// [4.3.2.5. EXT-X-MAP]: https://tools.ietf.org/html/rfc8216#section-4.3.2.5
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct ExtXMap {
|
pub struct ExtXMap {
|
||||||
uri: QuotedString,
|
uri: String,
|
||||||
range: Option<ByteRange>,
|
range: Option<ByteRange>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,30 +19,33 @@ impl ExtXMap {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-MAP:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-MAP:";
|
||||||
|
|
||||||
/// Makes a new `ExtXMap` tag.
|
/// Makes a new `ExtXMap` tag.
|
||||||
pub fn new(uri: QuotedString) -> Self {
|
pub fn new<T: ToString>(uri: T) -> Self {
|
||||||
ExtXMap { uri, range: None }
|
ExtXMap {
|
||||||
|
uri: uri.to_string(),
|
||||||
|
range: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Makes a new `ExtXMap` tag with the given range.
|
/// Makes a new `ExtXMap` tag with the given range.
|
||||||
pub fn with_range(uri: QuotedString, range: ByteRange) -> Self {
|
pub fn with_range<T: ToString>(uri: T, range: ByteRange) -> Self {
|
||||||
ExtXMap {
|
ExtXMap {
|
||||||
uri,
|
uri: uri.to_string(),
|
||||||
range: Some(range),
|
range: Some(range),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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.
|
||||||
pub fn uri(&self) -> &QuotedString {
|
pub const fn uri(&self) -> &String {
|
||||||
&self.uri
|
&self.uri
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the range of the media initialization section.
|
/// Returns the range of the media initialization section.
|
||||||
pub fn range(&self) -> Option<ByteRange> {
|
pub const fn range(&self) -> Option<ByteRange> {
|
||||||
self.range
|
self.range
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
pub fn requires_version(&self) -> ProtocolVersion {
|
pub const fn requires_version(&self) -> ProtocolVersion {
|
||||||
ProtocolVersion::V6
|
ProtocolVersion::V6
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,9 +53,9 @@ impl ExtXMap {
|
||||||
impl fmt::Display for ExtXMap {
|
impl fmt::Display for ExtXMap {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
write!(f, "URI={}", self.uri)?;
|
write!(f, "URI={}", quote(&self.uri))?;
|
||||||
if let Some(ref x) = self.range {
|
if let Some(value) = &self.range {
|
||||||
write!(f, ",BYTERANGE=\"{}\"", x)?;
|
write!(f, ",BYTERANGE={}", quote(value))?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -58,19 +63,18 @@ impl fmt::Display for ExtXMap {
|
||||||
|
|
||||||
impl FromStr for ExtXMap {
|
impl FromStr for ExtXMap {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
|
let input = tag(input, Self::PREFIX)?;
|
||||||
|
|
||||||
let mut uri = None;
|
let mut uri = None;
|
||||||
let mut range = None;
|
let mut range = None;
|
||||||
let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1);
|
|
||||||
for attr in attrs {
|
for (key, value) in input.parse::<AttributePairs>()? {
|
||||||
let (key, value) = track!(attr)?;
|
match key.as_str() {
|
||||||
match key {
|
"URI" => uri = Some(unquote(value)),
|
||||||
"URI" => uri = Some(track!(value.parse())?),
|
|
||||||
"BYTERANGE" => {
|
"BYTERANGE" => {
|
||||||
let s: QuotedString = track!(value.parse())?;
|
range = Some((unquote(value).parse())?);
|
||||||
range = Some(track!(s.parse())?);
|
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// [6.3.1. General Client Responsibilities]
|
// [6.3.1. General Client Responsibilities]
|
||||||
|
@ -79,7 +83,7 @@ impl FromStr for ExtXMap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let uri = track_assert_some!(uri, ErrorKind::InvalidInput);
|
let uri = uri.ok_or(Error::missing_value("EXT-X-URI"))?;
|
||||||
Ok(ExtXMap { uri, range })
|
Ok(ExtXMap { uri, range })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -90,21 +94,16 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ext_x_map() {
|
fn ext_x_map() {
|
||||||
let tag = ExtXMap::new(QuotedString::new("foo").unwrap());
|
let tag = ExtXMap::new("foo");
|
||||||
let text = r#"#EXT-X-MAP:URI="foo""#;
|
let text = r#"#EXT-X-MAP:URI="foo""#;
|
||||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||||
assert_eq!(tag.to_string(), text);
|
assert_eq!(tag.to_string(), text);
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V6);
|
assert_eq!(tag.requires_version(), ProtocolVersion::V6);
|
||||||
|
|
||||||
let tag = ExtXMap::with_range(
|
let tag = ExtXMap::with_range("foo", ByteRange::new(9, Some(2)));
|
||||||
QuotedString::new("foo").unwrap(),
|
|
||||||
ByteRange {
|
|
||||||
length: 9,
|
|
||||||
start: Some(2),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
let text = r#"#EXT-X-MAP:URI="foo",BYTERANGE="9@2""#;
|
let text = r#"#EXT-X-MAP:URI="foo",BYTERANGE="9@2""#;
|
||||||
track_try_unwrap!(ExtXMap::from_str(text));
|
ExtXMap::from_str(text).unwrap();
|
||||||
|
|
||||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||||
assert_eq!(tag.to_string(), text);
|
assert_eq!(tag.to_string(), text);
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V6);
|
assert_eq!(tag.requires_version(), ProtocolVersion::V6);
|
||||||
|
|
|
@ -1,49 +1,53 @@
|
||||||
use crate::types::{ProtocolVersion, SingleLineString};
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use chrono::{DateTime, FixedOffset};
|
||||||
|
|
||||||
|
use crate::types::ProtocolVersion;
|
||||||
|
use crate::utils::tag;
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]
|
/// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]
|
||||||
///
|
///
|
||||||
/// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]: https://tools.ietf.org/html/rfc8216#section-4.3.2.6
|
/// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]: https://tools.ietf.org/html/rfc8216#section-4.3.2.6
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
pub struct ExtXProgramDateTime {
|
pub struct ExtXProgramDateTime(DateTime<FixedOffset>);
|
||||||
date_time: SingleLineString,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ExtXProgramDateTime {
|
impl ExtXProgramDateTime {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-PROGRAM-DATE-TIME:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-PROGRAM-DATE-TIME:";
|
||||||
|
|
||||||
/// Makes a new `ExtXProgramDateTime` tag.
|
/// Makes a new `ExtXProgramDateTime` tag.
|
||||||
pub fn new(date_time: SingleLineString) -> Self {
|
pub fn new<T: Into<DateTime<FixedOffset>>>(date_time: T) -> Self {
|
||||||
ExtXProgramDateTime { date_time }
|
Self(date_time.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the date-time of the first sample of the associated media segment.
|
/// Returns the date-time of the first sample of the associated media segment.
|
||||||
pub fn date_time(&self) -> &SingleLineString {
|
pub const fn date_time(&self) -> &DateTime<FixedOffset> {
|
||||||
&self.date_time
|
&self.0
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
pub fn requires_version(&self) -> ProtocolVersion {
|
pub const fn requires_version(&self) -> ProtocolVersion {
|
||||||
ProtocolVersion::V1
|
ProtocolVersion::V1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXProgramDateTime {
|
impl fmt::Display for ExtXProgramDateTime {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(f, "{}{}", Self::PREFIX, self.date_time)
|
let date_time = self.0.to_rfc3339();
|
||||||
|
write!(f, "{}{}", Self::PREFIX, date_time)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXProgramDateTime {
|
impl FromStr for ExtXProgramDateTime {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
let suffix = s.split_at(Self::PREFIX.len()).1;
|
let input = tag(input, Self::PREFIX)?;
|
||||||
Ok(ExtXProgramDateTime {
|
|
||||||
date_time: track!(SingleLineString::new(suffix))?,
|
// TODO: parse with chrono
|
||||||
})
|
let date_time = DateTime::parse_from_rfc3339(input)?;
|
||||||
|
Ok(Self::new(date_time))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,12 +56,34 @@ mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ext_x_program_date_time() {
|
fn test_display() {
|
||||||
let text = "#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00";
|
let date_time = "2010-02-19T14:54:23.031+08:00"
|
||||||
assert!(text.parse::<ExtXProgramDateTime>().is_ok());
|
.parse::<DateTime<FixedOffset>>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let tag = text.parse::<ExtXProgramDateTime>().unwrap();
|
let program_date_time = ExtXProgramDateTime::new(date_time);
|
||||||
assert_eq!(tag.to_string(), text);
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
assert_eq!(
|
||||||
|
program_date_time.to_string(),
|
||||||
|
"#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00".to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parser() {
|
||||||
|
"#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00"
|
||||||
|
.parse::<ExtXProgramDateTime>()
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_requires_version() {
|
||||||
|
let date_time = "2010-02-19T14:54:23.031+08:00"
|
||||||
|
.parse::<DateTime<FixedOffset>>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let program_date_time = ExtXProgramDateTime::new(date_time);
|
||||||
|
|
||||||
|
assert_eq!(program_date_time.requires_version(), ProtocolVersion::V1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,6 @@
|
||||||
//!
|
//!
|
||||||
//! [4.3. Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3
|
//! [4.3. Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3
|
||||||
|
|
||||||
macro_rules! may_invalid {
|
|
||||||
($expr:expr) => {
|
|
||||||
$expr.map_err(|e| track!(Error::from(ErrorKind::InvalidInput.cause(e))))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! impl_from {
|
macro_rules! impl_from {
|
||||||
($to:ident, $from:ident) => {
|
($to:ident, $from:ident) => {
|
||||||
impl From<$from> for $to {
|
impl From<$from> for $to {
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
use crate::types::ProtocolVersion;
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::types::ProtocolVersion;
|
||||||
|
use crate::utils::tag;
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS]
|
/// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS]
|
||||||
///
|
///
|
||||||
/// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS]: https://tools.ietf.org/html/rfc8216#section-4.3.5.1
|
/// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS]: https://tools.ietf.org/html/rfc8216#section-4.3.5.1
|
||||||
|
@ -12,7 +14,7 @@ impl ExtXIndependentSegments {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-INDEPENDENT-SEGMENTS";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-INDEPENDENT-SEGMENTS";
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
pub fn requires_version(self) -> ProtocolVersion {
|
pub const fn requires_version(&self) -> ProtocolVersion {
|
||||||
ProtocolVersion::V1
|
ProtocolVersion::V1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,8 +27,9 @@ impl fmt::Display for ExtXIndependentSegments {
|
||||||
|
|
||||||
impl FromStr for ExtXIndependentSegments {
|
impl FromStr for ExtXIndependentSegments {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput);
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
|
tag(input, Self::PREFIX)?;
|
||||||
Ok(ExtXIndependentSegments)
|
Ok(ExtXIndependentSegments)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
use crate::attribute::AttributePairs;
|
|
||||||
use crate::types::{ProtocolVersion, SignedDecimalFloatingPoint};
|
|
||||||
use crate::utils::parse_yes_or_no;
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::attribute::AttributePairs;
|
||||||
|
use crate::types::{ProtocolVersion, SignedDecimalFloatingPoint};
|
||||||
|
use crate::utils::{parse_yes_or_no, tag};
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// [4.3.5.2. EXT-X-START]
|
/// [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
|
/// [4.3.5.2. EXT-X-START]: https://tools.ietf.org/html/rfc8216#section-4.3.5.2
|
||||||
|
@ -18,34 +19,46 @@ impl ExtXStart {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-START:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-START:";
|
||||||
|
|
||||||
/// Makes a new `ExtXStart` tag.
|
/// Makes a new `ExtXStart` tag.
|
||||||
pub fn new(time_offset: SignedDecimalFloatingPoint) -> Self {
|
/// # Panic
|
||||||
|
/// Panics if the time_offset value is infinite.
|
||||||
|
pub fn new(time_offset: f64) -> Self {
|
||||||
|
if time_offset.is_infinite() {
|
||||||
|
panic!("EXT-X-START: Floating point value must be finite!");
|
||||||
|
}
|
||||||
|
|
||||||
ExtXStart {
|
ExtXStart {
|
||||||
time_offset,
|
time_offset: SignedDecimalFloatingPoint::new(time_offset).unwrap(),
|
||||||
precise: false,
|
precise: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Makes a new `ExtXStart` tag with the given `precise` flag.
|
/// Makes a new `ExtXStart` tag with the given `precise` flag.
|
||||||
pub fn with_precise(time_offset: SignedDecimalFloatingPoint, precise: bool) -> Self {
|
/// # Panic
|
||||||
|
/// Panics if the time_offset value is infinite.
|
||||||
|
pub fn with_precise(time_offset: f64, precise: bool) -> Self {
|
||||||
|
if time_offset.is_infinite() {
|
||||||
|
panic!("EXT-X-START: Floating point value must be finite!");
|
||||||
|
}
|
||||||
|
|
||||||
ExtXStart {
|
ExtXStart {
|
||||||
time_offset,
|
time_offset: SignedDecimalFloatingPoint::new(time_offset).unwrap(),
|
||||||
precise,
|
precise,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the time offset of the media segments in the playlist.
|
/// Returns the time offset of the media segments in the playlist.
|
||||||
pub fn time_offset(&self) -> SignedDecimalFloatingPoint {
|
pub const fn time_offset(&self) -> f64 {
|
||||||
self.time_offset
|
self.time_offset.as_f64()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns whether clients should not render media stream whose presentation times are
|
/// Returns whether clients should not render media stream whose presentation times are
|
||||||
/// prior to the specified time offset.
|
/// prior to the specified time offset.
|
||||||
pub fn precise(&self) -> bool {
|
pub const fn precise(&self) -> bool {
|
||||||
self.precise
|
self.precise
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
pub fn requires_version(&self) -> ProtocolVersion {
|
pub const fn requires_version(&self) -> ProtocolVersion {
|
||||||
ProtocolVersion::V1
|
ProtocolVersion::V1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,17 +76,17 @@ impl fmt::Display for ExtXStart {
|
||||||
|
|
||||||
impl FromStr for ExtXStart {
|
impl FromStr for ExtXStart {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
|
let input = tag(input, Self::PREFIX)?;
|
||||||
|
|
||||||
let mut time_offset = None;
|
let mut time_offset = None;
|
||||||
let mut precise = false;
|
let mut precise = false;
|
||||||
let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1);
|
|
||||||
for attr in attrs {
|
for (key, value) in input.parse::<AttributePairs>()? {
|
||||||
let (key, value) = track!(attr)?;
|
match key.as_str() {
|
||||||
match key {
|
"TIME-OFFSET" => time_offset = Some((value.parse())?),
|
||||||
"TIME-OFFSET" => time_offset = Some(track!(value.parse())?),
|
"PRECISE" => precise = (parse_yes_or_no(value))?,
|
||||||
"PRECISE" => precise = track!(parse_yes_or_no(value))?,
|
|
||||||
_ => {
|
_ => {
|
||||||
// [6.3.1. General Client Responsibilities]
|
// [6.3.1. General Client Responsibilities]
|
||||||
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
||||||
|
@ -81,7 +94,8 @@ impl FromStr for ExtXStart {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let time_offset = track_assert_some!(time_offset, ErrorKind::InvalidInput);
|
let time_offset = time_offset.ok_or(Error::missing_value("EXT-X-TIME-OFFSET"))?;
|
||||||
|
|
||||||
Ok(ExtXStart {
|
Ok(ExtXStart {
|
||||||
time_offset,
|
time_offset,
|
||||||
precise,
|
precise,
|
||||||
|
@ -95,13 +109,13 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ext_x_start() {
|
fn ext_x_start() {
|
||||||
let tag = ExtXStart::new(SignedDecimalFloatingPoint::new(-1.23).unwrap());
|
let tag = ExtXStart::new(-1.23);
|
||||||
let text = "#EXT-X-START:TIME-OFFSET=-1.23";
|
let text = "#EXT-X-START:TIME-OFFSET=-1.23";
|
||||||
assert_eq!(text.parse().ok(), Some(tag));
|
assert_eq!(text.parse().ok(), Some(tag));
|
||||||
assert_eq!(tag.to_string(), text);
|
assert_eq!(tag.to_string(), text);
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||||
|
|
||||||
let tag = ExtXStart::with_precise(SignedDecimalFloatingPoint::new(1.23).unwrap(), true);
|
let tag = ExtXStart::with_precise(1.23, true);
|
||||||
let text = "#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES";
|
let text = "#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES";
|
||||||
assert_eq!(text.parse().ok(), Some(tag));
|
assert_eq!(text.parse().ok(), Some(tag));
|
||||||
assert_eq!(tag.to_string(), text);
|
assert_eq!(tag.to_string(), text);
|
||||||
|
|
|
@ -1,18 +1,31 @@
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::{self, FromStr};
|
use std::str::FromStr;
|
||||||
use trackable::error::ErrorKindExt;
|
|
||||||
|
use getset::{Getters, MutGetters, Setters};
|
||||||
|
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// Byte range.
|
/// Byte range.
|
||||||
///
|
///
|
||||||
/// See: [4.3.2.2. EXT-X-BYTERANGE]
|
/// See: [4.3.2.2. EXT-X-BYTERANGE]
|
||||||
///
|
///
|
||||||
/// [4.3.2.2. EXT-X-BYTERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.2
|
/// [4.3.2.2. EXT-X-BYTERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.2
|
||||||
#[allow(missing_docs)]
|
#[derive(Getters, Setters, MutGetters, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[get = "pub"]
|
||||||
|
#[set = "pub"]
|
||||||
|
#[get_mut = "pub"]
|
||||||
pub struct ByteRange {
|
pub struct ByteRange {
|
||||||
pub length: usize,
|
/// The length of the range.
|
||||||
pub start: Option<usize>,
|
length: usize,
|
||||||
|
/// The start of the range.
|
||||||
|
start: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ByteRange {
|
||||||
|
/// Creates a new [ByteRange].
|
||||||
|
pub const fn new(length: usize, start: Option<usize>) -> Self {
|
||||||
|
Self { length, start }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ByteRange {
|
impl fmt::Display for ByteRange {
|
||||||
|
@ -27,20 +40,23 @@ impl fmt::Display for ByteRange {
|
||||||
|
|
||||||
impl FromStr for ByteRange {
|
impl FromStr for ByteRange {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
let mut tokens = s.splitn(2, '@');
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
let length = tokens.next().expect("Never fails");
|
let tokens = s.splitn(2, '@').collect::<Vec<_>>();
|
||||||
let start = if let Some(start) = tokens.next() {
|
if tokens.is_empty() {
|
||||||
Some(track!(start
|
return Err(Error::invalid_input());
|
||||||
.parse()
|
}
|
||||||
.map_err(|e| ErrorKind::InvalidInput.cause(e)))?)
|
|
||||||
} else {
|
let length = tokens[0].parse()?;
|
||||||
None
|
|
||||||
|
let start = {
|
||||||
|
let mut result = None;
|
||||||
|
if tokens.len() == 2 {
|
||||||
|
result = Some(tokens[1].parse()?);
|
||||||
|
}
|
||||||
|
result
|
||||||
};
|
};
|
||||||
Ok(ByteRange {
|
Ok(ByteRange::new(length, start))
|
||||||
length: track!(length.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?,
|
|
||||||
start,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
use crate::types::QuotedString;
|
|
||||||
use crate::{Error, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::{self, FromStr};
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::utils::{quote, unquote};
|
||||||
|
use crate::{Error, Result};
|
||||||
|
|
||||||
/// The identifier of a closed captions group or its absence.
|
/// The identifier of a closed captions group or its absence.
|
||||||
///
|
///
|
||||||
|
@ -11,14 +12,14 @@ use std::str::{self, FromStr};
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub enum ClosedCaptions {
|
pub enum ClosedCaptions {
|
||||||
GroupId(QuotedString),
|
GroupId(String),
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ClosedCaptions {
|
impl fmt::Display for ClosedCaptions {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
match *self {
|
match &self {
|
||||||
ClosedCaptions::GroupId(ref x) => x.fmt(f),
|
ClosedCaptions::GroupId(value) => write!(f, "{}", quote(value)),
|
||||||
ClosedCaptions::None => "NONE".fmt(f),
|
ClosedCaptions::None => "NONE".fmt(f),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,7 +31,7 @@ impl FromStr for ClosedCaptions {
|
||||||
if s == "NONE" {
|
if s == "NONE" {
|
||||||
Ok(ClosedCaptions::None)
|
Ok(ClosedCaptions::None)
|
||||||
} else {
|
} else {
|
||||||
Ok(ClosedCaptions::GroupId(track!(s.parse())?))
|
Ok(ClosedCaptions::GroupId(unquote(s)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,7 +45,7 @@ mod tests {
|
||||||
let closed_captions = ClosedCaptions::None;
|
let closed_captions = ClosedCaptions::None;
|
||||||
assert_eq!(closed_captions.to_string(), "NONE".to_string());
|
assert_eq!(closed_captions.to_string(), "NONE".to_string());
|
||||||
|
|
||||||
let closed_captions = ClosedCaptions::GroupId(QuotedString::new("value").unwrap());
|
let closed_captions = ClosedCaptions::GroupId("value".into());
|
||||||
assert_eq!(closed_captions.to_string(), "\"value\"".to_string());
|
assert_eq!(closed_captions.to_string(), "\"value\"".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +54,7 @@ mod tests {
|
||||||
let closed_captions = ClosedCaptions::None;
|
let closed_captions = ClosedCaptions::None;
|
||||||
assert_eq!(closed_captions, "NONE".parse::<ClosedCaptions>().unwrap());
|
assert_eq!(closed_captions, "NONE".parse::<ClosedCaptions>().unwrap());
|
||||||
|
|
||||||
let closed_captions = ClosedCaptions::GroupId(QuotedString::new("value").unwrap());
|
let closed_captions = ClosedCaptions::GroupId("value".into());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
closed_captions,
|
closed_captions,
|
||||||
"\"value\"".parse::<ClosedCaptions>().unwrap()
|
"\"value\"".parse::<ClosedCaptions>().unwrap()
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::{self, FromStr};
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use trackable::error::ErrorKindExt;
|
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// Non-negative decimal floating-point number.
|
/// Non-negative decimal floating-point number.
|
||||||
///
|
///
|
||||||
|
@ -10,7 +10,7 @@ use trackable::error::ErrorKindExt;
|
||||||
///
|
///
|
||||||
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
|
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
|
||||||
pub struct DecimalFloatingPoint(f64);
|
pub(crate) struct DecimalFloatingPoint(f64);
|
||||||
|
|
||||||
impl DecimalFloatingPoint {
|
impl DecimalFloatingPoint {
|
||||||
/// Makes a new `DecimalFloatingPoint` instance.
|
/// Makes a new `DecimalFloatingPoint` instance.
|
||||||
|
@ -19,14 +19,15 @@ impl DecimalFloatingPoint {
|
||||||
///
|
///
|
||||||
/// The given value must have a positive sign and be finite,
|
/// The given value must have a positive sign and be finite,
|
||||||
/// otherwise this function will return an error that has the kind `ErrorKind::InvalidInput`.
|
/// otherwise this function will return an error that has the kind `ErrorKind::InvalidInput`.
|
||||||
pub fn new(n: f64) -> Result<Self> {
|
pub fn new(n: f64) -> crate::Result<Self> {
|
||||||
track_assert!(n.is_sign_positive(), ErrorKind::InvalidInput);
|
if !n.is_sign_positive() || !n.is_finite() {
|
||||||
track_assert!(n.is_finite(), ErrorKind::InvalidInput);
|
return Err(Error::invalid_input());
|
||||||
|
}
|
||||||
Ok(DecimalFloatingPoint(n))
|
Ok(DecimalFloatingPoint(n))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts `DecimalFloatingPoint` to `f64`.
|
/// Converts `DecimalFloatingPoint` to `f64`.
|
||||||
pub fn as_f64(self) -> f64 {
|
pub const fn as_f64(self) -> f64 {
|
||||||
self.0
|
self.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,13 +60,13 @@ impl fmt::Display for DecimalFloatingPoint {
|
||||||
|
|
||||||
impl FromStr for DecimalFloatingPoint {
|
impl FromStr for DecimalFloatingPoint {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
s.chars().all(|c| c.is_digit(10) || c == '.'),
|
if !input.chars().all(|c| c.is_digit(10) || c == '.') {
|
||||||
ErrorKind::InvalidInput
|
return Err(Error::invalid_input());
|
||||||
);
|
}
|
||||||
let n = track!(s.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?;
|
let n = input.parse()?;
|
||||||
Ok(DecimalFloatingPoint(n))
|
DecimalFloatingPoint::new(n)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::{self, FromStr};
|
use std::str::FromStr;
|
||||||
use trackable::error::ErrorKindExt;
|
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// Decimal resolution.
|
/// Decimal resolution.
|
||||||
///
|
///
|
||||||
|
@ -9,12 +9,38 @@ use trackable::error::ErrorKindExt;
|
||||||
///
|
///
|
||||||
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct DecimalResolution {
|
pub(crate) struct DecimalResolution {
|
||||||
|
width: usize,
|
||||||
|
height: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DecimalResolution {
|
||||||
|
/// Creates a new DecimalResolution.
|
||||||
|
pub const fn new(width: usize, height: usize) -> Self {
|
||||||
|
Self { width, height }
|
||||||
|
}
|
||||||
|
|
||||||
/// Horizontal pixel dimension.
|
/// Horizontal pixel dimension.
|
||||||
pub width: usize,
|
pub const fn width(&self) -> usize {
|
||||||
|
self.width
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets Horizontal pixel dimension.
|
||||||
|
pub fn set_width(&mut self, value: usize) -> &mut Self {
|
||||||
|
self.width = value;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Vertical pixel dimension.
|
/// Vertical pixel dimension.
|
||||||
pub height: usize,
|
pub const fn height(&self) -> usize {
|
||||||
|
self.height
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets Vertical pixel dimension.
|
||||||
|
pub fn set_height(&mut self, value: usize) -> &mut Self {
|
||||||
|
self.height = value;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for DecimalResolution {
|
impl fmt::Display for DecimalResolution {
|
||||||
|
@ -25,13 +51,23 @@ impl fmt::Display for DecimalResolution {
|
||||||
|
|
||||||
impl FromStr for DecimalResolution {
|
impl FromStr for DecimalResolution {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
let mut tokens = s.splitn(2, 'x');
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
let width = tokens.next().expect("Never fails");
|
let tokens = input.splitn(2, 'x').collect::<Vec<_>>();
|
||||||
let height = track_assert_some!(tokens.next(), ErrorKind::InvalidInput);
|
|
||||||
|
if tokens.len() != 2 {
|
||||||
|
return Err(Error::custom(format!(
|
||||||
|
"InvalidInput: Expected input format: [width]x[height] (ex. 1920x1080), got {:?}",
|
||||||
|
input,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let width = tokens[0];
|
||||||
|
let height = tokens[1];
|
||||||
|
|
||||||
Ok(DecimalResolution {
|
Ok(DecimalResolution {
|
||||||
width: track!(width.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?,
|
width: width.parse()?,
|
||||||
height: track!(height.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?,
|
height: height.parse()?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,37 +78,44 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_display() {
|
fn test_display() {
|
||||||
let decimal_resolution = DecimalResolution {
|
assert_eq!(
|
||||||
width: 1920,
|
DecimalResolution::new(1920, 1080).to_string(),
|
||||||
height: 1080,
|
"1920x1080".to_string()
|
||||||
};
|
);
|
||||||
assert_eq!(decimal_resolution.to_string(), "1920x1080".to_string());
|
|
||||||
|
|
||||||
let decimal_resolution = DecimalResolution {
|
assert_eq!(
|
||||||
width: 1280,
|
DecimalResolution::new(1280, 720).to_string(),
|
||||||
height: 720,
|
"1280x720".to_string()
|
||||||
};
|
);
|
||||||
assert_eq!(decimal_resolution.to_string(), "1280x720".to_string());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse() {
|
fn test_parse() {
|
||||||
let decimal_resolution = DecimalResolution {
|
|
||||||
width: 1920,
|
|
||||||
height: 1080,
|
|
||||||
};
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
decimal_resolution,
|
DecimalResolution::new(1920, 1080),
|
||||||
"1920x1080".parse::<DecimalResolution>().unwrap()
|
"1920x1080".parse::<DecimalResolution>().unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
let decimal_resolution = DecimalResolution {
|
|
||||||
width: 1280,
|
|
||||||
height: 720,
|
|
||||||
};
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
decimal_resolution,
|
DecimalResolution::new(1280, 720),
|
||||||
"1280x720".parse::<DecimalResolution>().unwrap()
|
"1280x720".parse::<DecimalResolution>().unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert!("1280".parse::<DecimalResolution>().is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_width() {
|
||||||
|
assert_eq!(DecimalResolution::new(1920, 1080).width(), 1920);
|
||||||
|
assert_eq!(DecimalResolution::new(1920, 1080).set_width(12).width(), 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_height() {
|
||||||
|
assert_eq!(DecimalResolution::new(1920, 1080).height(), 1080);
|
||||||
|
assert_eq!(
|
||||||
|
DecimalResolution::new(1920, 1080).set_height(12).height(),
|
||||||
|
12
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +1,316 @@
|
||||||
use crate::attribute::AttributePairs;
|
|
||||||
use crate::types::{EncryptionMethod, InitializationVector, ProtocolVersion, QuotedString};
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::{self, FromStr};
|
use std::str::FromStr;
|
||||||
|
|
||||||
/// Decryption key.
|
use derive_builder::Builder;
|
||||||
///
|
use url::Url;
|
||||||
/// See: [4.3.2.4. EXT-X-KEY]
|
|
||||||
///
|
use crate::attribute::AttributePairs;
|
||||||
/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4
|
use crate::types::{EncryptionMethod, InitializationVector, ProtocolVersion};
|
||||||
#[allow(missing_docs)]
|
use crate::utils::{quote, unquote};
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
use crate::Error;
|
||||||
|
|
||||||
|
#[derive(Builder, Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
#[builder(setter(into))]
|
||||||
pub struct DecryptionKey {
|
pub struct DecryptionKey {
|
||||||
pub method: EncryptionMethod,
|
pub(crate) method: EncryptionMethod,
|
||||||
pub uri: QuotedString,
|
#[builder(setter(into, strip_option), default)]
|
||||||
pub iv: Option<InitializationVector>,
|
pub(crate) uri: Option<Url>,
|
||||||
pub key_format: Option<QuotedString>,
|
#[builder(setter(into, strip_option), default)]
|
||||||
pub key_format_versions: Option<QuotedString>,
|
pub(crate) iv: Option<InitializationVector>,
|
||||||
|
#[builder(setter(into, strip_option), default)]
|
||||||
|
pub(crate) key_format: Option<String>,
|
||||||
|
#[builder(setter(into, strip_option), default)]
|
||||||
|
pub(crate) key_format_versions: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DecryptionKey {
|
impl DecryptionKey {
|
||||||
pub(crate) fn requires_version(&self) -> ProtocolVersion {
|
/// Makes a new `DecryptionKey`.
|
||||||
if self.key_format.is_some() | self.key_format_versions.is_some() {
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// use url::Url;
|
||||||
|
///
|
||||||
|
/// use hls_m3u8::types::{EncryptionMethod, DecryptionKey};
|
||||||
|
///
|
||||||
|
/// let key = DecryptionKey::new(
|
||||||
|
/// EncryptionMethod::Aes128,
|
||||||
|
/// "https://www.example.com".parse().unwrap()
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// assert_eq!(
|
||||||
|
/// key.to_string(),
|
||||||
|
/// "METHOD=AES-128,URI=\"https://www.example.com/\""
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
pub const fn new(method: EncryptionMethod, uri: Url) -> Self {
|
||||||
|
Self {
|
||||||
|
method,
|
||||||
|
uri: Some(uri),
|
||||||
|
iv: None,
|
||||||
|
key_format: None,
|
||||||
|
key_format_versions: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the [EncryptionMethod].
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod};
|
||||||
|
///
|
||||||
|
/// let key = DecryptionKey::new(
|
||||||
|
/// EncryptionMethod::Aes128,
|
||||||
|
/// "https://www.example.com".parse().unwrap()
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// assert_eq!(
|
||||||
|
/// key.method(),
|
||||||
|
/// EncryptionMethod::Aes128
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
pub const fn method(&self) -> EncryptionMethod {
|
||||||
|
self.method
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a Builder to build a `DecryptionKey`.
|
||||||
|
pub fn builder() -> DecryptionKeyBuilder {
|
||||||
|
DecryptionKeyBuilder::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [EncryptionMethod].
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod};
|
||||||
|
///
|
||||||
|
/// let mut key = DecryptionKey::new(
|
||||||
|
/// EncryptionMethod::Aes128,
|
||||||
|
/// "https://www.example.com".parse().unwrap()
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// key.set_method(EncryptionMethod::SampleAes);
|
||||||
|
///
|
||||||
|
/// assert_eq!(
|
||||||
|
/// key.to_string(),
|
||||||
|
/// "METHOD=SAMPLE-AES,URI=\"https://www.example.com/\"".to_string()
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
pub fn set_method(&mut self, value: EncryptionMethod) {
|
||||||
|
self.method = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an `URI` that specifies how to obtain the key.
|
||||||
|
///
|
||||||
|
/// This attribute is required, if the [EncryptionMethod] is not None.
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod};
|
||||||
|
///
|
||||||
|
/// let key = DecryptionKey::new(
|
||||||
|
/// EncryptionMethod::Aes128,
|
||||||
|
/// "https://www.example.com".parse().unwrap()
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// assert_eq!(
|
||||||
|
/// key.uri(),
|
||||||
|
/// &Some("https://www.example.com".parse().unwrap())
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
pub const fn uri(&self) -> &Option<Url> {
|
||||||
|
&self.uri
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the `URI` attribute.
|
||||||
|
///
|
||||||
|
/// This attribute is required, if the [EncryptionMethod] is not None.
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod};
|
||||||
|
///
|
||||||
|
/// let mut key = DecryptionKey::new(
|
||||||
|
/// EncryptionMethod::Aes128,
|
||||||
|
/// "https://www.example.com".parse().unwrap()
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// key.set_uri("http://www.google.com".parse().unwrap());
|
||||||
|
///
|
||||||
|
/// assert_eq!(
|
||||||
|
/// key.to_string(),
|
||||||
|
/// "METHOD=AES-128,URI=\"http://www.google.com/\"".to_string()
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
pub fn set_uri(&mut self, value: Url) {
|
||||||
|
self.uri = Some(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the IV (Initialization Vector) attribute.
|
||||||
|
///
|
||||||
|
/// This attribute is optional.
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod};
|
||||||
|
///
|
||||||
|
/// let mut key = DecryptionKey::new(
|
||||||
|
/// EncryptionMethod::Aes128,
|
||||||
|
/// "https://www.example.com".parse().unwrap()
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// key.set_iv([
|
||||||
|
/// 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7
|
||||||
|
/// ]);
|
||||||
|
///
|
||||||
|
/// assert_eq!(
|
||||||
|
/// key.iv(),
|
||||||
|
/// Some([1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7])
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
pub fn iv(&self) -> Option<[u8; 16]> {
|
||||||
|
if let Some(iv) = &self.iv {
|
||||||
|
Some(iv.to_slice())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the `IV` attribute.
|
||||||
|
///
|
||||||
|
/// This attribute is optional.
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod};
|
||||||
|
///
|
||||||
|
/// let mut key = DecryptionKey::new(
|
||||||
|
/// EncryptionMethod::Aes128,
|
||||||
|
/// "https://www.example.com".parse().unwrap()
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// key.set_iv([
|
||||||
|
/// 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7
|
||||||
|
/// ]);
|
||||||
|
///
|
||||||
|
/// assert_eq!(
|
||||||
|
/// key.to_string(),
|
||||||
|
/// "METHOD=AES-128,URI=\"https://www.example.com/\",IV=0x01020304050607080901020304050607".to_string()
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
pub fn set_iv<T>(&mut self, value: T)
|
||||||
|
where
|
||||||
|
T: Into<[u8; 16]>,
|
||||||
|
{
|
||||||
|
self.iv = Some(InitializationVector(value.into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a string that specifies how the key is
|
||||||
|
/// represented in the resource identified by the URI.
|
||||||
|
///
|
||||||
|
//// This attribute is optional.
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod};
|
||||||
|
///
|
||||||
|
/// let mut key = DecryptionKey::new(
|
||||||
|
/// EncryptionMethod::Aes128,
|
||||||
|
/// "https://www.example.com".parse().unwrap()
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// key.set_key_format("key_format_attribute");
|
||||||
|
///
|
||||||
|
/// assert_eq!(
|
||||||
|
/// key.key_format(),
|
||||||
|
/// &Some("key_format_attribute".to_string())
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
pub const fn key_format(&self) -> &Option<String> {
|
||||||
|
&self.key_format
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the `KEYFORMAT` attribute.
|
||||||
|
///
|
||||||
|
/// This attribute is optional.
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod};
|
||||||
|
///
|
||||||
|
/// let mut key = DecryptionKey::new(
|
||||||
|
/// EncryptionMethod::Aes128,
|
||||||
|
/// "https://www.example.com".parse().unwrap()
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// key.set_key_format("key_format_attribute");
|
||||||
|
///
|
||||||
|
/// assert_eq!(
|
||||||
|
/// key.to_string(),
|
||||||
|
/// "METHOD=AES-128,URI=\"https://www.example.com/\",KEYFORMAT=\"key_format_attribute\"".to_string()
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
pub fn set_key_format<T: ToString>(&mut self, value: T) {
|
||||||
|
self.key_format = Some(value.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a string containing one or more positive
|
||||||
|
/// integers separated by the "/" character (for example, "1", "1/2",
|
||||||
|
/// or "1/2/5"). If more than one version of a particular `KEYFORMAT`
|
||||||
|
/// is defined, this attribute can be used to indicate which
|
||||||
|
/// version(s) this instance complies with.
|
||||||
|
///
|
||||||
|
/// This attribute is optional.
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod};
|
||||||
|
///
|
||||||
|
/// let mut key = DecryptionKey::new(
|
||||||
|
/// EncryptionMethod::Aes128,
|
||||||
|
/// "https://www.example.com".parse().unwrap()
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// key.set_key_format_versions("1/2/3/4/5");
|
||||||
|
///
|
||||||
|
/// assert_eq!(
|
||||||
|
/// key.key_format_versions(),
|
||||||
|
/// &Some("1/2/3/4/5".to_string())
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
pub const fn key_format_versions(&self) -> &Option<String> {
|
||||||
|
&self.key_format_versions
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the `KEYFORMATVERSIONS` attribute.
|
||||||
|
///
|
||||||
|
/// This attribute is optional.
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod};
|
||||||
|
///
|
||||||
|
/// let mut key = DecryptionKey::new(
|
||||||
|
/// EncryptionMethod::Aes128,
|
||||||
|
/// "https://www.example.com".parse().unwrap()
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// key.set_key_format_versions("1/2/3/4/5");
|
||||||
|
///
|
||||||
|
/// assert_eq!(
|
||||||
|
/// key.to_string(),
|
||||||
|
/// "METHOD=AES-128,URI=\"https://www.example.com/\",KEYFORMATVERSIONS=\"1/2/3/4/5\"".to_string()
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
pub fn set_key_format_versions<T: ToString>(&mut self, value: T) {
|
||||||
|
self.key_format_versions = Some(value.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
|
/// # Example
|
||||||
|
/// ```
|
||||||
|
/// use hls_m3u8::types::{EncryptionMethod, ProtocolVersion, DecryptionKey};
|
||||||
|
///
|
||||||
|
/// let mut key = DecryptionKey::new(
|
||||||
|
/// EncryptionMethod::Aes128,
|
||||||
|
/// "https://www.example.com".parse().unwrap()
|
||||||
|
/// );
|
||||||
|
///
|
||||||
|
/// assert_eq!(
|
||||||
|
/// key.requires_version(),
|
||||||
|
/// ProtocolVersion::V1
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
|
pub fn requires_version(&self) -> ProtocolVersion {
|
||||||
|
if self.key_format.is_some() || self.key_format_versions.is_some() {
|
||||||
ProtocolVersion::V5
|
ProtocolVersion::V5
|
||||||
} else if self.iv.is_some() {
|
} else if self.iv.is_some() {
|
||||||
ProtocolVersion::V2
|
ProtocolVersion::V2
|
||||||
|
@ -31,48 +320,35 @@ impl DecryptionKey {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for DecryptionKey {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "METHOD={}", self.method)?;
|
|
||||||
write!(f, ",URI={}", self.uri)?;
|
|
||||||
if let Some(ref x) = self.iv {
|
|
||||||
write!(f, ",IV={}", x)?;
|
|
||||||
}
|
|
||||||
if let Some(ref x) = self.key_format {
|
|
||||||
write!(f, ",KEYFORMAT={}", x)?;
|
|
||||||
}
|
|
||||||
if let Some(ref x) = self.key_format_versions {
|
|
||||||
write!(f, ",KEYFORMATVERSIONS={}", x)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for DecryptionKey {
|
impl FromStr for DecryptionKey {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
let mut method = None;
|
let mut method = None;
|
||||||
let mut uri = None;
|
let mut uri = None;
|
||||||
let mut iv = None;
|
let mut iv = None;
|
||||||
let mut key_format = None;
|
let mut key_format = None;
|
||||||
let mut key_format_versions = None;
|
let mut key_format_versions = None;
|
||||||
let attrs = AttributePairs::parse(s);
|
|
||||||
for attr in attrs {
|
for (key, value) in input.parse::<AttributePairs>()? {
|
||||||
let (key, value) = track!(attr)?;
|
match key.as_str() {
|
||||||
match key {
|
"METHOD" => method = Some((value.parse())?),
|
||||||
"METHOD" => method = Some(track!(value.parse())?),
|
"URI" => uri = Some(unquote(value).parse()?),
|
||||||
"URI" => uri = Some(track!(value.parse())?),
|
"IV" => iv = Some((value.parse())?),
|
||||||
"IV" => iv = Some(track!(value.parse())?),
|
"KEYFORMAT" => key_format = Some(unquote(value)),
|
||||||
"KEYFORMAT" => key_format = Some(track!(value.parse())?),
|
"KEYFORMATVERSIONS" => key_format_versions = Some(unquote(value)),
|
||||||
"KEYFORMATVERSIONS" => key_format_versions = Some(track!(value.parse())?),
|
|
||||||
_ => {
|
_ => {
|
||||||
// [6.3.1. General Client Responsibilities]
|
// [6.3.1. General Client Responsibilities]
|
||||||
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let method = track_assert_some!(method, ErrorKind::InvalidInput);
|
|
||||||
let uri = track_assert_some!(uri, ErrorKind::InvalidInput);
|
let method = method.ok_or(Error::missing_value("METHOD"))?;
|
||||||
|
if method != EncryptionMethod::None && uri.is_none() {
|
||||||
|
return Err(Error::missing_value("URI"));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(DecryptionKey {
|
Ok(DecryptionKey {
|
||||||
method,
|
method,
|
||||||
uri,
|
uri,
|
||||||
|
@ -82,3 +358,108 @@ impl FromStr for DecryptionKey {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for DecryptionKey {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "METHOD={}", self.method)?;
|
||||||
|
|
||||||
|
if self.method == EncryptionMethod::None {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if let Some(uri) = &self.uri {
|
||||||
|
write!(f, ",URI={}", quote(uri))?;
|
||||||
|
}
|
||||||
|
if let Some(value) = &self.iv {
|
||||||
|
write!(f, ",IV={}", value)?;
|
||||||
|
}
|
||||||
|
if let Some(value) = &self.key_format {
|
||||||
|
write!(f, ",KEYFORMAT={}", quote(value))?;
|
||||||
|
}
|
||||||
|
if let Some(value) = &self.key_format_versions {
|
||||||
|
write!(f, ",KEYFORMATVERSIONS={}", quote(value))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use crate::types::EncryptionMethod;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_requires_version() {
|
||||||
|
let key = DecryptionKey::builder()
|
||||||
|
.method(EncryptionMethod::Aes128)
|
||||||
|
.uri("https://www.example.com".parse::<Url>().unwrap())
|
||||||
|
.iv([
|
||||||
|
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
||||||
|
])
|
||||||
|
.build()
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_display() {
|
||||||
|
let mut key = DecryptionKey::new(
|
||||||
|
EncryptionMethod::Aes128,
|
||||||
|
"https://www.example.com/hls-key/key.bin".parse().unwrap(),
|
||||||
|
);
|
||||||
|
key.set_iv([
|
||||||
|
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
key.to_string(),
|
||||||
|
"METHOD=AES-128,\
|
||||||
|
URI=\"https://www.example.com/hls-key/key.bin\",\
|
||||||
|
IV=0x10ef8f758ca555115584bb5b3c687f52"
|
||||||
|
.to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parser() {
|
||||||
|
assert_eq!(
|
||||||
|
r#"METHOD=AES-128,URI="https://priv.example.com/key.php?r=52""#
|
||||||
|
.parse::<DecryptionKey>()
|
||||||
|
.unwrap(),
|
||||||
|
DecryptionKey::new(
|
||||||
|
EncryptionMethod::Aes128,
|
||||||
|
"https://priv.example.com/key.php?r=52".parse().unwrap()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut key = DecryptionKey::new(
|
||||||
|
EncryptionMethod::Aes128,
|
||||||
|
"https://www.example.com/hls-key/key.bin".parse().unwrap(),
|
||||||
|
);
|
||||||
|
key.set_iv([
|
||||||
|
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
"METHOD=AES-128,\
|
||||||
|
URI=\"https://www.example.com/hls-key/key.bin\",\
|
||||||
|
IV=0X10ef8f758ca555115584bb5b3c687f52"
|
||||||
|
.parse::<DecryptionKey>()
|
||||||
|
.unwrap(),
|
||||||
|
key
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut key = DecryptionKey::new(
|
||||||
|
EncryptionMethod::Aes128,
|
||||||
|
"http://www.example.com".parse().unwrap(),
|
||||||
|
);
|
||||||
|
key.set_iv([
|
||||||
|
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
||||||
|
]);
|
||||||
|
key.set_key_format("baz");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
r#"METHOD=AES-128,URI="http://www.example.com",IV=0x10ef8f758ca555115584bb5b3c687f52,KEYFORMAT="baz""#
|
||||||
|
.parse::<DecryptionKey>().unwrap(),
|
||||||
|
key
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::{self, FromStr};
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// Encryption method.
|
/// Encryption method.
|
||||||
///
|
///
|
||||||
|
@ -10,30 +11,65 @@ use std::str::{self, FromStr};
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub enum EncryptionMethod {
|
pub enum EncryptionMethod {
|
||||||
|
/// `None` means that [MediaSegment]s are not encrypted.
|
||||||
|
///
|
||||||
|
/// [MediaSegment]: crate::MediaSegment
|
||||||
|
None,
|
||||||
|
/// `Aes128` signals that the [MediaSegment]s are completely encrypted
|
||||||
|
/// using the Advanced Encryption Standard ([AES_128]) with a 128-bit
|
||||||
|
/// key, Cipher Block Chaining (CBC), and
|
||||||
|
/// [Public-Key Cryptography Standards #7 (PKCS7)] padding.
|
||||||
|
/// CBC is restarted on each segment boundary, using either the
|
||||||
|
/// Initialization Vector (IV) attribute value or the Media Sequence
|
||||||
|
/// Number as the IV.
|
||||||
|
///
|
||||||
|
/// [MediaSegment]: crate::MediaSegment
|
||||||
|
/// [AES_128]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf
|
||||||
|
/// [Public-Key Cryptography Standards #7 (PKCS7)]: https://tools.ietf.org/html/rfc5652
|
||||||
Aes128,
|
Aes128,
|
||||||
|
/// `SampleAes` means that the [MediaSegment]s
|
||||||
|
/// contain media samples, such as audio or video, that are encrypted
|
||||||
|
/// using the Advanced Encryption Standard ([AES_128]). How these media
|
||||||
|
/// streams are encrypted and encapsulated in a segment depends on the
|
||||||
|
/// media encoding and the media format of the segment. fMP4 Media
|
||||||
|
/// Segments are encrypted using the 'cbcs' scheme of
|
||||||
|
/// [Common Encryption]. Encryption of other Media Segment
|
||||||
|
/// formats containing [H.264], [AAC], [AC-3],
|
||||||
|
/// and Enhanced [AC-3] media streams is described in the HTTP
|
||||||
|
/// Live Streaming (HLS) [SampleEncryption specification].
|
||||||
|
///
|
||||||
|
/// [MediaSegment]: crate::MediaSegment
|
||||||
|
/// [AES_128]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf
|
||||||
|
/// [Common Encryption]: https://tools.ietf.org/html/rfc8216#ref-COMMON_ENC
|
||||||
|
/// [H.264]: https://tools.ietf.org/html/rfc8216#ref-H_264
|
||||||
|
/// [AAC]: https://tools.ietf.org/html/rfc8216#ref-ISO_14496
|
||||||
|
/// [AC-3]: https://tools.ietf.org/html/rfc8216#ref-AC_3
|
||||||
|
/// [SampleEncryption specification]: https://tools.ietf.org/html/rfc8216#ref-SampleEnc
|
||||||
SampleAes,
|
SampleAes,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for EncryptionMethod {
|
impl fmt::Display for EncryptionMethod {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
match *self {
|
match &self {
|
||||||
EncryptionMethod::Aes128 => "AES-128".fmt(f),
|
EncryptionMethod::Aes128 => "AES-128".fmt(f),
|
||||||
EncryptionMethod::SampleAes => "SAMPLE-AES".fmt(f),
|
EncryptionMethod::SampleAes => "SAMPLE-AES".fmt(f),
|
||||||
|
EncryptionMethod::None => "NONE".fmt(f),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for EncryptionMethod {
|
impl FromStr for EncryptionMethod {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
match s {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
|
match input {
|
||||||
"AES-128" => Ok(EncryptionMethod::Aes128),
|
"AES-128" => Ok(EncryptionMethod::Aes128),
|
||||||
"SAMPLE-AES" => Ok(EncryptionMethod::SampleAes),
|
"SAMPLE-AES" => Ok(EncryptionMethod::SampleAes),
|
||||||
_ => track_panic!(
|
"NONE" => Ok(EncryptionMethod::None),
|
||||||
ErrorKind::InvalidInput,
|
_ => Err(Error::custom(format!(
|
||||||
"Unknown encryption method: {:?}",
|
"Unknown encryption method: {:?}",
|
||||||
s
|
input
|
||||||
),
|
))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,25 +80,29 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_display() {
|
fn test_display() {
|
||||||
let encryption_method = EncryptionMethod::Aes128;
|
assert_eq!(EncryptionMethod::Aes128.to_string(), "AES-128".to_string());
|
||||||
assert_eq!(encryption_method.to_string(), "AES-128".to_string());
|
assert_eq!(
|
||||||
|
EncryptionMethod::SampleAes.to_string(),
|
||||||
let encryption_method = EncryptionMethod::SampleAes;
|
"SAMPLE-AES".to_string()
|
||||||
assert_eq!(encryption_method.to_string(), "SAMPLE-AES".to_string());
|
);
|
||||||
|
assert_eq!(EncryptionMethod::None.to_string(), "NONE".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse() {
|
fn test_parse() {
|
||||||
let encryption_method = EncryptionMethod::Aes128;
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
encryption_method,
|
EncryptionMethod::Aes128,
|
||||||
"AES-128".parse::<EncryptionMethod>().unwrap()
|
"AES-128".parse::<EncryptionMethod>().unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
let encryption_method = EncryptionMethod::SampleAes;
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
encryption_method,
|
EncryptionMethod::SampleAes,
|
||||||
"SAMPLE-AES".parse::<EncryptionMethod>().unwrap()
|
"SAMPLE-AES".parse::<EncryptionMethod>().unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
EncryptionMethod::None,
|
||||||
|
"NONE".parse::<EncryptionMethod>().unwrap()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::{self, FromStr};
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// HDCP level.
|
/// HDCP level.
|
||||||
///
|
///
|
||||||
|
@ -16,7 +17,7 @@ pub enum HdcpLevel {
|
||||||
|
|
||||||
impl fmt::Display for HdcpLevel {
|
impl fmt::Display for HdcpLevel {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
match *self {
|
match &self {
|
||||||
HdcpLevel::Type0 => "TYPE-0".fmt(f),
|
HdcpLevel::Type0 => "TYPE-0".fmt(f),
|
||||||
HdcpLevel::None => "NONE".fmt(f),
|
HdcpLevel::None => "NONE".fmt(f),
|
||||||
}
|
}
|
||||||
|
@ -25,11 +26,12 @@ impl fmt::Display for HdcpLevel {
|
||||||
|
|
||||||
impl FromStr for HdcpLevel {
|
impl FromStr for HdcpLevel {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
match s {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
|
match input {
|
||||||
"TYPE-0" => Ok(HdcpLevel::Type0),
|
"TYPE-0" => Ok(HdcpLevel::Type0),
|
||||||
"NONE" => Ok(HdcpLevel::None),
|
"NONE" => Ok(HdcpLevel::None),
|
||||||
_ => track_panic!(ErrorKind::InvalidInput, "Unknown HDCP level: {:?}", s),
|
_ => Err(Error::custom(format!("Unknown HDCP level: {:?}", input))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
|
||||||
use std::ops::Deref;
|
|
||||||
use std::str::{self, FromStr};
|
|
||||||
use trackable::error::ErrorKindExt;
|
|
||||||
|
|
||||||
/// Hexadecimal sequence.
|
|
||||||
///
|
|
||||||
/// See: [4.2. Attribute Lists]
|
|
||||||
///
|
|
||||||
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
pub struct HexadecimalSequence(Vec<u8>);
|
|
||||||
|
|
||||||
impl HexadecimalSequence {
|
|
||||||
/// Makes a new `HexadecimalSequence` instance.
|
|
||||||
pub fn new<T: Into<Vec<u8>>>(v: T) -> Self {
|
|
||||||
HexadecimalSequence(v.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts into the underlying byte sequence.
|
|
||||||
pub fn into_bytes(self) -> Vec<u8> {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for HexadecimalSequence {
|
|
||||||
type Target = [u8];
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsRef<[u8]> for HexadecimalSequence {
|
|
||||||
fn as_ref(&self) -> &[u8] {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for HexadecimalSequence {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "0x")?;
|
|
||||||
for b in &self.0 {
|
|
||||||
write!(f, "{:02x}", b)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for HexadecimalSequence {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(
|
|
||||||
s.starts_with("0x") || s.starts_with("0X"),
|
|
||||||
ErrorKind::InvalidInput
|
|
||||||
);
|
|
||||||
track_assert!(s.len() % 2 == 0, ErrorKind::InvalidInput);
|
|
||||||
|
|
||||||
let mut v = Vec::with_capacity(s.len() / 2 - 1);
|
|
||||||
for c in s.as_bytes().chunks(2).skip(1) {
|
|
||||||
let d = track!(str::from_utf8(c).map_err(|e| ErrorKind::InvalidInput.cause(e)))?;
|
|
||||||
let b =
|
|
||||||
track!(u8::from_str_radix(d, 16).map_err(|e| ErrorKind::InvalidInput.cause(e)))?;
|
|
||||||
v.push(b);
|
|
||||||
}
|
|
||||||
Ok(HexadecimalSequence(v))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::{self, FromStr};
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// Identifier of a rendition within the segments in a media playlist.
|
/// Identifier of a rendition within the segments in a media playlist.
|
||||||
///
|
///
|
||||||
|
@ -87,8 +88,9 @@ impl fmt::Display for InStreamId {
|
||||||
|
|
||||||
impl FromStr for InStreamId {
|
impl FromStr for InStreamId {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
Ok(match s {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
|
Ok(match input {
|
||||||
"CC1" => InStreamId::Cc1,
|
"CC1" => InStreamId::Cc1,
|
||||||
"CC2" => InStreamId::Cc2,
|
"CC2" => InStreamId::Cc2,
|
||||||
"CC3" => InStreamId::Cc3,
|
"CC3" => InStreamId::Cc3,
|
||||||
|
@ -156,7 +158,7 @@ impl FromStr for InStreamId {
|
||||||
"SERVICE61" => InStreamId::Service61,
|
"SERVICE61" => InStreamId::Service61,
|
||||||
"SERVICE62" => InStreamId::Service62,
|
"SERVICE62" => InStreamId::Service62,
|
||||||
"SERVICE63" => InStreamId::Service63,
|
"SERVICE63" => InStreamId::Service63,
|
||||||
_ => track_panic!(ErrorKind::InvalidInput, "Unknown instream id: {:?}", s),
|
_ => return Err(Error::custom(format!("Unknown instream id: {:?}", input))),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use std::str::{self, FromStr};
|
use std::str::FromStr;
|
||||||
use trackable::error::ErrorKindExt;
|
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// Initialization vector.
|
/// Initialization vector.
|
||||||
///
|
///
|
||||||
|
@ -12,6 +12,19 @@ use trackable::error::ErrorKindExt;
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub struct InitializationVector(pub [u8; 16]);
|
pub struct InitializationVector(pub [u8; 16]);
|
||||||
|
|
||||||
|
impl InitializationVector {
|
||||||
|
/// Converts the initialization vector to a slice.
|
||||||
|
pub const fn to_slice(&self) -> [u8; 16] {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<[u8; 16]> for InitializationVector {
|
||||||
|
fn from(value: [u8; 16]) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Deref for InitializationVector {
|
impl Deref for InitializationVector {
|
||||||
type Target = [u8];
|
type Target = [u8];
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
|
@ -37,20 +50,22 @@ impl fmt::Display for InitializationVector {
|
||||||
|
|
||||||
impl FromStr for InitializationVector {
|
impl FromStr for InitializationVector {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
s.starts_with("0x") || s.starts_with("0X"),
|
if !(s.starts_with("0x") || s.starts_with("0X")) {
|
||||||
ErrorKind::InvalidInput
|
return Err(Error::invalid_input());
|
||||||
);
|
}
|
||||||
track_assert_eq!(s.len() - 2, 32, ErrorKind::InvalidInput);
|
if s.len() - 2 != 32 {
|
||||||
|
return Err(Error::invalid_input());
|
||||||
|
}
|
||||||
|
|
||||||
let mut v = [0; 16];
|
let mut v = [0; 16];
|
||||||
for (i, c) in s.as_bytes().chunks(2).skip(1).enumerate() {
|
for (i, c) in s.as_bytes().chunks(2).skip(1).enumerate() {
|
||||||
let d = track!(str::from_utf8(c).map_err(|e| ErrorKind::InvalidInput.cause(e)))?;
|
let d = std::str::from_utf8(c).map_err(|e| Error::custom(e))?;
|
||||||
let b =
|
let b = u8::from_str_radix(d, 16).map_err(|e| Error::custom(e))?;
|
||||||
track!(u8::from_str_radix(d, 16).map_err(|e| ErrorKind::InvalidInput.cause(e)))?;
|
|
||||||
v[i] = b;
|
v[i] = b;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(InitializationVector(v))
|
Ok(InitializationVector(v))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::{self, FromStr};
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// Media type.
|
/// Media type.
|
||||||
///
|
///
|
||||||
|
@ -18,7 +19,7 @@ pub enum MediaType {
|
||||||
|
|
||||||
impl fmt::Display for MediaType {
|
impl fmt::Display for MediaType {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
match *self {
|
match &self {
|
||||||
MediaType::Audio => "AUDIO".fmt(f),
|
MediaType::Audio => "AUDIO".fmt(f),
|
||||||
MediaType::Video => "VIDEO".fmt(f),
|
MediaType::Video => "VIDEO".fmt(f),
|
||||||
MediaType::Subtitles => "SUBTITLES".fmt(f),
|
MediaType::Subtitles => "SUBTITLES".fmt(f),
|
||||||
|
@ -29,13 +30,16 @@ impl fmt::Display for MediaType {
|
||||||
|
|
||||||
impl FromStr for MediaType {
|
impl FromStr for MediaType {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
Ok(match s {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
|
Ok(match input {
|
||||||
"AUDIO" => MediaType::Audio,
|
"AUDIO" => MediaType::Audio,
|
||||||
"VIDEO" => MediaType::Video,
|
"VIDEO" => MediaType::Video,
|
||||||
"SUBTITLES" => MediaType::Subtitles,
|
"SUBTITLES" => MediaType::Subtitles,
|
||||||
"CLOSED-CAPTIONS" => MediaType::ClosedCaptions,
|
"CLOSED-CAPTIONS" => MediaType::ClosedCaptions,
|
||||||
_ => track_panic!(ErrorKind::InvalidInput, "Unknown media type: {:?}", s),
|
_ => {
|
||||||
|
return Err(Error::invalid_input());
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,31 +6,21 @@ mod decimal_resolution;
|
||||||
mod decryption_key;
|
mod decryption_key;
|
||||||
mod encryption_method;
|
mod encryption_method;
|
||||||
mod hdcp_level;
|
mod hdcp_level;
|
||||||
mod hexadecimal_sequence;
|
|
||||||
mod in_stream_id;
|
mod in_stream_id;
|
||||||
mod initialization_vector;
|
mod initialization_vector;
|
||||||
mod media_type;
|
mod media_type;
|
||||||
mod playlist_type;
|
|
||||||
mod protocol_version;
|
mod protocol_version;
|
||||||
mod quoted_string;
|
|
||||||
mod session_data;
|
|
||||||
mod signed_decimal_floating_point;
|
mod signed_decimal_floating_point;
|
||||||
mod single_line_string;
|
|
||||||
|
|
||||||
pub use byte_range::*;
|
pub use byte_range::*;
|
||||||
pub use closed_captions::*;
|
pub use closed_captions::*;
|
||||||
pub use decimal_floating_point::*;
|
pub(crate) use decimal_floating_point::*;
|
||||||
pub use decimal_resolution::*;
|
pub(crate) use decimal_resolution::*;
|
||||||
pub use decryption_key::*;
|
pub use decryption_key::*;
|
||||||
pub use encryption_method::*;
|
pub use encryption_method::*;
|
||||||
pub use hdcp_level::*;
|
pub use hdcp_level::*;
|
||||||
pub use hexadecimal_sequence::*;
|
|
||||||
pub use in_stream_id::*;
|
pub use in_stream_id::*;
|
||||||
pub use initialization_vector::*;
|
pub use initialization_vector::*;
|
||||||
pub use media_type::*;
|
pub use media_type::*;
|
||||||
pub use playlist_type::*;
|
|
||||||
pub use protocol_version::*;
|
pub use protocol_version::*;
|
||||||
pub use quoted_string::*;
|
pub(crate) use signed_decimal_floating_point::*;
|
||||||
pub use session_data::*;
|
|
||||||
pub use signed_decimal_floating_point::*;
|
|
||||||
pub use single_line_string::*;
|
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
|
||||||
use std::str::{self, FromStr};
|
|
||||||
|
|
||||||
/// Playlist type.
|
|
||||||
///
|
|
||||||
/// See: [4.3.3.5. EXT-X-PLAYLIST-TYPE]
|
|
||||||
///
|
|
||||||
/// [4.3.3.5. EXT-X-PLAYLIST-TYPE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.5
|
|
||||||
#[allow(missing_docs)]
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub enum PlaylistType {
|
|
||||||
Event,
|
|
||||||
Vod,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for PlaylistType {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match *self {
|
|
||||||
PlaylistType::Event => write!(f, "EVENT"),
|
|
||||||
PlaylistType::Vod => write!(f, "VOD"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for PlaylistType {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
match s {
|
|
||||||
"EVENT" => Ok(PlaylistType::Event),
|
|
||||||
"VOD" => Ok(PlaylistType::Vod),
|
|
||||||
_ => track_panic!(ErrorKind::InvalidInput, "Unknown playlist type: {:?}", s),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::{self, FromStr};
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// [7. Protocol Version Compatibility]
|
/// [7. Protocol Version Compatibility]
|
||||||
///
|
///
|
||||||
|
@ -16,32 +17,46 @@ pub enum ProtocolVersion {
|
||||||
V6,
|
V6,
|
||||||
V7,
|
V7,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ProtocolVersion {
|
||||||
|
/// Returns the newest ProtocolVersion, that is supported by this library.
|
||||||
|
pub const fn latest() -> Self {
|
||||||
|
Self::V7
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Display for ProtocolVersion {
|
impl fmt::Display for ProtocolVersion {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
let n = match *self {
|
let n = {
|
||||||
ProtocolVersion::V1 => 1,
|
match &self {
|
||||||
ProtocolVersion::V2 => 2,
|
ProtocolVersion::V1 => 1,
|
||||||
ProtocolVersion::V3 => 3,
|
ProtocolVersion::V2 => 2,
|
||||||
ProtocolVersion::V4 => 4,
|
ProtocolVersion::V3 => 3,
|
||||||
ProtocolVersion::V5 => 5,
|
ProtocolVersion::V4 => 4,
|
||||||
ProtocolVersion::V6 => 6,
|
ProtocolVersion::V5 => 5,
|
||||||
ProtocolVersion::V7 => 7,
|
ProtocolVersion::V6 => 6,
|
||||||
|
ProtocolVersion::V7 => 7,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
write!(f, "{}", n)
|
write!(f, "{}", n)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ProtocolVersion {
|
impl FromStr for ProtocolVersion {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
Ok(match s {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
"1" => ProtocolVersion::V1,
|
Ok({
|
||||||
"2" => ProtocolVersion::V2,
|
match input {
|
||||||
"3" => ProtocolVersion::V3,
|
"1" => ProtocolVersion::V1,
|
||||||
"4" => ProtocolVersion::V4,
|
"2" => ProtocolVersion::V2,
|
||||||
"5" => ProtocolVersion::V5,
|
"3" => ProtocolVersion::V3,
|
||||||
"6" => ProtocolVersion::V6,
|
"4" => ProtocolVersion::V4,
|
||||||
"7" => ProtocolVersion::V7,
|
"5" => ProtocolVersion::V5,
|
||||||
_ => track_panic!(ErrorKind::InvalidInput, "Unknown protocol version: {:?}", s),
|
"6" => ProtocolVersion::V6,
|
||||||
|
"7" => ProtocolVersion::V7,
|
||||||
|
_ => return Err(Error::unknown_protocol_version(input)),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
|
||||||
use std::ops::Deref;
|
|
||||||
use std::str::{self, FromStr};
|
|
||||||
|
|
||||||
/// Quoted string.
|
|
||||||
///
|
|
||||||
/// See: [4.2. Attribute Lists]
|
|
||||||
///
|
|
||||||
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
pub struct QuotedString(String);
|
|
||||||
|
|
||||||
impl QuotedString {
|
|
||||||
/// Makes a new `QuotedString` instance.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// If the given string contains any control characters or double-quote character,
|
|
||||||
/// this function will return an error which has the kind `ErrorKind::InvalidInput`.
|
|
||||||
pub fn new<T: Into<String>>(s: T) -> Result<Self> {
|
|
||||||
let s = s.into();
|
|
||||||
track_assert!(
|
|
||||||
!s.chars().any(|c| c.is_control() || c == '"'),
|
|
||||||
ErrorKind::InvalidInput
|
|
||||||
);
|
|
||||||
Ok(QuotedString(s))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for QuotedString {
|
|
||||||
type Target = str;
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsRef<str> for QuotedString {
|
|
||||||
fn as_ref(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for QuotedString {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{:?}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for QuotedString {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
let len = s.len();
|
|
||||||
let bytes = s.as_bytes();
|
|
||||||
track_assert!(len >= 2, ErrorKind::InvalidInput);
|
|
||||||
track_assert_eq!(bytes[0], b'"', ErrorKind::InvalidInput);
|
|
||||||
track_assert_eq!(bytes[len - 1], b'"', ErrorKind::InvalidInput);
|
|
||||||
|
|
||||||
let s = unsafe { str::from_utf8_unchecked(&bytes[1..len - 1]) };
|
|
||||||
track!(QuotedString::new(s))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
use crate::types::QuotedString;
|
|
||||||
|
|
||||||
/// Session data.
|
|
||||||
///
|
|
||||||
/// See: [4.3.4.4. EXT-X-SESSION-DATA]
|
|
||||||
///
|
|
||||||
/// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4
|
|
||||||
#[allow(missing_docs)]
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
||||||
pub enum SessionData {
|
|
||||||
Value(QuotedString),
|
|
||||||
Uri(QuotedString),
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::{self, FromStr};
|
use std::str::FromStr;
|
||||||
use trackable::error::ErrorKindExt;
|
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// Signed decimal floating-point number.
|
/// Signed decimal floating-point number.
|
||||||
///
|
///
|
||||||
|
@ -9,7 +9,7 @@ use trackable::error::ErrorKindExt;
|
||||||
///
|
///
|
||||||
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
|
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
|
||||||
pub struct SignedDecimalFloatingPoint(f64);
|
pub(crate) struct SignedDecimalFloatingPoint(f64);
|
||||||
|
|
||||||
impl SignedDecimalFloatingPoint {
|
impl SignedDecimalFloatingPoint {
|
||||||
/// Makes a new `SignedDecimalFloatingPoint` instance.
|
/// Makes a new `SignedDecimalFloatingPoint` instance.
|
||||||
|
@ -18,13 +18,16 @@ impl SignedDecimalFloatingPoint {
|
||||||
///
|
///
|
||||||
/// The given value must be finite,
|
/// The given value must be finite,
|
||||||
/// otherwise this function will return an error that has the kind `ErrorKind::InvalidInput`.
|
/// otherwise this function will return an error that has the kind `ErrorKind::InvalidInput`.
|
||||||
pub fn new(n: f64) -> Result<Self> {
|
pub fn new(n: f64) -> crate::Result<Self> {
|
||||||
track_assert!(n.is_finite(), ErrorKind::InvalidInput);
|
if !n.is_finite() {
|
||||||
Ok(SignedDecimalFloatingPoint(n))
|
Err(Error::invalid_input())
|
||||||
|
} else {
|
||||||
|
Ok(SignedDecimalFloatingPoint(n))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts `DecimalFloatingPoint` to `f64`.
|
/// Converts `DecimalFloatingPoint` to `f64`.
|
||||||
pub fn as_f64(self) -> f64 {
|
pub const fn as_f64(self) -> f64 {
|
||||||
self.0
|
self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,12 +48,8 @@ impl fmt::Display for SignedDecimalFloatingPoint {
|
||||||
|
|
||||||
impl FromStr for SignedDecimalFloatingPoint {
|
impl FromStr for SignedDecimalFloatingPoint {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
s.chars().all(|c| c.is_digit(10) || c == '.' || c == '-'),
|
SignedDecimalFloatingPoint::new(input.parse().map_err(Error::parse_float_error)?)
|
||||||
ErrorKind::InvalidInput
|
|
||||||
);
|
|
||||||
let n = track!(s.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?;
|
|
||||||
Ok(SignedDecimalFloatingPoint(n))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
use crate::{ErrorKind, Result};
|
|
||||||
use std::fmt;
|
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
/// String that represents a single line in a playlist file.
|
|
||||||
///
|
|
||||||
/// See: [4.1. Definition of a Playlist]
|
|
||||||
///
|
|
||||||
/// [4.1. Definition of a Playlist]: https://tools.ietf.org/html/rfc8216#section-4.1
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
pub struct SingleLineString(String);
|
|
||||||
|
|
||||||
impl SingleLineString {
|
|
||||||
/// Makes a new `SingleLineString` instance.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// If the given string contains any control characters,
|
|
||||||
/// this function will return an error which has the kind `ErrorKind::InvalidInput`.
|
|
||||||
pub fn new<T: Into<String>>(s: T) -> Result<Self> {
|
|
||||||
let s = s.into();
|
|
||||||
track_assert!(!s.chars().any(|c| c.is_control()), ErrorKind::InvalidInput);
|
|
||||||
Ok(SingleLineString(s))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for SingleLineString {
|
|
||||||
type Target = str;
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsRef<str> for SingleLineString {
|
|
||||||
fn as_ref(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for SingleLineString {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
self.0.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn single_line_string() {
|
|
||||||
assert!(SingleLineString::new("foo").is_ok());
|
|
||||||
assert!(SingleLineString::new("b\rar").is_err());
|
|
||||||
}
|
|
||||||
}
|
|
102
src/utils.rs
102
src/utils.rs
|
@ -1,15 +1,103 @@
|
||||||
use crate::{ErrorKind, Result};
|
use crate::Error;
|
||||||
use trackable::error::ErrorKindExt;
|
|
||||||
|
|
||||||
pub fn parse_yes_or_no(s: &str) -> Result<bool> {
|
pub(crate) fn parse_yes_or_no<T: AsRef<str>>(s: T) -> crate::Result<bool> {
|
||||||
match s {
|
match s.as_ref() {
|
||||||
"YES" => Ok(true),
|
"YES" => Ok(true),
|
||||||
"NO" => Ok(false),
|
"NO" => Ok(false),
|
||||||
_ => track_panic!(ErrorKind::InvalidInput, "Unexpected value: {:?}", s),
|
_ => Err(Error::invalid_input()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_u64(s: &str) -> Result<u64> {
|
pub(crate) fn parse_u64<T: AsRef<str>>(s: T) -> crate::Result<u64> {
|
||||||
let n = track!(s.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?;
|
let n = s.as_ref().parse().map_err(Error::unknown)?; // TODO: Error::number
|
||||||
Ok(n)
|
Ok(n)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// According to the documentation the following characters are forbidden
|
||||||
|
/// inside a quoted string:
|
||||||
|
/// - carriage return (`\r`)
|
||||||
|
/// - new line (`\n`)
|
||||||
|
/// - double quotes (`"`)
|
||||||
|
///
|
||||||
|
/// Therefore it is safe to simply remove any occurence of those characters.
|
||||||
|
/// [rfc8216#section-4.2](https://tools.ietf.org/html/rfc8216#section-4.2)
|
||||||
|
pub(crate) fn unquote<T: ToString>(value: T) -> String {
|
||||||
|
value
|
||||||
|
.to_string()
|
||||||
|
.replace("\"", "")
|
||||||
|
.replace("\n", "")
|
||||||
|
.replace("\r", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Puts a string inside quotes.
|
||||||
|
pub(crate) fn quote<T: ToString>(value: T) -> String {
|
||||||
|
// the replace is for the case, that quote is called on an already quoted string, which could
|
||||||
|
// cause problems!
|
||||||
|
format!("\"{}\"", value.to_string().replace("\"", ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks, if the given tag is at the start of the input. If this is the case, it will remove it
|
||||||
|
/// and return the rest of the input.
|
||||||
|
///
|
||||||
|
/// # Error
|
||||||
|
/// This function will return `Error::MissingTag`, if the input doesn't start with the tag, that
|
||||||
|
/// has been passed to this function.
|
||||||
|
pub(crate) fn tag<T>(input: &str, tag: T) -> crate::Result<&str>
|
||||||
|
where
|
||||||
|
T: AsRef<str>,
|
||||||
|
{
|
||||||
|
if !input.starts_with(tag.as_ref()) {
|
||||||
|
return Err(Error::missing_tag(tag.as_ref(), input));
|
||||||
|
}
|
||||||
|
let result = input.split_at(tag.as_ref().len()).1;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_yes_or_no() {
|
||||||
|
assert!(parse_yes_or_no("YES").unwrap());
|
||||||
|
assert!(!parse_yes_or_no("NO").unwrap());
|
||||||
|
// TODO: test for error
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_u64() {
|
||||||
|
assert_eq!(parse_u64("1").unwrap(), 1);
|
||||||
|
assert_eq!(parse_u64("25").unwrap(), 25);
|
||||||
|
// TODO: test for error
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unquote() {
|
||||||
|
assert_eq!(unquote("\"TestValue\""), "TestValue".to_string());
|
||||||
|
assert_eq!(unquote("\"TestValue\n\""), "TestValue".to_string());
|
||||||
|
assert_eq!(unquote("\"TestValue\n\r\""), "TestValue".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_quote() {
|
||||||
|
assert_eq!(quote("value"), "\"value\"".to_string());
|
||||||
|
assert_eq!(quote("\"value\""), "\"value\"".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_tag() {
|
||||||
|
let input = "HelloMyFriendThisIsASampleString";
|
||||||
|
|
||||||
|
let input = tag(input, "Hello").unwrap();
|
||||||
|
assert_eq!(input, "MyFriendThisIsASampleString");
|
||||||
|
|
||||||
|
let input = tag(input, "My").unwrap();
|
||||||
|
assert_eq!(input, "FriendThisIsASampleString");
|
||||||
|
|
||||||
|
let input = tag(input, "FriendThisIs").unwrap();
|
||||||
|
assert_eq!(input, "ASampleString");
|
||||||
|
|
||||||
|
let input = tag(input, "A").unwrap();
|
||||||
|
assert_eq!(input, "SampleString");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
46
tests/playlist.rs
Normal file
46
tests/playlist.rs
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
//! Credits go to
|
||||||
|
//! - https://github.com/globocom/m3u8/blob/master/tests/playlists.py
|
||||||
|
use hls_m3u8::tags::*;
|
||||||
|
use hls_m3u8::MediaPlaylist;
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_simple_playlist() {
|
||||||
|
let playlist = r#"
|
||||||
|
#EXTM3U
|
||||||
|
#EXT-X-TARGETDURATION:5220
|
||||||
|
#EXTINF:0,
|
||||||
|
http://media.example.com/entire1.ts
|
||||||
|
#EXTINF:5220,
|
||||||
|
http://media.example.com/entire2.ts
|
||||||
|
#EXT-X-ENDLIST"#;
|
||||||
|
|
||||||
|
let media_playlist = playlist.parse::<MediaPlaylist>().unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
media_playlist.target_duration_tag(),
|
||||||
|
ExtXTargetDuration::new(Duration::from_secs(5220))
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(media_playlist.segments().len(), 2);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
media_playlist.segments()[0].inf_tag(),
|
||||||
|
&ExtInf::new(Duration::from_secs(0))
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
media_playlist.segments()[1].inf_tag(),
|
||||||
|
&ExtInf::new(Duration::from_secs(5220))
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
media_playlist.segments()[0].uri(),
|
||||||
|
&"http://media.example.com/entire1.ts".parse().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
media_playlist.segments()[1].uri(),
|
||||||
|
&"http://media.example.com/entire2.ts".parse().unwrap()
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in a new issue