mirror of
https://github.com/sile/hls_m3u8.git
synced 2024-11-21 14:50:59 +00:00
made master playlist smarter
This commit is contained in:
parent
5b44262dc8
commit
f76b223482
11 changed files with 539 additions and 137 deletions
|
@ -1,6 +1,5 @@
|
|||
use std::collections::HashSet;
|
||||
use std::fmt;
|
||||
use std::iter;
|
||||
use std::str::FromStr;
|
||||
|
||||
use derive_builder::Builder;
|
||||
|
@ -13,19 +12,13 @@ use crate::tags::{
|
|||
use crate::types::{ClosedCaptions, MediaType, ProtocolVersion};
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// Master playlist.
|
||||
#[derive(Debug, Clone, Builder)]
|
||||
#[derive(Debug, Clone, Builder, PartialEq)]
|
||||
#[builder(build_fn(validate = "Self::validate"))]
|
||||
#[builder(setter(into, strip_option))]
|
||||
/// Master playlist.
|
||||
pub struct MasterPlaylist {
|
||||
#[builder(default, setter(name = "version"))]
|
||||
/// Sets the protocol compatibility version of the resulting playlist.
|
||||
///
|
||||
/// If the resulting playlist has tags which requires a compatibility
|
||||
/// version greater than `version`,
|
||||
/// `build()` method will fail with an `ErrorKind::InvalidInput` error.
|
||||
///
|
||||
/// The default is the maximum version among the tags in the playlist.
|
||||
//#[builder(default, setter(name = "version"))]
|
||||
#[builder(default, setter(skip))]
|
||||
version_tag: ExtXVersion,
|
||||
#[builder(default)]
|
||||
/// Sets the [`ExtXIndependentSegments`] tag.
|
||||
|
@ -37,16 +30,16 @@ pub struct MasterPlaylist {
|
|||
/// Sets the [`ExtXMedia`] tag.
|
||||
media_tags: Vec<ExtXMedia>,
|
||||
#[builder(default)]
|
||||
/// Sets all [`ExtXStreamInf`]s.
|
||||
/// Sets all [`ExtXStreamInf`] tags.
|
||||
stream_inf_tags: Vec<ExtXStreamInf>,
|
||||
#[builder(default)]
|
||||
/// Sets all [`ExtXIFrameStreamInf`]s.
|
||||
/// Sets all [`ExtXIFrameStreamInf`] tags.
|
||||
i_frame_stream_inf_tags: Vec<ExtXIFrameStreamInf>,
|
||||
#[builder(default)]
|
||||
/// Sets all [`ExtXSessionData`]s.
|
||||
/// Sets all [`ExtXSessionData`] tags.
|
||||
session_data_tags: Vec<ExtXSessionData>,
|
||||
#[builder(default)]
|
||||
/// Sets all [`ExtXSessionKey`]s.
|
||||
/// Sets all [`ExtXSessionKey`] tags.
|
||||
session_key_tags: Vec<ExtXSessionKey>,
|
||||
}
|
||||
|
||||
|
@ -54,48 +47,152 @@ impl MasterPlaylist {
|
|||
/// Returns a Builder for a [`MasterPlaylist`].
|
||||
pub fn builder() -> MasterPlaylistBuilder { MasterPlaylistBuilder::default() }
|
||||
|
||||
/// Returns the [`ExtXVersion`] tag contained in the playlist.
|
||||
pub const fn version(&self) -> ExtXVersion { self.version_tag }
|
||||
|
||||
/// Returns the [`ExtXIndependentSegments`] tag contained in the playlist.
|
||||
pub const fn independent_segments_tag(&self) -> Option<ExtXIndependentSegments> {
|
||||
pub const fn independent_segments(&self) -> Option<ExtXIndependentSegments> {
|
||||
self.independent_segments_tag
|
||||
}
|
||||
|
||||
/// Sets the [`ExtXIndependentSegments`] tag contained in the playlist.
|
||||
pub fn set_independent_segments<T>(&mut self, value: Option<T>) -> &mut Self
|
||||
where
|
||||
T: Into<ExtXIndependentSegments>,
|
||||
{
|
||||
self.independent_segments_tag = value.map(|v| v.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the [`ExtXStart`] tag contained in the playlist.
|
||||
pub const fn start_tag(&self) -> Option<ExtXStart> { self.start_tag }
|
||||
pub const fn start(&self) -> Option<ExtXStart> { self.start_tag }
|
||||
|
||||
/// Sets the [`ExtXStart`] tag contained in the playlist.
|
||||
pub fn set_start<T>(&mut self, value: Option<T>) -> &mut Self
|
||||
where
|
||||
T: Into<ExtXStart>,
|
||||
{
|
||||
self.start_tag = value.map(|v| v.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the [`ExtXMedia`] tags contained in the playlist.
|
||||
pub const fn media_tags(&self) -> &Vec<ExtXMedia> { &self.media_tags }
|
||||
|
||||
/// Appends an [`ExtXMedia`].
|
||||
pub fn push_media_tag(&mut self, value: ExtXMedia) -> &mut Self {
|
||||
self.media_tags.push(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`ExtXMedia`] tags contained in the playlist.
|
||||
pub fn set_media_tags<T>(&mut self, value: Vec<T>) -> &mut Self
|
||||
where
|
||||
T: Into<ExtXMedia>,
|
||||
{
|
||||
self.media_tags = value.into_iter().map(|v| v.into()).collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the [`ExtXStreamInf`] tags contained in the playlist.
|
||||
pub const fn stream_inf_tags(&self) -> &Vec<ExtXStreamInf> { &self.stream_inf_tags }
|
||||
|
||||
/// Appends an [`ExtXStreamInf`].
|
||||
pub fn push_stream_inf(&mut self, value: ExtXStreamInf) -> &mut Self {
|
||||
self.stream_inf_tags.push(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`ExtXStreamInf`] tags contained in the playlist.
|
||||
pub fn set_stream_inf_tags<T>(&mut self, value: Vec<T>) -> &mut Self
|
||||
where
|
||||
T: Into<ExtXStreamInf>,
|
||||
{
|
||||
self.stream_inf_tags = value.into_iter().map(|v| v.into()).collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the [`ExtXIFrameStreamInf`] tags contained in the playlist.
|
||||
pub const fn i_frame_stream_inf_tags(&self) -> &Vec<ExtXIFrameStreamInf> {
|
||||
&self.i_frame_stream_inf_tags
|
||||
}
|
||||
|
||||
/// Appends an [`ExtXIFrameStreamInf`].
|
||||
pub fn push_i_frame_stream_inf(&mut self, value: ExtXIFrameStreamInf) -> &mut Self {
|
||||
self.i_frame_stream_inf_tags.push(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`ExtXIFrameStreamInf`] tags contained in the playlist.
|
||||
pub fn set_i_frame_stream_inf_tags<T>(&mut self, value: Vec<T>) -> &mut Self
|
||||
where
|
||||
T: Into<ExtXIFrameStreamInf>,
|
||||
{
|
||||
self.i_frame_stream_inf_tags = value.into_iter().map(|v| v.into()).collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the [`ExtXSessionData`] tags contained in the playlist.
|
||||
pub const fn session_data_tags(&self) -> &Vec<ExtXSessionData> { &self.session_data_tags }
|
||||
|
||||
/// Appends an [`ExtXSessionData`].
|
||||
pub fn push_session_data(&mut self, value: ExtXSessionData) -> &mut Self {
|
||||
self.session_data_tags.push(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`ExtXSessionData`] tags contained in the playlist.
|
||||
pub fn set_session_data_tags<T>(&mut self, value: Vec<T>) -> &mut Self
|
||||
where
|
||||
T: Into<ExtXSessionData>,
|
||||
{
|
||||
self.session_data_tags = value.into_iter().map(|v| v.into()).collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the [`ExtXSessionKey`] tags contained in the playlist.
|
||||
pub const fn session_key_tags(&self) -> &Vec<ExtXSessionKey> { &self.session_key_tags }
|
||||
|
||||
/// Appends an [`ExtXSessionKey`].
|
||||
pub fn push_session_key(&mut self, value: ExtXSessionKey) -> &mut Self {
|
||||
self.session_key_tags.push(value);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`ExtXSessionKey`] tags contained in the playlist.
|
||||
pub fn set_session_key_tags<T>(&mut self, value: Vec<T>) -> &mut Self
|
||||
where
|
||||
T: Into<ExtXSessionKey>,
|
||||
{
|
||||
self.session_key_tags = value.into_iter().map(|v| v.into()).collect();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! required_version {
|
||||
( $( $tag:expr ),* ) => {
|
||||
::core::iter::empty()
|
||||
$(
|
||||
.chain(::core::iter::once($tag.required_version()))
|
||||
)*
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl RequiredVersion for MasterPlaylist {
|
||||
fn required_version(&self) -> ProtocolVersion { self.version_tag.version() }
|
||||
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
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl MasterPlaylistBuilder {
|
||||
fn validate(&self) -> Result<(), String> {
|
||||
let required_version = self.required_version();
|
||||
let specified_version = self.version_tag.map_or(required_version, |p| p.version());
|
||||
|
||||
if required_version > specified_version {
|
||||
return Err(Error::required_version(required_version, specified_version).to_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())?;
|
||||
|
@ -105,54 +202,6 @@ impl MasterPlaylistBuilder {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
iter::empty()
|
||||
.chain(
|
||||
self.independent_segments_tag
|
||||
.flatten()
|
||||
.iter()
|
||||
.map(|p| p.required_version()),
|
||||
)
|
||||
.chain(
|
||||
self.start_tag
|
||||
.flatten()
|
||||
.iter()
|
||||
.map(|p| p.required_version()),
|
||||
)
|
||||
.chain(
|
||||
self.media_tags
|
||||
.iter()
|
||||
.map(|t| t.iter().map(|t| t.required_version()))
|
||||
.flatten(),
|
||||
)
|
||||
.chain(
|
||||
self.stream_inf_tags
|
||||
.iter()
|
||||
.map(|t| t.iter().map(|t| t.required_version()))
|
||||
.flatten(),
|
||||
)
|
||||
.chain(
|
||||
self.i_frame_stream_inf_tags
|
||||
.iter()
|
||||
.map(|t| t.iter().map(|t| t.required_version()))
|
||||
.flatten(),
|
||||
)
|
||||
.chain(
|
||||
self.session_data_tags
|
||||
.iter()
|
||||
.map(|t| t.iter().map(|t| t.required_version()))
|
||||
.flatten(),
|
||||
)
|
||||
.chain(
|
||||
self.session_key_tags
|
||||
.iter()
|
||||
.map(|t| t.iter().map(|t| t.required_version()))
|
||||
.flatten(),
|
||||
)
|
||||
.max()
|
||||
.unwrap_or_else(ProtocolVersion::latest)
|
||||
}
|
||||
|
||||
fn validate_stream_inf_tags(&self) -> crate::Result<()> {
|
||||
if let Some(value) = &self.stream_inf_tags {
|
||||
let mut has_none_closed_captions = false;
|
||||
|
@ -232,11 +281,29 @@ impl MasterPlaylistBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
impl RequiredVersion for MasterPlaylistBuilder {
|
||||
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![
|
||||
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
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for MasterPlaylist {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
writeln!(f, "{}", ExtM3u)?;
|
||||
if self.version_tag.version() != ProtocolVersion::V1 {
|
||||
writeln!(f, "{}", self.version_tag)?;
|
||||
if self.required_version() != ProtocolVersion::V1 {
|
||||
writeln!(f, "{}", ExtXVersion::new(self.required_version()))?;
|
||||
}
|
||||
for t in &self.media_tags {
|
||||
writeln!(f, "{}", t)?;
|
||||
|
@ -288,8 +355,12 @@ impl FromStr for MasterPlaylist {
|
|||
Tag::ExtM3u(_) => {
|
||||
return Err(Error::invalid_input());
|
||||
}
|
||||
Tag::ExtXVersion(t) => {
|
||||
builder.version(t.version());
|
||||
Tag::ExtXVersion(_) => {
|
||||
// This tag can be ignored, because the
|
||||
// MasterPlaylist will automatically set the
|
||||
// ExtXVersion tag to correct version!
|
||||
|
||||
// builder.version(t.version());
|
||||
}
|
||||
Tag::ExtInf(_)
|
||||
| Tag::ExtXByteRange(_)
|
||||
|
@ -359,36 +430,35 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_parser() {
|
||||
r#"#EXTM3U
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=150000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234
|
||||
http://example.com/low/index.m3u8
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=240000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234
|
||||
http://example.com/lo_mid/index.m3u8
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=440000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234
|
||||
http://example.com/hi_mid/index.m3u8
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=640000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=640x360
|
||||
http://example.com/high/index.m3u8
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.5"
|
||||
http://example.com/audio/index.m3u8
|
||||
"#
|
||||
.parse::<MasterPlaylist>()
|
||||
.unwrap();
|
||||
"#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>()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
let input = r#"#EXTM3U
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=150000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234
|
||||
http://example.com/low/index.m3u8
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=240000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234
|
||||
http://example.com/lo_mid/index.m3u8
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=440000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234
|
||||
http://example.com/hi_mid/index.m3u8
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=640000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=640x360
|
||||
http://example.com/high/index.m3u8
|
||||
#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.5"
|
||||
http://example.com/audio/index.m3u8
|
||||
"#;
|
||||
let input = "#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";
|
||||
|
||||
let playlist = input.parse::<MasterPlaylist>().unwrap();
|
||||
assert_eq!(playlist.to_string(), input);
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ use crate::utils::tag;
|
|||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// # [4.3.1.1. EXTM3U]
|
||||
///
|
||||
/// The [`ExtM3u`] tag indicates that the file is an **Ext**ended **[`M3U`]**
|
||||
/// Playlist file.
|
||||
/// It is the at the start of every [`Media Playlist`] and [`Master Playlist`].
|
||||
|
|
|
@ -6,6 +6,7 @@ use crate::utils::tag;
|
|||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// # [4.3.1.2. EXT-X-VERSION]
|
||||
///
|
||||
/// The [`ExtXVersion`] tag indicates the compatibility version of the
|
||||
/// [`Master Playlist`] or [`Media Playlist`] file.
|
||||
/// It applies to the entire Playlist.
|
||||
|
@ -41,7 +42,7 @@ use crate::{Error, RequiredVersion};
|
|||
///
|
||||
/// [`Media Playlist`]: crate::MediaPlaylist
|
||||
/// [`Master Playlist`]: crate::MasterPlaylist
|
||||
/// [4.4.1.2. EXT-X-VERSION]: https://tools.ietf.org/html/rfc8216#section-4.3.1.2
|
||||
/// [4.3.1.2. EXT-X-VERSION]: https://tools.ietf.org/html/rfc8216#section-4.3.1.2
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
|
||||
pub struct ExtXVersion(ProtocolVersion);
|
||||
|
||||
|
|
|
@ -3,11 +3,12 @@ use std::ops::{Deref, DerefMut};
|
|||
use std::str::FromStr;
|
||||
|
||||
use crate::attribute::AttributePairs;
|
||||
use crate::types::{ProtocolVersion, StreamInf};
|
||||
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.
|
||||
///
|
||||
|
@ -23,6 +24,72 @@ pub struct ExtXIFrameStreamInf {
|
|||
stream_inf: StreamInf,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq)]
|
||||
/// Builder for [`ExtXIFrameStreamInf`].
|
||||
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_error)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtXIFrameStreamInf {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAME-STREAM-INF:";
|
||||
|
||||
|
@ -40,6 +107,9 @@ impl ExtXIFrameStreamInf {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns a builder for [`ExtXIFrameStreamInf`].
|
||||
pub fn builder() -> ExtXIFrameStreamInfBuilder { ExtXIFrameStreamInfBuilder::default() }
|
||||
|
||||
/// Returns the `URI`, that identifies the associated [`media playlist`].
|
||||
///
|
||||
/// # Example
|
||||
|
|
|
@ -3,11 +3,22 @@ use std::ops::{Deref, DerefMut};
|
|||
use std::str::FromStr;
|
||||
|
||||
use crate::attribute::AttributePairs;
|
||||
use crate::types::{ClosedCaptions, DecimalFloatingPoint, ProtocolVersion, StreamInf};
|
||||
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]
|
||||
/// # [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(PartialOrd, Debug, Clone, PartialEq, Eq)]
|
||||
|
@ -20,6 +31,104 @@ pub struct ExtXStreamInf {
|
|||
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_error)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ExtXStreamInf {
|
||||
pub(crate) const PREFIX: &'static str = "#EXT-X-STREAM-INF:";
|
||||
|
||||
|
@ -41,6 +150,9 @@ impl ExtXStreamInf {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns a builder for [`ExtXStreamInf`].
|
||||
pub fn builder() -> ExtXStreamInfBuilder { ExtXStreamInfBuilder::default() }
|
||||
|
||||
/// Returns the `URI` that identifies the associated media playlist.
|
||||
///
|
||||
/// # Example
|
||||
|
@ -169,7 +281,7 @@ impl ExtXStreamInf {
|
|||
/// ```
|
||||
pub const fn closed_captions(&self) -> &Option<ClosedCaptions> { &self.closed_captions }
|
||||
|
||||
/// Returns the value of [`ClosedCaptions`] attribute.
|
||||
/// Sets the value of [`ClosedCaptions`] attribute.
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
|
|
|
@ -5,26 +5,25 @@ use crate::types::ProtocolVersion;
|
|||
use crate::utils::tag;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// # [4.4.3.5. EXT-X-PLAYLIST-TYPE]
|
||||
/// # [4.3.3.5. EXT-X-PLAYLIST-TYPE]
|
||||
///
|
||||
/// The [`ExtXPlaylistType`] tag provides mutability information about the
|
||||
/// [`Media Playlist`]. It applies to the entire [`Media Playlist`].
|
||||
///
|
||||
/// Its format is:
|
||||
/// ```text
|
||||
/// #EXT-X-PLAYLIST-TYPE:<type-enum>
|
||||
/// ```
|
||||
///
|
||||
/// [Media Playlist]: crate::MediaPlaylist
|
||||
/// [4.4.3.5. EXT-X-PLAYLIST-TYPE]:
|
||||
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.3.5
|
||||
/// [`Media Playlist`]: crate::MediaPlaylist
|
||||
/// [4.3.3.5. EXT-X-PLAYLIST-TYPE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.5
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum ExtXPlaylistType {
|
||||
/// If the [`ExtXPlaylistType`] is Event, Media Segments
|
||||
/// can only be added to the end of the Media Playlist.
|
||||
/// If the [`ExtXPlaylistType`] is Event, [`Media Segment`]s
|
||||
/// can only be added to the end of the [`Media Playlist`].
|
||||
///
|
||||
/// [`Media Segment`]: crate::MediaSegment
|
||||
/// [`Media Playlist`]: crate::MediaPlaylist
|
||||
Event,
|
||||
/// If the [`ExtXPlaylistType`] is Video On Demand (Vod),
|
||||
/// the Media Playlist cannot change.
|
||||
/// the [`Media Playlist`] cannot change.
|
||||
///
|
||||
/// [`Media Playlist`]: crate::MediaPlaylist
|
||||
Vod,
|
||||
}
|
||||
|
||||
|
@ -32,6 +31,7 @@ impl ExtXPlaylistType {
|
|||
pub(crate) const PREFIX: &'static str = "#EXT-X-PLAYLIST-TYPE:";
|
||||
}
|
||||
|
||||
/// This tag requires [`ProtocolVersion::V1`].
|
||||
impl RequiredVersion for ExtXPlaylistType {
|
||||
fn required_version(&self) -> ProtocolVersion { ProtocolVersion::V1 }
|
||||
}
|
||||
|
@ -81,6 +81,8 @@ mod test {
|
|||
assert!("#EXT-X-PLAYLIST-TYPE:H"
|
||||
.parse::<ExtXPlaylistType>()
|
||||
.is_err());
|
||||
|
||||
assert!("garbage".parse::<ExtXPlaylistType>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -6,20 +6,13 @@ use crate::types::ProtocolVersion;
|
|||
use crate::utils::tag;
|
||||
use crate::{Error, RequiredVersion};
|
||||
|
||||
/// # [4.4.2.1. EXTINF]
|
||||
/// # [4.3.2.1. EXTINF]
|
||||
///
|
||||
/// The [`ExtInf`] tag specifies the duration of a [`Media Segment`]. It applies
|
||||
/// only to the next [`Media Segment`].
|
||||
///
|
||||
/// Its format is:
|
||||
/// ```text
|
||||
/// #EXTINF:<duration>,[<title>]
|
||||
/// ```
|
||||
/// The title is an optional informative title about the [Media Segment].
|
||||
///
|
||||
/// [`Media Segment`]: crate::media_segment::MediaSegment
|
||||
/// [4.4.2.1. EXTINF]:
|
||||
/// https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis-04#section-4.4.2.1
|
||||
/// [4.3.2.1. EXTINF]: https://tools.ietf.org/html/rfc8216#section-4.3.2.1
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct ExtInf {
|
||||
duration: Duration,
|
||||
|
|
|
@ -122,3 +122,22 @@ pub trait RequiredVersion {
|
|||
/// Returns the protocol compatibility version that this tag requires.
|
||||
fn required_version(&self) -> ProtocolVersion;
|
||||
}
|
||||
|
||||
impl<T: RequiredVersion> RequiredVersion for Vec<T> {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
self.iter()
|
||||
.map(|v| v.required_version())
|
||||
.max()
|
||||
// return ProtocolVersion::V1, if the iterator is empty:
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: RequiredVersion> RequiredVersion for Option<T> {
|
||||
fn required_version(&self) -> ProtocolVersion {
|
||||
self.iter()
|
||||
.map(|v| v.required_version())
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,14 +4,15 @@ use derive_more::Display;
|
|||
|
||||
use crate::Error;
|
||||
|
||||
/// Decimal resolution.
|
||||
/// This is a simple wrapper type for the display resolution. (1920x1080,
|
||||
/// 1280x720, ...).
|
||||
///
|
||||
/// See: [4.2. Attribute Lists]
|
||||
///
|
||||
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
||||
#[derive(Ord, PartialOrd, Debug, Clone, Copy, PartialEq, Eq, Hash, Display)]
|
||||
#[display(fmt = "{}x{}", width, height)]
|
||||
pub(crate) struct DecimalResolution {
|
||||
pub struct DecimalResolution {
|
||||
width: usize,
|
||||
height: usize,
|
||||
}
|
||||
|
|
|
@ -1,26 +1,44 @@
|
|||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use derive_builder::Builder;
|
||||
|
||||
use crate::attribute::AttributePairs;
|
||||
use crate::types::{DecimalResolution, HdcpLevel};
|
||||
use crate::utils::{quote, unquote};
|
||||
use crate::Error;
|
||||
|
||||
/// [4.3.4.2. EXT-X-STREAM-INF]
|
||||
/// # [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(PartialOrd, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[derive(Builder, PartialOrd, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[builder(setter(into, strip_option))]
|
||||
#[builder(derive(Debug, PartialEq))]
|
||||
pub struct StreamInf {
|
||||
/// The maximum bandwidth of the stream.
|
||||
bandwidth: u64,
|
||||
#[builder(default)]
|
||||
/// The average bandwidth of the stream.
|
||||
average_bandwidth: Option<u64>,
|
||||
#[builder(default)]
|
||||
/// Every media format in any of the renditions specified by the Variant
|
||||
/// Stream.
|
||||
codecs: Option<String>,
|
||||
#[builder(default)]
|
||||
/// The resolution of the stream.
|
||||
resolution: Option<DecimalResolution>,
|
||||
#[builder(default)]
|
||||
/// High-bandwidth Digital Content Protection
|
||||
hdcp_level: Option<HdcpLevel>,
|
||||
#[builder(default)]
|
||||
/// It indicates the set of video renditions, that should be used when
|
||||
/// playing the presentation.
|
||||
video: Option<String>,
|
||||
}
|
||||
|
||||
impl StreamInf {
|
||||
/// Creates a new [StreamInf].
|
||||
/// Creates a new [`StreamInf`].
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use hls_m3u8::types::StreamInf;
|
||||
|
|
115
tests/master_playlist.rs
Normal file
115
tests/master_playlist.rs
Normal file
|
@ -0,0 +1,115 @@
|
|||
use hls_m3u8::tags::{ExtXIFrameStreamInf, ExtXStreamInf};
|
||||
use hls_m3u8::MasterPlaylist;
|
||||
|
||||
#[test]
|
||||
fn test_master_playlist() {
|
||||
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();
|
||||
|
||||
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(),
|
||||
])
|
||||
.build()
|
||||
.unwrap(),
|
||||
master_playlist
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_master_playlist_with_i_frames() {
|
||||
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();
|
||||
|
||||
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(),
|
||||
])
|
||||
.build()
|
||||
.unwrap(),
|
||||
master_playlist
|
||||
);
|
||||
}
|
Loading…
Reference in a new issue