1
0
Fork 0
mirror of https://github.com/sile/hls_m3u8.git synced 2024-05-18 16:28:20 +00:00

implement VariantStream

This commit is contained in:
Luro02 2020-02-10 13:20:39 +01:00
parent 90ff18e2b3
commit e6f5091f1b
No known key found for this signature in database
GPG key ID: B66FD4F74501A9CF
15 changed files with 1201 additions and 1288 deletions

View file

@ -1,65 +1,48 @@
use std::convert::TryFrom;
use std::fmt;
use std::str::FromStr;
use core::convert::TryFrom;
use core::fmt;
use core::str::FromStr;
use crate::tags;
use crate::Error;
#[derive(Debug, Clone)]
pub(crate) struct Lines<'a> {
lines: ::core::str::Lines<'a>,
lines: ::core::iter::FilterMap<::core::str::Lines<'a>, fn(&'a str) -> Option<&'a str>>,
}
impl<'a> Iterator for Lines<'a> {
type Item = crate::Result<Line<'a>>;
fn next(&mut self) -> Option<Self::Item> {
let mut stream_inf = false;
let mut stream_inf_line = None;
let line = self.lines.next()?;
while let Some(line) = self.lines.next() {
let line = line.trim();
if line.starts_with(tags::VariantStream::PREFIX_EXTXSTREAMINF) {
let uri = self.lines.next()?;
if line.is_empty() {
continue;
}
if line.starts_with(tags::ExtXStreamInf::PREFIX) {
stream_inf = true;
stream_inf_line = Some(line);
continue;
} else if line.starts_with("#EXT") {
return Some(Tag::try_from(line).map(Line::Tag));
} else if line.starts_with('#') {
continue; // ignore comments
} else {
// stream inf line needs special treatment
if stream_inf {
stream_inf = false;
if let Some(first_line) = stream_inf_line {
return Some(
tags::ExtXStreamInf::from_str(&format!("{}\n{}", first_line, line))
.map(|v| Line::Tag(Tag::ExtXStreamInf(v))),
);
} else {
continue;
}
} else {
return Some(Ok(Line::Uri(line)));
}
}
Some(
tags::VariantStream::from_str(&format!("{}\n{}", line, uri))
.map(|v| Line::Tag(Tag::VariantStream(v))),
)
} else if line.starts_with("#EXT") {
Some(Tag::try_from(line).map(Line::Tag))
} else if line.starts_with('#') {
Some(Ok(Line::Comment(line)))
} else {
Some(Ok(Line::Uri(line)))
}
None
}
}
impl<'a> From<&'a str> for Lines<'a> {
fn from(buffer: &'a str) -> Self {
Self {
lines: buffer.lines(),
lines: buffer.lines().filter_map(|line| {
if line.trim().is_empty() {
None
} else {
Some(line.trim())
}
}),
}
}
}
@ -67,6 +50,7 @@ impl<'a> From<&'a str> for Lines<'a> {
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum Line<'a> {
Tag(Tag<'a>),
Comment(&'a str),
Uri(&'a str),
}
@ -88,12 +72,11 @@ pub(crate) enum Tag<'a> {
ExtXPlaylistType(tags::ExtXPlaylistType),
ExtXIFramesOnly(tags::ExtXIFramesOnly),
ExtXMedia(tags::ExtXMedia),
ExtXStreamInf(tags::ExtXStreamInf),
ExtXIFrameStreamInf(tags::ExtXIFrameStreamInf),
ExtXSessionData(tags::ExtXSessionData),
ExtXSessionKey(tags::ExtXSessionKey),
ExtXIndependentSegments(tags::ExtXIndependentSegments),
ExtXStart(tags::ExtXStart),
VariantStream(tags::VariantStream),
Unknown(&'a str),
}
@ -115,8 +98,7 @@ impl<'a> fmt::Display for Tag<'a> {
Self::ExtXPlaylistType(value) => value.fmt(f),
Self::ExtXIFramesOnly(value) => value.fmt(f),
Self::ExtXMedia(value) => value.fmt(f),
Self::ExtXStreamInf(value) => value.fmt(f),
Self::ExtXIFrameStreamInf(value) => value.fmt(f),
Self::VariantStream(value) => value.fmt(f),
Self::ExtXSessionData(value) => value.fmt(f),
Self::ExtXSessionKey(value) => value.fmt(f),
Self::ExtXIndependentSegments(value) => value.fmt(f),
@ -160,10 +142,13 @@ impl<'a> TryFrom<&'a str> for Tag<'a> {
input.parse().map(Self::ExtXIFramesOnly)
} else if input.starts_with(tags::ExtXMedia::PREFIX) {
input.parse().map(Self::ExtXMedia).map_err(Error::custom)
} else if input.starts_with(tags::ExtXStreamInf::PREFIX) {
input.parse().map(Self::ExtXStreamInf)
} else if input.starts_with(tags::ExtXIFrameStreamInf::PREFIX) {
input.parse().map(Self::ExtXIFrameStreamInf)
} else if input.starts_with(tags::VariantStream::PREFIX_EXTXIFRAME)
|| input.starts_with(tags::VariantStream::PREFIX_EXTXSTREAMINF)
{
input
.parse()
.map(Self::VariantStream)
.map_err(Error::custom)
} else if input.starts_with(tags::ExtXSessionData::PREFIX) {
input.parse().map(Self::ExtXSessionData)
} else if input.starts_with(tags::ExtXSessionKey::PREFIX) {

View file

@ -7,68 +7,80 @@ use shorthand::ShortHand;
use crate::line::{Line, Lines, Tag};
use crate::tags::{
ExtM3u, ExtXIFrameStreamInf, ExtXIndependentSegments, ExtXMedia, ExtXSessionData,
ExtXSessionKey, ExtXStart, ExtXStreamInf, ExtXVersion,
ExtM3u, ExtXIndependentSegments, ExtXMedia, ExtXSessionData, ExtXSessionKey, ExtXStart,
ExtXVersion, VariantStream,
};
use crate::types::{ClosedCaptions, MediaType, ProtocolVersion};
use crate::utils::tag;
use crate::{Error, RequiredVersion};
/// Master playlist.
/// The master playlist describes all of the available variants for your
/// content. Each variant is a version of the stream at a particular bitrate
/// and is contained in a separate playlist.
#[derive(ShortHand, Debug, Clone, Builder, PartialEq)]
#[builder(build_fn(validate = "Self::validate"))]
#[builder(setter(into, strip_option))]
#[shorthand(enable(must_use, get_mut, collection_magic))]
pub struct MasterPlaylist {
/// The [`ExtXIndependentSegments`] tag of the playlist.
/// The [`ExtXIndependentSegments`] tag signals that all media samples in a
/// [`MediaSegment`] can be decoded without information from other segments.
///
/// # Note
///
/// This tag is optional.
///
/// If this tag is specified it will apply to every [`MediaSegment`] in
/// every [`MediaPlaylist`] in the [`MasterPlaylist`].
///
/// [`MediaSegment`]: crate::MediaSegment
/// [`MediaPlaylist`]: crate::MediaPlaylist
#[builder(default)]
independent_segments: Option<ExtXIndependentSegments>,
/// The [`ExtXStart`] tag indicates a preferred point at which to start
/// playing a Playlist.
///
/// # Note
///
/// This tag is optional.
#[builder(default)]
independent_segments_tag: Option<ExtXIndependentSegments>,
/// The [`ExtXStart`] tag of the playlist.
start: Option<ExtXStart>,
/// The [`ExtXMedia`] tag is used to relate [`MediaPlaylist`]s,
/// that contain alternative renditions of the same content.
///
/// For example, three [`ExtXMedia`] tags can be used to identify audio-only
/// [`MediaPlaylist`]s, that contain English, French, and Spanish
/// renditions of the same presentation. Or, two [`ExtXMedia`] tags can
/// be used to identify video-only [`MediaPlaylist`]s that show two
/// different camera angles.
///
/// # Note
///
/// This tag is optional.
///
/// [`MediaPlaylist`]: crate::MediaPlaylist
#[builder(default)]
media: Vec<ExtXMedia>,
/// A list of all streams of this [`MasterPlaylist`].
///
/// # Note
///
/// This tag is optional.
#[builder(default)]
start_tag: Option<ExtXStart>,
/// The [`ExtXMedia`] tags of the playlist.
///
/// # Note
///
/// This tag is optional.
#[builder(default)]
media_tags: Vec<ExtXMedia>,
/// The [`ExtXStreamInf`] tags of the playlist.
///
/// # Note
///
/// This tag is optional.
#[builder(default)]
stream_inf_tags: Vec<ExtXStreamInf>,
/// The [`ExtXIFrameStreamInf`] tags of the playlist.
///
/// # Note
///
/// This tag is optional.
#[builder(default)]
i_frame_stream_inf_tags: Vec<ExtXIFrameStreamInf>,
variants: Vec<VariantStream>,
/// The [`ExtXSessionData`] tags of the playlist.
///
/// # Note
///
/// This tag is optional.
#[builder(default)]
session_data_tags: Vec<ExtXSessionData>,
session_data: Vec<ExtXSessionData>,
/// The [`ExtXSessionKey`] tags of the playlist.
///
/// # Note
///
/// This tag is optional.
#[builder(default)]
session_key_tags: Vec<ExtXSessionKey>,
session_keys: Vec<ExtXSessionKey>,
/// A list of tags that are unknown.
///
/// # Note
@ -79,6 +91,7 @@ pub struct MasterPlaylist {
}
impl MasterPlaylist {
// TODO: finish builder example!
/// Returns a builder for a [`MasterPlaylist`].
///
/// # Example
@ -88,7 +101,7 @@ impl MasterPlaylist {
/// use hls_m3u8::MasterPlaylist;
///
/// MasterPlaylist::builder()
/// .start_tag(ExtXStart::new(20.123456))
/// .start(ExtXStart::new(20.123456))
/// .build()?;
/// # Ok::<(), Box<dyn ::std::error::Error>>(())
/// ```
@ -98,101 +111,128 @@ impl MasterPlaylist {
impl RequiredVersion for MasterPlaylist {
fn required_version(&self) -> ProtocolVersion {
required_version![
self.independent_segments_tag,
self.start_tag,
self.media_tags,
self.stream_inf_tags,
self.i_frame_stream_inf_tags,
self.session_data_tags,
self.session_key_tags
self.independent_segments,
self.start,
self.media,
self.variants,
self.session_data,
self.session_keys
]
}
}
impl MasterPlaylistBuilder {
fn validate(&self) -> Result<(), String> {
self.validate_stream_inf_tags().map_err(|e| e.to_string())?;
self.validate_i_frame_stream_inf_tags()
.map_err(|e| e.to_string())?;
self.validate_variants().map_err(|e| e.to_string())?;
self.validate_session_data_tags()
.map_err(|e| e.to_string())?;
Ok(())
}
fn validate_stream_inf_tags(&self) -> crate::Result<()> {
if let Some(value) = &self.stream_inf_tags {
let mut has_none_closed_captions = false;
fn validate_variants(&self) -> crate::Result<()> {
if let Some(variants) = &self.variants {
self.validate_stream_inf(variants)?;
self.validate_i_frame_stream_inf(variants)?;
}
for t in value {
if let Some(group_id) = t.audio() {
Ok(())
}
fn validate_stream_inf(&self, value: &[VariantStream]) -> crate::Result<()> {
let mut has_none_closed_captions = false;
for t in value {
if let VariantStream::ExtXStreamInf {
audio,
subtitles,
closed_captions,
stream_data,
..
} = &t
{
if let Some(group_id) = &audio {
if !self.check_media_group(MediaType::Audio, group_id) {
return Err(Error::unmatched_group(group_id));
}
}
if let Some(group_id) = t.video() {
if let Some(group_id) = &stream_data.video() {
if !self.check_media_group(MediaType::Video, group_id) {
return Err(Error::unmatched_group(group_id));
}
}
if let Some(group_id) = t.subtitles() {
if let Some(group_id) = &subtitles {
if !self.check_media_group(MediaType::Subtitles, group_id) {
return Err(Error::unmatched_group(group_id));
}
}
match &t.closed_captions() {
Some(ClosedCaptions::GroupId(group_id)) => {
if !self.check_media_group(MediaType::ClosedCaptions, group_id) {
return Err(Error::unmatched_group(group_id));
if let Some(closed_captions) = &closed_captions {
match &closed_captions {
ClosedCaptions::GroupId(group_id) => {
if !self.check_media_group(MediaType::ClosedCaptions, group_id) {
return Err(Error::unmatched_group(group_id));
}
}
ClosedCaptions::None => {
has_none_closed_captions = true;
}
}
Some(ClosedCaptions::None) => {
has_none_closed_captions = true;
}
_ => {}
}
}
if has_none_closed_captions
&& !value
.iter()
.all(|t| t.closed_captions() == Some(&ClosedCaptions::None))
{
return Err(Error::invalid_input());
}
}
if has_none_closed_captions
&& !value.iter().all(|t| {
if let VariantStream::ExtXStreamInf {
closed_captions, ..
} = &t
{
closed_captions == &Some(ClosedCaptions::None)
} else {
false
}
})
{
return Err(Error::invalid_input());
}
Ok(())
}
fn validate_i_frame_stream_inf_tags(&self) -> crate::Result<()> {
if let Some(value) = &self.i_frame_stream_inf_tags {
for t in value {
if let Some(group_id) = t.video() {
fn validate_i_frame_stream_inf(&self, value: &[VariantStream]) -> crate::Result<()> {
for t in value {
if let VariantStream::ExtXIFrame { stream_data, .. } = &t {
if let Some(group_id) = stream_data.video() {
if !self.check_media_group(MediaType::Video, group_id) {
return Err(Error::unmatched_group(group_id));
}
}
}
}
Ok(())
}
fn validate_session_data_tags(&self) -> crate::Result<()> {
let mut set = HashSet::new();
if let Some(value) = &self.session_data_tags {
if let Some(value) = &self.session_data {
for t in value {
if !set.insert((t.data_id(), t.language())) {
return Err(Error::custom(format!("Conflict: {}", t)));
}
}
}
Ok(())
}
fn check_media_group<T: ToString>(&self, media_type: MediaType, group_id: T) -> bool {
if let Some(value) = &self.media_tags {
fn check_media_group<T: AsRef<str>>(&self, media_type: MediaType, group_id: T) -> bool {
if let Some(value) = &self.media {
value
.iter()
.any(|t| t.media_type() == media_type && t.group_id() == &group_id.to_string())
.any(|t| t.media_type() == media_type && t.group_id().as_str() == group_id.as_ref())
} else {
false
}
@ -206,13 +246,12 @@ impl RequiredVersion for MasterPlaylistBuilder {
// not for Option<Option<T>>)
// https://github.com/rust-lang/chalk/issues/12
required_version![
self.independent_segments_tag.flatten(),
self.start_tag.flatten(),
self.media_tags,
self.stream_inf_tags,
self.i_frame_stream_inf_tags,
self.session_data_tags,
self.session_key_tags
self.independent_segments.flatten(),
self.start.flatten(),
self.media,
self.variants,
self.session_data,
self.session_keys
]
}
}
@ -220,35 +259,32 @@ impl RequiredVersion for MasterPlaylistBuilder {
impl fmt::Display for MasterPlaylist {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "{}", ExtM3u)?;
if self.required_version() != ProtocolVersion::V1 {
writeln!(f, "{}", ExtXVersion::new(self.required_version()))?;
}
for t in &self.media_tags {
for t in &self.media {
writeln!(f, "{}", t)?;
}
for t in &self.stream_inf_tags {
for t in &self.variants {
writeln!(f, "{}", t)?;
}
for t in &self.i_frame_stream_inf_tags {
for t in &self.session_data {
writeln!(f, "{}", t)?;
}
for t in &self.session_data_tags {
for t in &self.session_keys {
writeln!(f, "{}", t)?;
}
for t in &self.session_key_tags {
writeln!(f, "{}", t)?;
}
if let Some(value) = &self.independent_segments_tag {
if let Some(value) = &self.independent_segments {
writeln!(f, "{}", value)?;
}
if let Some(value) = &self.start_tag {
if let Some(value) = &self.start {
writeln!(f, "{}", value)?;
}
@ -267,11 +303,10 @@ impl FromStr for MasterPlaylist {
let input = tag(input, ExtM3u::PREFIX)?;
let mut builder = Self::builder();
let mut media_tags = vec![];
let mut stream_inf_tags = vec![];
let mut i_frame_stream_inf_tags = vec![];
let mut session_data_tags = vec![];
let mut session_key_tags = vec![];
let mut media = vec![];
let mut variants = vec![];
let mut session_data = vec![];
let mut session_keys = vec![];
let mut unknown_tags = vec![];
for line in Lines::from(input) {
@ -303,25 +338,22 @@ impl FromStr for MasterPlaylist {
)));
}
Tag::ExtXMedia(t) => {
media_tags.push(t);
media.push(t);
}
Tag::ExtXStreamInf(t) => {
stream_inf_tags.push(t);
}
Tag::ExtXIFrameStreamInf(t) => {
i_frame_stream_inf_tags.push(t);
Tag::VariantStream(t) => {
variants.push(t);
}
Tag::ExtXSessionData(t) => {
session_data_tags.push(t);
session_data.push(t);
}
Tag::ExtXSessionKey(t) => {
session_key_tags.push(t);
session_keys.push(t);
}
Tag::ExtXIndependentSegments(t) => {
builder.independent_segments_tag(t);
builder.independent_segments(t);
}
Tag::ExtXStart(t) => {
builder.start_tag(t);
builder.start(t);
}
_ => {
// [6.3.1. General Client Responsibilities]
@ -333,14 +365,14 @@ impl FromStr for MasterPlaylist {
Line::Uri(uri) => {
return Err(Error::custom(format!("Unexpected URI: {:?}", uri)));
}
_ => {}
}
}
builder.media_tags(media_tags);
builder.stream_inf_tags(stream_inf_tags);
builder.i_frame_stream_inf_tags(i_frame_stream_inf_tags);
builder.session_data_tags(session_data_tags);
builder.session_key_tags(session_key_tags);
builder.media(media);
builder.variants(variants);
builder.session_data(session_data);
builder.session_keys(session_keys);
builder.unknown_tags(unknown_tags);
builder.build().map_err(Error::builder)

View file

@ -27,56 +27,56 @@ pub struct MediaPlaylist {
///
/// This field is required.
#[shorthand(enable(copy))]
target_duration_tag: ExtXTargetDuration,
target_duration: ExtXTargetDuration,
/// Sets the [`ExtXMediaSequence`] tag.
///
/// # Note
///
/// This field is optional.
#[builder(default)]
media_sequence_tag: Option<ExtXMediaSequence>,
media_sequence: Option<ExtXMediaSequence>,
/// Sets the [`ExtXDiscontinuitySequence`] tag.
///
/// # Note
///
/// This field is optional.
#[builder(default)]
discontinuity_sequence_tag: Option<ExtXDiscontinuitySequence>,
discontinuity_sequence: Option<ExtXDiscontinuitySequence>,
/// Sets the [`ExtXPlaylistType`] tag.
///
/// # Note
///
/// This field is optional.
#[builder(default)]
playlist_type_tag: Option<ExtXPlaylistType>,
playlist_type: Option<ExtXPlaylistType>,
/// Sets the [`ExtXIFramesOnly`] tag.
///
/// # Note
///
/// This field is optional.
#[builder(default)]
i_frames_only_tag: Option<ExtXIFramesOnly>,
i_frames_only: Option<ExtXIFramesOnly>,
/// Sets the [`ExtXIndependentSegments`] tag.
///
/// # Note
///
/// This field is optional.
#[builder(default)]
independent_segments_tag: Option<ExtXIndependentSegments>,
independent_segments: Option<ExtXIndependentSegments>,
/// Sets the [`ExtXStart`] tag.
///
/// # Note
///
/// This field is optional.
#[builder(default)]
start_tag: Option<ExtXStart>,
start: Option<ExtXStart>,
/// Sets the [`ExtXEndList`] tag.
///
/// # Note
///
/// This field is optional.
#[builder(default)]
end_list_tag: Option<ExtXEndList>,
end_list: Option<ExtXEndList>,
/// A list of all [`MediaSegment`]s.
///
/// # Note
@ -110,7 +110,7 @@ pub struct MediaPlaylist {
impl MediaPlaylistBuilder {
fn validate(&self) -> Result<(), String> {
if let Some(target_duration) = &self.target_duration_tag {
if let Some(target_duration) = &self.target_duration {
self.validate_media_segments(target_duration.duration())
.map_err(|e| e.to_string())?;
}
@ -189,14 +189,14 @@ impl MediaPlaylistBuilder {
impl RequiredVersion for MediaPlaylistBuilder {
fn required_version(&self) -> ProtocolVersion {
required_version![
self.target_duration_tag,
self.media_sequence_tag,
self.discontinuity_sequence_tag,
self.playlist_type_tag,
self.i_frames_only_tag,
self.independent_segments_tag,
self.start_tag,
self.end_list_tag,
self.target_duration,
self.media_sequence,
self.discontinuity_sequence,
self.playlist_type,
self.i_frames_only,
self.independent_segments,
self.start,
self.end_list,
self.segments
]
}
@ -210,14 +210,14 @@ impl MediaPlaylist {
impl RequiredVersion for MediaPlaylist {
fn required_version(&self) -> ProtocolVersion {
required_version![
self.target_duration_tag,
self.media_sequence_tag,
self.discontinuity_sequence_tag,
self.playlist_type_tag,
self.i_frames_only_tag,
self.independent_segments_tag,
self.start_tag,
self.end_list_tag,
self.target_duration,
self.media_sequence,
self.discontinuity_sequence,
self.playlist_type,
self.i_frames_only,
self.independent_segments,
self.start,
self.end_list,
self.segments
]
}
@ -231,29 +231,29 @@ impl fmt::Display for MediaPlaylist {
writeln!(f, "{}", ExtXVersion::new(self.required_version()))?;
}
writeln!(f, "{}", self.target_duration_tag)?;
writeln!(f, "{}", self.target_duration)?;
if let Some(value) = &self.media_sequence_tag {
if let Some(value) = &self.media_sequence {
writeln!(f, "{}", value)?;
}
if let Some(value) = &self.discontinuity_sequence_tag {
if let Some(value) = &self.discontinuity_sequence {
writeln!(f, "{}", value)?;
}
if let Some(value) = &self.playlist_type_tag {
if let Some(value) = &self.playlist_type {
writeln!(f, "{}", value)?;
}
if let Some(value) = &self.i_frames_only_tag {
if let Some(value) = &self.i_frames_only {
writeln!(f, "{}", value)?;
}
if let Some(value) = &self.independent_segments_tag {
if let Some(value) = &self.independent_segments {
writeln!(f, "{}", value)?;
}
if let Some(value) = &self.start_tag {
if let Some(value) = &self.start {
writeln!(f, "{}", value)?;
}
@ -261,7 +261,7 @@ impl fmt::Display for MediaPlaylist {
write!(f, "{}", segment)?;
}
if let Some(value) = &self.end_list_tag {
if let Some(value) = &self.end_list {
writeln!(f, "{}", value)?;
}
@ -341,10 +341,10 @@ fn parse_media_playlist(
segment.date_range_tag(t);
}
Tag::ExtXTargetDuration(t) => {
builder.target_duration_tag(t);
builder.target_duration(t);
}
Tag::ExtXMediaSequence(t) => {
builder.media_sequence_tag(t);
builder.media_sequence(t);
}
Tag::ExtXDiscontinuitySequence(t) => {
if segments.is_empty() {
@ -353,29 +353,28 @@ fn parse_media_playlist(
if has_discontinuity_tag {
return Err(Error::invalid_input());
}
builder.discontinuity_sequence_tag(t);
builder.discontinuity_sequence(t);
}
Tag::ExtXEndList(t) => {
builder.end_list_tag(t);
builder.end_list(t);
}
Tag::ExtXPlaylistType(t) => {
builder.playlist_type_tag(t);
builder.playlist_type(t);
}
Tag::ExtXIFramesOnly(t) => {
builder.i_frames_only_tag(t);
builder.i_frames_only(t);
}
Tag::ExtXMedia(_)
| Tag::ExtXStreamInf(_)
| Tag::ExtXIFrameStreamInf(_)
| Tag::VariantStream(_)
| Tag::ExtXSessionData(_)
| Tag::ExtXSessionKey(_) => {
return Err(Error::unexpected_tag(tag));
}
Tag::ExtXIndependentSegments(t) => {
builder.independent_segments_tag(t);
builder.independent_segments(t);
}
Tag::ExtXStart(t) => {
builder.start_tag(t);
builder.start(t);
}
Tag::ExtXVersion(_) => {}
Tag::Unknown(_) => {

View file

@ -1,263 +0,0 @@
use std::fmt;
use std::str::FromStr;
use derive_more::{Deref, DerefMut};
use crate::attribute::AttributePairs;
use crate::types::{HdcpLevel, ProtocolVersion, StreamInf, StreamInfBuilder};
use crate::utils::{quote, tag, unquote};
use crate::{Error, RequiredVersion};
/// # [4.3.5.3. EXT-X-I-FRAME-STREAM-INF]
///
/// The [`ExtXIFrameStreamInf`] tag identifies a [`Media Playlist`] file,
/// containing the I-frames of a multimedia presentation.
///
/// I-frames are encoded video frames, whose decoding
/// does not depend on any other frame.
///
/// [`Master Playlist`]: crate::MasterPlaylist
/// [`Media Playlist`]: crate::MediaPlaylist
/// [4.3.5.3. EXT-X-I-FRAME-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5
#[derive(Deref, DerefMut, PartialOrd, Debug, Clone, PartialEq, Eq, Hash)]
pub struct ExtXIFrameStreamInf {
uri: String,
#[deref]
#[deref_mut]
stream_inf: StreamInf,
}
/// Builder for [`ExtXIFrameStreamInf`].
#[derive(Default, Debug, Clone, PartialEq)]
pub struct ExtXIFrameStreamInfBuilder {
uri: Option<String>,
stream_inf: StreamInfBuilder,
}
impl ExtXIFrameStreamInfBuilder {
/// An `URI` to the [`MediaPlaylist`] file.
///
/// [`MediaPlaylist`]: crate::MediaPlaylist
pub fn uri<T: Into<String>>(&mut self, value: T) -> &mut Self {
self.uri = Some(value.into());
self
}
/// The maximum bandwidth of the stream.
pub fn bandwidth(&mut self, value: u64) -> &mut Self {
self.stream_inf.bandwidth(value);
self
}
/// The average bandwidth of the stream.
pub fn average_bandwidth(&mut self, value: u64) -> &mut Self {
self.stream_inf.average_bandwidth(value);
self
}
/// Every media format in any of the renditions specified by the Variant
/// Stream.
pub fn codecs<T: Into<String>>(&mut self, value: T) -> &mut Self {
self.stream_inf.codecs(value);
self
}
/// The resolution of the stream.
pub fn resolution(&mut self, value: (usize, usize)) -> &mut Self {
self.stream_inf.resolution(value);
self
}
/// High-bandwidth Digital Content Protection
pub fn hdcp_level(&mut self, value: HdcpLevel) -> &mut Self {
self.stream_inf.hdcp_level(value);
self
}
/// It indicates the set of video renditions, that should be used when
/// playing the presentation.
pub fn video<T: Into<String>>(&mut self, value: T) -> &mut Self {
self.stream_inf.video(value);
self
}
/// Build an [`ExtXIFrameStreamInf`].
pub fn build(&self) -> crate::Result<ExtXIFrameStreamInf> {
Ok(ExtXIFrameStreamInf {
uri: self
.uri
.clone()
.ok_or_else(|| Error::missing_value("frame rate"))?,
stream_inf: self.stream_inf.build().map_err(Error::builder)?,
})
}
}
impl ExtXIFrameStreamInf {
pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAME-STREAM-INF:";
/// Makes a new [`ExtXIFrameStreamInf`] tag.
///
/// # Example
///
/// ```
/// # use hls_m3u8::tags::ExtXIFrameStreamInf;
/// let stream = ExtXIFrameStreamInf::new("https://www.example.com", 20);
/// ```
pub fn new<T: ToString>(uri: T, bandwidth: u64) -> Self {
Self {
uri: uri.to_string(),
stream_inf: StreamInf::new(bandwidth),
}
}
/// Returns a builder for [`ExtXIFrameStreamInf`].
pub fn builder() -> ExtXIFrameStreamInfBuilder { ExtXIFrameStreamInfBuilder::default() }
/// Returns the `URI`, that identifies the associated [`media playlist`].
///
/// # Example
///
/// ```
/// # use hls_m3u8::tags::ExtXIFrameStreamInf;
/// let stream = ExtXIFrameStreamInf::new("https://www.example.com", 20);
/// assert_eq!(stream.uri(), &"https://www.example.com".to_string());
/// ```
///
/// [`media playlist`]: crate::MediaPlaylist
pub const fn uri(&self) -> &String { &self.uri }
/// Sets the `URI`, that identifies the associated [`media playlist`].
///
/// # Example
///
/// ```
/// # use hls_m3u8::tags::ExtXIFrameStreamInf;
/// #
/// let mut stream = ExtXIFrameStreamInf::new("https://www.example.com", 20);
///
/// stream.set_uri("../new/uri");
/// assert_eq!(stream.uri(), &"../new/uri".to_string());
/// ```
///
/// [`media playlist`]: crate::MediaPlaylist
pub fn set_uri<T: ToString>(&mut self, value: T) -> &mut Self {
self.uri = value.to_string();
self
}
}
/// This tag requires [`ProtocolVersion::V1`].
impl RequiredVersion for ExtXIFrameStreamInf {
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
}
impl fmt::Display for ExtXIFrameStreamInf {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", Self::PREFIX)?;
write!(f, "URI={},{}", quote(&self.uri), self.stream_inf)?;
Ok(())
}
}
impl FromStr for ExtXIFrameStreamInf {
type Err = Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let input = tag(input, Self::PREFIX)?;
let mut uri = None;
for (key, value) in AttributePairs::new(input) {
if key == "URI" {
uri = Some(unquote(value));
}
}
let uri = uri.ok_or_else(|| Error::missing_value("URI"))?;
Ok(Self {
uri,
stream_inf: input.parse()?,
})
}
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_builder() {
let mut i_frame_stream_inf =
ExtXIFrameStreamInf::new("http://example.com/audio-only.m3u8", 200_000);
i_frame_stream_inf
.set_average_bandwidth(Some(100_000))
.set_codecs(Some("mp4a.40.5"))
.set_resolution(Some((1920, 1080)))
.set_hdcp_level(Some(HdcpLevel::None))
.set_video(Some("video"));
assert_eq!(
ExtXIFrameStreamInf::builder()
.uri("http://example.com/audio-only.m3u8")
.bandwidth(200_000)
.average_bandwidth(100_000)
.codecs("mp4a.40.5")
.resolution((1920, 1080))
.hdcp_level(HdcpLevel::None)
.video("video")
.build()
.unwrap(),
i_frame_stream_inf
);
}
#[test]
fn test_display() {
assert_eq!(
ExtXIFrameStreamInf::new("foo", 1000).to_string(),
"#EXT-X-I-FRAME-STREAM-INF:URI=\"foo\",BANDWIDTH=1000".to_string()
);
}
#[test]
fn test_parser() {
assert_eq!(
"#EXT-X-I-FRAME-STREAM-INF:URI=\"foo\",BANDWIDTH=1000"
.parse::<ExtXIFrameStreamInf>()
.unwrap(),
ExtXIFrameStreamInf::new("foo", 1000)
);
assert!("garbage".parse::<ExtXIFrameStreamInf>().is_err());
}
#[test]
fn test_required_version() {
assert_eq!(
ExtXIFrameStreamInf::new("foo", 1000).required_version(),
ProtocolVersion::V1
);
}
#[test]
fn test_deref() {
assert_eq!(
ExtXIFrameStreamInf::new("https://www.example.com", 20).average_bandwidth(),
None
)
}
#[test]
fn test_deref_mut() {
assert_eq!(
ExtXIFrameStreamInf::new("https://www.example.com", 20)
.set_average_bandwidth(Some(4))
.average_bandwidth(),
Some(4)
)
}
}

View file

@ -1,11 +1,9 @@
mod i_frame_stream_inf;
mod media;
mod session_data;
mod session_key;
mod stream_inf;
mod variant_stream;
pub use i_frame_stream_inf::*;
pub use media::*;
pub use session_data::*;
pub use session_key::*;
pub use stream_inf::*;
pub use variant_stream::*;

View file

@ -1,304 +0,0 @@
use std::fmt;
use std::str::FromStr;
use derive_more::{Deref, DerefMut};
use shorthand::ShortHand;
use crate::attribute::AttributePairs;
use crate::types::{
ClosedCaptions, DecimalFloatingPoint, HdcpLevel, ProtocolVersion, StreamInf, StreamInfBuilder,
};
use crate::utils::{quote, tag, unquote};
use crate::{Error, RequiredVersion};
/// # [4.3.4.2. EXT-X-STREAM-INF]
///
/// The [`ExtXStreamInf`] tag specifies a Variant Stream, which is a set
/// of Renditions that can be combined to play the presentation. The
/// attributes of the tag provide information about the Variant Stream.
///
/// The URI line that follows the [`ExtXStreamInf`] tag specifies a Media
/// Playlist that carries a rendition of the Variant Stream. The URI
/// line is REQUIRED. Clients that do not support multiple video
/// Renditions SHOULD play this Rendition.
///
/// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2
#[derive(Deref, DerefMut, ShortHand, PartialOrd, Debug, Clone, PartialEq)]
#[shorthand(enable(must_use, into))]
pub struct ExtXStreamInf {
/// The `URI` that identifies the associated media playlist.
uri: String,
#[shorthand(enable(skip))]
frame_rate: Option<DecimalFloatingPoint>,
/// The group identifier for the audio in the variant stream.
audio: Option<String>,
/// The group identifier for the subtitles in the variant stream.
subtitles: Option<String>,
/// The value of the [`ClosedCaptions`] attribute.
closed_captions: Option<ClosedCaptions>,
#[shorthand(enable(skip))]
#[deref]
#[deref_mut]
stream_inf: StreamInf,
}
#[derive(Default, Debug, Clone)]
/// Builder for [`ExtXStreamInf`].
pub struct ExtXStreamInfBuilder {
uri: Option<String>,
frame_rate: Option<DecimalFloatingPoint>,
audio: Option<String>,
subtitles: Option<String>,
closed_captions: Option<ClosedCaptions>,
stream_inf: StreamInfBuilder,
}
impl ExtXStreamInfBuilder {
/// An `URI` to the [`MediaPlaylist`] file.
///
/// [`MediaPlaylist`]: crate::MediaPlaylist
pub fn uri<T: Into<String>>(&mut self, value: T) -> &mut Self {
self.uri = Some(value.into());
self
}
/// Maximum frame rate for all the video in the variant stream.
pub fn frame_rate(&mut self, value: f64) -> &mut Self {
self.frame_rate = Some(value.into());
self
}
/// The group identifier for the audio in the variant stream.
pub fn audio<T: Into<String>>(&mut self, value: T) -> &mut Self {
self.audio = Some(value.into());
self
}
/// The group identifier for the subtitles in the variant stream.
pub fn subtitles<T: Into<String>>(&mut self, value: T) -> &mut Self {
self.subtitles = Some(value.into());
self
}
/// The value of [`ClosedCaptions`] attribute.
pub fn closed_captions<T: Into<ClosedCaptions>>(&mut self, value: T) -> &mut Self {
self.closed_captions = Some(value.into());
self
}
/// The maximum bandwidth of the stream.
pub fn bandwidth(&mut self, value: u64) -> &mut Self {
self.stream_inf.bandwidth(value);
self
}
/// The average bandwidth of the stream.
pub fn average_bandwidth(&mut self, value: u64) -> &mut Self {
self.stream_inf.average_bandwidth(value);
self
}
/// Every media format in any of the renditions specified by the Variant
/// Stream.
pub fn codecs<T: Into<String>>(&mut self, value: T) -> &mut Self {
self.stream_inf.codecs(value);
self
}
/// The resolution of the stream.
pub fn resolution(&mut self, value: (usize, usize)) -> &mut Self {
self.stream_inf.resolution(value);
self
}
/// High-bandwidth Digital Content Protection
pub fn hdcp_level(&mut self, value: HdcpLevel) -> &mut Self {
self.stream_inf.hdcp_level(value);
self
}
/// It indicates the set of video renditions, that should be used when
/// playing the presentation.
pub fn video<T: Into<String>>(&mut self, value: T) -> &mut Self {
self.stream_inf.video(value);
self
}
/// Build an [`ExtXStreamInf`].
pub fn build(&self) -> crate::Result<ExtXStreamInf> {
Ok(ExtXStreamInf {
uri: self
.uri
.clone()
.ok_or_else(|| Error::missing_value("frame rate"))?,
frame_rate: self.frame_rate,
audio: self.audio.clone(),
subtitles: self.subtitles.clone(),
closed_captions: self.closed_captions.clone(),
stream_inf: self.stream_inf.build().map_err(Error::builder)?,
})
}
}
impl ExtXStreamInf {
pub(crate) const PREFIX: &'static str = "#EXT-X-STREAM-INF:";
/// Creates a new [`ExtXStreamInf`] tag.
///
/// # Example
///
/// ```
/// # use hls_m3u8::tags::ExtXStreamInf;
/// let stream = ExtXStreamInf::new("https://www.example.com/", 20);
/// ```
pub fn new<T: ToString>(uri: T, bandwidth: u64) -> Self {
Self {
uri: uri.to_string(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_inf: StreamInf::new(bandwidth),
}
}
/// Returns a builder for [`ExtXStreamInf`].
pub fn builder() -> ExtXStreamInfBuilder { ExtXStreamInfBuilder::default() }
/// The maximum frame rate for all the video in the variant stream.
#[must_use]
#[inline]
pub fn frame_rate(&self) -> Option<f64> { self.frame_rate.map(DecimalFloatingPoint::as_f64) }
/// The maximum frame rate for all the video in the variant stream.
///
/// # Panic
///
/// This function panics, if the float is infinite or negative.
pub fn set_frame_rate<VALUE: Into<f64>>(&mut self, value_0: Option<VALUE>) -> &mut Self {
self.frame_rate = value_0.map(|v| {
DecimalFloatingPoint::new(v.into()).expect("the float must be positive and finite")
});
self
}
}
/// This tag requires [`ProtocolVersion::V1`].
impl RequiredVersion for ExtXStreamInf {
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
}
impl fmt::Display for ExtXStreamInf {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}{}", Self::PREFIX, self.stream_inf)?;
if let Some(value) = &self.frame_rate {
write!(f, ",FRAME-RATE={:.3}", value.as_f64())?;
}
if let Some(value) = &self.audio {
write!(f, ",AUDIO={}", quote(value))?;
}
if let Some(value) = &self.subtitles {
write!(f, ",SUBTITLES={}", quote(value))?;
}
if let Some(value) = &self.closed_captions {
write!(f, ",CLOSED-CAPTIONS={}", value)?;
}
write!(f, "\n{}", self.uri)?;
Ok(())
}
}
impl FromStr for ExtXStreamInf {
type Err = Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let mut lines = input.lines();
let first_line = lines
.next()
.ok_or_else(|| Error::missing_value("first_line"))?;
let uri = lines.next().ok_or_else(|| Error::missing_value("URI"))?;
let input = tag(first_line, Self::PREFIX)?;
let mut frame_rate = None;
let mut audio = None;
let mut subtitles = None;
let mut closed_captions = None;
for (key, value) in AttributePairs::new(input) {
match key {
"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()),
_ => {}
}
}
Ok(Self {
uri: uri.to_string(),
frame_rate,
audio,
subtitles,
closed_captions,
stream_inf: input.parse()?,
})
}
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_parser() {
let stream_inf = "#EXT-X-STREAM-INF:BANDWIDTH=1000\nhttp://www.example.com"
.parse::<ExtXStreamInf>()
.unwrap();
assert_eq!(
stream_inf,
ExtXStreamInf::new("http://www.example.com", 1000)
);
}
#[test]
fn test_display() {
assert_eq!(
ExtXStreamInf::new("http://www.example.com/", 1000).to_string(),
"#EXT-X-STREAM-INF:BANDWIDTH=1000\nhttp://www.example.com/".to_string()
);
}
#[test]
fn test_required_version() {
assert_eq!(
ProtocolVersion::V1,
ExtXStreamInf::new("http://www.example.com", 1000).required_version()
);
}
#[test]
fn test_deref() {
assert_eq!(
ExtXStreamInf::new("http://www.example.com", 1000).bandwidth(),
1000
);
}
#[test]
fn test_deref_mut() {
assert_eq!(
ExtXStreamInf::new("http://www.example.com", 1000)
.set_bandwidth(1)
.bandwidth(),
1
);
}
}

View file

@ -0,0 +1,286 @@
use core::fmt;
use core::ops::Deref;
use core::str::FromStr;
use crate::attribute::AttributePairs;
use crate::traits::RequiredVersion;
use crate::types::{ClosedCaptions, ProtocolVersion, StreamData, UFloat};
use crate::utils::{quote, tag, unquote};
use crate::Error;
/// A server MAY offer multiple Media Playlist files to provide different
/// encodings of the same presentation. If it does so, it SHOULD provide
/// a Master Playlist file that lists each Variant Stream to allow
/// clients to switch between encodings dynamically.
///
/// Master Playlists describe regular Variant Streams with EXT-X-STREAM-
/// INF tags and I-frame Variant Streams with EXT-X-I-FRAME-STREAM-INF
/// tags.
///
/// If an EXT-X-STREAM-INF tag or EXT-X-I-FRAME-STREAM-INF tag contains
/// the CODECS attribute, the attribute value MUST include every media
/// format [RFC6381] present in any Media Segment in any of the
/// Renditions specified by the Variant Stream.
///
/// The server MUST meet the following constraints when producing Variant
/// Streams in order to allow clients to switch between them seamlessly:
///
/// o Each Variant Stream MUST present the same content.
///
///
/// o Matching content in Variant Streams MUST have matching timestamps.
/// This allows clients to synchronize the media.
///
/// o Matching content in Variant Streams MUST have matching
/// Discontinuity Sequence Numbers (see Section 4.3.3.3).
///
/// o Each Media Playlist in each Variant Stream MUST have the same
/// target duration. The only exceptions are SUBTITLES Renditions and
/// Media Playlists containing an EXT-X-I-FRAMES-ONLY tag, which MAY
/// have different target durations if they have an EXT-X-PLAYLIST-
/// TYPE of VOD.
///
/// o Content that appears in a Media Playlist of one Variant Stream but
/// not in another MUST appear either at the beginning or at the end
/// of the Media Playlist file and MUST NOT be longer than the target
/// duration.
///
/// o If any Media Playlists have an EXT-X-PLAYLIST-TYPE tag, all Media
/// Playlists MUST have an EXT-X-PLAYLIST-TYPE tag with the same
/// value.
///
/// o If the Playlist contains an EXT-X-PLAYLIST-TYPE tag with the value
/// of VOD, the first segment of every Media Playlist in every Variant
/// Stream MUST start at the same media timestamp.
///
/// o If any Media Playlist in a Master Playlist contains an EXT-X-
/// PROGRAM-DATE-TIME tag, then all Media Playlists in that Master
/// Playlist MUST contain EXT-X-PROGRAM-DATE-TIME tags with consistent
/// mappings of date and time to media timestamps.
///
/// o Each Variant Stream MUST contain the same set of Date Ranges, each
/// one identified by an EXT-X-DATERANGE tag(s) with the same ID
/// attribute value and containing the same set of attribute/value
/// pairs.
///
/// In addition, for broadest compatibility, Variant Streams SHOULD
/// contain the same encoded audio bitstream. This allows clients to
/// switch between Variant Streams without audible glitching.
///
/// The rules for Variant Streams also apply to alternative Renditions
/// (see Section 4.3.4.2.1).
///
/// [RFC6381]: https://tools.ietf.org/html/rfc6381
#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub enum VariantStream {
ExtXIFrame {
/// The URI identifies the I-frame [`MediaPlaylist`] file.
/// That Playlist file must contain an [`ExtXIFramesOnly`] tag.
///
/// # Note
///
/// This field is required.
///
/// [`MediaPlaylist`]: crate::MediaPlaylist
uri: String,
/// Some fields are shared between [`VariantStream::ExtXStreamInf`] and
/// [`VariantStream::ExtXIFrame`].
///
/// # Note
///
/// This field is optional.
stream_data: StreamData,
},
ExtXStreamInf {
/// The URI specifies a [`MediaPlaylist`] that carries a rendition of
/// the [`VariantStream`]. Clients that do not support multiple video
/// renditions should play this rendition.
///
/// # Note
///
/// This field is required.
///
/// [`MediaPlaylist`]: crate::MediaPlaylist
uri: String,
/// The value is an unsigned float describing the maximum frame
/// rate for all the video in the [`VariantStream`].
///
/// # Note
///
/// Specifying the frame rate is optional, but is recommended if the
/// [`VariantStream`] includes video. It should be specified if any
/// video exceeds 30 frames per second.
frame_rate: Option<UFloat>,
/// It indicates the set of audio renditions that should be used when
/// playing the presentation.
///
/// It must match the value of the [`ExtXMedia::group_id`] of an
/// [`ExtXMedia`] tag elsewhere in the [`MasterPlaylist`] whose
/// [`ExtXMedia::media_type`] is [`MediaType::Audio`].
///
/// # Note
///
/// This field is optional.
///
/// [`ExtXMedia`]: crate::tags::ExtXMedia
/// [`ExtXMedia::group_id`]: crate::tags::ExtXMedia::group_id
/// [`MasterPlaylist`]: crate::MasterPlaylist
/// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type
/// [`MediaType::Audio`]: crate::types::MediaType::Audio
audio: Option<String>,
/// It indicates the set of subtitle renditions that can be used when
/// playing the presentation.
///
/// It must match the value of the [`ExtXMedia::group_id`] of an
/// [`ExtXMedia`] tag elsewhere in the [`MasterPlaylist`] whose
/// [`ExtXMedia::media_type`] is [`MediaType::Subtitles`].
///
/// # Note
///
/// This field is optional.
///
/// [`ExtXMedia`]: crate::tags::ExtXMedia
/// [`ExtXMedia::group_id`]: crate::tags::ExtXMedia::group_id
/// [`MasterPlaylist`]: crate::MasterPlaylist
/// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type
/// [`MediaType::Subtitles`]: crate::types::MediaType::Subtitles
subtitles: Option<String>,
/// 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>,
/// Some fields are shared between [`VariantStream::ExtXStreamInf`] and
/// [`VariantStream::ExtXIFrame`].
///
/// # Note
///
/// This field is optional.
stream_data: StreamData,
},
}
impl VariantStream {
pub(crate) const PREFIX_EXTXIFRAME: &'static str = "#EXT-X-I-FRAME-STREAM-INF:";
pub(crate) const PREFIX_EXTXSTREAMINF: &'static str = "#EXT-X-STREAM-INF:";
}
/// This tag requires [`ProtocolVersion::V1`].
impl RequiredVersion for VariantStream {
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
}
impl fmt::Display for VariantStream {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self {
Self::ExtXIFrame { uri, stream_data } => {
write!(f, "{}", Self::PREFIX_EXTXIFRAME)?;
write!(f, "URI={},{}", quote(uri), stream_data)?;
}
Self::ExtXStreamInf {
uri,
frame_rate,
audio,
subtitles,
closed_captions,
stream_data,
} => {
write!(f, "{}{}", Self::PREFIX_EXTXSTREAMINF, stream_data)?;
if let Some(value) = frame_rate {
write!(f, ",FRAME-RATE={:.3}", value.as_f32())?;
}
if let Some(value) = audio {
write!(f, ",AUDIO={}", quote(value))?;
}
if let Some(value) = subtitles {
write!(f, ",SUBTITLES={}", quote(value))?;
}
if let Some(value) = closed_captions {
write!(f, ",CLOSED-CAPTIONS={}", value)?;
}
write!(f, "\n{}", uri)?;
}
}
Ok(())
}
}
impl FromStr for VariantStream {
type Err = Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
if let Ok(input) = tag(input, Self::PREFIX_EXTXIFRAME) {
let uri = AttributePairs::new(input)
.find_map(|(key, value)| {
if key == "URI" {
Some(unquote(value))
} else {
None
}
})
.ok_or_else(|| Error::missing_value("URI"))?;
Ok(Self::ExtXIFrame {
uri,
stream_data: input.parse()?,
})
} else if let Ok(input) = tag(input, Self::PREFIX_EXTXSTREAMINF) {
let mut lines = input.lines();
let first_line = lines
.next()
.ok_or_else(|| Error::missing_value("first_line"))?;
let uri = lines.next().ok_or_else(|| Error::missing_value("URI"))?;
let mut frame_rate = None;
let mut audio = None;
let mut subtitles = None;
let mut closed_captions = None;
for (key, value) in AttributePairs::new(first_line) {
match key {
"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()),
_ => {}
}
}
Ok(Self::ExtXStreamInf {
uri: uri.to_string(),
frame_rate,
audio,
subtitles,
closed_captions,
stream_data: first_line.parse()?,
})
} else {
// TODO: custom error type? + attach input data
Err(Error::custom(format!(
"invalid start of input, expected either {:?} or {:?}",
Self::PREFIX_EXTXIFRAME,
Self::PREFIX_EXTXSTREAMINF
)))
}
}
}
impl Deref for VariantStream {
type Target = StreamData;
fn deref(&self) -> &Self::Target {
match &self {
Self::ExtXIFrame { stream_data, .. } | Self::ExtXStreamInf { stream_data, .. } => {
stream_data
}
}
}
}

View file

@ -5,14 +5,32 @@ use std::str::FromStr;
use crate::utils::{quote, unquote};
/// The identifier of a closed captions group or its absence.
///
/// See: [4.3.4.2. EXT-X-STREAM-INF]
///
/// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2
#[allow(missing_docs)]
#[non_exhaustive]
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub enum ClosedCaptions {
/// It indicates the set of closed-caption renditions that can be used when
/// playing the presentation.
///
/// The [`String`] must match [`ExtXMedia::group_id`] elsewhere in the
/// Playlist and it's [`ExtXMedia::media_type`] must be
/// [`MediaType::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),
/// [`ClosedCaptions::None`] indicates that there are no closed captions in
/// any [`VariantStream`] in the [`MasterPlaylist`], therefore all
/// [`VariantStream::ExtXStreamInf`] tags must have this attribute with a
/// value of [`ClosedCaptions::None`].
///
/// Having [`ClosedCaptions`] in one [`VariantStream`] but not in another
/// can trigger playback inconsistencies.
///
/// [`MasterPlaylist`]: crate::MasterPlaylist
/// [`VariantStream`]: crate::tags::VariantStream
/// [`VariantStream::ExtXStreamInf`]:
/// crate::tags::VariantStream::ExtXStreamInf
None,
}

View file

@ -11,7 +11,7 @@ mod key_format_versions;
mod media_type;
mod protocol_version;
mod resolution;
mod stream_inf;
mod stream_data;
mod value;
mod float;
@ -29,7 +29,7 @@ pub use key_format_versions::*;
pub use media_type::*;
pub use protocol_version::*;
pub use resolution::*;
pub use stream_inf::*;
pub use stream_data::*;
pub use value::*;
pub use float::Float;

370
src/types/stream_data.rs Normal file
View file

@ -0,0 +1,370 @@
use core::fmt;
use core::str::FromStr;
use derive_builder::Builder;
use shorthand::ShortHand;
use crate::attribute::AttributePairs;
use crate::types::{HdcpLevel, Resolution};
use crate::utils::{quote, unquote};
use crate::Error;
/// The [`StreamData`] struct contains the data that is shared between both
/// variants of the [`VariantStream`].
///
/// [`VariantStream`]: crate::tags::VariantStream
#[derive(ShortHand, Builder, PartialOrd, Debug, Clone, PartialEq, Eq, Hash, Ord)]
#[builder(setter(strip_option))]
#[builder(derive(Debug, PartialEq))]
#[shorthand(enable(must_use, into))]
pub struct StreamData {
/// The peak segment bitrate of the [`VariantStream`] in bits per second.
///
/// If all the [`MediaSegment`]s in a [`VariantStream`] have already been
/// created, the bandwidth value must be the largest sum of peak segment
/// bitrates that is produced by any playable combination of renditions.
///
/// (For a [`VariantStream`] with a single [`MediaPlaylist`], this is just
/// the peak segment bit rate of that [`MediaPlaylist`].)
///
/// An inaccurate value can cause playback stalls or prevent clients from
/// playing the variant. If the [`MasterPlaylist`] is to be made available
/// before all [`MediaSegment`]s in the presentation have been encoded, the
/// bandwidth value should be the bandwidth value of a representative
/// period of similar content, encoded using the same settings.
///
/// # Example
///
/// ```
/// # use hls_m3u8::types::StreamData;
/// #
/// let mut stream = StreamData::new(20);
///
/// stream.set_bandwidth(5);
/// assert_eq!(stream.bandwidth(), 5);
/// ```
///
/// # Note
///
/// This field is required.
///
/// [`VariantStream`]: crate::tags::VariantStream
/// [`MediaSegment`]: crate::MediaSegment
/// [`MasterPlaylist`]: crate::MasterPlaylist
/// [`MediaPlaylist`]: crate::MediaPlaylist
#[shorthand(disable(into))]
bandwidth: u64,
/// The average bandwidth of the stream in bits per second.
///
/// It represents the average segment bitrate of the [`VariantStream`]. If
/// all the [`MediaSegment`]s in a [`VariantStream`] have already been
/// created, the average bandwidth must be the largest sum of average
/// segment bitrates that is produced by any playable combination of
/// renditions.
///
/// (For a [`VariantStream`] with a single [`MediaPlaylist`], this is just
/// the average segment bitrate of that [`MediaPlaylist`].)
///
/// An inaccurate value can cause playback stalls or prevent clients from
/// playing the variant. If the [`MasterPlaylist`] is to be made available
/// before all [`MediaSegment`]s in the presentation have been encoded, the
/// average bandwidth should be the average bandwidth of a representative
/// period of similar content, encoded using the same settings.
///
/// # Example
///
/// ```
/// # use hls_m3u8::types::StreamData;
/// #
/// let mut stream = StreamData::new(20);
///
/// stream.set_average_bandwidth(Some(300));
/// assert_eq!(stream.average_bandwidth(), Some(300));
/// ```
///
/// # Note
///
/// This field is optional.
///
/// [`MediaSegment`]: crate::MediaSegment
/// [`MasterPlaylist`]: crate::MasterPlaylist
/// [`MediaPlaylist`]: crate::MediaPlaylist
/// [`VariantStream`]: crate::tags::VariantStream
#[builder(default)]
#[shorthand(enable(copy), disable(into, option_as_ref))]
average_bandwidth: Option<u64>,
/// A string that represents a list of formats, where each format specifies
/// a media sample type that is present in one or more renditions specified
/// by the [`VariantStream`].
///
/// Valid format identifiers are those in the ISO Base Media File Format
/// Name Space defined by "The 'Codecs' and 'Profiles' Parameters for
/// "Bucket" Media Types" [RFC6381].
///
/// For example, a stream containing AAC low complexity (AAC-LC) audio and
/// H.264 Main Profile Level 3.0 video would have a codecs value of
/// "mp4a.40.2,avc1.4d401e".
///
/// # Example
///
/// ```
/// # use hls_m3u8::types::StreamData;
/// #
/// let mut stream = StreamData::new(20);
///
/// stream.set_codecs(Some("mp4a.40.2,avc1.4d401e"));
/// assert_eq!(stream.codecs(), Some(&"mp4a.40.2,avc1.4d401e".to_string()));
/// ```
///
/// # Note
///
/// This field is optional, but every instance of
/// [`VariantStream::ExtXStreamInf`] should include a codecs attribute.
///
/// [`VariantStream`]: crate::tags::VariantStream
/// [`VariantStream::ExtXStreamInf`]:
/// crate::tags::VariantStream::ExtXStreamInf
/// [RFC6381]: https://tools.ietf.org/html/rfc6381
#[builder(default, setter(into))]
codecs: Option<String>,
/// The resolution of the stream.
///
/// # Example
///
/// ```
/// # use hls_m3u8::types::StreamData;
/// use hls_m3u8::types::Resolution;
///
/// let mut stream = StreamData::new(20);
///
/// stream.set_resolution(Some((1920, 1080)));
/// assert_eq!(stream.resolution(), Some(Resolution::new(1920, 1080)));
/// # stream.set_resolution(Some((1280, 10)));
/// # assert_eq!(stream.resolution(), Some(Resolution::new(1280, 10)));
/// ```
///
/// # Note
///
/// This field is optional, but it is recommended if the [`VariantStream`]
/// includes video.
///
/// [`VariantStream`]: crate::tags::VariantStream
#[builder(default, setter(into))]
#[shorthand(enable(copy))]
resolution: Option<Resolution>,
/// High-bandwidth Digital Content Protection level of the
/// [`VariantStream`].
///
/// # Example
///
/// ```
/// # use hls_m3u8::types::StreamData;
/// use hls_m3u8::types::HdcpLevel;
/// #
/// let mut stream = StreamData::new(20);
///
/// stream.set_hdcp_level(Some(HdcpLevel::None));
/// assert_eq!(stream.hdcp_level(), Some(HdcpLevel::None));
/// ```
///
/// # Note
///
/// This field is optional.
///
/// [`VariantStream`]: crate::tags::VariantStream
#[builder(default)]
#[shorthand(enable(copy), disable(into))]
hdcp_level: Option<HdcpLevel>,
/// It indicates the set of video renditions, that should be used when
/// playing the presentation.
///
/// It must match the value of the [`ExtXMedia::group_id`] attribute
/// [`ExtXMedia`] tag elsewhere in the [`MasterPlaylist`] whose
/// [`ExtXMedia::media_type`] attribute is video. It indicates the set of
/// video renditions that should be used when playing the presentation.
///
/// # Example
///
/// ```
/// # use hls_m3u8::types::StreamData;
/// #
/// let mut stream = StreamData::new(20);
///
/// stream.set_video(Some("video_01"));
/// assert_eq!(stream.video(), Some(&"video_01".to_string()));
/// ```
///
/// # Note
///
/// This field is optional.
///
/// [`ExtXMedia::group_id`]: crate::tags::ExtXMedia::group_id
/// [`ExtXMedia`]: crate::tags::ExtXMedia
/// [`MasterPlaylist`]: crate::MasterPlaylist
/// [`ExtXMedia::media_type`]: crate::tags::ExtXMedia::media_type
#[builder(default, setter(into))]
video: Option<String>,
}
impl StreamData {
/// Creates a new [`StreamData`].
///
/// # Example
///
/// ```
/// # use hls_m3u8::types::StreamData;
/// #
/// let stream = StreamData::new(20);
/// ```
pub const fn new(bandwidth: u64) -> Self {
Self {
bandwidth,
average_bandwidth: None,
codecs: None,
resolution: None,
hdcp_level: None,
video: None,
}
}
/// Returns a builder for [`StreamData`].
///
/// # Example
///
/// ```
/// use hls_m3u8::types::{HdcpLevel, StreamData};
///
/// StreamData::builder()
/// .bandwidth(200)
/// .average_bandwidth(15)
/// .codecs("mp4a.40.2,avc1.4d401e")
/// .resolution((1920, 1080))
/// .hdcp_level(HdcpLevel::Type0)
/// .video("video_01")
/// .build()?;
/// # Ok::<(), Box<dyn ::std::error::Error>>(())
/// ```
pub fn builder() -> StreamDataBuilder { StreamDataBuilder::default() }
}
impl fmt::Display for StreamData {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "BANDWIDTH={}", self.bandwidth)?;
if let Some(value) = &self.average_bandwidth {
write!(f, ",AVERAGE-BANDWIDTH={}", value)?;
}
if let Some(value) = &self.codecs {
write!(f, ",CODECS={}", quote(value))?;
}
if let Some(value) = &self.resolution {
write!(f, ",RESOLUTION={}", value)?;
}
if let Some(value) = &self.hdcp_level {
write!(f, ",HDCP-LEVEL={}", value)?;
}
if let Some(value) = &self.video {
write!(f, ",VIDEO={}", quote(value))?;
}
Ok(())
}
}
impl FromStr for StreamData {
type Err = Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let mut bandwidth = None;
let mut average_bandwidth = None;
let mut codecs = None;
let mut resolution = None;
let mut hdcp_level = None;
let mut video = None;
for (key, value) in AttributePairs::new(input) {
match key {
"BANDWIDTH" => bandwidth = Some(value.parse::<u64>().map_err(Error::parse_int)?),
"AVERAGE-BANDWIDTH" => {
average_bandwidth = Some(value.parse::<u64>().map_err(Error::parse_int)?)
}
"CODECS" => codecs = Some(unquote(value)),
"RESOLUTION" => resolution = Some(value.parse()?),
"HDCP-LEVEL" => {
hdcp_level = Some(value.parse::<HdcpLevel>().map_err(Error::strum)?)
}
"VIDEO" => video = Some(unquote(value)),
_ => {
// [6.3.1. General Client Responsibilities]
// > ignore any attribute/value pair with an unrecognized
// AttributeName.
}
}
}
let bandwidth = bandwidth.ok_or_else(|| Error::missing_value("BANDWIDTH"))?;
Ok(Self {
bandwidth,
average_bandwidth,
codecs,
resolution,
hdcp_level,
video,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_display() {
let mut stream_data = StreamData::new(200);
stream_data.set_average_bandwidth(Some(15));
stream_data.set_codecs(Some("mp4a.40.2,avc1.4d401e"));
stream_data.set_resolution(Some((1920, 1080)));
stream_data.set_hdcp_level(Some(HdcpLevel::Type0));
stream_data.set_video(Some("video"));
assert_eq!(
stream_data.to_string(),
concat!(
"BANDWIDTH=200,",
"AVERAGE-BANDWIDTH=15,",
"CODECS=\"mp4a.40.2,avc1.4d401e\",",
"RESOLUTION=1920x1080,",
"HDCP-LEVEL=TYPE-0,",
"VIDEO=\"video\""
)
.to_string()
);
}
#[test]
fn test_parser() {
let mut stream_data = StreamData::new(200);
stream_data.set_average_bandwidth(Some(15));
stream_data.set_codecs(Some("mp4a.40.2,avc1.4d401e"));
stream_data.set_resolution(Some((1920, 1080)));
stream_data.set_hdcp_level(Some(HdcpLevel::Type0));
stream_data.set_video(Some("video"));
assert_eq!(
stream_data,
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());
}
}

View file

@ -1,278 +0,0 @@
use std::fmt;
use std::str::FromStr;
use derive_builder::Builder;
use shorthand::ShortHand;
use crate::attribute::AttributePairs;
use crate::types::{HdcpLevel, Resolution};
use crate::utils::{quote, unquote};
use crate::Error;
/// # [4.3.4.2. EXT-X-STREAM-INF]
///
/// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2
#[derive(ShortHand, Builder, PartialOrd, Debug, Clone, PartialEq, Eq, Hash, Ord)]
#[builder(setter(into, strip_option))]
#[builder(derive(Debug, PartialEq))]
#[shorthand(enable(must_use, into))]
pub struct StreamInf {
/// The peak segment bit rate of the variant stream.
///
/// # Example
///
/// ```
/// # use hls_m3u8::types::StreamInf;
/// #
/// let mut stream = StreamInf::new(20);
///
/// stream.set_bandwidth(5);
/// assert_eq!(stream.bandwidth(), 5);
/// ```
///
/// # Note
///
/// This field is required.
#[shorthand(disable(into))]
bandwidth: u64,
/// The average bandwidth of the stream.
///
/// # Example
///
/// ```
/// # use hls_m3u8::types::StreamInf;
/// #
/// let mut stream = StreamInf::new(20);
///
/// stream.set_average_bandwidth(Some(300));
/// assert_eq!(stream.average_bandwidth(), Some(300));
/// ```
///
/// # Note
///
/// This field is optional.
#[builder(default)]
#[shorthand(enable(copy), disable(into, option_as_ref))]
average_bandwidth: Option<u64>,
/// A string that represents the list of codec types contained the variant
/// stream.
///
/// # Example
///
/// ```
/// # use hls_m3u8::types::StreamInf;
/// #
/// let mut stream = StreamInf::new(20);
///
/// stream.set_codecs(Some("mp4a.40.2,avc1.4d401e"));
/// assert_eq!(stream.codecs(), Some(&"mp4a.40.2,avc1.4d401e".to_string()));
/// ```
///
/// # Note
///
/// This field is optional.
#[builder(default)]
codecs: Option<String>,
/// The resolution of the stream.
///
/// # Example
///
/// ```
/// # use hls_m3u8::types::StreamInf;
/// use hls_m3u8::types::Resolution;
///
/// let mut stream = StreamInf::new(20);
///
/// stream.set_resolution(Some((1920, 1080)));
/// assert_eq!(stream.resolution(), Some(Resolution::new(1920, 1080)));
/// # stream.set_resolution(Some((1280, 10)));
/// # assert_eq!(stream.resolution(), Some(Resolution::new(1280, 10)));
/// ```
///
/// # Note
///
/// This field is optional.
#[builder(default)]
#[shorthand(enable(copy))]
resolution: Option<Resolution>,
/// High-bandwidth Digital Content Protection level of the variant stream.
///
/// # Example
///
/// ```
/// # use hls_m3u8::types::{HdcpLevel, StreamInf};
/// #
/// let mut stream = StreamInf::new(20);
///
/// stream.set_hdcp_level(Some(HdcpLevel::None));
/// assert_eq!(stream.hdcp_level(), Some(HdcpLevel::None));
/// ```
///
/// # Note
///
/// This field is optional.
#[builder(default)]
#[shorthand(enable(copy), disable(into))]
hdcp_level: Option<HdcpLevel>,
/// It indicates the set of video renditions, that should be used when
/// playing the presentation.
///
/// # Note
///
/// This field is optional.
#[builder(default)]
video: Option<String>,
}
impl StreamInf {
/// Creates a new [`StreamInf`].
///
/// # Example
///
/// ```
/// # use hls_m3u8::types::StreamInf;
/// #
/// let stream = StreamInf::new(20);
/// ```
pub const fn new(bandwidth: u64) -> Self {
Self {
bandwidth,
average_bandwidth: None,
codecs: None,
resolution: None,
hdcp_level: None,
video: None,
}
}
}
impl fmt::Display for StreamInf {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "BANDWIDTH={}", self.bandwidth)?;
if let Some(value) = &self.average_bandwidth {
write!(f, ",AVERAGE-BANDWIDTH={}", value)?;
}
if let Some(value) = &self.codecs {
write!(f, ",CODECS={}", quote(value))?;
}
if let Some(value) = &self.resolution {
write!(f, ",RESOLUTION={}", value)?;
}
if let Some(value) = &self.hdcp_level {
write!(f, ",HDCP-LEVEL={}", value)?;
}
if let Some(value) = &self.video {
write!(f, ",VIDEO={}", quote(value))?;
}
Ok(())
}
}
impl FromStr for StreamInf {
type Err = Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let mut bandwidth = None;
let mut average_bandwidth = None;
let mut codecs = None;
let mut resolution = None;
let mut hdcp_level = None;
let mut video = None;
for (key, value) in AttributePairs::new(input) {
match key {
"BANDWIDTH" => bandwidth = Some(value.parse::<u64>().map_err(Error::parse_int)?),
"AVERAGE-BANDWIDTH" => {
average_bandwidth = Some(value.parse::<u64>().map_err(Error::parse_int)?)
}
"CODECS" => codecs = Some(unquote(value)),
"RESOLUTION" => resolution = Some(value.parse()?),
"HDCP-LEVEL" => {
hdcp_level = Some(value.parse::<HdcpLevel>().map_err(Error::strum)?)
}
"VIDEO" => video = Some(unquote(value)),
_ => {
// [6.3.1. General Client Responsibilities]
// > ignore any attribute/value pair with an unrecognized
// AttributeName.
}
}
}
let bandwidth = bandwidth.ok_or_else(|| Error::missing_value("BANDWIDTH"))?;
Ok(Self {
bandwidth,
average_bandwidth,
codecs,
resolution,
hdcp_level,
video,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_display() {
let mut stream_inf = StreamInf::new(200);
stream_inf.set_average_bandwidth(Some(15));
stream_inf.set_codecs(Some("mp4a.40.2,avc1.4d401e"));
stream_inf.set_resolution(Some((1920, 1080)));
stream_inf.set_hdcp_level(Some(HdcpLevel::Type0));
stream_inf.set_video(Some("video"));
assert_eq!(
stream_inf.to_string(),
"BANDWIDTH=200,\
AVERAGE-BANDWIDTH=15,\
CODECS=\"mp4a.40.2,avc1.4d401e\",\
RESOLUTION=1920x1080,\
HDCP-LEVEL=TYPE-0,\
VIDEO=\"video\""
.to_string()
);
}
#[test]
fn test_parser() {
let mut stream_inf = StreamInf::new(200);
stream_inf.set_average_bandwidth(Some(15));
stream_inf.set_codecs(Some("mp4a.40.2,avc1.4d401e"));
stream_inf.set_resolution(Some((1920, 1080)));
stream_inf.set_hdcp_level(Some(HdcpLevel::Type0));
stream_inf.set_video(Some("video"));
assert_eq!(
stream_inf,
"BANDWIDTH=200,\
AVERAGE-BANDWIDTH=15,\
CODECS=\"mp4a.40.2,avc1.4d401e\",\
RESOLUTION=1920x1080,\
HDCP-LEVEL=TYPE-0,\
VIDEO=\"video\""
.parse()
.unwrap()
);
assert_eq!(
stream_inf,
"BANDWIDTH=200,\
AVERAGE-BANDWIDTH=15,\
CODECS=\"mp4a.40.2,avc1.4d401e\",\
RESOLUTION=1920x1080,\
HDCP-LEVEL=TYPE-0,\
VIDEO=\"video\",\
UNKNOWN=\"value\""
.parse()
.unwrap()
);
assert!("garbage".parse::<StreamInf>().is_err());
}
}

View file

@ -9,7 +9,7 @@ use crate::Error;
/// with a negative float (ex. `-1.1`), [`NaN`], [`INFINITY`] or
/// [`NEG_INFINITY`].
///
/// [`NaN`]: core::f32::NaN
/// [`NaN`]: core::f32::NAN
/// [`INFINITY`]: core::f32::INFINITY
/// [`NEG_INFINITY`]: core::f32::NEG_INFINITY
#[derive(Deref, Default, Debug, Copy, Clone, PartialEq, PartialOrd, Display)]

View file

@ -1,5 +1,5 @@
use hls_m3u8::tags::{ExtXIFrameStreamInf, ExtXMedia, ExtXStreamInf};
use hls_m3u8::types::MediaType;
use hls_m3u8::tags::{ExtXMedia, VariantStream};
use hls_m3u8::types::{MediaType, StreamData};
use hls_m3u8::MasterPlaylist;
use pretty_assertions::assert_eq;
@ -7,45 +7,71 @@ use pretty_assertions::assert_eq;
#[test]
fn test_master_playlist() {
// https://tools.ietf.org/html/rfc8216#section-8.4
let master_playlist = "#EXTM3U\n\
#EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1000000\n\
http://example.com/low.m3u8\n\
#EXT-X-STREAM-INF:BANDWIDTH=2560000,AVERAGE-BANDWIDTH=2000000\n\
http://example.com/mid.m3u8\n\
#EXT-X-STREAM-INF:BANDWIDTH=7680000,AVERAGE-BANDWIDTH=6000000\n\
http://example.com/hi.m3u8\n\
#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n\
http://example.com/audio-only.m3u8"
.parse::<MasterPlaylist>()
.unwrap();
let master_playlist = concat!(
"#EXTM3U\n",
"#EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1000000\n",
"http://example.com/low.m3u8\n",
"#EXT-X-STREAM-INF:BANDWIDTH=2560000,AVERAGE-BANDWIDTH=2000000\n",
"http://example.com/mid.m3u8\n",
"#EXT-X-STREAM-INF:BANDWIDTH=7680000,AVERAGE-BANDWIDTH=6000000\n",
"http://example.com/hi.m3u8\n",
"#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n",
"http://example.com/audio-only.m3u8",
)
.parse::<MasterPlaylist>()
.unwrap();
assert_eq!(
MasterPlaylist::builder()
.stream_inf_tags(vec![
ExtXStreamInf::builder()
.bandwidth(1280000)
.average_bandwidth(1000000)
.uri("http://example.com/low.m3u8")
.build()
.unwrap(),
ExtXStreamInf::builder()
.bandwidth(2560000)
.average_bandwidth(2000000)
.uri("http://example.com/mid.m3u8")
.build()
.unwrap(),
ExtXStreamInf::builder()
.bandwidth(7680000)
.average_bandwidth(6000000)
.uri("http://example.com/hi.m3u8")
.build()
.unwrap(),
ExtXStreamInf::builder()
.bandwidth(65000)
.codecs("mp4a.40.5")
.uri("http://example.com/audio-only.m3u8")
.build()
.unwrap(),
.variants(vec![
VariantStream::ExtXStreamInf {
uri: "http://example.com/low.m3u8".into(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
.bandwidth(1280000)
.average_bandwidth(1000000)
.build()
.unwrap()
},
VariantStream::ExtXStreamInf {
uri: "http://example.com/mid.m3u8".into(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
.bandwidth(2560000)
.average_bandwidth(2000000)
.build()
.unwrap()
},
VariantStream::ExtXStreamInf {
uri: "http://example.com/hi.m3u8".into(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
.bandwidth(7680000)
.average_bandwidth(6000000)
.build()
.unwrap()
},
VariantStream::ExtXStreamInf {
uri: "http://example.com/audio-only.m3u8".into(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
.bandwidth(65000)
.codecs("mp4a.40.5")
.build()
.unwrap()
},
])
.build()
.unwrap(),
@ -56,62 +82,75 @@ fn test_master_playlist() {
#[test]
fn test_master_playlist_with_i_frames() {
// https://tools.ietf.org/html/rfc8216#section-8.5
let master_playlist = "#EXTM3U\n\
#EXT-X-STREAM-INF:BANDWIDTH=1280000\n\
low/audio-video.m3u8\n\
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=86000,URI=\"low/iframe.m3u8\"\n\
#EXT-X-STREAM-INF:BANDWIDTH=2560000\n\
mid/audio-video.m3u8\n\
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=150000,URI=\"mid/iframe.m3u8\"\n\
#EXT-X-STREAM-INF:BANDWIDTH=7680000\n\
hi/audio-video.m3u8\n\
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=550000,URI=\"hi/iframe.m3u8\"\n\
#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n\
audio-only.m3u8"
.parse::<MasterPlaylist>()
.unwrap();
let master_playlist = concat!(
"#EXTM3U\n",
"#EXT-X-STREAM-INF:BANDWIDTH=1280000\n",
"low/audio-video.m3u8\n",
"#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=86000,URI=\"low/iframe.m3u8\"\n",
"#EXT-X-STREAM-INF:BANDWIDTH=2560000\n",
"mid/audio-video.m3u8\n",
"#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=150000,URI=\"mid/iframe.m3u8\"\n",
"#EXT-X-STREAM-INF:BANDWIDTH=7680000\n",
"hi/audio-video.m3u8\n",
// this one:
"#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=550000,URI=\"hi/iframe.m3u8\"\n",
"#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\"\n",
"audio-only.m3u8"
)
.parse::<MasterPlaylist>()
.unwrap();
assert_eq!(
MasterPlaylist::builder()
.stream_inf_tags(vec![
ExtXStreamInf::builder()
.bandwidth(1280000)
.uri("low/audio-video.m3u8")
.build()
.unwrap(),
ExtXStreamInf::builder()
.bandwidth(2560000)
.uri("mid/audio-video.m3u8")
.build()
.unwrap(),
ExtXStreamInf::builder()
.bandwidth(7680000)
.uri("hi/audio-video.m3u8")
.build()
.unwrap(),
ExtXStreamInf::builder()
.bandwidth(65000)
.codecs("mp4a.40.5")
.uri("audio-only.m3u8")
.build()
.unwrap(),
])
.i_frame_stream_inf_tags(vec![
ExtXIFrameStreamInf::builder()
.bandwidth(86000)
.uri("low/iframe.m3u8")
.build()
.unwrap(),
ExtXIFrameStreamInf::builder()
.bandwidth(150000)
.uri("mid/iframe.m3u8")
.build()
.unwrap(),
ExtXIFrameStreamInf::builder()
.bandwidth(550000)
.uri("hi/iframe.m3u8")
.build()
.unwrap(),
.variants(vec![
VariantStream::ExtXStreamInf {
uri: "low/audio-video.m3u8".into(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_data: StreamData::new(1280000)
},
VariantStream::ExtXIFrame {
uri: "low/iframe.m3u8".into(),
stream_data: StreamData::new(86000),
},
VariantStream::ExtXStreamInf {
uri: "mid/audio-video.m3u8".into(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_data: StreamData::new(2560000)
},
VariantStream::ExtXIFrame {
uri: "mid/iframe.m3u8".into(),
stream_data: StreamData::new(150000),
},
VariantStream::ExtXStreamInf {
uri: "hi/audio-video.m3u8".into(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_data: StreamData::new(7680000)
},
VariantStream::ExtXIFrame {
uri: "hi/iframe.m3u8".into(),
stream_data: StreamData::new(550000),
},
VariantStream::ExtXStreamInf {
uri: "audio-only.m3u8".into(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
.bandwidth(65000)
.codecs("mp4a.40.5")
.build()
.unwrap()
},
])
.build()
.unwrap(),
@ -123,33 +162,32 @@ fn test_master_playlist_with_i_frames() {
fn test_master_playlist_with_alternative_audio() {
// https://tools.ietf.org/html/rfc8216#section-8.6
// TODO: I think the CODECS=\"..." have to be replaced.
let master_playlist = "#EXTM3U\n\
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"English\", \
DEFAULT=YES,AUTOSELECT=YES,LANGUAGE=\"en\", \
URI=\"main/english-audio.m3u8\"\n\
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"Deutsch\", \
DEFAULT=NO,AUTOSELECT=YES,LANGUAGE=\"de\", \
URI=\"main/german-audio.m3u8\"\n\
#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"Commentary\", \
DEFAULT=NO,AUTOSELECT=NO,LANGUAGE=\"en\", \
URI=\"commentary/audio-only.m3u8\"\n\
#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"...\",AUDIO=\"aac\"\n\
low/video-only.m3u8\n\
#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS=\"...\",AUDIO=\"aac\"\n\
mid/video-only.m3u8\n\
#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS=\"...\",AUDIO=\"aac\"\n\
hi/video-only.m3u8\n\
#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\",AUDIO=\"aac\"\n\
main/english-audio.m3u8"
.parse::<MasterPlaylist>()
.unwrap();
let master_playlist = concat!(
"#EXTM3U\n",
"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"English\", ",
"DEFAULT=YES,AUTOSELECT=YES,LANGUAGE=\"en\", ",
"URI=\"main/english-audio.m3u8\"\n",
"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"Deutsch\", ",
"DEFAULT=NO,AUTOSELECT=YES,LANGUAGE=\"de\", ",
"URI=\"main/german-audio.m3u8\"\n",
"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID=\"aac\",NAME=\"Commentary\", ",
"DEFAULT=NO,AUTOSELECT=NO,LANGUAGE=\"en\", ",
"URI=\"commentary/audio-only.m3u8\"\n",
"#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"...\",AUDIO=\"aac\"\n",
"low/video-only.m3u8\n",
"#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS=\"...\",AUDIO=\"aac\"\n",
"mid/video-only.m3u8\n",
"#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS=\"...\",AUDIO=\"aac\"\n",
"hi/video-only.m3u8\n",
"#EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS=\"mp4a.40.5\",AUDIO=\"aac\"\n",
"main/english-audio.m3u8"
)
.parse::<MasterPlaylist>()
.unwrap();
assert_eq!(
MasterPlaylist::builder()
.media_tags(vec![
.media(vec![
ExtXMedia::builder()
.media_type(MediaType::Audio)
.group_id("aac")
@ -181,35 +219,55 @@ fn test_master_playlist_with_alternative_audio() {
.build()
.unwrap(),
])
.stream_inf_tags(vec![
ExtXStreamInf::builder()
.bandwidth(1280000)
.codecs("...")
.audio("aac")
.uri("low/video-only.m3u8")
.build()
.unwrap(),
ExtXStreamInf::builder()
.bandwidth(2560000)
.codecs("...")
.audio("aac")
.uri("mid/video-only.m3u8")
.build()
.unwrap(),
ExtXStreamInf::builder()
.bandwidth(7680000)
.codecs("...")
.audio("aac")
.uri("hi/video-only.m3u8")
.build()
.unwrap(),
ExtXStreamInf::builder()
.bandwidth(65000)
.codecs("mp4a.40.5")
.audio("aac")
.uri("main/english-audio.m3u8")
.build()
.unwrap(),
.variants(vec![
VariantStream::ExtXStreamInf {
uri: "low/video-only.m3u8".into(),
frame_rate: None,
audio: Some("aac".into()),
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
.bandwidth(1280000)
.codecs("...")
.build()
.unwrap()
},
VariantStream::ExtXStreamInf {
uri: "mid/video-only.m3u8".into(),
frame_rate: None,
audio: Some("aac".into()),
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
.bandwidth(2560000)
.codecs("...")
.build()
.unwrap()
},
VariantStream::ExtXStreamInf {
uri: "hi/video-only.m3u8".into(),
frame_rate: None,
audio: Some("aac".into()),
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
.bandwidth(7680000)
.codecs("...")
.build()
.unwrap()
},
VariantStream::ExtXStreamInf {
uri: "main/english-audio.m3u8".into(),
frame_rate: None,
audio: Some("aac".into()),
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
.bandwidth(65000)
.codecs("mp4a.40.5")
.build()
.unwrap()
},
])
.build()
.unwrap(),
@ -220,48 +278,39 @@ fn test_master_playlist_with_alternative_audio() {
#[test]
fn test_master_playlist_with_alternative_video() {
// https://tools.ietf.org/html/rfc8216#section-8.7
let master_playlist = "#EXTM3U\n\
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"low\",NAME=\"Main\", \
AUTOSELECT=YES,DEFAULT=YES,URI=\"low/main/audio-video.m3u8\"\n\
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"low\",NAME=\"Centerfield\", \
DEFAULT=NO,URI=\"low/centerfield/audio-video.m3u8\"\n\
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"low\",NAME=\"Dugout\", \
DEFAULT=NO,URI=\"low/dugout/audio-video.m3u8\"\n\
#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"...\",VIDEO=\"low\"\n\
low/main/audio-video.m3u8\n\
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"mid\",NAME=\"Main\", \
AUTOSELECT=YES,DEFAULT=YES,URI=\"mid/main/audio-video.m3u8\"\n\
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"mid\",NAME=\"Centerfield\", \
DEFAULT=NO,URI=\"mid/centerfield/audio-video.m3u8\"\n\
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"mid\",NAME=\"Dugout\", \
DEFAULT=NO,URI=\"mid/dugout/audio-video.m3u8\"\n\
#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS=\"...\",VIDEO=\"mid\"\n\
mid/main/audio-video.m3u8\n\
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"hi\",NAME=\"Main\", \
AUTOSELECT=YES,DEFAULT=YES,URI=\"hi/main/audio-video.m3u8\"\n\
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"hi\",NAME=\"Centerfield\", \
DEFAULT=NO,URI=\"hi/centerfield/audio-video.m3u8\"\n\
#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"hi\",NAME=\"Dugout\", \
DEFAULT=NO,URI=\"hi/dugout/audio-video.m3u8\"\n\
#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS=\"...\",VIDEO=\"hi\"
hi/main/audio-video.m3u8"
.parse::<MasterPlaylist>()
.unwrap();
let master_playlist = concat!(
"#EXTM3U\n",
"#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"low\",NAME=\"Main\", ",
"AUTOSELECT=YES,DEFAULT=YES,URI=\"low/main/audio-video.m3u8\"\n",
"#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"low\",NAME=\"Centerfield\", ",
"DEFAULT=NO,URI=\"low/centerfield/audio-video.m3u8\"\n",
"#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"low\",NAME=\"Dugout\", ",
"DEFAULT=NO,URI=\"low/dugout/audio-video.m3u8\"\n",
"#EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS=\"...\",VIDEO=\"low\"\n",
"low/main/audio-video.m3u8\n",
"#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"mid\",NAME=\"Main\", ",
"AUTOSELECT=YES,DEFAULT=YES,URI=\"mid/main/audio-video.m3u8\"\n",
"#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"mid\",NAME=\"Centerfield\", ",
"DEFAULT=NO,URI=\"mid/centerfield/audio-video.m3u8\"\n",
"#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"mid\",NAME=\"Dugout\", ",
"DEFAULT=NO,URI=\"mid/dugout/audio-video.m3u8\"\n",
"#EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS=\"...\",VIDEO=\"mid\"\n",
"mid/main/audio-video.m3u8\n",
"#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"hi\",NAME=\"Main\",",
"AUTOSELECT=YES,DEFAULT=YES,URI=\"hi/main/audio-video.m3u8\"\n",
"#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"hi\",NAME=\"Centerfield\", ",
"DEFAULT=NO,URI=\"hi/centerfield/audio-video.m3u8\"\n",
"#EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID=\"hi\",NAME=\"Dugout\", ",
"DEFAULT=NO,URI=\"hi/dugout/audio-video.m3u8\"\n",
"#EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS=\"...\",VIDEO=\"hi\"\n",
"hi/main/audio-video.m3u8"
)
.parse::<MasterPlaylist>()
.unwrap();
assert_eq!(
MasterPlaylist::builder()
.media_tags(vec![
.media(vec![
// low
ExtXMedia::builder()
.media_type(MediaType::Video)
@ -341,28 +390,46 @@ fn test_master_playlist_with_alternative_video() {
.build()
.unwrap(),
])
.stream_inf_tags(vec![
ExtXStreamInf::builder()
.bandwidth(1280000)
.codecs("...")
.video("low")
.uri("low/main/audio-video.m3u8")
.build()
.unwrap(),
ExtXStreamInf::builder()
.bandwidth(2560000)
.codecs("...")
.video("mid")
.uri("mid/main/audio-video.m3u8")
.build()
.unwrap(),
ExtXStreamInf::builder()
.bandwidth(7680000)
.codecs("...")
.video("hi")
.uri("hi/main/audio-video.m3u8")
.build()
.unwrap(),
.variants(vec![
VariantStream::ExtXStreamInf {
uri: "low/main/audio-video.m3u8".into(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
.bandwidth(1280000)
.codecs("...")
.video("low")
.build()
.unwrap()
},
VariantStream::ExtXStreamInf {
uri: "mid/main/audio-video.m3u8".into(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
.bandwidth(2560000)
.codecs("...")
.video("mid")
.build()
.unwrap()
},
VariantStream::ExtXStreamInf {
uri: "hi/main/audio-video.m3u8".into(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
.bandwidth(7680000)
.codecs("...")
.video("hi")
.build()
.unwrap()
},
])
.build()
.unwrap(),

View file

@ -6,26 +6,28 @@ use pretty_assertions::assert_eq;
#[test]
fn test_media_playlist_with_byterange() {
let media_playlist = "#EXTM3U\n\
#EXT-X-TARGETDURATION:10\n\
#EXT-X-VERSION:4\n\
#EXT-X-MEDIA-SEQUENCE:0\n\
#EXTINF:10.0,\n\
#EXT-X-BYTERANGE:75232@0\n\
video.ts\n\
#EXT-X-BYTERANGE:82112@752321\n\
#EXTINF:10.0,\n\
video.ts\n\
#EXTINF:10.0,\n\
#EXT-X-BYTERANGE:69864\n\
video.ts"
.parse::<MediaPlaylist>()
.unwrap();
let media_playlist = concat!(
"#EXTM3U\n",
"#EXT-X-TARGETDURATION:10\n",
"#EXT-X-VERSION:4\n",
"#EXT-X-MEDIA-SEQUENCE:0\n",
"#EXTINF:10.0,\n",
"#EXT-X-BYTERANGE:75232@0\n",
"video.ts\n",
"#EXT-X-BYTERANGE:82112@752321\n",
"#EXTINF:10.0,\n",
"video.ts\n",
"#EXTINF:10.0,\n",
"#EXT-X-BYTERANGE:69864\n",
"video.ts\n"
)
.parse::<MediaPlaylist>()
.unwrap();
assert_eq!(
MediaPlaylist::builder()
.target_duration_tag(ExtXTargetDuration::new(Duration::from_secs(10)))
.media_sequence_tag(ExtXMediaSequence::new(0))
.target_duration(ExtXTargetDuration::new(Duration::from_secs(10)))
.media_sequence(ExtXMediaSequence::new(0))
.segments(vec![
MediaSegment::builder()
.inf_tag(ExtInf::new(Duration::from_secs_f64(10.0)))

View file

@ -7,18 +7,19 @@ use std::time::Duration;
#[test]
fn test_simple_playlist() {
let playlist = r#"
#EXTM3U
#EXT-X-TARGETDURATION:5220
#EXTINF:0,
http://media.example.com/entire1.ts
#EXTINF:5220,
http://media.example.com/entire2.ts
#EXT-X-ENDLIST"#;
let playlist = concat!(
"#EXTM3U\n",
"#EXT-X-TARGETDURATION:5220\n",
"#EXTINF:0,\n",
"http://media.example.com/entire1.ts\n",
"#EXTINF:5220,\n",
"http://media.example.com/entire2.ts\n",
"#EXT-X-ENDLIST\n"
);
let media_playlist = playlist.parse::<MediaPlaylist>().unwrap();
assert_eq!(
media_playlist.target_duration_tag(),
media_playlist.target_duration(),
ExtXTargetDuration::new(Duration::from_secs(5220))
);