mirror of
https://github.com/sile/hls_m3u8.git
synced 2024-11-27 01:11:00 +00:00
Use Cow<'a, str>
to reduce clones #52
This commit is contained in:
parent
2d6a49662d
commit
c4643c7083
40 changed files with 1045 additions and 578 deletions
|
@ -30,6 +30,7 @@ thiserror = "1.0"
|
||||||
derive_more = "0.99"
|
derive_more = "0.99"
|
||||||
shorthand = "0.1"
|
shorthand = "0.1"
|
||||||
strum = { version = "0.17", features = ["derive"] }
|
strum = { version = "0.17", features = ["derive"] }
|
||||||
|
|
||||||
stable-vec = { version = "0.4" }
|
stable-vec = { version = "0.4" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
@ -58,7 +59,7 @@ fn create_manifest_data() -> Vec<u8> {
|
||||||
builder.build().unwrap().to_string().into_bytes()
|
builder.build().unwrap().to_string().into_bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn criterion_benchmark(c: &mut Criterion) {
|
fn media_playlist_from_str(c: &mut Criterion) {
|
||||||
let data = String::from_utf8(create_manifest_data()).unwrap();
|
let data = String::from_utf8(create_manifest_data()).unwrap();
|
||||||
|
|
||||||
let mut group = c.benchmark_group("MediaPlaylist::from_str");
|
let mut group = c.benchmark_group("MediaPlaylist::from_str");
|
||||||
|
@ -72,4 +73,18 @@ fn criterion_benchmark(c: &mut Criterion) {
|
||||||
group.finish();
|
group.finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
criterion_group!(benches, criterion_benchmark);
|
fn media_playlist_try_from(c: &mut Criterion) {
|
||||||
|
let data = String::from_utf8(create_manifest_data()).unwrap();
|
||||||
|
|
||||||
|
let mut group = c.benchmark_group("MediaPlaylist::try_from");
|
||||||
|
|
||||||
|
group.throughput(Throughput::Bytes(data.len() as u64));
|
||||||
|
|
||||||
|
group.bench_function("MediaPlaylist::try_from", |b| {
|
||||||
|
b.iter(|| MediaPlaylist::try_from(black_box(data.as_str())).unwrap());
|
||||||
|
});
|
||||||
|
|
||||||
|
group.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(benches, media_playlist_from_str, media_playlist_try_from);
|
||||||
|
|
|
@ -30,7 +30,7 @@ impl<'a> Iterator for AttributePairs<'a> {
|
||||||
// NOTE: it is okay to add 1 to the index, because an `=` is exactly 1 byte.
|
// NOTE: it is okay to add 1 to the index, because an `=` is exactly 1 byte.
|
||||||
self.index = end + 1;
|
self.index = end + 1;
|
||||||
|
|
||||||
::core::str::from_utf8(&self.string.as_bytes()[start..end]).unwrap()
|
&self.string[start..end]
|
||||||
};
|
};
|
||||||
|
|
||||||
let value = {
|
let value = {
|
||||||
|
@ -66,7 +66,7 @@ impl<'a> Iterator for AttributePairs<'a> {
|
||||||
self.index += end;
|
self.index += end;
|
||||||
self.index -= start;
|
self.index -= start;
|
||||||
|
|
||||||
::core::str::from_utf8(&self.string.as_bytes()[start..end]).unwrap()
|
&self.string[start..end]
|
||||||
};
|
};
|
||||||
|
|
||||||
Some((key, value))
|
Some((key, value))
|
||||||
|
|
25
src/lib.rs
25
src/lib.rs
|
@ -42,19 +42,22 @@
|
||||||
//!
|
//!
|
||||||
//! ```
|
//! ```
|
||||||
//! use hls_m3u8::MediaPlaylist;
|
//! use hls_m3u8::MediaPlaylist;
|
||||||
|
//! use std::convert::TryFrom;
|
||||||
//!
|
//!
|
||||||
//! let m3u8 = "#EXTM3U
|
//! let m3u8 = MediaPlaylist::try_from(concat!(
|
||||||
//! #EXT-X-TARGETDURATION:10
|
//! "#EXTM3U\n",
|
||||||
//! #EXT-X-VERSION:3
|
//! "#EXT-X-TARGETDURATION:10\n",
|
||||||
//! #EXTINF:9.009,
|
//! "#EXT-X-VERSION:3\n",
|
||||||
//! http://media.example.com/first.ts
|
//! "#EXTINF:9.009,\n",
|
||||||
//! #EXTINF:9.009,
|
//! "http://media.example.com/first.ts\n",
|
||||||
//! http://media.example.com/second.ts
|
//! "#EXTINF:9.009,\n",
|
||||||
//! #EXTINF:3.003,
|
//! "http://media.example.com/second.ts\n",
|
||||||
//! http://media.example.com/third.ts
|
//! "#EXTINF:3.003,\n",
|
||||||
//! #EXT-X-ENDLIST";
|
//! "http://media.example.com/third.ts\n",
|
||||||
|
//! "#EXT-X-ENDLIST",
|
||||||
|
//! ));
|
||||||
//!
|
//!
|
||||||
//! assert!(m3u8.parse::<MediaPlaylist>().is_ok());
|
//! assert!(m3u8.is_ok());
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! ## Crate Feature Flags
|
//! ## Crate Feature Flags
|
||||||
|
|
62
src/line.rs
62
src/line.rs
|
@ -1,6 +1,5 @@
|
||||||
use core::convert::TryFrom;
|
use core::convert::TryFrom;
|
||||||
use core::iter::FusedIterator;
|
use core::iter::FusedIterator;
|
||||||
use core::str::FromStr;
|
|
||||||
|
|
||||||
use derive_more::Display;
|
use derive_more::Display;
|
||||||
|
|
||||||
|
@ -23,7 +22,8 @@ impl<'a> Iterator for Lines<'a> {
|
||||||
let uri = self.lines.next()?;
|
let uri = self.lines.next()?;
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
tags::VariantStream::from_str(&format!("{}\n{}", line, uri))
|
tags::VariantStream::try_from(format!("{}\n{}", line, uri).as_str())
|
||||||
|
.map(|v| v.into_owned())
|
||||||
.map(|v| Line::Tag(Tag::VariantStream(v))),
|
.map(|v| Line::Tag(Tag::VariantStream(v))),
|
||||||
)
|
)
|
||||||
} else if line.starts_with("#EXT") {
|
} else if line.starts_with("#EXT") {
|
||||||
|
@ -60,25 +60,25 @@ pub(crate) enum Line<'a> {
|
||||||
#[display(fmt = "{}")]
|
#[display(fmt = "{}")]
|
||||||
pub(crate) enum Tag<'a> {
|
pub(crate) enum Tag<'a> {
|
||||||
ExtXVersion(tags::ExtXVersion),
|
ExtXVersion(tags::ExtXVersion),
|
||||||
ExtInf(tags::ExtInf),
|
ExtInf(tags::ExtInf<'a>),
|
||||||
ExtXByteRange(tags::ExtXByteRange),
|
ExtXByteRange(tags::ExtXByteRange),
|
||||||
ExtXDiscontinuity(tags::ExtXDiscontinuity),
|
ExtXDiscontinuity(tags::ExtXDiscontinuity),
|
||||||
ExtXKey(tags::ExtXKey),
|
ExtXKey(tags::ExtXKey<'a>),
|
||||||
ExtXMap(tags::ExtXMap),
|
ExtXMap(tags::ExtXMap<'a>),
|
||||||
ExtXProgramDateTime(tags::ExtXProgramDateTime),
|
ExtXProgramDateTime(tags::ExtXProgramDateTime<'a>),
|
||||||
ExtXDateRange(tags::ExtXDateRange),
|
ExtXDateRange(tags::ExtXDateRange<'a>),
|
||||||
ExtXTargetDuration(tags::ExtXTargetDuration),
|
ExtXTargetDuration(tags::ExtXTargetDuration),
|
||||||
ExtXMediaSequence(tags::ExtXMediaSequence),
|
ExtXMediaSequence(tags::ExtXMediaSequence),
|
||||||
ExtXDiscontinuitySequence(tags::ExtXDiscontinuitySequence),
|
ExtXDiscontinuitySequence(tags::ExtXDiscontinuitySequence),
|
||||||
ExtXEndList(tags::ExtXEndList),
|
ExtXEndList(tags::ExtXEndList),
|
||||||
PlaylistType(PlaylistType),
|
PlaylistType(PlaylistType),
|
||||||
ExtXIFramesOnly(tags::ExtXIFramesOnly),
|
ExtXIFramesOnly(tags::ExtXIFramesOnly),
|
||||||
ExtXMedia(tags::ExtXMedia),
|
ExtXMedia(tags::ExtXMedia<'a>),
|
||||||
ExtXSessionData(tags::ExtXSessionData),
|
ExtXSessionData(tags::ExtXSessionData<'a>),
|
||||||
ExtXSessionKey(tags::ExtXSessionKey),
|
ExtXSessionKey(tags::ExtXSessionKey<'a>),
|
||||||
ExtXIndependentSegments(tags::ExtXIndependentSegments),
|
ExtXIndependentSegments(tags::ExtXIndependentSegments),
|
||||||
ExtXStart(tags::ExtXStart),
|
ExtXStart(tags::ExtXStart),
|
||||||
VariantStream(tags::VariantStream),
|
VariantStream(tags::VariantStream<'a>),
|
||||||
Unknown(&'a str),
|
Unknown(&'a str),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,47 +87,47 @@ impl<'a> TryFrom<&'a str> for Tag<'a> {
|
||||||
|
|
||||||
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||||
if input.starts_with(tags::ExtXVersion::PREFIX) {
|
if input.starts_with(tags::ExtXVersion::PREFIX) {
|
||||||
input.parse().map(Self::ExtXVersion)
|
TryFrom::try_from(input).map(Self::ExtXVersion)
|
||||||
} else if input.starts_with(tags::ExtInf::PREFIX) {
|
} else if input.starts_with(tags::ExtInf::PREFIX) {
|
||||||
input.parse().map(Self::ExtInf)
|
TryFrom::try_from(input).map(Self::ExtInf)
|
||||||
} else if input.starts_with(tags::ExtXByteRange::PREFIX) {
|
} else if input.starts_with(tags::ExtXByteRange::PREFIX) {
|
||||||
input.parse().map(Self::ExtXByteRange)
|
TryFrom::try_from(input).map(Self::ExtXByteRange)
|
||||||
} else if input.starts_with(tags::ExtXDiscontinuity::PREFIX) {
|
} else if input.starts_with(tags::ExtXDiscontinuity::PREFIX) {
|
||||||
input.parse().map(Self::ExtXDiscontinuity)
|
TryFrom::try_from(input).map(Self::ExtXDiscontinuity)
|
||||||
} else if input.starts_with(tags::ExtXKey::PREFIX) {
|
} else if input.starts_with(tags::ExtXKey::PREFIX) {
|
||||||
input.parse().map(Self::ExtXKey)
|
TryFrom::try_from(input).map(Self::ExtXKey)
|
||||||
} else if input.starts_with(tags::ExtXMap::PREFIX) {
|
} else if input.starts_with(tags::ExtXMap::PREFIX) {
|
||||||
input.parse().map(Self::ExtXMap)
|
TryFrom::try_from(input).map(Self::ExtXMap)
|
||||||
} else if input.starts_with(tags::ExtXProgramDateTime::PREFIX) {
|
} else if input.starts_with(tags::ExtXProgramDateTime::PREFIX) {
|
||||||
input.parse().map(Self::ExtXProgramDateTime)
|
TryFrom::try_from(input).map(Self::ExtXProgramDateTime)
|
||||||
} else if input.starts_with(tags::ExtXTargetDuration::PREFIX) {
|
} else if input.starts_with(tags::ExtXTargetDuration::PREFIX) {
|
||||||
input.parse().map(Self::ExtXTargetDuration)
|
TryFrom::try_from(input).map(Self::ExtXTargetDuration)
|
||||||
} else if input.starts_with(tags::ExtXDateRange::PREFIX) {
|
} else if input.starts_with(tags::ExtXDateRange::PREFIX) {
|
||||||
input.parse().map(Self::ExtXDateRange)
|
TryFrom::try_from(input).map(Self::ExtXDateRange)
|
||||||
} else if input.starts_with(tags::ExtXMediaSequence::PREFIX) {
|
} else if input.starts_with(tags::ExtXMediaSequence::PREFIX) {
|
||||||
input.parse().map(Self::ExtXMediaSequence)
|
TryFrom::try_from(input).map(Self::ExtXMediaSequence)
|
||||||
} else if input.starts_with(tags::ExtXDiscontinuitySequence::PREFIX) {
|
} else if input.starts_with(tags::ExtXDiscontinuitySequence::PREFIX) {
|
||||||
input.parse().map(Self::ExtXDiscontinuitySequence)
|
TryFrom::try_from(input).map(Self::ExtXDiscontinuitySequence)
|
||||||
} else if input.starts_with(tags::ExtXEndList::PREFIX) {
|
} else if input.starts_with(tags::ExtXEndList::PREFIX) {
|
||||||
input.parse().map(Self::ExtXEndList)
|
TryFrom::try_from(input).map(Self::ExtXEndList)
|
||||||
} else if input.starts_with(PlaylistType::PREFIX) {
|
} else if input.starts_with(PlaylistType::PREFIX) {
|
||||||
input.parse().map(Self::PlaylistType)
|
TryFrom::try_from(input).map(Self::PlaylistType)
|
||||||
} else if input.starts_with(tags::ExtXIFramesOnly::PREFIX) {
|
} else if input.starts_with(tags::ExtXIFramesOnly::PREFIX) {
|
||||||
input.parse().map(Self::ExtXIFramesOnly)
|
TryFrom::try_from(input).map(Self::ExtXIFramesOnly)
|
||||||
} else if input.starts_with(tags::ExtXMedia::PREFIX) {
|
} else if input.starts_with(tags::ExtXMedia::PREFIX) {
|
||||||
input.parse().map(Self::ExtXMedia)
|
TryFrom::try_from(input).map(Self::ExtXMedia)
|
||||||
} else if input.starts_with(tags::VariantStream::PREFIX_EXTXIFRAME)
|
} else if input.starts_with(tags::VariantStream::PREFIX_EXTXIFRAME)
|
||||||
|| input.starts_with(tags::VariantStream::PREFIX_EXTXSTREAMINF)
|
|| input.starts_with(tags::VariantStream::PREFIX_EXTXSTREAMINF)
|
||||||
{
|
{
|
||||||
input.parse().map(Self::VariantStream)
|
TryFrom::try_from(input).map(Self::VariantStream)
|
||||||
} else if input.starts_with(tags::ExtXSessionData::PREFIX) {
|
} else if input.starts_with(tags::ExtXSessionData::PREFIX) {
|
||||||
input.parse().map(Self::ExtXSessionData)
|
TryFrom::try_from(input).map(Self::ExtXSessionData)
|
||||||
} else if input.starts_with(tags::ExtXSessionKey::PREFIX) {
|
} else if input.starts_with(tags::ExtXSessionKey::PREFIX) {
|
||||||
input.parse().map(Self::ExtXSessionKey)
|
TryFrom::try_from(input).map(Self::ExtXSessionKey)
|
||||||
} else if input.starts_with(tags::ExtXIndependentSegments::PREFIX) {
|
} else if input.starts_with(tags::ExtXIndependentSegments::PREFIX) {
|
||||||
input.parse().map(Self::ExtXIndependentSegments)
|
TryFrom::try_from(input).map(Self::ExtXIndependentSegments)
|
||||||
} else if input.starts_with(tags::ExtXStart::PREFIX) {
|
} else if input.starts_with(tags::ExtXStart::PREFIX) {
|
||||||
input.parse().map(Self::ExtXStart)
|
TryFrom::try_from(input).map(Self::ExtXStart)
|
||||||
} else {
|
} else {
|
||||||
Ok(Self::Unknown(input))
|
Ok(Self::Unknown(input))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use derive_builder::Builder;
|
use derive_builder::Builder;
|
||||||
|
|
||||||
|
@ -24,11 +25,11 @@ use crate::{Error, RequiredVersion};
|
||||||
/// A [`MasterPlaylist`] can be parsed from a `str`:
|
/// A [`MasterPlaylist`] can be parsed from a `str`:
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use core::str::FromStr;
|
/// use core::convert::TryFrom;
|
||||||
/// use hls_m3u8::MasterPlaylist;
|
/// use hls_m3u8::MasterPlaylist;
|
||||||
///
|
///
|
||||||
/// // the concat! macro joins multiple `&'static str`.
|
/// // the concat! macro joins multiple `&'static str`.
|
||||||
/// let master_playlist = concat!(
|
/// let master_playlist = MasterPlaylist::try_from(concat!(
|
||||||
/// "#EXTM3U\n",
|
/// "#EXTM3U\n",
|
||||||
/// "#EXT-X-STREAM-INF:",
|
/// "#EXT-X-STREAM-INF:",
|
||||||
/// "BANDWIDTH=150000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n",
|
/// "BANDWIDTH=150000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n",
|
||||||
|
@ -44,8 +45,7 @@ use crate::{Error, RequiredVersion};
|
||||||
/// "http://example.com/high/index.m3u8\n",
|
/// "http://example.com/high/index.m3u8\n",
|
||||||
/// "#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS=\"mp4a.40.5\"\n",
|
/// "#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS=\"mp4a.40.5\"\n",
|
||||||
/// "http://example.com/audio/index.m3u8\n"
|
/// "http://example.com/audio/index.m3u8\n"
|
||||||
/// )
|
/// ))?;
|
||||||
/// .parse::<MasterPlaylist>()?;
|
|
||||||
///
|
///
|
||||||
/// println!("{}", master_playlist.has_independent_segments);
|
/// println!("{}", master_playlist.has_independent_segments);
|
||||||
/// # Ok::<(), hls_m3u8::Error>(())
|
/// # Ok::<(), hls_m3u8::Error>(())
|
||||||
|
@ -98,7 +98,7 @@ use crate::{Error, RequiredVersion};
|
||||||
#[builder(build_fn(validate = "Self::validate"))]
|
#[builder(build_fn(validate = "Self::validate"))]
|
||||||
#[builder(setter(into, strip_option))]
|
#[builder(setter(into, strip_option))]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct MasterPlaylist {
|
pub struct MasterPlaylist<'a> {
|
||||||
/// Indicates that all media samples in a [`MediaSegment`] can be
|
/// Indicates that all media samples in a [`MediaSegment`] can be
|
||||||
/// decoded without information from other segments.
|
/// decoded without information from other segments.
|
||||||
///
|
///
|
||||||
|
@ -135,14 +135,14 @@ pub struct MasterPlaylist {
|
||||||
///
|
///
|
||||||
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||||
#[builder(default)]
|
#[builder(default)]
|
||||||
pub media: Vec<ExtXMedia>,
|
pub media: Vec<ExtXMedia<'a>>,
|
||||||
/// A list of all streams of this [`MasterPlaylist`].
|
/// A list of all streams of this [`MasterPlaylist`].
|
||||||
///
|
///
|
||||||
/// ### Note
|
/// ### Note
|
||||||
///
|
///
|
||||||
/// This field is optional.
|
/// This field is optional.
|
||||||
#[builder(default)]
|
#[builder(default)]
|
||||||
pub variant_streams: Vec<VariantStream>,
|
pub variant_streams: Vec<VariantStream<'a>>,
|
||||||
/// The [`ExtXSessionData`] tag allows arbitrary session data to be
|
/// The [`ExtXSessionData`] tag allows arbitrary session data to be
|
||||||
/// carried in a [`MasterPlaylist`].
|
/// carried in a [`MasterPlaylist`].
|
||||||
///
|
///
|
||||||
|
@ -150,7 +150,7 @@ pub struct MasterPlaylist {
|
||||||
///
|
///
|
||||||
/// This field is optional.
|
/// This field is optional.
|
||||||
#[builder(default)]
|
#[builder(default)]
|
||||||
pub session_data: Vec<ExtXSessionData>,
|
pub session_data: Vec<ExtXSessionData<'a>>,
|
||||||
/// A list of [`ExtXSessionKey`]s, that allows the client to preload
|
/// A list of [`ExtXSessionKey`]s, that allows the client to preload
|
||||||
/// these keys without having to read the [`MediaPlaylist`]s first.
|
/// these keys without having to read the [`MediaPlaylist`]s first.
|
||||||
///
|
///
|
||||||
|
@ -160,17 +160,17 @@ pub struct MasterPlaylist {
|
||||||
///
|
///
|
||||||
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||||
#[builder(default)]
|
#[builder(default)]
|
||||||
pub session_keys: Vec<ExtXSessionKey>,
|
pub session_keys: Vec<ExtXSessionKey<'a>>,
|
||||||
/// A list of all tags that could not be identified while parsing the input.
|
/// A list of all tags that could not be identified while parsing the input.
|
||||||
///
|
///
|
||||||
/// ### Note
|
/// ### Note
|
||||||
///
|
///
|
||||||
/// This field is optional.
|
/// This field is optional.
|
||||||
#[builder(default)]
|
#[builder(default)]
|
||||||
pub unknown_tags: Vec<String>,
|
pub unknown_tags: Vec<Cow<'a, str>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MasterPlaylist {
|
impl<'a> MasterPlaylist<'a> {
|
||||||
/// Returns a builder for a [`MasterPlaylist`].
|
/// Returns a builder for a [`MasterPlaylist`].
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
@ -216,10 +216,10 @@ impl MasterPlaylist {
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn builder() -> MasterPlaylistBuilder { MasterPlaylistBuilder::default() }
|
pub fn builder() -> MasterPlaylistBuilder<'a> { MasterPlaylistBuilder::default() }
|
||||||
|
|
||||||
/// Returns all streams, which have an audio group id.
|
/// Returns all streams, which have an audio group id.
|
||||||
pub fn audio_streams(&self) -> impl Iterator<Item = &VariantStream> {
|
pub fn audio_streams(&self) -> impl Iterator<Item = &VariantStream<'a>> {
|
||||||
self.variant_streams.iter().filter(|stream| {
|
self.variant_streams.iter().filter(|stream| {
|
||||||
if let VariantStream::ExtXStreamInf { audio: Some(_), .. } = stream {
|
if let VariantStream::ExtXStreamInf { audio: Some(_), .. } = stream {
|
||||||
true
|
true
|
||||||
|
@ -230,7 +230,7 @@ impl MasterPlaylist {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns all streams, which have a video group id.
|
/// Returns all streams, which have a video group id.
|
||||||
pub fn video_streams(&self) -> impl Iterator<Item = &VariantStream> {
|
pub fn video_streams(&self) -> impl Iterator<Item = &VariantStream<'a>> {
|
||||||
self.variant_streams.iter().filter(|stream| {
|
self.variant_streams.iter().filter(|stream| {
|
||||||
if let VariantStream::ExtXStreamInf { stream_data, .. } = stream {
|
if let VariantStream::ExtXStreamInf { stream_data, .. } = stream {
|
||||||
stream_data.video().is_some()
|
stream_data.video().is_some()
|
||||||
|
@ -243,7 +243,7 @@ impl MasterPlaylist {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns all streams, which have no group id.
|
/// Returns all streams, which have no group id.
|
||||||
pub fn unassociated_streams(&self) -> impl Iterator<Item = &VariantStream> {
|
pub fn unassociated_streams(&self) -> impl Iterator<Item = &VariantStream<'a>> {
|
||||||
self.variant_streams.iter().filter(|stream| {
|
self.variant_streams.iter().filter(|stream| {
|
||||||
if let VariantStream::ExtXStreamInf {
|
if let VariantStream::ExtXStreamInf {
|
||||||
stream_data,
|
stream_data,
|
||||||
|
@ -263,17 +263,52 @@ impl MasterPlaylist {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns all `ExtXMedia` tags, associated with the provided stream.
|
/// Returns all `ExtXMedia` tags, associated with the provided stream.
|
||||||
pub fn associated_with<'a>(
|
pub fn associated_with<'b>(
|
||||||
&'a self,
|
&'b self,
|
||||||
stream: &'a VariantStream,
|
stream: &'b VariantStream<'_>,
|
||||||
) -> impl Iterator<Item = &ExtXMedia> + 'a {
|
) -> impl Iterator<Item = &ExtXMedia<'a>> + 'b {
|
||||||
self.media
|
self.media
|
||||||
.iter()
|
.iter()
|
||||||
.filter(move |media| stream.is_associated(media))
|
.filter(move |media| stream.is_associated(media))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||||
|
/// internal [`Cow`]s.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This is a relatively expensive operation.
|
||||||
|
#[must_use]
|
||||||
|
pub fn into_owned(self) -> MasterPlaylist<'static> {
|
||||||
|
MasterPlaylist {
|
||||||
|
has_independent_segments: self.has_independent_segments,
|
||||||
|
start: self.start,
|
||||||
|
media: self.media.into_iter().map(|v| v.into_owned()).collect(),
|
||||||
|
variant_streams: self
|
||||||
|
.variant_streams
|
||||||
|
.into_iter()
|
||||||
|
.map(|v| v.into_owned())
|
||||||
|
.collect(),
|
||||||
|
session_data: self
|
||||||
|
.session_data
|
||||||
|
.into_iter()
|
||||||
|
.map(|v| v.into_owned())
|
||||||
|
.collect(),
|
||||||
|
session_keys: self
|
||||||
|
.session_keys
|
||||||
|
.into_iter()
|
||||||
|
.map(|v| v.into_owned())
|
||||||
|
.collect(),
|
||||||
|
unknown_tags: self
|
||||||
|
.unknown_tags
|
||||||
|
.into_iter()
|
||||||
|
.map(|v| Cow::Owned(v.into_owned()))
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RequiredVersion for MasterPlaylist {
|
impl<'a> RequiredVersion for MasterPlaylist<'a> {
|
||||||
fn required_version(&self) -> ProtocolVersion {
|
fn required_version(&self) -> ProtocolVersion {
|
||||||
required_version![
|
required_version![
|
||||||
self.has_independent_segments
|
self.has_independent_segments
|
||||||
|
@ -287,7 +322,7 @@ impl RequiredVersion for MasterPlaylist {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MasterPlaylistBuilder {
|
impl<'a> MasterPlaylistBuilder<'a> {
|
||||||
fn validate(&self) -> Result<(), String> {
|
fn validate(&self) -> Result<(), String> {
|
||||||
if let Some(variant_streams) = &self.variant_streams {
|
if let Some(variant_streams) = &self.variant_streams {
|
||||||
self.validate_variants(variant_streams)
|
self.validate_variants(variant_streams)
|
||||||
|
@ -300,7 +335,7 @@ impl MasterPlaylistBuilder {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_variants(&self, variant_streams: &[VariantStream]) -> crate::Result<()> {
|
fn validate_variants(&self, variant_streams: &[VariantStream<'_>]) -> crate::Result<()> {
|
||||||
let mut closed_captions_none = false;
|
let mut closed_captions_none = false;
|
||||||
|
|
||||||
for variant in variant_streams {
|
for variant in variant_streams {
|
||||||
|
@ -382,7 +417,7 @@ impl MasterPlaylistBuilder {
|
||||||
fn check_media_group<T: AsRef<str>>(&self, media_type: MediaType, group_id: T) -> bool {
|
fn check_media_group<T: AsRef<str>>(&self, media_type: MediaType, group_id: T) -> bool {
|
||||||
if let Some(value) = &self.media {
|
if let Some(value) = &self.media {
|
||||||
value.iter().any(|media| {
|
value.iter().any(|media| {
|
||||||
media.media_type == media_type && media.group_id().as_str() == group_id.as_ref()
|
media.media_type == media_type && media.group_id().as_ref() == group_id.as_ref()
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
@ -390,7 +425,7 @@ impl MasterPlaylistBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RequiredVersion for MasterPlaylistBuilder {
|
impl<'a> RequiredVersion for MasterPlaylistBuilder<'a> {
|
||||||
fn required_version(&self) -> ProtocolVersion {
|
fn required_version(&self) -> ProtocolVersion {
|
||||||
// TODO: the .flatten() can be removed as soon as `recursive traits` are
|
// TODO: the .flatten() can be removed as soon as `recursive traits` are
|
||||||
// supported. (RequiredVersion is implemented for Option<T>, but
|
// supported. (RequiredVersion is implemented for Option<T>, but
|
||||||
|
@ -409,7 +444,7 @@ impl RequiredVersion for MasterPlaylistBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for MasterPlaylist {
|
impl<'a> fmt::Display for MasterPlaylist<'a> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
writeln!(f, "{}", ExtM3u)?;
|
writeln!(f, "{}", ExtM3u)?;
|
||||||
|
|
||||||
|
@ -449,10 +484,10 @@ impl fmt::Display for MasterPlaylist {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for MasterPlaylist {
|
impl<'a> TryFrom<&'a str> for MasterPlaylist<'a> {
|
||||||
type Err = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||||
let input = tag(input, ExtM3u::PREFIX)?;
|
let input = tag(input, ExtM3u::PREFIX)?;
|
||||||
let mut builder = Self::builder();
|
let mut builder = Self::builder();
|
||||||
|
|
||||||
|
@ -505,10 +540,10 @@ impl FromStr for MasterPlaylist {
|
||||||
Tag::ExtXStart(t) => {
|
Tag::ExtXStart(t) => {
|
||||||
builder.start(t);
|
builder.start(t);
|
||||||
}
|
}
|
||||||
_ => {
|
Tag::Unknown(value) => {
|
||||||
// [6.3.1. General Client Responsibilities]
|
// [6.3.1. General Client Responsibilities]
|
||||||
// > ignore any unrecognized tags.
|
// > ignore any unrecognized tags.
|
||||||
unknown_tags.push(tag.to_string());
|
unknown_tags.push(Cow::Borrowed(value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -604,7 +639,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
concat!(
|
MasterPlaylist::try_from(concat!(
|
||||||
"#EXTM3U\n",
|
"#EXTM3U\n",
|
||||||
"#EXT-X-STREAM-INF:",
|
"#EXT-X-STREAM-INF:",
|
||||||
"BANDWIDTH=150000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n",
|
"BANDWIDTH=150000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n",
|
||||||
|
@ -620,8 +655,7 @@ mod tests {
|
||||||
"http://example.com/high/index.m3u8\n",
|
"http://example.com/high/index.m3u8\n",
|
||||||
"#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS=\"mp4a.40.5\"\n",
|
"#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS=\"mp4a.40.5\"\n",
|
||||||
"http://example.com/audio/index.m3u8\n"
|
"http://example.com/audio/index.m3u8\n"
|
||||||
)
|
))
|
||||||
.parse::<MasterPlaylist>()
|
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
MasterPlaylist::builder()
|
MasterPlaylist::builder()
|
||||||
.variant_streams(vec![
|
.variant_streams(vec![
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
@ -23,7 +25,7 @@ use crate::{Error, RequiredVersion};
|
||||||
#[derive(Builder, Debug, Clone, PartialEq, Eq)]
|
#[derive(Builder, Debug, Clone, PartialEq, Eq)]
|
||||||
#[builder(build_fn(skip), setter(strip_option))]
|
#[builder(build_fn(skip), setter(strip_option))]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct MediaPlaylist {
|
pub struct MediaPlaylist<'a> {
|
||||||
/// Specifies the maximum [`MediaSegment::duration`]. A typical target
|
/// Specifies the maximum [`MediaSegment::duration`]. A typical target
|
||||||
/// duration is 10 seconds.
|
/// duration is 10 seconds.
|
||||||
///
|
///
|
||||||
|
@ -106,7 +108,7 @@ pub struct MediaPlaylist {
|
||||||
///
|
///
|
||||||
/// This field is required.
|
/// This field is required.
|
||||||
#[builder(setter(custom))]
|
#[builder(setter(custom))]
|
||||||
pub segments: StableVec<MediaSegment>,
|
pub segments: StableVec<MediaSegment<'a>>,
|
||||||
/// The allowable excess duration of each media segment in the
|
/// The allowable excess duration of each media segment in the
|
||||||
/// associated playlist.
|
/// associated playlist.
|
||||||
///
|
///
|
||||||
|
@ -129,10 +131,10 @@ pub struct MediaPlaylist {
|
||||||
///
|
///
|
||||||
/// This field is optional.
|
/// This field is optional.
|
||||||
#[builder(default, setter(into))]
|
#[builder(default, setter(into))]
|
||||||
pub unknown: Vec<String>,
|
pub unknown: Vec<Cow<'a, str>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MediaPlaylistBuilder {
|
impl<'a> MediaPlaylistBuilder<'a> {
|
||||||
fn validate(&self) -> Result<(), String> {
|
fn validate(&self) -> Result<(), String> {
|
||||||
if let Some(target_duration) = &self.target_duration {
|
if let Some(target_duration) = &self.target_duration {
|
||||||
self.validate_media_segments(*target_duration)
|
self.validate_media_segments(*target_duration)
|
||||||
|
@ -225,7 +227,7 @@ impl MediaPlaylistBuilder {
|
||||||
|
|
||||||
/// Adds a media segment to the resulting playlist and assigns the next free
|
/// Adds a media segment to the resulting playlist and assigns the next free
|
||||||
/// [`MediaSegment::number`] to the segment.
|
/// [`MediaSegment::number`] to the segment.
|
||||||
pub fn push_segment(&mut self, segment: MediaSegment) -> &mut Self {
|
pub fn push_segment(&mut self, segment: MediaSegment<'a>) -> &mut Self {
|
||||||
let segments = self.segments.get_or_insert_with(StableVec::new);
|
let segments = self.segments.get_or_insert_with(StableVec::new);
|
||||||
|
|
||||||
if segment.explicit_number {
|
if segment.explicit_number {
|
||||||
|
@ -239,7 +241,7 @@ impl MediaPlaylistBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse the rest of the [`MediaPlaylist`] from an m3u8 file.
|
/// Parse the rest of the [`MediaPlaylist`] from an m3u8 file.
|
||||||
pub fn parse(&mut self, input: &str) -> crate::Result<MediaPlaylist> {
|
pub fn parse(&mut self, input: &'a str) -> crate::Result<MediaPlaylist<'a>> {
|
||||||
parse_media_playlist(input, self)
|
parse_media_playlist(input, self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,8 +255,8 @@ impl MediaPlaylistBuilder {
|
||||||
/// number has been set explicitly. This function assumes, that all segments
|
/// number has been set explicitly. This function assumes, that all segments
|
||||||
/// will be present in the final media playlist and the following is only
|
/// will be present in the final media playlist and the following is only
|
||||||
/// possible if the segment is marked with `ExtXDiscontinuity`.
|
/// possible if the segment is marked with `ExtXDiscontinuity`.
|
||||||
pub fn segments(&mut self, segments: Vec<MediaSegment>) -> &mut Self {
|
pub fn segments(&mut self, segments: Vec<MediaSegment<'a>>) -> &mut Self {
|
||||||
let mut vec = StableVec::<MediaSegment>::with_capacity(segments.len());
|
let mut vec = StableVec::<MediaSegment<'a>>::with_capacity(segments.len());
|
||||||
let mut remaining = Vec::with_capacity(segments.len());
|
let mut remaining = Vec::with_capacity(segments.len());
|
||||||
|
|
||||||
for segment in segments {
|
for segment in segments {
|
||||||
|
@ -278,7 +280,7 @@ impl MediaPlaylistBuilder {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
///
|
///
|
||||||
/// If a required field has not been initialized.
|
/// If a required field has not been initialized.
|
||||||
pub fn build(&self) -> Result<MediaPlaylist, String> {
|
pub fn build(&self) -> Result<MediaPlaylist<'a>, String> {
|
||||||
// validate builder
|
// validate builder
|
||||||
self.validate()?;
|
self.validate()?;
|
||||||
|
|
||||||
|
@ -371,7 +373,7 @@ impl MediaPlaylistBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RequiredVersion for MediaPlaylistBuilder {
|
impl<'a> RequiredVersion for MediaPlaylistBuilder<'a> {
|
||||||
fn required_version(&self) -> ProtocolVersion {
|
fn required_version(&self) -> ProtocolVersion {
|
||||||
required_version![
|
required_version![
|
||||||
self.target_duration.map(ExtXTargetDuration),
|
self.target_duration.map(ExtXTargetDuration),
|
||||||
|
@ -393,11 +395,11 @@ impl RequiredVersion for MediaPlaylistBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MediaPlaylist {
|
impl<'a> MediaPlaylist<'a> {
|
||||||
/// Returns a builder for [`MediaPlaylist`].
|
/// Returns a builder for [`MediaPlaylist`].
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn builder() -> MediaPlaylistBuilder { MediaPlaylistBuilder::default() }
|
pub fn builder() -> MediaPlaylistBuilder<'a> { MediaPlaylistBuilder::default() }
|
||||||
|
|
||||||
/// Computes the `Duration` of the [`MediaPlaylist`], by adding each segment
|
/// Computes the `Duration` of the [`MediaPlaylist`], by adding each segment
|
||||||
/// duration together.
|
/// duration together.
|
||||||
|
@ -405,9 +407,42 @@ impl MediaPlaylist {
|
||||||
pub fn duration(&self) -> Duration {
|
pub fn duration(&self) -> Duration {
|
||||||
self.segments.values().map(|s| s.duration.duration()).sum()
|
self.segments.values().map(|s| s.duration.duration()).sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||||
|
/// internal [`Cow`]s.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This is a relatively expensive operation.
|
||||||
|
#[must_use]
|
||||||
|
pub fn into_owned(self) -> MediaPlaylist<'static> {
|
||||||
|
MediaPlaylist {
|
||||||
|
target_duration: self.target_duration,
|
||||||
|
media_sequence: self.media_sequence,
|
||||||
|
discontinuity_sequence: self.discontinuity_sequence,
|
||||||
|
playlist_type: self.playlist_type,
|
||||||
|
has_i_frames_only: self.has_i_frames_only,
|
||||||
|
has_independent_segments: self.has_independent_segments,
|
||||||
|
start: self.start,
|
||||||
|
has_end_list: self.has_end_list,
|
||||||
|
segments: {
|
||||||
|
self.segments
|
||||||
|
.into_iter()
|
||||||
|
.map(|(_, s)| s.into_owned())
|
||||||
|
.collect()
|
||||||
|
},
|
||||||
|
allowable_excess_duration: self.allowable_excess_duration,
|
||||||
|
unknown: {
|
||||||
|
self.unknown
|
||||||
|
.into_iter()
|
||||||
|
.map(|v| Cow::Owned(v.into_owned()))
|
||||||
|
.collect()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RequiredVersion for MediaPlaylist {
|
impl<'a> RequiredVersion for MediaPlaylist<'a> {
|
||||||
fn required_version(&self) -> ProtocolVersion {
|
fn required_version(&self) -> ProtocolVersion {
|
||||||
required_version![
|
required_version![
|
||||||
ExtXTargetDuration(self.target_duration),
|
ExtXTargetDuration(self.target_duration),
|
||||||
|
@ -425,7 +460,7 @@ impl RequiredVersion for MediaPlaylist {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for MediaPlaylist {
|
impl<'a> fmt::Display for MediaPlaylist<'a> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
writeln!(f, "{}", ExtM3u)?;
|
writeln!(f, "{}", ExtM3u)?;
|
||||||
|
|
||||||
|
@ -463,7 +498,7 @@ impl fmt::Display for MediaPlaylist {
|
||||||
writeln!(f, "{}", value)?;
|
writeln!(f, "{}", value)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut available_keys = HashSet::<ExtXKey>::new();
|
let mut available_keys = HashSet::<ExtXKey<'_>>::new();
|
||||||
|
|
||||||
for segment in self.segments.values() {
|
for segment in self.segments.values() {
|
||||||
for key in &segment.keys {
|
for key in &segment.keys {
|
||||||
|
@ -530,10 +565,10 @@ impl fmt::Display for MediaPlaylist {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_media_playlist(
|
fn parse_media_playlist<'a>(
|
||||||
input: &str,
|
input: &'a str,
|
||||||
builder: &mut MediaPlaylistBuilder,
|
builder: &mut MediaPlaylistBuilder<'a>,
|
||||||
) -> crate::Result<MediaPlaylist> {
|
) -> crate::Result<MediaPlaylist<'a>> {
|
||||||
let input = tag(input, "#EXTM3U")?;
|
let input = tag(input, "#EXTM3U")?;
|
||||||
|
|
||||||
let mut segment = MediaSegment::builder();
|
let mut segment = MediaSegment::builder();
|
||||||
|
@ -656,10 +691,10 @@ fn parse_media_playlist(
|
||||||
builder.start(t);
|
builder.start(t);
|
||||||
}
|
}
|
||||||
Tag::ExtXVersion(_) => {}
|
Tag::ExtXVersion(_) => {}
|
||||||
Tag::Unknown(_) => {
|
Tag::Unknown(s) => {
|
||||||
// [6.3.1. General Client Responsibilities]
|
// [6.3.1. General Client Responsibilities]
|
||||||
// > ignore any unrecognized tags.
|
// > ignore any unrecognized tags.
|
||||||
unknown.push(tag.to_string());
|
unknown.push(Cow::Borrowed(s));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -684,10 +719,18 @@ fn parse_media_playlist(
|
||||||
builder.build().map_err(Error::builder)
|
builder.build().map_err(Error::builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for MediaPlaylist {
|
impl FromStr for MediaPlaylist<'static> {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
|
Ok(parse_media_playlist(input, &mut Self::builder())?.into_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> TryFrom<&'a str> for MediaPlaylist<'a> {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||||
parse_media_playlist(input, &mut Self::builder())
|
parse_media_playlist(input, &mut Self::builder())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -713,7 +756,7 @@ mod tests {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Error (allowable segment duration = target duration = 8)
|
// Error (allowable segment duration = target duration = 8)
|
||||||
assert!(playlist.parse::<MediaPlaylist>().is_err());
|
assert!(MediaPlaylist::try_from(playlist).is_err());
|
||||||
|
|
||||||
// Error (allowable segment duration = 9)
|
// Error (allowable segment duration = 9)
|
||||||
assert!(MediaPlaylist::builder()
|
assert!(MediaPlaylist::builder()
|
||||||
|
@ -819,6 +862,6 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_empty_playlist() {
|
fn test_empty_playlist() {
|
||||||
let playlist = "";
|
let playlist = "";
|
||||||
assert!(playlist.parse::<MediaPlaylist>().is_err());
|
assert!(MediaPlaylist::try_from(playlist).is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use derive_builder::Builder;
|
use derive_builder::Builder;
|
||||||
|
@ -34,7 +35,7 @@ use crate::{Decryptable, RequiredVersion};
|
||||||
#[derive(ShortHand, Debug, Clone, Builder, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(ShortHand, Debug, Clone, Builder, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
#[builder(setter(strip_option))]
|
#[builder(setter(strip_option))]
|
||||||
#[shorthand(enable(must_use, skip))]
|
#[shorthand(enable(must_use, skip))]
|
||||||
pub struct MediaSegment {
|
pub struct MediaSegment<'a> {
|
||||||
/// Each [`MediaSegment`] has a number, which allows synchronization between
|
/// Each [`MediaSegment`] has a number, which allows synchronization between
|
||||||
/// different variants.
|
/// different variants.
|
||||||
///
|
///
|
||||||
|
@ -80,7 +81,7 @@ pub struct MediaSegment {
|
||||||
/// [`KeyFormat`]: crate::types::KeyFormat
|
/// [`KeyFormat`]: crate::types::KeyFormat
|
||||||
/// [`EncryptionMethod`]: crate::types::EncryptionMethod
|
/// [`EncryptionMethod`]: crate::types::EncryptionMethod
|
||||||
#[builder(default, setter(into))]
|
#[builder(default, setter(into))]
|
||||||
pub keys: Vec<ExtXKey>,
|
pub keys: Vec<ExtXKey<'a>>,
|
||||||
/// This field specifies how to obtain the Media Initialization Section
|
/// This field specifies how to obtain the Media Initialization Section
|
||||||
/// required to parse the applicable `MediaSegment`s.
|
/// required to parse the applicable `MediaSegment`s.
|
||||||
///
|
///
|
||||||
|
@ -94,7 +95,7 @@ pub struct MediaSegment {
|
||||||
///
|
///
|
||||||
/// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly
|
/// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly
|
||||||
#[builder(default)]
|
#[builder(default)]
|
||||||
pub map: Option<ExtXMap>,
|
pub map: Option<ExtXMap<'a>>,
|
||||||
/// This field indicates that a `MediaSegment` is a sub-range of the
|
/// This field indicates that a `MediaSegment` is a sub-range of the
|
||||||
/// resource identified by its URI.
|
/// resource identified by its URI.
|
||||||
///
|
///
|
||||||
|
@ -110,7 +111,7 @@ pub struct MediaSegment {
|
||||||
///
|
///
|
||||||
/// This field is optional.
|
/// This field is optional.
|
||||||
#[builder(default)]
|
#[builder(default)]
|
||||||
pub date_range: Option<ExtXDateRange>,
|
pub date_range: Option<ExtXDateRange<'a>>,
|
||||||
/// This field indicates a discontinuity between the `MediaSegment` that
|
/// This field indicates a discontinuity between the `MediaSegment` that
|
||||||
/// follows it and the one that preceded it.
|
/// follows it and the one that preceded it.
|
||||||
///
|
///
|
||||||
|
@ -134,14 +135,14 @@ pub struct MediaSegment {
|
||||||
///
|
///
|
||||||
/// This field is optional.
|
/// This field is optional.
|
||||||
#[builder(default)]
|
#[builder(default)]
|
||||||
pub program_date_time: Option<ExtXProgramDateTime>,
|
pub program_date_time: Option<ExtXProgramDateTime<'a>>,
|
||||||
/// This field indicates the duration of a media segment.
|
/// This field indicates the duration of a media segment.
|
||||||
///
|
///
|
||||||
/// ## Note
|
/// ## Note
|
||||||
///
|
///
|
||||||
/// This field is required.
|
/// This field is required.
|
||||||
#[builder(setter(into))]
|
#[builder(setter(into))]
|
||||||
pub duration: ExtInf,
|
pub duration: ExtInf<'a>,
|
||||||
/// The URI of a media segment.
|
/// The URI of a media segment.
|
||||||
///
|
///
|
||||||
/// ## Note
|
/// ## Note
|
||||||
|
@ -149,10 +150,10 @@ pub struct MediaSegment {
|
||||||
/// This field is required.
|
/// This field is required.
|
||||||
#[builder(setter(into))]
|
#[builder(setter(into))]
|
||||||
#[shorthand(enable(into), disable(skip))]
|
#[shorthand(enable(into), disable(skip))]
|
||||||
uri: String,
|
uri: Cow<'a, str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MediaSegment {
|
impl<'a> MediaSegment<'a> {
|
||||||
/// Returns a builder for a [`MediaSegment`].
|
/// Returns a builder for a [`MediaSegment`].
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
@ -173,12 +174,34 @@ impl MediaSegment {
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn builder() -> MediaSegmentBuilder { MediaSegmentBuilder::default() }
|
pub fn builder() -> MediaSegmentBuilder<'static> { MediaSegmentBuilder::default() }
|
||||||
|
|
||||||
|
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||||
|
/// internal [`Cow`]s.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This is a relatively expensive operation.
|
||||||
|
#[must_use]
|
||||||
|
pub fn into_owned(self) -> MediaSegment<'static> {
|
||||||
|
MediaSegment {
|
||||||
|
number: self.number,
|
||||||
|
explicit_number: self.explicit_number,
|
||||||
|
keys: self.keys.into_iter().map(|k| k.into_owned()).collect(),
|
||||||
|
map: self.map.map(|v| v.into_owned()),
|
||||||
|
byte_range: self.byte_range,
|
||||||
|
date_range: self.date_range.map(|v| v.into_owned()),
|
||||||
|
has_discontinuity: self.has_discontinuity,
|
||||||
|
program_date_time: self.program_date_time.map(|v| v.into_owned()),
|
||||||
|
duration: self.duration.into_owned(),
|
||||||
|
uri: Cow::Owned(self.uri.into_owned()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MediaSegmentBuilder {
|
impl<'a> MediaSegmentBuilder<'a> {
|
||||||
/// Pushes an [`ExtXKey`] tag.
|
/// Pushes an [`ExtXKey`] tag.
|
||||||
pub fn push_key<VALUE: Into<ExtXKey>>(&mut self, value: VALUE) -> &mut Self {
|
pub fn push_key<VALUE: Into<ExtXKey<'a>>>(&mut self, value: VALUE) -> &mut Self {
|
||||||
if let Some(keys) = &mut self.keys {
|
if let Some(keys) = &mut self.keys {
|
||||||
keys.push(value.into());
|
keys.push(value.into());
|
||||||
} else {
|
} else {
|
||||||
|
@ -201,7 +224,7 @@ impl MediaSegmentBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for MediaSegment {
|
impl<'a> fmt::Display for MediaSegment<'a> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
// NOTE: self.keys will be printed by the `MediaPlaylist` to prevent redundance.
|
// NOTE: self.keys will be printed by the `MediaPlaylist` to prevent redundance.
|
||||||
|
|
||||||
|
@ -231,7 +254,7 @@ impl fmt::Display for MediaSegment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RequiredVersion for MediaSegment {
|
impl<'a> RequiredVersion for MediaSegment<'a> {
|
||||||
fn required_version(&self) -> ProtocolVersion {
|
fn required_version(&self) -> ProtocolVersion {
|
||||||
required_version![
|
required_version![
|
||||||
self.keys,
|
self.keys,
|
||||||
|
@ -251,8 +274,8 @@ impl RequiredVersion for MediaSegment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Decryptable for MediaSegment {
|
impl<'a> Decryptable<'a> for MediaSegment<'a> {
|
||||||
fn keys(&self) -> Vec<&DecryptionKey> {
|
fn keys(&self) -> Vec<&DecryptionKey<'a>> {
|
||||||
//
|
//
|
||||||
self.keys.iter().filter_map(ExtXKey::as_ref).collect()
|
self.keys.iter().filter_map(ExtXKey::as_ref).collect()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use crate::types::ProtocolVersion;
|
use crate::types::ProtocolVersion;
|
||||||
use crate::utils::tag;
|
use crate::utils::tag;
|
||||||
|
@ -28,10 +28,10 @@ impl fmt::Display for ExtM3u {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", Self::PREFIX) }
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", Self::PREFIX) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtM3u {
|
impl TryFrom<&str> for ExtM3u {
|
||||||
type Err = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn try_from(input: &str) -> Result<Self, Self::Error> {
|
||||||
tag(input, Self::PREFIX)?;
|
tag(input, Self::PREFIX)?;
|
||||||
Ok(Self)
|
Ok(Self)
|
||||||
}
|
}
|
||||||
|
@ -49,8 +49,8 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
assert_eq!("#EXTM3U".parse::<ExtM3u>().unwrap(), ExtM3u);
|
assert_eq!(ExtM3u::try_from("#EXTM3U").unwrap(), ExtM3u);
|
||||||
assert!("#EXTM2U".parse::<ExtM3u>().is_err());
|
assert!(ExtM3u::try_from("#EXTM2U").is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use crate::types::ProtocolVersion;
|
use crate::types::ProtocolVersion;
|
||||||
use crate::utils::tag;
|
use crate::utils::tag;
|
||||||
|
@ -67,10 +67,10 @@ impl From<ProtocolVersion> for ExtXVersion {
|
||||||
fn from(value: ProtocolVersion) -> Self { Self(value) }
|
fn from(value: ProtocolVersion) -> Self { Self(value) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXVersion {
|
impl TryFrom<&str> for ExtXVersion {
|
||||||
type Err = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn try_from(input: &str) -> Result<Self, Self::Error> {
|
||||||
let version = tag(input, Self::PREFIX)?.parse()?;
|
let version = tag(input, Self::PREFIX)?.parse()?;
|
||||||
Ok(Self::new(version))
|
Ok(Self::new(version))
|
||||||
}
|
}
|
||||||
|
@ -92,7 +92,7 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"#EXT-X-VERSION:6".parse::<ExtXVersion>().unwrap(),
|
ExtXVersion::try_from("#EXT-X-VERSION:6").unwrap(),
|
||||||
ExtXVersion::new(ProtocolVersion::V6)
|
ExtXVersion::new(ProtocolVersion::V6)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use derive_builder::Builder;
|
use derive_builder::Builder;
|
||||||
use shorthand::ShortHand;
|
use shorthand::ShortHand;
|
||||||
|
@ -21,7 +22,7 @@ use crate::{Error, RequiredVersion};
|
||||||
#[shorthand(enable(must_use, into))]
|
#[shorthand(enable(must_use, into))]
|
||||||
#[builder(setter(into))]
|
#[builder(setter(into))]
|
||||||
#[builder(build_fn(validate = "Self::validate"))]
|
#[builder(build_fn(validate = "Self::validate"))]
|
||||||
pub struct ExtXMedia {
|
pub struct ExtXMedia<'a> {
|
||||||
/// The [`MediaType`] associated with this tag.
|
/// The [`MediaType`] associated with this tag.
|
||||||
///
|
///
|
||||||
/// ### Note
|
/// ### Note
|
||||||
|
@ -31,24 +32,7 @@ pub struct ExtXMedia {
|
||||||
pub media_type: MediaType,
|
pub media_type: MediaType,
|
||||||
/// An `URI` to a [`MediaPlaylist`].
|
/// An `URI` to a [`MediaPlaylist`].
|
||||||
///
|
///
|
||||||
/// # Example
|
/// ### Note
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXMedia;
|
|
||||||
/// use hls_m3u8::types::MediaType;
|
|
||||||
///
|
|
||||||
/// let mut media = ExtXMedia::new(MediaType::Audio, "ag1", "english audio channel");
|
|
||||||
/// # assert_eq!(media.uri(), None);
|
|
||||||
///
|
|
||||||
/// media.set_uri(Some("https://www.example.com/stream1.m3u8"));
|
|
||||||
///
|
|
||||||
/// assert_eq!(
|
|
||||||
/// media.uri(),
|
|
||||||
/// Some(&"https://www.example.com/stream1.m3u8".to_string())
|
|
||||||
/// );
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// # Note
|
|
||||||
///
|
///
|
||||||
/// - This field is required, if the [`ExtXMedia::media_type`] is
|
/// - This field is required, if the [`ExtXMedia::media_type`] is
|
||||||
/// [`MediaType::Subtitles`].
|
/// [`MediaType::Subtitles`].
|
||||||
|
@ -64,65 +48,39 @@ pub struct ExtXMedia {
|
||||||
/// [`VariantStream::ExtXStreamInf`]:
|
/// [`VariantStream::ExtXStreamInf`]:
|
||||||
/// crate::tags::VariantStream::ExtXStreamInf
|
/// crate::tags::VariantStream::ExtXStreamInf
|
||||||
#[builder(setter(strip_option), default)]
|
#[builder(setter(strip_option), default)]
|
||||||
uri: Option<String>,
|
uri: Option<Cow<'a, str>>,
|
||||||
/// The identifier that specifies the group to which the rendition
|
/// The identifier that specifies the group to which the rendition
|
||||||
/// belongs.
|
/// belongs.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// ### Note
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXMedia;
|
|
||||||
/// use hls_m3u8::types::MediaType;
|
|
||||||
///
|
|
||||||
/// let mut media = ExtXMedia::new(MediaType::Audio, "ag1", "english audio channel");
|
|
||||||
///
|
|
||||||
/// media.set_group_id("ag2");
|
|
||||||
///
|
|
||||||
/// assert_eq!(media.group_id(), &"ag2".to_string());
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// # Note
|
|
||||||
///
|
///
|
||||||
/// This field is required.
|
/// This field is required.
|
||||||
group_id: String,
|
group_id: Cow<'a, str>,
|
||||||
/// The name of the primary language used in the rendition.
|
/// The name of the primary language used in the rendition.
|
||||||
/// The value has to conform to [`RFC5646`].
|
/// The value has to conform to [`RFC5646`].
|
||||||
///
|
///
|
||||||
/// # Example
|
/// ### Note
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use hls_m3u8::tags::ExtXMedia;
|
|
||||||
/// use hls_m3u8::types::MediaType;
|
|
||||||
///
|
|
||||||
/// let mut media = ExtXMedia::new(MediaType::Audio, "ag1", "english audio channel");
|
|
||||||
///
|
|
||||||
/// media.set_language(Some("en"));
|
|
||||||
///
|
|
||||||
/// assert_eq!(media.language(), Some(&"en".to_string()));
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// # Note
|
|
||||||
///
|
///
|
||||||
/// This field is optional.
|
/// This field is optional.
|
||||||
///
|
///
|
||||||
/// [`RFC5646`]: https://tools.ietf.org/html/rfc5646
|
/// [`RFC5646`]: https://tools.ietf.org/html/rfc5646
|
||||||
#[builder(setter(strip_option), default)]
|
#[builder(setter(strip_option), default)]
|
||||||
language: Option<String>,
|
language: Option<Cow<'a, str>>,
|
||||||
/// The name of a language associated with the rendition.
|
/// The name of a language associated with the rendition.
|
||||||
/// An associated language is often used in a different role, than the
|
/// An associated language is often used in a different role, than the
|
||||||
/// language specified by the [`language`] field (e.g., written versus
|
/// language specified by the [`language`] field (e.g., written versus
|
||||||
/// spoken, or a fallback dialect).
|
/// spoken, or a fallback dialect).
|
||||||
///
|
///
|
||||||
/// # Note
|
/// ### Note
|
||||||
///
|
///
|
||||||
/// This field is optional.
|
/// This field is optional.
|
||||||
///
|
///
|
||||||
/// [`language`]: #method.language
|
/// [`language`]: #method.language
|
||||||
#[builder(setter(strip_option), default)]
|
#[builder(setter(strip_option), default)]
|
||||||
assoc_language: Option<String>,
|
assoc_language: Option<Cow<'a, str>>,
|
||||||
/// A human-readable description of the rendition.
|
/// A human-readable description of the rendition.
|
||||||
///
|
///
|
||||||
/// # Note
|
/// ### Note
|
||||||
///
|
///
|
||||||
/// This field is required.
|
/// This field is required.
|
||||||
///
|
///
|
||||||
|
@ -130,7 +88,7 @@ pub struct ExtXMedia {
|
||||||
/// that language.
|
/// that language.
|
||||||
///
|
///
|
||||||
/// [`language`]: #method.language
|
/// [`language`]: #method.language
|
||||||
name: String,
|
name: Cow<'a, str>,
|
||||||
/// The value of the `default` flag.
|
/// The value of the `default` flag.
|
||||||
/// A value of `true` indicates, that the client should play
|
/// A value of `true` indicates, that the client should play
|
||||||
/// this rendition of the content in the absence of information
|
/// this rendition of the content in the absence of information
|
||||||
|
@ -189,13 +147,13 @@ pub struct ExtXMedia {
|
||||||
///
|
///
|
||||||
/// The characteristics field may include private UTIs.
|
/// The characteristics field may include private UTIs.
|
||||||
///
|
///
|
||||||
/// # Note
|
/// ### Note
|
||||||
///
|
///
|
||||||
/// This field is optional.
|
/// This field is optional.
|
||||||
///
|
///
|
||||||
/// [`UTI`]: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#ref-UTI
|
/// [`UTI`]: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#ref-UTI
|
||||||
#[builder(setter(strip_option), default)]
|
#[builder(setter(strip_option), default)]
|
||||||
characteristics: Option<String>,
|
characteristics: Option<Cow<'a, str>>,
|
||||||
/// A count of audio channels indicating the maximum number of independent,
|
/// A count of audio channels indicating the maximum number of independent,
|
||||||
/// simultaneous audio channels present in any [`MediaSegment`] in the
|
/// simultaneous audio channels present in any [`MediaSegment`] in the
|
||||||
/// rendition.
|
/// rendition.
|
||||||
|
@ -214,7 +172,7 @@ pub struct ExtXMedia {
|
||||||
pub channels: Option<Channels>,
|
pub channels: Option<Channels>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExtXMediaBuilder {
|
impl<'a> ExtXMediaBuilder<'a> {
|
||||||
fn validate(&self) -> Result<(), String> {
|
fn validate(&self) -> Result<(), String> {
|
||||||
// A MediaType is always required!
|
// A MediaType is always required!
|
||||||
let media_type = self
|
let media_type = self
|
||||||
|
@ -254,7 +212,7 @@ impl ExtXMediaBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExtXMedia {
|
impl<'a> ExtXMedia<'a> {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA:";
|
||||||
|
|
||||||
/// Makes a new [`ExtXMedia`] tag with the associated [`MediaType`], the
|
/// Makes a new [`ExtXMedia`] tag with the associated [`MediaType`], the
|
||||||
|
@ -275,8 +233,8 @@ impl ExtXMedia {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new<T, K>(media_type: MediaType, group_id: T, name: K) -> Self
|
pub fn new<T, K>(media_type: MediaType, group_id: T, name: K) -> Self
|
||||||
where
|
where
|
||||||
T: Into<String>,
|
T: Into<Cow<'a, str>>,
|
||||||
K: Into<String>,
|
K: Into<Cow<'a, str>>,
|
||||||
{
|
{
|
||||||
Self {
|
Self {
|
||||||
media_type,
|
media_type,
|
||||||
|
@ -317,22 +275,47 @@ impl ExtXMedia {
|
||||||
/// "public.accessibility.describes-music-and-sound"
|
/// "public.accessibility.describes-music-and-sound"
|
||||||
/// ))
|
/// ))
|
||||||
/// .build()?;
|
/// .build()?;
|
||||||
/// # Ok::<(), Box<dyn ::std::error::Error>>(())
|
/// # Ok::<(), String>(())
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn builder() -> ExtXMediaBuilder { ExtXMediaBuilder::default() }
|
#[inline]
|
||||||
|
pub fn builder() -> ExtXMediaBuilder<'a> { ExtXMediaBuilder::default() }
|
||||||
|
|
||||||
|
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||||
|
/// internal [`Cow`]s.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This is a relatively expensive operation.
|
||||||
|
#[must_use]
|
||||||
|
pub fn into_owned(self) -> ExtXMedia<'static> {
|
||||||
|
ExtXMedia {
|
||||||
|
media_type: self.media_type,
|
||||||
|
uri: self.uri.map(|v| Cow::Owned(v.into_owned())),
|
||||||
|
group_id: Cow::Owned(self.group_id.into_owned()),
|
||||||
|
language: self.language.map(|v| Cow::Owned(v.into_owned())),
|
||||||
|
assoc_language: self.assoc_language.map(|v| Cow::Owned(v.into_owned())),
|
||||||
|
name: Cow::Owned(self.name.into_owned()),
|
||||||
|
is_default: self.is_default,
|
||||||
|
is_autoselect: self.is_autoselect,
|
||||||
|
is_forced: self.is_forced,
|
||||||
|
instream_id: self.instream_id,
|
||||||
|
characteristics: self.characteristics.map(|v| Cow::Owned(v.into_owned())),
|
||||||
|
channels: self.channels,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This tag requires either `ProtocolVersion::V1` or if there is an
|
/// This tag requires either `ProtocolVersion::V1` or if there is an
|
||||||
/// `instream_id` it requires it's version.
|
/// `instream_id` it requires it's version.
|
||||||
impl RequiredVersion for ExtXMedia {
|
impl<'a> RequiredVersion for ExtXMedia<'a> {
|
||||||
fn required_version(&self) -> ProtocolVersion {
|
fn required_version(&self) -> ProtocolVersion {
|
||||||
self.instream_id
|
self.instream_id
|
||||||
.map_or(ProtocolVersion::V1, |i| i.required_version())
|
.map_or(ProtocolVersion::V1, |i| i.required_version())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXMedia {
|
impl<'a> fmt::Display for ExtXMedia<'a> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
write!(f, "TYPE={}", self.media_type)?;
|
write!(f, "TYPE={}", self.media_type)?;
|
||||||
|
@ -380,10 +363,10 @@ impl fmt::Display for ExtXMedia {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXMedia {
|
impl<'a> TryFrom<&'a str> for ExtXMedia<'a> {
|
||||||
type Err = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||||
let input = tag(input, Self::PREFIX)?;
|
let input = tag(input, Self::PREFIX)?;
|
||||||
|
|
||||||
let mut builder = Self::builder();
|
let mut builder = Self::builder();
|
||||||
|
@ -455,7 +438,7 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
$(
|
$(
|
||||||
assert_eq!($struct, $str.parse().unwrap());
|
assert_eq!($struct, TryFrom::try_from($str).unwrap());
|
||||||
)+
|
)+
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -754,25 +737,28 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser_error() {
|
fn test_parser_error() {
|
||||||
assert!("".parse::<ExtXMedia>().is_err());
|
assert_eq!(ExtXMedia::try_from("").is_err(), true);
|
||||||
assert!("garbage".parse::<ExtXMedia>().is_err());
|
assert_eq!(ExtXMedia::try_from("garbage").is_err(), true);
|
||||||
|
|
||||||
assert!(
|
assert_eq!(
|
||||||
"#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,URI=\"http://www.example.com\""
|
ExtXMedia::try_from("#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,URI=\"http://www.example.com\"")
|
||||||
.parse::<ExtXMedia>()
|
.is_err(),
|
||||||
.is_err()
|
true
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
ExtXMedia::try_from("#EXT-X-MEDIA:TYPE=AUDIO,INSTREAM-ID=CC1").is_err(),
|
||||||
|
true
|
||||||
);
|
);
|
||||||
assert!("#EXT-X-MEDIA:TYPE=AUDIO,INSTREAM-ID=CC1"
|
|
||||||
.parse::<ExtXMedia>()
|
|
||||||
.is_err());
|
|
||||||
|
|
||||||
assert!("#EXT-X-MEDIA:TYPE=AUDIO,DEFAULT=YES,AUTOSELECT=NO"
|
assert_eq!(
|
||||||
.parse::<ExtXMedia>()
|
ExtXMedia::try_from("#EXT-X-MEDIA:TYPE=AUDIO,DEFAULT=YES,AUTOSELECT=NO").is_err(),
|
||||||
.is_err());
|
true
|
||||||
|
);
|
||||||
|
|
||||||
assert!("#EXT-X-MEDIA:TYPE=AUDIO,FORCED=YES"
|
assert_eq!(
|
||||||
.parse::<ExtXMedia>()
|
ExtXMedia::try_from("#EXT-X-MEDIA:TYPE=AUDIO,FORCED=YES").is_err(),
|
||||||
.is_err());
|
true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use derive_builder::Builder;
|
use derive_builder::Builder;
|
||||||
use shorthand::ShortHand;
|
use shorthand::ShortHand;
|
||||||
|
@ -11,7 +12,7 @@ use crate::{Error, RequiredVersion};
|
||||||
|
|
||||||
/// The data of [`ExtXSessionData`].
|
/// The data of [`ExtXSessionData`].
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub enum SessionData {
|
pub enum SessionData<'a> {
|
||||||
/// Contains the data identified by the [`ExtXSessionData::data_id`].
|
/// Contains the data identified by the [`ExtXSessionData::data_id`].
|
||||||
///
|
///
|
||||||
/// If a [`language`] is specified, this variant should contain a
|
/// If a [`language`] is specified, this variant should contain a
|
||||||
|
@ -19,12 +20,28 @@ pub enum SessionData {
|
||||||
///
|
///
|
||||||
/// [`data_id`]: ExtXSessionData::data_id
|
/// [`data_id`]: ExtXSessionData::data_id
|
||||||
/// [`language`]: ExtXSessionData::language
|
/// [`language`]: ExtXSessionData::language
|
||||||
Value(String),
|
Value(Cow<'a, str>),
|
||||||
/// An [`URI`], which points to a [`json`] file.
|
/// An [`URI`], which points to a [`json`] file.
|
||||||
///
|
///
|
||||||
/// [`json`]: https://tools.ietf.org/html/rfc8259
|
/// [`json`]: https://tools.ietf.org/html/rfc8259
|
||||||
/// [`URI`]: https://tools.ietf.org/html/rfc3986
|
/// [`URI`]: https://tools.ietf.org/html/rfc3986
|
||||||
Uri(String),
|
Uri(Cow<'a, str>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> SessionData<'a> {
|
||||||
|
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||||
|
/// internal [`Cow`]s.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This is a relatively expensive operation.
|
||||||
|
#[must_use]
|
||||||
|
pub fn into_owned(self) -> SessionData<'static> {
|
||||||
|
match self {
|
||||||
|
Self::Value(v) => SessionData::Value(Cow::Owned(v.into_owned())),
|
||||||
|
Self::Uri(v) => SessionData::Uri(Cow::Owned(v.into_owned())),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Allows arbitrary session data to be carried in a [`MasterPlaylist`].
|
/// Allows arbitrary session data to be carried in a [`MasterPlaylist`].
|
||||||
|
@ -33,7 +50,7 @@ pub enum SessionData {
|
||||||
#[derive(ShortHand, Builder, Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)]
|
#[derive(ShortHand, Builder, Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)]
|
||||||
#[builder(setter(into))]
|
#[builder(setter(into))]
|
||||||
#[shorthand(enable(must_use, into))]
|
#[shorthand(enable(must_use, into))]
|
||||||
pub struct ExtXSessionData {
|
pub struct ExtXSessionData<'a> {
|
||||||
/// This should conform to a [reverse DNS] naming convention, such as
|
/// This should conform to a [reverse DNS] naming convention, such as
|
||||||
/// `com.example.movie.title`.
|
/// `com.example.movie.title`.
|
||||||
///
|
///
|
||||||
|
@ -45,7 +62,7 @@ pub struct ExtXSessionData {
|
||||||
/// This field is required.
|
/// This field is required.
|
||||||
///
|
///
|
||||||
/// [reverse DNS]: https://en.wikipedia.org/wiki/Reverse_domain_name_notation
|
/// [reverse DNS]: https://en.wikipedia.org/wiki/Reverse_domain_name_notation
|
||||||
data_id: String,
|
data_id: Cow<'a, str>,
|
||||||
/// The [`SessionData`] associated with the
|
/// The [`SessionData`] associated with the
|
||||||
/// [`data_id`](ExtXSessionData::data_id).
|
/// [`data_id`](ExtXSessionData::data_id).
|
||||||
///
|
///
|
||||||
|
@ -53,7 +70,7 @@ pub struct ExtXSessionData {
|
||||||
///
|
///
|
||||||
/// This field is required.
|
/// This field is required.
|
||||||
#[shorthand(enable(skip))]
|
#[shorthand(enable(skip))]
|
||||||
pub data: SessionData,
|
pub data: SessionData<'a>,
|
||||||
/// The `language` attribute identifies the language of the [`SessionData`].
|
/// The `language` attribute identifies the language of the [`SessionData`].
|
||||||
///
|
///
|
||||||
/// # Note
|
/// # Note
|
||||||
|
@ -62,11 +79,11 @@ pub struct ExtXSessionData {
|
||||||
/// [RFC5646].
|
/// [RFC5646].
|
||||||
///
|
///
|
||||||
/// [RFC5646]: https://tools.ietf.org/html/rfc5646
|
/// [RFC5646]: https://tools.ietf.org/html/rfc5646
|
||||||
#[builder(setter(into, strip_option), default)]
|
#[builder(setter(strip_option), default)]
|
||||||
language: Option<String>,
|
language: Option<Cow<'a, str>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExtXSessionData {
|
impl<'a> ExtXSessionData<'a> {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-DATA:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-DATA:";
|
||||||
|
|
||||||
/// Makes a new [`ExtXSessionData`] tag.
|
/// Makes a new [`ExtXSessionData`] tag.
|
||||||
|
@ -83,7 +100,7 @@ impl ExtXSessionData {
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new<T: Into<String>>(data_id: T, data: SessionData) -> Self {
|
pub fn new<T: Into<Cow<'a, str>>>(data_id: T, data: SessionData<'a>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
data_id: data_id.into(),
|
data_id: data_id.into(),
|
||||||
data,
|
data,
|
||||||
|
@ -107,7 +124,7 @@ impl ExtXSessionData {
|
||||||
/// # Ok::<(), String>(())
|
/// # Ok::<(), String>(())
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn builder() -> ExtXSessionDataBuilder { ExtXSessionDataBuilder::default() }
|
pub fn builder() -> ExtXSessionDataBuilder<'a> { ExtXSessionDataBuilder::default() }
|
||||||
|
|
||||||
/// Makes a new [`ExtXSessionData`] tag, with the given language.
|
/// Makes a new [`ExtXSessionData`] tag, with the given language.
|
||||||
///
|
///
|
||||||
|
@ -124,10 +141,10 @@ impl ExtXSessionData {
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn with_language<T, K>(data_id: T, data: SessionData, language: K) -> Self
|
pub fn with_language<T, K>(data_id: T, data: SessionData<'a>, language: K) -> Self
|
||||||
where
|
where
|
||||||
T: Into<String>,
|
T: Into<Cow<'a, str>>,
|
||||||
K: Into<String>,
|
K: Into<Cow<'a, str>>,
|
||||||
{
|
{
|
||||||
Self {
|
Self {
|
||||||
data_id: data_id.into(),
|
data_id: data_id.into(),
|
||||||
|
@ -135,14 +152,29 @@ impl ExtXSessionData {
|
||||||
language: Some(language.into()),
|
language: Some(language.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||||
|
/// internal [`Cow`]s.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This is a relatively expensive operation.
|
||||||
|
#[must_use]
|
||||||
|
pub fn into_owned(self) -> ExtXSessionData<'static> {
|
||||||
|
ExtXSessionData {
|
||||||
|
data_id: Cow::Owned(self.data_id.into_owned()),
|
||||||
|
data: self.data.into_owned(),
|
||||||
|
language: self.language.map(|v| Cow::Owned(v.into_owned())),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This tag requires [`ProtocolVersion::V1`].
|
/// This tag requires [`ProtocolVersion::V1`].
|
||||||
impl RequiredVersion for ExtXSessionData {
|
impl<'a> RequiredVersion for ExtXSessionData<'a> {
|
||||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXSessionData {
|
impl<'a> fmt::Display for ExtXSessionData<'a> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
write!(f, "DATA-ID={}", quote(&self.data_id))?;
|
write!(f, "DATA-ID={}", quote(&self.data_id))?;
|
||||||
|
@ -160,10 +192,10 @@ impl fmt::Display for ExtXSessionData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXSessionData {
|
impl<'a> TryFrom<&'a str> for ExtXSessionData<'a> {
|
||||||
type Err = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||||
let input = tag(input, Self::PREFIX)?;
|
let input = tag(input, Self::PREFIX)?;
|
||||||
|
|
||||||
let mut data_id = None;
|
let mut data_id = None;
|
||||||
|
@ -228,28 +260,26 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
$(
|
$(
|
||||||
assert_eq!($struct, $str.parse().unwrap());
|
assert_eq!($struct, TryFrom::try_from($str).unwrap());
|
||||||
)+
|
)+
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
concat!(
|
ExtXSessionData::try_from(concat!(
|
||||||
"#EXT-X-SESSION-DATA:",
|
"#EXT-X-SESSION-DATA:",
|
||||||
"DATA-ID=\"foo\",",
|
"DATA-ID=\"foo\",",
|
||||||
"LANGUAGE=\"baz\""
|
"LANGUAGE=\"baz\""
|
||||||
)
|
))
|
||||||
.parse::<ExtXSessionData>()
|
|
||||||
.is_err()
|
.is_err()
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
concat!(
|
ExtXSessionData::try_from(concat!(
|
||||||
"#EXT-X-SESSION-DATA:",
|
"#EXT-X-SESSION-DATA:",
|
||||||
"DATA-ID=\"foo\",",
|
"DATA-ID=\"foo\",",
|
||||||
"LANGUAGE=\"baz\",",
|
"LANGUAGE=\"baz\",",
|
||||||
"VALUE=\"VALUE\",",
|
"VALUE=\"VALUE\",",
|
||||||
"URI=\"https://www.example.com/\""
|
"URI=\"https://www.example.com/\""
|
||||||
)
|
))
|
||||||
.parse::<ExtXSessionData>()
|
|
||||||
.is_err()
|
.is_err()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -300,10 +330,7 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_required_version() {
|
fn test_required_version() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXSessionData::new(
|
ExtXSessionData::new("com.example.lyrics", SessionData::Uri("lyrics.json".into()))
|
||||||
"com.example.lyrics",
|
|
||||||
SessionData::Uri("lyrics.json".to_string())
|
|
||||||
)
|
|
||||||
.required_version(),
|
.required_version(),
|
||||||
ProtocolVersion::V1
|
ProtocolVersion::V1
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
use core::convert::TryFrom;
|
use core::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use derive_more::{AsMut, AsRef, From};
|
use derive_more::{AsMut, AsRef, From};
|
||||||
|
|
||||||
|
@ -22,9 +21,9 @@ use crate::{Error, RequiredVersion};
|
||||||
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
||||||
/// [`ExtXKey`]: crate::tags::ExtXKey
|
/// [`ExtXKey`]: crate::tags::ExtXKey
|
||||||
#[derive(AsRef, AsMut, From, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(AsRef, AsMut, From, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub struct ExtXSessionKey(pub DecryptionKey);
|
pub struct ExtXSessionKey<'a>(pub DecryptionKey<'a>);
|
||||||
|
|
||||||
impl ExtXSessionKey {
|
impl<'a> ExtXSessionKey<'a> {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-KEY:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-KEY:";
|
||||||
|
|
||||||
/// Makes a new [`ExtXSessionKey`] tag.
|
/// Makes a new [`ExtXSessionKey`] tag.
|
||||||
|
@ -42,13 +41,24 @@ impl ExtXSessionKey {
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[inline]
|
#[inline]
|
||||||
pub const fn new(inner: DecryptionKey) -> Self { Self(inner) }
|
pub const fn new(inner: DecryptionKey<'a>) -> Self { Self(inner) }
|
||||||
|
|
||||||
|
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||||
|
/// internal [`Cow`]s.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This is a relatively expensive operation.
|
||||||
|
///
|
||||||
|
/// [`Cow`]: std::borrow::Cow
|
||||||
|
#[must_use]
|
||||||
|
pub fn into_owned(self) -> ExtXSessionKey<'static> { ExtXSessionKey(self.0.into_owned()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<ExtXKey> for ExtXSessionKey {
|
impl<'a> TryFrom<ExtXKey<'a>> for ExtXSessionKey<'a> {
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn try_from(value: ExtXKey) -> Result<Self, Self::Error> {
|
fn try_from(value: ExtXKey<'a>) -> Result<Self, Self::Error> {
|
||||||
if let ExtXKey(Some(inner)) = value {
|
if let ExtXKey(Some(inner)) = value {
|
||||||
Ok(Self(inner))
|
Ok(Self(inner))
|
||||||
} else {
|
} else {
|
||||||
|
@ -59,21 +69,21 @@ impl TryFrom<ExtXKey> for ExtXSessionKey {
|
||||||
|
|
||||||
/// This tag requires the same [`ProtocolVersion`] that is returned by
|
/// This tag requires the same [`ProtocolVersion`] that is returned by
|
||||||
/// `DecryptionKey::required_version`.
|
/// `DecryptionKey::required_version`.
|
||||||
impl RequiredVersion for ExtXSessionKey {
|
impl<'a> RequiredVersion for ExtXSessionKey<'a> {
|
||||||
fn required_version(&self) -> ProtocolVersion { self.0.required_version() }
|
fn required_version(&self) -> ProtocolVersion { self.0.required_version() }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXSessionKey {
|
impl<'a> fmt::Display for ExtXSessionKey<'a> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "{}{}", Self::PREFIX, self.0.to_string())
|
write!(f, "{}{}", Self::PREFIX, self.0.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXSessionKey {
|
impl<'a> TryFrom<&'a str> for ExtXSessionKey<'a> {
|
||||||
type Err = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||||
Ok(Self(DecryptionKey::from_str(tag(input, Self::PREFIX)?)?))
|
Ok(Self(DecryptionKey::try_from(tag(input, Self::PREFIX)?)?))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,7 +105,7 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
$(
|
$(
|
||||||
assert_eq!($struct, $str.parse().unwrap());
|
assert_eq!($struct, TryFrom::try_from($str).unwrap());
|
||||||
)+
|
)+
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
use core::convert::TryFrom;
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
use core::ops::Deref;
|
use core::ops::Deref;
|
||||||
use core::str::FromStr;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use crate::attribute::AttributePairs;
|
use crate::attribute::AttributePairs;
|
||||||
use crate::tags::ExtXMedia;
|
use crate::tags::ExtXMedia;
|
||||||
|
@ -67,7 +68,7 @@ use crate::Error;
|
||||||
/// [`PlaylistType`]: crate::types::PlaylistType
|
/// [`PlaylistType`]: crate::types::PlaylistType
|
||||||
/// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly
|
/// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly
|
||||||
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
|
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
|
||||||
pub enum VariantStream {
|
pub enum VariantStream<'a> {
|
||||||
/// The [`VariantStream::ExtXIFrame`] variant identifies a [`MediaPlaylist`]
|
/// The [`VariantStream::ExtXIFrame`] variant identifies a [`MediaPlaylist`]
|
||||||
/// file containing the I-frames of a multimedia presentation.
|
/// file containing the I-frames of a multimedia presentation.
|
||||||
/// It stands alone, in that it does not apply to a particular URI in the
|
/// It stands alone, in that it does not apply to a particular URI in the
|
||||||
|
@ -85,14 +86,14 @@ pub enum VariantStream {
|
||||||
///
|
///
|
||||||
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||||
/// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly
|
/// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly
|
||||||
uri: String,
|
uri: Cow<'a, str>,
|
||||||
/// Some fields are shared between [`VariantStream::ExtXStreamInf`] and
|
/// Some fields are shared between [`VariantStream::ExtXStreamInf`] and
|
||||||
/// [`VariantStream::ExtXIFrame`].
|
/// [`VariantStream::ExtXIFrame`].
|
||||||
///
|
///
|
||||||
/// # Note
|
/// # Note
|
||||||
///
|
///
|
||||||
/// This field is optional.
|
/// This field is optional.
|
||||||
stream_data: StreamData,
|
stream_data: StreamData<'a>,
|
||||||
},
|
},
|
||||||
/// [`VariantStream::ExtXStreamInf`] specifies a [`VariantStream`], which is
|
/// [`VariantStream::ExtXStreamInf`] specifies a [`VariantStream`], which is
|
||||||
/// a set of renditions that can be combined to play the presentation.
|
/// a set of renditions that can be combined to play the presentation.
|
||||||
|
@ -106,7 +107,7 @@ pub enum VariantStream {
|
||||||
/// This field is required.
|
/// This field is required.
|
||||||
///
|
///
|
||||||
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||||
uri: String,
|
uri: Cow<'a, str>,
|
||||||
/// The value is an unsigned float describing the maximum frame
|
/// The value is an unsigned float describing the maximum frame
|
||||||
/// rate for all the video in the [`VariantStream`].
|
/// rate for all the video in the [`VariantStream`].
|
||||||
///
|
///
|
||||||
|
@ -132,7 +133,7 @@ pub enum VariantStream {
|
||||||
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
||||||
/// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type
|
/// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type
|
||||||
/// [`MediaType::Audio`]: crate::types::MediaType::Audio
|
/// [`MediaType::Audio`]: crate::types::MediaType::Audio
|
||||||
audio: Option<String>,
|
audio: Option<Cow<'a, str>>,
|
||||||
/// It indicates the set of subtitle renditions that can be used when
|
/// It indicates the set of subtitle renditions that can be used when
|
||||||
/// playing the presentation.
|
/// playing the presentation.
|
||||||
///
|
///
|
||||||
|
@ -149,25 +150,25 @@ pub enum VariantStream {
|
||||||
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
||||||
/// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type
|
/// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type
|
||||||
/// [`MediaType::Subtitles`]: crate::types::MediaType::Subtitles
|
/// [`MediaType::Subtitles`]: crate::types::MediaType::Subtitles
|
||||||
subtitles: Option<String>,
|
subtitles: Option<Cow<'a, str>>,
|
||||||
/// It indicates the set of closed-caption renditions that can be used
|
/// It indicates the set of closed-caption renditions that can be used
|
||||||
/// when playing the presentation.
|
/// when playing the presentation.
|
||||||
///
|
///
|
||||||
/// # Note
|
/// # Note
|
||||||
///
|
///
|
||||||
/// This field is optional.
|
/// This field is optional.
|
||||||
closed_captions: Option<ClosedCaptions>,
|
closed_captions: Option<ClosedCaptions<'a>>,
|
||||||
/// Some fields are shared between [`VariantStream::ExtXStreamInf`] and
|
/// Some fields are shared between [`VariantStream::ExtXStreamInf`] and
|
||||||
/// [`VariantStream::ExtXIFrame`].
|
/// [`VariantStream::ExtXIFrame`].
|
||||||
///
|
///
|
||||||
/// # Note
|
/// # Note
|
||||||
///
|
///
|
||||||
/// This field is optional.
|
/// This field is optional.
|
||||||
stream_data: StreamData,
|
stream_data: StreamData<'a>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VariantStream {
|
impl<'a> VariantStream<'a> {
|
||||||
pub(crate) const PREFIX_EXTXIFRAME: &'static str = "#EXT-X-I-FRAME-STREAM-INF:";
|
pub(crate) const PREFIX_EXTXIFRAME: &'static str = "#EXT-X-I-FRAME-STREAM-INF:";
|
||||||
pub(crate) const PREFIX_EXTXSTREAMINF: &'static str = "#EXT-X-STREAM-INF:";
|
pub(crate) const PREFIX_EXTXSTREAMINF: &'static str = "#EXT-X-STREAM-INF:";
|
||||||
|
|
||||||
|
@ -203,7 +204,7 @@ impl VariantStream {
|
||||||
/// ));
|
/// ));
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn is_associated(&self, media: &ExtXMedia) -> bool {
|
pub fn is_associated(&self, media: &ExtXMedia<'_>) -> bool {
|
||||||
match &self {
|
match &self {
|
||||||
Self::ExtXIFrame { stream_data, .. } => {
|
Self::ExtXIFrame { stream_data, .. } => {
|
||||||
if let MediaType::Video = media.media_type {
|
if let MediaType::Video = media.media_type {
|
||||||
|
@ -238,10 +239,45 @@ impl VariantStream {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||||
|
/// internal [`Cow`]s.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This is a relatively expensive operation.
|
||||||
|
#[must_use]
|
||||||
|
pub fn into_owned(self) -> VariantStream<'static> {
|
||||||
|
match self {
|
||||||
|
VariantStream::ExtXIFrame { uri, stream_data } => {
|
||||||
|
VariantStream::ExtXIFrame {
|
||||||
|
uri: Cow::Owned(uri.into_owned()),
|
||||||
|
stream_data: stream_data.into_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VariantStream::ExtXStreamInf {
|
||||||
|
uri,
|
||||||
|
frame_rate,
|
||||||
|
audio,
|
||||||
|
subtitles,
|
||||||
|
closed_captions,
|
||||||
|
stream_data,
|
||||||
|
} => {
|
||||||
|
VariantStream::ExtXStreamInf {
|
||||||
|
uri: Cow::Owned(uri.into_owned()),
|
||||||
|
frame_rate,
|
||||||
|
audio: audio.map(|v| Cow::Owned(v.into_owned())),
|
||||||
|
subtitles: subtitles.map(|v| Cow::Owned(v.into_owned())),
|
||||||
|
closed_captions: closed_captions.map(|v| v.into_owned()),
|
||||||
|
stream_data: stream_data.into_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This tag requires [`ProtocolVersion::V1`].
|
/// This tag requires [`ProtocolVersion::V1`].
|
||||||
impl RequiredVersion for VariantStream {
|
impl<'a> RequiredVersion for VariantStream<'a> {
|
||||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||||
|
|
||||||
fn introduced_version(&self) -> ProtocolVersion {
|
fn introduced_version(&self) -> ProtocolVersion {
|
||||||
|
@ -265,7 +301,7 @@ impl RequiredVersion for VariantStream {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for VariantStream {
|
impl<'a> fmt::Display for VariantStream<'a> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match &self {
|
match &self {
|
||||||
Self::ExtXIFrame { uri, stream_data } => {
|
Self::ExtXIFrame { uri, stream_data } => {
|
||||||
|
@ -306,10 +342,10 @@ impl fmt::Display for VariantStream {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for VariantStream {
|
impl<'a> TryFrom<&'a str> for VariantStream<'a> {
|
||||||
type Err = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||||
if let Ok(input) = tag(input, Self::PREFIX_EXTXIFRAME) {
|
if let Ok(input) = tag(input, Self::PREFIX_EXTXIFRAME) {
|
||||||
let uri = AttributePairs::new(input)
|
let uri = AttributePairs::new(input)
|
||||||
.find_map(|(key, value)| {
|
.find_map(|(key, value)| {
|
||||||
|
@ -323,7 +359,7 @@ impl FromStr for VariantStream {
|
||||||
|
|
||||||
Ok(Self::ExtXIFrame {
|
Ok(Self::ExtXIFrame {
|
||||||
uri,
|
uri,
|
||||||
stream_data: input.parse()?,
|
stream_data: StreamData::try_from(input)?,
|
||||||
})
|
})
|
||||||
} else if let Ok(input) = tag(input, Self::PREFIX_EXTXSTREAMINF) {
|
} else if let Ok(input) = tag(input, Self::PREFIX_EXTXSTREAMINF) {
|
||||||
let mut lines = input.lines();
|
let mut lines = input.lines();
|
||||||
|
@ -342,18 +378,20 @@ impl FromStr for VariantStream {
|
||||||
"FRAME-RATE" => frame_rate = Some(value.parse()?),
|
"FRAME-RATE" => frame_rate = Some(value.parse()?),
|
||||||
"AUDIO" => audio = Some(unquote(value)),
|
"AUDIO" => audio = Some(unquote(value)),
|
||||||
"SUBTITLES" => subtitles = Some(unquote(value)),
|
"SUBTITLES" => subtitles = Some(unquote(value)),
|
||||||
"CLOSED-CAPTIONS" => closed_captions = Some(value.parse().unwrap()),
|
"CLOSED-CAPTIONS" => {
|
||||||
|
closed_captions = Some(ClosedCaptions::try_from(value).unwrap())
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self::ExtXStreamInf {
|
Ok(Self::ExtXStreamInf {
|
||||||
uri: uri.to_string(),
|
uri: Cow::Borrowed(uri),
|
||||||
frame_rate,
|
frame_rate,
|
||||||
audio,
|
audio,
|
||||||
subtitles,
|
subtitles,
|
||||||
closed_captions,
|
closed_captions,
|
||||||
stream_data: first_line.parse()?,
|
stream_data: StreamData::try_from(first_line)?,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// TODO: custom error type? + attach input data
|
// TODO: custom error type? + attach input data
|
||||||
|
@ -366,8 +404,8 @@ impl FromStr for VariantStream {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for VariantStream {
|
impl<'a> Deref for VariantStream<'a> {
|
||||||
type Target = StreamData;
|
type Target = StreamData<'a>;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
match &self {
|
match &self {
|
||||||
|
@ -378,7 +416,7 @@ impl Deref for VariantStream {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq<&VariantStream> for VariantStream {
|
impl<'a> PartialEq<&VariantStream<'a>> for VariantStream<'a> {
|
||||||
fn eq(&self, other: &&Self) -> bool { self.eq(*other) }
|
fn eq(&self, other: &&Self) -> bool { self.eq(*other) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -386,7 +424,7 @@ impl PartialEq<&VariantStream> for VariantStream {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::types::InStreamId;
|
use crate::types::InStreamId;
|
||||||
//use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_required_version() {
|
fn test_required_version() {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use crate::types::ProtocolVersion;
|
use crate::types::ProtocolVersion;
|
||||||
use crate::utils::tag;
|
use crate::utils::tag;
|
||||||
|
@ -28,10 +28,10 @@ impl fmt::Display for ExtXDiscontinuitySequence {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXDiscontinuitySequence {
|
impl TryFrom<&str> for ExtXDiscontinuitySequence {
|
||||||
type Err = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn try_from(input: &str) -> Result<Self, Self::Error> {
|
||||||
let input = tag(input, Self::PREFIX)?;
|
let input = tag(input, Self::PREFIX)?;
|
||||||
let seq_num = input.parse().map_err(|e| Error::parse_int(input, e))?;
|
let seq_num = input.parse().map_err(|e| Error::parse_int(input, e))?;
|
||||||
|
|
||||||
|
@ -64,11 +64,11 @@ mod test {
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXDiscontinuitySequence(123),
|
ExtXDiscontinuitySequence(123),
|
||||||
"#EXT-X-DISCONTINUITY-SEQUENCE:123".parse().unwrap()
|
ExtXDiscontinuitySequence::try_from("#EXT-X-DISCONTINUITY-SEQUENCE:123").unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXDiscontinuitySequence::from_str("#EXT-X-DISCONTINUITY-SEQUENCE:12A"),
|
ExtXDiscontinuitySequence::try_from("#EXT-X-DISCONTINUITY-SEQUENCE:12A"),
|
||||||
Err(Error::parse_int("12A", "12A".parse::<u64>().expect_err("")))
|
Err(Error::parse_int("12A", "12A".parse::<u64>().expect_err("")))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use crate::types::ProtocolVersion;
|
use crate::types::ProtocolVersion;
|
||||||
use crate::utils::tag;
|
use crate::utils::tag;
|
||||||
|
@ -26,10 +26,10 @@ impl fmt::Display for ExtXEndList {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Self::PREFIX.fmt(f) }
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Self::PREFIX.fmt(f) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXEndList {
|
impl TryFrom<&str> for ExtXEndList {
|
||||||
type Err = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn try_from(input: &str) -> Result<Self, Self::Error> {
|
||||||
tag(input, Self::PREFIX)?;
|
tag(input, Self::PREFIX)?;
|
||||||
Ok(Self)
|
Ok(Self)
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,10 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
assert_eq!(ExtXEndList, "#EXT-X-ENDLIST".parse().unwrap());
|
assert_eq!(
|
||||||
|
ExtXEndList,
|
||||||
|
ExtXEndList::try_from("#EXT-X-ENDLIST").unwrap()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use crate::types::ProtocolVersion;
|
use crate::types::ProtocolVersion;
|
||||||
use crate::utils::tag;
|
use crate::utils::tag;
|
||||||
|
@ -21,10 +21,10 @@ impl fmt::Display for ExtXIFramesOnly {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Self::PREFIX.fmt(f) }
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Self::PREFIX.fmt(f) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXIFramesOnly {
|
impl TryFrom<&str> for ExtXIFramesOnly {
|
||||||
type Err = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn try_from(input: &str) -> Result<Self, Self::Error> {
|
||||||
tag(input, Self::PREFIX)?;
|
tag(input, Self::PREFIX)?;
|
||||||
Ok(Self)
|
Ok(Self)
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,12 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() { assert_eq!(ExtXIFramesOnly, "#EXT-X-I-FRAMES-ONLY".parse().unwrap(),) }
|
fn test_parser() {
|
||||||
|
assert_eq!(
|
||||||
|
ExtXIFramesOnly,
|
||||||
|
ExtXIFramesOnly::try_from("#EXT-X-I-FRAMES-ONLY").unwrap(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_required_version() {
|
fn test_required_version() {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use crate::types::ProtocolVersion;
|
use crate::types::ProtocolVersion;
|
||||||
use crate::utils::tag;
|
use crate::utils::tag;
|
||||||
|
@ -26,10 +26,10 @@ impl fmt::Display for ExtXMediaSequence {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXMediaSequence {
|
impl TryFrom<&str> for ExtXMediaSequence {
|
||||||
type Err = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn try_from(input: &str) -> Result<Self, Self::Error> {
|
||||||
let input = tag(input, Self::PREFIX)?;
|
let input = tag(input, Self::PREFIX)?;
|
||||||
let seq_num = input.parse().map_err(|e| Error::parse_int(input, e))?;
|
let seq_num = input.parse().map_err(|e| Error::parse_int(input, e))?;
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@ mod test {
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXMediaSequence(123),
|
ExtXMediaSequence(123),
|
||||||
"#EXT-X-MEDIA-SEQUENCE:123".parse().unwrap()
|
ExtXMediaSequence::try_from("#EXT-X-MEDIA-SEQUENCE:123").unwrap()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::types::ProtocolVersion;
|
use crate::types::ProtocolVersion;
|
||||||
|
@ -25,10 +25,10 @@ impl fmt::Display for ExtXTargetDuration {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXTargetDuration {
|
impl TryFrom<&str> for ExtXTargetDuration {
|
||||||
type Err = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn try_from(input: &str) -> Result<Self, Self::Error> {
|
||||||
let input = tag(input, Self::PREFIX)?
|
let input = tag(input, Self::PREFIX)?
|
||||||
.parse()
|
.parse()
|
||||||
.map_err(|e| Error::parse_int(input, e))?;
|
.map_err(|e| Error::parse_int(input, e))?;
|
||||||
|
@ -62,7 +62,7 @@ mod test {
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXTargetDuration(Duration::from_secs(5)),
|
ExtXTargetDuration(Duration::from_secs(5)),
|
||||||
"#EXT-X-TARGETDURATION:5".parse().unwrap()
|
ExtXTargetDuration::try_from("#EXT-X-TARGETDURATION:5").unwrap()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use core::ops::{Add, AddAssign, Sub, SubAssign};
|
use core::ops::{Add, AddAssign, Sub, SubAssign};
|
||||||
|
|
||||||
|
@ -187,13 +187,13 @@ impl fmt::Display for ExtXByteRange {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXByteRange {
|
impl TryFrom<&str> for ExtXByteRange {
|
||||||
type Err = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn try_from(input: &str) -> Result<Self, Self::Error> {
|
||||||
let input = tag(input, Self::PREFIX)?;
|
let input = tag(input, Self::PREFIX)?;
|
||||||
|
|
||||||
Ok(Self(ByteRange::from_str(input)?))
|
Ok(Self(ByteRange::try_from(input)?))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,12 +219,12 @@ mod test {
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXByteRange::from(2..15),
|
ExtXByteRange::from(2..15),
|
||||||
"#EXT-X-BYTERANGE:13@2".parse().unwrap()
|
ExtXByteRange::try_from("#EXT-X-BYTERANGE:13@2").unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXByteRange::from(..22),
|
ExtXByteRange::from(..22),
|
||||||
"#EXT-X-BYTERANGE:22".parse().unwrap()
|
ExtXByteRange::try_from("#EXT-X-BYTERANGE:22").unwrap()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
#[cfg(feature = "chrono")]
|
#[cfg(feature = "chrono")]
|
||||||
|
@ -18,13 +19,13 @@ use crate::{Error, RequiredVersion};
|
||||||
#[derive(ShortHand, Builder, Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
|
#[derive(ShortHand, Builder, Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
|
||||||
#[builder(setter(into))]
|
#[builder(setter(into))]
|
||||||
#[shorthand(enable(must_use, into))]
|
#[shorthand(enable(must_use, into))]
|
||||||
pub struct ExtXDateRange {
|
pub struct ExtXDateRange<'a> {
|
||||||
/// A string that uniquely identifies an [`ExtXDateRange`] in the playlist.
|
/// A string that uniquely identifies an [`ExtXDateRange`] in the playlist.
|
||||||
///
|
///
|
||||||
/// ## Note
|
/// ## Note
|
||||||
///
|
///
|
||||||
/// This field is required.
|
/// This field is required.
|
||||||
id: String,
|
id: Cow<'a, str>,
|
||||||
/// A client-defined string that specifies some set of attributes and their
|
/// A client-defined string that specifies some set of attributes and their
|
||||||
/// associated value semantics. All [`ExtXDateRange`]s with the same class
|
/// associated value semantics. All [`ExtXDateRange`]s with the same class
|
||||||
/// attribute value must adhere to these semantics.
|
/// attribute value must adhere to these semantics.
|
||||||
|
@ -33,7 +34,7 @@ pub struct ExtXDateRange {
|
||||||
///
|
///
|
||||||
/// This field is optional.
|
/// This field is optional.
|
||||||
#[builder(setter(strip_option), default)]
|
#[builder(setter(strip_option), default)]
|
||||||
class: Option<String>,
|
class: Option<Cow<'a, str>>,
|
||||||
/// The date at which the [`ExtXDateRange`] begins.
|
/// The date at which the [`ExtXDateRange`] begins.
|
||||||
///
|
///
|
||||||
/// ## Note
|
/// ## Note
|
||||||
|
@ -56,7 +57,7 @@ pub struct ExtXDateRange {
|
||||||
/// here.
|
/// here.
|
||||||
#[cfg(not(feature = "chrono"))]
|
#[cfg(not(feature = "chrono"))]
|
||||||
#[builder(setter(strip_option), default)]
|
#[builder(setter(strip_option), default)]
|
||||||
start_date: Option<String>,
|
start_date: Option<Cow<'a, str>>,
|
||||||
/// The date at which the [`ExtXDateRange`] ends. It must be equal to or
|
/// The date at which the [`ExtXDateRange`] ends. It must be equal to or
|
||||||
/// later than the value of the [`start-date`] attribute.
|
/// later than the value of the [`start-date`] attribute.
|
||||||
///
|
///
|
||||||
|
@ -79,7 +80,7 @@ pub struct ExtXDateRange {
|
||||||
/// [`start-date`]: #method.start_date
|
/// [`start-date`]: #method.start_date
|
||||||
#[cfg(not(feature = "chrono"))]
|
#[cfg(not(feature = "chrono"))]
|
||||||
#[builder(setter(strip_option), default)]
|
#[builder(setter(strip_option), default)]
|
||||||
end_date: Option<String>,
|
end_date: Option<Cow<'a, str>>,
|
||||||
/// The duration of the [`ExtXDateRange`]. A single instant in time (e.g.,
|
/// The duration of the [`ExtXDateRange`]. A single instant in time (e.g.,
|
||||||
/// crossing a finish line) should be represented with a duration of 0.
|
/// crossing a finish line) should be represented with a duration of 0.
|
||||||
///
|
///
|
||||||
|
@ -114,7 +115,7 @@ pub struct ExtXDateRange {
|
||||||
///
|
///
|
||||||
/// This field is optional.
|
/// This field is optional.
|
||||||
#[builder(setter(strip_option), default)]
|
#[builder(setter(strip_option), default)]
|
||||||
scte35_cmd: Option<String>,
|
scte35_cmd: Option<Cow<'a, str>>,
|
||||||
/// SCTE-35 (ANSI/SCTE 35 2013) is a joint ANSI/Society of Cable and
|
/// SCTE-35 (ANSI/SCTE 35 2013) is a joint ANSI/Society of Cable and
|
||||||
/// Telecommunications Engineers standard that describes the inline
|
/// Telecommunications Engineers standard that describes the inline
|
||||||
/// insertion of cue tones in mpeg-ts streams.
|
/// insertion of cue tones in mpeg-ts streams.
|
||||||
|
@ -131,7 +132,7 @@ pub struct ExtXDateRange {
|
||||||
///
|
///
|
||||||
/// This field is optional.
|
/// This field is optional.
|
||||||
#[builder(setter(strip_option), default)]
|
#[builder(setter(strip_option), default)]
|
||||||
scte35_out: Option<String>,
|
scte35_out: Option<Cow<'a, str>>,
|
||||||
/// SCTE-35 (ANSI/SCTE 35 2013) is a joint ANSI/Society of Cable and
|
/// SCTE-35 (ANSI/SCTE 35 2013) is a joint ANSI/Society of Cable and
|
||||||
/// Telecommunications Engineers standard that describes the inline
|
/// Telecommunications Engineers standard that describes the inline
|
||||||
/// insertion of cue tones in mpeg-ts streams.
|
/// insertion of cue tones in mpeg-ts streams.
|
||||||
|
@ -148,7 +149,7 @@ pub struct ExtXDateRange {
|
||||||
///
|
///
|
||||||
/// This field is optional.
|
/// This field is optional.
|
||||||
#[builder(setter(strip_option), default)]
|
#[builder(setter(strip_option), default)]
|
||||||
scte35_in: Option<String>,
|
scte35_in: Option<Cow<'a, str>>,
|
||||||
/// This field indicates that the [`ExtXDateRange::end_date`] is equal to
|
/// This field indicates that the [`ExtXDateRange::end_date`] is equal to
|
||||||
/// the [`ExtXDateRange::start_date`] of the following range.
|
/// the [`ExtXDateRange::start_date`] of the following range.
|
||||||
///
|
///
|
||||||
|
@ -179,30 +180,25 @@ pub struct ExtXDateRange {
|
||||||
/// This field is optional.
|
/// This field is optional.
|
||||||
#[builder(default)]
|
#[builder(default)]
|
||||||
#[shorthand(enable(collection_magic), disable(set, get))]
|
#[shorthand(enable(collection_magic), disable(set, get))]
|
||||||
pub client_attributes: BTreeMap<String, Value>,
|
pub client_attributes: BTreeMap<Cow<'a, str>, Value<'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExtXDateRangeBuilder {
|
impl<'a> ExtXDateRangeBuilder<'a> {
|
||||||
/// Inserts a key value pair.
|
/// Inserts a key value pair.
|
||||||
pub fn insert_client_attribute<K: Into<String>, V: Into<Value>>(
|
pub fn insert_client_attribute<K: Into<Cow<'a, str>>, V: Into<Value<'a>>>(
|
||||||
&mut self,
|
&mut self,
|
||||||
key: K,
|
key: K,
|
||||||
value: V,
|
value: V,
|
||||||
) -> &mut Self {
|
) -> &mut Self {
|
||||||
if self.client_attributes.is_none() {
|
let attrs = self.client_attributes.get_or_insert_with(BTreeMap::new);
|
||||||
self.client_attributes = Some(BTreeMap::new());
|
|
||||||
}
|
attrs.insert(key.into(), value.into());
|
||||||
|
|
||||||
if let Some(client_attributes) = &mut self.client_attributes {
|
|
||||||
client_attributes.insert(key.into(), value.into());
|
|
||||||
} else {
|
|
||||||
unreachable!();
|
|
||||||
}
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExtXDateRange {
|
impl<'a> ExtXDateRange<'a> {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-DATERANGE:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-DATERANGE:";
|
||||||
|
|
||||||
/// Makes a new [`ExtXDateRange`] tag.
|
/// Makes a new [`ExtXDateRange`] tag.
|
||||||
|
@ -237,7 +233,7 @@ let date_range = ExtXDateRange::new("id", "2010-02-19T14:54:23.031+08:00");
|
||||||
"#
|
"#
|
||||||
)]
|
)]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new<T: Into<String>, #[cfg(not(feature = "chrono"))] I: Into<String>>(
|
pub fn new<T: Into<Cow<'a, str>>, #[cfg(not(feature = "chrono"))] I: Into<Cow<'a, str>>>(
|
||||||
id: T,
|
id: T,
|
||||||
#[cfg(feature = "chrono")] start_date: DateTime<FixedOffset>,
|
#[cfg(feature = "chrono")] start_date: DateTime<FixedOffset>,
|
||||||
#[cfg(not(feature = "chrono"))] start_date: I,
|
#[cfg(not(feature = "chrono"))] start_date: I,
|
||||||
|
@ -316,18 +312,52 @@ let date_range = ExtXDateRange::builder()
|
||||||
)]
|
)]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn builder() -> ExtXDateRangeBuilder { ExtXDateRangeBuilder::default() }
|
pub fn builder() -> ExtXDateRangeBuilder<'a> { ExtXDateRangeBuilder::default() }
|
||||||
|
|
||||||
|
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||||
|
/// internal [`Cow`]s.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This is a relatively expensive operation.
|
||||||
|
#[must_use]
|
||||||
|
pub fn into_owned(self) -> ExtXDateRange<'static> {
|
||||||
|
ExtXDateRange {
|
||||||
|
id: Cow::Owned(self.id.into_owned()),
|
||||||
|
class: self.class.map(|v| Cow::Owned(v.into_owned())),
|
||||||
|
#[cfg(not(feature = "chrono"))]
|
||||||
|
start_date: self.start_date.map(|v| Cow::Owned(v.into_owned())),
|
||||||
|
#[cfg(feature = "chrono")]
|
||||||
|
start_date: self.start_date,
|
||||||
|
#[cfg(not(feature = "chrono"))]
|
||||||
|
end_date: self.end_date.map(|v| Cow::Owned(v.into_owned())),
|
||||||
|
#[cfg(feature = "chrono")]
|
||||||
|
end_date: self.end_date,
|
||||||
|
scte35_cmd: self.scte35_cmd.map(|v| Cow::Owned(v.into_owned())),
|
||||||
|
scte35_out: self.scte35_out.map(|v| Cow::Owned(v.into_owned())),
|
||||||
|
scte35_in: self.scte35_in.map(|v| Cow::Owned(v.into_owned())),
|
||||||
|
client_attributes: {
|
||||||
|
self.client_attributes
|
||||||
|
.into_iter()
|
||||||
|
.map(|(k, v)| (Cow::Owned(k.into_owned()), v.into_owned()))
|
||||||
|
.collect()
|
||||||
|
},
|
||||||
|
duration: self.duration,
|
||||||
|
end_on_next: self.end_on_next,
|
||||||
|
planned_duration: self.planned_duration,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This tag requires [`ProtocolVersion::V1`].
|
/// This tag requires [`ProtocolVersion::V1`].
|
||||||
impl RequiredVersion for ExtXDateRange {
|
impl<'a> RequiredVersion for ExtXDateRange<'a> {
|
||||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXDateRange {
|
impl<'a> TryFrom<&'a str> for ExtXDateRange<'a> {
|
||||||
type Err = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||||
let input = tag(input, Self::PREFIX)?;
|
let input = tag(input, Self::PREFIX)?;
|
||||||
|
|
||||||
let mut id = None;
|
let mut id = None;
|
||||||
|
@ -398,7 +428,7 @@ impl FromStr for ExtXDateRange {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
client_attributes.insert(key.to_string(), value.parse()?);
|
client_attributes.insert(Cow::Borrowed(key), Value::try_from(value)?);
|
||||||
} else {
|
} else {
|
||||||
// [6.3.1. General Client Responsibilities]
|
// [6.3.1. General Client Responsibilities]
|
||||||
// > ignore any attribute/value pair with an
|
// > ignore any attribute/value pair with an
|
||||||
|
@ -451,7 +481,7 @@ impl FromStr for ExtXDateRange {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXDateRange {
|
impl<'a> fmt::Display for ExtXDateRange<'a> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
write!(f, "ID={}", quote(&self.id))?;
|
write!(f, "ID={}", quote(&self.id))?;
|
||||||
|
@ -547,22 +577,20 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
$(
|
$(
|
||||||
assert_eq!($left, $right.parse().unwrap());
|
assert_eq!($left, TryFrom::try_from($right).unwrap());
|
||||||
)*
|
)*
|
||||||
assert!("#EXT-X-DATERANGE:END-ON-NEXT=NO"
|
assert!(ExtXDateRange::try_from("#EXT-X-DATERANGE:END-ON-NEXT=NO")
|
||||||
.parse::<ExtXDateRange>()
|
|
||||||
.is_err());
|
.is_err());
|
||||||
|
|
||||||
assert!("garbage".parse::<ExtXDateRange>().is_err());
|
assert!(ExtXDateRange::try_from("garbage").is_err());
|
||||||
assert!("".parse::<ExtXDateRange>().is_err());
|
assert!(ExtXDateRange::try_from("").is_err());
|
||||||
|
|
||||||
assert!(concat!(
|
assert!(ExtXDateRange::try_from(concat!(
|
||||||
"#EXT-X-DATERANGE:",
|
"#EXT-X-DATERANGE:",
|
||||||
"ID=\"test_id\",",
|
"ID=\"test_id\",",
|
||||||
"START-DATE=\"2014-03-05T11:15:00Z\",",
|
"START-DATE=\"2014-03-05T11:15:00Z\",",
|
||||||
"END-ON-NEXT=YES"
|
"END-ON-NEXT=YES"
|
||||||
)
|
))
|
||||||
.parse::<ExtXDateRange>()
|
|
||||||
.is_err());
|
.is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use crate::types::ProtocolVersion;
|
use crate::types::ProtocolVersion;
|
||||||
use crate::utils::tag;
|
use crate::utils::tag;
|
||||||
|
@ -23,10 +23,10 @@ impl fmt::Display for ExtXDiscontinuity {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Self::PREFIX.fmt(f) }
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Self::PREFIX.fmt(f) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXDiscontinuity {
|
impl TryFrom<&str> for ExtXDiscontinuity {
|
||||||
type Err = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn try_from(input: &str) -> Result<Self, Self::Error> {
|
||||||
tag(input, Self::PREFIX)?;
|
tag(input, Self::PREFIX)?;
|
||||||
Ok(Self)
|
Ok(Self)
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,12 @@ mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() { assert_eq!(ExtXDiscontinuity, "#EXT-X-DISCONTINUITY".parse().unwrap()) }
|
fn test_parser() {
|
||||||
|
assert_eq!(
|
||||||
|
ExtXDiscontinuity,
|
||||||
|
ExtXDiscontinuity::try_from("#EXT-X-DISCONTINUITY").unwrap()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_required_version() {
|
fn test_required_version() {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use derive_more::AsRef;
|
use derive_more::AsRef;
|
||||||
|
@ -12,13 +13,13 @@ use crate::{Error, RequiredVersion};
|
||||||
///
|
///
|
||||||
/// [`Media Segment`]: crate::media_segment::MediaSegment
|
/// [`Media Segment`]: crate::media_segment::MediaSegment
|
||||||
#[derive(AsRef, Default, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
#[derive(AsRef, Default, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
pub struct ExtInf {
|
pub struct ExtInf<'a> {
|
||||||
#[as_ref]
|
#[as_ref]
|
||||||
duration: Duration,
|
duration: Duration,
|
||||||
title: Option<String>,
|
title: Option<Cow<'a, str>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExtInf {
|
impl<'a> ExtInf<'a> {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXTINF:";
|
pub(crate) const PREFIX: &'static str = "#EXTINF:";
|
||||||
|
|
||||||
/// Makes a new [`ExtInf`] tag.
|
/// Makes a new [`ExtInf`] tag.
|
||||||
|
@ -50,7 +51,7 @@ impl ExtInf {
|
||||||
/// let ext_inf = ExtInf::with_title(Duration::from_secs(5), "title");
|
/// let ext_inf = ExtInf::with_title(Duration::from_secs(5), "title");
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn with_title<T: Into<String>>(duration: Duration, title: T) -> Self {
|
pub fn with_title<T: Into<Cow<'a, str>>>(duration: Duration, title: T) -> Self {
|
||||||
Self {
|
Self {
|
||||||
duration,
|
duration,
|
||||||
title: Some(title.into()),
|
title: Some(title.into()),
|
||||||
|
@ -101,10 +102,10 @@ impl ExtInf {
|
||||||
///
|
///
|
||||||
/// let ext_inf = ExtInf::with_title(Duration::from_secs(5), "title");
|
/// let ext_inf = ExtInf::with_title(Duration::from_secs(5), "title");
|
||||||
///
|
///
|
||||||
/// assert_eq!(ext_inf.title(), &Some("title".to_string()));
|
/// assert_eq!(ext_inf.title(), &Some("title".into()));
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn title(&self) -> &Option<String> { &self.title }
|
pub const fn title(&self) -> &Option<Cow<'a, str>> { &self.title }
|
||||||
|
|
||||||
/// Sets the title of the associated media segment.
|
/// Sets the title of the associated media segment.
|
||||||
///
|
///
|
||||||
|
@ -118,17 +119,31 @@ impl ExtInf {
|
||||||
///
|
///
|
||||||
/// ext_inf.set_title(Some("better title"));
|
/// ext_inf.set_title(Some("better title"));
|
||||||
///
|
///
|
||||||
/// assert_eq!(ext_inf.title(), &Some("better title".to_string()));
|
/// assert_eq!(ext_inf.title(), &Some("better title".into()));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn set_title<T: ToString>(&mut self, value: Option<T>) -> &mut Self {
|
pub fn set_title<T: Into<Cow<'a, str>>>(&mut self, value: Option<T>) -> &mut Self {
|
||||||
self.title = value.map(|v| v.to_string());
|
self.title = value.map(|v| v.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||||
|
/// internal [`Cow`]s.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This is a relatively expensive operation.
|
||||||
|
#[must_use]
|
||||||
|
pub fn into_owned(self) -> ExtInf<'static> {
|
||||||
|
ExtInf {
|
||||||
|
duration: self.duration,
|
||||||
|
title: self.title.map(|v| Cow::Owned(v.into_owned())),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This tag requires [`ProtocolVersion::V1`], if the duration does not have
|
/// This tag requires [`ProtocolVersion::V1`], if the duration does not have
|
||||||
/// nanoseconds, otherwise it requires [`ProtocolVersion::V3`].
|
/// nanoseconds, otherwise it requires [`ProtocolVersion::V3`].
|
||||||
impl RequiredVersion for ExtInf {
|
impl<'a> RequiredVersion for ExtInf<'a> {
|
||||||
fn required_version(&self) -> ProtocolVersion {
|
fn required_version(&self) -> ProtocolVersion {
|
||||||
if self.duration.subsec_nanos() == 0 {
|
if self.duration.subsec_nanos() == 0 {
|
||||||
ProtocolVersion::V1
|
ProtocolVersion::V1
|
||||||
|
@ -138,7 +153,7 @@ impl RequiredVersion for ExtInf {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtInf {
|
impl<'a> fmt::Display for ExtInf<'a> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
write!(f, "{},", self.duration.as_secs_f64())?;
|
write!(f, "{},", self.duration.as_secs_f64())?;
|
||||||
|
@ -150,10 +165,10 @@ impl fmt::Display for ExtInf {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtInf {
|
impl<'a> TryFrom<&'a str> for ExtInf<'a> {
|
||||||
type Err = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||||
let mut input = tag(input, Self::PREFIX)?.splitn(2, ',');
|
let mut input = tag(input, Self::PREFIX)?.splitn(2, ',');
|
||||||
|
|
||||||
let duration = input.next().unwrap();
|
let duration = input.next().unwrap();
|
||||||
|
@ -167,13 +182,13 @@ impl FromStr for ExtInf {
|
||||||
.next()
|
.next()
|
||||||
.map(str::trim)
|
.map(str::trim)
|
||||||
.filter(|value| !value.is_empty())
|
.filter(|value| !value.is_empty())
|
||||||
.map(ToString::to_string);
|
.map(|v| Cow::Borrowed(v));
|
||||||
|
|
||||||
Ok(Self { duration, title })
|
Ok(Self { duration, title })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Duration> for ExtInf {
|
impl<'a> From<Duration> for ExtInf<'a> {
|
||||||
fn from(value: Duration) -> Self { Self::new(value) }
|
fn from(value: Duration) -> Self { Self::new(value) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,32 +221,32 @@ mod test {
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
// #EXTINF:<duration>,[<title>]
|
// #EXTINF:<duration>,[<title>]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"#EXTINF:5".parse::<ExtInf>().unwrap(),
|
ExtInf::try_from("#EXTINF:5").unwrap(),
|
||||||
ExtInf::new(Duration::from_secs(5))
|
ExtInf::new(Duration::from_secs(5))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"#EXTINF:5,".parse::<ExtInf>().unwrap(),
|
ExtInf::try_from("#EXTINF:5,").unwrap(),
|
||||||
ExtInf::new(Duration::from_secs(5))
|
ExtInf::new(Duration::from_secs(5))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"#EXTINF:5.5".parse::<ExtInf>().unwrap(),
|
ExtInf::try_from("#EXTINF:5.5").unwrap(),
|
||||||
ExtInf::new(Duration::from_millis(5500))
|
ExtInf::new(Duration::from_millis(5500))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"#EXTINF:5.5,".parse::<ExtInf>().unwrap(),
|
ExtInf::try_from("#EXTINF:5.5,").unwrap(),
|
||||||
ExtInf::new(Duration::from_millis(5500))
|
ExtInf::new(Duration::from_millis(5500))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"#EXTINF:5.5,title".parse::<ExtInf>().unwrap(),
|
ExtInf::try_from("#EXTINF:5.5,title").unwrap(),
|
||||||
ExtInf::with_title(Duration::from_millis(5500), "title")
|
ExtInf::with_title(Duration::from_millis(5500), "title")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"#EXTINF:5,title".parse::<ExtInf>().unwrap(),
|
ExtInf::try_from("#EXTINF:5,title").unwrap(),
|
||||||
ExtInf::with_title(Duration::from_secs(5), "title")
|
ExtInf::with_title(Duration::from_secs(5), "title")
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!("#EXTINF:".parse::<ExtInf>().is_err());
|
assert!(ExtInf::try_from("#EXTINF:").is_err());
|
||||||
assert!("#EXTINF:garbage".parse::<ExtInf>().is_err());
|
assert!(ExtInf::try_from("#EXTINF:garbage").is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -239,7 +254,7 @@ mod test {
|
||||||
assert_eq!(ExtInf::new(Duration::from_secs(5)).title(), &None);
|
assert_eq!(ExtInf::new(Duration::from_secs(5)).title(), &None);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtInf::with_title(Duration::from_secs(5), "title").title(),
|
ExtInf::with_title(Duration::from_secs(5), "title").title(),
|
||||||
&Some("title".to_string())
|
&Some("title".into())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use crate::types::{DecryptionKey, ProtocolVersion};
|
use crate::types::{DecryptionKey, ProtocolVersion};
|
||||||
use crate::utils::tag;
|
use crate::utils::tag;
|
||||||
|
@ -9,9 +9,9 @@ use crate::{Error, RequiredVersion};
|
||||||
///
|
///
|
||||||
/// An unencrypted segment should be marked with [`ExtXKey::empty`].
|
/// An unencrypted segment should be marked with [`ExtXKey::empty`].
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
||||||
pub struct ExtXKey(pub Option<DecryptionKey>);
|
pub struct ExtXKey<'a>(pub Option<DecryptionKey<'a>>);
|
||||||
|
|
||||||
impl ExtXKey {
|
impl<'a> ExtXKey<'a> {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-KEY:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-KEY:";
|
||||||
|
|
||||||
/// Constructs an [`ExtXKey`] tag.
|
/// Constructs an [`ExtXKey`] tag.
|
||||||
|
@ -37,7 +37,7 @@ impl ExtXKey {
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[inline]
|
#[inline]
|
||||||
pub const fn new(inner: DecryptionKey) -> Self { Self(Some(inner)) }
|
pub const fn new(inner: DecryptionKey<'a>) -> Self { Self(Some(inner)) }
|
||||||
|
|
||||||
/// Constructs an empty [`ExtXKey`], which signals that a segment is
|
/// Constructs an empty [`ExtXKey`], which signals that a segment is
|
||||||
/// unencrypted.
|
/// unencrypted.
|
||||||
|
@ -124,7 +124,7 @@ impl ExtXKey {
|
||||||
/// let decryption_key: DecryptionKey = ExtXKey::empty().unwrap(); // panics
|
/// let decryption_key: DecryptionKey = ExtXKey::empty().unwrap(); // panics
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn unwrap(self) -> DecryptionKey {
|
pub fn unwrap(self) -> DecryptionKey<'a> {
|
||||||
match self.0 {
|
match self.0 {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => panic!("called `ExtXKey::unwrap()` on an empty key"),
|
None => panic!("called `ExtXKey::unwrap()` on an empty key"),
|
||||||
|
@ -134,7 +134,7 @@ impl ExtXKey {
|
||||||
/// Returns a reference to the underlying [`DecryptionKey`].
|
/// Returns a reference to the underlying [`DecryptionKey`].
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn as_ref(&self) -> Option<&DecryptionKey> { self.0.as_ref() }
|
pub fn as_ref(&self) -> Option<&DecryptionKey<'a>> { self.0.as_ref() }
|
||||||
|
|
||||||
/// Converts an [`ExtXKey`] into an `Option<DecryptionKey>`.
|
/// Converts an [`ExtXKey`] into an `Option<DecryptionKey>`.
|
||||||
///
|
///
|
||||||
|
@ -160,7 +160,19 @@ impl ExtXKey {
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn into_option(self) -> Option<DecryptionKey> { self.0 }
|
pub fn into_option(self) -> Option<DecryptionKey<'a>> { self.0 }
|
||||||
|
|
||||||
|
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||||
|
/// internal [`Cow`]s.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This is a relatively expensive operation.
|
||||||
|
///
|
||||||
|
/// [`Cow`]: std::borrow::Cow
|
||||||
|
#[must_use]
|
||||||
|
#[inline]
|
||||||
|
pub fn into_owned(self) -> ExtXKey<'static> { ExtXKey(self.0.map(|v| v.into_owned())) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This tag requires [`ProtocolVersion::V5`], if [`KeyFormat`] or
|
/// This tag requires [`ProtocolVersion::V5`], if [`KeyFormat`] or
|
||||||
|
@ -168,7 +180,7 @@ impl ExtXKey {
|
||||||
/// specified.
|
/// specified.
|
||||||
///
|
///
|
||||||
/// Otherwise [`ProtocolVersion::V1`] is required.
|
/// Otherwise [`ProtocolVersion::V1`] is required.
|
||||||
impl RequiredVersion for ExtXKey {
|
impl<'a> RequiredVersion for ExtXKey<'a> {
|
||||||
fn required_version(&self) -> ProtocolVersion {
|
fn required_version(&self) -> ProtocolVersion {
|
||||||
self.0
|
self.0
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
@ -176,33 +188,33 @@ impl RequiredVersion for ExtXKey {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXKey {
|
impl<'a> TryFrom<&'a str> for ExtXKey<'a> {
|
||||||
type Err = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||||
let input = tag(input, Self::PREFIX)?;
|
let input = tag(input, Self::PREFIX)?;
|
||||||
|
|
||||||
if input.trim() == "METHOD=NONE" {
|
if input.trim() == "METHOD=NONE" {
|
||||||
Ok(Self(None))
|
Ok(Self(None))
|
||||||
} else {
|
} else {
|
||||||
Ok(DecryptionKey::from_str(input)?.into())
|
Ok(DecryptionKey::try_from(input)?.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Option<DecryptionKey>> for ExtXKey {
|
impl<'a> From<Option<DecryptionKey<'a>>> for ExtXKey<'a> {
|
||||||
fn from(value: Option<DecryptionKey>) -> Self { Self(value) }
|
fn from(value: Option<DecryptionKey<'a>>) -> Self { Self(value) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<DecryptionKey> for ExtXKey {
|
impl<'a> From<DecryptionKey<'a>> for ExtXKey<'a> {
|
||||||
fn from(value: DecryptionKey) -> Self { Self(Some(value)) }
|
fn from(value: DecryptionKey<'a>) -> Self { Self(Some(value)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<crate::tags::ExtXSessionKey> for ExtXKey {
|
impl<'a> From<crate::tags::ExtXSessionKey<'a>> for ExtXKey<'a> {
|
||||||
fn from(value: crate::tags::ExtXSessionKey) -> Self { Self(Some(value.0)) }
|
fn from(value: crate::tags::ExtXSessionKey<'a>) -> Self { Self(Some(value.0)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXKey {
|
impl<'a> fmt::Display for ExtXKey<'a> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
|
|
||||||
|
@ -232,7 +244,7 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
$(
|
$(
|
||||||
assert_eq!($struct, $str.parse().unwrap());
|
assert_eq!($struct, TryFrom::try_from($str).unwrap());
|
||||||
)+
|
)+
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -242,15 +254,15 @@ mod test {
|
||||||
"http://www.example.com"
|
"http://www.example.com"
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
concat!(
|
ExtXKey::try_from(concat!(
|
||||||
"#EXT-X-KEY:",
|
"#EXT-X-KEY:",
|
||||||
"METHOD=AES-128,",
|
"METHOD=AES-128,",
|
||||||
"URI=\"http://www.example.com\",",
|
"URI=\"http://www.example.com\",",
|
||||||
"UNKNOWNTAG=abcd"
|
"UNKNOWNTAG=abcd"
|
||||||
).parse().unwrap(),
|
)).unwrap(),
|
||||||
);
|
);
|
||||||
assert!("#EXT-X-KEY:METHOD=AES-128,URI=".parse::<ExtXKey>().is_err());
|
assert!(ExtXKey::try_from("#EXT-X-KEY:METHOD=AES-128,URI=").is_err());
|
||||||
assert!("garbage".parse::<ExtXKey>().is_err());
|
assert!(ExtXKey::try_from("garbage").is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::convert::{TryFrom, TryInto};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use shorthand::ShortHand;
|
use shorthand::ShortHand;
|
||||||
|
|
||||||
|
@ -33,18 +34,18 @@ use crate::{Decryptable, Error, RequiredVersion};
|
||||||
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||||
#[derive(ShortHand, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
#[derive(ShortHand, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
#[shorthand(enable(must_use, into))]
|
#[shorthand(enable(must_use, into))]
|
||||||
pub struct ExtXMap {
|
pub struct ExtXMap<'a> {
|
||||||
/// The `URI` that identifies a resource, that contains the media
|
/// The `URI` that identifies a resource, that contains the media
|
||||||
/// initialization section.
|
/// initialization section.
|
||||||
uri: String,
|
uri: Cow<'a, str>,
|
||||||
/// The range of the media initialization section.
|
/// The range of the media initialization section.
|
||||||
#[shorthand(enable(copy))]
|
#[shorthand(enable(copy))]
|
||||||
range: Option<ByteRange>,
|
range: Option<ByteRange>,
|
||||||
#[shorthand(enable(skip))]
|
#[shorthand(enable(skip))]
|
||||||
pub(crate) keys: Vec<ExtXKey>,
|
pub(crate) keys: Vec<ExtXKey<'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExtXMap {
|
impl<'a> ExtXMap<'a> {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-MAP:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-MAP:";
|
||||||
|
|
||||||
/// Makes a new [`ExtXMap`] tag.
|
/// Makes a new [`ExtXMap`] tag.
|
||||||
|
@ -55,7 +56,8 @@ impl ExtXMap {
|
||||||
/// # use hls_m3u8::tags::ExtXMap;
|
/// # use hls_m3u8::tags::ExtXMap;
|
||||||
/// let map = ExtXMap::new("https://prod.mediaspace.com/init.bin");
|
/// let map = ExtXMap::new("https://prod.mediaspace.com/init.bin");
|
||||||
/// ```
|
/// ```
|
||||||
pub fn new<T: Into<String>>(uri: T) -> Self {
|
#[must_use]
|
||||||
|
pub fn new<T: Into<Cow<'a, str>>>(uri: T) -> Self {
|
||||||
Self {
|
Self {
|
||||||
uri: uri.into(),
|
uri: uri.into(),
|
||||||
range: None,
|
range: None,
|
||||||
|
@ -71,19 +73,35 @@ impl ExtXMap {
|
||||||
/// # use hls_m3u8::tags::ExtXMap;
|
/// # use hls_m3u8::tags::ExtXMap;
|
||||||
/// use hls_m3u8::types::ByteRange;
|
/// use hls_m3u8::types::ByteRange;
|
||||||
///
|
///
|
||||||
/// ExtXMap::with_range("https://prod.mediaspace.com/init.bin", 2..11);
|
/// let map = ExtXMap::with_range("https://prod.mediaspace.com/init.bin", 2..11);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn with_range<I: Into<String>, B: Into<ByteRange>>(uri: I, range: B) -> Self {
|
#[must_use]
|
||||||
|
pub fn with_range<I: Into<Cow<'a, str>>, B: Into<ByteRange>>(uri: I, range: B) -> Self {
|
||||||
Self {
|
Self {
|
||||||
uri: uri.into(),
|
uri: uri.into(),
|
||||||
range: Some(range.into()),
|
range: Some(range.into()),
|
||||||
keys: vec![],
|
keys: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||||
|
/// internal [`Cow`]s.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This is a relatively expensive operation.
|
||||||
|
#[must_use]
|
||||||
|
pub fn into_owned(self) -> ExtXMap<'static> {
|
||||||
|
ExtXMap {
|
||||||
|
uri: Cow::Owned(self.uri.into_owned()),
|
||||||
|
range: self.range,
|
||||||
|
keys: self.keys.into_iter().map(|v| v.into_owned()).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Decryptable for ExtXMap {
|
impl<'a> Decryptable<'a> for ExtXMap<'a> {
|
||||||
fn keys(&self) -> Vec<&DecryptionKey> {
|
fn keys(&self) -> Vec<&DecryptionKey<'a>> {
|
||||||
//
|
//
|
||||||
self.keys.iter().filter_map(ExtXKey::as_ref).collect()
|
self.keys.iter().filter_map(ExtXKey::as_ref).collect()
|
||||||
}
|
}
|
||||||
|
@ -97,7 +115,7 @@ impl Decryptable for ExtXMap {
|
||||||
///
|
///
|
||||||
/// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly
|
/// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly
|
||||||
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
/// [`MediaPlaylist`]: crate::MediaPlaylist
|
||||||
impl RequiredVersion for ExtXMap {
|
impl<'a> RequiredVersion for ExtXMap<'a> {
|
||||||
// this should return ProtocolVersion::V5, if it does not contain an
|
// this should return ProtocolVersion::V5, if it does not contain an
|
||||||
// EXT-X-I-FRAMES-ONLY!
|
// EXT-X-I-FRAMES-ONLY!
|
||||||
// http://alexzambelli.com/blog/2016/05/04/understanding-hls-versions-and-client-compatibility/
|
// http://alexzambelli.com/blog/2016/05/04/understanding-hls-versions-and-client-compatibility/
|
||||||
|
@ -106,7 +124,7 @@ impl RequiredVersion for ExtXMap {
|
||||||
fn introduced_version(&self) -> ProtocolVersion { ProtocolVersion::V5 }
|
fn introduced_version(&self) -> ProtocolVersion { ProtocolVersion::V5 }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXMap {
|
impl<'a> fmt::Display for ExtXMap<'a> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
write!(f, "URI={}", quote(&self.uri))?;
|
write!(f, "URI={}", quote(&self.uri))?;
|
||||||
|
@ -119,10 +137,10 @@ impl fmt::Display for ExtXMap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXMap {
|
impl<'a> TryFrom<&'a str> for ExtXMap<'a> {
|
||||||
type Err = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||||
let input = tag(input, Self::PREFIX)?;
|
let input = tag(input, Self::PREFIX)?;
|
||||||
|
|
||||||
let mut uri = None;
|
let mut uri = None;
|
||||||
|
@ -132,7 +150,7 @@ impl FromStr for ExtXMap {
|
||||||
match key {
|
match key {
|
||||||
"URI" => uri = Some(unquote(value)),
|
"URI" => uri = Some(unquote(value)),
|
||||||
"BYTERANGE" => {
|
"BYTERANGE" => {
|
||||||
range = Some(unquote(value).parse()?);
|
range = Some(unquote(value).try_into()?);
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// [6.3.1. General Client Responsibilities]
|
// [6.3.1. General Client Responsibilities]
|
||||||
|
@ -174,18 +192,17 @@ mod test {
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXMap::new("foo"),
|
ExtXMap::new("foo"),
|
||||||
"#EXT-X-MAP:URI=\"foo\"".parse().unwrap()
|
ExtXMap::try_from("#EXT-X-MAP:URI=\"foo\"").unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXMap::with_range("foo", ByteRange::from(2..11)),
|
ExtXMap::with_range("foo", ByteRange::from(2..11)),
|
||||||
"#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\"".parse().unwrap()
|
ExtXMap::try_from("#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\"").unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXMap::with_range("foo", ByteRange::from(2..11)),
|
ExtXMap::with_range("foo", ByteRange::from(2..11)),
|
||||||
"#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\",UNKNOWN=IGNORED"
|
ExtXMap::try_from("#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\",UNKNOWN=IGNORED").unwrap()
|
||||||
.parse()
|
|
||||||
.unwrap()
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,6 +217,6 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_decryptable() {
|
fn test_decryptable() {
|
||||||
assert_eq!(ExtXMap::new("foo").keys(), Vec::<&DecryptionKey>::new());
|
assert_eq!(ExtXMap::new("foo").keys(), Vec::<&DecryptionKey<'_>>::new());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
|
#[cfg(not(feature = "chrono"))]
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
#[cfg(feature = "chrono")]
|
#[cfg(feature = "chrono")]
|
||||||
use chrono::{DateTime, FixedOffset, SecondsFormat};
|
use chrono::{DateTime, FixedOffset, SecondsFormat};
|
||||||
|
@ -27,17 +30,18 @@ use crate::{Error, RequiredVersion};
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
#[cfg_attr(feature = "chrono", derive(Deref, DerefMut, Copy))]
|
#[cfg_attr(feature = "chrono", derive(Deref, DerefMut, Copy))]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct ExtXProgramDateTime {
|
pub struct ExtXProgramDateTime<'a> {
|
||||||
/// The date-time of the first sample of the associated media segment.
|
/// The date-time of the first sample of the associated media segment.
|
||||||
#[cfg(feature = "chrono")]
|
#[cfg(feature = "chrono")]
|
||||||
#[cfg_attr(feature = "chrono", deref_mut, deref)]
|
#[cfg_attr(feature = "chrono", deref_mut, deref)]
|
||||||
pub date_time: DateTime<FixedOffset>,
|
pub date_time: DateTime<FixedOffset>,
|
||||||
/// The date-time of the first sample of the associated media segment.
|
/// The date-time of the first sample of the associated media segment.
|
||||||
#[cfg(not(feature = "chrono"))]
|
#[cfg(not(feature = "chrono"))]
|
||||||
pub date_time: String,
|
pub date_time: Cow<'a, str>,
|
||||||
|
_p: PhantomData<&'a str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExtXProgramDateTime {
|
impl<'a> ExtXProgramDateTime<'a> {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-PROGRAM-DATE-TIME:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-PROGRAM-DATE-TIME:";
|
||||||
|
|
||||||
/// Makes a new [`ExtXProgramDateTime`] tag.
|
/// Makes a new [`ExtXProgramDateTime`] tag.
|
||||||
|
@ -58,7 +62,12 @@ impl ExtXProgramDateTime {
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[cfg(feature = "chrono")]
|
#[cfg(feature = "chrono")]
|
||||||
pub const fn new(date_time: DateTime<FixedOffset>) -> Self { Self { date_time } }
|
pub const fn new(date_time: DateTime<FixedOffset>) -> Self {
|
||||||
|
Self {
|
||||||
|
date_time,
|
||||||
|
_p: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Makes a new [`ExtXProgramDateTime`] tag.
|
/// Makes a new [`ExtXProgramDateTime`] tag.
|
||||||
///
|
///
|
||||||
|
@ -69,19 +78,37 @@ impl ExtXProgramDateTime {
|
||||||
/// let program_date_time = ExtXProgramDateTime::new("2010-02-19T14:54:23.031+08:00");
|
/// let program_date_time = ExtXProgramDateTime::new("2010-02-19T14:54:23.031+08:00");
|
||||||
/// ```
|
/// ```
|
||||||
#[cfg(not(feature = "chrono"))]
|
#[cfg(not(feature = "chrono"))]
|
||||||
pub fn new<T: Into<String>>(date_time: T) -> Self {
|
pub fn new<T: Into<Cow<'a, str>>>(date_time: T) -> Self {
|
||||||
Self {
|
Self {
|
||||||
date_time: date_time.into(),
|
date_time: date_time.into(),
|
||||||
|
_p: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||||
|
/// internal [`Cow`]s.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This is a relatively expensive operation.
|
||||||
|
#[must_use]
|
||||||
|
pub fn into_owned(self) -> ExtXProgramDateTime<'static> {
|
||||||
|
ExtXProgramDateTime {
|
||||||
|
#[cfg(not(feature = "chrono"))]
|
||||||
|
date_time: Cow::Owned(self.date_time.into_owned()),
|
||||||
|
#[cfg(feature = "chrono")]
|
||||||
|
date_time: self.date_time,
|
||||||
|
_p: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This tag requires [`ProtocolVersion::V1`].
|
/// This tag requires [`ProtocolVersion::V1`].
|
||||||
impl RequiredVersion for ExtXProgramDateTime {
|
impl<'a> RequiredVersion for ExtXProgramDateTime<'a> {
|
||||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXProgramDateTime {
|
impl<'a> fmt::Display for ExtXProgramDateTime<'a> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
let date_time = {
|
let date_time = {
|
||||||
#[cfg(feature = "chrono")]
|
#[cfg(feature = "chrono")]
|
||||||
|
@ -97,10 +124,10 @@ impl fmt::Display for ExtXProgramDateTime {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXProgramDateTime {
|
impl<'a> TryFrom<&'a str> for ExtXProgramDateTime<'a> {
|
||||||
type Err = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||||
let input = tag(input, Self::PREFIX)?;
|
let input = tag(input, Self::PREFIX)?;
|
||||||
|
|
||||||
Ok(Self::new({
|
Ok(Self::new({
|
||||||
|
@ -163,8 +190,7 @@ mod test {
|
||||||
"2010-02-19T14:54:23.031+08:00"
|
"2010-02-19T14:54:23.031+08:00"
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
"#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00"
|
ExtXProgramDateTime::try_from("#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00")
|
||||||
.parse::<ExtXProgramDateTime>()
|
|
||||||
.unwrap()
|
.unwrap()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use crate::types::ProtocolVersion;
|
use crate::types::ProtocolVersion;
|
||||||
use crate::utils::tag;
|
use crate::utils::tag;
|
||||||
|
@ -25,10 +25,10 @@ impl fmt::Display for ExtXIndependentSegments {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Self::PREFIX.fmt(f) }
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { Self::PREFIX.fmt(f) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXIndependentSegments {
|
impl TryFrom<&str> for ExtXIndependentSegments {
|
||||||
type Err = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn try_from(input: &str) -> Result<Self, Self::Error> {
|
||||||
tag(input, Self::PREFIX)?;
|
tag(input, Self::PREFIX)?;
|
||||||
Ok(Self)
|
Ok(Self)
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ mod test {
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXIndependentSegments,
|
ExtXIndependentSegments,
|
||||||
"#EXT-X-INDEPENDENT-SEGMENTS".parse().unwrap(),
|
ExtXIndependentSegments::try_from("#EXT-X-INDEPENDENT-SEGMENTS").unwrap(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use shorthand::ShortHand;
|
use shorthand::ShortHand;
|
||||||
|
|
||||||
|
@ -111,10 +111,10 @@ impl fmt::Display for ExtXStart {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXStart {
|
impl TryFrom<&str> for ExtXStart {
|
||||||
type Err = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn try_from(input: &str) -> Result<Self, Self::Error> {
|
||||||
let input = tag(input, Self::PREFIX)?;
|
let input = tag(input, Self::PREFIX)?;
|
||||||
|
|
||||||
let mut time_offset = None;
|
let mut time_offset = None;
|
||||||
|
@ -176,19 +176,17 @@ mod test {
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXStart::new(Float::new(-1.23)),
|
ExtXStart::new(Float::new(-1.23)),
|
||||||
"#EXT-X-START:TIME-OFFSET=-1.23".parse().unwrap(),
|
ExtXStart::try_from("#EXT-X-START:TIME-OFFSET=-1.23").unwrap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXStart::with_precise(Float::new(1.23), true),
|
ExtXStart::with_precise(Float::new(1.23), true),
|
||||||
"#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES".parse().unwrap(),
|
ExtXStart::try_from("#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES").unwrap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ExtXStart::with_precise(Float::new(1.23), true),
|
ExtXStart::with_precise(Float::new(1.23), true),
|
||||||
"#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES,UNKNOWN=TAG"
|
ExtXStart::try_from("#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES,UNKNOWN=TAG").unwrap(),
|
||||||
.parse()
|
|
||||||
.unwrap(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,8 @@ use crate::types::{DecryptionKey, ProtocolVersion};
|
||||||
|
|
||||||
mod private {
|
mod private {
|
||||||
pub trait Sealed {}
|
pub trait Sealed {}
|
||||||
impl Sealed for crate::MediaSegment {}
|
impl<'a> Sealed for crate::MediaSegment<'a> {}
|
||||||
impl Sealed for crate::tags::ExtXMap {}
|
impl<'a> Sealed for crate::tags::ExtXMap<'a> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Signals that a type or some of the asssociated data might need to be
|
/// Signals that a type or some of the asssociated data might need to be
|
||||||
|
@ -16,7 +16,7 @@ mod private {
|
||||||
/// # Note
|
/// # Note
|
||||||
///
|
///
|
||||||
/// You are not supposed to implement this trait, therefore it is "sealed".
|
/// You are not supposed to implement this trait, therefore it is "sealed".
|
||||||
pub trait Decryptable: private::Sealed {
|
pub trait Decryptable<'a>: private::Sealed {
|
||||||
/// Returns all keys, associated with the type.
|
/// Returns all keys, associated with the type.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
@ -36,13 +36,13 @@ pub trait Decryptable: private::Sealed {
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
fn keys(&self) -> Vec<&DecryptionKey>;
|
fn keys(&self) -> Vec<&DecryptionKey<'a>>;
|
||||||
|
|
||||||
/// Most of the time only a single key is provided, so instead of iterating
|
/// Most of the time only a single key is provided, so instead of iterating
|
||||||
/// through all keys, one might as well just get the first key.
|
/// through all keys, one might as well just get the first key.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[inline]
|
#[inline]
|
||||||
fn first_key(&self) -> Option<&DecryptionKey> {
|
fn first_key(&self) -> Option<&DecryptionKey<'a>> {
|
||||||
<Self as Decryptable>::keys(self).first().copied()
|
<Self as Decryptable>::keys(self).first().copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
use core::convert::TryInto;
|
use core::convert::{TryFrom, TryInto};
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
use core::ops::{
|
use core::ops::{
|
||||||
Add, AddAssign, Bound, Range, RangeBounds, RangeInclusive, RangeTo, RangeToInclusive, Sub,
|
Add, AddAssign, Bound, Range, RangeBounds, RangeInclusive, RangeTo, RangeToInclusive, Sub,
|
||||||
SubAssign,
|
SubAssign,
|
||||||
};
|
};
|
||||||
use core::str::FromStr;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use shorthand::ShortHand;
|
use shorthand::ShortHand;
|
||||||
|
|
||||||
|
@ -408,10 +408,10 @@ impl fmt::Display for ByteRange {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ByteRange {
|
impl TryFrom<&str> for ByteRange {
|
||||||
type Err = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn try_from(input: &str) -> Result<Self, Self::Error> {
|
||||||
let mut input = input.splitn(2, '@');
|
let mut input = input.splitn(2, '@');
|
||||||
|
|
||||||
let length = input.next().unwrap();
|
let length = input.next().unwrap();
|
||||||
|
@ -431,6 +431,15 @@ impl FromStr for ByteRange {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> TryFrom<Cow<'a, str>> for ByteRange {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(input: Cow<'a, str>) -> Result<Self, Self::Error> {
|
||||||
|
//
|
||||||
|
Self::try_from(input.as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -658,20 +667,20 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
assert_eq!(ByteRange::from(2..22), "20@2".parse().unwrap());
|
assert_eq!(ByteRange::from(2..22), ByteRange::try_from("20@2").unwrap());
|
||||||
|
|
||||||
assert_eq!(ByteRange::from(..300), "300".parse().unwrap());
|
assert_eq!(ByteRange::from(..300), ByteRange::try_from("300").unwrap());
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ByteRange::from_str("a"),
|
ByteRange::try_from("a"),
|
||||||
Err(Error::parse_int("a", "a".parse::<usize>().unwrap_err()))
|
Err(Error::parse_int("a", "a".parse::<usize>().unwrap_err()))
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ByteRange::from_str("1@a"),
|
ByteRange::try_from("1@a"),
|
||||||
Err(Error::parse_int("a", "a".parse::<usize>().unwrap_err()))
|
Err(Error::parse_int("a", "a".parse::<usize>().unwrap_err()))
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!("".parse::<ByteRange>().is_err());
|
assert!(ByteRange::try_from("").is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
use core::convert::Infallible;
|
use core::convert::{Infallible, TryFrom};
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use crate::utils::{quote, unquote};
|
use crate::utils::{quote, unquote};
|
||||||
|
|
||||||
/// The identifier of a closed captions group or its absence.
|
/// The identifier of a closed captions group or its absence.
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||||
pub enum ClosedCaptions {
|
pub enum ClosedCaptions<'a> {
|
||||||
/// It indicates the set of closed-caption renditions that can be used when
|
/// It indicates the set of closed-caption renditions that can be used when
|
||||||
/// playing the presentation.
|
/// playing the presentation.
|
||||||
///
|
///
|
||||||
|
@ -18,7 +18,7 @@ pub enum ClosedCaptions {
|
||||||
/// [`ExtXMedia::group_id`]: crate::tags::ExtXMedia::group_id
|
/// [`ExtXMedia::group_id`]: crate::tags::ExtXMedia::group_id
|
||||||
/// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type
|
/// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type
|
||||||
/// [`MediaType::ClosedCaptions`]: crate::types::MediaType::ClosedCaptions
|
/// [`MediaType::ClosedCaptions`]: crate::types::MediaType::ClosedCaptions
|
||||||
GroupId(String),
|
GroupId(Cow<'a, str>),
|
||||||
/// This variant indicates that there are no closed captions in
|
/// This variant indicates that there are no closed captions in
|
||||||
/// any [`VariantStream`] in the [`MasterPlaylist`], therefore all
|
/// any [`VariantStream`] in the [`MasterPlaylist`], therefore all
|
||||||
/// [`VariantStream::ExtXStreamInf`] tags must have this attribute with a
|
/// [`VariantStream::ExtXStreamInf`] tags must have this attribute with a
|
||||||
|
@ -34,7 +34,7 @@ pub enum ClosedCaptions {
|
||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClosedCaptions {
|
impl<'a> ClosedCaptions<'a> {
|
||||||
/// Creates a [`ClosedCaptions::GroupId`] with the provided [`String`].
|
/// Creates a [`ClosedCaptions::GroupId`] with the provided [`String`].
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
@ -47,13 +47,29 @@ impl ClosedCaptions {
|
||||||
/// ClosedCaptions::GroupId("vg1".into())
|
/// ClosedCaptions::GroupId("vg1".into())
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
pub fn group_id<I: Into<String>>(value: I) -> Self {
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn group_id<I: Into<Cow<'a, str>>>(value: I) -> Self {
|
||||||
//
|
//
|
||||||
Self::GroupId(value.into())
|
Self::GroupId(value.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||||
|
/// internal [`Cow`]s.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This is a relatively expensive operation.
|
||||||
|
#[must_use]
|
||||||
|
pub fn into_owned(self) -> ClosedCaptions<'static> {
|
||||||
|
match self {
|
||||||
|
Self::GroupId(id) => ClosedCaptions::GroupId(Cow::Owned(id.into_owned())),
|
||||||
|
Self::None => ClosedCaptions::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: PartialEq<str>> PartialEq<T> for ClosedCaptions {
|
impl<'a, T: PartialEq<str>> PartialEq<T> for ClosedCaptions<'a> {
|
||||||
fn eq(&self, other: &T) -> bool {
|
fn eq(&self, other: &T) -> bool {
|
||||||
match &self {
|
match &self {
|
||||||
Self::GroupId(value) => other.eq(value),
|
Self::GroupId(value) => other.eq(value),
|
||||||
|
@ -62,7 +78,7 @@ impl<T: PartialEq<str>> PartialEq<T> for ClosedCaptions {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ClosedCaptions {
|
impl<'a> fmt::Display for ClosedCaptions<'a> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match &self {
|
match &self {
|
||||||
Self::GroupId(value) => write!(f, "{}", quote(value)),
|
Self::GroupId(value) => write!(f, "{}", quote(value)),
|
||||||
|
@ -71,10 +87,10 @@ impl fmt::Display for ClosedCaptions {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ClosedCaptions {
|
impl<'a> TryFrom<&'a str> for ClosedCaptions<'a> {
|
||||||
type Err = Infallible;
|
type Error = Infallible;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||||
if input.trim() == "NONE" {
|
if input.trim() == "NONE" {
|
||||||
Ok(Self::None)
|
Ok(Self::None)
|
||||||
} else {
|
} else {
|
||||||
|
@ -102,12 +118,12 @@ mod tests {
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ClosedCaptions::None,
|
ClosedCaptions::None,
|
||||||
"NONE".parse::<ClosedCaptions>().unwrap()
|
ClosedCaptions::try_from("NONE").unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
ClosedCaptions::GroupId("value".into()),
|
ClosedCaptions::GroupId("value".into()),
|
||||||
"\"value\"".parse::<ClosedCaptions>().unwrap()
|
ClosedCaptions::try_from("\"value\"").unwrap()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
use core::convert::TryFrom;
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
use core::str::FromStr;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use derive_more::{AsMut, AsRef, Deref, DerefMut};
|
use derive_more::{AsMut, AsRef, Deref, DerefMut};
|
||||||
|
|
||||||
|
@ -25,11 +26,11 @@ use crate::Error;
|
||||||
#[derive(
|
#[derive(
|
||||||
AsMut, AsRef, Deref, DerefMut, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default,
|
AsMut, AsRef, Deref, DerefMut, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default,
|
||||||
)]
|
)]
|
||||||
pub struct Codecs {
|
pub struct Codecs<'a> {
|
||||||
list: Vec<String>,
|
list: Vec<Cow<'a, str>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Codecs {
|
impl<'a> Codecs<'a> {
|
||||||
/// Makes a new (empty) [`Codecs`] struct.
|
/// Makes a new (empty) [`Codecs`] struct.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
@ -41,9 +42,82 @@ impl Codecs {
|
||||||
#[inline]
|
#[inline]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn new() -> Self { Self { list: Vec::new() } }
|
pub const fn new() -> Self { Self { list: Vec::new() } }
|
||||||
|
|
||||||
|
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||||
|
/// internal [`Cow`]s.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This is a relatively expensive operation.
|
||||||
|
#[must_use]
|
||||||
|
pub fn into_owned(self) -> Codecs<'static> {
|
||||||
|
Codecs {
|
||||||
|
list: self
|
||||||
|
.list
|
||||||
|
.into_iter()
|
||||||
|
.map(|v| Cow::Owned(v.into_owned()))
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Codecs {
|
impl<'a, T> From<Vec<T>> for Codecs<'a>
|
||||||
|
where
|
||||||
|
T: Into<Cow<'a, str>>,
|
||||||
|
{
|
||||||
|
fn from(value: Vec<T>) -> Self {
|
||||||
|
Self {
|
||||||
|
list: value.into_iter().map(|v| v.into()).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: this should be implemented with const generics in the future!
|
||||||
|
macro_rules! implement_from {
|
||||||
|
($($size:expr),*) => {
|
||||||
|
$(
|
||||||
|
impl<'a> From<[&'a str; $size]> for Codecs<'a> {
|
||||||
|
fn from(value: [&'a str; $size]) -> Self {
|
||||||
|
Self {
|
||||||
|
list: {
|
||||||
|
let mut result = Vec::with_capacity($size);
|
||||||
|
|
||||||
|
for i in 0..$size {
|
||||||
|
result.push(Cow::Borrowed(value[i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&[&'a str; $size]> for Codecs<'a> {
|
||||||
|
fn from(value: &[&'a str; $size]) -> Self {
|
||||||
|
Self {
|
||||||
|
list: {
|
||||||
|
let mut result = Vec::with_capacity($size);
|
||||||
|
|
||||||
|
for i in 0..$size {
|
||||||
|
result.push(Cow::Borrowed(value[i]))
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
implement_from!(
|
||||||
|
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
|
||||||
|
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F,
|
||||||
|
0x20
|
||||||
|
);
|
||||||
|
|
||||||
|
impl<'a> fmt::Display for Codecs<'a> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
if let Some(codec) = self.list.iter().next() {
|
if let Some(codec) = self.list.iter().next() {
|
||||||
write!(f, "{}", codec)?;
|
write!(f, "{}", codec)?;
|
||||||
|
@ -56,20 +130,24 @@ impl fmt::Display for Codecs {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl FromStr for Codecs {
|
|
||||||
type Err = Error;
|
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
impl<'a> TryFrom<&'a str> for Codecs<'a> {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
list: input.split(',').map(|s| s.into()).collect(),
|
list: input.split(',').map(|s| s.into()).collect(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: AsRef<str>, I: IntoIterator<Item = T>> From<I> for Codecs {
|
impl<'a> TryFrom<Cow<'a, str>> for Codecs<'a> {
|
||||||
fn from(value: I) -> Self {
|
type Error = Error;
|
||||||
Self {
|
|
||||||
list: value.into_iter().map(|s| s.as_ref().to_string()).collect(),
|
fn try_from(input: Cow<'a, str>) -> Result<Self, Self::Error> {
|
||||||
|
match input {
|
||||||
|
Cow::Owned(o) => Ok(Codecs::try_from(o.as_str())?.into_owned()),
|
||||||
|
Cow::Borrowed(b) => Self::try_from(b),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -86,7 +164,7 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_display() {
|
fn test_display() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Codecs::from(vec!["mp4a.40.2", "avc1.4d401e"]).to_string(),
|
Codecs::from(["mp4a.40.2", "avc1.4d401e"]).to_string(),
|
||||||
"mp4a.40.2,avc1.4d401e".to_string()
|
"mp4a.40.2,avc1.4d401e".to_string()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -94,8 +172,8 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Codecs::from_str("mp4a.40.2,avc1.4d401e").unwrap(),
|
Codecs::try_from("mp4a.40.2,avc1.4d401e").unwrap(),
|
||||||
Codecs::from(vec!["mp4a.40.2", "avc1.4d401e"])
|
Codecs::from(["mp4a.40.2", "avc1.4d401e"])
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use derive_builder::Builder;
|
use derive_builder::Builder;
|
||||||
use shorthand::ShortHand;
|
use shorthand::ShortHand;
|
||||||
|
@ -16,7 +17,7 @@ use crate::{Error, RequiredVersion};
|
||||||
#[builder(setter(into), build_fn(validate = "Self::validate"))]
|
#[builder(setter(into), build_fn(validate = "Self::validate"))]
|
||||||
#[shorthand(enable(skip, must_use, into))]
|
#[shorthand(enable(skip, must_use, into))]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub struct DecryptionKey {
|
pub struct DecryptionKey<'a> {
|
||||||
/// The encryption method, which has been used to encrypt the data.
|
/// The encryption method, which has been used to encrypt the data.
|
||||||
///
|
///
|
||||||
/// An [`EncryptionMethod::Aes128`] signals that the data is encrypted using
|
/// An [`EncryptionMethod::Aes128`] signals that the data is encrypted using
|
||||||
|
@ -45,7 +46,7 @@ pub struct DecryptionKey {
|
||||||
/// This field is required.
|
/// This field is required.
|
||||||
#[builder(setter(into, strip_option), default)]
|
#[builder(setter(into, strip_option), default)]
|
||||||
#[shorthand(disable(skip))]
|
#[shorthand(disable(skip))]
|
||||||
pub(crate) uri: String,
|
pub(crate) uri: Cow<'a, str>,
|
||||||
/// An initialization vector (IV) is a fixed size input that can be used
|
/// An initialization vector (IV) is a fixed size input that can be used
|
||||||
/// along with a secret key for data encryption.
|
/// along with a secret key for data encryption.
|
||||||
///
|
///
|
||||||
|
@ -80,7 +81,7 @@ pub struct DecryptionKey {
|
||||||
pub versions: Option<KeyFormatVersions>,
|
pub versions: Option<KeyFormatVersions>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DecryptionKey {
|
impl<'a> DecryptionKey<'a> {
|
||||||
/// Creates a new `DecryptionKey` from an uri pointing to the key data and
|
/// Creates a new `DecryptionKey` from an uri pointing to the key data and
|
||||||
/// an `EncryptionMethod`.
|
/// an `EncryptionMethod`.
|
||||||
///
|
///
|
||||||
|
@ -94,7 +95,7 @@ impl DecryptionKey {
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn new<I: Into<String>>(method: EncryptionMethod, uri: I) -> Self {
|
pub fn new<I: Into<Cow<'a, str>>>(method: EncryptionMethod, uri: I) -> Self {
|
||||||
Self {
|
Self {
|
||||||
method,
|
method,
|
||||||
uri: uri.into(),
|
uri: uri.into(),
|
||||||
|
@ -125,7 +126,24 @@ impl DecryptionKey {
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn builder() -> DecryptionKeyBuilder { DecryptionKeyBuilder::default() }
|
pub fn builder() -> DecryptionKeyBuilder<'a> { DecryptionKeyBuilder::default() }
|
||||||
|
|
||||||
|
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||||
|
/// internal [`Cow`]s.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This is a relatively expensive operation.
|
||||||
|
#[must_use]
|
||||||
|
pub fn into_owned(self) -> DecryptionKey<'static> {
|
||||||
|
DecryptionKey {
|
||||||
|
method: self.method,
|
||||||
|
uri: Cow::Owned(self.uri.into_owned()),
|
||||||
|
iv: self.iv,
|
||||||
|
format: self.format,
|
||||||
|
versions: self.versions,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This tag requires [`ProtocolVersion::V5`], if [`KeyFormat`] or
|
/// This tag requires [`ProtocolVersion::V5`], if [`KeyFormat`] or
|
||||||
|
@ -133,7 +151,7 @@ impl DecryptionKey {
|
||||||
/// specified.
|
/// specified.
|
||||||
///
|
///
|
||||||
/// Otherwise [`ProtocolVersion::V1`] is required.
|
/// Otherwise [`ProtocolVersion::V1`] is required.
|
||||||
impl RequiredVersion for DecryptionKey {
|
impl<'a> RequiredVersion for DecryptionKey<'a> {
|
||||||
fn required_version(&self) -> ProtocolVersion {
|
fn required_version(&self) -> ProtocolVersion {
|
||||||
if self.format.is_some() || self.versions.is_some() {
|
if self.format.is_some() || self.versions.is_some() {
|
||||||
ProtocolVersion::V5
|
ProtocolVersion::V5
|
||||||
|
@ -145,10 +163,10 @@ impl RequiredVersion for DecryptionKey {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for DecryptionKey {
|
impl<'a> TryFrom<&'a str> for DecryptionKey<'a> {
|
||||||
type Err = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||||
let mut method = None;
|
let mut method = None;
|
||||||
let mut uri = None;
|
let mut uri = None;
|
||||||
let mut iv = None;
|
let mut iv = None;
|
||||||
|
@ -190,7 +208,7 @@ impl FromStr for DecryptionKey {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for DecryptionKey {
|
impl<'a> fmt::Display for DecryptionKey<'a> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "METHOD={},URI={}", self.method, quote(&self.uri))?;
|
write!(f, "METHOD={},URI={}", self.method, quote(&self.uri))?;
|
||||||
|
|
||||||
|
@ -212,7 +230,7 @@ impl fmt::Display for DecryptionKey {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DecryptionKeyBuilder {
|
impl<'a> DecryptionKeyBuilder<'a> {
|
||||||
fn validate(&self) -> Result<(), String> {
|
fn validate(&self) -> Result<(), String> {
|
||||||
// a decryption key must contain a uri and a method
|
// a decryption key must contain a uri and a method
|
||||||
if self.method.is_none() {
|
if self.method.is_none() {
|
||||||
|
@ -243,19 +261,19 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
$(
|
$(
|
||||||
assert_eq!($struct, $str.parse().unwrap());
|
assert_eq!($struct, TryFrom::try_from($str).unwrap());
|
||||||
)+
|
)+
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
DecryptionKey::new(EncryptionMethod::Aes128, "http://www.example.com"),
|
DecryptionKey::new(EncryptionMethod::Aes128, "http://www.example.com"),
|
||||||
concat!(
|
DecryptionKey::try_from(concat!(
|
||||||
"METHOD=AES-128,",
|
"METHOD=AES-128,",
|
||||||
"URI=\"http://www.example.com\",",
|
"URI=\"http://www.example.com\",",
|
||||||
"UNKNOWNTAG=abcd"
|
"UNKNOWNTAG=abcd"
|
||||||
).parse().unwrap(),
|
)).unwrap(),
|
||||||
);
|
);
|
||||||
assert!("METHOD=AES-128,URI=".parse::<DecryptionKey>().is_err());
|
assert!(DecryptionKey::try_from("METHOD=AES-128,URI=").is_err());
|
||||||
assert!("garbage".parse::<DecryptionKey>().is_err());
|
assert!(DecryptionKey::try_from("garbage").is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use crate::types::ProtocolVersion;
|
use crate::types::ProtocolVersion;
|
||||||
use crate::utils::tag;
|
use crate::utils::tag;
|
||||||
|
@ -43,10 +43,10 @@ impl fmt::Display for PlaylistType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for PlaylistType {
|
impl TryFrom<&str> for PlaylistType {
|
||||||
type Err = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn try_from(input: &str) -> Result<Self, Self::Error> {
|
||||||
let input = tag(input, Self::PREFIX)?;
|
let input = tag(input, Self::PREFIX)?;
|
||||||
match input {
|
match input {
|
||||||
"EVENT" => Ok(Self::Event),
|
"EVENT" => Ok(Self::Event),
|
||||||
|
@ -64,20 +64,18 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"#EXT-X-PLAYLIST-TYPE:VOD".parse::<PlaylistType>().unwrap(),
|
PlaylistType::try_from("#EXT-X-PLAYLIST-TYPE:VOD").unwrap(),
|
||||||
PlaylistType::Vod,
|
PlaylistType::Vod,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"#EXT-X-PLAYLIST-TYPE:EVENT"
|
PlaylistType::try_from("#EXT-X-PLAYLIST-TYPE:EVENT").unwrap(),
|
||||||
.parse::<PlaylistType>()
|
|
||||||
.unwrap(),
|
|
||||||
PlaylistType::Event,
|
PlaylistType::Event,
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!("#EXT-X-PLAYLIST-TYPE:H".parse::<PlaylistType>().is_err());
|
assert!(PlaylistType::try_from("#EXT-X-PLAYLIST-TYPE:H").is_err());
|
||||||
|
|
||||||
assert!("garbage".parse::<PlaylistType>().is_err());
|
assert!(PlaylistType::try_from("garbage").is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
use core::convert::TryFrom;
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
use core::str::FromStr;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use derive_builder::Builder;
|
use derive_builder::Builder;
|
||||||
use shorthand::ShortHand;
|
use shorthand::ShortHand;
|
||||||
|
@ -17,7 +18,7 @@ use crate::{Error, RequiredVersion};
|
||||||
#[builder(setter(strip_option))]
|
#[builder(setter(strip_option))]
|
||||||
#[builder(derive(Debug, PartialEq, PartialOrd, Ord, Eq, Hash))]
|
#[builder(derive(Debug, PartialEq, PartialOrd, Ord, Eq, Hash))]
|
||||||
#[shorthand(enable(must_use, into))]
|
#[shorthand(enable(must_use, into))]
|
||||||
pub struct StreamData {
|
pub struct StreamData<'a> {
|
||||||
/// The peak segment bitrate of the [`VariantStream`] in bits per second.
|
/// The peak segment bitrate of the [`VariantStream`] in bits per second.
|
||||||
///
|
///
|
||||||
/// If all the [`MediaSegment`]s in a [`VariantStream`] have already been
|
/// If all the [`MediaSegment`]s in a [`VariantStream`] have already been
|
||||||
|
@ -133,7 +134,7 @@ pub struct StreamData {
|
||||||
/// crate::tags::VariantStream::ExtXStreamInf
|
/// crate::tags::VariantStream::ExtXStreamInf
|
||||||
/// [RFC6381]: https://tools.ietf.org/html/rfc6381
|
/// [RFC6381]: https://tools.ietf.org/html/rfc6381
|
||||||
#[builder(default, setter(into))]
|
#[builder(default, setter(into))]
|
||||||
codecs: Option<Codecs>,
|
codecs: Option<Codecs<'a>>,
|
||||||
/// The resolution of the stream.
|
/// The resolution of the stream.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
@ -198,7 +199,7 @@ pub struct StreamData {
|
||||||
/// let mut stream = StreamData::new(20);
|
/// let mut stream = StreamData::new(20);
|
||||||
///
|
///
|
||||||
/// stream.set_video(Some("video_01"));
|
/// stream.set_video(Some("video_01"));
|
||||||
/// assert_eq!(stream.video(), Some(&"video_01".to_string()));
|
/// assert_eq!(stream.video(), Some(&"video_01".into()));
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// # Note
|
/// # Note
|
||||||
|
@ -210,10 +211,10 @@ pub struct StreamData {
|
||||||
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
/// [`MasterPlaylist`]: crate::MasterPlaylist
|
||||||
/// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type
|
/// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type
|
||||||
#[builder(default, setter(into))]
|
#[builder(default, setter(into))]
|
||||||
video: Option<String>,
|
video: Option<Cow<'a, str>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StreamData {
|
impl<'a> StreamData<'a> {
|
||||||
/// Creates a new [`StreamData`].
|
/// Creates a new [`StreamData`].
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
|
@ -253,10 +254,28 @@ impl StreamData {
|
||||||
/// # Ok::<(), Box<dyn ::std::error::Error>>(())
|
/// # Ok::<(), Box<dyn ::std::error::Error>>(())
|
||||||
/// ```
|
/// ```
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn builder() -> StreamDataBuilder { StreamDataBuilder::default() }
|
pub fn builder() -> StreamDataBuilder<'a> { StreamDataBuilder::default() }
|
||||||
|
|
||||||
|
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||||
|
/// internal [`Cow`]s.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This is a relatively expensive operation.
|
||||||
|
#[must_use]
|
||||||
|
pub fn into_owned(self) -> StreamData<'static> {
|
||||||
|
StreamData {
|
||||||
|
bandwidth: self.bandwidth,
|
||||||
|
average_bandwidth: self.average_bandwidth,
|
||||||
|
codecs: self.codecs.map(|v| v.into_owned()),
|
||||||
|
resolution: self.resolution,
|
||||||
|
hdcp_level: self.hdcp_level,
|
||||||
|
video: self.video.map(|v| Cow::Owned(v.into_owned())),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for StreamData {
|
impl<'a> fmt::Display for StreamData<'a> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "BANDWIDTH={}", self.bandwidth)?;
|
write!(f, "BANDWIDTH={}", self.bandwidth)?;
|
||||||
|
|
||||||
|
@ -279,10 +298,10 @@ impl fmt::Display for StreamData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for StreamData {
|
impl<'a> TryFrom<&'a str> for StreamData<'a> {
|
||||||
type Err = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||||
let mut bandwidth = None;
|
let mut bandwidth = None;
|
||||||
let mut average_bandwidth = None;
|
let mut average_bandwidth = None;
|
||||||
let mut codecs = None;
|
let mut codecs = None;
|
||||||
|
@ -306,7 +325,7 @@ impl FromStr for StreamData {
|
||||||
.map_err(|e| Error::parse_int(value, e))?,
|
.map_err(|e| Error::parse_int(value, e))?,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
"CODECS" => codecs = Some(unquote(value).parse()?),
|
"CODECS" => codecs = Some(TryFrom::try_from(unquote(value))?),
|
||||||
"RESOLUTION" => resolution = Some(value.parse()?),
|
"RESOLUTION" => resolution = Some(value.parse()?),
|
||||||
"HDCP-LEVEL" => {
|
"HDCP-LEVEL" => {
|
||||||
hdcp_level = Some(value.parse::<HdcpLevel>().map_err(Error::strum)?)
|
hdcp_level = Some(value.parse::<HdcpLevel>().map_err(Error::strum)?)
|
||||||
|
@ -334,7 +353,7 @@ impl FromStr for StreamData {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This struct requires [`ProtocolVersion::V1`].
|
/// This struct requires [`ProtocolVersion::V1`].
|
||||||
impl RequiredVersion for StreamData {
|
impl<'a> RequiredVersion for StreamData<'a> {
|
||||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||||
|
|
||||||
fn introduced_version(&self) -> ProtocolVersion {
|
fn introduced_version(&self) -> ProtocolVersion {
|
||||||
|
@ -385,18 +404,17 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
stream_data,
|
stream_data,
|
||||||
concat!(
|
StreamData::try_from(concat!(
|
||||||
"BANDWIDTH=200,",
|
"BANDWIDTH=200,",
|
||||||
"AVERAGE-BANDWIDTH=15,",
|
"AVERAGE-BANDWIDTH=15,",
|
||||||
"CODECS=\"mp4a.40.2,avc1.4d401e\",",
|
"CODECS=\"mp4a.40.2,avc1.4d401e\",",
|
||||||
"RESOLUTION=1920x1080,",
|
"RESOLUTION=1920x1080,",
|
||||||
"HDCP-LEVEL=TYPE-0,",
|
"HDCP-LEVEL=TYPE-0,",
|
||||||
"VIDEO=\"video\""
|
"VIDEO=\"video\""
|
||||||
)
|
))
|
||||||
.parse()
|
|
||||||
.unwrap()
|
.unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!("garbage".parse::<StreamData>().is_err());
|
assert!(StreamData::try_from("garbage").is_err());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
use crate::types::Float;
|
use crate::types::Float;
|
||||||
use crate::utils::{quote, unquote};
|
use crate::utils::{quote, unquote};
|
||||||
|
@ -8,16 +9,33 @@ use crate::Error;
|
||||||
/// A `Value`.
|
/// A `Value`.
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
|
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
|
||||||
pub enum Value {
|
pub enum Value<'a> {
|
||||||
/// A `String`.
|
/// A `String`.
|
||||||
String(String),
|
String(Cow<'a, str>),
|
||||||
/// A sequence of bytes.
|
/// A sequence of bytes.
|
||||||
Hex(Vec<u8>),
|
Hex(Vec<u8>),
|
||||||
/// A floating point number, that's neither NaN nor infinite.
|
/// A floating point number, that's neither NaN nor infinite.
|
||||||
Float(Float),
|
Float(Float),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Value {
|
impl<'a> Value<'a> {
|
||||||
|
/// Makes the struct independent of its lifetime, by taking ownership of all
|
||||||
|
/// internal [`Cow`]s.
|
||||||
|
///
|
||||||
|
/// # Note
|
||||||
|
///
|
||||||
|
/// This is a relatively expensive operation.
|
||||||
|
#[must_use]
|
||||||
|
pub fn into_owned(self) -> Value<'static> {
|
||||||
|
match self {
|
||||||
|
Self::String(value) => Value::String(Cow::Owned(value.into_owned())),
|
||||||
|
Self::Hex(value) => Value::Hex(value),
|
||||||
|
Self::Float(value) => Value::Float(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> fmt::Display for Value<'a> {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match &self {
|
match &self {
|
||||||
Self::String(value) => write!(f, "{}", quote(value)),
|
Self::String(value) => write!(f, "{}", quote(value)),
|
||||||
|
@ -27,10 +45,10 @@ impl fmt::Display for Value {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for Value {
|
impl<'a> TryFrom<&'a str> for Value<'a> {
|
||||||
type Err = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
|
||||||
if input.starts_with("0x") || input.starts_with("0X") {
|
if input.starts_with("0x") || input.starts_with("0X") {
|
||||||
Ok(Self::Hex(
|
Ok(Self::Hex(
|
||||||
hex::decode(input.trim_start_matches("0x").trim_start_matches("0X"))
|
hex::decode(input.trim_start_matches("0x").trim_start_matches("0X"))
|
||||||
|
@ -45,20 +63,16 @@ impl FromStr for Value {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Into<Float>> From<T> for Value {
|
impl<T: Into<Float>> From<T> for Value<'static> {
|
||||||
fn from(value: T) -> Self { Self::Float(value.into()) }
|
fn from(value: T) -> Self { Self::Float(value.into()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Vec<u8>> for Value {
|
impl From<Vec<u8>> for Value<'static> {
|
||||||
fn from(value: Vec<u8>) -> Self { Self::Hex(value) }
|
fn from(value: Vec<u8>) -> Self { Self::Hex(value) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<String> for Value {
|
impl From<String> for Value<'static> {
|
||||||
fn from(value: String) -> Self { Self::String(unquote(value)) }
|
fn from(value: String) -> Self { Self::String(Cow::Owned(unquote(&value).into_owned())) }
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&str> for Value {
|
|
||||||
fn from(value: &str) -> Self { Self::String(unquote(value)) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -70,7 +84,7 @@ mod tests {
|
||||||
fn test_display() {
|
fn test_display() {
|
||||||
assert_eq!(Value::Float(Float::new(1.1)).to_string(), "1.1".to_string());
|
assert_eq!(Value::Float(Float::new(1.1)).to_string(), "1.1".to_string());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Value::String("&str".to_string()).to_string(),
|
Value::String("&str".into()).to_string(),
|
||||||
"\"&str\"".to_string()
|
"\"&str\"".to_string()
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -81,23 +95,31 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
assert_eq!(Value::Float(Float::new(1.1)), "1.1".parse().unwrap());
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Value::String("&str".to_string()),
|
Value::Float(Float::new(1.1)),
|
||||||
"\"&str\"".parse().unwrap()
|
Value::try_from("1.1").unwrap()
|
||||||
);
|
);
|
||||||
assert_eq!(Value::Hex(vec![1, 2, 3]), "0x010203".parse().unwrap());
|
assert_eq!(
|
||||||
assert_eq!(Value::Hex(vec![1, 2, 3]), "0X010203".parse().unwrap());
|
Value::String("&str".into()),
|
||||||
assert!("0x010203Z".parse::<Value>().is_err());
|
Value::try_from("\"&str\"").unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Value::Hex(vec![1, 2, 3]),
|
||||||
|
Value::try_from("0x010203").unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Value::Hex(vec![1, 2, 3]),
|
||||||
|
Value::try_from("0X010203").unwrap()
|
||||||
|
);
|
||||||
|
assert!(Value::try_from("0x010203Z").is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_from() {
|
fn test_from() {
|
||||||
assert_eq!(Value::from(1_u8), Value::Float(Float::new(1.0)));
|
assert_eq!(Value::from(1_u8), Value::Float(Float::new(1.0)));
|
||||||
assert_eq!(Value::from("\"&str\""), Value::String("&str".to_string()));
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Value::from("&str".to_string()),
|
Value::from("&str".to_string()),
|
||||||
Value::String("&str".to_string())
|
Value::String("&str".into())
|
||||||
);
|
);
|
||||||
assert_eq!(Value::from(vec![1, 2, 3]), Value::Hex(vec![1, 2, 3]));
|
assert_eq!(Value::from(vec![1, 2, 3]), Value::Hex(vec![1, 2, 3]));
|
||||||
}
|
}
|
||||||
|
|
23
src/utils.rs
23
src/utils.rs
|
@ -1,5 +1,7 @@
|
||||||
use crate::Error;
|
|
||||||
use core::iter;
|
use core::iter;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use crate::Error;
|
||||||
|
|
||||||
/// This is an extension trait that adds the below method to `bool`.
|
/// This is an extension trait that adds the below method to `bool`.
|
||||||
/// Those methods are already planned for the standard library, but are not
|
/// Those methods are already planned for the standard library, but are not
|
||||||
|
@ -67,12 +69,25 @@ pub(crate) fn parse_yes_or_no<T: AsRef<str>>(s: T) -> crate::Result<bool> {
|
||||||
///
|
///
|
||||||
/// Therefore it is safe to simply remove any occurence of those characters.
|
/// Therefore it is safe to simply remove any occurence of those characters.
|
||||||
/// [rfc8216#section-4.2](https://tools.ietf.org/html/rfc8216#section-4.2)
|
/// [rfc8216#section-4.2](https://tools.ietf.org/html/rfc8216#section-4.2)
|
||||||
pub(crate) fn unquote<T: AsRef<str>>(value: T) -> String {
|
pub(crate) fn unquote(value: &str) -> Cow<'_, str> {
|
||||||
|
if value.starts_with('"') && value.ends_with('"') {
|
||||||
|
let result = Cow::Borrowed(&value[1..value.len() - 1]);
|
||||||
|
|
||||||
|
if result
|
||||||
|
.chars()
|
||||||
|
.find(|c| *c == '"' || *c == '\n' || *c == '\r')
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Cow::Owned(
|
||||||
value
|
value
|
||||||
.as_ref()
|
|
||||||
.chars()
|
.chars()
|
||||||
.filter(|c| *c != '"' && *c != '\n' && *c != '\r')
|
.filter(|c| *c != '"' && *c != '\n' && *c != '\r')
|
||||||
.collect()
|
.collect(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Puts a string inside quotes.
|
/// Puts a string inside quotes.
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
use hls_m3u8::tags::{ExtXMedia, VariantStream};
|
use hls_m3u8::tags::{ExtXMedia, VariantStream};
|
||||||
use hls_m3u8::types::{MediaType, StreamData};
|
use hls_m3u8::types::{MediaType, StreamData};
|
||||||
use hls_m3u8::MasterPlaylist;
|
use hls_m3u8::MasterPlaylist;
|
||||||
|
@ -9,7 +11,7 @@ macro_rules! generate_tests {
|
||||||
$(
|
$(
|
||||||
#[test]
|
#[test]
|
||||||
fn $fnname() {
|
fn $fnname() {
|
||||||
assert_eq!($struct, $str.parse().unwrap());
|
assert_eq!($struct, TryFrom::try_from($str).unwrap());
|
||||||
|
|
||||||
assert_eq!($struct.to_string(), $str.to_string());
|
assert_eq!($struct.to_string(), $str.to_string());
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
//!
|
//!
|
||||||
//! TODO: the rest of the tests
|
//! TODO: the rest of the tests
|
||||||
|
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use hls_m3u8::tags::{ExtInf, ExtXByteRange};
|
use hls_m3u8::tags::{ExtInf, ExtXByteRange};
|
||||||
|
@ -15,7 +16,7 @@ macro_rules! generate_tests {
|
||||||
$(
|
$(
|
||||||
#[test]
|
#[test]
|
||||||
fn $fnname() {
|
fn $fnname() {
|
||||||
assert_eq!($struct, $str.parse().unwrap());
|
assert_eq!($struct, TryFrom::try_from($str).unwrap());
|
||||||
|
|
||||||
assert_eq!($struct.to_string(), $str.to_string());
|
assert_eq!($struct.to_string(), $str.to_string());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
// https://tools.ietf.org/html/rfc8216#section-8
|
// https://tools.ietf.org/html/rfc8216#section-8
|
||||||
|
use std::convert::TryFrom;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use hls_m3u8::tags::{ExtInf, ExtXKey, ExtXMedia, VariantStream};
|
use hls_m3u8::tags::{ExtInf, ExtXKey, ExtXMedia, VariantStream};
|
||||||
|
@ -11,7 +12,7 @@ macro_rules! generate_tests {
|
||||||
$(
|
$(
|
||||||
#[test]
|
#[test]
|
||||||
fn $fnname() {
|
fn $fnname() {
|
||||||
assert_eq!($struct, $str.parse().unwrap());
|
assert_eq!($struct, TryFrom::try_from($str).unwrap());
|
||||||
|
|
||||||
assert_eq!($struct.to_string(), $str.to_string());
|
assert_eq!($struct.to_string(), $str.to_string());
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue