mirror of
https://github.com/sile/hls_m3u8.git
synced 2024-11-21 14:50:59 +00:00
Merge pull request #34 from Luro02/master
Documentation Improvements + more tests
This commit is contained in:
commit
b197d5fbd7
35 changed files with 1587 additions and 784 deletions
|
@ -61,10 +61,10 @@ impl FromStr for AttributePairs {
|
|||
let key = pair[0].to_uppercase();
|
||||
let value = pair[1].to_string();
|
||||
|
||||
result.insert(key.to_string(), value.to_string());
|
||||
result.insert(key.trim().to_string(), value.trim().to_string());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(test)] // this is very useful, when a test fails!
|
||||
dbg!(&result);
|
||||
Ok(result)
|
||||
}
|
||||
|
@ -110,7 +110,7 @@ mod test {
|
|||
|
||||
#[test]
|
||||
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>()
|
||||
.unwrap();
|
||||
|
||||
|
|
|
@ -69,6 +69,10 @@ pub enum ErrorKind {
|
|||
/// An Error from a Builder.
|
||||
BuilderError(String),
|
||||
|
||||
#[fail(display = "Missing Attribute: {}", _0)]
|
||||
/// An attribute is missing.
|
||||
MissingAttribute(String),
|
||||
|
||||
/// Hints that destructuring should not be exhaustive.
|
||||
///
|
||||
/// 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 {
|
||||
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 {
|
||||
|
|
|
@ -10,7 +10,7 @@ use crate::tags::{
|
|||
ExtM3u, ExtXIFrameStreamInf, ExtXIndependentSegments, ExtXMedia, ExtXSessionData,
|
||||
ExtXSessionKey, ExtXStart, ExtXStreamInf, ExtXVersion,
|
||||
};
|
||||
use crate::types::{ClosedCaptions, MediaType, ProtocolVersion};
|
||||
use crate::types::{ClosedCaptions, MediaType, ProtocolVersion, RequiredVersion};
|
||||
use crate::Error;
|
||||
|
||||
/// Master playlist.
|
||||
|
@ -92,6 +92,12 @@ impl MasterPlaylist {
|
|||
}
|
||||
}
|
||||
|
||||
impl RequiredVersion for MasterPlaylist {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
self.version_tag.version()
|
||||
}
|
||||
}
|
||||
|
||||
impl MasterPlaylistBuilder {
|
||||
fn validate(&self) -> Result<(), String> {
|
||||
let required_version = self.required_version();
|
||||
|
@ -118,43 +124,43 @@ impl MasterPlaylistBuilder {
|
|||
.chain(
|
||||
self.independent_segments_tag
|
||||
.iter()
|
||||
.map(|t| t.iter().map(|t| t.requires_version()))
|
||||
.map(|t| t.iter().map(|t| t.required_version()))
|
||||
.flatten(),
|
||||
)
|
||||
.chain(
|
||||
self.start_tag
|
||||
.iter()
|
||||
.map(|t| t.iter().map(|t| t.requires_version()))
|
||||
.map(|t| t.iter().map(|t| t.required_version()))
|
||||
.flatten(),
|
||||
)
|
||||
.chain(
|
||||
self.media_tags
|
||||
.iter()
|
||||
.map(|t| t.iter().map(|t| t.requires_version()))
|
||||
.map(|t| t.iter().map(|t| t.required_version()))
|
||||
.flatten(),
|
||||
)
|
||||
.chain(
|
||||
self.stream_inf_tags
|
||||
.iter()
|
||||
.map(|t| t.iter().map(|t| t.requires_version()))
|
||||
.map(|t| t.iter().map(|t| t.required_version()))
|
||||
.flatten(),
|
||||
)
|
||||
.chain(
|
||||
self.i_frame_stream_inf_tags
|
||||
.iter()
|
||||
.map(|t| t.iter().map(|t| t.requires_version()))
|
||||
.map(|t| t.iter().map(|t| t.required_version()))
|
||||
.flatten(),
|
||||
)
|
||||
.chain(
|
||||
self.session_data_tags
|
||||
.iter()
|
||||
.map(|t| t.iter().map(|t| t.requires_version()))
|
||||
.map(|t| t.iter().map(|t| t.required_version()))
|
||||
.flatten(),
|
||||
)
|
||||
.chain(
|
||||
self.session_key_tags
|
||||
.iter()
|
||||
.map(|t| t.iter().map(|t| t.requires_version()))
|
||||
.map(|t| t.iter().map(|t| t.required_version()))
|
||||
.flatten(),
|
||||
)
|
||||
.max()
|
||||
|
|
|
@ -11,7 +11,7 @@ use crate::tags::{
|
|||
ExtM3u, ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly, ExtXIndependentSegments,
|
||||
ExtXMediaSequence, ExtXPlaylistType, ExtXStart, ExtXTargetDuration, ExtXVersion,
|
||||
};
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::Error;
|
||||
|
||||
/// Media playlist.
|
||||
|
@ -142,60 +142,60 @@ impl MediaPlaylistBuilder {
|
|||
.chain(
|
||||
self.target_duration_tag
|
||||
.iter()
|
||||
.map(|t| t.requires_version()),
|
||||
.map(|t| t.required_version()),
|
||||
)
|
||||
.chain(self.media_sequence_tag.iter().map(|t| {
|
||||
if let Some(p) = t {
|
||||
p.requires_version()
|
||||
p.required_version()
|
||||
} else {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}))
|
||||
.chain(self.discontinuity_sequence_tag.iter().map(|t| {
|
||||
if let Some(p) = t {
|
||||
p.requires_version()
|
||||
p.required_version()
|
||||
} else {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}))
|
||||
.chain(self.playlist_type_tag.iter().map(|t| {
|
||||
if let Some(p) = t {
|
||||
p.requires_version()
|
||||
p.required_version()
|
||||
} else {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}))
|
||||
.chain(self.i_frames_only_tag.iter().map(|t| {
|
||||
if let Some(p) = t {
|
||||
p.requires_version()
|
||||
p.required_version()
|
||||
} else {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}))
|
||||
.chain(self.independent_segments_tag.iter().map(|t| {
|
||||
if let Some(p) = t {
|
||||
p.requires_version()
|
||||
p.required_version()
|
||||
} else {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}))
|
||||
.chain(self.start_tag.iter().map(|t| {
|
||||
if let Some(p) = t {
|
||||
p.requires_version()
|
||||
p.required_version()
|
||||
} else {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}))
|
||||
.chain(self.end_list_tag.iter().map(|t| {
|
||||
if let Some(p) = t {
|
||||
p.requires_version()
|
||||
p.required_version()
|
||||
} else {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}))
|
||||
.chain(self.segments.iter().map(|t| {
|
||||
t.iter()
|
||||
.map(|s| s.requires_version())
|
||||
.map(|s| s.required_version())
|
||||
.max()
|
||||
.unwrap_or(ProtocolVersion::V1)
|
||||
}))
|
||||
|
|
|
@ -6,7 +6,7 @@ use derive_builder::Builder;
|
|||
use crate::tags::{
|
||||
ExtInf, ExtXByteRange, ExtXDateRange, ExtXDiscontinuity, ExtXKey, ExtXMap, ExtXProgramDateTime,
|
||||
};
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
|
||||
/// Media segment.
|
||||
#[derive(Debug, Clone, Builder)]
|
||||
|
@ -118,21 +118,22 @@ impl MediaSegment {
|
|||
pub fn key_tags(&self) -> &[ExtXKey] {
|
||||
&self.key_tags
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this segment requires.
|
||||
pub fn requires_version(&self) -> ProtocolVersion {
|
||||
impl RequiredVersion for MediaSegment {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
iter::empty()
|
||||
.chain(self.key_tags.iter().map(|t| t.requires_version()))
|
||||
.chain(self.map_tag.iter().map(|t| t.requires_version()))
|
||||
.chain(self.byte_range_tag.iter().map(|t| t.requires_version()))
|
||||
.chain(self.date_range_tag.iter().map(|t| t.requires_version()))
|
||||
.chain(self.discontinuity_tag.iter().map(|t| t.requires_version()))
|
||||
.chain(self.key_tags.iter().map(|t| t.required_version()))
|
||||
.chain(self.map_tag.iter().map(|t| t.required_version()))
|
||||
.chain(self.byte_range_tag.iter().map(|t| t.required_version()))
|
||||
.chain(self.date_range_tag.iter().map(|t| t.required_version()))
|
||||
.chain(self.discontinuity_tag.iter().map(|t| t.required_version()))
|
||||
.chain(
|
||||
self.program_date_time_tag
|
||||
.iter()
|
||||
.map(|t| t.requires_version()),
|
||||
.map(|t| t.required_version()),
|
||||
)
|
||||
.chain(iter::once(self.inf_tag.requires_version()))
|
||||
.chain(iter::once(self.inf_tag.required_version()))
|
||||
.max()
|
||||
.unwrap_or(ProtocolVersion::V7)
|
||||
}
|
||||
|
|
|
@ -1,21 +1,30 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::utils::tag;
|
||||
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
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default)]
|
||||
pub struct ExtM3u;
|
||||
|
||||
impl ExtM3u {
|
||||
pub(crate) const PREFIX: &'static str = "#EXTM3U";
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub const fn requires_version(&self) -> ProtocolVersion {
|
||||
impl RequiredVersion for ExtM3u {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
|
@ -50,7 +59,7 @@ mod test {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_requires_version() {
|
||||
assert_eq!(ExtM3u.requires_version(), ProtocolVersion::V1);
|
||||
fn test_required_version() {
|
||||
assert_eq!(ExtM3u.required_version(), ProtocolVersion::V1);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,31 +1,62 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::utils::tag;
|
||||
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
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||
pub struct ExtXVersion(ProtocolVersion);
|
||||
|
||||
impl ExtXVersion {
|
||||
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 {
|
||||
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
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXVersion;
|
||||
/// use hls_m3u8::types::ProtocolVersion;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// ExtXVersion::new(ProtocolVersion::V6).version(),
|
||||
/// ProtocolVersion::V6
|
||||
/// );
|
||||
/// ```
|
||||
pub const fn version(&self) -> ProtocolVersion {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub const fn requires_version(&self) -> ProtocolVersion {
|
||||
impl RequiredVersion for ExtXVersion {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
|
@ -72,24 +103,16 @@ mod test {
|
|||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(
|
||||
"#EXT-X-VERSION:6".parse().ok(),
|
||||
Some(ExtXVersion::new(ProtocolVersion::V6))
|
||||
"#EXT-X-VERSION:6".parse::<ExtXVersion>().unwrap(),
|
||||
ExtXVersion::new(ProtocolVersion::V6)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_requires_version() {
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
ExtXVersion::new(ProtocolVersion::V6).requires_version(),
|
||||
ExtXVersion::new(ProtocolVersion::V6).required_version(),
|
||||
ProtocolVersion::V1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_version() {
|
||||
assert_eq!(
|
||||
ExtXVersion::new(ProtocolVersion::V6).version(),
|
||||
ProtocolVersion::V6
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,12 +3,23 @@ use std::ops::{Deref, DerefMut};
|
|||
use std::str::FromStr;
|
||||
|
||||
use crate::attribute::AttributePairs;
|
||||
use crate::types::{ProtocolVersion, StreamInf};
|
||||
use crate::types::{ProtocolVersion, RequiredVersion, StreamInf};
|
||||
use crate::utils::{quote, tag, unquote};
|
||||
use crate::Error;
|
||||
|
||||
/// [4.3.4.3. EXT-X-I-FRAME-STREAM-INF]
|
||||
/// # [4.3.4.3. EXT-X-I-FRAME-STREAM-INF]
|
||||
/// The [ExtXIFrameStreamInf] tag identifies a [Media Playlist] file
|
||||
/// 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].
|
||||
///
|
||||
/// Its format is:
|
||||
///
|
||||
/// ```text
|
||||
/// #EXT-X-I-FRAME-STREAM-INF:<attribute-list>
|
||||
/// ```
|
||||
///
|
||||
/// [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)]
|
||||
pub struct ExtXIFrameStreamInf {
|
||||
|
@ -19,7 +30,7 @@ pub struct ExtXIFrameStreamInf {
|
|||
impl ExtXIFrameStreamInf {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAME-STREAM-INF:";
|
||||
|
||||
/// Makes a new `ExtXIFrameStreamInf` tag.
|
||||
/// Makes a new [ExtXIFrameStreamInf] tag.
|
||||
pub fn new<T: ToString>(uri: T, bandwidth: u64) -> Self {
|
||||
ExtXIFrameStreamInf {
|
||||
uri: uri.to_string(),
|
||||
|
@ -27,7 +38,7 @@ impl ExtXIFrameStreamInf {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns the URI, that identifies the associated media playlist.
|
||||
/// Returns the `URI`, that identifies the associated media playlist.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
|
@ -40,7 +51,7 @@ impl ExtXIFrameStreamInf {
|
|||
&self.uri
|
||||
}
|
||||
|
||||
/// Sets the URI, that identifies the associated media playlist.
|
||||
/// Sets the `URI`, that identifies the associated media playlist.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
|
@ -55,9 +66,10 @@ impl ExtXIFrameStreamInf {
|
|||
self.uri = value.to_string();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub const fn requires_version(&self) -> ProtocolVersion {
|
||||
impl RequiredVersion for ExtXIFrameStreamInf {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
|
@ -114,28 +126,26 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
let text = r#"#EXT-X-I-FRAME-STREAM-INF:URI="foo",BANDWIDTH=1000"#;
|
||||
assert_eq!(ExtXIFrameStreamInf::new("foo", 1000).to_string(), text);
|
||||
assert_eq!(
|
||||
ExtXIFrameStreamInf::new("foo", 1000).to_string(),
|
||||
"#EXT-X-I-FRAME-STREAM-INF:URI=\"foo\",BANDWIDTH=1000".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
let text = r#"#EXT-X-I-FRAME-STREAM-INF:URI="foo",BANDWIDTH=1000"#;
|
||||
let i_frame_stream_inf = ExtXIFrameStreamInf::new("foo", 1000);
|
||||
assert_eq!(
|
||||
text.parse::<ExtXIFrameStreamInf>().unwrap(),
|
||||
i_frame_stream_inf.clone()
|
||||
"#EXT-X-I-FRAME-STREAM-INF:URI=\"foo\",BANDWIDTH=1000"
|
||||
.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]
|
||||
fn test_requires_version() {
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
ExtXIFrameStreamInf::new("foo", 1000).requires_version(),
|
||||
ExtXIFrameStreamInf::new("foo", 1000).required_version(),
|
||||
ProtocolVersion::V1
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,199 +1,110 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use derive_builder::Builder;
|
||||
|
||||
use crate::attribute::AttributePairs;
|
||||
use crate::types::{InStreamId, MediaType, ProtocolVersion};
|
||||
use crate::types::{InStreamId, MediaType, ProtocolVersion, RequiredVersion};
|
||||
use crate::utils::{parse_yes_or_no, quote, tag, unquote};
|
||||
use crate::Error;
|
||||
|
||||
/// `ExtXMedia` builder.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExtXMediaBuilder {
|
||||
media_type: Option<MediaType>,
|
||||
/// # [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.
|
||||
///
|
||||
/// 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)]
|
||||
#[builder(setter(into))]
|
||||
#[builder(build_fn(validate = "Self::validate"))]
|
||||
pub struct ExtXMedia {
|
||||
/// Sets the media type of the rendition.
|
||||
media_type: MediaType,
|
||||
#[builder(setter(strip_option, into), default)]
|
||||
/// Sets the URI that identifies the media playlist.
|
||||
uri: Option<String>,
|
||||
group_id: Option<String>,
|
||||
/// Sets the identifier that specifies the group to which the rendition belongs.
|
||||
group_id: String,
|
||||
/// Sets the name of the primary language used in the rendition.
|
||||
#[builder(setter(strip_option, into), default)]
|
||||
language: Option<String>,
|
||||
/// Sets the name of a language associated with the rendition.
|
||||
#[builder(setter(strip_option, into), default)]
|
||||
assoc_language: Option<String>,
|
||||
name: Option<String>,
|
||||
default: bool,
|
||||
autoselect: Option<bool>,
|
||||
forced: Option<bool>,
|
||||
/// Sets a human-readable description of the rendition.
|
||||
name: String,
|
||||
/// Sets the value of the `default` flag.
|
||||
#[builder(default)]
|
||||
is_default: bool,
|
||||
/// Sets the value of the `autoselect` flag.
|
||||
#[builder(default)]
|
||||
is_autoselect: bool,
|
||||
/// Sets the value of the `forced` flag.
|
||||
#[builder(default)]
|
||||
is_forced: bool,
|
||||
/// Sets the identifier that specifies a rendition within the segments in the media playlist.
|
||||
#[builder(setter(strip_option, into), default)]
|
||||
instream_id: Option<InStreamId>,
|
||||
/// Sets the string that represents uniform type identifiers (UTI).
|
||||
#[builder(setter(strip_option, into), default)]
|
||||
characteristics: Option<String>,
|
||||
/// Sets the string that represents the parameters of the rendition.
|
||||
#[builder(setter(strip_option, into), default)]
|
||||
channels: Option<String>,
|
||||
}
|
||||
|
||||
impl ExtXMediaBuilder {
|
||||
/// Makes a `ExtXMediaBuilder` instance.
|
||||
pub const fn new() -> Self {
|
||||
ExtXMediaBuilder {
|
||||
media_type: None,
|
||||
uri: None,
|
||||
group_id: None,
|
||||
language: None,
|
||||
assoc_language: None,
|
||||
name: None,
|
||||
default: false,
|
||||
autoselect: None,
|
||||
forced: None,
|
||||
instream_id: None,
|
||||
characteristics: None,
|
||||
channels: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the media type of the rendition.
|
||||
pub fn media_type(&mut self, media_type: MediaType) -> &mut Self {
|
||||
self.media_type = Some(media_type);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the identifier that specifies the group to which the rendition belongs.
|
||||
pub fn group_id<T: ToString>(&mut self, group_id: T) -> &mut Self {
|
||||
self.group_id = Some(group_id.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets a human-readable description of the rendition.
|
||||
pub fn name<T: ToString>(&mut self, name: T) -> &mut Self {
|
||||
self.name = Some(name.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the URI that identifies the media playlist.
|
||||
pub fn uri<T: ToString>(&mut self, uri: T) -> &mut Self {
|
||||
self.uri = Some(uri.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the name of the primary language used in the rendition.
|
||||
pub fn language<T: ToString>(&mut self, language: T) -> &mut Self {
|
||||
self.language = Some(language.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the name of a language associated with the rendition.
|
||||
pub fn assoc_language<T: ToString>(&mut self, language: T) -> &mut Self {
|
||||
self.assoc_language = Some(language.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the value of the `default` flag.
|
||||
pub fn default(&mut self, b: bool) -> &mut Self {
|
||||
self.default = b;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the value of the `autoselect` flag.
|
||||
pub fn autoselect(&mut self, b: bool) -> &mut Self {
|
||||
self.autoselect = Some(b);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the value of the `forced` flag.
|
||||
pub fn forced(&mut self, b: bool) -> &mut Self {
|
||||
self.forced = Some(b);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the identifier that specifies a rendition within the segments in the media playlist.
|
||||
pub fn instream_id(&mut self, id: InStreamId) -> &mut Self {
|
||||
self.instream_id = Some(id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the string that represents uniform type identifiers (UTI).
|
||||
pub fn characteristics<T: ToString>(&mut self, characteristics: T) -> &mut Self {
|
||||
self.characteristics = Some(characteristics.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the string that represents the parameters of the rendition.
|
||||
pub fn channels<T: ToString>(&mut self, channels: T) -> &mut Self {
|
||||
self.channels = Some(channels.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Builds a `ExtXMedia` instance.
|
||||
pub fn finish(self) -> crate::Result<ExtXMedia> {
|
||||
fn validate(&self) -> Result<(), String> {
|
||||
let media_type = self
|
||||
.media_type
|
||||
.ok_or(Error::missing_value("self.media_type"))?;
|
||||
let group_id = self.group_id.ok_or(Error::missing_value("self.group_id"))?;
|
||||
let name = self.name.ok_or(Error::missing_value("self.name"))?;
|
||||
.ok_or(Error::missing_attribute("MEDIA-TYPE").to_string())?;
|
||||
|
||||
if MediaType::ClosedCaptions == media_type {
|
||||
if let None = self.uri {
|
||||
return Err(Error::missing_value("self.uri"));
|
||||
if self.uri.is_some() {
|
||||
return Err(Error::custom(
|
||||
"Unexpected attribute: \"URL\" for MediaType::ClosedCaptions!",
|
||||
)
|
||||
.to_string());
|
||||
}
|
||||
self.instream_id
|
||||
.ok_or(Error::missing_value("self.instream_id"))?;
|
||||
.ok_or(Error::missing_attribute("INSTREAM-ID").to_string())?;
|
||||
} else {
|
||||
if let Some(_) = &self.instream_id {
|
||||
Err(Error::invalid_input())?;
|
||||
if self.instream_id.is_some() {
|
||||
return Err(Error::custom("Unexpected attribute: \"INSTREAM-ID\"!").to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if self.default && self.autoselect.is_some() {
|
||||
if let Some(value) = &self.autoselect {
|
||||
if *value {
|
||||
Err(Error::invalid_input())?;
|
||||
}
|
||||
}
|
||||
if self.is_default.unwrap_or(false) && !self.is_autoselect.unwrap_or(false) {
|
||||
return Err(
|
||||
Error::custom("If `DEFAULT` is true, `AUTOSELECT` has to be true too!").to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
if MediaType::Subtitles != media_type {
|
||||
if self.forced.is_some() {
|
||||
Err(Error::invalid_input())?;
|
||||
if self.is_forced.is_some() {
|
||||
return Err(Error::invalid_input().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ExtXMedia {
|
||||
media_type,
|
||||
uri: self.uri,
|
||||
group_id,
|
||||
language: self.language,
|
||||
assoc_language: self.assoc_language,
|
||||
name,
|
||||
default: self.default,
|
||||
autoselect: self.autoselect.unwrap_or(false),
|
||||
forced: self.forced.unwrap_or(false),
|
||||
instream_id: self.instream_id,
|
||||
characteristics: self.characteristics,
|
||||
channels: self.channels,
|
||||
})
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ExtXMediaBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// [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
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ExtXMedia {
|
||||
media_type: MediaType,
|
||||
uri: Option<String>,
|
||||
group_id: String,
|
||||
language: Option<String>,
|
||||
assoc_language: Option<String>,
|
||||
name: String,
|
||||
default: bool,
|
||||
autoselect: bool,
|
||||
forced: bool,
|
||||
instream_id: Option<InStreamId>,
|
||||
characteristics: Option<String>,
|
||||
channels: Option<String>,
|
||||
}
|
||||
|
||||
impl ExtXMedia {
|
||||
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 {
|
||||
ExtXMedia {
|
||||
media_type,
|
||||
|
@ -202,15 +113,20 @@ impl ExtXMedia {
|
|||
language: None,
|
||||
assoc_language: None,
|
||||
name: name.to_string(),
|
||||
default: false,
|
||||
autoselect: false,
|
||||
forced: false,
|
||||
is_default: false,
|
||||
is_autoselect: false,
|
||||
is_forced: false,
|
||||
instream_id: None,
|
||||
characteristics: None,
|
||||
channels: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes a [ExtXMediaBuilder] for [ExtXMedia].
|
||||
pub fn builder() -> ExtXMediaBuilder {
|
||||
ExtXMediaBuilder::default()
|
||||
}
|
||||
|
||||
/// Returns the type of the media associated with this tag.
|
||||
pub const fn media_type(&self) -> MediaType {
|
||||
self.media_type
|
||||
|
@ -242,19 +158,19 @@ impl ExtXMedia {
|
|||
}
|
||||
|
||||
/// Returns whether this is the default rendition.
|
||||
pub const fn default(&self) -> bool {
|
||||
self.default
|
||||
pub const fn is_default(&self) -> bool {
|
||||
self.is_default
|
||||
}
|
||||
|
||||
/// Returns whether the client may choose to
|
||||
/// play this rendition in the absence of explicit user preference.
|
||||
pub const fn autoselect(&self) -> bool {
|
||||
self.autoselect
|
||||
self.is_autoselect
|
||||
}
|
||||
|
||||
/// Returns whether the rendition contains content that is considered essential to play.
|
||||
pub const fn forced(&self) -> bool {
|
||||
self.forced
|
||||
pub const fn is_forced(&self) -> bool {
|
||||
self.is_forced
|
||||
}
|
||||
|
||||
/// Returns the identifier that specifies a rendition within the segments in the media playlist.
|
||||
|
@ -273,9 +189,10 @@ impl ExtXMedia {
|
|||
pub fn channels(&self) -> Option<&String> {
|
||||
self.channels.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub fn requires_version(&self) -> ProtocolVersion {
|
||||
impl RequiredVersion for ExtXMedia {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
match self.instream_id {
|
||||
None
|
||||
| Some(InStreamId::Cc1)
|
||||
|
@ -302,13 +219,13 @@ impl fmt::Display for ExtXMedia {
|
|||
write!(f, ",ASSOC-LANGUAGE={}", quote(value))?;
|
||||
}
|
||||
write!(f, ",NAME={}", quote(&self.name))?;
|
||||
if self.default {
|
||||
if self.is_default {
|
||||
write!(f, ",DEFAULT=YES")?;
|
||||
}
|
||||
if self.autoselect {
|
||||
if self.is_autoselect {
|
||||
write!(f, ",AUTOSELECT=YES")?;
|
||||
}
|
||||
if self.forced {
|
||||
if self.is_forced {
|
||||
write!(f, ",FORCED=YES")?;
|
||||
}
|
||||
if let Some(value) = &self.instream_id {
|
||||
|
@ -330,12 +247,12 @@ impl FromStr for ExtXMedia {
|
|||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
let input = tag(input, Self::PREFIX)?;
|
||||
|
||||
let mut builder = ExtXMediaBuilder::new();
|
||||
let mut builder = ExtXMedia::builder();
|
||||
|
||||
for (key, value) in input.parse::<AttributePairs>()? {
|
||||
match key.as_str() {
|
||||
"TYPE" => {
|
||||
builder.media_type(value.parse()?);
|
||||
builder.media_type(value.parse::<MediaType>()?);
|
||||
}
|
||||
"URI" => {
|
||||
builder.uri(unquote(value));
|
||||
|
@ -353,16 +270,16 @@ impl FromStr for ExtXMedia {
|
|||
builder.name(unquote(value));
|
||||
}
|
||||
"DEFAULT" => {
|
||||
builder.default((parse_yes_or_no(value))?);
|
||||
builder.is_default(parse_yes_or_no(value)?);
|
||||
}
|
||||
"AUTOSELECT" => {
|
||||
builder.autoselect((parse_yes_or_no(value))?);
|
||||
builder.is_autoselect(parse_yes_or_no(value)?);
|
||||
}
|
||||
"FORCED" => {
|
||||
builder.forced((parse_yes_or_no(value))?);
|
||||
builder.is_forced(parse_yes_or_no(value)?);
|
||||
}
|
||||
"INSTREAM-ID" => {
|
||||
builder.instream_id(unquote(value).parse()?);
|
||||
builder.instream_id(unquote(value).parse::<InStreamId>()?);
|
||||
}
|
||||
"CHARACTERISTICS" => {
|
||||
builder.characteristics(unquote(value));
|
||||
|
@ -376,7 +293,7 @@ impl FromStr for ExtXMedia {
|
|||
}
|
||||
}
|
||||
}
|
||||
(builder.finish())
|
||||
builder.build().map_err(Error::builder_error)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -385,11 +302,74 @@ mod test {
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ext_x_media() {
|
||||
let tag = ExtXMedia::new(MediaType::Audio, "foo", "bar");
|
||||
let text = r#"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="foo",NAME="bar""#;
|
||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
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!(
|
||||
ExtXMedia::new(MediaType::Audio, "foo", "bar").to_string(),
|
||||
"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"foo\",NAME=\"bar\"".to_string()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(
|
||||
ExtXMedia::new(MediaType::Audio, "foo", "bar"),
|
||||
"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"foo\",NAME=\"bar\""
|
||||
.parse()
|
||||
.unwrap()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
ExtXMedia::new(MediaType::Audio, "foo", "bar").required_version(),
|
||||
ProtocolVersion::V1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ use std::str::FromStr;
|
|||
use derive_builder::Builder;
|
||||
|
||||
use crate::attribute::AttributePairs;
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::utils::{quote, tag, unquote};
|
||||
use crate::Error;
|
||||
|
||||
|
@ -22,11 +22,12 @@ pub enum SessionData {
|
|||
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
|
||||
/// 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
|
||||
#[derive(Builder, Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)]
|
||||
#[builder(setter(into))]
|
||||
|
@ -243,18 +244,10 @@ impl ExtXSessionData {
|
|||
self.data = value;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version, that this tag requires.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// # use hls_m3u8::types::ProtocolVersion;
|
||||
/// # use hls_m3u8::tags::{ExtXSessionData, SessionData};
|
||||
/// #
|
||||
/// let tag = ExtXSessionData::new("foo", SessionData::Value("bar".into()));
|
||||
/// assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
/// ```
|
||||
pub const fn requires_version(&self) -> ProtocolVersion {
|
||||
impl RequiredVersion for ExtXSessionData {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
|
@ -327,40 +320,73 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
let tag = ExtXSessionData::new("foo", SessionData::Value("bar".into()));
|
||||
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar""#;
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(
|
||||
"#EXT-X-SESSION-DATA:DATA-ID=\"com.example.lyrics\",URI=\"lyrics.json\"".to_string(),
|
||||
ExtXSessionData::new(
|
||||
"com.example.lyrics",
|
||||
SessionData::Uri("lyrics.json".to_string())
|
||||
)
|
||||
.to_string()
|
||||
);
|
||||
|
||||
let tag = ExtXSessionData::new("foo", SessionData::Uri("bar".into()));
|
||||
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",URI="bar""#;
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(
|
||||
"#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\
|
||||
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");
|
||||
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar",LANGUAGE="baz""#;
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(
|
||||
"#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\
|
||||
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]
|
||||
fn test_parser() {
|
||||
let tag = "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.lyrics\",URI=\"lyrics.json\""
|
||||
.parse::<ExtXSessionData>()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
tag,
|
||||
"#EXT-X-SESSION-DATA:DATA-ID=\"com.example.lyrics\",URI=\"lyrics.json\""
|
||||
.parse::<ExtXSessionData>()
|
||||
.unwrap(),
|
||||
ExtXSessionData::new(
|
||||
"com.example.lyrics",
|
||||
SessionData::Uri("lyrics.json".to_string())
|
||||
)
|
||||
);
|
||||
|
||||
let tag = "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\
|
||||
assert_eq!(
|
||||
"#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\
|
||||
LANGUAGE=\"en\", VALUE=\"This is an example\""
|
||||
.parse::<ExtXSessionData>()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
tag,
|
||||
.unwrap(),
|
||||
ExtXSessionData::with_language(
|
||||
"com.example.title",
|
||||
SessionData::Value("This is an example".to_string()),
|
||||
|
@ -368,13 +394,11 @@ mod test {
|
|||
)
|
||||
);
|
||||
|
||||
let tag = "#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\
|
||||
assert_eq!(
|
||||
"#EXT-X-SESSION-DATA:DATA-ID=\"com.example.title\",\
|
||||
LANGUAGE=\"es\", VALUE=\"Este es un ejemplo\""
|
||||
.parse::<ExtXSessionData>()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
tag,
|
||||
.unwrap(),
|
||||
ExtXSessionData::with_language(
|
||||
"com.example.title",
|
||||
SessionData::Value("Este es un ejemplo".to_string()),
|
||||
|
@ -382,16 +406,37 @@ mod test {
|
|||
)
|
||||
);
|
||||
|
||||
let tag = ExtXSessionData::new("foo", SessionData::Value("bar".into()));
|
||||
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar""#;
|
||||
assert_eq!(text.parse::<ExtXSessionData>().unwrap(), tag);
|
||||
assert_eq!(
|
||||
"#EXT-X-SESSION-DATA:DATA-ID=\"foo\",VALUE=\"bar\""
|
||||
.parse::<ExtXSessionData>()
|
||||
.unwrap(),
|
||||
ExtXSessionData::new("foo", SessionData::Value("bar".into()))
|
||||
);
|
||||
|
||||
let tag = ExtXSessionData::new("foo", SessionData::Uri("bar".into()));
|
||||
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",URI="bar""#;
|
||||
assert_eq!(text.parse::<ExtXSessionData>().unwrap(), tag);
|
||||
assert_eq!(
|
||||
"#EXT-X-SESSION-DATA:DATA-ID=\"foo\",URI=\"bar\""
|
||||
.parse::<ExtXSessionData>()
|
||||
.unwrap(),
|
||||
ExtXSessionData::new("foo", SessionData::Uri("bar".into()))
|
||||
);
|
||||
|
||||
let tag = ExtXSessionData::with_language("foo", SessionData::Value("bar".into()), "baz");
|
||||
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar",LANGUAGE="baz""#;
|
||||
assert_eq!(text.parse::<ExtXSessionData>().unwrap(), tag);
|
||||
assert_eq!(
|
||||
"#EXT-X-SESSION-DATA:DATA-ID=\"foo\",VALUE=\"bar\",LANGUAGE=\"baz\""
|
||||
.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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,12 +2,23 @@ use std::fmt;
|
|||
use std::ops::{Deref, DerefMut};
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::{DecryptionKey, EncryptionMethod, ProtocolVersion};
|
||||
use crate::types::{DecryptionKey, EncryptionMethod, ProtocolVersion, RequiredVersion};
|
||||
use crate::utils::tag;
|
||||
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
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ExtXSessionKey(DecryptionKey);
|
||||
|
@ -16,8 +27,21 @@ impl ExtXSessionKey {
|
|||
pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-KEY:";
|
||||
|
||||
/// Makes a new [ExtXSessionKey] tag.
|
||||
///
|
||||
/// # 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 {
|
||||
if method == EncryptionMethod::None {
|
||||
panic!("The EncryptionMethod is not allowed to be None");
|
||||
|
@ -25,36 +49,19 @@ impl ExtXSessionKey {
|
|||
|
||||
Self(DecryptionKey::new(method, uri))
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use hls_m3u8::tags::ExtXSessionKey;
|
||||
/// use hls_m3u8::types::{EncryptionMethod, ProtocolVersion};
|
||||
///
|
||||
/// let mut key = ExtXSessionKey::new(
|
||||
/// EncryptionMethod::Aes128,
|
||||
/// "https://www.example.com/"
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// key.requires_version(),
|
||||
/// ProtocolVersion::V1
|
||||
/// );
|
||||
/// ```
|
||||
pub fn requires_version(&self) -> ProtocolVersion {
|
||||
if self.0.key_format.is_some() | self.0.key_format_versions.is_some() {
|
||||
ProtocolVersion::V5
|
||||
} else if self.0.iv.is_some() {
|
||||
ProtocolVersion::V2
|
||||
} else {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
|
||||
impl RequiredVersion for ExtXSessionKey {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
self.0.required_version()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ExtXSessionKey {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -85,7 +92,7 @@ impl DerefMut for ExtXSessionKey {
|
|||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::types::EncryptionMethod;
|
||||
use crate::types::{EncryptionMethod, KeyFormat};
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
@ -135,12 +142,26 @@ mod test {
|
|||
key
|
||||
);
|
||||
|
||||
key.set_key_format("baz");
|
||||
key.set_key_format(Some(KeyFormat::Identity));
|
||||
|
||||
assert_eq!(
|
||||
r#"#EXT-X-SESSION-KEY:METHOD=AES-128,URI="https://www.example.com/hls-key/key.bin",IV=0x10ef8f758ca555115584bb5b3c687f52,KEYFORMAT="baz""#
|
||||
.parse::<ExtXSessionKey>().unwrap(),
|
||||
"#EXT-X-SESSION-KEY:\
|
||||
METHOD=AES-128,\
|
||||
URI=\"https://www.example.com/hls-key/key.bin\",\
|
||||
IV=0x10ef8f758ca555115584bb5b3c687f52,\
|
||||
KEYFORMAT=\"identity\""
|
||||
.parse::<ExtXSessionKey>()
|
||||
.unwrap(),
|
||||
key
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
ExtXSessionKey::new(EncryptionMethod::Aes128, "https://www.example.com/")
|
||||
.required_version(),
|
||||
ProtocolVersion::V1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,9 @@ use std::ops::{Deref, DerefMut};
|
|||
use std::str::FromStr;
|
||||
|
||||
use crate::attribute::AttributePairs;
|
||||
use crate::types::{ClosedCaptions, DecimalFloatingPoint, ProtocolVersion, StreamInf};
|
||||
use crate::types::{
|
||||
ClosedCaptions, DecimalFloatingPoint, ProtocolVersion, RequiredVersion, StreamInf,
|
||||
};
|
||||
use crate::utils::{quote, tag, unquote};
|
||||
use crate::Error;
|
||||
|
||||
|
@ -23,7 +25,9 @@ pub struct ExtXStreamInf {
|
|||
impl ExtXStreamInf {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-STREAM-INF:";
|
||||
|
||||
/// Makes a new [ExtXStreamInf] tag.
|
||||
/// Creates a new [ExtXStreamInf] tag.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use hls_m3u8::tags::ExtXStreamInf;
|
||||
/// #
|
||||
|
@ -40,11 +44,23 @@ impl ExtXStreamInf {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns the URI that identifies the associated media playlist.
|
||||
/// Sets the `URI` that identifies the associated media playlist.
|
||||
pub fn set_uri<T: ToString>(&mut self, value: T) -> &mut Self {
|
||||
self.uri = value.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the `URI` that identifies the associated media playlist.
|
||||
pub const fn uri(&self) -> &String {
|
||||
&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 {
|
||||
self.frame_rate = value.map(|v| v.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the maximum frame rate for all the video in the variant stream.
|
||||
pub fn frame_rate(&self) -> Option<f64> {
|
||||
self.frame_rate.map_or(None, |v| Some(v.as_f64()))
|
||||
|
@ -64,9 +80,10 @@ impl ExtXStreamInf {
|
|||
pub fn closed_captions(&self) -> Option<&ClosedCaptions> {
|
||||
self.closed_captions.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub const fn requires_version(&self) -> ProtocolVersion {
|
||||
impl RequiredVersion for ExtXStreamInf {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
|
@ -158,10 +175,10 @@ mod test {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_requires_version() {
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
ProtocolVersion::V1,
|
||||
ExtXStreamInf::new("http://www.example.com", 1000).requires_version()
|
||||
ExtXStreamInf::new("http://www.example.com", 1000).required_version()
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,40 +1,81 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
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
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ExtXDiscontinuitySequence {
|
||||
seq_num: u64,
|
||||
}
|
||||
/// The [ExtXDiscontinuitySequence] tag allows synchronization between
|
||||
/// 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);
|
||||
|
||||
impl ExtXDiscontinuitySequence {
|
||||
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 {
|
||||
ExtXDiscontinuitySequence { seq_num }
|
||||
Self(seq_num)
|
||||
}
|
||||
|
||||
/// Returns the discontinuity sequence number of
|
||||
/// the first media segment that appears in the associated playlist.
|
||||
pub const fn seq_num(self) -> u64 {
|
||||
self.seq_num
|
||||
///
|
||||
/// # 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 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub const fn requires_version(self) -> ProtocolVersion {
|
||||
/// 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 {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ExtXDiscontinuitySequence {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}{}", Self::PREFIX, self.seq_num)
|
||||
write!(f, "{}{}", Self::PREFIX, self.0)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -42,7 +83,7 @@ impl FromStr for ExtXDiscontinuitySequence {
|
|||
type Err = crate::Error;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
let seq_num = tag(input, Self::PREFIX)?.parse().unwrap(); // TODO!
|
||||
let seq_num = tag(input, Self::PREFIX)?.parse()?;
|
||||
Ok(Self::new(seq_num))
|
||||
}
|
||||
}
|
||||
|
@ -52,11 +93,26 @@ mod test {
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ext_x_discontinuity_sequence() {
|
||||
let tag = ExtXDiscontinuitySequence::new(123);
|
||||
let text = "#EXT-X-DISCONTINUITY-SEQUENCE:123";
|
||||
assert_eq!(text.parse().ok(), Some(tag));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
fn test_display() {
|
||||
assert_eq!(
|
||||
ExtXDiscontinuitySequence::new(123).to_string(),
|
||||
"#EXT-X-DISCONTINUITY-SEQUENCE:123".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
ExtXDiscontinuitySequence::new(123).required_version(),
|
||||
ProtocolVersion::V1
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(
|
||||
ExtXDiscontinuitySequence::new(123),
|
||||
"#EXT-X-DISCONTINUITY-SEQUENCE:123".parse().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,32 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::utils::tag;
|
||||
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)]
|
||||
pub struct ExtXEndList;
|
||||
|
||||
impl ExtXEndList {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-ENDLIST";
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub const fn requires_version(self) -> ProtocolVersion {
|
||||
impl RequiredVersion for ExtXEndList {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
|
@ -39,11 +51,17 @@ mod test {
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ext_x_endlist() {
|
||||
let tag = ExtXEndList;
|
||||
let text = "#EXT-X-ENDLIST";
|
||||
assert_eq!(text.parse().ok(), Some(tag));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
fn test_display() {
|
||||
assert_eq!(ExtXEndList.to_string(), "#EXT-X-ENDLIST".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(ExtXEndList, "#EXT-X-ENDLIST".parse().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(ExtXEndList.required_version(), ProtocolVersion::V1);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,34 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::utils::tag;
|
||||
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)]
|
||||
pub struct ExtXIFramesOnly;
|
||||
|
||||
impl ExtXIFramesOnly {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAMES-ONLY";
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub const fn requires_version(self) -> ProtocolVersion {
|
||||
impl RequiredVersion for ExtXIFramesOnly {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V4
|
||||
}
|
||||
}
|
||||
|
@ -40,11 +53,20 @@ mod test {
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ext_i_frames_only() {
|
||||
let tag = ExtXIFramesOnly;
|
||||
let text = "#EXT-X-I-FRAMES-ONLY";
|
||||
assert_eq!(text.parse().ok(), Some(tag));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V4);
|
||||
fn test_display() {
|
||||
assert_eq!(
|
||||
ExtXIFramesOnly.to_string(),
|
||||
"#EXT-X-I-FRAMES-ONLY".to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(ExtXIFramesOnly, "#EXT-X-I-FRAMES-ONLY".parse().unwrap(),)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(ExtXIFramesOnly.required_version(), ProtocolVersion::V4)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,40 +1,79 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::utils::tag;
|
||||
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
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ExtXMediaSequence {
|
||||
seq_num: u64,
|
||||
}
|
||||
/// Its format is:
|
||||
/// ```text
|
||||
/// #EXT-X-MEDIA-SEQUENCE:<number>
|
||||
/// ```
|
||||
/// where `number` is a [u64].
|
||||
///
|
||||
/// [Media Segment]: crate::MediaSegment
|
||||
/// [4.4.3.2. EXT-X-MEDIA-SEQUENCE]:
|
||||
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.2
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
|
||||
pub struct ExtXMediaSequence(u64);
|
||||
|
||||
impl ExtXMediaSequence {
|
||||
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 {
|
||||
ExtXMediaSequence { seq_num }
|
||||
Self(seq_num)
|
||||
}
|
||||
|
||||
/// Returns the sequence number of the first media segment that appears in the associated playlist.
|
||||
pub const fn seq_num(self) -> u64 {
|
||||
self.seq_num
|
||||
/// Returns the sequence number of the first media segment,
|
||||
/// 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 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub const fn requires_version(self) -> ProtocolVersion {
|
||||
/// 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 {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ExtXMediaSequence {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}{}", Self::PREFIX, self.seq_num)
|
||||
write!(f, "{}{}", Self::PREFIX, self.0)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,7 +82,6 @@ impl FromStr for ExtXMediaSequence {
|
|||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
let seq_num = tag(input, Self::PREFIX)?.parse()?;
|
||||
|
||||
Ok(ExtXMediaSequence::new(seq_num))
|
||||
}
|
||||
}
|
||||
|
@ -53,11 +91,26 @@ mod test {
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ext_x_media_sequence() {
|
||||
let tag = ExtXMediaSequence::new(123);
|
||||
let text = "#EXT-X-MEDIA-SEQUENCE:123";
|
||||
assert_eq!(text.parse().ok(), Some(tag));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
fn test_display() {
|
||||
assert_eq!(
|
||||
ExtXMediaSequence::new(123).to_string(),
|
||||
"#EXT-X-MEDIA-SEQUENCE:123".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
ExtXMediaSequence::new(123).required_version(),
|
||||
ProtocolVersion::V1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(
|
||||
ExtXMediaSequence::new(123),
|
||||
"#EXT-X-MEDIA-SEQUENCE:123".parse().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,39 +1,39 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::utils::tag;
|
||||
use crate::Error;
|
||||
|
||||
/// [4.3.3.5. EXT-X-PLAYLIST-TYPE](https://tools.ietf.org/html/rfc8216#section-4.3.3.5)
|
||||
/// # [4.4.3.5. EXT-X-PLAYLIST-TYPE]
|
||||
///
|
||||
/// The EXT-X-PLAYLIST-TYPE tag provides mutability information about the
|
||||
/// Media Playlist. It applies to the entire Media Playlist.
|
||||
/// It is OPTIONAL. Its format is:
|
||||
/// The [ExtXPlaylistType] tag provides mutability information about the
|
||||
/// [Media Playlist]. It applies to the entire [Media Playlist].
|
||||
///
|
||||
/// Its format is:
|
||||
/// ```text
|
||||
/// #EXT-X-PLAYLIST-TYPE:<type-enum>
|
||||
/// ```
|
||||
///
|
||||
/// # Note
|
||||
/// If the EXT-X-PLAYLIST-TYPE tag is omitted from a Media Playlist, the
|
||||
/// Playlist can be updated according to the rules in Section 6.2.1 with
|
||||
/// no additional restrictions.
|
||||
/// [Media Playlist]: crate::MediaPlaylist
|
||||
/// [4.4.3.5. EXT-X-PLAYLIST-TYPE]:
|
||||
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.5
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
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.
|
||||
Event,
|
||||
/// If the ExtXPlaylistType is Video On Demand (Vod),
|
||||
/// If the [ExtXPlaylistType] is Video On Demand (Vod),
|
||||
/// the Media Playlist cannot change.
|
||||
Vod,
|
||||
}
|
||||
|
||||
impl ExtXPlaylistType {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-PLAYLIST-TYPE:";
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub const fn requires_version(&self) -> ProtocolVersion {
|
||||
impl RequiredVersion for ExtXPlaylistType {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
|
@ -99,13 +99,13 @@ mod test {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_requires_version() {
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
ExtXPlaylistType::Vod.requires_version(),
|
||||
ExtXPlaylistType::Vod.required_version(),
|
||||
ProtocolVersion::V1
|
||||
);
|
||||
assert_eq!(
|
||||
ExtXPlaylistType::Event.requires_version(),
|
||||
ExtXPlaylistType::Event.required_version(),
|
||||
ProtocolVersion::V1
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,43 +2,53 @@ use std::fmt;
|
|||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::utils::tag;
|
||||
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)]
|
||||
pub struct ExtXTargetDuration {
|
||||
duration: Duration,
|
||||
}
|
||||
pub struct ExtXTargetDuration(Duration);
|
||||
|
||||
impl ExtXTargetDuration {
|
||||
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 {
|
||||
let duration = Duration::from_secs(duration.as_secs());
|
||||
ExtXTargetDuration { duration }
|
||||
// TOOD: round instead of discarding?
|
||||
Self(Duration::from_secs(duration.as_secs()))
|
||||
}
|
||||
|
||||
/// Returns the maximum media segment duration in the associated playlist.
|
||||
pub const fn duration(&self) -> Duration {
|
||||
self.duration
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub const fn requires_version(&self) -> ProtocolVersion {
|
||||
impl RequiredVersion for ExtXTargetDuration {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ExtXTargetDuration {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}{}", Self::PREFIX, self.duration.as_secs())
|
||||
write!(f, "{}{}", Self::PREFIX, self.0.as_secs())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -47,9 +57,7 @@ impl FromStr for ExtXTargetDuration {
|
|||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
let input = tag(input, Self::PREFIX)?.parse()?;
|
||||
Ok(ExtXTargetDuration {
|
||||
duration: Duration::from_secs(input),
|
||||
})
|
||||
Ok(Self::new(Duration::from_secs(input)))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -58,11 +66,26 @@ mod test {
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ext_x_targetduration() {
|
||||
let tag = ExtXTargetDuration::new(Duration::from_secs(5));
|
||||
let text = "#EXT-X-TARGETDURATION:5";
|
||||
assert_eq!(text.parse().ok(), Some(tag));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
fn test_display() {
|
||||
assert_eq!(
|
||||
ExtXTargetDuration::new(Duration::from_secs(5)).to_string(),
|
||||
"#EXT-X-TARGETDURATION:5".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
ExtXTargetDuration::new(Duration::from_secs(5)).required_version(),
|
||||
ProtocolVersion::V1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(
|
||||
ExtXTargetDuration::new(Duration::from_secs(5)),
|
||||
"#EXT-X-TARGETDURATION:5".parse().unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +1,39 @@
|
|||
use std::fmt;
|
||||
use std::ops::Deref;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::{ByteRange, ProtocolVersion};
|
||||
use crate::types::{ByteRange, ProtocolVersion, RequiredVersion};
|
||||
use crate::utils::tag;
|
||||
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)]
|
||||
pub struct ExtXByteRange(ByteRange);
|
||||
|
||||
impl ExtXByteRange {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-BYTERANGE:";
|
||||
|
||||
/// Makes a new `ExtXByteRange` tag.
|
||||
/// Makes a new [ExtXByteRange] tag.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use hls_m3u8::tags::ExtXByteRange;
|
||||
///
|
||||
/// # use hls_m3u8::tags::ExtXByteRange;
|
||||
/// let byte_range = ExtXByteRange::new(20, Some(5));
|
||||
/// ```
|
||||
pub const fn new(length: usize, start: Option<usize>) -> Self {
|
||||
|
@ -27,9 +41,10 @@ impl ExtXByteRange {
|
|||
}
|
||||
|
||||
/// Converts the [ExtXByteRange] to a [ByteRange].
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use hls_m3u8::tags::ExtXByteRange;
|
||||
/// # use hls_m3u8::tags::ExtXByteRange;
|
||||
/// use hls_m3u8::types::ByteRange;
|
||||
///
|
||||
/// let byte_range = ExtXByteRange::new(20, Some(5));
|
||||
|
@ -38,17 +53,10 @@ impl ExtXByteRange {
|
|||
pub const fn to_range(&self) -> ByteRange {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use hls_m3u8::tags::ExtXByteRange;
|
||||
/// use hls_m3u8::types::ProtocolVersion;
|
||||
///
|
||||
/// let byte_range = ExtXByteRange::new(20, Some(5));
|
||||
/// assert_eq!(byte_range.requires_version(), ProtocolVersion::V4);
|
||||
/// ```
|
||||
pub const fn requires_version(&self) -> ProtocolVersion {
|
||||
impl RequiredVersion for ExtXByteRange {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V4
|
||||
}
|
||||
}
|
||||
|
@ -61,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 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", Self::PREFIX)?;
|
||||
|
@ -76,6 +90,7 @@ impl FromStr for ExtXByteRange {
|
|||
let input = tag(input, Self::PREFIX)?;
|
||||
|
||||
let tokens = input.splitn(2, '@').collect::<Vec<_>>();
|
||||
|
||||
if tokens.is_empty() {
|
||||
return Err(Error::invalid_input());
|
||||
}
|
||||
|
@ -135,4 +150,23 @@ mod test {
|
|||
assert_eq!(byte_range.length(), 0);
|
||||
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]
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
ExtXByteRange::new(20, Some(5)).required_version(),
|
||||
ProtocolVersion::V4
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ use std::time::Duration;
|
|||
use chrono::{DateTime, FixedOffset};
|
||||
|
||||
use crate::attribute::AttributePairs;
|
||||
use crate::types::{DecimalFloatingPoint, ProtocolVersion};
|
||||
use crate::types::{DecimalFloatingPoint, ProtocolVersion, RequiredVersion};
|
||||
use crate::utils::{quote, tag, unquote};
|
||||
use crate::Error;
|
||||
|
||||
|
@ -18,8 +18,8 @@ use crate::Error;
|
|||
#[allow(missing_docs)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ExtXDateRange {
|
||||
/// A string that uniquely identifies a Date Range in the Playlist.
|
||||
/// This attribute is REQUIRED.
|
||||
/// A string that uniquely identifies a [ExtXDateRange] in the Playlist.
|
||||
/// This attribute is required.
|
||||
id: String,
|
||||
/// 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
|
||||
|
@ -63,9 +63,10 @@ pub struct ExtXDateRange {
|
|||
|
||||
impl ExtXDateRange {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-DATERANGE:";
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub const fn requires_version(&self) -> ProtocolVersion {
|
||||
impl RequiredVersion for ExtXDateRange {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,31 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::utils::tag;
|
||||
use crate::{Error, Result};
|
||||
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)]
|
||||
pub struct ExtXDiscontinuity;
|
||||
|
||||
impl ExtXDiscontinuity {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY";
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub const fn requires_version(self) -> ProtocolVersion {
|
||||
impl RequiredVersion for ExtXDiscontinuity {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +39,7 @@ impl fmt::Display for ExtXDiscontinuity {
|
|||
impl FromStr for ExtXDiscontinuity {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self> {
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
tag(input, Self::PREFIX)?;
|
||||
Ok(ExtXDiscontinuity)
|
||||
}
|
||||
|
@ -40,10 +50,20 @@ mod test {
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ext_x_discontinuity() {
|
||||
let tag = ExtXDiscontinuity;
|
||||
assert_eq!("#EXT-X-DISCONTINUITY".parse().ok(), Some(tag));
|
||||
assert_eq!(tag.to_string(), "#EXT-X-DISCONTINUITY");
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
fn test_display() {
|
||||
assert_eq!(
|
||||
ExtXDiscontinuity.to_string(),
|
||||
"#EXT-X-DISCONTINUITY".to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(ExtXDiscontinuity, "#EXT-X-DISCONTINUITY".parse().unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(ExtXDiscontinuity.required_version(), ProtocolVersion::V1)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,14 +2,14 @@ use std::fmt;
|
|||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::types::{DecimalFloatingPoint, ProtocolVersion};
|
||||
use crate::types::{DecimalFloatingPoint, ProtocolVersion, RequiredVersion};
|
||||
use crate::utils::tag;
|
||||
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
|
||||
/// only to the next [Media Segment]. This tag is REQUIRED for each [Media Segment].
|
||||
/// only to the next [Media Segment].
|
||||
///
|
||||
/// Its format is:
|
||||
/// ```text
|
||||
|
@ -18,32 +18,8 @@ use crate::Error;
|
|||
/// The title is an optional informative title about the [Media Segment].
|
||||
///
|
||||
/// [Media Segment]: crate::media_segment::MediaSegment
|
||||
///
|
||||
/// # Examples
|
||||
/// Parsing from a String:
|
||||
/// ```
|
||||
/// use std::time::Duration;
|
||||
/// use hls_m3u8::tags::ExtInf;
|
||||
///
|
||||
/// let ext_inf = "#EXTINF:8,".parse::<ExtInf>().expect("Failed to parse tag!");
|
||||
///
|
||||
/// assert_eq!(ext_inf.duration(), Duration::from_secs(8));
|
||||
/// assert_eq!(ext_inf.title(), None);
|
||||
/// ```
|
||||
///
|
||||
/// Converting to a String:
|
||||
/// ```
|
||||
/// use std::time::Duration;
|
||||
/// use hls_m3u8::tags::ExtInf;
|
||||
///
|
||||
/// let ext_inf = ExtInf::with_title(
|
||||
/// Duration::from_millis(88),
|
||||
/// "title"
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(ext_inf.duration(), Duration::from_millis(88));
|
||||
/// assert_eq!(ext_inf.to_string(), "#EXTINF:0.088,title".to_string());
|
||||
/// ```
|
||||
/// [4.4.2.1. EXTINF]:
|
||||
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.1
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct ExtInf {
|
||||
duration: Duration,
|
||||
|
@ -53,7 +29,15 @@ pub struct ExtInf {
|
|||
impl 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 {
|
||||
ExtInf {
|
||||
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 {
|
||||
ExtInf {
|
||||
duration,
|
||||
|
@ -70,17 +62,86 @@ impl ExtInf {
|
|||
}
|
||||
|
||||
/// 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 {
|
||||
self.duration
|
||||
}
|
||||
|
||||
/// Returns the title of the associated media segment.
|
||||
pub fn title(&self) -> Option<&String> {
|
||||
self.title.as_ref()
|
||||
/// 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 protocol compatibility version that this tag requires.
|
||||
pub fn requires_version(&self) -> ProtocolVersion {
|
||||
/// Returns the title of the associated media segment.
|
||||
///
|
||||
/// # 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
|
||||
}
|
||||
}
|
||||
|
||||
impl RequiredVersion for ExtInf {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
if self.duration.subsec_nanos() == 0 {
|
||||
ProtocolVersion::V1
|
||||
} else {
|
||||
|
@ -198,21 +259,21 @@ mod test {
|
|||
|
||||
#[test]
|
||||
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!(
|
||||
ExtInf::with_title(Duration::from_secs(5), "title").title(),
|
||||
Some(&"title".to_string())
|
||||
&Some("title".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_requires_version() {
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
ExtInf::new(Duration::from_secs(4)).requires_version(),
|
||||
ExtInf::new(Duration::from_secs(4)).required_version(),
|
||||
ProtocolVersion::V1
|
||||
);
|
||||
assert_eq!(
|
||||
ExtInf::new(Duration::from_millis(4400)).requires_version(),
|
||||
ExtInf::new(Duration::from_millis(4400)).required_version(),
|
||||
ProtocolVersion::V3
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,15 +2,30 @@ use std::fmt;
|
|||
use std::ops::{Deref, DerefMut};
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::{DecryptionKey, EncryptionMethod};
|
||||
use crate::types::{DecryptionKey, EncryptionMethod, KeyFormatVersions};
|
||||
use crate::utils::tag;
|
||||
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
|
||||
/// 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)]
|
||||
pub struct ExtXKey(DecryptionKey);
|
||||
|
||||
|
@ -49,13 +64,13 @@ impl ExtXKey {
|
|||
/// "#EXT-X-KEY:METHOD=NONE"
|
||||
/// );
|
||||
/// ```
|
||||
pub const fn empty() -> Self {
|
||||
pub fn empty() -> Self {
|
||||
Self(DecryptionKey {
|
||||
method: EncryptionMethod::None,
|
||||
uri: None,
|
||||
iv: None,
|
||||
key_format: None,
|
||||
key_format_versions: None,
|
||||
key_format_versions: KeyFormatVersions::new(),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -109,7 +124,7 @@ impl DerefMut for ExtXKey {
|
|||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::types::EncryptionMethod;
|
||||
use crate::types::{EncryptionMethod, KeyFormat};
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
|
@ -120,12 +135,12 @@ mod test {
|
|||
|
||||
let mut key = ExtXKey::empty();
|
||||
// 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([
|
||||
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("1/2/3");
|
||||
key.set_key_format_versions(vec![1, 2, 3]);
|
||||
|
||||
assert_eq!(key.to_string(), "#EXT-X-KEY:METHOD=NONE".to_string());
|
||||
}
|
||||
|
@ -133,7 +148,9 @@ mod test {
|
|||
#[test]
|
||||
fn test_parser() {
|
||||
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>()
|
||||
.unwrap(),
|
||||
ExtXKey::new(
|
||||
|
|
|
@ -2,13 +2,22 @@ use std::fmt;
|
|||
use std::str::FromStr;
|
||||
|
||||
use crate::attribute::AttributePairs;
|
||||
use crate::types::{ByteRange, ProtocolVersion};
|
||||
use crate::types::{ByteRange, ProtocolVersion, RequiredVersion};
|
||||
use crate::utils::{quote, tag, unquote};
|
||||
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)]
|
||||
pub struct ExtXMap {
|
||||
uri: String,
|
||||
|
@ -43,9 +52,10 @@ impl ExtXMap {
|
|||
pub const fn range(&self) -> Option<ByteRange> {
|
||||
self.range
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub const fn requires_version(&self) -> ProtocolVersion {
|
||||
impl RequiredVersion for ExtXMap {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V6
|
||||
}
|
||||
}
|
||||
|
@ -93,19 +103,37 @@ mod test {
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ext_x_map() {
|
||||
let tag = ExtXMap::new("foo");
|
||||
let text = r#"#EXT-X-MAP:URI="foo""#;
|
||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V6);
|
||||
fn test_display() {
|
||||
assert_eq!(
|
||||
ExtXMap::new("foo").to_string(),
|
||||
"#EXT-X-MAP:URI=\"foo\"".to_string(),
|
||||
);
|
||||
|
||||
let tag = ExtXMap::with_range("foo", ByteRange::new(9, Some(2)));
|
||||
let text = r#"#EXT-X-MAP:URI="foo",BYTERANGE="9@2""#;
|
||||
ExtXMap::from_str(text).unwrap();
|
||||
assert_eq!(
|
||||
ExtXMap::with_range("foo", ByteRange::new(9, Some(2))).to_string(),
|
||||
"#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\"".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V6);
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(
|
||||
ExtXMap::new("foo"),
|
||||
"#EXT-X-MAP:URI=\"foo\"".parse().unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ExtXMap::with_range("foo", ByteRange::new(9, Some(2))),
|
||||
"#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\"".parse().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(ExtXMap::new("foo").required_version(), ProtocolVersion::V6);
|
||||
assert_eq!(
|
||||
ExtXMap::with_range("foo", ByteRange::new(9, Some(2))).required_version(),
|
||||
ProtocolVersion::V6
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
use std::fmt;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::str::FromStr;
|
||||
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::utils::tag;
|
||||
use crate::Error;
|
||||
|
||||
/// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]
|
||||
/// # [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]
|
||||
/// 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
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct ExtXProgramDateTime(DateTime<FixedOffset>);
|
||||
|
@ -17,17 +21,38 @@ impl ExtXProgramDateTime {
|
|||
pub(crate) const PREFIX: &'static str = "#EXT-X-PROGRAM-DATE-TIME:";
|
||||
|
||||
/// 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.
|
||||
pub const fn date_time(&self) -> &DateTime<FixedOffset> {
|
||||
&self.0
|
||||
pub const fn date_time(&self) -> DateTime<FixedOffset> {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub const fn requires_version(&self) -> ProtocolVersion {
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
impl RequiredVersion for ExtXProgramDateTime {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
|
@ -45,45 +70,69 @@ impl FromStr for ExtXProgramDateTime {
|
|||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
let input = tag(input, Self::PREFIX)?;
|
||||
|
||||
// TODO: parse with chrono
|
||||
let date_time = DateTime::parse_from_rfc3339(input)?;
|
||||
Ok(Self::new(date_time))
|
||||
}
|
||||
}
|
||||
|
||||
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)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use chrono::TimeZone;
|
||||
|
||||
const HOURS_IN_SECS: i32 = 3600; // 1 hour = 3600 seconds
|
||||
|
||||
#[test]
|
||||
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!(
|
||||
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()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(
|
||||
ExtXProgramDateTime::new(
|
||||
FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
.ymd(2010, 2, 19)
|
||||
.and_hms_milli(14, 54, 23, 31)
|
||||
),
|
||||
"#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00"
|
||||
.parse::<ExtXProgramDateTime>()
|
||||
.unwrap();
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_requires_version() {
|
||||
let date_time = "2010-02-19T14:54:23.031+08:00"
|
||||
.parse::<DateTime<FixedOffset>>()
|
||||
.unwrap();
|
||||
|
||||
let program_date_time = ExtXProgramDateTime::new(date_time);
|
||||
|
||||
assert_eq!(program_date_time.requires_version(), ProtocolVersion::V1);
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
ExtXProgramDateTime::new(
|
||||
FixedOffset::east(8 * HOURS_IN_SECS)
|
||||
.ymd(2010, 2, 19)
|
||||
.and_hms_milli(14, 54, 23, 31),
|
||||
)
|
||||
.required_version(),
|
||||
ProtocolVersion::V1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,16 +2,6 @@
|
|||
//!
|
||||
//! [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 master_playlist;
|
||||
mod media_playlist;
|
||||
|
@ -23,79 +13,3 @@ pub use master_playlist::*;
|
|||
pub use media_playlist::*;
|
||||
pub use media_segment::*;
|
||||
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);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::types::ProtocolVersion;
|
||||
use crate::types::{ProtocolVersion, RequiredVersion};
|
||||
use crate::utils::tag;
|
||||
use crate::Error;
|
||||
|
||||
|
@ -10,11 +10,13 @@ use crate::Error;
|
|||
/// [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)]
|
||||
pub struct ExtXIndependentSegments;
|
||||
|
||||
impl ExtXIndependentSegments {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-INDEPENDENT-SEGMENTS";
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub const fn requires_version(&self) -> ProtocolVersion {
|
||||
impl RequiredVersion for ExtXIndependentSegments {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
|
@ -39,11 +41,26 @@ mod test {
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ext_x_independent_segments() {
|
||||
let tag = ExtXIndependentSegments;
|
||||
let text = "#EXT-X-INDEPENDENT-SEGMENTS";
|
||||
assert_eq!(text.parse().ok(), Some(tag));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
fn test_display() {
|
||||
assert_eq!(
|
||||
ExtXIndependentSegments.to_string(),
|
||||
"#EXT-X-INDEPENDENT-SEGMENTS".to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(
|
||||
ExtXIndependentSegments,
|
||||
"#EXT-X-INDEPENDENT-SEGMENTS".parse().unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
ExtXIndependentSegments.required_version(),
|
||||
ProtocolVersion::V1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ use std::fmt;
|
|||
use std::str::FromStr;
|
||||
|
||||
use crate::attribute::AttributePairs;
|
||||
use crate::types::{ProtocolVersion, SignedDecimalFloatingPoint};
|
||||
use crate::types::{ProtocolVersion, RequiredVersion, SignedDecimalFloatingPoint};
|
||||
use crate::utils::{parse_yes_or_no, tag};
|
||||
use crate::Error;
|
||||
|
||||
|
@ -19,6 +19,7 @@ impl ExtXStart {
|
|||
pub(crate) const PREFIX: &'static str = "#EXT-X-START:";
|
||||
|
||||
/// Makes a new `ExtXStart` tag.
|
||||
///
|
||||
/// # Panic
|
||||
/// Panics if the time_offset value is infinite.
|
||||
pub fn new(time_offset: f64) -> Self {
|
||||
|
@ -33,6 +34,7 @@ impl ExtXStart {
|
|||
}
|
||||
|
||||
/// Makes a new `ExtXStart` tag with the given `precise` flag.
|
||||
///
|
||||
/// # Panic
|
||||
/// Panics if the time_offset value is infinite.
|
||||
pub fn with_precise(time_offset: f64, precise: bool) -> Self {
|
||||
|
@ -56,9 +58,10 @@ impl ExtXStart {
|
|||
pub const fn precise(&self) -> bool {
|
||||
self.precise
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
pub const fn requires_version(&self) -> ProtocolVersion {
|
||||
impl RequiredVersion for ExtXStart {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
ProtocolVersion::V1
|
||||
}
|
||||
}
|
||||
|
@ -108,17 +111,41 @@ mod test {
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ext_x_start() {
|
||||
let tag = ExtXStart::new(-1.23);
|
||||
let text = "#EXT-X-START:TIME-OFFSET=-1.23";
|
||||
assert_eq!(text.parse().ok(), Some(tag));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
fn test_display() {
|
||||
assert_eq!(
|
||||
ExtXStart::new(-1.23).to_string(),
|
||||
"#EXT-X-START:TIME-OFFSET=-1.23".to_string(),
|
||||
);
|
||||
|
||||
let tag = ExtXStart::with_precise(1.23, true);
|
||||
let text = "#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES";
|
||||
assert_eq!(text.parse().ok(), Some(tag));
|
||||
assert_eq!(tag.to_string(), text);
|
||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||
assert_eq!(
|
||||
ExtXStart::with_precise(1.23, true).to_string(),
|
||||
"#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
ExtXStart::new(-1.23).required_version(),
|
||||
ProtocolVersion::V1,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ExtXStart::with_precise(1.23, true).required_version(),
|
||||
ProtocolVersion::V1,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(
|
||||
ExtXStart::new(-1.23),
|
||||
"#EXT-X-START:TIME-OFFSET=-1.23".parse().unwrap(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
ExtXStart::with_precise(1.23, true),
|
||||
"#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES".parse().unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,6 +70,18 @@ impl FromStr for DecimalFloatingPoint {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<f64> for DecimalFloatingPoint {
|
||||
fn from(value: f64) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f32> for DecimalFloatingPoint {
|
||||
fn from(value: f32) -> Self {
|
||||
Self(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -4,39 +4,49 @@ use std::str::FromStr;
|
|||
use derive_builder::Builder;
|
||||
|
||||
use crate::attribute::AttributePairs;
|
||||
use crate::types::{EncryptionMethod, InitializationVector, ProtocolVersion};
|
||||
use crate::types::{
|
||||
EncryptionMethod, InitializationVector, KeyFormat, KeyFormatVersions, ProtocolVersion,
|
||||
RequiredVersion,
|
||||
};
|
||||
use crate::utils::{quote, unquote};
|
||||
use crate::Error;
|
||||
|
||||
#[derive(Builder, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[builder(setter(into))]
|
||||
/// [DecryptionKey] contains data, that is shared between [ExtXSessionKey] and [ExtXKey].
|
||||
///
|
||||
/// [ExtXSessionKey]: crate::tags::ExtXSessionKey
|
||||
/// [ExtXKey]: crate::tags::ExtXKey
|
||||
pub struct DecryptionKey {
|
||||
/// The [EncryptionMethod].
|
||||
pub(crate) method: EncryptionMethod,
|
||||
#[builder(setter(into, strip_option), default)]
|
||||
/// An `URI`, that specifies how to obtain the key.
|
||||
pub(crate) uri: Option<String>,
|
||||
#[builder(setter(into, strip_option), default)]
|
||||
/// The IV (Initialization Vector) attribute.
|
||||
pub(crate) iv: Option<InitializationVector>,
|
||||
#[builder(setter(into, strip_option), default)]
|
||||
pub(crate) key_format: Option<String>,
|
||||
#[builder(setter(into, strip_option), default)]
|
||||
pub(crate) key_format_versions: Option<String>,
|
||||
/// 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,
|
||||
}
|
||||
|
||||
impl DecryptionKey {
|
||||
/// Makes a new `DecryptionKey`.
|
||||
/// Makes a new [DecryptionKey].
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use hls_m3u8::types::{EncryptionMethod, DecryptionKey};
|
||||
/// # use hls_m3u8::types::DecryptionKey;
|
||||
/// use hls_m3u8::types::EncryptionMethod;
|
||||
///
|
||||
/// let key = DecryptionKey::new(
|
||||
/// EncryptionMethod::Aes128,
|
||||
/// "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 {
|
||||
Self {
|
||||
|
@ -44,14 +54,16 @@ impl DecryptionKey {
|
|||
uri: Some(uri.to_string()),
|
||||
iv: None,
|
||||
key_format: None,
|
||||
key_format_versions: None,
|
||||
key_format_versions: KeyFormatVersions::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the [EncryptionMethod].
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod};
|
||||
/// # use hls_m3u8::types::DecryptionKey;
|
||||
/// use hls_m3u8::types::EncryptionMethod;
|
||||
///
|
||||
/// let key = DecryptionKey::new(
|
||||
/// EncryptionMethod::Aes128,
|
||||
|
@ -67,15 +79,17 @@ impl DecryptionKey {
|
|||
self.method
|
||||
}
|
||||
|
||||
/// Returns a Builder to build a `DecryptionKey`.
|
||||
/// Returns a Builder to build a [DecryptionKey].
|
||||
pub fn builder() -> DecryptionKeyBuilder {
|
||||
DecryptionKeyBuilder::default()
|
||||
}
|
||||
|
||||
/// Sets the [EncryptionMethod].
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod};
|
||||
/// # use hls_m3u8::types::DecryptionKey;
|
||||
/// use hls_m3u8::types::EncryptionMethod;
|
||||
///
|
||||
/// let mut key = DecryptionKey::new(
|
||||
/// EncryptionMethod::Aes128,
|
||||
|
@ -93,12 +107,14 @@ impl DecryptionKey {
|
|||
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.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod};
|
||||
/// # use hls_m3u8::types::DecryptionKey;
|
||||
/// use hls_m3u8::types::EncryptionMethod;
|
||||
///
|
||||
/// let key = DecryptionKey::new(
|
||||
/// EncryptionMethod::Aes128,
|
||||
|
@ -121,7 +137,8 @@ impl DecryptionKey {
|
|||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod};
|
||||
/// # use hls_m3u8::types::DecryptionKey;
|
||||
/// use hls_m3u8::types::EncryptionMethod;
|
||||
///
|
||||
/// let mut key = DecryptionKey::new(
|
||||
/// EncryptionMethod::Aes128,
|
||||
|
@ -142,9 +159,11 @@ impl DecryptionKey {
|
|||
/// Returns the IV (Initialization Vector) attribute.
|
||||
///
|
||||
/// This attribute is optional.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod};
|
||||
/// # use hls_m3u8::types::DecryptionKey;
|
||||
/// use hls_m3u8::types::EncryptionMethod;
|
||||
///
|
||||
/// let mut key = DecryptionKey::new(
|
||||
/// EncryptionMethod::Aes128,
|
||||
|
@ -171,9 +190,11 @@ impl DecryptionKey {
|
|||
/// Sets the `IV` attribute.
|
||||
///
|
||||
/// This attribute is optional.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod};
|
||||
/// # use hls_m3u8::types::DecryptionKey;
|
||||
/// use hls_m3u8::types::EncryptionMethod;
|
||||
///
|
||||
/// let mut key = DecryptionKey::new(
|
||||
/// EncryptionMethod::Aes128,
|
||||
|
@ -197,119 +218,110 @@ impl DecryptionKey {
|
|||
}
|
||||
|
||||
/// 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
|
||||
/// ```
|
||||
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod};
|
||||
/// # use hls_m3u8::types::DecryptionKey;
|
||||
/// use hls_m3u8::types::{KeyFormat, EncryptionMethod};
|
||||
///
|
||||
/// let mut key = DecryptionKey::new(
|
||||
/// EncryptionMethod::Aes128,
|
||||
/// "https://www.example.com/"
|
||||
/// );
|
||||
///
|
||||
/// key.set_key_format("key_format_attribute");
|
||||
/// key.set_key_format(Some(KeyFormat::Identity));
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// key.key_format(),
|
||||
/// &Some("key_format_attribute".to_string())
|
||||
/// Some(KeyFormat::Identity)
|
||||
/// );
|
||||
/// ```
|
||||
pub const fn key_format(&self) -> &Option<String> {
|
||||
&self.key_format
|
||||
pub const fn key_format(&self) -> Option<KeyFormat> {
|
||||
self.key_format
|
||||
}
|
||||
|
||||
/// Sets the `KEYFORMAT` attribute.
|
||||
///
|
||||
/// This attribute is optional.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod};
|
||||
/// # use hls_m3u8::types::DecryptionKey;
|
||||
/// use hls_m3u8::types::{KeyFormat, EncryptionMethod};
|
||||
///
|
||||
/// let mut key = DecryptionKey::new(
|
||||
/// EncryptionMethod::Aes128,
|
||||
/// "https://www.example.com/"
|
||||
/// );
|
||||
///
|
||||
/// key.set_key_format("key_format_attribute");
|
||||
/// key.set_key_format(Some(KeyFormat::Identity));
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// key.to_string(),
|
||||
/// "METHOD=AES-128,URI=\"https://www.example.com/\",KEYFORMAT=\"key_format_attribute\"".to_string()
|
||||
/// key.key_format(),
|
||||
/// Some(KeyFormat::Identity)
|
||||
/// );
|
||||
/// ```
|
||||
pub fn set_key_format<T: ToString>(&mut self, value: T) {
|
||||
self.key_format = Some(value.to_string());
|
||||
pub fn set_key_format<T: Into<KeyFormat>>(&mut self, value: Option<T>) {
|
||||
self.key_format = value.map(|v| v.into());
|
||||
}
|
||||
|
||||
/// Returns a string containing one or more positive
|
||||
/// integers separated by the "/" character (for example, "1", "1/2",
|
||||
/// or "1/2/5"). If more than one version of a particular `KEYFORMAT`
|
||||
/// is defined, this attribute can be used to indicate which
|
||||
/// version(s) this instance complies with.
|
||||
/// Returns the [KeyFormatVersions] attribute.
|
||||
///
|
||||
/// This attribute is optional.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod};
|
||||
/// # use hls_m3u8::types::DecryptionKey;
|
||||
/// use hls_m3u8::types::{KeyFormatVersions, EncryptionMethod};
|
||||
///
|
||||
/// let mut key = DecryptionKey::new(
|
||||
/// EncryptionMethod::Aes128,
|
||||
/// "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!(
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Sets the `KEYFORMATVERSIONS` attribute.
|
||||
/// Sets the [KeyFormatVersions] attribute.
|
||||
///
|
||||
/// This attribute is optional.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use hls_m3u8::types::{DecryptionKey, EncryptionMethod};
|
||||
/// # use hls_m3u8::types::DecryptionKey;
|
||||
/// use hls_m3u8::types::EncryptionMethod;
|
||||
///
|
||||
/// let mut key = DecryptionKey::new(
|
||||
/// EncryptionMethod::Aes128,
|
||||
/// "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!(
|
||||
/// key.to_string(),
|
||||
/// "METHOD=AES-128,URI=\"https://www.example.com/\",KEYFORMATVERSIONS=\"1/2/3/4/5\"".to_string()
|
||||
/// );
|
||||
/// ```
|
||||
pub fn set_key_format_versions<T: ToString>(&mut self, value: T) {
|
||||
self.key_format_versions = Some(value.to_string());
|
||||
pub fn set_key_format_versions<T: Into<KeyFormatVersions>>(&mut self, value: T) {
|
||||
self.key_format_versions = value.into();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
/// # Example
|
||||
/// ```
|
||||
/// use hls_m3u8::types::{EncryptionMethod, ProtocolVersion, DecryptionKey};
|
||||
///
|
||||
/// let mut key = DecryptionKey::new(
|
||||
/// EncryptionMethod::Aes128,
|
||||
/// "https://www.example.com/"
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// key.requires_version(),
|
||||
/// ProtocolVersion::V1
|
||||
/// );
|
||||
/// ```
|
||||
pub fn requires_version(&self) -> ProtocolVersion {
|
||||
if self.key_format.is_some() || self.key_format_versions.is_some() {
|
||||
impl RequiredVersion for DecryptionKey {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
if self.key_format.is_some() || !self.key_format_versions.is_default() {
|
||||
ProtocolVersion::V5
|
||||
} else if self.iv.is_some() {
|
||||
ProtocolVersion::V2
|
||||
|
@ -331,11 +343,11 @@ impl FromStr for DecryptionKey {
|
|||
|
||||
for (key, value) in input.parse::<AttributePairs>()? {
|
||||
match key.as_str() {
|
||||
"METHOD" => method = Some((value.parse())?),
|
||||
"METHOD" => method = Some(value.parse()?),
|
||||
"URI" => uri = Some(unquote(value)),
|
||||
"IV" => iv = Some((value.parse())?),
|
||||
"KEYFORMAT" => key_format = Some(unquote(value)),
|
||||
"KEYFORMATVERSIONS" => key_format_versions = Some(unquote(value)),
|
||||
"IV" => iv = Some(value.parse()?),
|
||||
"KEYFORMAT" => key_format = Some(value.parse()?),
|
||||
"KEYFORMATVERSIONS" => key_format_versions = Some(value.parse()?),
|
||||
_ => {
|
||||
// [6.3.1. General Client Responsibilities]
|
||||
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
||||
|
@ -353,7 +365,7 @@ impl FromStr for DecryptionKey {
|
|||
uri,
|
||||
iv,
|
||||
key_format,
|
||||
key_format_versions,
|
||||
key_format_versions: key_format_versions.unwrap_or(KeyFormatVersions::new()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -374,8 +386,8 @@ impl fmt::Display for DecryptionKey {
|
|||
if let Some(value) = &self.key_format {
|
||||
write!(f, ",KEYFORMAT={}", quote(value))?;
|
||||
}
|
||||
if let Some(value) = &self.key_format_versions {
|
||||
write!(f, ",KEYFORMATVERSIONS={}", quote(value))?;
|
||||
if !self.key_format_versions.is_default() {
|
||||
write!(f, ",KEYFORMATVERSIONS={}", &self.key_format_versions)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -394,13 +406,19 @@ mod test {
|
|||
.iv([
|
||||
16, 239, 143, 117, 140, 165, 85, 17, 85, 132, 187, 91, 60, 104, 127, 82,
|
||||
])
|
||||
.key_format("ABC123")
|
||||
.key_format_versions("1,2,3,4,5/12345")
|
||||
.key_format(KeyFormat::Identity)
|
||||
.key_format_versions(vec![1, 2, 3, 4, 5])
|
||||
.build()
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
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()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -426,7 +444,8 @@ mod test {
|
|||
#[test]
|
||||
fn test_parser() {
|
||||
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>()
|
||||
.unwrap(),
|
||||
DecryptionKey::new(
|
||||
|
@ -456,12 +475,25 @@ mod test {
|
|||
key.set_iv([
|
||||
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!(
|
||||
r#"METHOD=AES-128,URI="http://www.example.com",IV=0x10ef8f758ca555115584bb5b3c687f52,KEYFORMAT="baz""#
|
||||
.parse::<DecryptionKey>().unwrap(),
|
||||
"METHOD=AES-128,\
|
||||
URI=\"http://www.example.com\",\
|
||||
IV=0x10ef8f758ca555115584bb5b3c687f52,\
|
||||
KEYFORMAT=\"identity\""
|
||||
.parse::<DecryptionKey>()
|
||||
.unwrap(),
|
||||
key
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_required_version() {
|
||||
assert_eq!(
|
||||
DecryptionKey::new(EncryptionMethod::Aes128, "https://www.example.com/")
|
||||
.required_version(),
|
||||
ProtocolVersion::V1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
68
src/types/key_format.rs
Normal file
68
src/types/key_format.rs
Normal 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());
|
||||
}
|
||||
}
|
170
src/types/key_format_versions.rs
Normal file
170
src/types/key_format_versions.rs
Normal 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]));
|
||||
}
|
||||
}
|
|
@ -8,6 +8,8 @@ mod encryption_method;
|
|||
mod hdcp_level;
|
||||
mod in_stream_id;
|
||||
mod initialization_vector;
|
||||
mod key_format;
|
||||
mod key_format_versions;
|
||||
mod media_type;
|
||||
mod protocol_version;
|
||||
mod signed_decimal_floating_point;
|
||||
|
@ -22,6 +24,8 @@ pub use encryption_method::*;
|
|||
pub use hdcp_level::*;
|
||||
pub use in_stream_id::*;
|
||||
pub use initialization_vector::*;
|
||||
pub use key_format::*;
|
||||
pub use key_format_versions::*;
|
||||
pub use media_type::*;
|
||||
pub use protocol_version::*;
|
||||
pub(crate) use signed_decimal_floating_point::*;
|
||||
|
|
|
@ -3,6 +3,30 @@ use std::str::FromStr;
|
|||
|
||||
use crate::Error;
|
||||
|
||||
/// # Example
|
||||
/// Implementing it:
|
||||
/// ```
|
||||
/// # use hls_m3u8::types::{ProtocolVersion, RequiredVersion};
|
||||
/// #
|
||||
/// struct NewTag(u64);
|
||||
///
|
||||
/// impl RequiredVersion for NewTag {
|
||||
/// fn required_version(&self) -> ProtocolVersion {
|
||||
/// if self.0 == 5 {
|
||||
/// ProtocolVersion::V4
|
||||
/// } else {
|
||||
/// ProtocolVersion::V1
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// assert_eq!(NewTag(5).required_version(), ProtocolVersion::V4);
|
||||
/// assert_eq!(NewTag(2).required_version(), ProtocolVersion::V1);
|
||||
/// ```
|
||||
pub trait RequiredVersion {
|
||||
/// Returns the protocol compatibility version that this tag requires.
|
||||
fn required_version(&self) -> ProtocolVersion;
|
||||
}
|
||||
|
||||
/// [7. Protocol Version Compatibility]
|
||||
///
|
||||
/// [7. Protocol Version Compatibility]: https://tools.ietf.org/html/rfc8216#section-7
|
||||
|
@ -29,13 +53,13 @@ impl fmt::Display for ProtocolVersion {
|
|||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let n = {
|
||||
match &self {
|
||||
ProtocolVersion::V1 => 1,
|
||||
ProtocolVersion::V2 => 2,
|
||||
ProtocolVersion::V3 => 3,
|
||||
ProtocolVersion::V4 => 4,
|
||||
ProtocolVersion::V5 => 5,
|
||||
ProtocolVersion::V6 => 6,
|
||||
ProtocolVersion::V7 => 7,
|
||||
Self::V1 => 1,
|
||||
Self::V2 => 2,
|
||||
Self::V3 => 3,
|
||||
Self::V4 => 4,
|
||||
Self::V5 => 5,
|
||||
Self::V6 => 6,
|
||||
Self::V7 => 7,
|
||||
}
|
||||
};
|
||||
write!(f, "{}", n)
|
||||
|
@ -47,16 +71,49 @@ impl FromStr for ProtocolVersion {
|
|||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
Ok({
|
||||
match input {
|
||||
"1" => ProtocolVersion::V1,
|
||||
"2" => ProtocolVersion::V2,
|
||||
"3" => ProtocolVersion::V3,
|
||||
"4" => ProtocolVersion::V4,
|
||||
"5" => ProtocolVersion::V5,
|
||||
"6" => ProtocolVersion::V6,
|
||||
"7" => ProtocolVersion::V7,
|
||||
match input.trim() {
|
||||
"1" => Self::V1,
|
||||
"2" => Self::V2,
|
||||
"3" => Self::V3,
|
||||
"4" => Self::V4,
|
||||
"5" => Self::V5,
|
||||
"6" => Self::V6,
|
||||
"7" => Self::V7,
|
||||
_ => return Err(Error::unknown_protocol_version(input)),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ProtocolVersion {
|
||||
fn default() -> Self {
|
||||
Self::V1
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
assert_eq!(ProtocolVersion::V1.to_string(), "1".to_string());
|
||||
assert_eq!(ProtocolVersion::V2.to_string(), "2".to_string());
|
||||
assert_eq!(ProtocolVersion::V3.to_string(), "3".to_string());
|
||||
assert_eq!(ProtocolVersion::V4.to_string(), "4".to_string());
|
||||
assert_eq!(ProtocolVersion::V5.to_string(), "5".to_string());
|
||||
assert_eq!(ProtocolVersion::V6.to_string(), "6".to_string());
|
||||
assert_eq!(ProtocolVersion::V7.to_string(), "7".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
assert_eq!(ProtocolVersion::V1, "1".parse().unwrap());
|
||||
assert_eq!(ProtocolVersion::V2, "2".parse().unwrap());
|
||||
assert_eq!(ProtocolVersion::V3, "3".parse().unwrap());
|
||||
assert_eq!(ProtocolVersion::V4, "4".parse().unwrap());
|
||||
assert_eq!(ProtocolVersion::V5, "5".parse().unwrap());
|
||||
assert_eq!(ProtocolVersion::V6, "6".parse().unwrap());
|
||||
assert_eq!(ProtocolVersion::V7, "7".parse().unwrap());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ pub(crate) fn tag<T>(input: &str, tag: T) -> crate::Result<&str>
|
|||
where
|
||||
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));
|
||||
}
|
||||
let result = input.split_at(tag.as_ref().len()).1;
|
||||
|
|
Loading…
Reference in a new issue