1
0
Fork 0
mirror of https://github.com/sile/hls_m3u8.git synced 2024-06-02 21:49:35 +00:00

more documentation #31 + tests #25

This commit is contained in:
Luro02 2019-09-22 18:00:38 +02:00
parent 3240417304
commit 0c4fa008e6
28 changed files with 984 additions and 335 deletions

View file

@ -110,7 +110,7 @@ mod test {
#[test] #[test]
fn test_parser() { fn test_parser() {
let pairs = ("FOO=BAR,BAR=\"baz,qux\",ABC=12.3") let pairs = "FOO=BAR,BAR=\"baz,qux\",ABC=12.3"
.parse::<AttributePairs>() .parse::<AttributePairs>()
.unwrap(); .unwrap();

View file

@ -69,6 +69,10 @@ pub enum ErrorKind {
/// An Error from a Builder. /// An Error from a Builder.
BuilderError(String), BuilderError(String),
#[fail(display = "Missing Attribute: {}", _0)]
/// An attribute is missing.
MissingAttribute(String),
/// Hints that destructuring should not be exhaustive. /// Hints that destructuring should not be exhaustive.
/// ///
/// This enum may grow additional variants, so this makes sure clients /// This enum may grow additional variants, so this makes sure clients
@ -185,6 +189,10 @@ impl Error {
pub(crate) fn chrono<T: ToString>(value: T) -> Self { pub(crate) fn chrono<T: ToString>(value: T) -> Self {
Self::from(ErrorKind::ChronoParseError(value.to_string())) Self::from(ErrorKind::ChronoParseError(value.to_string()))
} }
pub(crate) fn missing_attribute<T: ToString>(value: T) -> Self {
Self::from(ErrorKind::MissingAttribute(value.to_string()))
}
} }
impl From<::std::num::ParseIntError> for Error { impl From<::std::num::ParseIntError> for Error {

View file

@ -5,10 +5,18 @@ use crate::types::{ProtocolVersion, RequiredVersion};
use crate::utils::tag; use crate::utils::tag;
use crate::Error; use crate::Error;
/// [4.3.1.1. EXTM3U] /// # [4.3.1.1. EXTM3U]
/// The [ExtM3u] tag indicates that the file is an Extended [M3U]
/// Playlist file.
/// ///
/// Its format is:
/// ```text
/// #EXTM3U
/// ```
///
/// [M3U]: https://en.wikipedia.org/wiki/M3U
/// [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(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
pub struct ExtM3u; pub struct ExtM3u;
impl ExtM3u { impl ExtM3u {

View file

@ -5,21 +5,40 @@ use crate::types::{ProtocolVersion, RequiredVersion};
use crate::utils::tag; use crate::utils::tag;
use crate::Error; use crate::Error;
/// [4.3.1.2. EXT-X-VERSION] /// # [4.3.1.2. EXT-X-VERSION]
/// The [ExtXVersion] tag indicates the compatibility version of the
/// Playlist file, its associated media, and its server.
///
/// The [ExtXVersion] tag applies to the entire Playlist file. Its
/// format is:
///
/// ```text
/// #EXT-X-VERSION:<n>
/// ```
/// where `n` is an integer indicating the protocol compatibility version
/// number.
/// ///
/// [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(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub struct ExtXVersion(ProtocolVersion); pub struct ExtXVersion(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.
///
/// # Example
/// ```
/// # use hls_m3u8::tags::ExtXVersion;
/// use hls_m3u8::types::ProtocolVersion;
///
/// let version_tag = ExtXVersion::new(ProtocolVersion::V2);
/// ```
pub const fn new(version: ProtocolVersion) -> Self { pub const fn new(version: ProtocolVersion) -> Self {
Self(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.
/// ///
/// # Example /// # Example
/// ``` /// ```
@ -84,8 +103,8 @@ mod test {
#[test] #[test]
fn test_parser() { fn test_parser() {
assert_eq!( assert_eq!(
"#EXT-X-VERSION:6".parse().ok(), "#EXT-X-VERSION:6".parse::<ExtXVersion>().unwrap(),
Some(ExtXVersion::new(ProtocolVersion::V6)) ExtXVersion::new(ProtocolVersion::V6)
); );
} }

View file

@ -10,7 +10,7 @@ use crate::Error;
/// # [4.3.4.3. EXT-X-I-FRAME-STREAM-INF] /// # [4.3.4.3. EXT-X-I-FRAME-STREAM-INF]
/// The [ExtXIFrameStreamInf] tag identifies a [Media Playlist] file /// The [ExtXIFrameStreamInf] tag identifies a [Media Playlist] file
/// containing the I-frames of a multimedia presentation. It stands /// containing the I-frames of a multimedia presentation. It stands
/// alone, in that it does not apply to a particular URI in the [Master Playlist]. /// alone, in that it does not apply to a particular `URI` in the [Master Playlist].
/// ///
/// Its format is: /// Its format is:
/// ///
@ -38,7 +38,7 @@ impl ExtXIFrameStreamInf {
} }
} }
/// Returns the URI, that identifies the associated media playlist. /// Returns the `URI`, that identifies the associated media playlist.
/// ///
/// # Example /// # Example
/// ``` /// ```
@ -51,7 +51,7 @@ impl ExtXIFrameStreamInf {
&self.uri &self.uri
} }
/// Sets the URI, that identifies the associated media playlist. /// Sets the `URI`, that identifies the associated media playlist.
/// ///
/// # Example /// # Example
/// ``` /// ```
@ -126,22 +126,20 @@ mod test {
#[test] #[test]
fn test_display() { fn test_display() {
let text = r#"#EXT-X-I-FRAME-STREAM-INF:URI="foo",BANDWIDTH=1000"#; assert_eq!(
assert_eq!(ExtXIFrameStreamInf::new("foo", 1000).to_string(), text); ExtXIFrameStreamInf::new("foo", 1000).to_string(),
"#EXT-X-I-FRAME-STREAM-INF:URI=\"foo\",BANDWIDTH=1000".to_string()
);
} }
#[test] #[test]
fn test_parser() { 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!( assert_eq!(
text.parse::<ExtXIFrameStreamInf>().unwrap(), "#EXT-X-I-FRAME-STREAM-INF:URI=\"foo\",BANDWIDTH=1000"
i_frame_stream_inf.clone() .parse::<ExtXIFrameStreamInf>()
.unwrap(),
ExtXIFrameStreamInf::new("foo", 1000)
); );
assert_eq!(i_frame_stream_inf.uri(), "foo");
assert_eq!(i_frame_stream_inf.bandwidth(), 1000);
// TODO: test all the optional fields
} }
#[test] #[test]

View file

@ -8,9 +8,23 @@ use crate::types::{InStreamId, MediaType, ProtocolVersion, RequiredVersion};
use crate::utils::{parse_yes_or_no, quote, tag, unquote}; use crate::utils::{parse_yes_or_no, quote, tag, unquote};
use crate::Error; use crate::Error;
/// [4.3.4.1. EXT-X-MEDIA] /// # [4.4.4.1. EXT-X-MEDIA]
/// The [ExtXMedia] tag is used to relate [Media Playlist]s that contain
/// alternative Renditions of the same content. For
/// example, three [ExtXMedia] tags can be used to identify audio-only
/// [Media Playlist]s, that contain English, French, and Spanish Renditions
/// of the same presentation. Or, two [ExtXMedia] tags can be used to
/// identify video-only [Media Playlist]s that show two different camera
/// angles.
/// ///
/// [4.3.4.1. EXT-X-MEDIA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.1 /// Its format is:
/// ```text
/// #EXT-X-MEDIA:<attribute-list>
/// ```
///
/// [Media Playlist]: crate::MediaPlaylist
/// [4.4.4.1. EXT-X-MEDIA]:
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.4.1
#[derive(Builder, Debug, Clone, PartialEq, Eq, Hash)] #[derive(Builder, Debug, Clone, PartialEq, Eq, Hash)]
#[builder(setter(into))] #[builder(setter(into))]
#[builder(build_fn(validate = "Self::validate"))] #[builder(build_fn(validate = "Self::validate"))]
@ -54,22 +68,27 @@ impl ExtXMediaBuilder {
fn validate(&self) -> Result<(), String> { fn validate(&self) -> Result<(), String> {
let media_type = self let media_type = self
.media_type .media_type
.ok_or(Error::missing_value("self.media_type").to_string())?; .ok_or(Error::missing_attribute("MEDIA-TYPE").to_string())?;
if MediaType::ClosedCaptions == media_type { if MediaType::ClosedCaptions == media_type {
if let None = self.uri { if self.uri.is_some() {
return Err(Error::missing_value("self.uri").to_string()); return Err(Error::custom(
"Unexpected attribute: \"URL\" for MediaType::ClosedCaptions!",
)
.to_string());
} }
self.instream_id self.instream_id
.ok_or(Error::missing_value("self.instream_id").to_string())?; .ok_or(Error::missing_attribute("INSTREAM-ID").to_string())?;
} else { } else {
if let Some(_) = &self.instream_id { if self.instream_id.is_some() {
return Err(Error::invalid_input().to_string()); return Err(Error::custom("Unexpected attribute: \"INSTREAM-ID\"!").to_string());
} }
} }
if self.is_default.unwrap_or(false) && self.is_autoselect.unwrap_or(false) { if self.is_default.unwrap_or(false) && !self.is_autoselect.unwrap_or(false) {
return Err(Error::invalid_input().to_string()); return Err(
Error::custom("If `DEFAULT` is true, `AUTOSELECT` has to be true too!").to_string(),
);
} }
if MediaType::Subtitles != media_type { if MediaType::Subtitles != media_type {
@ -85,7 +104,7 @@ impl ExtXMediaBuilder {
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<T: ToString>(media_type: MediaType, group_id: T, name: T) -> Self { pub fn new<T: ToString>(media_type: MediaType, group_id: T, name: T) -> Self {
ExtXMedia { ExtXMedia {
media_type, media_type,
@ -284,6 +303,52 @@ mod test {
#[test] #[test]
fn test_display() { fn test_display() {
// 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()
.to_string(),
"#EXT-X-MEDIA:\
TYPE=AUDIO,\
URI=\"eng/prog_index.m3u8\",\
GROUP-ID=\"audio\",\
LANGUAGE=\"eng\",\
NAME=\"English\",\
DEFAULT=YES,\
AUTOSELECT=YES"
.to_string()
);
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()
.to_string(),
"#EXT-X-MEDIA:\
TYPE=AUDIO,\
URI=\"fre/prog_index.m3u8\",\
GROUP-ID=\"audio\",\
LANGUAGE=\"fre\",\
NAME=\"Français\",\
AUTOSELECT=YES"
.to_string()
);
assert_eq!( assert_eq!(
ExtXMedia::new(MediaType::Audio, "foo", "bar").to_string(), ExtXMedia::new(MediaType::Audio, "foo", "bar").to_string(),
"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"foo\",NAME=\"bar\"".to_string() "#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"foo\",NAME=\"bar\"".to_string()

View file

@ -22,11 +22,12 @@ pub enum SessionData {
Uri(String), Uri(String),
} }
/// [4.3.4.4. EXT-X-SESSION-DATA] /// # [4.3.4.4. EXT-X-SESSION-DATA]
/// ///
/// The [ExtXSessionData] tag allows arbitrary session data to be /// The [ExtXSessionData] tag allows arbitrary session data to be
/// carried in a [Master Playlist](crate::MasterPlaylist). /// carried in a [Master Playlist].
/// ///
/// [Master Playlist]: crate::MasterPlaylist
/// [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(Builder, Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)] #[derive(Builder, Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)]
#[builder(setter(into))] #[builder(setter(into))]
@ -319,40 +320,73 @@ mod test {
#[test] #[test]
fn test_display() { fn test_display() {
let tag = ExtXSessionData::new("foo", SessionData::Value("bar".into())); assert_eq!(
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar""#; "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.lyrics\",URI=\"lyrics.json\"".to_string(),
assert_eq!(tag.to_string(), text); ExtXSessionData::new(
"com.example.lyrics",
SessionData::Uri("lyrics.json".to_string())
)
.to_string()
);
let tag = ExtXSessionData::new("foo", SessionData::Uri("bar".into())); assert_eq!(
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",URI="bar""#; "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\
assert_eq!(tag.to_string(), text); VALUE=\"This is an example\",LANGUAGE=\"en\""
.to_string(),
ExtXSessionData::with_language(
"com.example.title",
SessionData::Value("This is an example".to_string()),
"en"
)
.to_string()
);
let tag = ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz"); assert_eq!(
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar",LANGUAGE="baz""#; "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\
assert_eq!(tag.to_string(), text); VALUE=\"Este es un ejemplo\",LANGUAGE=\"es\""
.to_string(),
ExtXSessionData::with_language(
"com.example.title",
SessionData::Value("Este es un ejemplo".to_string()),
"es"
)
.to_string()
);
assert_eq!(
"#EXT-X-SESSION-DATA:DATA-ID=\"foo\",VALUE=\"bar\"".to_string(),
ExtXSessionData::new("foo", SessionData::Value("bar".into())).to_string()
);
assert_eq!(
"#EXT-X-SESSION-DATA:DATA-ID=\"foo\",URI=\"bar\"".to_string(),
ExtXSessionData::new("foo", SessionData::Uri("bar".into())).to_string()
);
assert_eq!(
"#EXT-X-SESSION-DATA:DATA-ID=\"foo\",VALUE=\"bar\",LANGUAGE=\"baz\"".to_string(),
ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz")
.to_string()
);
} }
#[test] #[test]
fn test_parser() { fn test_parser() {
let tag = "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.lyrics\",URI=\"lyrics.json\""
.parse::<ExtXSessionData>()
.unwrap();
assert_eq!( assert_eq!(
tag, "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.lyrics\",URI=\"lyrics.json\""
.parse::<ExtXSessionData>()
.unwrap(),
ExtXSessionData::new( ExtXSessionData::new(
"com.example.lyrics", "com.example.lyrics",
SessionData::Uri("lyrics.json".to_string()) SessionData::Uri("lyrics.json".to_string())
) )
); );
let tag = "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\
LANGUAGE=\"en\", VALUE=\"This is an example\""
.parse::<ExtXSessionData>()
.unwrap();
assert_eq!( assert_eq!(
tag, "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\
LANGUAGE=\"en\", VALUE=\"This is an example\""
.parse::<ExtXSessionData>()
.unwrap(),
ExtXSessionData::with_language( ExtXSessionData::with_language(
"com.example.title", "com.example.title",
SessionData::Value("This is an example".to_string()), SessionData::Value("This is an example".to_string()),
@ -360,13 +394,11 @@ mod test {
) )
); );
let tag = "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\
LANGUAGE=\"es\", VALUE=\"Este es un ejemplo\""
.parse::<ExtXSessionData>()
.unwrap();
assert_eq!( assert_eq!(
tag, "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\
LANGUAGE=\"es\", VALUE=\"Este es un ejemplo\""
.parse::<ExtXSessionData>()
.unwrap(),
ExtXSessionData::with_language( ExtXSessionData::with_language(
"com.example.title", "com.example.title",
SessionData::Value("Este es un ejemplo".to_string()), SessionData::Value("Este es un ejemplo".to_string()),
@ -374,16 +406,37 @@ mod test {
) )
); );
let tag = ExtXSessionData::new("foo", SessionData::Value("bar".into())); assert_eq!(
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar""#; "#EXT-X-SESSION-DATA:DATA-ID=\"foo\",VALUE=\"bar\""
assert_eq!(text.parse::<ExtXSessionData>().unwrap(), tag); .parse::<ExtXSessionData>()
.unwrap(),
ExtXSessionData::new("foo", SessionData::Value("bar".into()))
);
let tag = ExtXSessionData::new("foo", SessionData::Uri("bar".into())); assert_eq!(
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",URI="bar""#; "#EXT-X-SESSION-DATA:DATA-ID=\"foo\",URI=\"bar\""
assert_eq!(text.parse::<ExtXSessionData>().unwrap(), tag); .parse::<ExtXSessionData>()
.unwrap(),
ExtXSessionData::new("foo", SessionData::Uri("bar".into()))
);
let tag = ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz"); assert_eq!(
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar",LANGUAGE="baz""#; "#EXT-X-SESSION-DATA:DATA-ID=\"foo\",VALUE=\"bar\",LANGUAGE=\"baz\""
assert_eq!(text.parse::<ExtXSessionData>().unwrap(), tag); .parse::<ExtXSessionData>()
.unwrap(),
ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz")
);
}
#[test]
fn test_required_version() {
assert_eq!(
ExtXSessionData::new(
"com.example.lyrics",
SessionData::Uri("lyrics.json".to_string())
)
.required_version(),
ProtocolVersion::V1
);
} }
} }

View file

@ -6,8 +6,19 @@ use crate::types::{DecryptionKey, EncryptionMethod, ProtocolVersion, RequiredVer
use crate::utils::tag; use crate::utils::tag;
use crate::Error; use crate::Error;
/// [4.3.4.5. EXT-X-SESSION-KEY] /// # [4.3.4.5. EXT-X-SESSION-KEY]
/// The [ExtXSessionKey] tag allows encryption keys from [Media Playlist]s
/// to be specified in a [Master Playlist]. This allows the client to
/// preload these keys without having to read the [Media Playlist]s
/// first.
/// ///
/// Its format is:
/// ```text
/// #EXT-X-SESSION-KEY:<attribute-list>
/// ```
///
/// [Media Playlist]: crate::MediaPlaylist
/// [Master Playlist]: crate::MasterPlaylist
/// [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(DecryptionKey); pub struct ExtXSessionKey(DecryptionKey);
@ -16,8 +27,21 @@ 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.
///
/// # Panic /// # Panic
/// This method will panic, if the [EncryptionMethod] is None. /// An [ExtXSessionKey] should only be used, if the segments of the stream are encrypted.
/// Therefore this function will panic, if the `method` is [EncryptionMethod::None].
///
/// # Example
/// ```
/// # use hls_m3u8::tags::ExtXSessionKey;
/// use hls_m3u8::types::EncryptionMethod;
///
/// let session_key = ExtXSessionKey::new(
/// EncryptionMethod::Aes128,
/// "https://www.example.com/"
/// );
/// ```
pub fn new<T: ToString>(method: EncryptionMethod, uri: T) -> Self { pub fn new<T: ToString>(method: EncryptionMethod, uri: T) -> Self {
if method == EncryptionMethod::None { if method == EncryptionMethod::None {
panic!("The EncryptionMethod is not allowed to be None"); panic!("The EncryptionMethod is not allowed to be None");
@ -29,18 +53,15 @@ impl ExtXSessionKey {
impl RequiredVersion for ExtXSessionKey { impl RequiredVersion for ExtXSessionKey {
fn required_version(&self) -> ProtocolVersion { fn required_version(&self) -> ProtocolVersion {
if self.0.key_format.is_some() | self.0.key_format_versions.is_some() { self.0.required_version()
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 {
if self.0.method == EncryptionMethod::None {
return Err(fmt::Error);
}
write!(f, "{}{}", Self::PREFIX, self.0) write!(f, "{}{}", Self::PREFIX, self.0)
} }
} }
@ -71,7 +92,7 @@ impl DerefMut for ExtXSessionKey {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::types::EncryptionMethod; use crate::types::{EncryptionMethod, KeyFormat};
#[test] #[test]
fn test_display() { fn test_display() {
@ -121,11 +142,16 @@ mod test {
key key
); );
key.set_key_format("baz"); key.set_key_format(Some(KeyFormat::Identity));
assert_eq!( assert_eq!(
r#"#EXT-X-SESSION-KEY:METHOD=AES-128,URI="https://www.example.com/hls-key/key.bin",IV=0x10ef8f758ca555115584bb5b3c687f52,KEYFORMAT="baz""# "#EXT-X-SESSION-KEY:\
.parse::<ExtXSessionKey>().unwrap(), METHOD=AES-128,\
URI=\"https://www.example.com/hls-key/key.bin\",\
IV=0x10ef8f758ca555115584bb5b3c687f52,\
KEYFORMAT=\"identity\""
.parse::<ExtXSessionKey>()
.unwrap(),
key key
) )
} }

View file

@ -44,16 +44,18 @@ impl ExtXStreamInf {
} }
} }
/// Sets the `URI` that identifies the associated media playlist.
pub fn set_uri<T: ToString>(&mut self, value: T) -> &mut Self { pub fn set_uri<T: ToString>(&mut self, value: T) -> &mut Self {
self.uri = value.to_string(); self.uri = value.to_string();
self self
} }
/// Returns the URI that identifies the associated media playlist. /// Returns the `URI` that identifies the associated media playlist.
pub const fn uri(&self) -> &String { pub const fn uri(&self) -> &String {
&self.uri &self.uri
} }
/// 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 { 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.into());
self self

View file

@ -4,25 +4,67 @@ use std::str::FromStr;
use crate::types::{ProtocolVersion, RequiredVersion}; use crate::types::{ProtocolVersion, RequiredVersion};
use crate::utils::tag; use crate::utils::tag;
/// [4.3.3.3. EXT-X-DISCONTINUITY-SEQUENCE] /// # [4.4.3.3. EXT-X-DISCONTINUITY-SEQUENCE]
/// ///
/// [4.3.3.3. EXT-X-DISCONTINUITY-SEQUENCE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.3 /// The [ExtXDiscontinuitySequence] tag allows synchronization between
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] /// different Renditions of the same Variant Stream or different Variant
/// Streams that have [ExtXDiscontinuity] tags in their [Media Playlist]s.
///
/// Its format is:
/// ```text
/// #EXT-X-DISCONTINUITY-SEQUENCE:<number>
/// ```
/// where `number` is a [u64].
///
/// [ExtXDiscontinuity]: crate::tags::ExtXDiscontinuity
/// [Media Playlist]: crate::MediaPlaylist
/// [4.4.3.3. EXT-X-DISCONTINUITY-SEQUENCE]:
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.3
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub struct ExtXDiscontinuitySequence(u64); pub struct ExtXDiscontinuitySequence(u64);
impl ExtXDiscontinuitySequence { 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.
///
/// # Example
/// ```
/// # use hls_m3u8::tags::ExtXDiscontinuitySequence;
/// let discontinuity_sequence = ExtXDiscontinuitySequence::new(5);
/// ```
pub const fn new(seq_num: u64) -> Self { pub const fn new(seq_num: u64) -> Self {
Self(seq_num) Self(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.
///
/// # Example
/// ```
/// # use hls_m3u8::tags::ExtXDiscontinuitySequence;
/// let discontinuity_sequence = ExtXDiscontinuitySequence::new(5);
///
/// assert_eq!(discontinuity_sequence.seq_num(), 5);
/// ```
pub const fn seq_num(&self) -> u64 { pub const fn seq_num(&self) -> u64 {
self.0 self.0
} }
/// Sets the sequence number.
///
/// # Example
/// ```
/// # use hls_m3u8::tags::ExtXDiscontinuitySequence;
/// let mut discontinuity_sequence = ExtXDiscontinuitySequence::new(5);
///
/// discontinuity_sequence.set_seq_num(10);
/// assert_eq!(discontinuity_sequence.seq_num(), 10);
/// ```
pub fn set_seq_num(&mut self, value: u64) -> &mut Self {
self.0 = value;
self
}
} }
impl RequiredVersion for ExtXDiscontinuitySequence { impl RequiredVersion for ExtXDiscontinuitySequence {

View file

@ -5,9 +5,19 @@ use crate::types::{ProtocolVersion, RequiredVersion};
use crate::utils::tag; use crate::utils::tag;
use crate::Error; use crate::Error;
/// [4.3.3.4. EXT-X-ENDLIST] /// # [4.4.3.4. EXT-X-ENDLIST]
/// The [ExtXEndList] tag indicates, that no more [Media Segment]s will be
/// added to the [Media Playlist] file.
/// ///
/// [4.3.3.4. EXT-X-ENDLIST]: https://tools.ietf.org/html/rfc8216#section-4.3.3.4 /// Its format is:
/// ```text
/// #EXT-X-ENDLIST
/// ```
///
/// [Media Segment]: crate::MediaSegment
/// [Media Playlist]: crate::MediaPlaylist
/// [4.4.3.4. EXT-X-ENDLIST]:
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.4
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ExtXEndList; pub struct ExtXEndList;

View file

@ -5,9 +5,21 @@ use crate::types::{ProtocolVersion, RequiredVersion};
use crate::utils::tag; use crate::utils::tag;
use crate::Error; use crate::Error;
/// [4.3.3.6. EXT-X-I-FRAMES-ONLY] /// # [4.4.3.6. EXT-X-I-FRAMES-ONLY]
/// The [ExtXIFramesOnly] tag indicates that each [Media Segment] in the
/// Playlist describes a single I-frame. I-frames are encoded video
/// frames, whose decoding does not depend on any other frame. I-frame
/// Playlists can be used for trick play, such as fast forward, rapid
/// reverse, and scrubbing.
/// ///
/// [4.3.3.6. EXT-X-I-FRAMES-ONLY]: https://tools.ietf.org/html/rfc8216#section-4.3.3.6 /// Its format is:
/// ```text
/// #EXT-X-I-FRAMES-ONLY
/// ```
///
/// [Media Segment]: crate::MediaSegment
/// [4.4.3.6. EXT-X-I-FRAMES-ONLY]:
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.6
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ExtXIFramesOnly; pub struct ExtXIFramesOnly;

View file

@ -5,25 +5,64 @@ use crate::types::{ProtocolVersion, RequiredVersion};
use crate::utils::tag; use crate::utils::tag;
use crate::Error; use crate::Error;
/// [4.3.3.2. EXT-X-MEDIA-SEQUENCE] /// # [4.4.3.2. EXT-X-MEDIA-SEQUENCE]
/// The [ExtXMediaSequence] tag indicates the Media Sequence Number of
/// the first [Media Segment] that appears in a Playlist file.
/// ///
/// [4.3.3.2. EXT-X-MEDIA-SEQUENCE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.2 /// Its format is:
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] /// ```text
/// #EXT-X-MEDIA-SEQUENCE:<number>
/// ```
/// where `number` is a [u64].
///
/// [Media Segment]: crate::MediaSegment
/// [4.4.3.2. EXT-X-MEDIA-SEQUENCE]:
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.2
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub struct ExtXMediaSequence(u64); pub struct ExtXMediaSequence(u64);
impl ExtXMediaSequence { 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.
///
/// # Example
/// ```
/// # use hls_m3u8::tags::ExtXMediaSequence;
/// let media_sequence = ExtXMediaSequence::new(5);
/// ```
pub const fn new(seq_num: u64) -> Self { pub const fn new(seq_num: u64) -> Self {
Self(seq_num) Self(seq_num)
} }
/// Returns the sequence number of the first media segment, /// Returns the sequence number of the first media segment,
/// that appears in the associated playlist. /// that appears in the associated playlist.
///
/// # Example
/// ```
/// # use hls_m3u8::tags::ExtXMediaSequence;
/// let media_sequence = ExtXMediaSequence::new(5);
///
/// assert_eq!(media_sequence.seq_num(), 5);
/// ```
pub const fn seq_num(&self) -> u64 { pub const fn seq_num(&self) -> u64 {
self.0 self.0
} }
/// Sets the sequence number.
///
/// # Example
/// ```
/// # use hls_m3u8::tags::ExtXMediaSequence;
/// let mut media_sequence = ExtXMediaSequence::new(5);
///
/// media_sequence.set_seq_num(10);
/// assert_eq!(media_sequence.seq_num(), 10);
/// ```
pub fn set_seq_num(&mut self, value: u64) -> &mut Self {
self.0 = value;
self
}
} }
impl RequiredVersion for ExtXMediaSequence { impl RequiredVersion for ExtXMediaSequence {

View file

@ -5,26 +5,25 @@ use crate::types::{ProtocolVersion, RequiredVersion};
use crate::utils::tag; use crate::utils::tag;
use crate::Error; use crate::Error;
/// [4.3.3.5. EXT-X-PLAYLIST-TYPE](https://tools.ietf.org/html/rfc8216#section-4.3.3.5) /// # [4.4.3.5. EXT-X-PLAYLIST-TYPE]
/// ///
/// The EXT-X-PLAYLIST-TYPE tag provides mutability information about the /// The [ExtXPlaylistType] tag provides mutability information about the
/// Media Playlist. It applies to the entire Media Playlist. /// [Media Playlist]. It applies to the entire [Media Playlist].
/// It is OPTIONAL. Its format is:
/// ///
/// Its format is:
/// ```text /// ```text
/// #EXT-X-PLAYLIST-TYPE:<type-enum> /// #EXT-X-PLAYLIST-TYPE:<type-enum>
/// ``` /// ```
/// ///
/// # Note /// [Media Playlist]: crate::MediaPlaylist
/// If the EXT-X-PLAYLIST-TYPE tag is omitted from a Media Playlist, the /// [4.4.3.5. EXT-X-PLAYLIST-TYPE]:
/// Playlist can be updated according to the rules in Section 6.2.1 with /// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.5
/// no additional restrictions.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ExtXPlaylistType { pub enum ExtXPlaylistType {
/// If the ExtXPlaylistType is Event, Media Segments can only be added to /// If the [ExtXPlaylistType] is Event, Media Segments can only be added to
/// the end of the Media Playlist. /// the end of the Media Playlist.
Event, Event,
/// If the ExtXPlaylistType is Video On Demand (Vod), /// If the [ExtXPlaylistType] is Video On Demand (Vod),
/// the Media Playlist cannot change. /// the Media Playlist cannot change.
Vod, Vod,
} }

View file

@ -6,19 +6,31 @@ use crate::types::{ProtocolVersion, RequiredVersion};
use crate::utils::tag; use crate::utils::tag;
use crate::Error; use crate::Error;
/// [4.3.3.1. EXT-X-TARGETDURATION] /// # [4.4.3.1. EXT-X-TARGETDURATION]
/// The [ExtXTargetDuration] tag specifies the maximum [Media Segment]
/// duration.
/// ///
/// [4.3.3.1. EXT-X-TARGETDURATION]: https://tools.ietf.org/html/rfc8216#section-4.3.3.1 /// Its format is:
/// ```text
/// #EXT-X-TARGETDURATION:<s>
/// ```
/// where `s` is the target [Duration] in seconds.
///
/// [Media Segment]: crate::MediaSegment
/// [4.4.3.1. EXT-X-TARGETDURATION]:
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.1
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct ExtXTargetDuration(Duration); pub struct ExtXTargetDuration(Duration);
impl ExtXTargetDuration { impl ExtXTargetDuration {
pub(crate) const PREFIX: &'static str = "#EXT-X-TARGETDURATION:"; pub(crate) const PREFIX: &'static str = "#EXT-X-TARGETDURATION:";
/// Makes a new `ExtXTargetduration` tag. /// Makes a new [ExtXTargetduration] tag.
/// ///
/// Note that the nanoseconds part of the `duration` will be discarded. /// # Note
/// The nanoseconds part of the [Duration] will be discarded.
pub const fn new(duration: Duration) -> Self { pub const fn new(duration: Duration) -> Self {
// TOOD: round instead of discarding?
Self(Duration::from_secs(duration.as_secs())) Self(Duration::from_secs(duration.as_secs()))
} }

View file

@ -1,26 +1,39 @@
use std::fmt; use std::fmt;
use std::ops::Deref; use std::ops::{Deref, DerefMut};
use std::str::FromStr; use std::str::FromStr;
use crate::types::{ByteRange, ProtocolVersion, RequiredVersion}; use crate::types::{ByteRange, ProtocolVersion, RequiredVersion};
use crate::utils::tag; use crate::utils::tag;
use crate::Error; use crate::Error;
/// [4.3.2.2. EXT-X-BYTERANGE] /// # [4.4.2.2. EXT-X-BYTERANGE]
/// ///
/// [4.3.2.2. EXT-X-BYTERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.2 /// The [ExtXByteRange] tag indicates that a [Media Segment] is a sub-range
/// of the resource identified by its `URI`.
///
/// Its format is:
/// ```text
/// #EXT-X-BYTERANGE:<n>[@<o>]
/// ```
///
/// where `n` is a [usize] indicating the length of the sub-range in bytes.
/// If present, `o` is a [usize] indicating the start of the sub-range,
/// as a byte offset from the beginning of the resource.
///
/// [Media Segment]: crate::MediaSegment
/// [4.4.2.2. EXT-X-BYTERANGE]:
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.2
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ExtXByteRange(ByteRange); pub struct ExtXByteRange(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.
/// ///
/// # Example /// # Example
/// ``` /// ```
/// use hls_m3u8::tags::ExtXByteRange; /// # use hls_m3u8::tags::ExtXByteRange;
///
/// let byte_range = ExtXByteRange::new(20, Some(5)); /// let byte_range = ExtXByteRange::new(20, Some(5));
/// ``` /// ```
pub const fn new(length: usize, start: Option<usize>) -> Self { pub const fn new(length: usize, start: Option<usize>) -> Self {
@ -31,7 +44,7 @@ impl ExtXByteRange {
/// ///
/// # Example /// # Example
/// ``` /// ```
/// use hls_m3u8::tags::ExtXByteRange; /// # use hls_m3u8::tags::ExtXByteRange;
/// use hls_m3u8::types::ByteRange; /// use hls_m3u8::types::ByteRange;
/// ///
/// let byte_range = ExtXByteRange::new(20, Some(5)); /// let byte_range = ExtXByteRange::new(20, Some(5));
@ -56,6 +69,12 @@ impl Deref for ExtXByteRange {
} }
} }
impl DerefMut for ExtXByteRange {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut 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)?; write!(f, "{}", Self::PREFIX)?;
@ -132,6 +151,17 @@ mod test {
assert_eq!(byte_range.start(), Some(22)); assert_eq!(byte_range.start(), Some(22));
} }
#[test]
fn test_deref_mut() {
let mut byte_range = ExtXByteRange::new(0, Some(22));
byte_range.set_length(100);
byte_range.set_start(Some(50));
assert_eq!(byte_range.length(), 100);
assert_eq!(byte_range.start(), Some(50));
}
#[test] #[test]
fn test_required_version() { fn test_required_version() {
assert_eq!( assert_eq!(

View file

@ -18,8 +18,8 @@ use crate::Error;
#[allow(missing_docs)] #[allow(missing_docs)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ExtXDateRange { pub struct ExtXDateRange {
/// A string that uniquely identifies a Date Range in the Playlist. /// A string that uniquely identifies a [ExtXDateRange] in the Playlist.
/// This attribute is REQUIRED. /// This attribute is required.
id: String, id: String,
/// A client-defined string that specifies some set of attributes and their associated value /// A client-defined string that specifies some set of attributes and their associated value
/// semantics. All Date Ranges with the same CLASS attribute value MUST adhere to these /// semantics. All Date Ranges with the same CLASS attribute value MUST adhere to these

View file

@ -5,9 +5,18 @@ use crate::types::{ProtocolVersion, RequiredVersion};
use crate::utils::tag; use crate::utils::tag;
use crate::Error; use crate::Error;
/// [4.3.2.3. EXT-X-DISCONTINUITY] /// # [4.4.2.3. EXT-X-DISCONTINUITY]
/// The [ExtXDiscontinuity] tag indicates a discontinuity between the
/// [Media Segment] that follows it and the one that preceded it.
/// ///
/// [4.3.2.3. EXT-X-DISCONTINUITY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.3 /// Its format is:
/// ```text
/// #EXT-X-DISCONTINUITY
/// ```
///
/// [Media Segment]: crate::MediaSegment
/// [4.4.2.3. EXT-X-DISCONTINUITY]:
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.3
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ExtXDiscontinuity; pub struct ExtXDiscontinuity;
@ -50,7 +59,7 @@ mod test {
#[test] #[test]
fn test_parser() { fn test_parser() {
assert_eq!(ExtXDiscontinuity, "#EXT-X-DISCONTINUITY".parse().unwrap(),) assert_eq!(ExtXDiscontinuity, "#EXT-X-DISCONTINUITY".parse().unwrap())
} }
#[test] #[test]

View file

@ -6,10 +6,10 @@ use crate::types::{DecimalFloatingPoint, ProtocolVersion, RequiredVersion};
use crate::utils::tag; use crate::utils::tag;
use crate::Error; use crate::Error;
/// [4.3.2.1. EXTINF](https://tools.ietf.org/html/rfc8216#section-4.3.2.1) /// # [4.4.2.1. EXTINF]
/// ///
/// The [ExtInf] tag specifies the duration of a [Media Segment]. It applies /// The [ExtInf] tag specifies the duration of a [Media Segment]. It applies
/// only to the next [Media Segment]. This tag is REQUIRED for each [Media Segment]. /// only to the next [Media Segment].
/// ///
/// Its format is: /// Its format is:
/// ```text /// ```text
@ -18,32 +18,8 @@ use crate::Error;
/// The title is an optional informative title about the [Media Segment]. /// The title is an optional informative title about the [Media Segment].
/// ///
/// [Media Segment]: crate::media_segment::MediaSegment /// [Media Segment]: crate::media_segment::MediaSegment
/// /// [4.4.2.1. EXTINF]:
/// # Examples /// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.1
/// 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)] #[derive(Default, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ExtInf { pub struct ExtInf {
duration: Duration, duration: Duration,
@ -53,7 +29,15 @@ pub struct ExtInf {
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.
///
/// # Example
/// ```
/// # use hls_m3u8::tags::ExtInf;
/// use std::time::Duration;
///
/// let ext_inf = ExtInf::new(Duration::from_secs(5));
/// ```
pub const fn new(duration: Duration) -> Self { pub const fn new(duration: Duration) -> Self {
ExtInf { ExtInf {
duration, duration,
@ -61,7 +45,15 @@ impl ExtInf {
} }
} }
/// Makes a new `ExtInf` tag with the given title. /// Makes a new [ExtInf] tag with the given title.
///
/// # Example
/// ```
/// # use hls_m3u8::tags::ExtInf;
/// use std::time::Duration;
///
/// let ext_inf = ExtInf::with_title(Duration::from_secs(5), "title");
/// ```
pub fn with_title<T: ToString>(duration: Duration, title: T) -> Self { pub fn with_title<T: ToString>(duration: Duration, title: T) -> Self {
ExtInf { ExtInf {
duration, duration,
@ -70,13 +62,81 @@ impl ExtInf {
} }
/// Returns the duration of the associated media segment. /// Returns the duration of the associated media segment.
///
/// # Example
/// ```
/// # use hls_m3u8::tags::ExtInf;
/// use std::time::Duration;
///
/// let ext_inf = ExtInf::new(Duration::from_secs(5));
///
/// assert_eq!(
/// ext_inf.duration(),
/// Duration::from_secs(5)
/// );
/// ```
pub const fn duration(&self) -> Duration { pub const fn duration(&self) -> Duration {
self.duration self.duration
} }
/// Sets the duration of the associated media segment.
///
/// # Example
/// ```
/// # use hls_m3u8::tags::ExtInf;
/// use std::time::Duration;
///
/// let mut ext_inf = ExtInf::new(Duration::from_secs(5));
///
/// ext_inf.set_duration(Duration::from_secs(10));
///
/// assert_eq!(
/// ext_inf.duration(),
/// Duration::from_secs(10)
/// );
/// ```
pub fn set_duration(&mut self, value: Duration) -> &mut Self {
self.duration = value;
self
}
/// Returns the title of the associated media segment. /// Returns the title of the associated media segment.
pub fn title(&self) -> Option<&String> { ///
self.title.as_ref() /// # Example
/// ```
/// # use hls_m3u8::tags::ExtInf;
/// use std::time::Duration;
///
/// let ext_inf = ExtInf::with_title(Duration::from_secs(5), "title");
///
/// assert_eq!(
/// ext_inf.title(),
/// &Some("title".to_string())
/// );
/// ```
pub const fn title(&self) -> &Option<String> {
&self.title
}
/// Sets the title of the associated media segment.
///
/// # Example
/// ```
/// # use hls_m3u8::tags::ExtInf;
/// use std::time::Duration;
///
/// let mut ext_inf = ExtInf::with_title(Duration::from_secs(5), "title");
///
/// ext_inf.set_title(Some("better title"));
///
/// assert_eq!(
/// ext_inf.title(),
/// &Some("better title".to_string())
/// );
/// ```
pub fn set_title<T: ToString>(&mut self, value: Option<T>) -> &mut Self {
self.title = value.map(|v| v.to_string());
self
} }
} }
@ -199,10 +259,10 @@ mod test {
#[test] #[test]
fn test_title() { fn test_title() {
assert_eq!(ExtInf::new(Duration::from_secs(5)).title(), None); assert_eq!(ExtInf::new(Duration::from_secs(5)).title(), &None);
assert_eq!( assert_eq!(
ExtInf::with_title(Duration::from_secs(5), "title").title(), ExtInf::with_title(Duration::from_secs(5), "title").title(),
Some(&"title".to_string()) &Some("title".to_string())
); );
} }

View file

@ -2,15 +2,30 @@ use std::fmt;
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use std::str::FromStr; use std::str::FromStr;
use crate::types::{DecryptionKey, EncryptionMethod}; use crate::types::{DecryptionKey, EncryptionMethod, KeyFormatVersions};
use crate::utils::tag; use crate::utils::tag;
use crate::Error; use crate::Error;
/// [4.3.2.4. EXT-X-KEY] /// # [4.4.2.4. EXT-X-KEY]
/// [Media Segment]s may be encrypted. The [ExtXKey] tag specifies how to
/// decrypt them. It applies to every [Media Segment] and to every Media
/// Initialization Section declared by an [ExtXMap] tag, that appears
/// between it and the next [ExtXKey] tag in the Playlist file with the
/// same [KeyFormat] attribute (or the end of the Playlist file).
///
/// The format is:
/// ```text
/// #EXT-X-KEY:<attribute-list>
/// ```
/// ///
/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4
/// # Note /// # Note
/// In case of an empty key (`EncryptionMethod::None`), all attributes will be ignored. /// In case of an empty key (`EncryptionMethod::None`), all attributes will be ignored.
///
/// [KeyFormat]: crate::types::KeyFormat
/// [ExtXMap]: crate::tags::ExtXMap
/// [Media Segment]: crate::MediaSegment
/// [4.4.2.4. EXT-X-KEY]:
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.4
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ExtXKey(DecryptionKey); pub struct ExtXKey(DecryptionKey);
@ -49,13 +64,13 @@ impl ExtXKey {
/// "#EXT-X-KEY:METHOD=NONE" /// "#EXT-X-KEY:METHOD=NONE"
/// ); /// );
/// ``` /// ```
pub const fn empty() -> Self { pub fn empty() -> Self {
Self(DecryptionKey { Self(DecryptionKey {
method: EncryptionMethod::None, method: EncryptionMethod::None,
uri: None, uri: None,
iv: None, iv: None,
key_format: None, key_format: None,
key_format_versions: None, key_format_versions: KeyFormatVersions::new(),
}) })
} }
@ -109,7 +124,7 @@ impl DerefMut for ExtXKey {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::types::EncryptionMethod; use crate::types::{EncryptionMethod, KeyFormat};
#[test] #[test]
fn test_display() { fn test_display() {
@ -120,12 +135,12 @@ mod test {
let mut key = ExtXKey::empty(); 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 in an empty key!
key.set_key_format("hi"); key.set_key_format(Some(KeyFormat::Identity));
key.set_iv([ key.set_iv([
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, 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_uri(Some("https://www.example.com"));
key.set_key_format_versions("1/2/3"); key.set_key_format_versions(vec![1, 2, 3]);
assert_eq!(key.to_string(), "#EXT-X-KEY:METHOD=NONE".to_string()); assert_eq!(key.to_string(), "#EXT-X-KEY:METHOD=NONE".to_string());
} }
@ -133,7 +148,9 @@ mod test {
#[test] #[test]
fn test_parser() { fn test_parser() {
assert_eq!( assert_eq!(
r#"#EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52""# "#EXT-X-KEY:\
METHOD=AES-128,\
URI=\"https://priv.example.com/key.php?r=52\""
.parse::<ExtXKey>() .parse::<ExtXKey>()
.unwrap(), .unwrap(),
ExtXKey::new( ExtXKey::new(

View file

@ -6,9 +6,18 @@ use crate::types::{ByteRange, ProtocolVersion, RequiredVersion};
use crate::utils::{quote, tag, unquote}; use crate::utils::{quote, tag, unquote};
use crate::Error; use crate::Error;
/// [4.3.2.5. EXT-X-MAP] /// # [4.4.2.5. EXT-X-MAP]
/// The [ExtXMap] tag specifies how to obtain the Media Initialization
/// Section, required to parse the applicable [Media Segment]s.
/// ///
/// [4.3.2.5. EXT-X-MAP]: https://tools.ietf.org/html/rfc8216#section-4.3.2.5 /// Its format is:
/// ```text
/// #EXT-X-MAP:<attribute-list>
/// ```
///
/// [Media Segment]: crate::MediaSegment
/// [4.4.2.5. EXT-X-MAP]:
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.5
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ExtXMap { pub struct ExtXMap {
uri: String, uri: String,

View file

@ -1,4 +1,5 @@
use std::fmt; use std::fmt;
use std::ops::{Deref, DerefMut};
use std::str::FromStr; use std::str::FromStr;
use chrono::{DateTime, FixedOffset}; use chrono::{DateTime, FixedOffset};
@ -7,8 +8,11 @@ use crate::types::{ProtocolVersion, RequiredVersion};
use crate::utils::tag; use crate::utils::tag;
use crate::Error; use crate::Error;
/// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME] /// # [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]
/// The [ExtXProgramDateTime] tag associates the first sample of a
/// [Media Segment] with an absolute date and/or time.
/// ///
/// [Media Segment]: crate::MediaSegment
/// [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, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ExtXProgramDateTime(DateTime<FixedOffset>); pub struct ExtXProgramDateTime(DateTime<FixedOffset>);
@ -17,13 +21,33 @@ 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<T: Into<DateTime<FixedOffset>>>(date_time: T) -> Self { ///
Self(date_time.into()) /// # Example
/// ```
/// use hls_m3u8::tags::ExtXProgramDateTime;
/// use chrono::{FixedOffset, TimeZone};
///
/// const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
///
/// let program_date_time = ExtXProgramDateTime::new(
/// FixedOffset::east(8 * HOURS_IN_SECS)
/// .ymd(2010, 2, 19)
/// .and_hms_milli(14, 54, 23, 31)
/// );
/// ```
pub const fn new(date_time: DateTime<FixedOffset>) -> Self {
Self(date_time)
} }
/// 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 const fn date_time(&self) -> &DateTime<FixedOffset> { pub const fn date_time(&self) -> DateTime<FixedOffset> {
&self.0 self.0
}
/// Sets the date-time of the first sample of the associated media segment.
pub fn set_date_time(&mut self, value: DateTime<FixedOffset>) -> &mut Self {
self.0 = value;
self
} }
} }
@ -51,6 +75,20 @@ impl FromStr for ExtXProgramDateTime {
} }
} }
impl Deref for ExtXProgramDateTime {
type Target = DateTime<FixedOffset>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for ExtXProgramDateTime {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
@ -60,14 +98,13 @@ mod test {
#[test] #[test]
fn test_display() { fn test_display() {
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!( assert_eq!(
program_date_time.to_string(), ExtXProgramDateTime::new(
FixedOffset::east(8 * HOURS_IN_SECS)
.ymd(2010, 2, 19)
.and_hms_milli(14, 54, 23, 31)
)
.to_string(),
"#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00".to_string() "#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00".to_string()
); );
} }
@ -88,12 +125,14 @@ mod test {
#[test] #[test]
fn test_required_version() { fn test_required_version() {
let program_date_time = ExtXProgramDateTime::new( assert_eq!(
FixedOffset::east(8 * HOURS_IN_SECS) ExtXProgramDateTime::new(
.ymd(2010, 2, 19) FixedOffset::east(8 * HOURS_IN_SECS)
.and_hms_milli(14, 54, 23, 31), .ymd(2010, 2, 19)
.and_hms_milli(14, 54, 23, 31),
)
.required_version(),
ProtocolVersion::V1
); );
assert_eq!(program_date_time.required_version(), ProtocolVersion::V1);
} }
} }

View file

@ -2,16 +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! impl_from {
($to:ident, $from:ident) => {
impl From<$from> for $to {
fn from(f: $from) -> Self {
$to::$from(f)
}
}
};
}
mod basic; mod basic;
mod master_playlist; mod master_playlist;
mod media_playlist; mod media_playlist;
@ -23,79 +13,3 @@ pub use master_playlist::*;
pub use media_playlist::*; pub use media_playlist::*;
pub use media_segment::*; pub use media_segment::*;
pub use shared::*; pub use shared::*;
/// [4.3.4. Master Playlist Tags]
///
/// See also [4.3.5. Media or Master Playlist Tags]
///
/// [4.3.4. Master Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3.4
/// [4.3.5. Media or Master Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3.5
#[allow(missing_docs)]
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MasterPlaylistTag {
ExtXMedia(ExtXMedia),
ExtXStreamInf(ExtXStreamInf),
ExtXIFrameStreamInf(ExtXIFrameStreamInf),
ExtXSessionData(ExtXSessionData),
ExtXSessionKey(ExtXSessionKey),
ExtXIndependentSegments(ExtXIndependentSegments),
ExtXStart(ExtXStart),
}
impl_from!(MasterPlaylistTag, ExtXMedia);
impl_from!(MasterPlaylistTag, ExtXStreamInf);
impl_from!(MasterPlaylistTag, ExtXIFrameStreamInf);
impl_from!(MasterPlaylistTag, ExtXSessionData);
impl_from!(MasterPlaylistTag, ExtXSessionKey);
impl_from!(MasterPlaylistTag, ExtXIndependentSegments);
impl_from!(MasterPlaylistTag, ExtXStart);
/// [4.3.3. Media Playlist Tags]
///
/// See also [4.3.5. Media or Master Playlist Tags]
///
/// [4.3.3. Media Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3.3
/// [4.3.5. Media or Master Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3.5
#[allow(missing_docs)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MediaPlaylistTag {
ExtXTargetDuration(ExtXTargetDuration),
ExtXMediaSequence(ExtXMediaSequence),
ExtXDiscontinuitySequence(ExtXDiscontinuitySequence),
ExtXEndList(ExtXEndList),
ExtXPlaylistType(ExtXPlaylistType),
ExtXIFramesOnly(ExtXIFramesOnly),
ExtXIndependentSegments(ExtXIndependentSegments),
ExtXStart(ExtXStart),
}
impl_from!(MediaPlaylistTag, ExtXTargetDuration);
impl_from!(MediaPlaylistTag, ExtXMediaSequence);
impl_from!(MediaPlaylistTag, ExtXDiscontinuitySequence);
impl_from!(MediaPlaylistTag, ExtXEndList);
impl_from!(MediaPlaylistTag, ExtXPlaylistType);
impl_from!(MediaPlaylistTag, ExtXIFramesOnly);
impl_from!(MediaPlaylistTag, ExtXIndependentSegments);
impl_from!(MediaPlaylistTag, ExtXStart);
/// [4.3.2. Media Segment Tags]
///
/// [4.3.2. Media Segment Tags]: https://tools.ietf.org/html/rfc8216#section-4.3.2
#[allow(missing_docs)]
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MediaSegmentTag {
ExtInf(ExtInf),
ExtXByteRange(ExtXByteRange),
ExtXDateRange(ExtXDateRange),
ExtXDiscontinuity(ExtXDiscontinuity),
ExtXKey(ExtXKey),
ExtXMap(ExtXMap),
ExtXProgramDateTime(ExtXProgramDateTime),
}
impl_from!(MediaSegmentTag, ExtInf);
impl_from!(MediaSegmentTag, ExtXByteRange);
impl_from!(MediaSegmentTag, ExtXDateRange);
impl_from!(MediaSegmentTag, ExtXDiscontinuity);
impl_from!(MediaSegmentTag, ExtXKey);
impl_from!(MediaSegmentTag, ExtXMap);
impl_from!(MediaSegmentTag, ExtXProgramDateTime);

View file

@ -4,39 +4,49 @@ use std::str::FromStr;
use derive_builder::Builder; use derive_builder::Builder;
use crate::attribute::AttributePairs; use crate::attribute::AttributePairs;
use crate::types::{EncryptionMethod, InitializationVector, ProtocolVersion, RequiredVersion}; use crate::types::{
EncryptionMethod, InitializationVector, KeyFormat, KeyFormatVersions, ProtocolVersion,
RequiredVersion,
};
use crate::utils::{quote, unquote}; use crate::utils::{quote, unquote};
use crate::Error; use crate::Error;
#[derive(Builder, Debug, Clone, PartialEq, Eq, Hash)] #[derive(Builder, Debug, Clone, PartialEq, Eq, Hash)]
#[builder(setter(into))] #[builder(setter(into))]
/// [DecryptionKey] contains data, that is shared between [ExtXSessionKey] and [ExtXKey].
///
/// [ExtXSessionKey]: crate::tags::ExtXSessionKey
/// [ExtXKey]: crate::tags::ExtXKey
pub struct DecryptionKey { pub struct DecryptionKey {
/// The [EncryptionMethod].
pub(crate) method: EncryptionMethod, pub(crate) method: EncryptionMethod,
#[builder(setter(into, strip_option), default)] #[builder(setter(into, strip_option), default)]
/// An `URI`, that specifies how to obtain the key.
pub(crate) uri: Option<String>, pub(crate) uri: Option<String>,
#[builder(setter(into, strip_option), default)] #[builder(setter(into, strip_option), default)]
/// The IV (Initialization Vector) attribute.
pub(crate) iv: Option<InitializationVector>, pub(crate) iv: Option<InitializationVector>,
#[builder(setter(into, strip_option), default)] #[builder(setter(into, strip_option), default)]
pub(crate) key_format: Option<String>, /// A string that specifies how the key is
#[builder(setter(into, strip_option), default)] /// represented in the resource identified by the `URI`.
pub(crate) key_format_versions: Option<String>, pub(crate) key_format: Option<KeyFormat>,
#[builder(setter(into), default)]
/// The `KEYFORMATVERSIONS` attribute.
pub(crate) key_format_versions: KeyFormatVersions,
} }
impl DecryptionKey { impl DecryptionKey {
/// Makes a new `DecryptionKey`. /// Makes a new [DecryptionKey].
///
/// # Example /// # Example
/// ``` /// ```
/// use hls_m3u8::types::{EncryptionMethod, DecryptionKey}; /// # use hls_m3u8::types::DecryptionKey;
/// use hls_m3u8::types::EncryptionMethod;
/// ///
/// let key = DecryptionKey::new( /// let key = DecryptionKey::new(
/// EncryptionMethod::Aes128, /// EncryptionMethod::Aes128,
/// "https://www.example.com/" /// "https://www.example.com/"
/// ); /// );
///
/// assert_eq!(
/// key.to_string(),
/// "METHOD=AES-128,URI=\"https://www.example.com/\""
/// );
/// ``` /// ```
pub fn new<T: ToString>(method: EncryptionMethod, uri: T) -> Self { pub fn new<T: ToString>(method: EncryptionMethod, uri: T) -> Self {
Self { Self {
@ -44,14 +54,16 @@ impl DecryptionKey {
uri: Some(uri.to_string()), uri: Some(uri.to_string()),
iv: None, iv: None,
key_format: None, key_format: None,
key_format_versions: None, key_format_versions: KeyFormatVersions::new(),
} }
} }
/// Returns the [EncryptionMethod]. /// Returns the [EncryptionMethod].
///
/// # Example /// # Example
/// ``` /// ```
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; /// # use hls_m3u8::types::DecryptionKey;
/// use hls_m3u8::types::EncryptionMethod;
/// ///
/// let key = DecryptionKey::new( /// let key = DecryptionKey::new(
/// EncryptionMethod::Aes128, /// EncryptionMethod::Aes128,
@ -67,15 +79,17 @@ impl DecryptionKey {
self.method self.method
} }
/// Returns a Builder to build a `DecryptionKey`. /// Returns a Builder to build a [DecryptionKey].
pub fn builder() -> DecryptionKeyBuilder { pub fn builder() -> DecryptionKeyBuilder {
DecryptionKeyBuilder::default() DecryptionKeyBuilder::default()
} }
/// Sets the [EncryptionMethod]. /// Sets the [EncryptionMethod].
///
/// # Example /// # Example
/// ``` /// ```
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; /// # use hls_m3u8::types::DecryptionKey;
/// use hls_m3u8::types::EncryptionMethod;
/// ///
/// let mut key = DecryptionKey::new( /// let mut key = DecryptionKey::new(
/// EncryptionMethod::Aes128, /// EncryptionMethod::Aes128,
@ -93,12 +107,14 @@ impl DecryptionKey {
self.method = value; self.method = value;
} }
/// Returns an `URI` that specifies how to obtain the key. /// Returns an `URI`, that specifies how to obtain the key.
/// ///
/// This attribute is required, if the [EncryptionMethod] is not None. /// This attribute is required, if the [EncryptionMethod] is not None.
///
/// # Example /// # Example
/// ``` /// ```
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; /// # use hls_m3u8::types::DecryptionKey;
/// use hls_m3u8::types::EncryptionMethod;
/// ///
/// let key = DecryptionKey::new( /// let key = DecryptionKey::new(
/// EncryptionMethod::Aes128, /// EncryptionMethod::Aes128,
@ -121,7 +137,8 @@ impl DecryptionKey {
/// ///
/// # Example /// # Example
/// ``` /// ```
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; /// # use hls_m3u8::types::DecryptionKey;
/// use hls_m3u8::types::EncryptionMethod;
/// ///
/// let mut key = DecryptionKey::new( /// let mut key = DecryptionKey::new(
/// EncryptionMethod::Aes128, /// EncryptionMethod::Aes128,
@ -142,9 +159,11 @@ impl DecryptionKey {
/// Returns the IV (Initialization Vector) attribute. /// Returns the IV (Initialization Vector) attribute.
/// ///
/// This attribute is optional. /// This attribute is optional.
///
/// # Example /// # Example
/// ``` /// ```
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; /// # use hls_m3u8::types::DecryptionKey;
/// use hls_m3u8::types::EncryptionMethod;
/// ///
/// let mut key = DecryptionKey::new( /// let mut key = DecryptionKey::new(
/// EncryptionMethod::Aes128, /// EncryptionMethod::Aes128,
@ -171,9 +190,11 @@ impl DecryptionKey {
/// Sets the `IV` attribute. /// Sets the `IV` attribute.
/// ///
/// This attribute is optional. /// This attribute is optional.
///
/// # Example /// # Example
/// ``` /// ```
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; /// # use hls_m3u8::types::DecryptionKey;
/// use hls_m3u8::types::EncryptionMethod;
/// ///
/// let mut key = DecryptionKey::new( /// let mut key = DecryptionKey::new(
/// EncryptionMethod::Aes128, /// EncryptionMethod::Aes128,
@ -197,106 +218,110 @@ impl DecryptionKey {
} }
/// Returns a string that specifies how the key is /// Returns a string that specifies how the key is
/// represented in the resource identified by the URI. /// represented in the resource identified by the `URI`.
///
/// This attribute is optional.
/// ///
//// This attribute is optional.
/// # Example /// # Example
/// ``` /// ```
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; /// # use hls_m3u8::types::DecryptionKey;
/// use hls_m3u8::types::{KeyFormat, EncryptionMethod};
/// ///
/// let mut key = DecryptionKey::new( /// let mut key = DecryptionKey::new(
/// EncryptionMethod::Aes128, /// EncryptionMethod::Aes128,
/// "https://www.example.com/" /// "https://www.example.com/"
/// ); /// );
/// ///
/// key.set_key_format("key_format_attribute"); /// key.set_key_format(Some(KeyFormat::Identity));
/// ///
/// assert_eq!( /// assert_eq!(
/// key.key_format(), /// key.key_format(),
/// &Some("key_format_attribute".to_string()) /// Some(KeyFormat::Identity)
/// ); /// );
/// ``` /// ```
pub const fn key_format(&self) -> &Option<String> { pub const fn key_format(&self) -> Option<KeyFormat> {
&self.key_format self.key_format
} }
/// Sets the `KEYFORMAT` attribute. /// Sets the `KEYFORMAT` attribute.
/// ///
/// This attribute is optional. /// This attribute is optional.
///
/// # Example /// # Example
/// ``` /// ```
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; /// # use hls_m3u8::types::DecryptionKey;
/// use hls_m3u8::types::{KeyFormat, EncryptionMethod};
/// ///
/// let mut key = DecryptionKey::new( /// let mut key = DecryptionKey::new(
/// EncryptionMethod::Aes128, /// EncryptionMethod::Aes128,
/// "https://www.example.com/" /// "https://www.example.com/"
/// ); /// );
/// ///
/// key.set_key_format("key_format_attribute"); /// key.set_key_format(Some(KeyFormat::Identity));
/// ///
/// assert_eq!( /// assert_eq!(
/// key.to_string(), /// key.key_format(),
/// "METHOD=AES-128,URI=\"https://www.example.com/\",KEYFORMAT=\"key_format_attribute\"".to_string() /// Some(KeyFormat::Identity)
/// ); /// );
/// ``` /// ```
pub fn set_key_format<T: ToString>(&mut self, value: T) { pub fn set_key_format<T: Into<KeyFormat>>(&mut self, value: Option<T>) {
self.key_format = Some(value.to_string()); self.key_format = value.map(|v| v.into());
} }
/// Returns a string containing one or more positive /// Returns the [KeyFormatVersions] attribute.
/// 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. /// This attribute is optional.
///
/// # Example /// # Example
/// ``` /// ```
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; /// # use hls_m3u8::types::DecryptionKey;
/// use hls_m3u8::types::{KeyFormatVersions, EncryptionMethod};
/// ///
/// let mut key = DecryptionKey::new( /// let mut key = DecryptionKey::new(
/// EncryptionMethod::Aes128, /// EncryptionMethod::Aes128,
/// "https://www.example.com/" /// "https://www.example.com/"
/// ); /// );
/// ///
/// key.set_key_format_versions("1/2/3/4/5"); /// key.set_key_format_versions(vec![1, 2, 3, 4, 5]);
/// ///
/// assert_eq!( /// assert_eq!(
/// key.key_format_versions(), /// key.key_format_versions(),
/// &Some("1/2/3/4/5".to_string()) /// &KeyFormatVersions::from(vec![1, 2, 3, 4, 5])
/// ); /// );
/// ``` /// ```
pub const fn key_format_versions(&self) -> &Option<String> { pub const fn key_format_versions(&self) -> &KeyFormatVersions {
&self.key_format_versions &self.key_format_versions
} }
/// Sets the `KEYFORMATVERSIONS` attribute. /// Sets the [KeyFormatVersions] attribute.
/// ///
/// This attribute is optional. /// This attribute is optional.
///
/// # Example /// # Example
/// ``` /// ```
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod}; /// # use hls_m3u8::types::DecryptionKey;
/// use hls_m3u8::types::EncryptionMethod;
/// ///
/// let mut key = DecryptionKey::new( /// let mut key = DecryptionKey::new(
/// EncryptionMethod::Aes128, /// EncryptionMethod::Aes128,
/// "https://www.example.com/" /// "https://www.example.com/"
/// ); /// );
/// ///
/// key.set_key_format_versions("1/2/3/4/5"); /// key.set_key_format_versions(vec![1, 2, 3, 4, 5]);
/// ///
/// assert_eq!( /// assert_eq!(
/// key.to_string(), /// key.to_string(),
/// "METHOD=AES-128,URI=\"https://www.example.com/\",KEYFORMATVERSIONS=\"1/2/3/4/5\"".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) { pub fn set_key_format_versions<T: Into<KeyFormatVersions>>(&mut self, value: T) {
self.key_format_versions = Some(value.to_string()); self.key_format_versions = value.into();
} }
} }
impl RequiredVersion for DecryptionKey { impl RequiredVersion for DecryptionKey {
fn required_version(&self) -> ProtocolVersion { fn required_version(&self) -> ProtocolVersion {
if self.key_format.is_some() || self.key_format_versions.is_some() { if self.key_format.is_some() || !self.key_format_versions.is_default() {
ProtocolVersion::V5 ProtocolVersion::V5
} else if self.iv.is_some() { } else if self.iv.is_some() {
ProtocolVersion::V2 ProtocolVersion::V2
@ -318,11 +343,11 @@ impl FromStr for DecryptionKey {
for (key, value) in input.parse::<AttributePairs>()? { for (key, value) in input.parse::<AttributePairs>()? {
match key.as_str() { match key.as_str() {
"METHOD" => method = Some((value.parse())?), "METHOD" => method = Some(value.parse()?),
"URI" => uri = Some(unquote(value)), "URI" => uri = Some(unquote(value)),
"IV" => iv = Some((value.parse())?), "IV" => iv = Some(value.parse()?),
"KEYFORMAT" => key_format = Some(unquote(value)), "KEYFORMAT" => key_format = Some(value.parse()?),
"KEYFORMATVERSIONS" => key_format_versions = Some(unquote(value)), "KEYFORMATVERSIONS" => key_format_versions = Some(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.
@ -340,7 +365,7 @@ impl FromStr for DecryptionKey {
uri, uri,
iv, iv,
key_format, key_format,
key_format_versions, key_format_versions: key_format_versions.unwrap_or(KeyFormatVersions::new()),
}) })
} }
} }
@ -361,8 +386,8 @@ impl fmt::Display for DecryptionKey {
if let Some(value) = &self.key_format { if let Some(value) = &self.key_format {
write!(f, ",KEYFORMAT={}", quote(value))?; write!(f, ",KEYFORMAT={}", quote(value))?;
} }
if let Some(value) = &self.key_format_versions { if !self.key_format_versions.is_default() {
write!(f, ",KEYFORMATVERSIONS={}", quote(value))?; write!(f, ",KEYFORMATVERSIONS={}", &self.key_format_versions)?;
} }
Ok(()) Ok(())
} }
@ -381,13 +406,19 @@ mod test {
.iv([ .iv([
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
]) ])
.key_format("ABC123") .key_format(KeyFormat::Identity)
.key_format_versions("1,2,3,4,5/12345") .key_format_versions(vec![1, 2, 3, 4, 5])
.build() .build()
.unwrap(); .unwrap();
assert_eq!( assert_eq!(
key.to_string(), key.to_string(),
"METHOD=AES-128,URI=\"https://www.example.com/\",IV=0x10ef8f758ca555115584bb5b3c687f52,KEYFORMAT=\"ABC123\",KEYFORMATVERSIONS=\"1,2,3,4,5/12345\"".to_string() "METHOD=AES-128,\
URI=\"https://www.example.com/\",\
IV=0x10ef8f758ca555115584bb5b3c687f52,\
KEYFORMAT=\"identity\",\
KEYFORMATVERSIONS=\"1/2/3/4/5\"\
"
.to_string()
) )
} }
@ -413,7 +444,8 @@ mod test {
#[test] #[test]
fn test_parser() { fn test_parser() {
assert_eq!( assert_eq!(
r#"METHOD=AES-128,URI="https://priv.example.com/key.php?r=52""# "METHOD=AES-128,\
URI=\"https://priv.example.com/key.php?r=52\""
.parse::<DecryptionKey>() .parse::<DecryptionKey>()
.unwrap(), .unwrap(),
DecryptionKey::new( DecryptionKey::new(
@ -443,11 +475,15 @@ mod test {
key.set_iv([ key.set_iv([
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82, 16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
]); ]);
key.set_key_format("baz"); key.set_key_format(Some(KeyFormat::Identity));
assert_eq!( assert_eq!(
r#"METHOD=AES-128,URI="http://www.example.com",IV=0x10ef8f758ca555115584bb5b3c687f52,KEYFORMAT="baz""# "METHOD=AES-128,\
.parse::<DecryptionKey>().unwrap(), URI=\"http://www.example.com\",\
IV=0x10ef8f758ca555115584bb5b3c687f52,\
KEYFORMAT=\"identity\""
.parse::<DecryptionKey>()
.unwrap(),
key key
) )
} }

68
src/types/key_format.rs Normal file
View file

@ -0,0 +1,68 @@
use std::fmt;
use std::str::FromStr;
use crate::types::{ProtocolVersion, RequiredVersion};
use crate::utils::{quote, tag, unquote};
use crate::Error;
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
/// KeyFormat specifies, how the key is represented in the resource identified by the URI
pub enum KeyFormat {
/// The key is a single packed array of 16 octets in binary format.
Identity,
}
impl Default for KeyFormat {
fn default() -> Self {
Self::Identity
}
}
impl FromStr for KeyFormat {
type Err = Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
tag(&unquote(input), "identity")?; // currently only KeyFormat::Identity exists!
Ok(Self::Identity)
}
}
impl fmt::Display for KeyFormat {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", quote("identity"))
}
}
impl RequiredVersion for KeyFormat {
fn required_version(&self) -> ProtocolVersion {
ProtocolVersion::V5
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_display() {
assert_eq!(KeyFormat::Identity.to_string(), quote("identity"));
}
#[test]
fn test_parser() {
assert_eq!(KeyFormat::Identity, quote("identity").parse().unwrap());
assert_eq!(KeyFormat::Identity, "identity".parse().unwrap());
}
#[test]
fn test_required_version() {
assert_eq!(KeyFormat::Identity.required_version(), ProtocolVersion::V5)
}
#[test]
fn test_default() {
assert_eq!(KeyFormat::Identity, KeyFormat::default());
}
}

View file

@ -0,0 +1,170 @@
use std::fmt;
use std::ops::{Deref, DerefMut};
use std::str::FromStr;
use crate::types::{ProtocolVersion, RequiredVersion};
use crate::utils::{quote, unquote};
use crate::Error;
/// A list of [usize], that can be used to indicate which version(s)
/// this instance complies with, if more than one version of a particular
/// [KeyFormat] is defined.
///
/// [KeyFormat]: crate::types::KeyFormat
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub struct KeyFormatVersions(Vec<usize>);
impl Default for KeyFormatVersions {
fn default() -> Self {
Self(vec![1])
}
}
impl KeyFormatVersions {
/// Makes a new [KeyFormatVersions].
pub fn new() -> Self {
Self::default()
}
/// Add a value to the [KeyFormatVersions].
pub fn push(&mut self, value: usize) {
if self.is_default() {
self.0 = vec![value];
} else {
self.0.push(value);
}
}
/// 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()
}
}
impl Deref for KeyFormatVersions {
type Target = Vec<usize>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for KeyFormatVersions {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl RequiredVersion for KeyFormatVersions {
fn required_version(&self) -> ProtocolVersion {
ProtocolVersion::V5
}
}
impl FromStr for KeyFormatVersions {
type Err = Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let mut result = unquote(input)
.split("/")
.filter_map(|v| v.parse().ok())
.collect::<Vec<_>>();
if result.is_empty() {
result.push(1);
}
Ok(Self(result))
}
}
impl fmt::Display for KeyFormatVersions {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.is_default() {
return write!(f, "{}", quote("1"));
}
write!(
f,
"{}",
quote(
// vec![1, 2, 3] -> "1/2/3"
self.0
.iter()
.map(|v| v.to_string())
.collect::<Vec<String>>()
.join("/")
)
)
}
}
impl<T: Into<Vec<usize>>> From<T> for KeyFormatVersions {
fn from(value: T) -> Self {
Self(value.into())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_display() {
assert_eq!(
KeyFormatVersions::from(vec![1, 2, 3, 4, 5]).to_string(),
quote("1/2/3/4/5")
);
assert_eq!(KeyFormatVersions::from(vec![]).to_string(), quote("1"));
assert_eq!(KeyFormatVersions::new().to_string(), quote("1"));
}
#[test]
fn test_parser() {
assert_eq!(
KeyFormatVersions::from(vec![1, 2, 3, 4, 5]),
quote("1/2/3/4/5").parse().unwrap()
);
assert_eq!(KeyFormatVersions::from(vec![1]), "1".parse().unwrap());
assert_eq!(KeyFormatVersions::from(vec![1, 2]), "1/2".parse().unwrap());
}
#[test]
fn test_required_version() {
assert_eq!(
KeyFormatVersions::new().required_version(),
ProtocolVersion::V5
)
}
#[test]
fn test_is_default() {
assert!(KeyFormatVersions::new().is_default());
assert!(KeyFormatVersions::from(vec![]).is_default());
assert!(!KeyFormatVersions::from(vec![1, 2, 3]).is_default());
}
#[test]
fn test_push() {
let mut key_format_versions = KeyFormatVersions::from(vec![]);
key_format_versions.push(2);
assert_eq!(KeyFormatVersions::from(vec![2]), key_format_versions);
}
#[test]
fn test_deref() {
assert!(!KeyFormatVersions::new().is_empty());
}
#[test]
fn test_deref_mut() {
let mut key_format_versions = KeyFormatVersions::from(vec![1, 2, 3]);
key_format_versions.pop();
assert_eq!(key_format_versions, KeyFormatVersions::from(vec![1, 2]));
}
}

View file

@ -8,6 +8,8 @@ mod encryption_method;
mod hdcp_level; mod hdcp_level;
mod in_stream_id; mod in_stream_id;
mod initialization_vector; mod initialization_vector;
mod key_format;
mod key_format_versions;
mod media_type; mod media_type;
mod protocol_version; mod protocol_version;
mod signed_decimal_floating_point; mod signed_decimal_floating_point;
@ -22,6 +24,8 @@ pub use encryption_method::*;
pub use hdcp_level::*; pub use hdcp_level::*;
pub use in_stream_id::*; pub use in_stream_id::*;
pub use initialization_vector::*; pub use initialization_vector::*;
pub use key_format::*;
pub use key_format_versions::*;
pub use media_type::*; pub use media_type::*;
pub use protocol_version::*; pub use protocol_version::*;
pub(crate) use signed_decimal_floating_point::*; pub(crate) use signed_decimal_floating_point::*;

View file

@ -46,7 +46,7 @@ pub(crate) fn tag<T>(input: &str, tag: T) -> crate::Result<&str>
where where
T: AsRef<str>, T: AsRef<str>,
{ {
if !input.starts_with(tag.as_ref()) { if !input.trim().starts_with(tag.as_ref()) {
return Err(Error::missing_tag(tag.as_ref(), input)); return Err(Error::missing_tag(tag.as_ref(), input));
} }
let result = input.split_at(tag.as_ref().len()).1; let result = input.split_at(tag.as_ref().len()).1;