mirror of
https://github.com/sile/hls_m3u8.git
synced 2024-06-18 12:50:31 +00:00
more codecov
This commit is contained in:
parent
cc5eb14dc6
commit
3c8d325599
15
.travis.yml
15
.travis.yml
|
@ -1,14 +1,15 @@
|
|||
language: rust
|
||||
|
||||
# before_cache: |
|
||||
# if [[ "$TRAVIS_RUST_VERSION" == stable ]]; then
|
||||
# cargo install cargo-tarpaulin -f
|
||||
# fi
|
||||
cache: cargo
|
||||
|
||||
before_cache: |
|
||||
bash <(curl https://raw.githubusercontent.com/xd009642/tarpaulin/master/travis-install.sh)
|
||||
if [[ "$TRAVIS_RUST_VERSION" == stable ]]; then
|
||||
cargo install cargo-tarpaulin
|
||||
fi
|
||||
|
||||
before_cache:
|
||||
- rm -rf /home/travis/.cargo/registry
|
||||
|
||||
cache: cargo
|
||||
|
||||
rust:
|
||||
- stable
|
||||
|
@ -26,7 +27,7 @@ script:
|
|||
after_success: |
|
||||
if [[ "$TRAVIS_RUST_VERSION" == stable ]]; then
|
||||
# Uncomment the following line for coveralls.io
|
||||
cargo tarpaulin --ciserver travis-ci --coveralls $TRAVIS_JOB_ID
|
||||
cargo tarpaulin --ciserver travis-ci --coveralls $TRAVIS_JOB_ID --run-types Tests Doctests
|
||||
|
||||
# Uncomment the following two lines create and upload a report for codecov.io
|
||||
cargo tarpaulin --out Xml
|
||||
|
|
|
@ -12,13 +12,15 @@ edition = "2018"
|
|||
categories = ["parser"]
|
||||
|
||||
[badges]
|
||||
travis-ci = {repository = "sile/hls_m3u8"}
|
||||
codecov = {repository = "sile/hls_m3u8"}
|
||||
travis-ci = { repository = "sile/hls_m3u8" }
|
||||
codecov = { repository = "sile/hls_m3u8" }
|
||||
|
||||
[dependencies]
|
||||
failure = "0.1.5"
|
||||
derive_builder = "0.7.2"
|
||||
chrono = "0.4.9"
|
||||
strum = { version = "0.16.0", features = ["derive"] }
|
||||
derive_more = "0.15.0"
|
||||
|
||||
[dev-dependencies]
|
||||
clap = "2"
|
||||
|
|
|
@ -55,11 +55,14 @@ impl FromStr for AttributePairs {
|
|||
let pair = split(line.trim(), '=');
|
||||
|
||||
if pair.len() < 2 {
|
||||
return Err(Error::invalid_input());
|
||||
continue;
|
||||
}
|
||||
|
||||
let key = pair[0].to_uppercase();
|
||||
let value = pair[1].to_string();
|
||||
let key = pair[0].trim().to_uppercase();
|
||||
let value = pair[1].trim().to_string();
|
||||
if value.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.insert(key.trim().to_string(), value.trim().to_string());
|
||||
}
|
||||
|
@ -123,7 +126,10 @@ mod test {
|
|||
let mut iterator = pairs.iter();
|
||||
assert!(iterator.any(|(k, v)| k == "ABC" && v == "12.3"));
|
||||
|
||||
assert!("FOO=BAR,VAL".parse::<AttributePairs>().is_err());
|
||||
let mut pairs = AttributePairs::new();
|
||||
pairs.insert("FOO".to_string(), "BAR".to_string());
|
||||
|
||||
assert_eq!("FOO=BAR,VAL".parse::<AttributePairs>().unwrap(), pairs);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
14
src/error.rs
14
src/error.rs
|
@ -1,4 +1,3 @@
|
|||
use std::error;
|
||||
use std::fmt;
|
||||
|
||||
use failure::{Backtrace, Context, Fail};
|
||||
|
@ -118,13 +117,6 @@ impl From<Context<ErrorKind>> for Error {
|
|||
}
|
||||
|
||||
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()))
|
||||
}
|
||||
|
@ -218,3 +210,9 @@ impl From<::chrono::ParseError> for Error {
|
|||
Error::chrono(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<::strum::ParseError> for Error {
|
||||
fn from(value: ::strum::ParseError) -> Self {
|
||||
Error::custom(value) // TODO!
|
||||
}
|
||||
}
|
||||
|
|
10
src/lib.rs
10
src/lib.rs
|
@ -1,9 +1,17 @@
|
|||
#![forbid(unsafe_code)]
|
||||
#![warn(
|
||||
//clippy::pedantic,
|
||||
clippy::nursery,
|
||||
clippy::cargo
|
||||
)]
|
||||
#![warn(missing_docs)]
|
||||
#![allow(clippy::multiple_crate_versions)]
|
||||
#![warn(
|
||||
missing_docs,
|
||||
missing_copy_implementations,
|
||||
missing_debug_implementations,
|
||||
trivial_casts, // TODO (needed?)
|
||||
trivial_numeric_casts
|
||||
)]
|
||||
//! [HLS] m3u8 parser/generator.
|
||||
//!
|
||||
//! [HLS]: https://tools.ietf.org/html/rfc8216
|
||||
|
|
|
@ -21,7 +21,7 @@ use crate::Error;
|
|||
/// [Master Playlist]: crate::MasterPlaylist
|
||||
/// [Media Playlist]: crate::MediaPlaylist
|
||||
/// [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(PartialOrd, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ExtXIFrameStreamInf {
|
||||
uri: String,
|
||||
stream_inf: StreamInf,
|
||||
|
|
|
@ -102,7 +102,7 @@ impl ExtXMedia {
|
|||
|
||||
/// Makes a new [ExtXMedia] tag.
|
||||
pub fn new<T: ToString>(media_type: MediaType, group_id: T, name: T) -> Self {
|
||||
ExtXMedia {
|
||||
Self {
|
||||
media_type,
|
||||
uri: None,
|
||||
group_id: group_id.to_string(),
|
||||
|
@ -118,72 +118,478 @@ impl ExtXMedia {
|
|||
}
|
||||
}
|
||||
|
||||
/// Makes a [ExtXMediaBuilder] for [ExtXMedia].
|
||||
/// Returns a builder for [ExtXMedia].
|
||||
pub fn builder() -> ExtXMediaBuilder {
|
||||
ExtXMediaBuilder::default()
|
||||
}
|
||||
|
||||
/// Returns the type of the media associated with this tag.
|
||||
/// Returns the type of the media, associated with this tag.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMedia;
|
||||
/// use hls_m3u8::types::MediaType;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// ExtXMedia::new(MediaType::Audio, "audio", "name").media_type(),
|
||||
/// MediaType::Audio
|
||||
/// );
|
||||
/// ```
|
||||
pub const fn media_type(&self) -> MediaType {
|
||||
self.media_type
|
||||
}
|
||||
|
||||
/// Sets the type of the media, associated with this tag.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMedia;
|
||||
/// use hls_m3u8::types::MediaType;
|
||||
///
|
||||
/// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name");
|
||||
///
|
||||
/// media.set_media_type(MediaType::Video);
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// media.media_type(),
|
||||
/// MediaType::Video
|
||||
/// );
|
||||
/// ```
|
||||
pub fn set_media_type(&mut self, value: MediaType) -> &mut Self {
|
||||
self.media_type = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the identifier that specifies the group to which the rendition belongs.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMedia;
|
||||
/// use hls_m3u8::types::MediaType;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// ExtXMedia::new(MediaType::Audio, "audio", "name").group_id(),
|
||||
/// &"audio".to_string()
|
||||
/// );
|
||||
/// ```
|
||||
pub const fn group_id(&self) -> &String {
|
||||
&self.group_id
|
||||
}
|
||||
|
||||
/// Sets the identifier that specifies the group, to which the rendition belongs.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMedia;
|
||||
/// use hls_m3u8::types::MediaType;
|
||||
///
|
||||
/// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name");
|
||||
///
|
||||
/// media.set_group_id("video");
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// media.group_id(),
|
||||
/// &"video".to_string()
|
||||
/// );
|
||||
/// ```
|
||||
pub fn set_group_id<T: Into<String>>(&mut self, value: T) -> &mut Self {
|
||||
self.group_id = value.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns a human-readable description of the rendition.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMedia;
|
||||
/// use hls_m3u8::types::MediaType;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// ExtXMedia::new(MediaType::Audio, "audio", "name").name(),
|
||||
/// &"name".to_string()
|
||||
/// );
|
||||
/// ```
|
||||
pub const fn name(&self) -> &String {
|
||||
&self.name
|
||||
}
|
||||
|
||||
/// Returns the URI that identifies the media playlist.
|
||||
pub fn uri(&self) -> Option<&String> {
|
||||
self.uri.as_ref()
|
||||
/// Sets a human-readable description of the rendition.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMedia;
|
||||
/// use hls_m3u8::types::MediaType;
|
||||
///
|
||||
/// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name");
|
||||
///
|
||||
/// media.set_name("new_name");
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// media.name(),
|
||||
/// &"new_name".to_string()
|
||||
/// );
|
||||
/// ```
|
||||
pub fn set_name<T: Into<String>>(&mut self, value: T) -> &mut Self {
|
||||
self.name = value.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the `URI`, that identifies the [MediaPlaylist].
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMedia;
|
||||
/// use hls_m3u8::types::MediaType;
|
||||
///
|
||||
/// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name");
|
||||
/// # assert_eq!(media.uri(), &None);
|
||||
///
|
||||
/// media.set_uri(Some("https://www.example.com/"));
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// media.uri(),
|
||||
/// &Some("https://www.example.com/".into())
|
||||
/// );
|
||||
/// ```
|
||||
pub const fn uri(&self) -> &Option<String> {
|
||||
&self.uri
|
||||
}
|
||||
|
||||
/// Sets the `URI`, that identifies the [MediaPlaylist].
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMedia;
|
||||
/// use hls_m3u8::types::MediaType;
|
||||
///
|
||||
/// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name");
|
||||
/// # assert_eq!(media.uri(), &None);
|
||||
///
|
||||
/// media.set_uri(Some("https://www.example.com/"));
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// media.uri(),
|
||||
/// &Some("https://www.example.com/".into())
|
||||
/// );
|
||||
/// ```
|
||||
pub fn set_uri<T: Into<String>>(&mut self, value: Option<T>) -> &mut Self {
|
||||
self.uri = value.map(|v| v.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the name of the primary language used in the rendition.
|
||||
pub fn language(&self) -> Option<&String> {
|
||||
self.language.as_ref()
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMedia;
|
||||
/// use hls_m3u8::types::MediaType;
|
||||
///
|
||||
/// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name");
|
||||
/// # assert_eq!(media.language(), &None);
|
||||
///
|
||||
/// media.set_language(Some("english"));
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// media.language(),
|
||||
/// &Some("english".into())
|
||||
/// );
|
||||
/// ```
|
||||
pub const fn language(&self) -> &Option<String> {
|
||||
&self.language
|
||||
}
|
||||
|
||||
/// Sets the name of the primary language used in the rendition.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMedia;
|
||||
/// use hls_m3u8::types::MediaType;
|
||||
///
|
||||
/// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name");
|
||||
/// # assert_eq!(media.language(), &None);
|
||||
///
|
||||
/// media.set_language(Some("english"));
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// media.language(),
|
||||
/// &Some("english".into())
|
||||
/// );
|
||||
/// ```
|
||||
pub fn set_language<T: Into<String>>(&mut self, value: Option<T>) -> &mut Self {
|
||||
self.language = value.map(|v| v.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the name of a language associated with the rendition.
|
||||
pub fn assoc_language(&self) -> Option<&String> {
|
||||
self.assoc_language.as_ref()
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMedia;
|
||||
/// use hls_m3u8::types::MediaType;
|
||||
///
|
||||
/// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name");
|
||||
/// # assert_eq!(media.assoc_language(), &None);
|
||||
///
|
||||
/// media.set_assoc_language(Some("spanish"));
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// media.assoc_language(),
|
||||
/// &Some("spanish".into())
|
||||
/// );
|
||||
/// ```
|
||||
pub const fn assoc_language(&self) -> &Option<String> {
|
||||
&self.assoc_language
|
||||
}
|
||||
|
||||
/// Sets the name of a language associated with the rendition.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMedia;
|
||||
/// use hls_m3u8::types::MediaType;
|
||||
///
|
||||
/// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name");
|
||||
/// # assert_eq!(media.assoc_language(), &None);
|
||||
///
|
||||
/// media.set_assoc_language(Some("spanish"));
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// media.assoc_language(),
|
||||
/// &Some("spanish".into())
|
||||
/// );
|
||||
/// ```
|
||||
pub fn set_assoc_language<T: Into<String>>(&mut self, value: Option<T>) -> &mut Self {
|
||||
self.assoc_language = value.map(|v| v.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns whether this is the default rendition.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMedia;
|
||||
/// use hls_m3u8::types::MediaType;
|
||||
///
|
||||
/// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name");
|
||||
/// # assert_eq!(media.is_default(), false);
|
||||
///
|
||||
/// media.set_default(true);
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// media.is_default(),
|
||||
/// true
|
||||
/// );
|
||||
/// ```
|
||||
pub const fn is_default(&self) -> bool {
|
||||
self.is_default
|
||||
}
|
||||
|
||||
/// Sets the `default` flag.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMedia;
|
||||
/// use hls_m3u8::types::MediaType;
|
||||
///
|
||||
/// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name");
|
||||
/// # assert_eq!(media.is_default(), false);
|
||||
///
|
||||
/// media.set_default(true);
|
||||
///
|
||||
/// assert_eq!(media.is_default(), true);
|
||||
/// ```
|
||||
pub fn set_default(&mut self, value: bool) -> &mut Self {
|
||||
self.is_default = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns whether the client may choose to
|
||||
/// play this rendition in the absence of explicit user preference.
|
||||
pub const fn autoselect(&self) -> bool {
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMedia;
|
||||
/// use hls_m3u8::types::MediaType;
|
||||
///
|
||||
/// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name");
|
||||
/// # assert_eq!(media.is_autoselect(), false);
|
||||
///
|
||||
/// media.set_autoselect(true);
|
||||
///
|
||||
/// assert_eq!(media.is_autoselect(), true);
|
||||
/// ```
|
||||
pub const fn is_autoselect(&self) -> bool {
|
||||
self.is_autoselect
|
||||
}
|
||||
|
||||
/// Sets the `autoselect` flag.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMedia;
|
||||
/// use hls_m3u8::types::MediaType;
|
||||
///
|
||||
/// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name");
|
||||
/// # assert_eq!(media.is_autoselect(), false);
|
||||
///
|
||||
/// media.set_autoselect(true);
|
||||
///
|
||||
/// assert_eq!(media.is_autoselect(), true);
|
||||
/// ```
|
||||
pub fn set_autoselect(&mut self, value: bool) -> &mut Self {
|
||||
self.is_autoselect = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns whether the rendition contains content that is considered essential to play.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMedia;
|
||||
/// use hls_m3u8::types::MediaType;
|
||||
///
|
||||
/// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name");
|
||||
/// # assert_eq!(media.is_forced(), false);
|
||||
///
|
||||
/// media.set_forced(true);
|
||||
///
|
||||
/// assert_eq!(media.is_forced(), true);
|
||||
/// ```
|
||||
pub const fn is_forced(&self) -> bool {
|
||||
self.is_forced
|
||||
}
|
||||
|
||||
/// Returns the identifier that specifies a rendition within the segments in the media playlist.
|
||||
/// Sets the `forced` flag.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMedia;
|
||||
/// use hls_m3u8::types::MediaType;
|
||||
///
|
||||
/// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name");
|
||||
/// # assert_eq!(media.is_forced(), false);
|
||||
///
|
||||
/// media.set_forced(true);
|
||||
///
|
||||
/// assert_eq!(media.is_forced(), true);
|
||||
/// ```
|
||||
pub fn set_forced(&mut self, value: bool) -> &mut Self {
|
||||
self.is_forced = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the identifier that specifies a rendition within the segments in the
|
||||
/// [MediaPlaylist].
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMedia;
|
||||
/// use hls_m3u8::types::{InStreamId, MediaType};
|
||||
///
|
||||
/// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name");
|
||||
/// # assert_eq!(media.instream_id(), None);
|
||||
///
|
||||
/// media.set_instream_id(Some(InStreamId::Cc1));
|
||||
///
|
||||
/// assert_eq!(media.instream_id(), Some(InStreamId::Cc1));
|
||||
/// ```
|
||||
pub const fn instream_id(&self) -> Option<InStreamId> {
|
||||
self.instream_id
|
||||
}
|
||||
|
||||
/// Sets the [InStreamId].
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMedia;
|
||||
/// use hls_m3u8::types::{InStreamId, MediaType};
|
||||
///
|
||||
/// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name");
|
||||
/// # assert_eq!(media.instream_id(), None);
|
||||
///
|
||||
/// media.set_instream_id(Some(InStreamId::Cc1));
|
||||
///
|
||||
/// assert_eq!(media.instream_id(), Some(InStreamId::Cc1));
|
||||
/// ```
|
||||
pub fn set_instream_id(&mut self, value: Option<InStreamId>) -> &mut Self {
|
||||
self.instream_id = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns a string that represents uniform type identifiers (UTI).
|
||||
///
|
||||
/// Each UTI indicates an individual characteristic of the rendition.
|
||||
pub fn characteristics(&self) -> Option<&String> {
|
||||
self.characteristics.as_ref()
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMedia;
|
||||
/// use hls_m3u8::types::MediaType;
|
||||
///
|
||||
/// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name");
|
||||
/// # assert_eq!(media.characteristics(), &None);
|
||||
///
|
||||
/// media.set_characteristics(Some("characteristic"));
|
||||
///
|
||||
/// assert_eq!(media.characteristics(), &Some("characteristic".into()));
|
||||
/// ```
|
||||
pub const fn characteristics(&self) -> &Option<String> {
|
||||
&self.characteristics
|
||||
}
|
||||
|
||||
/// Returns a string that represents the parameters of the rendition.
|
||||
pub fn channels(&self) -> Option<&String> {
|
||||
self.channels.as_ref()
|
||||
/// Sets the characteristics.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMedia;
|
||||
/// use hls_m3u8::types::MediaType;
|
||||
///
|
||||
/// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name");
|
||||
/// # assert_eq!(media.characteristics(), &None);
|
||||
///
|
||||
/// media.set_characteristics(Some("characteristic"));
|
||||
///
|
||||
/// assert_eq!(media.characteristics(), &Some("characteristic".into()));
|
||||
/// ```
|
||||
pub fn set_characteristics<T: Into<String>>(&mut self, value: Option<T>) -> &mut Self {
|
||||
self.characteristics = value.map(|v| v.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns a [String] that represents the parameters of the rendition.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMedia;
|
||||
/// use hls_m3u8::types::MediaType;
|
||||
///
|
||||
/// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name");
|
||||
/// # assert_eq!(media.channels(), &None);
|
||||
///
|
||||
/// media.set_channels(Some("channel"));
|
||||
///
|
||||
/// assert_eq!(media.channels(), &Some("channel".into()));
|
||||
/// ```
|
||||
pub const fn channels(&self) -> &Option<String> {
|
||||
&self.channels
|
||||
}
|
||||
|
||||
/// Sets the channels.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXMedia;
|
||||
/// use hls_m3u8::types::MediaType;
|
||||
///
|
||||
/// let mut media = ExtXMedia::new(MediaType::Audio, "audio", "name");
|
||||
/// # assert_eq!(media.characteristics(), &None);
|
||||
///
|
||||
/// media.set_characteristics(Some("characteristic"));
|
||||
///
|
||||
/// assert_eq!(media.characteristics(), &Some("characteristic".into()));
|
||||
/// ```
|
||||
pub fn set_channels<T: Into<String>>(&mut self, value: Option<T>) -> &mut Self {
|
||||
self.channels = value.map(|v| v.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -243,7 +649,7 @@ impl FromStr for ExtXMedia {
|
|||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
let input = tag(input, Self::PREFIX)?;
|
||||
|
||||
let mut builder = ExtXMedia::builder();
|
||||
let mut builder = Self::builder();
|
||||
|
||||
for (key, value) in input.parse::<AttributePairs>()? {
|
||||
match key.as_str() {
|
||||
|
@ -345,6 +751,162 @@ mod test {
|
|||
.to_string()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio")
|
||||
.language("sp")
|
||||
.name("Espanol")
|
||||
.is_autoselect(true)
|
||||
.is_default(false)
|
||||
.uri("sp/prog_index.m3u8")
|
||||
.build()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
"#EXT-X-MEDIA:\
|
||||
TYPE=AUDIO,\
|
||||
URI=\"sp/prog_index.m3u8\",\
|
||||
GROUP-ID=\"audio\",\
|
||||
LANGUAGE=\"sp\",\
|
||||
NAME=\"Espanol\",\
|
||||
AUTOSELECT=YES"
|
||||
.to_string()
|
||||
);
|
||||
// ----
|
||||
assert_eq!(
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio-lo")
|
||||
.language("eng")
|
||||
.name("English")
|
||||
.is_autoselect(true)
|
||||
.is_default(true)
|
||||
.uri("englo/prog_index.m3u8")
|
||||
.build()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
"#EXT-X-MEDIA:\
|
||||
TYPE=AUDIO,\
|
||||
URI=\"englo/prog_index.m3u8\",\
|
||||
GROUP-ID=\"audio-lo\",\
|
||||
LANGUAGE=\"eng\",\
|
||||
NAME=\"English\",\
|
||||
DEFAULT=YES,\
|
||||
AUTOSELECT=YES"
|
||||
.to_string()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio-lo")
|
||||
.language("fre")
|
||||
.name("Français")
|
||||
.is_autoselect(true)
|
||||
.is_default(false)
|
||||
.uri("frelo/prog_index.m3u8")
|
||||
.build()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
"#EXT-X-MEDIA:\
|
||||
TYPE=AUDIO,\
|
||||
URI=\"frelo/prog_index.m3u8\",\
|
||||
GROUP-ID=\"audio-lo\",\
|
||||
LANGUAGE=\"fre\",\
|
||||
NAME=\"Français\",\
|
||||
AUTOSELECT=YES"
|
||||
.to_string()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio-lo")
|
||||
.language("es")
|
||||
.name("Espanol")
|
||||
.is_autoselect(true)
|
||||
.is_default(false)
|
||||
.uri("splo/prog_index.m3u8")
|
||||
.build()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
"#EXT-X-MEDIA:\
|
||||
TYPE=AUDIO,\
|
||||
URI=\"splo/prog_index.m3u8\",\
|
||||
GROUP-ID=\"audio-lo\",\
|
||||
LANGUAGE=\"es\",\
|
||||
NAME=\"Espanol\",\
|
||||
AUTOSELECT=YES"
|
||||
.to_string()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio-hi")
|
||||
.language("eng")
|
||||
.name("English")
|
||||
.is_autoselect(true)
|
||||
.is_default(true)
|
||||
.uri("eng/prog_index.m3u8")
|
||||
.build()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
"#EXT-X-MEDIA:\
|
||||
TYPE=AUDIO,\
|
||||
URI=\"eng/prog_index.m3u8\",\
|
||||
GROUP-ID=\"audio-hi\",\
|
||||
LANGUAGE=\"eng\",\
|
||||
NAME=\"English\",\
|
||||
DEFAULT=YES,\
|
||||
AUTOSELECT=YES"
|
||||
.to_string()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio-hi")
|
||||
.language("fre")
|
||||
.name("Français")
|
||||
.is_autoselect(true)
|
||||
.is_default(false)
|
||||
.uri("fre/prog_index.m3u8")
|
||||
.build()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
"#EXT-X-MEDIA:\
|
||||
TYPE=AUDIO,\
|
||||
URI=\"fre/prog_index.m3u8\",\
|
||||
GROUP-ID=\"audio-hi\",\
|
||||
LANGUAGE=\"fre\",\
|
||||
NAME=\"Français\",\
|
||||
AUTOSELECT=YES"
|
||||
.to_string()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio-hi")
|
||||
.language("es")
|
||||
.name("Espanol")
|
||||
.is_autoselect(true)
|
||||
.is_default(false)
|
||||
.uri("sp/prog_index.m3u8")
|
||||
.build()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
"#EXT-X-MEDIA:\
|
||||
TYPE=AUDIO,\
|
||||
URI=\"sp/prog_index.m3u8\",\
|
||||
GROUP-ID=\"audio-hi\",\
|
||||
LANGUAGE=\"es\",\
|
||||
NAME=\"Espanol\",\
|
||||
AUTOSELECT=YES"
|
||||
.to_string()
|
||||
);
|
||||
// ----
|
||||
assert_eq!(
|
||||
ExtXMedia::new(MediaType::Audio, "foo", "bar").to_string(),
|
||||
"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"foo\",NAME=\"bar\"".to_string()
|
||||
|
@ -353,6 +915,208 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
// TODO: https://developer.apple.com/documentation/http_live_streaming/example_playlists_for_http_live_streaming/adding_alternate_media_to_a_playlist
|
||||
assert_eq!(
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio")
|
||||
.language("eng")
|
||||
.name("English")
|
||||
.is_autoselect(true)
|
||||
.is_default(true)
|
||||
.uri("eng/prog_index.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
"#EXT-X-MEDIA:\
|
||||
TYPE=AUDIO,\
|
||||
URI=\"eng/prog_index.m3u8\",\
|
||||
GROUP-ID=\"audio\",\
|
||||
LANGUAGE=\"eng\",\
|
||||
NAME=\"English\",\
|
||||
DEFAULT=YES,\
|
||||
AUTOSELECT=YES"
|
||||
.parse()
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio")
|
||||
.language("fre")
|
||||
.name("Français")
|
||||
.is_autoselect(true)
|
||||
.is_default(false)
|
||||
.uri("fre/prog_index.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
"#EXT-X-MEDIA:\
|
||||
TYPE=AUDIO,\
|
||||
URI=\"fre/prog_index.m3u8\",\
|
||||
GROUP-ID=\"audio\",\
|
||||
LANGUAGE=\"fre\",\
|
||||
NAME=\"Français\",\
|
||||
AUTOSELECT=YES"
|
||||
.parse()
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio")
|
||||
.language("sp")
|
||||
.name("Espanol")
|
||||
.is_autoselect(true)
|
||||
.is_default(false)
|
||||
.uri("sp/prog_index.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
"#EXT-X-MEDIA:\
|
||||
TYPE=AUDIO,\
|
||||
URI=\"sp/prog_index.m3u8\",\
|
||||
GROUP-ID=\"audio\",\
|
||||
LANGUAGE=\"sp\",\
|
||||
NAME=\"Espanol\",\
|
||||
AUTOSELECT=YES"
|
||||
.parse()
|
||||
.unwrap()
|
||||
);
|
||||
// ----
|
||||
assert_eq!(
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio-lo")
|
||||
.language("eng")
|
||||
.name("English")
|
||||
.is_autoselect(true)
|
||||
.is_default(true)
|
||||
.uri("englo/prog_index.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
"#EXT-X-MEDIA:\
|
||||
TYPE=AUDIO,\
|
||||
URI=\"englo/prog_index.m3u8\",\
|
||||
GROUP-ID=\"audio-lo\",\
|
||||
LANGUAGE=\"eng\",\
|
||||
NAME=\"English\",\
|
||||
DEFAULT=YES,\
|
||||
AUTOSELECT=YES"
|
||||
.parse()
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio-lo")
|
||||
.language("fre")
|
||||
.name("Français")
|
||||
.is_autoselect(true)
|
||||
.is_default(false)
|
||||
.uri("frelo/prog_index.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
"#EXT-X-MEDIA:\
|
||||
TYPE=AUDIO,\
|
||||
URI=\"frelo/prog_index.m3u8\",\
|
||||
GROUP-ID=\"audio-lo\",\
|
||||
LANGUAGE=\"fre\",\
|
||||
NAME=\"Français\",\
|
||||
AUTOSELECT=YES"
|
||||
.parse()
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio-lo")
|
||||
.language("es")
|
||||
.name("Espanol")
|
||||
.is_autoselect(true)
|
||||
.is_default(false)
|
||||
.uri("splo/prog_index.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
"#EXT-X-MEDIA:\
|
||||
TYPE=AUDIO,\
|
||||
URI=\"splo/prog_index.m3u8\",\
|
||||
GROUP-ID=\"audio-lo\",\
|
||||
LANGUAGE=\"es\",\
|
||||
NAME=\"Espanol\",\
|
||||
AUTOSELECT=YES"
|
||||
.parse()
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio-hi")
|
||||
.language("eng")
|
||||
.name("English")
|
||||
.is_autoselect(true)
|
||||
.is_default(true)
|
||||
.uri("eng/prog_index.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
"#EXT-X-MEDIA:\
|
||||
TYPE=AUDIO,\
|
||||
URI=\"eng/prog_index.m3u8\",\
|
||||
GROUP-ID=\"audio-hi\",\
|
||||
LANGUAGE=\"eng\",\
|
||||
NAME=\"English\",\
|
||||
DEFAULT=YES,\
|
||||
AUTOSELECT=YES"
|
||||
.parse()
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio-hi")
|
||||
.language("fre")
|
||||
.name("Français")
|
||||
.is_autoselect(true)
|
||||
.is_default(false)
|
||||
.uri("fre/prog_index.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
"#EXT-X-MEDIA:\
|
||||
TYPE=AUDIO,\
|
||||
URI=\"fre/prog_index.m3u8\",\
|
||||
GROUP-ID=\"audio-hi\",\
|
||||
LANGUAGE=\"fre\",\
|
||||
NAME=\"Français\",\
|
||||
AUTOSELECT=YES"
|
||||
.parse()
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio-hi")
|
||||
.language("es")
|
||||
.name("Espanol")
|
||||
.is_autoselect(true)
|
||||
.is_default(false)
|
||||
.uri("sp/prog_index.m3u8")
|
||||
.build()
|
||||
.unwrap(),
|
||||
"#EXT-X-MEDIA:\
|
||||
TYPE=AUDIO,\
|
||||
URI=\"sp/prog_index.m3u8\",\
|
||||
GROUP-ID=\"audio-hi\",\
|
||||
LANGUAGE=\"es\",\
|
||||
NAME=\"Espanol\",\
|
||||
AUTOSELECT=YES"
|
||||
.parse()
|
||||
.unwrap()
|
||||
);
|
||||
// ----
|
||||
assert_eq!(
|
||||
ExtXMedia::new(MediaType::Audio, "foo", "bar"),
|
||||
"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"foo\",NAME=\"bar\""
|
||||
|
@ -363,9 +1127,41 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
macro_rules! gen_required_version {
|
||||
( $( $id:expr => $output:expr, )* ) => {
|
||||
$(
|
||||
assert_eq!(
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::ClosedCaptions)
|
||||
.group_id("audio")
|
||||
.name("English")
|
||||
.instream_id($id)
|
||||
.build()
|
||||
.unwrap()
|
||||
.required_version(),
|
||||
$output
|
||||
);
|
||||
)*
|
||||
}
|
||||
}
|
||||
|
||||
gen_required_version![
|
||||
InStreamId::Cc1 => ProtocolVersion::V1,
|
||||
InStreamId::Cc2 => ProtocolVersion::V1,
|
||||
InStreamId::Cc3 => ProtocolVersion::V1,
|
||||
InStreamId::Cc4 => ProtocolVersion::V1,
|
||||
InStreamId::Service1 => ProtocolVersion::V7,
|
||||
];
|
||||
|
||||
assert_eq!(
|
||||
ExtXMedia::new(MediaType::Audio, "foo", "bar").required_version(),
|
||||
ExtXMedia::builder()
|
||||
.media_type(MediaType::Audio)
|
||||
.group_id("audio")
|
||||
.name("English")
|
||||
.build()
|
||||
.unwrap()
|
||||
.required_version(),
|
||||
ProtocolVersion::V1
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -100,9 +100,9 @@ mod test {
|
|||
EncryptionMethod::Aes128,
|
||||
"https://www.example.com/hls-key/key.bin",
|
||||
);
|
||||
key.set_iv([
|
||||
key.set_iv(Some([
|
||||
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
||||
]);
|
||||
]));
|
||||
|
||||
assert_eq!(
|
||||
key.to_string(),
|
||||
|
@ -129,9 +129,9 @@ mod test {
|
|||
EncryptionMethod::Aes128,
|
||||
"https://www.example.com/hls-key/key.bin",
|
||||
);
|
||||
key.set_iv([
|
||||
key.set_iv(Some([
|
||||
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,\
|
||||
|
|
|
@ -12,7 +12,7 @@ use crate::Error;
|
|||
/// [4.3.4.2. EXT-X-STREAM-INF]
|
||||
///
|
||||
/// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(PartialOrd, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ExtXStreamInf {
|
||||
uri: String,
|
||||
frame_rate: Option<DecimalFloatingPoint>,
|
||||
|
@ -57,7 +57,7 @@ impl ExtXStreamInf {
|
|||
|
||||
/// Sets the maximum frame rate for all the video in the variant stream.
|
||||
pub fn set_frame_rate(&mut self, value: Option<f64>) -> &mut Self {
|
||||
self.frame_rate = value.map(|v| v.into());
|
||||
self.frame_rate = value.map(|v| (v as u32).into());
|
||||
self
|
||||
}
|
||||
|
||||
|
|
|
@ -6,13 +6,13 @@ use std::time::Duration;
|
|||
use chrono::{DateTime, FixedOffset};
|
||||
|
||||
use crate::attribute::AttributePairs;
|
||||
use crate::types::{DecimalFloatingPoint, ProtocolVersion, RequiredVersion};
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
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
|
||||
#[allow(missing_docs)]
|
||||
|
@ -63,6 +63,36 @@ pub struct ExtXDateRange {
|
|||
|
||||
impl ExtXDateRange {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-DATERANGE:";
|
||||
|
||||
/// Makes a new [ExtXDateRange] tag.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXDateRange;
|
||||
/// use chrono::{DateTime, FixedOffset};
|
||||
/// use chrono::offset::TimeZone;
|
||||
///
|
||||
/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||
///
|
||||
/// ExtXDateRange::new("id", FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
/// .ymd(2010, 2, 19)
|
||||
/// .and_hms_milli(14, 54, 23, 31));
|
||||
/// ```
|
||||
pub fn new<T: ToString>(id: T, start_date: DateTime<FixedOffset>) -> Self {
|
||||
Self {
|
||||
id: id.to_string(),
|
||||
class: None,
|
||||
start_date,
|
||||
end_date: None,
|
||||
duration: None,
|
||||
planned_duration: None,
|
||||
scte35_cmd: None,
|
||||
scte35_out: None,
|
||||
scte35_in: None,
|
||||
end_on_next: false,
|
||||
client_attributes: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RequiredVersion for ExtXDateRange {
|
||||
|
@ -82,15 +112,11 @@ impl fmt::Display for ExtXDateRange {
|
|||
if let Some(value) = &self.end_date {
|
||||
write!(f, ",END-DATE={}", quote(value))?;
|
||||
}
|
||||
if let Some(x) = self.duration {
|
||||
write!(f, ",DURATION={}", DecimalFloatingPoint::from_duration(x))?;
|
||||
if let Some(value) = &self.duration {
|
||||
write!(f, ",DURATION={}", value.as_secs_f64())?;
|
||||
}
|
||||
if let Some(x) = self.planned_duration {
|
||||
write!(
|
||||
f,
|
||||
",PLANNED-DURATION={}",
|
||||
DecimalFloatingPoint::from_duration(x)
|
||||
)?;
|
||||
if let Some(value) = &self.planned_duration {
|
||||
write!(f, ",PLANNED-DURATION={}", value.as_secs_f64())?;
|
||||
}
|
||||
if let Some(value) = &self.scte35_cmd {
|
||||
write!(f, ",SCTE35-CMD={}", quote(value))?;
|
||||
|
@ -137,12 +163,10 @@ impl FromStr for ExtXDateRange {
|
|||
"START-DATE" => start_date = Some(unquote(value)),
|
||||
"END-DATE" => end_date = Some(unquote(value).parse()?),
|
||||
"DURATION" => {
|
||||
let seconds: DecimalFloatingPoint = (value.parse())?;
|
||||
duration = Some(seconds.to_duration());
|
||||
duration = Some(Duration::from_secs_f64(value.parse()?));
|
||||
}
|
||||
"PLANNED-DURATION" => {
|
||||
let seconds: DecimalFloatingPoint = (value.parse())?;
|
||||
planned_duration = Some(seconds.to_duration());
|
||||
planned_duration = Some(Duration::from_secs_f64(value.parse()?));
|
||||
}
|
||||
"SCTE35-CMD" => scte35_cmd = Some(unquote(value)),
|
||||
"SCTE35-OUT" => scte35_out = Some(unquote(value)),
|
||||
|
@ -164,15 +188,15 @@ impl FromStr for ExtXDateRange {
|
|||
}
|
||||
}
|
||||
|
||||
let id = id.ok_or_else(|| Error::missing_value("EXT-X-ID"))?;
|
||||
let id = id.ok_or_else(|| Error::missing_value("ID"))?;
|
||||
let start_date = start_date
|
||||
.ok_or_else(|| Error::missing_value("EXT-X-START-DATE"))?
|
||||
.ok_or_else(|| Error::missing_value("START-DATE"))?
|
||||
.parse()?;
|
||||
|
||||
if end_on_next && class.is_none() {
|
||||
return Err(Error::invalid_input());
|
||||
}
|
||||
Ok(ExtXDateRange {
|
||||
Ok(Self {
|
||||
id,
|
||||
class,
|
||||
start_date,
|
||||
|
@ -191,7 +215,21 @@ impl FromStr for ExtXDateRange {
|
|||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use chrono::offset::TimeZone;
|
||||
|
||||
#[test] // TODO; write some tests
|
||||
fn it_works() {}
|
||||
const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
ExtXDateRange::new(
|
||||
"id",
|
||||
FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
.ymd(2010, 2, 19)
|
||||
.and_hms_milli(14, 54, 23, 31)
|
||||
)
|
||||
.required_version(),
|
||||
ProtocolVersion::V1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ use std::fmt;
|
|||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::types::{DecimalFloatingPoint, ProtocolVersion, RequiredVersion};
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::utils::tag;
|
||||
use crate::Error;
|
||||
|
||||
|
@ -39,7 +39,7 @@ impl ExtInf {
|
|||
/// let ext_inf = ExtInf::new(Duration::from_secs(5));
|
||||
/// ```
|
||||
pub const fn new(duration: Duration) -> Self {
|
||||
ExtInf {
|
||||
Self {
|
||||
duration,
|
||||
title: None,
|
||||
}
|
||||
|
@ -55,7 +55,7 @@ impl ExtInf {
|
|||
/// let ext_inf = ExtInf::with_title(Duration::from_secs(5), "title");
|
||||
/// ```
|
||||
pub fn with_title<T: ToString>(duration: Duration, title: T) -> Self {
|
||||
ExtInf {
|
||||
Self {
|
||||
duration,
|
||||
title: Some(title.to_string()),
|
||||
}
|
||||
|
@ -170,7 +170,6 @@ impl FromStr for ExtInf {
|
|||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
let input = tag(input, Self::PREFIX)?;
|
||||
dbg!(&input);
|
||||
let tokens = input.splitn(2, ',').collect::<Vec<_>>();
|
||||
|
||||
if tokens.is_empty() {
|
||||
|
@ -180,7 +179,7 @@ impl FromStr for ExtInf {
|
|||
)));
|
||||
}
|
||||
|
||||
let duration = tokens[0].parse::<DecimalFloatingPoint>()?.to_duration();
|
||||
let duration = Duration::from_secs_f64(tokens[0].parse()?);
|
||||
|
||||
let title = {
|
||||
if tokens.len() >= 2 {
|
||||
|
@ -194,7 +193,7 @@ impl FromStr for ExtInf {
|
|||
}
|
||||
};
|
||||
|
||||
Ok(ExtInf { duration, title })
|
||||
Ok(Self { duration, title })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ use std::fmt;
|
|||
use std::ops::{Deref, DerefMut};
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::{DecryptionKey, EncryptionMethod, KeyFormatVersions};
|
||||
use crate::types::{DecryptionKey, EncryptionMethod};
|
||||
use crate::utils::tag;
|
||||
use crate::Error;
|
||||
|
||||
|
@ -64,13 +64,13 @@ impl ExtXKey {
|
|||
/// "#EXT-X-KEY:METHOD=NONE"
|
||||
/// );
|
||||
/// ```
|
||||
pub fn empty() -> Self {
|
||||
pub const fn empty() -> Self {
|
||||
Self(DecryptionKey {
|
||||
method: EncryptionMethod::None,
|
||||
uri: None,
|
||||
iv: None,
|
||||
key_format: None,
|
||||
key_format_versions: KeyFormatVersions::new(),
|
||||
key_format_versions: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -134,13 +134,13 @@ mod test {
|
|||
);
|
||||
|
||||
let mut key = ExtXKey::empty();
|
||||
// it is expected, that all attributes will be ignored in an empty key!
|
||||
// it is expected, that all attributes will be ignored for an empty key!
|
||||
key.set_key_format(Some(KeyFormat::Identity));
|
||||
key.set_iv([
|
||||
key.set_iv(Some([
|
||||
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
||||
]);
|
||||
]));
|
||||
key.set_uri(Some("https://www.example.com"));
|
||||
key.set_key_format_versions(vec![1, 2, 3]);
|
||||
key.set_key_format_versions(Some(vec![1, 2, 3]));
|
||||
|
||||
assert_eq!(key.to_string(), "#EXT-X-KEY:METHOD=NONE".to_string());
|
||||
}
|
||||
|
@ -163,8 +163,8 @@ mod test {
|
|||
EncryptionMethod::Aes128,
|
||||
"https://www.example.com/hls-key/key.bin",
|
||||
);
|
||||
key.set_iv([
|
||||
key.set_iv(Some([
|
||||
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
||||
]);
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ use crate::Error;
|
|||
/// [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
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
|
||||
pub struct ExtXIndependentSegments;
|
||||
|
||||
impl ExtXIndependentSegments {
|
||||
|
|
|
@ -9,7 +9,7 @@ use crate::Error;
|
|||
/// [4.3.5.2. EXT-X-START]
|
||||
///
|
||||
/// [4.3.5.2. EXT-X-START]: https://tools.ietf.org/html/rfc8216#section-4.3.5.2
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(PartialOrd, Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ExtXStart {
|
||||
time_offset: SignedDecimalFloatingPoint,
|
||||
precise: bool,
|
||||
|
|
|
@ -144,23 +144,29 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
let byte_range = ByteRange {
|
||||
length: 99999,
|
||||
start: Some(2),
|
||||
};
|
||||
assert_eq!(byte_range, "99999@2".parse::<ByteRange>().unwrap());
|
||||
assert_eq!(
|
||||
ByteRange {
|
||||
length: 99999,
|
||||
start: Some(2),
|
||||
},
|
||||
"99999@2".parse::<ByteRange>().unwrap()
|
||||
);
|
||||
|
||||
let byte_range = ByteRange {
|
||||
length: 99999,
|
||||
start: Some(2),
|
||||
};
|
||||
assert_eq!(byte_range, "99999@2".parse::<ByteRange>().unwrap());
|
||||
assert_eq!(
|
||||
ByteRange {
|
||||
length: 99999,
|
||||
start: Some(2),
|
||||
},
|
||||
"99999@2".parse::<ByteRange>().unwrap()
|
||||
);
|
||||
|
||||
let byte_range = ByteRange {
|
||||
length: 99999,
|
||||
start: None,
|
||||
};
|
||||
assert_eq!(byte_range, "99999".parse::<ByteRange>().unwrap());
|
||||
assert_eq!(
|
||||
ByteRange {
|
||||
length: 99999,
|
||||
start: None,
|
||||
},
|
||||
"99999".parse::<ByteRange>().unwrap()
|
||||
);
|
||||
|
||||
assert!("".parse::<ByteRange>().is_err());
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use core::ops::Deref;
|
||||
use core::str::FromStr;
|
||||
|
||||
use derive_more::Display;
|
||||
|
||||
use crate::Error;
|
||||
|
||||
|
@ -8,78 +9,71 @@ use crate::Error;
|
|||
///
|
||||
/// See: [4.2. Attribute Lists]
|
||||
///
|
||||
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
||||
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
|
||||
/// [4.2. Attribute Lists]:
|
||||
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-4.2
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, PartialOrd, Display)]
|
||||
pub(crate) struct DecimalFloatingPoint(f64);
|
||||
|
||||
impl DecimalFloatingPoint {
|
||||
/// Makes a new `DecimalFloatingPoint` instance.
|
||||
/// Makes a new [DecimalFloatingPoint] instance.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// The given value must have a positive sign and be finite,
|
||||
/// otherwise this function will return an error that has the kind `ErrorKind::InvalidInput`.
|
||||
pub fn new(n: f64) -> crate::Result<Self> {
|
||||
if n.is_sign_negative() || n.is_infinite() {
|
||||
pub fn new(value: f64) -> crate::Result<Self> {
|
||||
if value.is_sign_negative() || value.is_infinite() {
|
||||
return Err(Error::invalid_input());
|
||||
}
|
||||
Ok(Self(n))
|
||||
Ok(Self(value))
|
||||
}
|
||||
|
||||
/// Converts `DecimalFloatingPoint` to `f64`.
|
||||
pub(crate) const fn from_f64_unchecked(value: f64) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
|
||||
/// Converts [DecimalFloatingPoint] to [f64].
|
||||
pub const fn as_f64(self) -> f64 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub(crate) fn to_duration(self) -> Duration {
|
||||
Duration::from_secs_f64(self.0)
|
||||
}
|
||||
|
||||
pub(crate) fn from_duration(value: Duration) -> Self {
|
||||
Self::from(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for DecimalFloatingPoint {
|
||||
fn from(f: u32) -> Self {
|
||||
Self(f64::from(f))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Duration> for DecimalFloatingPoint {
|
||||
fn from(value: Duration) -> Self {
|
||||
Self(value.as_secs_f64())
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for DecimalFloatingPoint {}
|
||||
|
||||
impl fmt::Display for DecimalFloatingPoint {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
// this trait is implemented manually, so it doesn't construct a [DecimalFloatingPoint],
|
||||
// with a negative value.
|
||||
impl FromStr for DecimalFloatingPoint {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
if !input.chars().all(|c| c.is_digit(10) || c == '.') {
|
||||
return Err(Error::invalid_input());
|
||||
}
|
||||
Self::new(input.parse()?)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for DecimalFloatingPoint {
|
||||
type Target = f64;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for DecimalFloatingPoint {
|
||||
fn from(value: f64) -> Self {
|
||||
Self(value)
|
||||
let mut result = value;
|
||||
|
||||
// guard against the unlikely case of an infinite value...
|
||||
if result.is_infinite() {
|
||||
result = 0.0;
|
||||
}
|
||||
|
||||
Self(result.abs())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f32> for DecimalFloatingPoint {
|
||||
fn from(value: f32) -> Self {
|
||||
Self(value.into())
|
||||
(value as f64).into()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,6 +81,24 @@ impl From<f32> for DecimalFloatingPoint {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
macro_rules! test_from {
|
||||
( $($input:expr),* ) => {
|
||||
use ::core::convert::From;
|
||||
|
||||
#[test]
|
||||
fn test_from() {
|
||||
$(
|
||||
assert_eq!(
|
||||
DecimalFloatingPoint::from($input),
|
||||
DecimalFloatingPoint::new(1.0).unwrap(),
|
||||
);
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test_from![1u8, 1u16, 1u32, 1.0f32, -1.0f32, 1.0f64, -1.0f64];
|
||||
|
||||
#[test]
|
||||
pub fn test_display() {
|
||||
let decimal_floating_point = DecimalFloatingPoint::new(22.0).unwrap();
|
||||
|
@ -111,6 +123,7 @@ mod tests {
|
|||
);
|
||||
|
||||
assert!("1#".parse::<DecimalFloatingPoint>().is_err());
|
||||
assert!("-1.0".parse::<DecimalFloatingPoint>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -125,28 +138,15 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_from_duration() {
|
||||
fn test_from_inf() {
|
||||
assert_eq!(
|
||||
DecimalFloatingPoint::from_duration(Duration::from_nanos(11_234_500_112_345)),
|
||||
DecimalFloatingPoint::new(11234.500112345).unwrap()
|
||||
DecimalFloatingPoint::from(::std::f64::INFINITY),
|
||||
DecimalFloatingPoint::new(0.0).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from() {
|
||||
assert_eq!(
|
||||
DecimalFloatingPoint::from(1u32),
|
||||
DecimalFloatingPoint::new(1.0).unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
DecimalFloatingPoint::from(1 as f64),
|
||||
DecimalFloatingPoint::new(1.0).unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
DecimalFloatingPoint::from(1 as f32),
|
||||
DecimalFloatingPoint::new(1.0).unwrap()
|
||||
);
|
||||
fn test_deref() {
|
||||
assert_eq!(DecimalFloatingPoint::from(0.1).floor(), 0.0);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use derive_more::Display;
|
||||
|
||||
use crate::Error;
|
||||
|
||||
/// Decimal resolution.
|
||||
|
@ -8,7 +9,8 @@ use crate::Error;
|
|||
/// See: [4.2. Attribute Lists]
|
||||
///
|
||||
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display)]
|
||||
#[display(fmt = "{}x{}", width, height)]
|
||||
pub(crate) struct DecimalResolution {
|
||||
width: usize,
|
||||
height: usize,
|
||||
|
@ -43,20 +45,10 @@ impl DecimalResolution {
|
|||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DecimalResolution {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}x{}", self.width, self.height)
|
||||
}
|
||||
}
|
||||
|
||||
/// [DecimalResolution] can be constructed from a tuple; (width, height).
|
||||
impl<T, U> From<(T, U)> for DecimalResolution
|
||||
where
|
||||
T: Into<usize>,
|
||||
U: Into<usize>,
|
||||
{
|
||||
fn from(value: (T, U)) -> Self {
|
||||
Self::new(value.0.into(), value.1.into())
|
||||
/// [DecimalResolution] can be constructed from a tuple; `(width, height)`.
|
||||
impl From<(usize, usize)> for DecimalResolution {
|
||||
fn from(value: (usize, usize)) -> Self {
|
||||
DecimalResolution::new(value.0, value.1)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -73,12 +65,9 @@ impl FromStr for DecimalResolution {
|
|||
)));
|
||||
}
|
||||
|
||||
let width = tokens[0];
|
||||
let height = tokens[1];
|
||||
|
||||
Ok(Self {
|
||||
width: width.parse()?,
|
||||
height: height.parse()?,
|
||||
width: tokens[0].parse()?,
|
||||
height: tokens[1].parse()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -129,4 +118,12 @@ mod tests {
|
|||
12
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from() {
|
||||
assert_eq!(
|
||||
DecimalResolution::from((1920, 1080)),
|
||||
DecimalResolution::new(1920, 1080)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ use crate::utils::{quote, unquote};
|
|||
use crate::Error;
|
||||
|
||||
#[derive(Builder, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[builder(setter(into))]
|
||||
#[builder(setter(into), build_fn(validate = "Self::validate"))]
|
||||
/// [DecryptionKey] contains data, that is shared between [ExtXSessionKey] and [ExtXKey].
|
||||
///
|
||||
/// [ExtXSessionKey]: crate::tags::ExtXSessionKey
|
||||
|
@ -30,9 +30,18 @@ pub struct DecryptionKey {
|
|||
/// A string that specifies how the key is
|
||||
/// represented in the resource identified by the `URI`.
|
||||
pub(crate) key_format: Option<KeyFormat>,
|
||||
#[builder(setter(into), default)]
|
||||
/// The `KEYFORMATVERSIONS` attribute.
|
||||
pub(crate) key_format_versions: KeyFormatVersions,
|
||||
#[builder(setter(into, strip_option), default)]
|
||||
/// The [KeyFormatVersions] attribute.
|
||||
pub(crate) key_format_versions: Option<KeyFormatVersions>,
|
||||
}
|
||||
|
||||
impl DecryptionKeyBuilder {
|
||||
fn validate(&self) -> Result<(), String> {
|
||||
if self.method != Some(EncryptionMethod::None) && self.uri.is_none() {
|
||||
return Err(Error::custom("Missing URL").to_string());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl DecryptionKey {
|
||||
|
@ -54,7 +63,7 @@ impl DecryptionKey {
|
|||
uri: Some(uri.to_string()),
|
||||
iv: None,
|
||||
key_format: None,
|
||||
key_format_versions: KeyFormatVersions::new(),
|
||||
key_format_versions: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -103,13 +112,15 @@ impl DecryptionKey {
|
|||
/// "METHOD=SAMPLE-AES,URI=\"https://www.example.com/\"".to_string()
|
||||
/// );
|
||||
/// ```
|
||||
pub fn set_method(&mut self, value: EncryptionMethod) {
|
||||
pub fn set_method(&mut self, value: EncryptionMethod) -> &mut Self {
|
||||
self.method = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns an `URI`, that specifies how to obtain the key.
|
||||
///
|
||||
/// This attribute is required, if the [EncryptionMethod] is not None.
|
||||
/// # Note
|
||||
/// This attribute is required, if the [EncryptionMethod] is not `None`.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
|
@ -152,14 +163,13 @@ impl DecryptionKey {
|
|||
/// "METHOD=AES-128,URI=\"http://www.google.com/\"".to_string()
|
||||
/// );
|
||||
/// ```
|
||||
pub fn set_uri<T: ToString>(&mut self, value: Option<T>) {
|
||||
pub fn set_uri<T: ToString>(&mut self, value: Option<T>) -> &mut Self {
|
||||
self.uri = value.map(|v| v.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the IV (Initialization Vector) attribute.
|
||||
///
|
||||
/// This attribute is optional.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::types::DecryptionKey;
|
||||
|
@ -170,9 +180,10 @@ impl DecryptionKey {
|
|||
/// "https://www.example.com/"
|
||||
/// );
|
||||
///
|
||||
/// key.set_iv([
|
||||
/// # assert_eq!(key.iv(), None);
|
||||
/// key.set_iv(Some([
|
||||
/// 1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2, 3, 4, 5, 6, 7
|
||||
/// ]);
|
||||
/// ]));
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// key.iv(),
|
||||
|
@ -189,8 +200,6 @@ impl DecryptionKey {
|
|||
|
||||
/// Sets the `IV` attribute.
|
||||
///
|
||||
/// This attribute is optional.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::types::DecryptionKey;
|
||||
|
@ -201,27 +210,26 @@ impl DecryptionKey {
|
|||
/// "https://www.example.com/"
|
||||
/// );
|
||||
///
|
||||
/// key.set_iv([
|
||||
/// key.set_iv(Some([
|
||||
/// 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)
|
||||
pub fn set_iv<T>(&mut self, value: Option<T>) -> &mut Self
|
||||
where
|
||||
T: Into<[u8; 16]>,
|
||||
{
|
||||
self.iv = Some(InitializationVector(value.into()));
|
||||
self.iv = value.map(|v| InitializationVector(v.into()));
|
||||
self
|
||||
}
|
||||
|
||||
/// 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;
|
||||
|
@ -243,9 +251,7 @@ impl DecryptionKey {
|
|||
self.key_format
|
||||
}
|
||||
|
||||
/// Sets the `KEYFORMAT` attribute.
|
||||
///
|
||||
/// This attribute is optional.
|
||||
/// Sets the [KeyFormat] attribute.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
|
@ -264,14 +270,13 @@ impl DecryptionKey {
|
|||
/// Some(KeyFormat::Identity)
|
||||
/// );
|
||||
/// ```
|
||||
pub fn set_key_format<T: Into<KeyFormat>>(&mut self, value: Option<T>) {
|
||||
pub fn set_key_format<T: Into<KeyFormat>>(&mut self, value: Option<T>) -> &mut Self {
|
||||
self.key_format = value.map(|v| v.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the [KeyFormatVersions] attribute.
|
||||
///
|
||||
/// This attribute is optional.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::types::DecryptionKey;
|
||||
|
@ -282,21 +287,19 @@ impl DecryptionKey {
|
|||
/// "https://www.example.com/"
|
||||
/// );
|
||||
///
|
||||
/// key.set_key_format_versions(vec![1, 2, 3, 4, 5]);
|
||||
/// key.set_key_format_versions(Some(vec![1, 2, 3, 4, 5]));
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// key.key_format_versions(),
|
||||
/// &KeyFormatVersions::from(vec![1, 2, 3, 4, 5])
|
||||
/// &Some(KeyFormatVersions::from(vec![1, 2, 3, 4, 5]))
|
||||
/// );
|
||||
/// ```
|
||||
pub const fn key_format_versions(&self) -> &KeyFormatVersions {
|
||||
pub const fn key_format_versions(&self) -> &Option<KeyFormatVersions> {
|
||||
&self.key_format_versions
|
||||
}
|
||||
|
||||
/// Sets the [KeyFormatVersions] attribute.
|
||||
///
|
||||
/// This attribute is optional.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::types::DecryptionKey;
|
||||
|
@ -307,21 +310,25 @@ impl DecryptionKey {
|
|||
/// "https://www.example.com/"
|
||||
/// );
|
||||
///
|
||||
/// key.set_key_format_versions(vec![1, 2, 3, 4, 5]);
|
||||
/// key.set_key_format_versions(Some(vec![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: Into<KeyFormatVersions>>(&mut self, value: T) {
|
||||
self.key_format_versions = value.into();
|
||||
pub fn set_key_format_versions<T: Into<KeyFormatVersions>>(
|
||||
&mut self,
|
||||
value: Option<T>,
|
||||
) -> &mut Self {
|
||||
self.key_format_versions = value.map(|v| v.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RequiredVersion for DecryptionKey {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
if self.key_format.is_some() || !self.key_format_versions.is_default() {
|
||||
if self.key_format.is_some() || self.key_format_versions.is_some() {
|
||||
ProtocolVersion::V5
|
||||
} else if self.iv.is_some() {
|
||||
ProtocolVersion::V2
|
||||
|
@ -360,12 +367,12 @@ impl FromStr for DecryptionKey {
|
|||
return Err(Error::missing_value("URI"));
|
||||
}
|
||||
|
||||
Ok(DecryptionKey {
|
||||
Ok(Self {
|
||||
method,
|
||||
uri,
|
||||
iv,
|
||||
key_format,
|
||||
key_format_versions: key_format_versions.unwrap_or_default(),
|
||||
key_format_versions,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -386,8 +393,11 @@ impl fmt::Display for DecryptionKey {
|
|||
if let Some(value) = &self.key_format {
|
||||
write!(f, ",KEYFORMAT={}", quote(value))?;
|
||||
}
|
||||
if !self.key_format_versions.is_default() {
|
||||
write!(f, ",KEYFORMATVERSIONS={}", &self.key_format_versions)?;
|
||||
|
||||
if let Some(key_format_versions) = &self.key_format_versions {
|
||||
if !key_format_versions.is_default() {
|
||||
write!(f, ",KEYFORMATVERSIONS={}", key_format_versions)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -410,6 +420,7 @@ mod test {
|
|||
.key_format_versions(vec![1, 2, 3, 4, 5])
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
key.to_string(),
|
||||
"METHOD=AES-128,\
|
||||
|
@ -419,7 +430,13 @@ mod test {
|
|||
KEYFORMATVERSIONS=\"1/2/3/4/5\"\
|
||||
"
|
||||
.to_string()
|
||||
)
|
||||
);
|
||||
|
||||
assert!(DecryptionKey::builder().build().is_err());
|
||||
assert!(DecryptionKey::builder()
|
||||
.method(EncryptionMethod::Aes128)
|
||||
.build()
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -428,9 +445,9 @@ mod test {
|
|||
EncryptionMethod::Aes128,
|
||||
"https://www.example.com/hls-key/key.bin",
|
||||
);
|
||||
key.set_iv([
|
||||
key.set_iv(Some([
|
||||
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
||||
]);
|
||||
]));
|
||||
|
||||
assert_eq!(
|
||||
key.to_string(),
|
||||
|
@ -458,9 +475,9 @@ mod test {
|
|||
EncryptionMethod::Aes128,
|
||||
"https://www.example.com/hls-key/key.bin",
|
||||
);
|
||||
key.set_iv([
|
||||
key.set_iv(Some([
|
||||
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
||||
]);
|
||||
]));
|
||||
|
||||
assert_eq!(
|
||||
"METHOD=AES-128,\
|
||||
|
@ -472,9 +489,9 @@ mod test {
|
|||
);
|
||||
|
||||
let mut key = DecryptionKey::new(EncryptionMethod::Aes128, "http://www.example.com");
|
||||
key.set_iv([
|
||||
key.set_iv(Some([
|
||||
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
||||
]);
|
||||
]));
|
||||
key.set_key_format(Some(KeyFormat::Identity));
|
||||
|
||||
assert_eq!(
|
||||
|
@ -485,7 +502,30 @@ mod test {
|
|||
.parse::<DecryptionKey>()
|
||||
.unwrap(),
|
||||
key
|
||||
)
|
||||
);
|
||||
|
||||
key.set_key_format_versions(Some(vec![1, 2, 3]));
|
||||
assert_eq!(
|
||||
"METHOD=AES-128,\
|
||||
URI=\"http://www.example.com\",\
|
||||
IV=0x10ef8f758ca555115584bb5b3c687f52,\
|
||||
KEYFORMAT=\"identity\",\
|
||||
KEYFORMATVERSIONS=\"1/2/3\""
|
||||
.parse::<DecryptionKey>()
|
||||
.unwrap(),
|
||||
key
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
"METHOD=AES-128,\
|
||||
URI=\"http://www.example.com\",\
|
||||
UNKNOWNTAG=abcd"
|
||||
.parse::<DecryptionKey>()
|
||||
.unwrap(),
|
||||
DecryptionKey::new(EncryptionMethod::Aes128, "http://www.example.com")
|
||||
);
|
||||
assert!("METHOD=AES-128,URI=".parse::<DecryptionKey>().is_err());
|
||||
assert!("garbage".parse::<DecryptionKey>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::Error;
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
/// Encryption method.
|
||||
///
|
||||
|
@ -9,7 +6,8 @@ use crate::Error;
|
|||
///
|
||||
/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)]
|
||||
#[strum(serialize_all = "SCREAMING-KEBAB-CASE")]
|
||||
pub enum EncryptionMethod {
|
||||
/// `None` means that [MediaSegment]s are not encrypted.
|
||||
///
|
||||
|
@ -26,6 +24,7 @@ pub enum EncryptionMethod {
|
|||
/// [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
|
||||
#[strum(serialize = "AES-128")]
|
||||
Aes128,
|
||||
/// `SampleAes` means that the [MediaSegment]s
|
||||
/// contain media samples, such as audio or video, that are encrypted
|
||||
|
@ -48,32 +47,6 @@ pub enum EncryptionMethod {
|
|||
SampleAes,
|
||||
}
|
||||
|
||||
impl fmt::Display for EncryptionMethod {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match &self {
|
||||
EncryptionMethod::Aes128 => "AES-128".fmt(f),
|
||||
EncryptionMethod::SampleAes => "SAMPLE-AES".fmt(f),
|
||||
EncryptionMethod::None => "NONE".fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for EncryptionMethod {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
match input {
|
||||
"AES-128" => Ok(EncryptionMethod::Aes128),
|
||||
"SAMPLE-AES" => Ok(EncryptionMethod::SampleAes),
|
||||
"NONE" => Ok(EncryptionMethod::None),
|
||||
_ => Err(Error::custom(format!(
|
||||
"Unknown encryption method: {:?}",
|
||||
input
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::Error;
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
/// HDCP level.
|
||||
///
|
||||
|
@ -9,33 +6,14 @@ use crate::Error;
|
|||
///
|
||||
/// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)]
|
||||
#[strum(serialize_all = "SCREAMING-KEBAB-CASE")]
|
||||
pub enum HdcpLevel {
|
||||
#[strum(serialize = "TYPE-0")]
|
||||
Type0,
|
||||
None,
|
||||
}
|
||||
|
||||
impl fmt::Display for HdcpLevel {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match &self {
|
||||
HdcpLevel::Type0 => "TYPE-0".fmt(f),
|
||||
HdcpLevel::None => "NONE".fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for HdcpLevel {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
match input {
|
||||
"TYPE-0" => Ok(HdcpLevel::Type0),
|
||||
"NONE" => Ok(HdcpLevel::None),
|
||||
_ => Err(Error::custom(format!("Unknown HDCP level: {:?}", input))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::Error;
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
/// Identifier of a rendition within the segments in a media playlist.
|
||||
///
|
||||
|
@ -9,7 +6,8 @@ use crate::Error;
|
|||
///
|
||||
/// [4.3.4.1. EXT-X-MEDIA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.1
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display, EnumString)]
|
||||
#[strum(serialize_all = "UPPERCASE")]
|
||||
pub enum InStreamId {
|
||||
Cc1,
|
||||
Cc2,
|
||||
|
@ -80,89 +78,6 @@ pub enum InStreamId {
|
|||
Service63,
|
||||
}
|
||||
|
||||
impl fmt::Display for InStreamId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
format!("{:?}", self).to_uppercase().fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for InStreamId {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
Ok(match input {
|
||||
"CC1" => Self::Cc1,
|
||||
"CC2" => Self::Cc2,
|
||||
"CC3" => Self::Cc3,
|
||||
"CC4" => Self::Cc4,
|
||||
"SERVICE1" => Self::Service1,
|
||||
"SERVICE2" => Self::Service2,
|
||||
"SERVICE3" => Self::Service3,
|
||||
"SERVICE4" => Self::Service4,
|
||||
"SERVICE5" => Self::Service5,
|
||||
"SERVICE6" => Self::Service6,
|
||||
"SERVICE7" => Self::Service7,
|
||||
"SERVICE8" => Self::Service8,
|
||||
"SERVICE9" => Self::Service9,
|
||||
"SERVICE10" => Self::Service10,
|
||||
"SERVICE11" => Self::Service11,
|
||||
"SERVICE12" => Self::Service12,
|
||||
"SERVICE13" => Self::Service13,
|
||||
"SERVICE14" => Self::Service14,
|
||||
"SERVICE15" => Self::Service15,
|
||||
"SERVICE16" => Self::Service16,
|
||||
"SERVICE17" => Self::Service17,
|
||||
"SERVICE18" => Self::Service18,
|
||||
"SERVICE19" => Self::Service19,
|
||||
"SERVICE20" => Self::Service20,
|
||||
"SERVICE21" => Self::Service21,
|
||||
"SERVICE22" => Self::Service22,
|
||||
"SERVICE23" => Self::Service23,
|
||||
"SERVICE24" => Self::Service24,
|
||||
"SERVICE25" => Self::Service25,
|
||||
"SERVICE26" => Self::Service26,
|
||||
"SERVICE27" => Self::Service27,
|
||||
"SERVICE28" => Self::Service28,
|
||||
"SERVICE29" => Self::Service29,
|
||||
"SERVICE30" => Self::Service30,
|
||||
"SERVICE31" => Self::Service31,
|
||||
"SERVICE32" => Self::Service32,
|
||||
"SERVICE33" => Self::Service33,
|
||||
"SERVICE34" => Self::Service34,
|
||||
"SERVICE35" => Self::Service35,
|
||||
"SERVICE36" => Self::Service36,
|
||||
"SERVICE37" => Self::Service37,
|
||||
"SERVICE38" => Self::Service38,
|
||||
"SERVICE39" => Self::Service39,
|
||||
"SERVICE40" => Self::Service40,
|
||||
"SERVICE41" => Self::Service41,
|
||||
"SERVICE42" => Self::Service42,
|
||||
"SERVICE43" => Self::Service43,
|
||||
"SERVICE44" => Self::Service44,
|
||||
"SERVICE45" => Self::Service45,
|
||||
"SERVICE46" => Self::Service46,
|
||||
"SERVICE47" => Self::Service47,
|
||||
"SERVICE48" => Self::Service48,
|
||||
"SERVICE49" => Self::Service49,
|
||||
"SERVICE50" => Self::Service50,
|
||||
"SERVICE51" => Self::Service51,
|
||||
"SERVICE52" => Self::Service52,
|
||||
"SERVICE53" => Self::Service53,
|
||||
"SERVICE54" => Self::Service54,
|
||||
"SERVICE55" => Self::Service55,
|
||||
"SERVICE56" => Self::Service56,
|
||||
"SERVICE57" => Self::Service57,
|
||||
"SERVICE58" => Self::Service58,
|
||||
"SERVICE59" => Self::Service59,
|
||||
"SERVICE60" => Self::Service60,
|
||||
"SERVICE61" => Self::Service61,
|
||||
"SERVICE62" => Self::Service62,
|
||||
"SERVICE63" => Self::Service63,
|
||||
_ => return Err(Error::custom(format!("Unknown instream id: {:?}", input))),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -13,7 +13,7 @@ use crate::Error;
|
|||
pub struct InitializationVector(pub [u8; 16]);
|
||||
|
||||
impl InitializationVector {
|
||||
/// Converts the initialization vector to a slice.
|
||||
/// Converts the [InitializationVector] to a slice.
|
||||
pub const fn to_slice(&self) -> [u8; 16] {
|
||||
self.0
|
||||
}
|
||||
|
@ -51,21 +51,106 @@ impl fmt::Display for InitializationVector {
|
|||
impl FromStr for InitializationVector {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if !(s.starts_with("0x") || s.starts_with("0X")) {
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
if !(input.starts_with("0x") || input.starts_with("0X")) {
|
||||
return Err(Error::invalid_input());
|
||||
}
|
||||
if s.len() - 2 != 32 {
|
||||
if input.len() - 2 != 32 {
|
||||
return Err(Error::invalid_input());
|
||||
}
|
||||
|
||||
let mut v = [0; 16];
|
||||
for (i, c) in s.as_bytes().chunks(2).skip(1).enumerate() {
|
||||
let mut result = [0; 16];
|
||||
for (i, c) in input.as_bytes().chunks(2).skip(1).enumerate() {
|
||||
let d = std::str::from_utf8(c).map_err(Error::custom)?;
|
||||
let b = u8::from_str_radix(d, 16).map_err(Error::custom)?;
|
||||
v[i] = b;
|
||||
result[i] = b;
|
||||
}
|
||||
|
||||
Ok(InitializationVector(v))
|
||||
Ok(Self(result))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(
|
||||
"0x10ef8f758ca555115584bb5b3c687f52".to_string(),
|
||||
InitializationVector([
|
||||
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82
|
||||
])
|
||||
.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(
|
||||
"0x10ef8f758ca555115584bb5b3c687f52"
|
||||
.parse::<InitializationVector>()
|
||||
.unwrap(),
|
||||
InitializationVector([
|
||||
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82
|
||||
])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
"0X10ef8f758ca555115584bb5b3c687f52"
|
||||
.parse::<InitializationVector>()
|
||||
.unwrap(),
|
||||
InitializationVector([
|
||||
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82
|
||||
])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
"0X10EF8F758CA555115584BB5B3C687F52"
|
||||
.parse::<InitializationVector>()
|
||||
.unwrap(),
|
||||
InitializationVector([
|
||||
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82
|
||||
])
|
||||
);
|
||||
|
||||
assert!("garbage".parse::<InitializationVector>().is_err());
|
||||
assert!("0xgarbage".parse::<InitializationVector>().is_err());
|
||||
assert!("0x12".parse::<InitializationVector>().is_err());
|
||||
assert!("0X10EF8F758CA555115584BB5B3C687F5Z"
|
||||
.parse::<InitializationVector>()
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_as_ref() {
|
||||
assert_eq!(
|
||||
InitializationVector([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).as_ref(),
|
||||
&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deref() {
|
||||
assert_eq!(
|
||||
InitializationVector([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).deref(),
|
||||
&[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from() {
|
||||
assert_eq!(
|
||||
InitializationVector::from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
|
||||
InitializationVector([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_slice() {
|
||||
assert_eq!(
|
||||
InitializationVector([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).to_slice(),
|
||||
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -54,6 +54,8 @@ mod tests {
|
|||
assert_eq!(KeyFormat::Identity, quote("identity").parse().unwrap());
|
||||
|
||||
assert_eq!(KeyFormat::Identity, "identity".parse().unwrap());
|
||||
|
||||
assert!("garbage".parse::<KeyFormat>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -37,7 +37,7 @@ impl KeyFormatVersions {
|
|||
|
||||
/// Returns `true`, if [KeyFormatVersions] has the default value of `vec![1]`.
|
||||
pub fn is_default(&self) -> bool {
|
||||
self.0 == vec![1] || self.0.is_empty()
|
||||
self.0 == vec![1] && self.0.len() == 1 || self.0.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,15 +1,9 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
use crate::Error;
|
||||
|
||||
/// Media type.
|
||||
///
|
||||
/// See: [4.3.4.1. EXT-X-MEDIA]
|
||||
///
|
||||
/// [4.3.4.1. EXT-X-MEDIA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.1
|
||||
/// Specifies the media type.
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Ord, PartialOrd, Display, EnumString, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[strum(serialize_all = "SCREAMING-KEBAB-CASE")]
|
||||
pub enum MediaType {
|
||||
Audio,
|
||||
Video,
|
||||
|
@ -17,29 +11,29 @@ pub enum MediaType {
|
|||
ClosedCaptions,
|
||||
}
|
||||
|
||||
impl fmt::Display for MediaType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match &self {
|
||||
MediaType::Audio => "AUDIO".fmt(f),
|
||||
MediaType::Video => "VIDEO".fmt(f),
|
||||
MediaType::Subtitles => "SUBTITLES".fmt(f),
|
||||
MediaType::ClosedCaptions => "CLOSED-CAPTIONS".fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for MediaType {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
Ok(match input {
|
||||
"AUDIO" => MediaType::Audio,
|
||||
"VIDEO" => MediaType::Video,
|
||||
"SUBTITLES" => MediaType::Subtitles,
|
||||
"CLOSED-CAPTIONS" => MediaType::ClosedCaptions,
|
||||
_ => {
|
||||
return Err(Error::invalid_input());
|
||||
}
|
||||
})
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(MediaType::Audio, "AUDIO".parse().unwrap());
|
||||
assert_eq!(MediaType::Video, "VIDEO".parse().unwrap());
|
||||
assert_eq!(MediaType::Subtitles, "SUBTITLES".parse().unwrap());
|
||||
assert_eq!(
|
||||
MediaType::ClosedCaptions,
|
||||
"CLOSED-CAPTIONS".parse().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(MediaType::Audio.to_string(), "AUDIO".to_string());
|
||||
assert_eq!(MediaType::Video.to_string(), "VIDEO".to_string());
|
||||
assert_eq!(MediaType::Subtitles.to_string(), "SUBTITLES".to_string());
|
||||
assert_eq!(
|
||||
MediaType::ClosedCaptions.to_string(),
|
||||
"CLOSED-CAPTIONS".to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,9 +27,12 @@ pub trait RequiredVersion {
|
|||
fn required_version(&self) -> ProtocolVersion;
|
||||
}
|
||||
|
||||
/// [7. Protocol Version Compatibility]
|
||||
/// # [7. Protocol Version Compatibility]
|
||||
/// The [ProtocolVersion] specifies, which m3u8 revision is required, to parse
|
||||
/// a certain tag correctly.
|
||||
///
|
||||
/// [7. Protocol Version Compatibility]: https://tools.ietf.org/html/rfc8216#section-7
|
||||
/// [7. Protocol Version Compatibility]:
|
||||
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#section-7
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum ProtocolVersion {
|
||||
|
@ -43,7 +46,13 @@ pub enum ProtocolVersion {
|
|||
}
|
||||
|
||||
impl ProtocolVersion {
|
||||
/// Returns the newest ProtocolVersion, that is supported by this library.
|
||||
/// Returns the newest [ProtocolVersion], that is supported by this library.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::types::ProtocolVersion;
|
||||
/// assert_eq!(ProtocolVersion::latest(), ProtocolVersion::V7);
|
||||
/// ```
|
||||
pub const fn latest() -> Self {
|
||||
Self::V7
|
||||
}
|
||||
|
@ -51,18 +60,15 @@ impl ProtocolVersion {
|
|||
|
||||
impl fmt::Display for ProtocolVersion {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let n = {
|
||||
match &self {
|
||||
Self::V1 => 1,
|
||||
Self::V2 => 2,
|
||||
Self::V3 => 3,
|
||||
Self::V4 => 4,
|
||||
Self::V5 => 5,
|
||||
Self::V6 => 6,
|
||||
Self::V7 => 7,
|
||||
}
|
||||
};
|
||||
write!(f, "{}", n)
|
||||
match &self {
|
||||
Self::V1 => write!(f, "1"),
|
||||
Self::V2 => write!(f, "2"),
|
||||
Self::V3 => write!(f, "3"),
|
||||
Self::V4 => write!(f, "4"),
|
||||
Self::V5 => write!(f, "5"),
|
||||
Self::V6 => write!(f, "6"),
|
||||
Self::V7 => write!(f, "7"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -115,5 +121,18 @@ mod tests {
|
|||
assert_eq!(ProtocolVersion::V5, "5".parse().unwrap());
|
||||
assert_eq!(ProtocolVersion::V6, "6".parse().unwrap());
|
||||
assert_eq!(ProtocolVersion::V7, "7".parse().unwrap());
|
||||
|
||||
assert_eq!(ProtocolVersion::V7, " 7 ".parse().unwrap());
|
||||
assert!("garbage".parse::<ProtocolVersion>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default() {
|
||||
assert_eq!(ProtocolVersion::default(), ProtocolVersion::V1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_latest() {
|
||||
assert_eq!(ProtocolVersion::latest(), ProtocolVersion::V7);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::Error;
|
||||
use core::ops::Deref;
|
||||
use derive_more::{Display, FromStr};
|
||||
|
||||
/// Signed decimal floating-point number.
|
||||
///
|
||||
/// See: [4.2. Attribute Lists]
|
||||
///
|
||||
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
||||
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, PartialOrd, Display, FromStr)]
|
||||
pub(crate) struct SignedDecimalFloatingPoint(f64);
|
||||
|
||||
impl SignedDecimalFloatingPoint {
|
||||
|
@ -16,11 +14,15 @@ impl SignedDecimalFloatingPoint {
|
|||
///
|
||||
/// # Panics
|
||||
/// The given value must be finite, otherwise this function will panic!
|
||||
pub fn new(n: f64) -> Self {
|
||||
if n.is_infinite() {
|
||||
pub fn new(value: f64) -> Self {
|
||||
if value.is_infinite() {
|
||||
panic!("Floating point value must be finite!");
|
||||
}
|
||||
Self(n)
|
||||
Self(value)
|
||||
}
|
||||
|
||||
pub(crate) const fn from_f64_unchecked(value: f64) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
|
||||
/// Converts [DecimalFloatingPoint] to [f64].
|
||||
|
@ -29,32 +31,47 @@ impl SignedDecimalFloatingPoint {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<i32> for SignedDecimalFloatingPoint {
|
||||
fn from(f: i32) -> Self {
|
||||
Self(f64::from(f))
|
||||
impl Deref for SignedDecimalFloatingPoint {
|
||||
type Target = f64;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for SignedDecimalFloatingPoint {}
|
||||
|
||||
impl fmt::Display for SignedDecimalFloatingPoint {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for SignedDecimalFloatingPoint {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
Ok(Self::new(input.parse().map_err(Error::parse_float_error)?))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
macro_rules! test_from {
|
||||
( $( $input:expr => $output:expr ),* ) => {
|
||||
use ::core::convert::From;
|
||||
|
||||
#[test]
|
||||
fn test_from() {
|
||||
$(
|
||||
assert_eq!(
|
||||
$input,
|
||||
$output,
|
||||
);
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test_from![
|
||||
SignedDecimalFloatingPoint::from(1u8) => SignedDecimalFloatingPoint::new(1.0),
|
||||
SignedDecimalFloatingPoint::from(1i8) => SignedDecimalFloatingPoint::new(1.0),
|
||||
SignedDecimalFloatingPoint::from(1u16) => SignedDecimalFloatingPoint::new(1.0),
|
||||
SignedDecimalFloatingPoint::from(1i16) => SignedDecimalFloatingPoint::new(1.0),
|
||||
SignedDecimalFloatingPoint::from(1u32) => SignedDecimalFloatingPoint::new(1.0),
|
||||
SignedDecimalFloatingPoint::from(1i32) => SignedDecimalFloatingPoint::new(1.0),
|
||||
SignedDecimalFloatingPoint::from(1.0f32) => SignedDecimalFloatingPoint::new(1.0),
|
||||
SignedDecimalFloatingPoint::from(1.0f64) => SignedDecimalFloatingPoint::new(1.0)
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(
|
||||
|
@ -75,13 +92,17 @@ mod tests {
|
|||
SignedDecimalFloatingPoint::new(1.0),
|
||||
"1.0".parse::<SignedDecimalFloatingPoint>().unwrap()
|
||||
);
|
||||
|
||||
assert!("garbage".parse::<SignedDecimalFloatingPoint>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_from() {
|
||||
assert_eq!(
|
||||
SignedDecimalFloatingPoint::from(1i32),
|
||||
SignedDecimalFloatingPoint::new(1.0)
|
||||
);
|
||||
fn test_as_f64() {
|
||||
assert_eq!(SignedDecimalFloatingPoint::new(1.0).as_f64(), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deref() {
|
||||
assert_eq!(SignedDecimalFloatingPoint::from(0.1).floor(), 0.0);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,14 +3,13 @@ use std::str::FromStr;
|
|||
|
||||
use crate::attribute::AttributePairs;
|
||||
use crate::types::{DecimalResolution, HdcpLevel};
|
||||
use crate::utils::parse_u64;
|
||||
use crate::utils::{quote, unquote};
|
||||
use crate::Error;
|
||||
|
||||
/// [4.3.4.2. EXT-X-STREAM-INF]
|
||||
///
|
||||
/// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[derive(PartialOrd, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct StreamInf {
|
||||
bandwidth: u64,
|
||||
average_bandwidth: Option<u64>,
|
||||
|
@ -182,6 +181,8 @@ impl StreamInf {
|
|||
///
|
||||
/// stream.set_resolution(1920, 1080);
|
||||
/// assert_eq!(stream.resolution(), Some((1920, 1080)));
|
||||
/// # stream.set_resolution(1280, 10);
|
||||
/// # assert_eq!(stream.resolution(), Some((1280, 10)));
|
||||
/// ```
|
||||
pub fn set_resolution(&mut self, width: usize, height: usize) -> &mut Self {
|
||||
if let Some(res) = &mut self.resolution {
|
||||
|
@ -259,8 +260,8 @@ impl FromStr for StreamInf {
|
|||
|
||||
for (key, value) in input.parse::<AttributePairs>()? {
|
||||
match key.as_str() {
|
||||
"BANDWIDTH" => bandwidth = Some(parse_u64(value)?),
|
||||
"AVERAGE-BANDWIDTH" => average_bandwidth = Some(parse_u64(value)?),
|
||||
"BANDWIDTH" => bandwidth = Some(value.parse::<u64>()?),
|
||||
"AVERAGE-BANDWIDTH" => average_bandwidth = Some(value.parse::<u64>()?),
|
||||
"CODECS" => codecs = Some(unquote(value)),
|
||||
"RESOLUTION" => resolution = Some(value.parse()?),
|
||||
"HDCP-LEVEL" => hdcp_level = Some(value.parse()?),
|
||||
|
@ -284,3 +285,66 @@ impl FromStr for StreamInf {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
let mut stream_inf = StreamInf::new(200);
|
||||
stream_inf.set_average_bandwidth(Some(15));
|
||||
stream_inf.set_codecs(Some("mp4a.40.2,avc1.4d401e"));
|
||||
stream_inf.set_resolution(1920, 1080);
|
||||
stream_inf.set_hdcp_level(Some(HdcpLevel::Type0));
|
||||
stream_inf.set_video(Some("video"));
|
||||
|
||||
assert_eq!(
|
||||
stream_inf.to_string(),
|
||||
"BANDWIDTH=200,\
|
||||
AVERAGE-BANDWIDTH=15,\
|
||||
CODECS=\"mp4a.40.2,avc1.4d401e\",\
|
||||
RESOLUTION=1920x1080,\
|
||||
HDCP-LEVEL=TYPE-0,\
|
||||
VIDEO=\"video\""
|
||||
.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
let mut stream_inf = StreamInf::new(200);
|
||||
stream_inf.set_average_bandwidth(Some(15));
|
||||
stream_inf.set_codecs(Some("mp4a.40.2,avc1.4d401e"));
|
||||
stream_inf.set_resolution(1920, 1080);
|
||||
stream_inf.set_hdcp_level(Some(HdcpLevel::Type0));
|
||||
stream_inf.set_video(Some("video"));
|
||||
|
||||
assert_eq!(
|
||||
stream_inf,
|
||||
"BANDWIDTH=200,\
|
||||
AVERAGE-BANDWIDTH=15,\
|
||||
CODECS=\"mp4a.40.2,avc1.4d401e\",\
|
||||
RESOLUTION=1920x1080,\
|
||||
HDCP-LEVEL=TYPE-0,\
|
||||
VIDEO=\"video\""
|
||||
.parse()
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
stream_inf,
|
||||
"BANDWIDTH=200,\
|
||||
AVERAGE-BANDWIDTH=15,\
|
||||
CODECS=\"mp4a.40.2,avc1.4d401e\",\
|
||||
RESOLUTION=1920x1080,\
|
||||
HDCP-LEVEL=TYPE-0,\
|
||||
VIDEO=\"video\",\
|
||||
UNKNOWN=\"value\""
|
||||
.parse()
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
assert!("garbage".parse::<StreamInf>().is_err());
|
||||
}
|
||||
}
|
||||
|
|
33
src/utils.rs
33
src/utils.rs
|
@ -1,5 +1,26 @@
|
|||
use crate::Error;
|
||||
|
||||
macro_rules! impl_from {
|
||||
( $($( $type:tt ),* => $target:path ),* ) => {
|
||||
use ::core::convert::From;
|
||||
|
||||
$( // repeat $target
|
||||
$( // repeat $type
|
||||
impl From<$type> for $target {
|
||||
fn from(value: $type) -> Self {
|
||||
Self::from_f64_unchecked(value.into())
|
||||
}
|
||||
}
|
||||
)*
|
||||
)*
|
||||
};
|
||||
}
|
||||
|
||||
impl_from![
|
||||
u8, u16, u32 => crate::types::DecimalFloatingPoint,
|
||||
u8, i8, u16, i16, u32, i32, f32, f64 => crate::types::SignedDecimalFloatingPoint
|
||||
];
|
||||
|
||||
pub(crate) fn parse_yes_or_no<T: AsRef<str>>(s: T) -> crate::Result<bool> {
|
||||
match s.as_ref() {
|
||||
"YES" => Ok(true),
|
||||
|
@ -8,11 +29,6 @@ pub(crate) fn parse_yes_or_no<T: AsRef<str>>(s: T) -> crate::Result<bool> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_u64<T: AsRef<str>>(s: T) -> crate::Result<u64> {
|
||||
let n = s.as_ref().parse().map_err(Error::unknown)?; // TODO: Error::number
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
/// According to the documentation the following characters are forbidden
|
||||
/// inside a quoted string:
|
||||
/// - carriage return (`\r`)
|
||||
|
@ -64,13 +80,6 @@ mod tests {
|
|||
assert!(parse_yes_or_no("garbage").is_err());
|
||||
}
|
||||
|
||||
#[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());
|
||||
|
|
352
tarpaulin-report.html
Normal file
352
tarpaulin-report.html
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue