1
0
Fork 0
mirror of https://github.com/sile/hls_m3u8.git synced 2024-11-23 23:51:00 +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"
keywords = ["hls", "m3u8"]
edition = "2018"
categories = ["parser"]
categories = ["parser-implementations"]
[features]
default = []
perf = []
[badges]
codecov = { repository = "sile/hls_m3u8" }
travis-ci = { repository = "sile/hls_m3u8" }
@ -29,7 +31,14 @@ derive_more = "0.99"
shorthand = "0.1"
strum = { version = "0.17", features = ["derive"] }
stable-vec = { version = "0.4" }
[dev-dependencies]
pretty_assertions = "0.6"
version-sync = "0.9"
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:
let start = self.index;
// the key ends at an `=`:
let end = self
.string
let end = self.string[self.index..]
.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 `=`)
// NOTE: it is okay to add 1 to the index, because an `=` is exactly 1 byte.
self.index = end + 1;
::core::str::from_utf8(&self.string.as_bytes()[start..end]).unwrap()
&self.string[start..end]
};
let value = {
@ -44,11 +43,7 @@ impl<'a> Iterator for AttributePairs<'a> {
let end = {
let mut result = self.string.len();
for (i, c) in self
.string
.char_indices()
.skip_while(|(i, _)| *i < self.index)
{
for (i, c) in self.string[self.index..].char_indices() {
// if a quote is encountered
if c == '"' {
// update variable
@ -60,7 +55,7 @@ impl<'a> Iterator for AttributePairs<'a> {
self.index += 1;
// the result is the index of the comma (comma is not included in the
// resulting string)
result = i;
result = i + self.index - 1;
break;
}
}
@ -71,10 +66,10 @@ impl<'a> Iterator for AttributePairs<'a> {
self.index += end;
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>) {
@ -84,11 +79,7 @@ impl<'a> Iterator for AttributePairs<'a> {
// this also ignores `=` inside quotes!
let mut inside_quotes = false;
for (_, c) in self
.string
.char_indices()
.skip_while(|(i, _)| *i < self.index)
{
for (_, c) in self.string[self.index..].char_indices() {
if c == '=' && !inside_quotes {
remaining += 1;
} else if c == '"' {
@ -199,7 +190,7 @@ mod test {
assert_eq!((1, Some(1)), pairs.size_hint());
assert_eq!(
pairs.next(),
Some(("ध्वनि स्थिति और्४५० नीचे", "देखने लाभो द्वारा करके(विशेष"))
Some(("ध्वनि स्थिति और्४५० नीचे ", "देखने लाभो द्वारा करके(विशेष"))
);
assert_eq!((0, Some(0)), pairs.size_hint());

View file

@ -42,19 +42,22 @@
//!
//! ```
//! use hls_m3u8::MediaPlaylist;
//! use std::convert::TryFrom;
//!
//! let m3u8 = "#EXTM3U
//! #EXT-X-TARGETDURATION:10
//! #EXT-X-VERSION:3
//! #EXTINF:9.009,
//! http://media.example.com/first.ts
//! #EXTINF:9.009,
//! http://media.example.com/second.ts
//! #EXTINF:3.003,
//! http://media.example.com/third.ts
//! #EXT-X-ENDLIST";
//! let m3u8 = MediaPlaylist::try_from(concat!(
//! "#EXTM3U\n",
//! "#EXT-X-TARGETDURATION:10\n",
//! "#EXT-X-VERSION:3\n",
//! "#EXTINF:9.009,\n",
//! "http://media.example.com/first.ts\n",
//! "#EXTINF:9.009,\n",
//! "http://media.example.com/second.ts\n",
//! "#EXTINF:3.003,\n",
//! "http://media.example.com/third.ts\n",
//! "#EXT-X-ENDLIST",
//! ));
//!
//! assert!(m3u8.parse::<MediaPlaylist>().is_ok());
//! assert!(m3u8.is_ok());
//! ```
//!
//! ## Crate Feature Flags
@ -136,4 +139,5 @@ mod media_segment;
mod traits;
pub use error::Result;
pub use stable_vec;
pub use traits::*;

View file

@ -1,6 +1,5 @@
use core::convert::TryFrom;
use core::iter::FusedIterator;
use core::str::FromStr;
use derive_more::Display;
@ -23,7 +22,8 @@ impl<'a> Iterator for Lines<'a> {
let uri = self.lines.next()?;
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))),
)
} else if line.starts_with("#EXT") {
@ -60,25 +60,25 @@ pub(crate) enum Line<'a> {
#[display(fmt = "{}")]
pub(crate) enum Tag<'a> {
ExtXVersion(tags::ExtXVersion),
ExtInf(tags::ExtInf),
ExtInf(tags::ExtInf<'a>),
ExtXByteRange(tags::ExtXByteRange),
ExtXDiscontinuity(tags::ExtXDiscontinuity),
ExtXKey(tags::ExtXKey),
ExtXMap(tags::ExtXMap),
ExtXProgramDateTime(tags::ExtXProgramDateTime),
ExtXDateRange(tags::ExtXDateRange),
ExtXKey(tags::ExtXKey<'a>),
ExtXMap(tags::ExtXMap<'a>),
ExtXProgramDateTime(tags::ExtXProgramDateTime<'a>),
ExtXDateRange(tags::ExtXDateRange<'a>),
ExtXTargetDuration(tags::ExtXTargetDuration),
ExtXMediaSequence(tags::ExtXMediaSequence),
ExtXDiscontinuitySequence(tags::ExtXDiscontinuitySequence),
ExtXEndList(tags::ExtXEndList),
PlaylistType(PlaylistType),
ExtXIFramesOnly(tags::ExtXIFramesOnly),
ExtXMedia(tags::ExtXMedia),
ExtXSessionData(tags::ExtXSessionData),
ExtXSessionKey(tags::ExtXSessionKey),
ExtXMedia(tags::ExtXMedia<'a>),
ExtXSessionData(tags::ExtXSessionData<'a>),
ExtXSessionKey(tags::ExtXSessionKey<'a>),
ExtXIndependentSegments(tags::ExtXIndependentSegments),
ExtXStart(tags::ExtXStart),
VariantStream(tags::VariantStream),
VariantStream(tags::VariantStream<'a>),
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> {
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) {
input.parse().map(Self::ExtInf)
TryFrom::try_from(input).map(Self::ExtInf)
} 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) {
input.parse().map(Self::ExtXDiscontinuity)
TryFrom::try_from(input).map(Self::ExtXDiscontinuity)
} 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) {
input.parse().map(Self::ExtXMap)
TryFrom::try_from(input).map(Self::ExtXMap)
} 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) {
input.parse().map(Self::ExtXTargetDuration)
TryFrom::try_from(input).map(Self::ExtXTargetDuration)
} 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) {
input.parse().map(Self::ExtXMediaSequence)
TryFrom::try_from(input).map(Self::ExtXMediaSequence)
} 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) {
input.parse().map(Self::ExtXEndList)
TryFrom::try_from(input).map(Self::ExtXEndList)
} 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) {
input.parse().map(Self::ExtXIFramesOnly)
TryFrom::try_from(input).map(Self::ExtXIFramesOnly)
} 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)
|| 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) {
input.parse().map(Self::ExtXSessionData)
TryFrom::try_from(input).map(Self::ExtXSessionData)
} 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) {
input.parse().map(Self::ExtXIndependentSegments)
TryFrom::try_from(input).map(Self::ExtXIndependentSegments)
} else if input.starts_with(tags::ExtXStart::PREFIX) {
input.parse().map(Self::ExtXStart)
TryFrom::try_from(input).map(Self::ExtXStart)
} else {
Ok(Self::Unknown(input))
}

View file

@ -1,6 +1,7 @@
use std::borrow::Cow;
use std::collections::HashSet;
use std::convert::TryFrom;
use std::fmt;
use std::str::FromStr;
use derive_builder::Builder;
@ -24,11 +25,11 @@ use crate::{Error, RequiredVersion};
/// A [`MasterPlaylist`] can be parsed from a `str`:
///
/// ```
/// use core::str::FromStr;
/// use core::convert::TryFrom;
/// use hls_m3u8::MasterPlaylist;
///
/// // the concat! macro joins multiple `&'static str`.
/// let master_playlist = concat!(
/// let master_playlist = MasterPlaylist::try_from(concat!(
/// "#EXTM3U\n",
/// "#EXT-X-STREAM-INF:",
/// "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",
/// "#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS=\"mp4a.40.5\"\n",
/// "http://example.com/audio/index.m3u8\n"
/// )
/// .parse::<MasterPlaylist>()?;
/// ))?;
///
/// println!("{}", master_playlist.has_independent_segments);
/// # Ok::<(), hls_m3u8::Error>(())
@ -98,7 +98,7 @@ use crate::{Error, RequiredVersion};
#[builder(build_fn(validate = "Self::validate"))]
#[builder(setter(into, strip_option))]
#[non_exhaustive]
pub struct MasterPlaylist {
pub struct MasterPlaylist<'a> {
/// Indicates that all media samples in a [`MediaSegment`] can be
/// decoded without information from other segments.
///
@ -135,14 +135,14 @@ pub struct MasterPlaylist {
///
/// [`MediaPlaylist`]: crate::MediaPlaylist
#[builder(default)]
pub media: Vec<ExtXMedia>,
pub media: Vec<ExtXMedia<'a>>,
/// A list of all streams of this [`MasterPlaylist`].
///
/// ### Note
///
/// This field is optional.
#[builder(default)]
pub variant_streams: Vec<VariantStream>,
pub variant_streams: Vec<VariantStream<'a>>,
/// The [`ExtXSessionData`] tag allows arbitrary session data to be
/// carried in a [`MasterPlaylist`].
///
@ -150,7 +150,7 @@ pub struct MasterPlaylist {
///
/// This field is optional.
#[builder(default)]
pub session_data: Vec<ExtXSessionData>,
pub session_data: Vec<ExtXSessionData<'a>>,
/// A list of [`ExtXSessionKey`]s, that allows the client to preload
/// these keys without having to read the [`MediaPlaylist`]s first.
///
@ -160,17 +160,17 @@ pub struct MasterPlaylist {
///
/// [`MediaPlaylist`]: crate::MediaPlaylist
#[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.
///
/// ### Note
///
/// This field is optional.
#[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`].
///
/// # Example
@ -216,10 +216,10 @@ impl MasterPlaylist {
/// ```
#[must_use]
#[inline]
pub fn builder() -> MasterPlaylistBuilder { MasterPlaylistBuilder::default() }
pub fn builder() -> MasterPlaylistBuilder<'a> { MasterPlaylistBuilder::default() }
/// 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| {
if let VariantStream::ExtXStreamInf { audio: Some(_), .. } = stream {
true
@ -230,7 +230,7 @@ impl MasterPlaylist {
}
/// 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| {
if let VariantStream::ExtXStreamInf { stream_data, .. } = stream {
stream_data.video().is_some()
@ -243,7 +243,7 @@ impl MasterPlaylist {
}
/// 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| {
if let VariantStream::ExtXStreamInf {
stream_data,
@ -263,17 +263,52 @@ impl MasterPlaylist {
}
/// Returns all `ExtXMedia` tags, associated with the provided stream.
pub fn associated_with<'a>(
&'a self,
stream: &'a VariantStream,
) -> impl Iterator<Item = &ExtXMedia> + 'a {
pub fn associated_with<'b>(
&'b self,
stream: &'b VariantStream<'_>,
) -> impl Iterator<Item = &ExtXMedia<'a>> + 'b {
self.media
.iter()
.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 {
required_version![
self.has_independent_segments
@ -287,7 +322,7 @@ impl RequiredVersion for MasterPlaylist {
}
}
impl MasterPlaylistBuilder {
impl<'a> MasterPlaylistBuilder<'a> {
fn validate(&self) -> Result<(), String> {
if let Some(variant_streams) = &self.variant_streams {
self.validate_variants(variant_streams)
@ -300,7 +335,7 @@ impl MasterPlaylistBuilder {
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;
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 {
if let Some(value) = &self.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 {
false
@ -390,7 +425,7 @@ impl MasterPlaylistBuilder {
}
}
impl RequiredVersion for MasterPlaylistBuilder {
impl<'a> RequiredVersion for MasterPlaylistBuilder<'a> {
fn required_version(&self) -> ProtocolVersion {
// TODO: the .flatten() can be removed as soon as `recursive traits` are
// 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 {
writeln!(f, "{}", ExtM3u)?;
@ -449,10 +484,10 @@ impl fmt::Display for MasterPlaylist {
}
}
impl FromStr for MasterPlaylist {
type Err = Error;
impl<'a> TryFrom<&'a str> for MasterPlaylist<'a> {
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 mut builder = Self::builder();
@ -505,10 +540,10 @@ impl FromStr for MasterPlaylist {
Tag::ExtXStart(t) => {
builder.start(t);
}
_ => {
Tag::Unknown(value) => {
// [6.3.1. General Client Responsibilities]
// > ignore any unrecognized tags.
unknown_tags.push(tag.to_string());
unknown_tags.push(Cow::Borrowed(value));
}
}
}
@ -604,7 +639,7 @@ mod tests {
#[test]
fn test_parser() {
assert_eq!(
concat!(
MasterPlaylist::try_from(concat!(
"#EXTM3U\n",
"#EXT-X-STREAM-INF:",
"BANDWIDTH=150000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n",
@ -620,8 +655,7 @@ mod tests {
"http://example.com/high/index.m3u8\n",
"#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS=\"mp4a.40.5\"\n",
"http://example.com/audio/index.m3u8\n"
)
.parse::<MasterPlaylist>()
))
.unwrap(),
MasterPlaylist::builder()
.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::str::FromStr;
use std::time::Duration;
use derive_builder::Builder;
use stable_vec::StableVec;
use crate::line::{Line, Lines, Tag};
use crate::media_segment::MediaSegment;
@ -19,10 +22,10 @@ use crate::utils::{tag, BoolExt};
use crate::{Error, RequiredVersion};
/// Media playlist.
#[derive(Builder, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[derive(Builder, Debug, Clone, PartialEq, Eq)]
#[builder(build_fn(skip), setter(strip_option))]
#[non_exhaustive]
pub struct MediaPlaylist {
pub struct MediaPlaylist<'a> {
/// Specifies the maximum [`MediaSegment::duration`]. A typical target
/// duration is 10 seconds.
///
@ -105,7 +108,7 @@ pub struct MediaPlaylist {
///
/// This field is required.
#[builder(setter(custom))]
pub segments: BTreeMap<usize, MediaSegment>,
pub segments: StableVec<MediaSegment<'a>>,
/// The allowable excess duration of each media segment in the
/// associated playlist.
///
@ -128,10 +131,10 @@ pub struct MediaPlaylist {
///
/// This field is optional.
#[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> {
if let Some(target_duration) = &self.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
/// [`MediaSegment::number`] to the segment.
pub fn push_segment(&mut self, segment: MediaSegment) -> &mut Self {
let segments = self.segments.get_or_insert_with(BTreeMap::new);
pub fn push_segment(&mut self, segment: MediaSegment<'a>) -> &mut Self {
let segments = self.segments.get_or_insert_with(StableVec::new);
let number = {
if segment.explicit_number {
segment.number
} else {
segments.keys().last().copied().unwrap_or(0) + 1
}
};
if segment.explicit_number {
segments.reserve_for(segment.number);
segments.insert(segment.number, segment);
} else {
segments.push(segment);
}
segments.insert(number, segment);
self
}
/// 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)
}
@ -254,12 +255,23 @@ impl MediaPlaylistBuilder {
/// number has been set explicitly. This function assumes, that all segments
/// will be present in the final media playlist and the following is only
/// possible if the segment is marked with `ExtXDiscontinuity`.
pub fn segments(&mut self, segments: Vec<MediaSegment>) -> &mut Self {
// media segments are numbered starting at either 0 or the discontinuity
// sequence, but it might not be available at the moment.
//
// -> final numbering will be applied in the build function
self.segments = Some(segments.into_iter().enumerate().collect());
pub fn segments(&mut self, segments: Vec<MediaSegment<'a>>) -> &mut Self {
let mut vec = StableVec::<MediaSegment<'a>>::with_capacity(segments.len());
let mut remaining = Vec::with_capacity(segments.len());
for segment in segments {
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
}
@ -268,26 +280,20 @@ impl MediaPlaylistBuilder {
/// # Errors
///
/// 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
self.validate()?;
let sequence_number = self.media_sequence.unwrap_or(0);
let segments = self
let mut segments = self
.segments
.as_ref()
.clone()
.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
if let Some(first_segment) = result_segments.keys().min() {
if sequence_number > *first_segment {
if let Some(first_segment) = segments.find_first() {
if sequence_number > first_segment.number && first_segment.explicit_number {
return Err(format!(
"there should be no segment ({}) before the sequence_number ({})",
first_segment, sequence_number,
@ -295,20 +301,14 @@ impl MediaPlaylistBuilder {
}
}
let mut position = sequence_number;
let mut previous_range: Option<ExtXByteRange> = None;
for segment in segments
.iter()
.filter_map(|(_, s)| if s.explicit_number { None } else { Some(s) })
{
while result_segments.contains_key(&position) {
position += 1;
for (i, segment) in segments.iter_mut() {
// assign the correct number to all implcitly numbered segments:
if !segment.explicit_number {
segment.number = i + sequence_number;
}
let mut segment = segment.clone();
segment.number = position;
// add the segment number as iv, if the iv is missing:
for key in &mut segment.keys {
if let ExtXKey(Some(DecryptionKey {
@ -340,21 +340,17 @@ impl MediaPlaylistBuilder {
previous_range = segment.byte_range;
}
result_segments.insert(segment.number, segment);
position += 1;
}
let mut previous_n = None;
for n in result_segments.keys() {
if let Some(previous_n) = previous_n {
if previous_n + 1 != *n {
return Err(format!("missing segment ({})", previous_n + 1));
}
}
previous_n = Some(n);
// TODO: can segments be missing?
if !segments.is_compact() {
// find the missing segment by iterating through all segments:
// let missing = segments
// .iter()
// .enumerate()
// .find_map(|(i, e)| e.is_none().athen(i))
// .unwrap();
return Err(format!("a segment is missing"));
}
Ok(MediaPlaylist {
@ -368,7 +364,7 @@ impl MediaPlaylistBuilder {
has_independent_segments: self.has_independent_segments.unwrap_or(false),
start: self.start.unwrap_or(None),
has_end_list: self.has_end_list.unwrap_or(false),
segments: result_segments,
segments,
allowable_excess_duration: self
.allowable_excess_duration
.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 {
required_version![
self.target_duration.map(ExtXTargetDuration),
@ -399,11 +395,11 @@ impl RequiredVersion for MediaPlaylistBuilder {
}
}
impl MediaPlaylist {
impl<'a> MediaPlaylist<'a> {
/// Returns a builder for [`MediaPlaylist`].
#[must_use]
#[inline]
pub fn builder() -> MediaPlaylistBuilder { MediaPlaylistBuilder::default() }
pub fn builder() -> MediaPlaylistBuilder<'a> { MediaPlaylistBuilder::default() }
/// Computes the `Duration` of the [`MediaPlaylist`], by adding each segment
/// duration together.
@ -411,9 +407,42 @@ impl MediaPlaylist {
pub fn duration(&self) -> Duration {
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 {
required_version![
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 {
writeln!(f, "{}", ExtM3u)?;
@ -469,7 +498,7 @@ impl fmt::Display for MediaPlaylist {
writeln!(f, "{}", value)?;
}
let mut available_keys = HashSet::<ExtXKey>::new();
let mut available_keys = HashSet::<ExtXKey<'_>>::new();
for segment in self.segments.values() {
for key in &segment.keys {
@ -536,10 +565,10 @@ impl fmt::Display for MediaPlaylist {
}
}
fn parse_media_playlist(
input: &str,
builder: &mut MediaPlaylistBuilder,
) -> crate::Result<MediaPlaylist> {
fn parse_media_playlist<'a>(
input: &'a str,
builder: &mut MediaPlaylistBuilder<'a>,
) -> crate::Result<MediaPlaylist<'a>> {
let input = tag(input, "#EXTM3U")?;
let mut segment = MediaSegment::builder();
@ -662,10 +691,10 @@ fn parse_media_playlist(
builder.start(t);
}
Tag::ExtXVersion(_) => {}
Tag::Unknown(_) => {
Tag::Unknown(s) => {
// [6.3.1. General Client Responsibilities]
// > 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)
}
impl FromStr for MediaPlaylist {
impl FromStr for MediaPlaylist<'static> {
type Err = Error;
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())
}
}
@ -719,7 +756,7 @@ mod tests {
);
// 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)
assert!(MediaPlaylist::builder()
@ -816,15 +853,15 @@ mod tests {
.build()
.unwrap();
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((2681, 2681)));
assert_eq!(segments.next(), Some((2682, 2682)));
assert_eq!(segments.next(), Some((0, 2680)));
assert_eq!(segments.next(), Some((1, 2681)));
assert_eq!(segments.next(), Some((2, 2682)));
assert_eq!(segments.next(), None);
}
#[test]
fn test_empty_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 derive_builder::Builder;
@ -34,7 +35,7 @@ use crate::{Decryptable, RequiredVersion};
#[derive(ShortHand, Debug, Clone, Builder, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[builder(setter(strip_option))]
#[shorthand(enable(must_use, skip))]
pub struct MediaSegment {
pub struct MediaSegment<'a> {
/// Each [`MediaSegment`] has a number, which allows synchronization between
/// different variants.
///
@ -80,7 +81,7 @@ pub struct MediaSegment {
/// [`KeyFormat`]: crate::types::KeyFormat
/// [`EncryptionMethod`]: crate::types::EncryptionMethod
#[builder(default, setter(into))]
pub keys: Vec<ExtXKey>,
pub keys: Vec<ExtXKey<'a>>,
/// This field specifies how to obtain the Media Initialization Section
/// required to parse the applicable `MediaSegment`s.
///
@ -94,7 +95,7 @@ pub struct MediaSegment {
///
/// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly
#[builder(default)]
pub map: Option<ExtXMap>,
pub map: Option<ExtXMap<'a>>,
/// This field indicates that a `MediaSegment` is a sub-range of the
/// resource identified by its URI.
///
@ -110,7 +111,7 @@ pub struct MediaSegment {
///
/// This field is optional.
#[builder(default)]
pub date_range: Option<ExtXDateRange>,
pub date_range: Option<ExtXDateRange<'a>>,
/// This field indicates a discontinuity between the `MediaSegment` that
/// follows it and the one that preceded it.
///
@ -134,14 +135,14 @@ pub struct MediaSegment {
///
/// This field is optional.
#[builder(default)]
pub program_date_time: Option<ExtXProgramDateTime>,
pub program_date_time: Option<ExtXProgramDateTime<'a>>,
/// This field indicates the duration of a media segment.
///
/// ## Note
///
/// This field is required.
#[builder(setter(into))]
pub duration: ExtInf,
pub duration: ExtInf<'a>,
/// The URI of a media segment.
///
/// ## Note
@ -149,10 +150,10 @@ pub struct MediaSegment {
/// This field is required.
#[builder(setter(into))]
#[shorthand(enable(into), disable(skip))]
uri: String,
uri: Cow<'a, str>,
}
impl MediaSegment {
impl<'a> MediaSegment<'a> {
/// Returns a builder for a [`MediaSegment`].
///
/// # Example
@ -173,12 +174,34 @@ impl MediaSegment {
/// ```
#[must_use]
#[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.
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 {
keys.push(value.into());
} 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 {
// 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 {
required_version![
self.keys,
@ -251,8 +274,8 @@ impl RequiredVersion for MediaSegment {
}
}
impl Decryptable for MediaSegment {
fn keys(&self) -> Vec<&DecryptionKey> {
impl<'a> Decryptable<'a> for MediaSegment<'a> {
fn keys(&self) -> Vec<&DecryptionKey<'a>> {
//
self.keys.iter().filter_map(ExtXKey::as_ref).collect()
}

View file

@ -1,5 +1,5 @@
use std::convert::TryFrom;
use std::fmt;
use std::str::FromStr;
use crate::types::ProtocolVersion;
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) }
}
impl FromStr for ExtM3u {
type Err = Error;
impl TryFrom<&str> for ExtM3u {
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)?;
Ok(Self)
}
@ -49,8 +49,8 @@ mod test {
#[test]
fn test_parser() {
assert_eq!("#EXTM3U".parse::<ExtM3u>().unwrap(), ExtM3u);
assert!("#EXTM2U".parse::<ExtM3u>().is_err());
assert_eq!(ExtM3u::try_from("#EXTM3U").unwrap(), ExtM3u);
assert!(ExtM3u::try_from("#EXTM2U").is_err());
}
#[test]

View file

@ -1,5 +1,5 @@
use std::convert::TryFrom;
use std::fmt;
use std::str::FromStr;
use crate::types::ProtocolVersion;
use crate::utils::tag;
@ -67,10 +67,10 @@ impl From<ProtocolVersion> for ExtXVersion {
fn from(value: ProtocolVersion) -> Self { Self(value) }
}
impl FromStr for ExtXVersion {
type Err = Error;
impl TryFrom<&str> for ExtXVersion {
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()?;
Ok(Self::new(version))
}
@ -92,7 +92,7 @@ mod test {
#[test]
fn test_parser() {
assert_eq!(
"#EXT-X-VERSION:6".parse::<ExtXVersion>().unwrap(),
ExtXVersion::try_from("#EXT-X-VERSION:6").unwrap(),
ExtXVersion::new(ProtocolVersion::V6)
);
}

View file

@ -1,5 +1,6 @@
use std::borrow::Cow;
use std::convert::TryFrom;
use std::fmt;
use std::str::FromStr;
use derive_builder::Builder;
use shorthand::ShortHand;
@ -21,7 +22,7 @@ use crate::{Error, RequiredVersion};
#[shorthand(enable(must_use, into))]
#[builder(setter(into))]
#[builder(build_fn(validate = "Self::validate"))]
pub struct ExtXMedia {
pub struct ExtXMedia<'a> {
/// The [`MediaType`] associated with this tag.
///
/// ### Note
@ -31,24 +32,7 @@ pub struct ExtXMedia {
pub media_type: MediaType,
/// An `URI` to a [`MediaPlaylist`].
///
/// # Example
///
/// ```
/// # 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
/// ### Note
///
/// - This field is required, if the [`ExtXMedia::media_type`] is
/// [`MediaType::Subtitles`].
@ -64,65 +48,39 @@ pub struct ExtXMedia {
/// [`VariantStream::ExtXStreamInf`]:
/// crate::tags::VariantStream::ExtXStreamInf
#[builder(setter(strip_option), default)]
uri: Option<String>,
uri: Option<Cow<'a, str>>,
/// The identifier that specifies the group to which the rendition
/// belongs.
///
/// # Example
///
/// ```
/// # use hls_m3u8::tags::ExtXMedia;
/// use hls_m3u8::types::MediaType;
///
/// let mut media = ExtXMedia::new(MediaType::Audio, "ag1", "english audio channel");
///
/// media.set_group_id("ag2");
///
/// assert_eq!(media.group_id(), &"ag2".to_string());
/// ```
///
/// # Note
/// ### Note
///
/// This field is required.
group_id: String,
group_id: Cow<'a, str>,
/// The name of the primary language used in the rendition.
/// The value has to conform to [`RFC5646`].
///
/// # Example
///
/// ```
/// # 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
/// ### Note
///
/// This field is optional.
///
/// [`RFC5646`]: https://tools.ietf.org/html/rfc5646
#[builder(setter(strip_option), default)]
language: Option<String>,
language: Option<Cow<'a, str>>,
/// The name of a language associated with the rendition.
/// An associated language is often used in a different role, than the
/// language specified by the [`language`] field (e.g., written versus
/// spoken, or a fallback dialect).
///
/// # Note
/// ### Note
///
/// This field is optional.
///
/// [`language`]: #method.language
#[builder(setter(strip_option), default)]
assoc_language: Option<String>,
assoc_language: Option<Cow<'a, str>>,
/// A human-readable description of the rendition.
///
/// # Note
/// ### Note
///
/// This field is required.
///
@ -130,7 +88,7 @@ pub struct ExtXMedia {
/// that language.
///
/// [`language`]: #method.language
name: String,
name: Cow<'a, str>,
/// The value of the `default` flag.
/// A value of `true` indicates, that the client should play
/// this rendition of the content in the absence of information
@ -189,13 +147,13 @@ pub struct ExtXMedia {
///
/// The characteristics field may include private UTIs.
///
/// # Note
/// ### Note
///
/// This field is optional.
///
/// [`UTI`]: https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-05#ref-UTI
#[builder(setter(strip_option), default)]
characteristics: Option<String>,
characteristics: Option<Cow<'a, str>>,
/// A count of audio channels indicating the maximum number of independent,
/// simultaneous audio channels present in any [`MediaSegment`] in the
/// rendition.
@ -214,7 +172,7 @@ pub struct ExtXMedia {
pub channels: Option<Channels>,
}
impl ExtXMediaBuilder {
impl<'a> ExtXMediaBuilder<'a> {
fn validate(&self) -> Result<(), String> {
// A MediaType is always required!
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:";
/// Makes a new [`ExtXMedia`] tag with the associated [`MediaType`], the
@ -283,8 +241,8 @@ impl ExtXMedia {
#[must_use]
pub fn new<T, K>(media_type: MediaType, group_id: T, name: K) -> Self
where
T: Into<String>,
K: Into<String>,
T: Into<Cow<'a, str>>,
K: Into<Cow<'a, str>>,
{
Self {
media_type,
@ -325,22 +283,47 @@ impl ExtXMedia {
/// "public.accessibility.describes-music-and-sound"
/// ))
/// .build()?;
/// # Ok::<(), Box<dyn ::std::error::Error>>(())
/// # Ok::<(), String>(())
/// ```
#[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
/// `instream_id` it requires it's version.
impl RequiredVersion for ExtXMedia {
impl<'a> RequiredVersion for ExtXMedia<'a> {
fn required_version(&self) -> ProtocolVersion {
self.instream_id
.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 {
write!(f, "{}", Self::PREFIX)?;
write!(f, "TYPE={}", self.media_type)?;
@ -388,10 +371,10 @@ impl fmt::Display for ExtXMedia {
}
}
impl FromStr for ExtXMedia {
type Err = Error;
impl<'a> TryFrom<&'a str> for ExtXMedia<'a> {
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 mut builder = Self::builder();
@ -463,7 +446,7 @@ mod test {
#[test]
fn test_parser() {
$(
assert_eq!($struct, $str.parse().unwrap());
assert_eq!($struct, TryFrom::try_from($str).unwrap());
)+
}
}
@ -762,25 +745,28 @@ mod test {
#[test]
fn test_parser_error() {
assert!("".parse::<ExtXMedia>().is_err());
assert!("garbage".parse::<ExtXMedia>().is_err());
assert_eq!(ExtXMedia::try_from("").is_err(), true);
assert_eq!(ExtXMedia::try_from("garbage").is_err(), true);
assert!(
"#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,URI=\"http://www.example.com\""
.parse::<ExtXMedia>()
.is_err()
assert_eq!(
ExtXMedia::try_from("#EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,URI=\"http://www.example.com\"")
.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"
.parse::<ExtXMedia>()
.is_err());
assert_eq!(
ExtXMedia::try_from("#EXT-X-MEDIA:TYPE=AUDIO,DEFAULT=YES,AUTOSELECT=NO").is_err(),
true
);
assert!("#EXT-X-MEDIA:TYPE=AUDIO,FORCED=YES"
.parse::<ExtXMedia>()
.is_err());
assert_eq!(
ExtXMedia::try_from("#EXT-X-MEDIA:TYPE=AUDIO,FORCED=YES").is_err(),
true
);
}
#[test]

View file

@ -1,5 +1,6 @@
use std::borrow::Cow;
use std::convert::TryFrom;
use std::fmt;
use std::str::FromStr;
use derive_builder::Builder;
use shorthand::ShortHand;
@ -11,7 +12,7 @@ use crate::{Error, RequiredVersion};
/// The data of [`ExtXSessionData`].
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum SessionData {
pub enum SessionData<'a> {
/// Contains the data identified by the [`ExtXSessionData::data_id`].
///
/// If a [`language`] is specified, this variant should contain a
@ -19,12 +20,28 @@ pub enum SessionData {
///
/// [`data_id`]: ExtXSessionData::data_id
/// [`language`]: ExtXSessionData::language
Value(String),
Value(Cow<'a, str>),
/// An [`URI`], which points to a [`json`] file.
///
/// [`json`]: https://tools.ietf.org/html/rfc8259
/// [`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`].
@ -33,7 +50,7 @@ pub enum SessionData {
#[derive(ShortHand, Builder, Hash, Eq, Ord, Debug, PartialEq, Clone, PartialOrd)]
#[builder(setter(into))]
#[shorthand(enable(must_use, into))]
pub struct ExtXSessionData {
pub struct ExtXSessionData<'a> {
/// This should conform to a [reverse DNS] naming convention, such as
/// `com.example.movie.title`.
///
@ -45,7 +62,7 @@ pub struct ExtXSessionData {
/// This field is required.
///
/// [reverse DNS]: https://en.wikipedia.org/wiki/Reverse_domain_name_notation
data_id: String,
data_id: Cow<'a, str>,
/// The [`SessionData`] associated with the
/// [`data_id`](ExtXSessionData::data_id).
///
@ -53,7 +70,7 @@ pub struct ExtXSessionData {
///
/// This field is required.
#[shorthand(enable(skip))]
pub data: SessionData,
pub data: SessionData<'a>,
/// The `language` attribute identifies the language of the [`SessionData`].
///
/// # Note
@ -62,11 +79,11 @@ pub struct ExtXSessionData {
/// [RFC5646].
///
/// [RFC5646]: https://tools.ietf.org/html/rfc5646
#[builder(setter(into, strip_option), default)]
language: Option<String>,
#[builder(setter(strip_option), default)]
language: Option<Cow<'a, str>>,
}
impl ExtXSessionData {
impl<'a> ExtXSessionData<'a> {
pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-DATA:";
/// Makes a new [`ExtXSessionData`] tag.
@ -83,7 +100,7 @@ impl ExtXSessionData {
/// );
/// ```
#[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 {
data_id: data_id.into(),
data,
@ -107,7 +124,7 @@ impl ExtXSessionData {
/// # Ok::<(), String>(())
/// ```
#[must_use]
pub fn builder() -> ExtXSessionDataBuilder { ExtXSessionDataBuilder::default() }
pub fn builder() -> ExtXSessionDataBuilder<'a> { ExtXSessionDataBuilder::default() }
/// Makes a new [`ExtXSessionData`] tag, with the given language.
///
@ -124,10 +141,10 @@ impl ExtXSessionData {
/// );
/// ```
#[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
T: Into<String>,
K: Into<String>,
T: Into<Cow<'a, str>>,
K: Into<Cow<'a, str>>,
{
Self {
data_id: data_id.into(),
@ -135,14 +152,29 @@ impl ExtXSessionData {
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`].
impl RequiredVersion for ExtXSessionData {
impl<'a> RequiredVersion for ExtXSessionData<'a> {
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 {
write!(f, "{}", Self::PREFIX)?;
write!(f, "DATA-ID={}", quote(&self.data_id))?;
@ -160,10 +192,10 @@ impl fmt::Display for ExtXSessionData {
}
}
impl FromStr for ExtXSessionData {
type Err = Error;
impl<'a> TryFrom<&'a str> for ExtXSessionData<'a> {
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 mut data_id = None;
@ -228,28 +260,26 @@ mod test {
#[test]
fn test_parser() {
$(
assert_eq!($struct, $str.parse().unwrap());
assert_eq!($struct, TryFrom::try_from($str).unwrap());
)+
assert!(
concat!(
ExtXSessionData::try_from(concat!(
"#EXT-X-SESSION-DATA:",
"DATA-ID=\"foo\",",
"LANGUAGE=\"baz\""
)
.parse::<ExtXSessionData>()
))
.is_err()
);
assert!(
concat!(
ExtXSessionData::try_from(concat!(
"#EXT-X-SESSION-DATA:",
"DATA-ID=\"foo\",",
"LANGUAGE=\"baz\",",
"VALUE=\"VALUE\",",
"URI=\"https://www.example.com/\""
)
.parse::<ExtXSessionData>()
))
.is_err()
);
}
@ -300,11 +330,8 @@ mod test {
#[test]
fn test_required_version() {
assert_eq!(
ExtXSessionData::new(
"com.example.lyrics",
SessionData::Uri("lyrics.json".to_string())
)
.required_version(),
ExtXSessionData::new("com.example.lyrics", SessionData::Uri("lyrics.json".into()))
.required_version(),
ProtocolVersion::V1
);
}

View file

@ -1,6 +1,5 @@
use core::convert::TryFrom;
use std::fmt;
use std::str::FromStr;
use derive_more::{AsMut, AsRef, From};
@ -22,9 +21,9 @@ use crate::{Error, RequiredVersion};
/// [`MasterPlaylist`]: crate::MasterPlaylist
/// [`ExtXKey`]: crate::tags::ExtXKey
#[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:";
/// Makes a new [`ExtXSessionKey`] tag.
@ -42,13 +41,24 @@ impl ExtXSessionKey {
/// ```
#[must_use]
#[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;
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 {
Ok(Self(inner))
} else {
@ -59,21 +69,21 @@ impl TryFrom<ExtXKey> for ExtXSessionKey {
/// This tag requires the same [`ProtocolVersion`] that is returned by
/// `DecryptionKey::required_version`.
impl RequiredVersion for ExtXSessionKey {
impl<'a> RequiredVersion for ExtXSessionKey<'a> {
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 {
write!(f, "{}{}", Self::PREFIX, self.0.to_string())
}
}
impl FromStr for ExtXSessionKey {
type Err = Error;
impl<'a> TryFrom<&'a str> for ExtXSessionKey<'a> {
type Error = Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
Ok(Self(DecryptionKey::from_str(tag(input, Self::PREFIX)?)?))
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
Ok(Self(DecryptionKey::try_from(tag(input, Self::PREFIX)?)?))
}
}
@ -95,7 +105,7 @@ mod test {
#[test]
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::ops::Deref;
use core::str::FromStr;
use std::borrow::Cow;
use crate::attribute::AttributePairs;
use crate::tags::ExtXMedia;
@ -67,7 +68,7 @@ use crate::Error;
/// [`PlaylistType`]: crate::types::PlaylistType
/// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
pub enum VariantStream {
pub enum VariantStream<'a> {
/// The [`VariantStream::ExtXIFrame`] variant identifies a [`MediaPlaylist`]
/// file containing the I-frames of a multimedia presentation.
/// 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
/// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly
uri: String,
uri: Cow<'a, str>,
/// Some fields are shared between [`VariantStream::ExtXStreamInf`] and
/// [`VariantStream::ExtXIFrame`].
///
/// # Note
///
/// This field is optional.
stream_data: StreamData,
stream_data: StreamData<'a>,
},
/// [`VariantStream::ExtXStreamInf`] specifies a [`VariantStream`], which is
/// a set of renditions that can be combined to play the presentation.
@ -106,7 +107,7 @@ pub enum VariantStream {
/// This field is required.
///
/// [`MediaPlaylist`]: crate::MediaPlaylist
uri: String,
uri: Cow<'a, str>,
/// The value is an unsigned float describing the maximum frame
/// rate for all the video in the [`VariantStream`].
///
@ -132,7 +133,7 @@ pub enum VariantStream {
/// [`MasterPlaylist`]: crate::MasterPlaylist
/// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type
/// [`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
/// playing the presentation.
///
@ -149,25 +150,25 @@ pub enum VariantStream {
/// [`MasterPlaylist`]: crate::MasterPlaylist
/// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type
/// [`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
/// when playing the presentation.
///
/// # Note
///
/// This field is optional.
closed_captions: Option<ClosedCaptions>,
closed_captions: Option<ClosedCaptions<'a>>,
/// Some fields are shared between [`VariantStream::ExtXStreamInf`] and
/// [`VariantStream::ExtXIFrame`].
///
/// # Note
///
/// 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_EXTXSTREAMINF: &'static str = "#EXT-X-STREAM-INF:";
@ -203,7 +204,7 @@ impl VariantStream {
/// ));
/// ```
#[must_use]
pub fn is_associated(&self, media: &ExtXMedia) -> bool {
pub fn is_associated(&self, media: &ExtXMedia<'_>) -> bool {
match &self {
Self::ExtXIFrame { stream_data, .. } => {
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`].
impl RequiredVersion for VariantStream {
impl<'a> RequiredVersion for VariantStream<'a> {
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
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 {
match &self {
Self::ExtXIFrame { uri, stream_data } => {
@ -306,10 +342,10 @@ impl fmt::Display for VariantStream {
}
}
impl FromStr for VariantStream {
type Err = Error;
impl<'a> TryFrom<&'a str> for VariantStream<'a> {
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) {
let uri = AttributePairs::new(input)
.find_map(|(key, value)| {
@ -323,7 +359,7 @@ impl FromStr for VariantStream {
Ok(Self::ExtXIFrame {
uri,
stream_data: input.parse()?,
stream_data: StreamData::try_from(input)?,
})
} else if let Ok(input) = tag(input, Self::PREFIX_EXTXSTREAMINF) {
let mut lines = input.lines();
@ -342,18 +378,20 @@ impl FromStr for VariantStream {
"FRAME-RATE" => frame_rate = Some(value.parse()?),
"AUDIO" => audio = 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 {
uri: uri.to_string(),
uri: Cow::Borrowed(uri),
frame_rate,
audio,
subtitles,
closed_captions,
stream_data: first_line.parse()?,
stream_data: StreamData::try_from(first_line)?,
})
} else {
// TODO: custom error type? + attach input data
@ -366,8 +404,8 @@ impl FromStr for VariantStream {
}
}
impl Deref for VariantStream {
type Target = StreamData;
impl<'a> Deref for VariantStream<'a> {
type Target = StreamData<'a>;
fn deref(&self) -> &Self::Target {
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) }
}
@ -386,7 +424,7 @@ impl PartialEq<&VariantStream> for VariantStream {
mod tests {
use super::*;
use crate::types::InStreamId;
//use pretty_assertions::assert_eq;
use pretty_assertions::assert_eq;
#[test]
fn test_required_version() {

View file

@ -1,5 +1,5 @@
use std::convert::TryFrom;
use std::fmt;
use std::str::FromStr;
use crate::types::ProtocolVersion;
use crate::utils::tag;
@ -28,10 +28,10 @@ impl fmt::Display for ExtXDiscontinuitySequence {
}
}
impl FromStr for ExtXDiscontinuitySequence {
type Err = Error;
impl TryFrom<&str> for ExtXDiscontinuitySequence {
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 seq_num = input.parse().map_err(|e| Error::parse_int(input, e))?;
@ -64,11 +64,11 @@ mod test {
fn test_parser() {
assert_eq!(
ExtXDiscontinuitySequence(123),
"#EXT-X-DISCONTINUITY-SEQUENCE:123".parse().unwrap()
ExtXDiscontinuitySequence::try_from("#EXT-X-DISCONTINUITY-SEQUENCE:123").unwrap()
);
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("")))
);
}

View file

@ -1,5 +1,5 @@
use std::convert::TryFrom;
use std::fmt;
use std::str::FromStr;
use crate::types::ProtocolVersion;
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) }
}
impl FromStr for ExtXEndList {
type Err = Error;
impl TryFrom<&str> for ExtXEndList {
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)?;
Ok(Self)
}
@ -47,7 +47,10 @@ mod test {
#[test]
fn test_parser() {
assert_eq!(ExtXEndList, "#EXT-X-ENDLIST".parse().unwrap());
assert_eq!(
ExtXEndList,
ExtXEndList::try_from("#EXT-X-ENDLIST").unwrap()
);
}
#[test]

View file

@ -1,5 +1,5 @@
use std::convert::TryFrom;
use std::fmt;
use std::str::FromStr;
use crate::types::ProtocolVersion;
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) }
}
impl FromStr for ExtXIFramesOnly {
type Err = Error;
impl TryFrom<&str> for ExtXIFramesOnly {
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)?;
Ok(Self)
}
@ -44,7 +44,12 @@ mod 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]
fn test_required_version() {

View file

@ -1,5 +1,5 @@
use std::convert::TryFrom;
use std::fmt;
use std::str::FromStr;
use crate::types::ProtocolVersion;
use crate::utils::tag;
@ -26,10 +26,10 @@ impl fmt::Display for ExtXMediaSequence {
}
}
impl FromStr for ExtXMediaSequence {
type Err = Error;
impl TryFrom<&str> for ExtXMediaSequence {
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 seq_num = input.parse().map_err(|e| Error::parse_int(input, e))?;
@ -62,7 +62,7 @@ mod test {
fn test_parser() {
assert_eq!(
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::str::FromStr;
use std::time::Duration;
use crate::types::ProtocolVersion;
@ -25,10 +25,10 @@ impl fmt::Display for ExtXTargetDuration {
}
}
impl FromStr for ExtXTargetDuration {
type Err = Error;
impl TryFrom<&str> for ExtXTargetDuration {
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)?
.parse()
.map_err(|e| Error::parse_int(input, e))?;
@ -62,7 +62,7 @@ mod test {
fn test_parser() {
assert_eq!(
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::str::FromStr;
use core::ops::{Add, AddAssign, Sub, SubAssign};
@ -187,13 +187,13 @@ impl fmt::Display for ExtXByteRange {
}
}
impl FromStr for ExtXByteRange {
type Err = Error;
impl TryFrom<&str> for ExtXByteRange {
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)?;
Ok(Self(ByteRange::from_str(input)?))
Ok(Self(ByteRange::try_from(input)?))
}
}
@ -219,12 +219,12 @@ mod test {
fn test_parser() {
assert_eq!(
ExtXByteRange::from(2..15),
"#EXT-X-BYTERANGE:13@2".parse().unwrap()
ExtXByteRange::try_from("#EXT-X-BYTERANGE:13@2").unwrap()
);
assert_eq!(
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::convert::TryFrom;
use std::fmt;
use std::str::FromStr;
use std::time::Duration;
#[cfg(feature = "chrono")]
@ -18,13 +19,13 @@ use crate::{Error, RequiredVersion};
#[derive(ShortHand, Builder, Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
#[builder(setter(into))]
#[shorthand(enable(must_use, into))]
pub struct ExtXDateRange {
pub struct ExtXDateRange<'a> {
/// A string that uniquely identifies an [`ExtXDateRange`] in the playlist.
///
/// ## Note
///
/// This field is required.
id: String,
id: Cow<'a, str>,
/// A client-defined string that specifies some set of attributes and their
/// associated value semantics. All [`ExtXDateRange`]s with the same class
/// attribute value must adhere to these semantics.
@ -33,7 +34,7 @@ pub struct ExtXDateRange {
///
/// This field is optional.
#[builder(setter(strip_option), default)]
class: Option<String>,
class: Option<Cow<'a, str>>,
/// The date at which the [`ExtXDateRange`] begins.
///
/// ## Note
@ -56,7 +57,7 @@ pub struct ExtXDateRange {
/// here.
#[cfg(not(feature = "chrono"))]
#[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
/// later than the value of the [`start-date`] attribute.
///
@ -79,7 +80,7 @@ pub struct ExtXDateRange {
/// [`start-date`]: #method.start_date
#[cfg(not(feature = "chrono"))]
#[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.,
/// crossing a finish line) should be represented with a duration of 0.
///
@ -114,7 +115,7 @@ pub struct ExtXDateRange {
///
/// This field is optional.
#[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
/// Telecommunications Engineers standard that describes the inline
/// insertion of cue tones in mpeg-ts streams.
@ -131,7 +132,7 @@ pub struct ExtXDateRange {
///
/// This field is optional.
#[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
/// Telecommunications Engineers standard that describes the inline
/// insertion of cue tones in mpeg-ts streams.
@ -148,7 +149,7 @@ pub struct ExtXDateRange {
///
/// This field is optional.
#[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
/// the [`ExtXDateRange::start_date`] of the following range.
///
@ -179,30 +180,25 @@ pub struct ExtXDateRange {
/// This field is optional.
#[builder(default)]
#[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.
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,
key: K,
value: V,
) -> &mut Self {
if self.client_attributes.is_none() {
self.client_attributes = Some(BTreeMap::new());
}
let attrs = self.client_attributes.get_or_insert_with(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
}
}
impl ExtXDateRange {
impl<'a> ExtXDateRange<'a> {
pub(crate) const PREFIX: &'static str = "#EXT-X-DATERANGE:";
/// 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]
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,
#[cfg(feature = "chrono")] start_date: DateTime<FixedOffset>,
#[cfg(not(feature = "chrono"))] start_date: I,
@ -316,18 +312,52 @@ let date_range = ExtXDateRange::builder()
)]
#[must_use]
#[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`].
impl RequiredVersion for ExtXDateRange {
impl<'a> RequiredVersion for ExtXDateRange<'a> {
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
}
impl FromStr for ExtXDateRange {
type Err = Error;
impl<'a> TryFrom<&'a str> for ExtXDateRange<'a> {
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 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 {
// [6.3.1. General Client Responsibilities]
// > 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 {
write!(f, "{}", Self::PREFIX)?;
write!(f, "ID={}", quote(&self.id))?;
@ -547,22 +577,20 @@ mod test {
#[test]
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"
.parse::<ExtXDateRange>()
assert!(ExtXDateRange::try_from("#EXT-X-DATERANGE:END-ON-NEXT=NO")
.is_err());
assert!("garbage".parse::<ExtXDateRange>().is_err());
assert!("".parse::<ExtXDateRange>().is_err());
assert!(ExtXDateRange::try_from("garbage").is_err());
assert!(ExtXDateRange::try_from("").is_err());
assert!(concat!(
assert!(ExtXDateRange::try_from(concat!(
"#EXT-X-DATERANGE:",
"ID=\"test_id\",",
"START-DATE=\"2014-03-05T11:15:00Z\",",
"END-ON-NEXT=YES"
)
.parse::<ExtXDateRange>()
))
.is_err());
}
}

View file

@ -1,5 +1,5 @@
use std::convert::TryFrom;
use std::fmt;
use std::str::FromStr;
use crate::types::ProtocolVersion;
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) }
}
impl FromStr for ExtXDiscontinuity {
type Err = Error;
impl TryFrom<&str> for ExtXDiscontinuity {
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)?;
Ok(Self)
}
@ -46,7 +46,12 @@ mod 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]
fn test_required_version() {

View file

@ -1,5 +1,6 @@
use std::borrow::Cow;
use std::convert::TryFrom;
use std::fmt;
use std::str::FromStr;
use std::time::Duration;
use derive_more::AsRef;
@ -12,13 +13,13 @@ use crate::{Error, RequiredVersion};
///
/// [`Media Segment`]: crate::media_segment::MediaSegment
#[derive(AsRef, Default, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ExtInf {
pub struct ExtInf<'a> {
#[as_ref]
duration: Duration,
title: Option<String>,
title: Option<Cow<'a, str>>,
}
impl ExtInf {
impl<'a> ExtInf<'a> {
pub(crate) const PREFIX: &'static str = "#EXTINF:";
/// Makes a new [`ExtInf`] tag.
@ -50,7 +51,7 @@ impl ExtInf {
/// let ext_inf = ExtInf::with_title(Duration::from_secs(5), "title");
/// ```
#[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 {
duration,
title: Some(title.into()),
@ -101,10 +102,10 @@ impl ExtInf {
///
/// 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]
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.
///
@ -118,17 +119,31 @@ impl ExtInf {
///
/// 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 {
self.title = value.map(|v| v.to_string());
pub fn set_title<T: Into<Cow<'a, str>>>(&mut self, value: Option<T>) -> &mut Self {
self.title = value.map(|v| v.into());
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
/// nanoseconds, otherwise it requires [`ProtocolVersion::V3`].
impl RequiredVersion for ExtInf {
impl<'a> RequiredVersion for ExtInf<'a> {
fn required_version(&self) -> ProtocolVersion {
if self.duration.subsec_nanos() == 0 {
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 {
write!(f, "{}", Self::PREFIX)?;
write!(f, "{},", self.duration.as_secs_f64())?;
@ -150,10 +165,10 @@ impl fmt::Display for ExtInf {
}
}
impl FromStr for ExtInf {
type Err = Error;
impl<'a> TryFrom<&'a str> for ExtInf<'a> {
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 duration = input.next().unwrap();
@ -167,13 +182,13 @@ impl FromStr for ExtInf {
.next()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToString::to_string);
.map(|v| Cow::Borrowed(v));
Ok(Self { duration, title })
}
}
impl From<Duration> for ExtInf {
impl<'a> From<Duration> for ExtInf<'a> {
fn from(value: Duration) -> Self { Self::new(value) }
}
@ -206,32 +221,32 @@ mod test {
fn test_parser() {
// #EXTINF:<duration>,[<title>]
assert_eq!(
"#EXTINF:5".parse::<ExtInf>().unwrap(),
ExtInf::try_from("#EXTINF:5").unwrap(),
ExtInf::new(Duration::from_secs(5))
);
assert_eq!(
"#EXTINF:5,".parse::<ExtInf>().unwrap(),
ExtInf::try_from("#EXTINF:5,").unwrap(),
ExtInf::new(Duration::from_secs(5))
);
assert_eq!(
"#EXTINF:5.5".parse::<ExtInf>().unwrap(),
ExtInf::try_from("#EXTINF:5.5").unwrap(),
ExtInf::new(Duration::from_millis(5500))
);
assert_eq!(
"#EXTINF:5.5,".parse::<ExtInf>().unwrap(),
ExtInf::try_from("#EXTINF:5.5,").unwrap(),
ExtInf::new(Duration::from_millis(5500))
);
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")
);
assert_eq!(
"#EXTINF:5,title".parse::<ExtInf>().unwrap(),
ExtInf::try_from("#EXTINF:5,title").unwrap(),
ExtInf::with_title(Duration::from_secs(5), "title")
);
assert!("#EXTINF:".parse::<ExtInf>().is_err());
assert!("#EXTINF:garbage".parse::<ExtInf>().is_err());
assert!(ExtInf::try_from("#EXTINF:").is_err());
assert!(ExtInf::try_from("#EXTINF:garbage").is_err());
}
#[test]
@ -239,7 +254,7 @@ mod test {
assert_eq!(ExtInf::new(Duration::from_secs(5)).title(), &None);
assert_eq!(
ExtInf::with_title(Duration::from_secs(5), "title").title(),
&Some("title".to_string())
&Some("title".into())
);
}

View file

@ -1,5 +1,5 @@
use std::convert::TryFrom;
use std::fmt;
use std::str::FromStr;
use crate::types::{DecryptionKey, ProtocolVersion};
use crate::utils::tag;
@ -9,9 +9,9 @@ use crate::{Error, RequiredVersion};
///
/// An unencrypted segment should be marked with [`ExtXKey::empty`].
#[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:";
/// Constructs an [`ExtXKey`] tag.
@ -37,7 +37,7 @@ impl ExtXKey {
/// ```
#[must_use]
#[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
/// unencrypted.
@ -124,7 +124,7 @@ impl ExtXKey {
/// let decryption_key: DecryptionKey = ExtXKey::empty().unwrap(); // panics
/// ```
#[must_use]
pub fn unwrap(self) -> DecryptionKey {
pub fn unwrap(self) -> DecryptionKey<'a> {
match self.0 {
Some(v) => v,
None => panic!("called `ExtXKey::unwrap()` on an empty key"),
@ -134,7 +134,7 @@ impl ExtXKey {
/// Returns a reference to the underlying [`DecryptionKey`].
#[must_use]
#[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>`.
///
@ -160,7 +160,19 @@ impl ExtXKey {
/// ```
#[must_use]
#[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
@ -168,7 +180,7 @@ impl ExtXKey {
/// specified.
///
/// Otherwise [`ProtocolVersion::V1`] is required.
impl RequiredVersion for ExtXKey {
impl<'a> RequiredVersion for ExtXKey<'a> {
fn required_version(&self) -> ProtocolVersion {
self.0
.as_ref()
@ -176,33 +188,33 @@ impl RequiredVersion for ExtXKey {
}
}
impl FromStr for ExtXKey {
type Err = Error;
impl<'a> TryFrom<&'a str> for ExtXKey<'a> {
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)?;
if input.trim() == "METHOD=NONE" {
Ok(Self(None))
} else {
Ok(DecryptionKey::from_str(input)?.into())
Ok(DecryptionKey::try_from(input)?.into())
}
}
}
impl From<Option<DecryptionKey>> for ExtXKey {
fn from(value: Option<DecryptionKey>) -> Self { Self(value) }
impl<'a> From<Option<DecryptionKey<'a>>> for ExtXKey<'a> {
fn from(value: Option<DecryptionKey<'a>>) -> Self { Self(value) }
}
impl From<DecryptionKey> for ExtXKey {
fn from(value: DecryptionKey) -> Self { Self(Some(value)) }
impl<'a> From<DecryptionKey<'a>> for ExtXKey<'a> {
fn from(value: DecryptionKey<'a>) -> Self { Self(Some(value)) }
}
impl From<crate::tags::ExtXSessionKey> for ExtXKey {
fn from(value: crate::tags::ExtXSessionKey) -> Self { Self(Some(value.0)) }
impl<'a> From<crate::tags::ExtXSessionKey<'a>> for ExtXKey<'a> {
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 {
write!(f, "{}", Self::PREFIX)?;
@ -232,7 +244,7 @@ mod test {
#[test]
fn test_parser() {
$(
assert_eq!($struct, $str.parse().unwrap());
assert_eq!($struct, TryFrom::try_from($str).unwrap());
)+
assert_eq!(
@ -242,15 +254,15 @@ mod test {
"http://www.example.com"
)
),
concat!(
ExtXKey::try_from(concat!(
"#EXT-X-KEY:",
"METHOD=AES-128,",
"URI=\"http://www.example.com\",",
"UNKNOWNTAG=abcd"
).parse().unwrap(),
)).unwrap(),
);
assert!("#EXT-X-KEY:METHOD=AES-128,URI=".parse::<ExtXKey>().is_err());
assert!("garbage".parse::<ExtXKey>().is_err());
assert!(ExtXKey::try_from("#EXT-X-KEY:METHOD=AES-128,URI=").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::str::FromStr;
use shorthand::ShortHand;
@ -33,18 +34,18 @@ use crate::{Decryptable, Error, RequiredVersion};
/// [`MediaPlaylist`]: crate::MediaPlaylist
#[derive(ShortHand, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[shorthand(enable(must_use, into))]
pub struct ExtXMap {
pub struct ExtXMap<'a> {
/// The `URI` that identifies a resource, that contains the media
/// initialization section.
uri: String,
uri: Cow<'a, str>,
/// The range of the media initialization section.
#[shorthand(enable(copy))]
range: Option<ByteRange>,
#[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:";
/// Makes a new [`ExtXMap`] tag.
@ -55,7 +56,8 @@ impl ExtXMap {
/// # use hls_m3u8::tags::ExtXMap;
/// 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 {
uri: uri.into(),
range: None,
@ -71,19 +73,35 @@ impl ExtXMap {
/// # use hls_m3u8::tags::ExtXMap;
/// 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 {
uri: uri.into(),
range: Some(range.into()),
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 {
fn keys(&self) -> Vec<&DecryptionKey> {
impl<'a> Decryptable<'a> for ExtXMap<'a> {
fn keys(&self) -> Vec<&DecryptionKey<'a>> {
//
self.keys.iter().filter_map(ExtXKey::as_ref).collect()
}
@ -97,7 +115,7 @@ impl Decryptable for ExtXMap {
///
/// [`ExtXIFramesOnly`]: crate::tags::ExtXIFramesOnly
/// [`MediaPlaylist`]: crate::MediaPlaylist
impl RequiredVersion for ExtXMap {
impl<'a> RequiredVersion for ExtXMap<'a> {
// this should return ProtocolVersion::V5, if it does not contain an
// EXT-X-I-FRAMES-ONLY!
// 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 }
}
impl fmt::Display for ExtXMap {
impl<'a> fmt::Display for ExtXMap<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", Self::PREFIX)?;
write!(f, "URI={}", quote(&self.uri))?;
@ -119,10 +137,10 @@ impl fmt::Display for ExtXMap {
}
}
impl FromStr for ExtXMap {
type Err = Error;
impl<'a> TryFrom<&'a str> for ExtXMap<'a> {
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 mut uri = None;
@ -132,7 +150,7 @@ impl FromStr for ExtXMap {
match key {
"URI" => uri = Some(unquote(value)),
"BYTERANGE" => {
range = Some(unquote(value).parse()?);
range = Some(unquote(value).try_into()?);
}
_ => {
// [6.3.1. General Client Responsibilities]
@ -174,18 +192,17 @@ mod test {
fn test_parser() {
assert_eq!(
ExtXMap::new("foo"),
"#EXT-X-MAP:URI=\"foo\"".parse().unwrap()
ExtXMap::try_from("#EXT-X-MAP:URI=\"foo\"").unwrap()
);
assert_eq!(
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!(
ExtXMap::with_range("foo", ByteRange::from(2..11)),
"#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\",UNKNOWN=IGNORED"
.parse()
.unwrap()
ExtXMap::try_from("#EXT-X-MAP:URI=\"foo\",BYTERANGE=\"9@2\",UNKNOWN=IGNORED").unwrap()
);
}
@ -200,6 +217,6 @@ mod test {
#[test]
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::str::FromStr;
use std::marker::PhantomData;
#[cfg(feature = "chrono")]
use chrono::{DateTime, FixedOffset, SecondsFormat};
@ -27,17 +30,18 @@ use crate::{Error, RequiredVersion};
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[cfg_attr(feature = "chrono", derive(Deref, DerefMut, Copy))]
#[non_exhaustive]
pub struct ExtXProgramDateTime {
pub struct ExtXProgramDateTime<'a> {
/// The date-time of the first sample of the associated media segment.
#[cfg(feature = "chrono")]
#[cfg_attr(feature = "chrono", deref_mut, deref)]
pub date_time: DateTime<FixedOffset>,
/// The date-time of the first sample of the associated media segment.
#[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:";
/// Makes a new [`ExtXProgramDateTime`] tag.
@ -58,7 +62,12 @@ impl ExtXProgramDateTime {
/// ```
#[must_use]
#[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.
///
@ -69,19 +78,37 @@ impl ExtXProgramDateTime {
/// let program_date_time = ExtXProgramDateTime::new("2010-02-19T14:54:23.031+08:00");
/// ```
#[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 {
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`].
impl RequiredVersion for ExtXProgramDateTime {
impl<'a> RequiredVersion for ExtXProgramDateTime<'a> {
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 {
let date_time = {
#[cfg(feature = "chrono")]
@ -97,10 +124,10 @@ impl fmt::Display for ExtXProgramDateTime {
}
}
impl FromStr for ExtXProgramDateTime {
type Err = Error;
impl<'a> TryFrom<&'a str> for ExtXProgramDateTime<'a> {
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)?;
Ok(Self::new({
@ -163,8 +190,7 @@ mod test {
"2010-02-19T14:54:23.031+08:00"
}
}),
"#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00"
.parse::<ExtXProgramDateTime>()
ExtXProgramDateTime::try_from("#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00")
.unwrap()
);
}

View file

@ -1,5 +1,5 @@
use std::convert::TryFrom;
use std::fmt;
use std::str::FromStr;
use crate::types::ProtocolVersion;
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) }
}
impl FromStr for ExtXIndependentSegments {
type Err = Error;
impl TryFrom<&str> for ExtXIndependentSegments {
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)?;
Ok(Self)
}
@ -51,7 +51,7 @@ mod test {
fn test_parser() {
assert_eq!(
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::str::FromStr;
use shorthand::ShortHand;
@ -111,10 +111,10 @@ impl fmt::Display for ExtXStart {
}
}
impl FromStr for ExtXStart {
type Err = Error;
impl TryFrom<&str> for ExtXStart {
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 mut time_offset = None;
@ -176,19 +176,17 @@ mod test {
fn test_parser() {
assert_eq!(
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!(
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!(
ExtXStart::with_precise(Float::new(1.23), true),
"#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES,UNKNOWN=TAG"
.parse()
.unwrap(),
ExtXStart::try_from("#EXT-X-START:TIME-OFFSET=1.23,PRECISE=YES,UNKNOWN=TAG").unwrap(),
);
}
}

View file

@ -1,11 +1,13 @@
use std::collections::{BTreeMap, HashMap};
use stable_vec::StableVec;
use crate::types::{DecryptionKey, ProtocolVersion};
mod private {
pub trait Sealed {}
impl Sealed for crate::MediaSegment {}
impl Sealed for crate::tags::ExtXMap {}
impl<'a> Sealed for crate::MediaSegment<'a> {}
impl<'a> Sealed for crate::tags::ExtXMap<'a> {}
}
/// Signals that a type or some of the asssociated data might need to be
@ -14,7 +16,7 @@ mod private {
/// # Note
///
/// 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.
///
/// # Example
@ -34,13 +36,13 @@ pub trait Decryptable: private::Sealed {
/// }
/// ```
#[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
/// through all keys, one might as well just get the first key.
#[must_use]
#[inline]
fn first_key(&self) -> Option<&DecryptionKey> {
fn first_key(&self) -> Option<&DecryptionKey<'a>> {
<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)]
mod tests {
use super::*;

View file

@ -1,10 +1,10 @@
use core::convert::TryInto;
use core::convert::{TryFrom, TryInto};
use core::fmt;
use core::ops::{
Add, AddAssign, Bound, Range, RangeBounds, RangeInclusive, RangeTo, RangeToInclusive, Sub,
SubAssign,
};
use core::str::FromStr;
use std::borrow::Cow;
use shorthand::ShortHand;
@ -408,10 +408,10 @@ impl fmt::Display for ByteRange {
}
}
impl FromStr for ByteRange {
type Err = Error;
impl TryFrom<&str> for ByteRange {
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 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)]
mod tests {
use super::*;
@ -658,20 +667,20 @@ mod tests {
#[test]
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!(
ByteRange::from_str("a"),
ByteRange::try_from("a"),
Err(Error::parse_int("a", "a".parse::<usize>().unwrap_err()))
);
assert_eq!(
ByteRange::from_str("1@a"),
ByteRange::try_from("1@a"),
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::str::FromStr;
use crate::utils::{quote, unquote};
/// The identifier of a closed captions group or its absence.
#[non_exhaustive]
#[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
/// playing the presentation.
///
@ -18,7 +18,7 @@ pub enum ClosedCaptions {
/// [`ExtXMedia::group_id`]: crate::tags::ExtXMedia::group_id
/// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type
/// [`MediaType::ClosedCaptions`]: crate::types::MediaType::ClosedCaptions
GroupId(String),
GroupId(Cow<'a, str>),
/// This variant indicates that there are no closed captions in
/// any [`VariantStream`] in the [`MasterPlaylist`], therefore all
/// [`VariantStream::ExtXStreamInf`] tags must have this attribute with a
@ -34,7 +34,7 @@ pub enum ClosedCaptions {
None,
}
impl ClosedCaptions {
impl<'a> ClosedCaptions<'a> {
/// Creates a [`ClosedCaptions::GroupId`] with the provided [`String`].
///
/// # Example
@ -47,13 +47,29 @@ impl ClosedCaptions {
/// 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())
}
/// 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 {
match &self {
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 {
match &self {
Self::GroupId(value) => write!(f, "{}", quote(value)),
@ -71,10 +87,10 @@ impl fmt::Display for ClosedCaptions {
}
}
impl FromStr for ClosedCaptions {
type Err = Infallible;
impl<'a> TryFrom<&'a str> for ClosedCaptions<'a> {
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" {
Ok(Self::None)
} else {
@ -102,12 +118,12 @@ mod tests {
fn test_parser() {
assert_eq!(
ClosedCaptions::None,
"NONE".parse::<ClosedCaptions>().unwrap()
ClosedCaptions::try_from("NONE").unwrap()
);
assert_eq!(
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::str::FromStr;
use std::borrow::Cow;
use derive_more::{AsMut, AsRef, Deref, DerefMut};
@ -25,11 +26,11 @@ use crate::Error;
#[derive(
AsMut, AsRef, Deref, DerefMut, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default,
)]
pub struct Codecs {
list: Vec<String>,
pub struct Codecs<'a> {
list: Vec<Cow<'a, str>>,
}
impl Codecs {
impl<'a> Codecs<'a> {
/// Makes a new (empty) [`Codecs`] struct.
///
/// # Example
@ -41,9 +42,82 @@ impl Codecs {
#[inline]
#[must_use]
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 {
if let Some(codec) = self.list.iter().next() {
write!(f, "{}", codec)?;
@ -56,20 +130,24 @@ impl fmt::Display for Codecs {
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 {
list: input.split(',').map(|s| s.into()).collect(),
})
}
}
impl<T: AsRef<str>, I: IntoIterator<Item = T>> From<I> for Codecs {
fn from(value: I) -> Self {
Self {
list: value.into_iter().map(|s| s.as_ref().to_string()).collect(),
impl<'a> TryFrom<Cow<'a, str>> for Codecs<'a> {
type Error = Error;
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]
fn test_display() {
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()
);
}
@ -94,8 +172,8 @@ mod tests {
#[test]
fn test_parser() {
assert_eq!(
Codecs::from_str("mp4a.40.2,avc1.4d401e").unwrap(),
Codecs::from(vec!["mp4a.40.2", "avc1.4d401e"])
Codecs::try_from("mp4a.40.2,avc1.4d401e").unwrap(),
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::str::FromStr;
use derive_builder::Builder;
use shorthand::ShortHand;
@ -16,7 +17,7 @@ use crate::{Error, RequiredVersion};
#[builder(setter(into), build_fn(validate = "Self::validate"))]
#[shorthand(enable(skip, must_use, into))]
#[non_exhaustive]
pub struct DecryptionKey {
pub struct DecryptionKey<'a> {
/// The encryption method, which has been used to encrypt the data.
///
/// An [`EncryptionMethod::Aes128`] signals that the data is encrypted using
@ -45,7 +46,7 @@ pub struct DecryptionKey {
/// This field is required.
#[builder(setter(into, strip_option), default)]
#[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
/// along with a secret key for data encryption.
///
@ -80,7 +81,7 @@ pub struct DecryptionKey {
pub versions: Option<KeyFormatVersions>,
}
impl DecryptionKey {
impl<'a> DecryptionKey<'a> {
/// Creates a new `DecryptionKey` from an uri pointing to the key data and
/// an `EncryptionMethod`.
///
@ -94,7 +95,7 @@ impl DecryptionKey {
/// ```
#[must_use]
#[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 {
method,
uri: uri.into(),
@ -125,7 +126,24 @@ impl DecryptionKey {
/// ```
#[must_use]
#[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
@ -133,7 +151,7 @@ impl DecryptionKey {
/// specified.
///
/// Otherwise [`ProtocolVersion::V1`] is required.
impl RequiredVersion for DecryptionKey {
impl<'a> RequiredVersion for DecryptionKey<'a> {
fn required_version(&self) -> ProtocolVersion {
if self.format.is_some() || self.versions.is_some() {
ProtocolVersion::V5
@ -145,10 +163,10 @@ impl RequiredVersion for DecryptionKey {
}
}
impl FromStr for DecryptionKey {
type Err = Error;
impl<'a> TryFrom<&'a str> for DecryptionKey<'a> {
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 uri = 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 {
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> {
// a decryption key must contain a uri and a method
if self.method.is_none() {
@ -243,19 +261,19 @@ mod test {
#[test]
fn test_parser() {
$(
assert_eq!($struct, $str.parse().unwrap());
assert_eq!($struct, TryFrom::try_from($str).unwrap());
)+
assert_eq!(
DecryptionKey::new(EncryptionMethod::Aes128, "http://www.example.com"),
concat!(
DecryptionKey::try_from(concat!(
"METHOD=AES-128,",
"URI=\"http://www.example.com\",",
"UNKNOWNTAG=abcd"
).parse().unwrap(),
)).unwrap(),
);
assert!("METHOD=AES-128,URI=".parse::<DecryptionKey>().is_err());
assert!("garbage".parse::<DecryptionKey>().is_err());
assert!(DecryptionKey::try_from("METHOD=AES-128,URI=").is_err());
assert!(DecryptionKey::try_from("garbage").is_err());
}
}
}

View file

@ -1,5 +1,5 @@
use std::convert::TryFrom;
use std::fmt;
use std::str::FromStr;
use crate::types::ProtocolVersion;
use crate::utils::tag;
@ -43,10 +43,10 @@ impl fmt::Display for PlaylistType {
}
}
impl FromStr for PlaylistType {
type Err = Error;
impl TryFrom<&str> for PlaylistType {
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)?;
match input {
"EVENT" => Ok(Self::Event),
@ -64,20 +64,18 @@ mod test {
#[test]
fn test_parser() {
assert_eq!(
"#EXT-X-PLAYLIST-TYPE:VOD".parse::<PlaylistType>().unwrap(),
PlaylistType::try_from("#EXT-X-PLAYLIST-TYPE:VOD").unwrap(),
PlaylistType::Vod,
);
assert_eq!(
"#EXT-X-PLAYLIST-TYPE:EVENT"
.parse::<PlaylistType>()
.unwrap(),
PlaylistType::try_from("#EXT-X-PLAYLIST-TYPE:EVENT").unwrap(),
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]

View file

@ -1,5 +1,6 @@
use core::convert::TryFrom;
use core::fmt;
use core::str::FromStr;
use std::borrow::Cow;
use derive_builder::Builder;
use shorthand::ShortHand;
@ -17,7 +18,7 @@ use crate::{Error, RequiredVersion};
#[builder(setter(strip_option))]
#[builder(derive(Debug, PartialEq, PartialOrd, Ord, Eq, Hash))]
#[shorthand(enable(must_use, into))]
pub struct StreamData {
pub struct StreamData<'a> {
/// The peak segment bitrate of the [`VariantStream`] in bits per second.
///
/// If all the [`MediaSegment`]s in a [`VariantStream`] have already been
@ -133,7 +134,7 @@ pub struct StreamData {
/// crate::tags::VariantStream::ExtXStreamInf
/// [RFC6381]: https://tools.ietf.org/html/rfc6381
#[builder(default, setter(into))]
codecs: Option<Codecs>,
codecs: Option<Codecs<'a>>,
/// The resolution of the stream.
///
/// # Example
@ -198,7 +199,7 @@ pub struct StreamData {
/// let mut stream = StreamData::new(20);
///
/// stream.set_video(Some("video_01"));
/// assert_eq!(stream.video(), Some(&"video_01".to_string()));
/// assert_eq!(stream.video(), Some(&"video_01".into()));
/// ```
///
/// # Note
@ -210,10 +211,10 @@ pub struct StreamData {
/// [`MasterPlaylist`]: crate::MasterPlaylist
/// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type
#[builder(default, setter(into))]
video: Option<String>,
video: Option<Cow<'a, str>>,
}
impl StreamData {
impl<'a> StreamData<'a> {
/// Creates a new [`StreamData`].
///
/// # Example
@ -253,10 +254,28 @@ impl StreamData {
/// # Ok::<(), Box<dyn ::std::error::Error>>(())
/// ```
#[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 {
write!(f, "BANDWIDTH={}", self.bandwidth)?;
@ -279,10 +298,10 @@ impl fmt::Display for StreamData {
}
}
impl FromStr for StreamData {
type Err = Error;
impl<'a> TryFrom<&'a str> for StreamData<'a> {
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 average_bandwidth = None;
let mut codecs = None;
@ -306,7 +325,7 @@ impl FromStr for StreamData {
.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()?),
"HDCP-LEVEL" => {
hdcp_level = Some(value.parse::<HdcpLevel>().map_err(Error::strum)?)
@ -334,7 +353,7 @@ impl FromStr for StreamData {
}
/// This struct requires [`ProtocolVersion::V1`].
impl RequiredVersion for StreamData {
impl<'a> RequiredVersion for StreamData<'a> {
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
fn introduced_version(&self) -> ProtocolVersion {
@ -385,18 +404,17 @@ mod tests {
assert_eq!(
stream_data,
concat!(
StreamData::try_from(concat!(
"BANDWIDTH=200,",
"AVERAGE-BANDWIDTH=15,",
"CODECS=\"mp4a.40.2,avc1.4d401e\",",
"RESOLUTION=1920x1080,",
"HDCP-LEVEL=TYPE-0,",
"VIDEO=\"video\""
)
.parse()
))
.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::str::FromStr;
use crate::types::Float;
use crate::utils::{quote, unquote};
@ -8,16 +9,33 @@ use crate::Error;
/// A `Value`.
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
pub enum Value {
pub enum Value<'a> {
/// A `String`.
String(String),
String(Cow<'a, str>),
/// A sequence of bytes.
Hex(Vec<u8>),
/// A floating point number, that's neither NaN nor infinite.
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 {
match &self {
Self::String(value) => write!(f, "{}", quote(value)),
@ -27,10 +45,10 @@ impl fmt::Display for Value {
}
}
impl FromStr for Value {
type Err = Error;
impl<'a> TryFrom<&'a str> for Value<'a> {
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") {
Ok(Self::Hex(
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()) }
}
impl From<Vec<u8>> for Value {
impl From<Vec<u8>> for Value<'static> {
fn from(value: Vec<u8>) -> Self { Self::Hex(value) }
}
impl From<String> for Value {
fn from(value: String) -> Self { Self::String(unquote(value)) }
}
impl From<&str> for Value {
fn from(value: &str) -> Self { Self::String(unquote(value)) }
impl From<String> for Value<'static> {
fn from(value: String) -> Self { Self::String(Cow::Owned(unquote(&value).into_owned())) }
}
#[cfg(test)]
@ -70,7 +84,7 @@ mod tests {
fn test_display() {
assert_eq!(Value::Float(Float::new(1.1)).to_string(), "1.1".to_string());
assert_eq!(
Value::String("&str".to_string()).to_string(),
Value::String("&str".into()).to_string(),
"\"&str\"".to_string()
);
assert_eq!(
@ -81,23 +95,31 @@ mod tests {
#[test]
fn test_parser() {
assert_eq!(Value::Float(Float::new(1.1)), "1.1".parse().unwrap());
assert_eq!(
Value::String("&str".to_string()),
"\"&str\"".parse().unwrap()
Value::Float(Float::new(1.1)),
Value::try_from("1.1").unwrap()
);
assert_eq!(Value::Hex(vec![1, 2, 3]), "0x010203".parse().unwrap());
assert_eq!(Value::Hex(vec![1, 2, 3]), "0X010203".parse().unwrap());
assert!("0x010203Z".parse::<Value>().is_err());
assert_eq!(
Value::String("&str".into()),
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]
fn test_from() {
assert_eq!(Value::from(1_u8), Value::Float(Float::new(1.0)));
assert_eq!(Value::from("\"&str\""), Value::String("&str".to_string()));
assert_eq!(
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]));
}

View file

@ -1,5 +1,7 @@
use crate::Error;
use core::iter;
use std::borrow::Cow;
use crate::Error;
/// This is an extension trait that adds the below method to `bool`.
/// 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.
/// [rfc8216#section-4.2](https://tools.ietf.org/html/rfc8216#section-4.2)
pub(crate) fn unquote<T: AsRef<str>>(value: T) -> String {
value
.as_ref()
.chars()
.filter(|c| *c != '"' && *c != '\n' && *c != '\r')
.collect()
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
.chars()
.filter(|c| *c != '"' && *c != '\n' && *c != '\r')
.collect(),
)
}
/// Puts a string inside quotes.

View file

@ -1,3 +1,5 @@
use std::convert::TryFrom;
use hls_m3u8::tags::{ExtXMedia, VariantStream};
use hls_m3u8::types::{MediaType, StreamData};
use hls_m3u8::MasterPlaylist;
@ -9,7 +11,7 @@ macro_rules! generate_tests {
$(
#[test]
fn $fnname() {
assert_eq!($struct, $str.parse().unwrap());
assert_eq!($struct, TryFrom::try_from($str).unwrap());
assert_eq!($struct.to_string(), $str.to_string());
}

View file

@ -3,6 +3,7 @@
//!
//! TODO: the rest of the tests
use std::convert::TryFrom;
use std::time::Duration;
use hls_m3u8::tags::{ExtInf, ExtXByteRange};
@ -15,7 +16,7 @@ macro_rules! generate_tests {
$(
#[test]
fn $fnname() {
assert_eq!($struct, $str.parse().unwrap());
assert_eq!($struct, TryFrom::try_from($str).unwrap());
assert_eq!($struct.to_string(), $str.to_string());
}

View file

@ -1,4 +1,5 @@
// https://tools.ietf.org/html/rfc8216#section-8
use std::convert::TryFrom;
use std::time::Duration;
use hls_m3u8::tags::{ExtInf, ExtXKey, ExtXMedia, VariantStream};
@ -11,7 +12,7 @@ macro_rules! generate_tests {
$(
#[test]
fn $fnname() {
assert_eq!($struct, $str.parse().unwrap());
assert_eq!($struct, TryFrom::try_from($str).unwrap());
assert_eq!($struct.to_string(), $str.to_string());
}