1
0
Fork 0
mirror of https://github.com/sile/hls_m3u8.git synced 2024-11-24 08:00:59 +00:00

Merge branch 'master' into 0.3.1

This commit is contained in:
Lucas 2020-04-25 09:54:01 +02:00 committed by GitHub
commit 85df5c94ad
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 1217 additions and 646 deletions

15
CHANGELOG.md Normal file
View file

@ -0,0 +1,15 @@
# hls_m3u8
## {next}
* Performance improvements:
+ Changed `MediaPlaylist::segments` from `BTreeMap<usize, MediaSegment>`
to `StableVec<MediaSegment>`
+ Added `perf` feature, which can be used to improve performance in the future
+ Changed all instances of `String` to `Cow<'a, str>` to reduce `Clone`-ing.
* Most structs now implement [`TryFrom<&'a str>`][TryFrom] instead of [`FromStr`][FromStr].
[TryFrom]: https://doc.rust-lang.org/std/convert/trait.TryFrom.html
[FromStr]: https://doc.rust-lang.org/std/str/trait.FromStr.html

View file

@ -9,10 +9,12 @@ readme = "README.md"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
keywords = ["hls", "m3u8"] keywords = ["hls", "m3u8"]
edition = "2018" edition = "2018"
categories = ["parser"] categories = ["parser-implementations"]
[features] [features]
default = [] default = []
perf = []
[badges] [badges]
codecov = { repository = "sile/hls_m3u8" } codecov = { repository = "sile/hls_m3u8" }
travis-ci = { repository = "sile/hls_m3u8" } travis-ci = { repository = "sile/hls_m3u8" }
@ -29,7 +31,14 @@ 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" }
[dev-dependencies] [dev-dependencies]
pretty_assertions = "0.6" pretty_assertions = "0.6"
version-sync = "0.9" version-sync = "0.9"
automod = "0.2" automod = "0.2"
criterion = "0.3.1"
[[bench]]
name = "bench_main"
harness = false

7
benches/bench_main.rs Normal file
View file

@ -0,0 +1,7 @@
use criterion::criterion_main;
mod benchmarks;
criterion_main! {
benchmarks::media_playlist::benches,
}

View file

@ -0,0 +1,90 @@
use std::convert::TryFrom;
use std::str::FromStr;
use std::time::Duration;
use criterion::{black_box, criterion_group, Criterion, Throughput};
use hls_m3u8::tags::{ExtXDateRange, ExtXProgramDateTime};
use hls_m3u8::types::Value;
use hls_m3u8::{MediaPlaylist, MediaSegment};
fn create_manifest_data() -> Vec<u8> {
let mut builder = MediaPlaylist::builder();
builder.media_sequence(826176645);
builder.has_independent_segments(true);
builder.target_duration(Duration::from_secs(2));
for i in 0..4000 {
let mut seg = MediaSegment::builder();
seg.duration(Duration::from_secs_f64(1.92)).uri(format!(
"avc_unencrypted_global-video=3000000-{}.ts?variant=italy",
826176659 + i
));
if i == 0 {
seg.program_date_time(ExtXProgramDateTime::new("2020-04-07T11:32:38Z"));
}
if i % 100 == 0 {
seg.date_range(
ExtXDateRange::builder()
.id(format!("date_id_{}", i / 100))
.start_date("2020-04-07T11:40:02.040000Z")
.duration(Duration::from_secs_f64(65.2))
.insert_client_attribute(
"SCTE35-OUT",
Value::Hex(
hex::decode(concat!(
"FC30250000",
"0000000000",
"FFF0140500",
"001C207FEF",
"FE0030E3A0",
"FE005989E0",
"0001000000",
"0070BA5ABF"
))
.unwrap(),
),
)
.build()
.unwrap(),
);
}
builder.push_segment(seg.build().unwrap());
}
builder.build().unwrap().to_string().into_bytes()
}
fn media_playlist_from_str(c: &mut Criterion) {
let data = String::from_utf8(create_manifest_data()).unwrap();
let mut group = c.benchmark_group("MediaPlaylist::from_str");
group.throughput(Throughput::Bytes(data.len() as u64));
group.bench_function("MediaPlaylist::from_str", |b| {
b.iter(|| MediaPlaylist::from_str(black_box(&data)).unwrap());
});
group.finish();
}
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);

View file

@ -0,0 +1 @@
pub mod media_playlist;

View file

@ -21,17 +21,16 @@ impl<'a> Iterator for AttributePairs<'a> {
// the position in the string: // the position in the string:
let start = self.index; let start = self.index;
// the key ends at an `=`: // the key ends at an `=`:
let end = self let end = self.string[self.index..]
.string
.char_indices() .char_indices()
.skip_while(|(i, _)| *i < self.index) .find_map(|(i, c)| if c == '=' { Some(i) } else { None })?
.find_map(|(i, c)| if c == '=' { Some(i) } else { None })?; + self.index;
// advance the index to the char after the end of the key (to skip the `=`) // advance the index to the char after the end of the key (to skip the `=`)
// 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 = {
@ -44,11 +43,7 @@ impl<'a> Iterator for AttributePairs<'a> {
let end = { let end = {
let mut result = self.string.len(); let mut result = self.string.len();
for (i, c) in self for (i, c) in self.string[self.index..].char_indices() {
.string
.char_indices()
.skip_while(|(i, _)| *i < self.index)
{
// if a quote is encountered // if a quote is encountered
if c == '"' { if c == '"' {
// update variable // update variable
@ -60,7 +55,7 @@ impl<'a> Iterator for AttributePairs<'a> {
self.index += 1; self.index += 1;
// the result is the index of the comma (comma is not included in the // the result is the index of the comma (comma is not included in the
// resulting string) // resulting string)
result = i; result = i + self.index - 1;
break; break;
} }
} }
@ -71,10 +66,10 @@ 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.trim(), value.trim())) Some((key, value))
} }
fn size_hint(&self) -> (usize, Option<usize>) { fn size_hint(&self) -> (usize, Option<usize>) {
@ -84,11 +79,7 @@ impl<'a> Iterator for AttributePairs<'a> {
// this also ignores `=` inside quotes! // this also ignores `=` inside quotes!
let mut inside_quotes = false; let mut inside_quotes = false;
for (_, c) in self for (_, c) in self.string[self.index..].char_indices() {
.string
.char_indices()
.skip_while(|(i, _)| *i < self.index)
{
if c == '=' && !inside_quotes { if c == '=' && !inside_quotes {
remaining += 1; remaining += 1;
} else if c == '"' { } else if c == '"' {

View file

@ -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
@ -136,4 +139,5 @@ mod media_segment;
mod traits; mod traits;
pub use error::Result; pub use error::Result;
pub use stable_vec;
pub use traits::*; pub use traits::*;

View file

@ -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))
} }

View file

@ -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![

View file

@ -1,9 +1,12 @@
use std::collections::{BTreeMap, HashSet}; use std::borrow::Cow;
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;
use derive_builder::Builder; use derive_builder::Builder;
use stable_vec::StableVec;
use crate::line::{Line, Lines, Tag}; use crate::line::{Line, Lines, Tag};
use crate::media_segment::MediaSegment; use crate::media_segment::MediaSegment;
@ -19,10 +22,10 @@ use crate::utils::{tag, BoolExt};
use crate::{Error, RequiredVersion}; use crate::{Error, RequiredVersion};
/// Media playlist. /// Media playlist.
#[derive(Builder, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[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.
/// ///
@ -105,7 +108,7 @@ pub struct MediaPlaylist {
/// ///
/// This field is required. /// This field is required.
#[builder(setter(custom))] #[builder(setter(custom))]
pub segments: BTreeMap<usize, 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.
/// ///
@ -128,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)
@ -224,23 +227,21 @@ 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(BTreeMap::new); let segments = self.segments.get_or_insert_with(StableVec::new);
let number = {
if segment.explicit_number { if segment.explicit_number {
segment.number segments.reserve_for(segment.number);
segments.insert(segment.number, segment);
} else { } else {
segments.keys().last().copied().unwrap_or(0) + 1 segments.push(segment);
} }
};
segments.insert(number, segment);
self self
} }
/// 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)
} }
@ -254,12 +255,23 @@ 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 {
// media segments are numbered starting at either 0 or the discontinuity let mut vec = StableVec::<MediaSegment<'a>>::with_capacity(segments.len());
// sequence, but it might not be available at the moment. let mut remaining = Vec::with_capacity(segments.len());
//
// -> final numbering will be applied in the build function for segment in segments {
self.segments = Some(segments.into_iter().enumerate().collect()); if segment.explicit_number {
vec.insert(segment.number, segment);
} else {
remaining.push(segment);
}
}
for segment in remaining {
vec.push(segment);
}
self.segments = Some(vec);
self self
} }
@ -268,26 +280,20 @@ 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()?;
let sequence_number = self.media_sequence.unwrap_or(0); let sequence_number = self.media_sequence.unwrap_or(0);
let segments = self let mut segments = self
.segments .segments
.as_ref() .clone()
.ok_or_else(|| "missing field `segments`".to_string())?; .ok_or_else(|| "missing field `segments`".to_string())?;
// insert all explictly numbered segments into the result
let mut result_segments = segments
.iter()
.filter_map(|(_, s)| s.explicit_number.athen(|| (s.number, s.clone())))
.collect::<BTreeMap<_, _>>();
// no segment should exist before the sequence_number // no segment should exist before the sequence_number
if let Some(first_segment) = result_segments.keys().min() { if let Some(first_segment) = segments.find_first() {
if sequence_number > *first_segment { if sequence_number > first_segment.number && first_segment.explicit_number {
return Err(format!( return Err(format!(
"there should be no segment ({}) before the sequence_number ({})", "there should be no segment ({}) before the sequence_number ({})",
first_segment, sequence_number, first_segment, sequence_number,
@ -295,20 +301,14 @@ impl MediaPlaylistBuilder {
} }
} }
let mut position = sequence_number;
let mut previous_range: Option<ExtXByteRange> = None; let mut previous_range: Option<ExtXByteRange> = None;
for segment in segments for (i, segment) in segments.iter_mut() {
.iter() // assign the correct number to all implcitly numbered segments:
.filter_map(|(_, s)| if s.explicit_number { None } else { Some(s) }) if !segment.explicit_number {
{ segment.number = i + sequence_number;
while result_segments.contains_key(&position) {
position += 1;
} }
let mut segment = segment.clone();
segment.number = position;
// add the segment number as iv, if the iv is missing: // add the segment number as iv, if the iv is missing:
for key in &mut segment.keys { for key in &mut segment.keys {
if let ExtXKey(Some(DecryptionKey { if let ExtXKey(Some(DecryptionKey {
@ -340,21 +340,17 @@ impl MediaPlaylistBuilder {
previous_range = segment.byte_range; previous_range = segment.byte_range;
} }
result_segments.insert(segment.number, segment);
position += 1;
} }
let mut previous_n = None; // TODO: can segments be missing?
if !segments.is_compact() {
for n in result_segments.keys() { // find the missing segment by iterating through all segments:
if let Some(previous_n) = previous_n { // let missing = segments
if previous_n + 1 != *n { // .iter()
return Err(format!("missing segment ({})", previous_n + 1)); // .enumerate()
} // .find_map(|(i, e)| e.is_none().athen(i))
} // .unwrap();
return Err(format!("a segment is missing"));
previous_n = Some(n);
} }
Ok(MediaPlaylist { Ok(MediaPlaylist {
@ -368,7 +364,7 @@ impl MediaPlaylistBuilder {
has_independent_segments: self.has_independent_segments.unwrap_or(false), has_independent_segments: self.has_independent_segments.unwrap_or(false),
start: self.start.unwrap_or(None), start: self.start.unwrap_or(None),
has_end_list: self.has_end_list.unwrap_or(false), has_end_list: self.has_end_list.unwrap_or(false),
segments: result_segments, segments,
allowable_excess_duration: self allowable_excess_duration: self
.allowable_excess_duration .allowable_excess_duration
.unwrap_or_else(|| Duration::from_secs(0)), .unwrap_or_else(|| Duration::from_secs(0)),
@ -377,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),
@ -399,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.
@ -411,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),
@ -431,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)?;
@ -469,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 {
@ -536,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();
@ -662,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));
} }
} }
} }
@ -690,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())
} }
} }
@ -719,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()
@ -816,15 +853,15 @@ mod tests {
.build() .build()
.unwrap(); .unwrap();
let mut segments = playlist.segments.into_iter().map(|(k, v)| (k, v.number)); let mut segments = playlist.segments.into_iter().map(|(k, v)| (k, v.number));
assert_eq!(segments.next(), Some((2680, 2680))); assert_eq!(segments.next(), Some((0, 2680)));
assert_eq!(segments.next(), Some((2681, 2681))); assert_eq!(segments.next(), Some((1, 2681)));
assert_eq!(segments.next(), Some((2682, 2682))); assert_eq!(segments.next(), Some((2, 2682)));
assert_eq!(segments.next(), None); assert_eq!(segments.next(), None);
} }
#[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());
} }
} }

View file

@ -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()
} }

View file

@ -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]

View file

@ -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)
); );
} }

View file

@ -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
@ -262,7 +220,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
@ -283,8 +241,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,
@ -325,22 +283,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)?;
@ -388,10 +371,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();
@ -463,7 +446,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());
)+ )+
} }
} }
@ -762,25 +745,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]

View file

@ -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
); );

View file

@ -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());
)+ )+
} }
} }

View file

@ -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() {

View file

@ -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("")))
); );
} }

View file

@ -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]

View file

@ -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() {

View file

@ -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()
); );
} }
} }

View file

@ -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()
); );
} }
} }

View file

@ -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()
); );
} }

View file

@ -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());
} }
} }

View file

@ -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() {

View file

@ -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())
); );
} }

View file

@ -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());
} }
} }
} }

View file

@ -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());
} }
} }

View file

@ -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()
); );
} }

View file

@ -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(),
) )
} }

View file

@ -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(),
); );
} }
} }

View file

@ -1,11 +1,13 @@
use std::collections::{BTreeMap, HashMap}; use std::collections::{BTreeMap, HashMap};
use stable_vec::StableVec;
use crate::types::{DecryptionKey, ProtocolVersion}; 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
@ -14,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
@ -34,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()
} }
@ -108,6 +110,16 @@ impl<K, V: RequiredVersion, S> RequiredVersion for HashMap<K, V, S> {
} }
} }
impl<T: RequiredVersion> RequiredVersion for StableVec<T> {
fn required_version(&self) -> ProtocolVersion {
self.values()
.map(RequiredVersion::required_version)
.max()
// return ProtocolVersion::V1, if the iterator is empty:
.unwrap_or_default()
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -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());
} }
} }

View file

@ -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()
); );
} }
} }

View file

@ -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"])
); );
} }
} }

View file

@ -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());
} }
} }
} }

View file

@ -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]

View file

@ -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());
} }
} }

View file

@ -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]));
} }

View file

@ -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.

View file

@ -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());
} }

View file

@ -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());
} }

View file

@ -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());
} }