1
0
Fork 0
mirror of https://github.com/sile/hls_m3u8.git synced 2024-05-20 01:08:04 +00:00
hls_m3u8/src/master_playlist.rs

826 lines
30 KiB
Rust
Raw Normal View History

use std::borrow::Cow;
2019-09-13 14:06:52 +00:00
use std::collections::HashSet;
use std::convert::TryFrom;
2019-09-13 14:06:52 +00:00
use std::fmt;
2019-09-14 11:26:16 +00:00
use derive_builder::Builder;
2019-03-31 09:58:11 +00:00
use crate::line::{Line, Lines, Tag};
use crate::tags::{
2020-02-10 12:20:39 +00:00
ExtM3u, ExtXIndependentSegments, ExtXMedia, ExtXSessionData, ExtXSessionKey, ExtXStart,
ExtXVersion, VariantStream,
2019-03-31 09:58:11 +00:00
};
2019-10-04 09:02:21 +00:00
use crate::types::{ClosedCaptions, MediaType, ProtocolVersion};
2020-03-28 09:46:07 +00:00
use crate::utils::{tag, BoolExt};
2019-10-04 09:02:21 +00:00
use crate::{Error, RequiredVersion};
2018-02-14 01:31:24 +00:00
2020-02-10 12:20:39 +00:00
/// The master playlist describes all of the available variants for your
2020-02-14 12:05:18 +00:00
/// content.
2020-03-17 14:58:43 +00:00
///
2020-02-14 12:05:18 +00:00
/// Each variant is a version of the stream at a particular bitrate and is
2020-03-17 14:58:43 +00:00
/// contained in a separate playlist called [`MediaPlaylist`].
///
/// # Examples
///
/// A [`MasterPlaylist`] can be parsed from a `str`:
///
/// ```
/// use core::convert::TryFrom;
2020-03-17 14:58:43 +00:00
/// use hls_m3u8::MasterPlaylist;
///
/// // the concat! macro joins multiple `&'static str`.
/// let master_playlist = MasterPlaylist::try_from(concat!(
2020-03-17 14:58:43 +00:00
/// "#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"
/// ))?;
2020-03-17 14:58:43 +00:00
///
/// 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
2020-03-25 15:13:40 +00:00
#[derive(Builder, Default, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
2019-09-14 11:26:16 +00:00
#[builder(build_fn(validate = "Self::validate"))]
2019-09-14 19:08:35 +00:00
#[builder(setter(into, strip_option))]
2020-03-28 09:46:07 +00:00
#[non_exhaustive]
pub struct MasterPlaylist<'a> {
2020-03-17 14:58:43 +00:00
/// Indicates that all media samples in a [`MediaSegment`] can be
/// decoded without information from other segments.
2019-10-05 12:45:40 +00:00
///
2020-03-17 14:58:43 +00:00
/// ### Note
2020-02-02 12:38:11 +00:00
///
2020-03-17 14:58:43 +00:00
/// This field is optional and by default `false`. If the field is `true`,
/// it applies to every [`MediaSegment`] in every [`MediaPlaylist`] of this
/// [`MasterPlaylist`].
2020-02-02 12:38:11 +00:00
///
2020-02-10 12:20:39 +00:00
/// [`MediaSegment`]: crate::MediaSegment
/// [`MediaPlaylist`]: crate::MediaPlaylist
2019-10-05 07:44:23 +00:00
#[builder(default)]
2020-03-17 14:58:43 +00:00
pub has_independent_segments: bool,
/// A preferred point at which to start playing a playlist.
2019-10-05 12:45:40 +00:00
///
2020-03-17 14:58:43 +00:00
/// ### Note
2020-02-02 12:38:11 +00:00
///
2020-03-17 14:58:43 +00:00
/// This field is optional and by default the playlist should be played from
/// the start.
2019-10-05 07:44:23 +00:00
#[builder(default)]
2020-03-17 14:58:43 +00:00
pub start: Option<ExtXStart>,
/// A list of all [`ExtXMedia`] tags, which describe an alternative
/// rendition.
2020-02-10 12:20:39 +00:00
///
/// 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.
2019-10-05 12:45:40 +00:00
///
2020-03-17 14:58:43 +00:00
/// ### Note
2020-02-02 12:38:11 +00:00
///
2020-03-17 14:58:43 +00:00
/// This field is optional.
2020-02-10 12:20:39 +00:00
///
/// [`MediaPlaylist`]: crate::MediaPlaylist
2019-10-05 07:44:23 +00:00
#[builder(default)]
pub media: Vec<ExtXMedia<'a>>,
2020-02-10 12:20:39 +00:00
/// A list of all streams of this [`MasterPlaylist`].
2019-10-05 12:45:40 +00:00
///
2020-03-17 14:58:43 +00:00
/// ### Note
2020-02-02 12:38:11 +00:00
///
2020-03-17 14:58:43 +00:00
/// This field is optional.
2019-10-05 07:44:23 +00:00
#[builder(default)]
pub variant_streams: Vec<VariantStream<'a>>,
2020-02-14 12:05:18 +00:00
/// The [`ExtXSessionData`] tag allows arbitrary session data to be
/// carried in a [`MasterPlaylist`].
2019-10-05 12:45:40 +00:00
///
2020-03-17 14:58:43 +00:00
/// ### Note
2020-02-02 12:38:11 +00:00
///
2020-03-17 14:58:43 +00:00
/// This field is optional.
2019-10-05 07:44:23 +00:00
#[builder(default)]
pub session_data: Vec<ExtXSessionData<'a>>,
2020-03-17 14:58:43 +00:00
/// A list of [`ExtXSessionKey`]s, that allows the client to preload
2020-02-14 12:05:18 +00:00
/// these keys without having to read the [`MediaPlaylist`]s first.
2019-10-05 12:45:40 +00:00
///
2020-03-17 14:58:43 +00:00
/// ### Note
2020-02-02 12:38:11 +00:00
///
2020-03-17 14:58:43 +00:00
/// This field is optional.
2020-02-21 19:44:09 +00:00
///
/// [`MediaPlaylist`]: crate::MediaPlaylist
2020-02-02 12:38:11 +00:00
#[builder(default)]
pub session_keys: Vec<ExtXSessionKey<'a>>,
2020-03-17 14:58:43 +00:00
/// A list of all tags that could not be identified while parsing the input.
///
2020-03-17 14:58:43 +00:00
/// ### Note
///
2020-03-17 14:58:43 +00:00
/// This field is optional.
#[builder(default)]
pub unknown_tags: Vec<Cow<'a, str>>,
2018-02-14 17:52:56 +00:00
}
2019-09-08 09:30:52 +00:00
impl<'a> MasterPlaylist<'a> {
2020-02-02 12:38:11 +00:00
/// Returns a builder for a [`MasterPlaylist`].
2019-10-05 12:45:40 +00:00
///
/// # Example
///
2019-10-05 12:45:40 +00:00
/// ```
2020-02-24 15:44:02 +00:00
/// # use hls_m3u8::MasterPlaylist;
2020-03-17 14:58:43 +00:00
/// use hls_m3u8::tags::{ExtXStart, VariantStream};
/// use hls_m3u8::types::{Float, StreamData};
2019-10-05 12:45:40 +00:00
///
/// MasterPlaylist::builder()
2020-03-17 14:58:43 +00:00
/// .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)))
2019-10-05 12:45:40 +00:00
/// .build()?;
/// # Ok::<(), Box<dyn ::std::error::Error>>(())
2019-10-05 12:45:40 +00:00
/// ```
2020-02-24 15:30:43 +00:00
#[must_use]
#[inline]
pub fn builder() -> MasterPlaylistBuilder<'a> { MasterPlaylistBuilder::default() }
2020-03-17 14:58:43 +00:00
/// Returns all streams, which have an audio group id.
pub fn audio_streams(&self) -> impl Iterator<Item = &VariantStream<'a>> {
2021-10-01 11:04:57 +00:00
self.variant_streams
.iter()
.filter(|stream| matches!(stream, VariantStream::ExtXStreamInf { audio: Some(_), .. }))
2020-03-17 14:58:43 +00:00
}
/// Returns all streams, which have a video group id.
pub fn video_streams(&self) -> impl Iterator<Item = &VariantStream<'a>> {
2020-03-17 14:58:43 +00:00
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<'a>> {
2020-03-17 14:58:43 +00:00
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<'b>(
&'b self,
stream: &'b VariantStream<'_>,
) -> impl Iterator<Item = &ExtXMedia<'a>> + 'b {
2020-03-17 14:58:43 +00:00
self.media
.iter()
.filter(move |media| stream.is_associated(media))
}
/// Makes the struct independent of its lifetime, by taking ownership of all
/// internal [`Cow`]s.
///
/// # Note
///
/// This is a relatively expensive operation.
#[must_use]
2020-08-11 09:13:14 +00:00
#[allow(clippy::redundant_closure_for_method_calls)]
pub fn into_owned(self) -> MasterPlaylist<'static> {
MasterPlaylist {
has_independent_segments: self.has_independent_segments,
start: self.start,
media: self.media.into_iter().map(|v| v.into_owned()).collect(),
variant_streams: self
.variant_streams
.into_iter()
.map(|v| v.into_owned())
.collect(),
session_data: self
.session_data
.into_iter()
.map(|v| v.into_owned())
.collect(),
session_keys: self
.session_keys
.into_iter()
.map(|v| v.into_owned())
.collect(),
unknown_tags: self
.unknown_tags
.into_iter()
.map(|v| Cow::Owned(v.into_owned()))
.collect(),
}
}
2019-10-05 10:49:08 +00:00
}
impl<'a> RequiredVersion for MasterPlaylist<'a> {
2019-10-05 10:49:08 +00:00
fn required_version(&self) -> ProtocolVersion {
required_version![
2020-03-28 09:46:07 +00:00
self.has_independent_segments
.athen_some(ExtXIndependentSegments),
2020-02-10 12:20:39 +00:00
self.start,
self.media,
2020-03-17 14:58:43 +00:00
self.variant_streams,
2020-02-10 12:20:39 +00:00
self.session_data,
self.session_keys
2019-10-05 10:49:08 +00:00
]
}
2019-09-22 08:57:28 +00:00
}
impl<'a> MasterPlaylistBuilder<'a> {
2019-09-14 19:08:35 +00:00
fn validate(&self) -> Result<(), String> {
2020-03-17 14:58:43 +00:00
if let Some(variant_streams) = &self.variant_streams {
self.validate_variants(variant_streams)
.map_err(|e| e.to_string())?;
}
2019-09-14 19:08:35 +00:00
self.validate_session_data_tags()
.map_err(|e| e.to_string())?;
Ok(())
}
fn validate_variants(&self, variant_streams: &[VariantStream<'_>]) -> crate::Result<()> {
2020-02-14 12:05:18 +00:00
let mut closed_captions_none = false;
2020-02-10 12:20:39 +00:00
2020-03-17 14:58:43 +00:00
for variant in variant_streams {
match &variant {
VariantStream::ExtXStreamInf {
audio,
subtitles,
closed_captions,
stream_data,
..
} => {
if let Some(group_id) = &audio {
if !self.check_media_group(MediaType::Audio, group_id) {
return Err(Error::unmatched_group(group_id));
}
2019-09-14 11:26:16 +00:00
}
2020-03-17 14:58:43 +00:00
if let Some(group_id) = &stream_data.video() {
if !self.check_media_group(MediaType::Video, group_id) {
return Err(Error::unmatched_group(group_id));
}
2019-09-14 11:26:16 +00:00
}
2020-03-17 14:58:43 +00:00
if let Some(group_id) = &subtitles {
if !self.check_media_group(MediaType::Subtitles, group_id) {
return Err(Error::unmatched_group(group_id));
}
2019-09-14 11:26:16 +00:00
}
2020-02-10 12:20:39 +00:00
2020-03-17 14:58:43 +00:00
if let Some(closed_captions) = &closed_captions {
match &closed_captions {
ClosedCaptions::GroupId(group_id) => {
if closed_captions_none {
return Err(Error::custom("ClosedCaptions has to be `None`"));
}
2020-02-14 12:05:18 +00:00
2020-03-17 14:58:43 +00:00
if !self.check_media_group(MediaType::ClosedCaptions, group_id) {
return Err(Error::unmatched_group(group_id));
}
2020-02-10 12:20:39 +00:00
}
2020-03-17 14:58:43 +00:00
_ => {
if !closed_captions_none {
closed_captions_none = true;
}
2020-02-14 12:05:18 +00:00
}
2019-09-14 11:26:16 +00:00
}
}
}
2020-03-17 14:58:43 +00:00
VariantStream::ExtXIFrame { stream_data, .. } => {
if let Some(group_id) = stream_data.video() {
if !self.check_media_group(MediaType::Video, group_id) {
return Err(Error::unmatched_group(group_id));
}
2019-09-14 11:26:16 +00:00
}
}
}
}
2020-02-10 12:20:39 +00:00
2019-09-14 19:08:35 +00:00
Ok(())
}
2019-09-14 11:26:16 +00:00
2019-09-14 19:08:35 +00:00
fn validate_session_data_tags(&self) -> crate::Result<()> {
let mut set = HashSet::new();
2020-02-10 12:20:39 +00:00
2020-03-17 14:58:43 +00:00
if let Some(values) = &self.session_data {
set.reserve(values.len());
2020-02-14 12:05:18 +00:00
2020-03-17 14:58:43 +00:00
for tag in values {
if !set.insert((tag.data_id(), tag.language())) {
return Err(Error::custom(format!("conflict: {}", tag)));
2019-09-14 11:26:16 +00:00
}
}
}
2020-02-10 12:20:39 +00:00
2019-09-14 19:08:35 +00:00
Ok(())
}
2019-09-14 11:26:16 +00:00
2020-02-10 12:20:39 +00:00
fn check_media_group<T: AsRef<str>>(&self, media_type: MediaType, group_id: T) -> bool {
2021-10-01 11:04:57 +00:00
self.media.as_ref().map_or(false, |value| {
2020-03-17 14:39:07 +00:00
value.iter().any(|media| {
media.media_type == media_type && media.group_id().as_ref() == group_id.as_ref()
2020-03-17 14:39:07 +00:00
})
2021-10-01 11:04:57 +00:00
})
2019-09-14 11:26:16 +00:00
}
}
impl<'a> RequiredVersion for MasterPlaylistBuilder<'a> {
2019-10-05 10:49:08 +00:00
fn required_version(&self) -> ProtocolVersion {
// TODO: the .flatten() can be removed as soon as `recursive traits` are
// supported. (RequiredVersion is implemented for Option<T>, but
// not for Option<Option<T>>)
// https://github.com/rust-lang/chalk/issues/12
required_version![
2020-03-28 09:46:07 +00:00
self.has_independent_segments
.unwrap_or(false)
.athen_some(ExtXIndependentSegments),
2020-02-10 12:20:39 +00:00
self.start.flatten(),
self.media,
2020-03-17 14:58:43 +00:00
self.variant_streams,
2020-02-10 12:20:39 +00:00
self.session_data,
self.session_keys
2019-10-05 10:49:08 +00:00
]
}
}
impl<'a> fmt::Display for MasterPlaylist<'a> {
2020-04-09 06:43:13 +00:00
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2018-02-14 01:31:24 +00:00
writeln!(f, "{}", ExtM3u)?;
2020-02-10 12:20:39 +00:00
2019-10-05 10:49:08 +00:00
if self.required_version() != ProtocolVersion::V1 {
writeln!(f, "{}", ExtXVersion::new(self.required_version()))?;
2018-02-14 01:31:24 +00:00
}
2020-02-02 12:38:11 +00:00
2020-02-14 12:05:18 +00:00
for value in &self.media {
writeln!(f, "{}", value)?;
2018-02-14 01:31:24 +00:00
}
2020-02-02 12:38:11 +00:00
2020-03-17 14:58:43 +00:00
for value in &self.variant_streams {
2020-02-14 12:05:18 +00:00
writeln!(f, "{}", value)?;
2018-02-14 01:31:24 +00:00
}
2020-02-02 12:38:11 +00:00
2020-02-14 12:05:18 +00:00
for value in &self.session_data {
writeln!(f, "{}", value)?;
2018-02-14 01:31:24 +00:00
}
2020-02-02 12:38:11 +00:00
2020-02-14 12:05:18 +00:00
for value in &self.session_keys {
writeln!(f, "{}", value)?;
2018-02-14 01:31:24 +00:00
}
2020-02-02 12:38:11 +00:00
2020-03-17 14:58:43 +00:00
if self.has_independent_segments {
writeln!(f, "{}", ExtXIndependentSegments)?;
2018-02-14 01:31:24 +00:00
}
2020-02-02 12:38:11 +00:00
2020-02-10 12:20:39 +00:00
if let Some(value) = &self.start {
2019-09-14 19:42:06 +00:00
writeln!(f, "{}", value)?;
2018-02-14 01:31:24 +00:00
}
2020-02-02 12:38:11 +00:00
2020-02-14 12:05:18 +00:00
for value in &self.unknown_tags {
writeln!(f, "{}", value)?;
}
2018-02-14 01:31:24 +00:00
Ok(())
}
}
2019-09-13 14:06:52 +00:00
impl<'a> TryFrom<&'a str> for MasterPlaylist<'a> {
type Error = Error;
2019-09-14 09:57:56 +00:00
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
2020-02-06 11:27:48 +00:00
let input = tag(input, ExtM3u::PREFIX)?;
2019-10-05 14:08:03 +00:00
let mut builder = Self::builder();
2019-09-14 11:26:16 +00:00
2020-02-10 12:20:39 +00:00
let mut media = vec![];
2020-03-17 14:58:43 +00:00
let mut variant_streams = vec![];
2020-02-10 12:20:39 +00:00
let mut session_data = vec![];
let mut session_keys = vec![];
let mut unknown_tags = vec![];
2019-09-14 11:26:16 +00:00
2020-02-06 11:27:48 +00:00
for line in Lines::from(input) {
2020-02-02 13:33:57 +00:00
match line? {
2018-02-14 01:31:24 +00:00
Line::Tag(tag) => {
match tag {
2019-10-05 10:49:08 +00:00
Tag::ExtXVersion(_) => {
// This tag can be ignored, because the
// MasterPlaylist will automatically set the
2020-02-06 11:27:48 +00:00
// ExtXVersion tag to the minimum required version
// TODO: this might be verified?
2018-02-14 01:31:24 +00:00
}
Tag::ExtInf(_)
| Tag::ExtXByteRange(_)
| Tag::ExtXDiscontinuity(_)
| Tag::ExtXKey(_)
| Tag::ExtXMap(_)
| Tag::ExtXProgramDateTime(_)
| Tag::ExtXDateRange(_)
| Tag::ExtXTargetDuration(_)
| Tag::ExtXMediaSequence(_)
| Tag::ExtXDiscontinuitySequence(_)
| Tag::ExtXEndList(_)
| Tag::PlaylistType(_)
2018-02-14 01:31:24 +00:00
| Tag::ExtXIFramesOnly(_) => {
2020-03-17 14:58:43 +00:00
return Err(Error::unexpected_tag(tag));
2018-02-14 01:31:24 +00:00
}
Tag::ExtXMedia(t) => {
2020-02-10 12:20:39 +00:00
media.push(t);
2018-02-14 01:31:24 +00:00
}
2020-02-10 12:20:39 +00:00
Tag::VariantStream(t) => {
2020-03-17 14:58:43 +00:00
variant_streams.push(t);
2018-02-14 01:31:24 +00:00
}
Tag::ExtXSessionData(t) => {
2020-02-10 12:20:39 +00:00
session_data.push(t);
2018-02-14 01:31:24 +00:00
}
Tag::ExtXSessionKey(t) => {
2020-02-10 12:20:39 +00:00
session_keys.push(t);
2018-02-14 01:31:24 +00:00
}
2020-03-17 14:58:43 +00:00
Tag::ExtXIndependentSegments(_) => {
builder.has_independent_segments(true);
2018-02-14 01:31:24 +00:00
}
Tag::ExtXStart(t) => {
2020-02-10 12:20:39 +00:00
builder.start(t);
2018-02-14 01:31:24 +00:00
}
Tag::Unknown(value) => {
2018-02-14 15:50:57 +00:00
// [6.3.1. General Client Responsibilities]
// > ignore any unrecognized tags.
unknown_tags.push(Cow::Borrowed(value));
2018-02-14 15:50:57 +00:00
}
2018-02-14 01:31:24 +00:00
}
}
Line::Uri(uri) => {
2020-03-17 14:58:43 +00:00
return Err(Error::custom(format!("unexpected uri: {:?}", uri)));
2018-02-14 01:31:24 +00:00
}
2020-08-11 09:13:14 +00:00
Line::Comment(_) => {}
2018-02-14 01:31:24 +00:00
}
}
2019-09-14 11:26:16 +00:00
2020-02-10 12:20:39 +00:00
builder.media(media);
2020-03-17 14:58:43 +00:00
builder.variant_streams(variant_streams);
2020-02-10 12:20:39 +00:00
builder.session_data(session_data);
builder.session_keys(session_keys);
builder.unknown_tags(unknown_tags);
2019-09-14 11:26:16 +00:00
builder.build().map_err(Error::builder)
2018-02-14 01:31:24 +00:00
}
}
2019-09-14 09:57:56 +00:00
#[cfg(test)]
mod tests {
use super::*;
2020-02-14 12:05:18 +00:00
use crate::types::StreamData;
use pretty_assertions::assert_eq;
2019-09-14 09:57:56 +00:00
2020-03-17 14:58:43 +00:00
#[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()
2020-08-11 09:13:14 +00:00
.bandwidth(150_000)
2020-03-17 14:58:43 +00:00
.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()
2020-08-11 09:13:14 +00:00
.bandwidth(240_000)
2020-03-17 14:58:43 +00:00
.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);
}
2019-09-14 09:57:56 +00:00
#[test]
2019-09-14 10:34:34 +00:00
fn test_parser() {
2020-02-14 12:05:18 +00:00
assert_eq!(
MasterPlaylist::try_from(concat!(
2020-02-14 12:05:18 +00:00
"#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"
))
2020-02-14 12:05:18 +00:00
.unwrap(),
MasterPlaylist::builder()
2020-03-17 14:58:43 +00:00
.variant_streams(vec![
2020-02-14 12:05:18 +00:00
VariantStream::ExtXStreamInf {
uri: "http://example.com/low/index.m3u8".into(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
2020-08-11 09:13:14 +00:00
.bandwidth(150_000)
2020-02-24 13:09:26 +00:00
.codecs(&["avc1.42e00a", "mp4a.40.2"])
2020-02-14 12:05:18 +00:00
.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()
2020-08-11 09:13:14 +00:00
.bandwidth(240_000)
2020-02-24 13:09:26 +00:00
.codecs(&["avc1.42e00a", "mp4a.40.2"])
2020-02-14 12:05:18 +00:00
.resolution((416, 234))
.build()
.unwrap()
},
VariantStream::ExtXStreamInf {
uri: "http://example.com/hi_mid/index.m3u8".into(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
2020-08-11 09:13:14 +00:00
.bandwidth(440_000)
2020-02-24 13:09:26 +00:00
.codecs(&["avc1.42e00a", "mp4a.40.2"])
2020-02-14 12:05:18 +00:00
.resolution((416, 234))
.build()
.unwrap()
},
VariantStream::ExtXStreamInf {
uri: "http://example.com/high/index.m3u8".into(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
2020-08-11 09:13:14 +00:00
.bandwidth(640_000)
2020-02-24 13:09:26 +00:00
.codecs(&["avc1.42e00a", "mp4a.40.2"])
2020-02-14 12:05:18 +00:00
.resolution((640, 360))
.build()
.unwrap()
},
VariantStream::ExtXStreamInf {
uri: "http://example.com/audio/index.m3u8".into(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
.bandwidth(64000)
2020-02-24 13:09:26 +00:00
.codecs(&["mp4a.40.5"])
2020-02-14 12:05:18 +00:00
.build()
.unwrap()
},
])
.build()
.unwrap()
);
2019-09-14 10:34:34 +00:00
}
2019-09-14 11:26:16 +00:00
#[test]
fn test_display() {
2020-02-14 12:05:18 +00:00
assert_eq!(
MasterPlaylist::builder()
2020-03-17 14:58:43 +00:00
.variant_streams(vec![
2020-02-14 12:05:18 +00:00
VariantStream::ExtXStreamInf {
uri: "http://example.com/low/index.m3u8".into(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
2020-08-11 09:13:14 +00:00
.bandwidth(150_000)
2020-02-24 13:09:26 +00:00
.codecs(&["avc1.42e00a", "mp4a.40.2"])
2020-02-14 12:05:18 +00:00
.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()
2020-08-11 09:13:14 +00:00
.bandwidth(240_000)
2020-02-24 13:09:26 +00:00
.codecs(&["avc1.42e00a", "mp4a.40.2"])
2020-02-14 12:05:18 +00:00
.resolution((416, 234))
.build()
.unwrap()
},
VariantStream::ExtXStreamInf {
uri: "http://example.com/hi_mid/index.m3u8".into(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
2020-08-11 09:13:14 +00:00
.bandwidth(440_000)
2020-02-24 13:09:26 +00:00
.codecs(&["avc1.42e00a", "mp4a.40.2"])
2020-02-14 12:05:18 +00:00
.resolution((416, 234))
.build()
.unwrap()
},
VariantStream::ExtXStreamInf {
uri: "http://example.com/high/index.m3u8".into(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
2020-08-11 09:13:14 +00:00
.bandwidth(640_000)
2020-02-24 13:09:26 +00:00
.codecs(&["avc1.42e00a", "mp4a.40.2"])
2020-02-14 12:05:18 +00:00
.resolution((640, 360))
.build()
.unwrap()
},
VariantStream::ExtXStreamInf {
uri: "http://example.com/audio/index.m3u8".into(),
frame_rate: None,
audio: None,
subtitles: None,
closed_captions: None,
stream_data: StreamData::builder()
.bandwidth(64000)
2020-02-24 13:09:26 +00:00
.codecs(&["mp4a.40.5"])
2020-02-14 12:05:18 +00:00
.build()
.unwrap()
},
])
.build()
.unwrap()
2020-02-16 16:14:28 +00:00
.to_string(),
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"
)
.to_string()
2020-02-14 12:05:18 +00:00
);
2019-09-14 11:26:16 +00:00
}
2019-09-14 09:57:56 +00:00
}