mirror of
https://github.com/sile/hls_m3u8.git
synced 2024-11-21 14:50:59 +00:00
implement VariantStream
This commit is contained in:
parent
90ff18e2b3
commit
e6f5091f1b
15 changed files with 1201 additions and 1288 deletions
83
src/line.rs
83
src/line.rs
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(_) => {
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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::*;
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
286
src/tags/master_playlist/variant_stream.rs
Normal file
286
src/tags/master_playlist/variant_stream.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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
370
src/types/stream_data.rs
Normal 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());
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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)]
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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))
|
||||
);
|
||||
|
||||
|
|
Loading…
Reference in a new issue