1
0
Fork 0
mirror of https://github.com/sile/hls_m3u8.git synced 2024-11-22 15:21:01 +00:00

improve MasterPlaylist

This commit is contained in:
Luro02 2020-03-17 15:58:43 +01:00
parent 025add6dc3
commit a797e401ed
No known key found for this signature in database
GPG key ID: B66FD4F74501A9CF

View file

@ -3,7 +3,6 @@ use std::fmt;
use std::str::FromStr; use std::str::FromStr;
use derive_builder::Builder; use derive_builder::Builder;
use shorthand::ShortHand;
use crate::line::{Line, Lines, Tag}; use crate::line::{Line, Lines, Tag};
use crate::tags::{ use crate::tags::{
@ -16,37 +15,112 @@ use crate::{Error, RequiredVersion};
/// The master playlist describes all of the available variants for your /// The master playlist describes all of the available variants for your
/// content. /// content.
///
/// Each variant is a version of the stream at a particular bitrate and is /// Each variant is a version of the stream at a particular bitrate and is
/// contained in a separate playlist. /// contained in a separate playlist called [`MediaPlaylist`].
#[derive(ShortHand, Debug, Clone, Builder, PartialEq)] ///
/// # Examples
///
/// A [`MasterPlaylist`] can be parsed from a `str`:
///
/// ```
/// use core::str::FromStr;
/// use hls_m3u8::MasterPlaylist;
///
/// // the concat! macro joins multiple `&'static str`.
/// let master_playlist = concat!(
/// "#EXTM3U\n",
/// "#EXT-X-STREAM-INF:",
/// "BANDWIDTH=150000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n",
/// "http://example.com/low/index.m3u8\n",
/// "#EXT-X-STREAM-INF:",
/// "BANDWIDTH=240000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n",
/// "http://example.com/lo_mid/index.m3u8\n",
/// "#EXT-X-STREAM-INF:",
/// "BANDWIDTH=440000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=416x234\n",
/// "http://example.com/hi_mid/index.m3u8\n",
/// "#EXT-X-STREAM-INF:",
/// "BANDWIDTH=640000,CODECS=\"avc1.42e00a,mp4a.40.2\",RESOLUTION=640x360\n",
/// "http://example.com/high/index.m3u8\n",
/// "#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS=\"mp4a.40.5\"\n",
/// "http://example.com/audio/index.m3u8\n"
/// )
/// .parse::<MasterPlaylist>()?;
///
/// println!("{}", master_playlist.has_independent_segments);
/// # Ok::<(), hls_m3u8::Error>(())
/// ```
///
/// or it can be constructed through a builder
///
/// ```
/// # use hls_m3u8::MasterPlaylist;
/// use hls_m3u8::tags::{ExtXStart, VariantStream};
/// use hls_m3u8::types::{Float, StreamData};
///
/// MasterPlaylist::builder()
/// .variant_streams(vec![
/// VariantStream::ExtXStreamInf {
/// uri: "http://example.com/low/index.m3u8".into(),
/// frame_rate: None,
/// audio: None,
/// subtitles: None,
/// closed_captions: None,
/// stream_data: StreamData::builder()
/// .bandwidth(150000)
/// .codecs(&["avc1.42e00a", "mp4a.40.2"])
/// .resolution((416, 234))
/// .build()
/// .unwrap(),
/// },
/// VariantStream::ExtXStreamInf {
/// uri: "http://example.com/lo_mid/index.m3u8".into(),
/// frame_rate: None,
/// audio: None,
/// subtitles: None,
/// closed_captions: None,
/// stream_data: StreamData::builder()
/// .bandwidth(240000)
/// .codecs(&["avc1.42e00a", "mp4a.40.2"])
/// .resolution((416, 234))
/// .build()
/// .unwrap(),
/// },
/// ])
/// .has_independent_segments(true)
/// .start(ExtXStart::new(Float::new(1.23)))
/// .build()?;
/// # Ok::<(), Box<dyn ::std::error::Error>>(())
/// ```
///
/// [`MediaPlaylist`]: crate::MediaPlaylist
#[derive(Debug, Clone, Builder, PartialEq, Default)]
#[builder(build_fn(validate = "Self::validate"))] #[builder(build_fn(validate = "Self::validate"))]
#[builder(setter(into, strip_option))] #[builder(setter(into, strip_option))]
#[shorthand(enable(must_use, get_mut, collection_magic))]
pub struct MasterPlaylist { pub struct MasterPlaylist {
/// The [`ExtXIndependentSegments`] tag signals that all media samples in a /// Indicates that all media samples in a [`MediaSegment`] can be
/// [`MediaSegment`] can be decoded without information from other segments. /// decoded without information from other segments.
/// ///
/// # Note /// ### Note
/// ///
/// This tag is optional. /// This field is optional and by default `false`. If the field is `true`,
/// /// it applies to every [`MediaSegment`] in every [`MediaPlaylist`] of this
/// If this tag is specified it will apply to every [`MediaSegment`] in /// [`MasterPlaylist`].
/// every [`MediaPlaylist`] in the [`MasterPlaylist`].
/// ///
/// [`MediaSegment`]: crate::MediaSegment /// [`MediaSegment`]: crate::MediaSegment
/// [`MediaPlaylist`]: crate::MediaPlaylist /// [`MediaPlaylist`]: crate::MediaPlaylist
#[builder(default)] #[builder(default)]
independent_segments: Option<ExtXIndependentSegments>, pub has_independent_segments: bool,
/// The [`ExtXStart`] tag indicates a preferred point at which to start /// A preferred point at which to start playing a playlist.
/// playing a Playlist.
/// ///
/// # Note /// ### Note
/// ///
/// This tag is optional. /// This field is optional and by default the playlist should be played from
/// the start.
#[builder(default)] #[builder(default)]
start: Option<ExtXStart>, pub start: Option<ExtXStart>,
/// The [`ExtXMedia`] tag is used to relate [`MediaPlaylist`]s, /// A list of all [`ExtXMedia`] tags, which describe an alternative
/// that contain alternative renditions of the same content. /// rendition.
/// ///
/// For example, three [`ExtXMedia`] tags can be used to identify audio-only /// For example, three [`ExtXMedia`] tags can be used to identify audio-only
/// [`MediaPlaylist`]s, that contain English, French, and Spanish /// [`MediaPlaylist`]s, that contain English, French, and Spanish
@ -54,76 +128,165 @@ pub struct MasterPlaylist {
/// be used to identify video-only [`MediaPlaylist`]s that show two /// be used to identify video-only [`MediaPlaylist`]s that show two
/// different camera angles. /// different camera angles.
/// ///
/// # Note /// ### Note
/// ///
/// This tag is optional. /// This field is optional.
/// ///
/// [`MediaPlaylist`]: crate::MediaPlaylist /// [`MediaPlaylist`]: crate::MediaPlaylist
#[builder(default)] #[builder(default)]
media: Vec<ExtXMedia>, pub media: Vec<ExtXMedia>,
/// A list of all streams of this [`MasterPlaylist`]. /// A list of all streams of this [`MasterPlaylist`].
/// ///
/// # Note /// ### Note
/// ///
/// This tag is optional. /// This field is optional.
#[builder(default)] #[builder(default)]
variants: Vec<VariantStream>, pub variant_streams: Vec<VariantStream>,
/// The [`ExtXSessionData`] tag allows arbitrary session data to be /// The [`ExtXSessionData`] tag allows arbitrary session data to be
/// carried in a [`MasterPlaylist`]. /// carried in a [`MasterPlaylist`].
/// ///
/// # Note /// ### Note
/// ///
/// This tag is optional. /// This field is optional.
#[builder(default)] #[builder(default)]
session_data: Vec<ExtXSessionData>, pub session_data: Vec<ExtXSessionData>,
/// This is a list of [`ExtXSessionKey`]s, that allows the client to preload /// A list of [`ExtXSessionKey`]s, that allows the client to preload
/// these keys without having to read the [`MediaPlaylist`]s first. /// these keys without having to read the [`MediaPlaylist`]s first.
/// ///
/// # Note /// ### Note
/// ///
/// This tag is optional. /// This field is optional.
/// ///
/// [`MediaPlaylist`]: crate::MediaPlaylist /// [`MediaPlaylist`]: crate::MediaPlaylist
#[builder(default)] #[builder(default)]
session_keys: Vec<ExtXSessionKey>, pub session_keys: Vec<ExtXSessionKey>,
/// This is a list of all tags that could not be identified while parsing /// A list of all tags that could not be identified while parsing the input.
/// the input.
/// ///
/// # Note /// ### Note
/// ///
/// This tag is optional. /// This field is optional.
#[builder(default)] #[builder(default)]
unknown_tags: Vec<String>, pub unknown_tags: Vec<String>,
#[builder(default, field(private))]
__non_exhaustive: (),
} }
impl MasterPlaylist { impl MasterPlaylist {
// TODO: finish builder example!
/// Returns a builder for a [`MasterPlaylist`]. /// Returns a builder for a [`MasterPlaylist`].
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```
/// # use hls_m3u8::MasterPlaylist; /// # use hls_m3u8::MasterPlaylist;
/// use hls_m3u8::tags::ExtXStart; /// use hls_m3u8::tags::{ExtXStart, VariantStream};
/// use hls_m3u8::types::Float; /// use hls_m3u8::types::{Float, StreamData};
/// ///
/// MasterPlaylist::builder() /// MasterPlaylist::builder()
/// .start(ExtXStart::new(Float::new(20.3))) /// .variant_streams(vec![
/// VariantStream::ExtXStreamInf {
/// uri: "http://example.com/low/index.m3u8".into(),
/// frame_rate: None,
/// audio: None,
/// subtitles: None,
/// closed_captions: None,
/// stream_data: StreamData::builder()
/// .bandwidth(150000)
/// .codecs(&["avc1.42e00a", "mp4a.40.2"])
/// .resolution((416, 234))
/// .build()
/// .unwrap(),
/// },
/// VariantStream::ExtXStreamInf {
/// uri: "http://example.com/lo_mid/index.m3u8".into(),
/// frame_rate: None,
/// audio: None,
/// subtitles: None,
/// closed_captions: None,
/// stream_data: StreamData::builder()
/// .bandwidth(240000)
/// .codecs(&["avc1.42e00a", "mp4a.40.2"])
/// .resolution((416, 234))
/// .build()
/// .unwrap(),
/// },
/// ])
/// .has_independent_segments(true)
/// .start(ExtXStart::new(Float::new(1.23)))
/// .build()?; /// .build()?;
/// # Ok::<(), Box<dyn ::std::error::Error>>(()) /// # Ok::<(), Box<dyn ::std::error::Error>>(())
/// ``` /// ```
#[must_use] #[must_use]
#[inline] #[inline]
pub fn builder() -> MasterPlaylistBuilder { MasterPlaylistBuilder::default() } pub fn builder() -> MasterPlaylistBuilder { MasterPlaylistBuilder::default() }
/// Returns all streams, which have an audio group id.
pub fn audio_streams(&self) -> impl Iterator<Item = &VariantStream> {
self.variant_streams.iter().filter(|stream| {
if let VariantStream::ExtXStreamInf { audio: Some(_), .. } = stream {
true
} else {
false
}
})
}
/// Returns all streams, which have a video group id.
pub fn video_streams(&self) -> impl Iterator<Item = &VariantStream> {
self.variant_streams.iter().filter(|stream| {
if let VariantStream::ExtXStreamInf { stream_data, .. } = stream {
stream_data.video().is_some()
} else if let VariantStream::ExtXIFrame { stream_data, .. } = stream {
stream_data.video().is_some()
} else {
false
}
})
}
/// Returns all streams, which have no group id.
pub fn unassociated_streams(&self) -> impl Iterator<Item = &VariantStream> {
self.variant_streams.iter().filter(|stream| {
if let VariantStream::ExtXStreamInf {
stream_data,
audio: None,
subtitles: None,
closed_captions: None,
..
} = stream
{
stream_data.video().is_none()
} else if let VariantStream::ExtXIFrame { stream_data, .. } = stream {
stream_data.video().is_none()
} else {
false
}
})
}
/// Returns all `ExtXMedia` tags, associated with the provided stream.
pub fn associated_with<'a>(
&'a self,
stream: &'a VariantStream,
) -> impl Iterator<Item = &ExtXMedia> + 'a {
self.media
.iter()
.filter(move |media| stream.is_associated(media))
}
} }
impl RequiredVersion for MasterPlaylist { impl RequiredVersion for MasterPlaylist {
fn required_version(&self) -> ProtocolVersion { fn required_version(&self) -> ProtocolVersion {
required_version![ required_version![
self.independent_segments, {
if self.has_independent_segments {
Some(ExtXIndependentSegments)
} else {
None
}
},
self.start, self.start,
self.media, self.media,
self.variants, self.variant_streams,
self.session_data, self.session_data,
self.session_keys self.session_keys
] ]
@ -132,44 +295,41 @@ impl RequiredVersion for MasterPlaylist {
impl MasterPlaylistBuilder { impl MasterPlaylistBuilder {
fn validate(&self) -> Result<(), String> { fn validate(&self) -> Result<(), String> {
self.validate_variants().map_err(|e| e.to_string())?; if let Some(variant_streams) = &self.variant_streams {
self.validate_variants(variant_streams)
.map_err(|e| e.to_string())?;
}
self.validate_session_data_tags() self.validate_session_data_tags()
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
Ok(()) Ok(())
} }
fn validate_variants(&self) -> crate::Result<()> { fn validate_variants(&self, variant_streams: &[VariantStream]) -> crate::Result<()> {
if let Some(variants) = &self.variants {
self.validate_stream_inf(variants)?;
self.validate_i_frame_stream_inf(variants)?;
}
Ok(())
}
fn validate_stream_inf(&self, value: &[VariantStream]) -> crate::Result<()> {
let mut closed_captions_none = false; let mut closed_captions_none = false;
for t in value { for variant in variant_streams {
if let VariantStream::ExtXStreamInf { match &variant {
VariantStream::ExtXStreamInf {
audio, audio,
subtitles, subtitles,
closed_captions, closed_captions,
stream_data, stream_data,
.. ..
} = &t } => {
{
if let Some(group_id) = &audio { if let Some(group_id) = &audio {
if !self.check_media_group(MediaType::Audio, group_id) { if !self.check_media_group(MediaType::Audio, group_id) {
return Err(Error::unmatched_group(group_id)); return Err(Error::unmatched_group(group_id));
} }
} }
if let Some(group_id) = &stream_data.video() { if let Some(group_id) = &stream_data.video() {
if !self.check_media_group(MediaType::Video, group_id) { if !self.check_media_group(MediaType::Video, group_id) {
return Err(Error::unmatched_group(group_id)); return Err(Error::unmatched_group(group_id));
} }
} }
if let Some(group_id) = &subtitles { if let Some(group_id) = &subtitles {
if !self.check_media_group(MediaType::Subtitles, group_id) { if !self.check_media_group(MediaType::Subtitles, group_id) {
return Err(Error::unmatched_group(group_id)); return Err(Error::unmatched_group(group_id));
@ -180,9 +340,7 @@ impl MasterPlaylistBuilder {
match &closed_captions { match &closed_captions {
ClosedCaptions::GroupId(group_id) => { ClosedCaptions::GroupId(group_id) => {
if closed_captions_none { if closed_captions_none {
return Err(Error::custom( return Err(Error::custom("ClosedCaptions has to be `None`"));
"If one ClosedCaptions is None all have to be None!",
));
} }
if !self.check_media_group(MediaType::ClosedCaptions, group_id) { if !self.check_media_group(MediaType::ClosedCaptions, group_id) {
@ -197,14 +355,8 @@ impl MasterPlaylistBuilder {
} }
} }
} }
}
Ok(()) VariantStream::ExtXIFrame { stream_data, .. } => {
}
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 let Some(group_id) = stream_data.video() {
if !self.check_media_group(MediaType::Video, group_id) { if !self.check_media_group(MediaType::Video, group_id) {
return Err(Error::unmatched_group(group_id)); return Err(Error::unmatched_group(group_id));
@ -212,6 +364,7 @@ impl MasterPlaylistBuilder {
} }
} }
} }
}
Ok(()) Ok(())
} }
@ -219,12 +372,12 @@ impl MasterPlaylistBuilder {
fn validate_session_data_tags(&self) -> crate::Result<()> { fn validate_session_data_tags(&self) -> crate::Result<()> {
let mut set = HashSet::new(); let mut set = HashSet::new();
if let Some(value) = &self.session_data { if let Some(values) = &self.session_data {
set.reserve(value.len()); set.reserve(values.len());
for t in value { for tag in values {
if !set.insert((t.data_id(), t.language())) { if !set.insert((tag.data_id(), tag.language())) {
return Err(Error::custom(format!("Conflict: {}", t))); return Err(Error::custom(format!("conflict: {}", tag)));
} }
} }
} }
@ -250,10 +403,16 @@ impl RequiredVersion for MasterPlaylistBuilder {
// not for Option<Option<T>>) // not for Option<Option<T>>)
// https://github.com/rust-lang/chalk/issues/12 // https://github.com/rust-lang/chalk/issues/12
required_version![ required_version![
self.independent_segments.flatten(), {
if self.has_independent_segments.unwrap_or(false) {
Some(ExtXIndependentSegments)
} else {
None
}
},
self.start.flatten(), self.start.flatten(),
self.media, self.media,
self.variants, self.variant_streams,
self.session_data, self.session_data,
self.session_keys self.session_keys
] ]
@ -272,7 +431,7 @@ impl fmt::Display for MasterPlaylist {
writeln!(f, "{}", value)?; writeln!(f, "{}", value)?;
} }
for value in &self.variants { for value in &self.variant_streams {
writeln!(f, "{}", value)?; writeln!(f, "{}", value)?;
} }
@ -284,8 +443,8 @@ impl fmt::Display for MasterPlaylist {
writeln!(f, "{}", value)?; writeln!(f, "{}", value)?;
} }
if let Some(value) = &self.independent_segments { if self.has_independent_segments {
writeln!(f, "{}", value)?; writeln!(f, "{}", ExtXIndependentSegments)?;
} }
if let Some(value) = &self.start { if let Some(value) = &self.start {
@ -308,7 +467,7 @@ impl FromStr for MasterPlaylist {
let mut builder = Self::builder(); let mut builder = Self::builder();
let mut media = vec![]; let mut media = vec![];
let mut variants = vec![]; let mut variant_streams = vec![];
let mut session_data = vec![]; let mut session_data = vec![];
let mut session_keys = vec![]; let mut session_keys = vec![];
let mut unknown_tags = vec![]; let mut unknown_tags = vec![];
@ -336,16 +495,13 @@ impl FromStr for MasterPlaylist {
| Tag::ExtXEndList(_) | Tag::ExtXEndList(_)
| Tag::ExtXPlaylistType(_) | Tag::ExtXPlaylistType(_)
| Tag::ExtXIFramesOnly(_) => { | Tag::ExtXIFramesOnly(_) => {
return Err(Error::custom(format!( return Err(Error::unexpected_tag(tag));
"This tag isn't allowed in a master playlist: {}",
tag
)));
} }
Tag::ExtXMedia(t) => { Tag::ExtXMedia(t) => {
media.push(t); media.push(t);
} }
Tag::VariantStream(t) => { Tag::VariantStream(t) => {
variants.push(t); variant_streams.push(t);
} }
Tag::ExtXSessionData(t) => { Tag::ExtXSessionData(t) => {
session_data.push(t); session_data.push(t);
@ -353,8 +509,8 @@ impl FromStr for MasterPlaylist {
Tag::ExtXSessionKey(t) => { Tag::ExtXSessionKey(t) => {
session_keys.push(t); session_keys.push(t);
} }
Tag::ExtXIndependentSegments(t) => { Tag::ExtXIndependentSegments(_) => {
builder.independent_segments(t); builder.has_independent_segments(true);
} }
Tag::ExtXStart(t) => { Tag::ExtXStart(t) => {
builder.start(t); builder.start(t);
@ -367,14 +523,14 @@ impl FromStr for MasterPlaylist {
} }
} }
Line::Uri(uri) => { Line::Uri(uri) => {
return Err(Error::custom(format!("Unexpected URI: {:?}", uri))); return Err(Error::custom(format!("unexpected uri: {:?}", uri)));
} }
_ => {} _ => {}
} }
} }
builder.media(media); builder.media(media);
builder.variants(variants); builder.variant_streams(variant_streams);
builder.session_data(session_data); builder.session_data(session_data);
builder.session_keys(session_keys); builder.session_keys(session_keys);
builder.unknown_tags(unknown_tags); builder.unknown_tags(unknown_tags);
@ -389,6 +545,72 @@ mod tests {
use crate::types::StreamData; use crate::types::StreamData;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
#[test]
fn test_audio_streams() {
let astreams = vec![
VariantStream::ExtXStreamInf {
uri: "http://example.com/low/index.m3u8".into(),
frame_rate: None,
audio: Some("ag0".into()),
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
.bandwidth(150000)
.codecs(&["avc1.42e00a", "mp4a.40.2"])
.resolution((416, 234))
.build()
.unwrap(),
},
VariantStream::ExtXStreamInf {
uri: "http://example.com/lo_mid/index.m3u8".into(),
frame_rate: None,
audio: Some("ag1".into()),
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
.bandwidth(240000)
.codecs(&["avc1.42e00a", "mp4a.40.2"])
.resolution((416, 234))
.build()
.unwrap(),
},
];
let master_playlist = MasterPlaylist::builder()
.variant_streams(astreams.clone())
.media(vec![
ExtXMedia::builder()
.media_type(MediaType::Audio)
.uri("https://www.example.com/ag0.m3u8")
.group_id("ag0")
.language("english")
.name("alternative rendition for ag0")
.build()
.unwrap(),
ExtXMedia::builder()
.media_type(MediaType::Audio)
.uri("https://www.example.com/ag1.m3u8")
.group_id("ag1")
.language("english")
.name("alternative rendition for ag1")
.build()
.unwrap(),
])
.build()
.unwrap();
assert_eq!(
master_playlist.variant_streams,
master_playlist.audio_streams().collect::<Vec<_>>()
);
let mut audio_streams = master_playlist.audio_streams();
assert_eq!(audio_streams.next(), Some(&astreams[0]));
assert_eq!(audio_streams.next(), Some(&astreams[1]));
assert_eq!(audio_streams.next(), None);
}
#[test] #[test]
fn test_parser() { fn test_parser() {
assert_eq!( assert_eq!(
@ -412,7 +634,7 @@ mod tests {
.parse::<MasterPlaylist>() .parse::<MasterPlaylist>()
.unwrap(), .unwrap(),
MasterPlaylist::builder() MasterPlaylist::builder()
.variants(vec![ .variant_streams(vec![
VariantStream::ExtXStreamInf { VariantStream::ExtXStreamInf {
uri: "http://example.com/low/index.m3u8".into(), uri: "http://example.com/low/index.m3u8".into(),
frame_rate: None, frame_rate: None,
@ -487,7 +709,7 @@ mod tests {
fn test_display() { fn test_display() {
assert_eq!( assert_eq!(
MasterPlaylist::builder() MasterPlaylist::builder()
.variants(vec![ .variant_streams(vec![
VariantStream::ExtXStreamInf { VariantStream::ExtXStreamInf {
uri: "http://example.com/low/index.m3u8".into(), uri: "http://example.com/low/index.m3u8".into(),
frame_rate: None, frame_rate: None,