1
0
Fork 0
mirror of https://github.com/sile/hls_m3u8.git synced 2024-11-28 17:50:59 +00:00

added media_playlist builder

This commit is contained in:
Luro02 2019-09-14 21:08:35 +02:00
parent b1aa512679
commit dd1a40abc9
6 changed files with 462 additions and 389 deletions

View file

@ -6,56 +6,63 @@ use failure::{Backtrace, Context, Fail};
/// This crate specific `Result` type. /// This crate specific `Result` type.
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Fail, Clone)] /// The ErrorKind.
pub enum AttributeError {
#[fail(display = "The attribute has an invalid name; {:?}", _0)]
InvalidAttribute(String),
#[fail(display = "A value is missing for the attribute: {}", _0)]
MissingValue(String),
}
#[derive(Debug, Fail, Clone)] #[derive(Debug, Fail, Clone)]
pub enum ErrorKind { pub enum ErrorKind {
#[fail(display = "AttributeError: {}", _0)]
AttributeError(AttributeError),
#[fail(display = "UnknownError: {}", _0)] #[fail(display = "UnknownError: {}", _0)]
/// An unknown error occured.
UnknownError(String), UnknownError(String),
#[fail(display = "A value is missing for the attribute {}", _0)] #[fail(display = "A value is missing for the attribute {}", _0)]
/// A required value is missing.
MissingValue(String), MissingValue(String),
#[fail(display = "Invalid Input")] #[fail(display = "Invalid Input")]
/// Error for anything.
InvalidInput, InvalidInput,
#[fail(display = "ParseIntError: {}", _0)] #[fail(display = "ParseIntError: {}", _0)]
/// Failed to parse a String to int.
ParseIntError(String), ParseIntError(String),
#[fail(display = "ParseFloatError: {}", _0)] #[fail(display = "ParseFloatError: {}", _0)]
/// Failed to parse a String to float.
ParseFloatError(String), ParseFloatError(String),
#[fail(display = "MissingTag: Expected {} at the start of {:?}", tag, input)] #[fail(display = "MissingTag: Expected {} at the start of {:?}", tag, input)]
MissingTag { tag: String, input: String }, /// A tag is missing, that is required at the start of the input.
MissingTag {
/// The required tag.
tag: String,
/// The unparsed input data.
input: String,
},
#[fail(display = "CustomError: {}", _0)] #[fail(display = "CustomError: {}", _0)]
/// A custom error.
Custom(String), Custom(String),
#[fail(display = "Unmatched Group: {:?}", _0)] #[fail(display = "Unmatched Group: {:?}", _0)]
/// Unmatched Group
UnmatchedGroup(String), UnmatchedGroup(String),
#[fail(display = "Unknown Protocol version: {:?}", _0)] #[fail(display = "Unknown Protocol version: {:?}", _0)]
/// Unknown m3u8 version. This library supports up to ProtocolVersion 7.
UnknownProtocolVersion(String), UnknownProtocolVersion(String),
#[fail(display = "IoError: {}", _0)] #[fail(display = "IoError: {}", _0)]
/// Some io error
Io(String), Io(String),
#[fail( #[fail(
display = "VersionError: required_version: {:?}, specified_version: {:?}", display = "VersionError: required_version: {:?}, specified_version: {:?}",
_0, _1 _0, _1
)] )]
/// This error occurs, if there is a ProtocolVersion mismatch.
VersionError(String, String), VersionError(String, String),
#[fail(display = "BuilderError: {}", _0)] #[fail(display = "BuilderError: {}", _0)]
/// An Error from a Builder.
BuilderError(String), BuilderError(String),
/// Hints that destructuring should not be exhaustive. /// Hints that destructuring should not be exhaustive.
@ -69,6 +76,7 @@ pub enum ErrorKind {
} }
#[derive(Debug)] #[derive(Debug)]
/// The Error type of this library.
pub struct Error { pub struct Error {
inner: Context<ErrorKind>, inner: Context<ErrorKind>,
} }
@ -101,33 +109,7 @@ impl From<Context<ErrorKind>> for Error {
} }
} }
macro_rules! from_error {
( $( $f:tt ),* ) => {
$(
impl From<$f> for ErrorKind {
fn from(value: $f) -> Self {
Self::$f(value)
}
}
)*
}
}
from_error!(AttributeError);
impl Error { impl Error {
pub(crate) fn invalid_attribute<T: ToString>(value: T) -> Self {
Self::from(ErrorKind::from(AttributeError::InvalidAttribute(
value.to_string(),
)))
}
pub(crate) fn missing_attribute_value<T: ToString>(value: T) -> Self {
Self::from(ErrorKind::from(AttributeError::MissingValue(
value.to_string(),
)))
}
pub(crate) fn unknown<T>(value: T) -> Self pub(crate) fn unknown<T>(value: T) -> Self
where where
T: error::Error, T: error::Error,
@ -197,19 +179,19 @@ impl Error {
} }
} }
impl From<std::num::ParseIntError> for Error { impl From<::std::num::ParseIntError> for Error {
fn from(value: ::std::num::ParseIntError) -> Self { fn from(value: ::std::num::ParseIntError) -> Self {
Error::parse_int_error(value) Error::parse_int_error(value)
} }
} }
impl From<std::num::ParseFloatError> for Error { impl From<::std::num::ParseFloatError> for Error {
fn from(value: ::std::num::ParseFloatError) -> Self { fn from(value: ::std::num::ParseFloatError) -> Self {
Error::parse_float_error(value) Error::parse_float_error(value)
} }
} }
impl From<std::io::Error> for Error { impl From<::std::io::Error> for Error {
fn from(value: ::std::io::Error) -> Self { fn from(value: ::std::io::Error) -> Self {
Error::io(value) Error::io(value)
} }

View file

@ -29,7 +29,7 @@
pub use error::{Error, ErrorKind}; pub use error::{Error, ErrorKind};
pub use master_playlist::{MasterPlaylist, MasterPlaylistBuilder}; pub use master_playlist::{MasterPlaylist, MasterPlaylistBuilder};
pub use media_playlist::{MediaPlaylist, MediaPlaylistBuilder, MediaPlaylistOptions}; pub use media_playlist::{MediaPlaylist, MediaPlaylistBuilder};
pub use media_segment::{MediaSegment, MediaSegmentBuilder}; pub use media_segment::{MediaSegment, MediaSegmentBuilder};
pub mod tags; pub mod tags;

View file

@ -1,5 +1,6 @@
use std::collections::HashSet; use std::collections::HashSet;
use std::fmt; use std::fmt;
use std::iter;
use std::str::FromStr; use std::str::FromStr;
use derive_builder::Builder; use derive_builder::Builder;
@ -13,17 +14,34 @@ use crate::types::{ClosedCaptions, MediaType, ProtocolVersion};
use crate::Error; use crate::Error;
/// Master playlist. /// Master playlist.
#[derive(Debug, Clone, Builder, Default)] #[derive(Debug, Clone, Builder)]
#[builder(build_fn(validate = "Self::validate"))] #[builder(build_fn(validate = "Self::validate"))]
#[builder(setter(into, strip_option), default)] #[builder(setter(into, strip_option))]
pub struct MasterPlaylist { 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.
version_tag: ExtXVersion, version_tag: ExtXVersion,
#[builder(default)]
/// Sets the [ExtXIndependentSegments] tag.
independent_segments_tag: Option<ExtXIndependentSegments>, independent_segments_tag: Option<ExtXIndependentSegments>,
#[builder(default)]
/// Sets the [ExtXStart] tag.
start_tag: Option<ExtXStart>, start_tag: Option<ExtXStart>,
/// Sets the [ExtXMedia] tag.
media_tags: Vec<ExtXMedia>, media_tags: Vec<ExtXMedia>,
/// Sets all [ExtXStreamInf]s.
stream_inf_tags: Vec<ExtXStreamInf>, stream_inf_tags: Vec<ExtXStreamInf>,
/// Sets all [ExtXIFrameStreamInf]s.
i_frame_stream_inf_tags: Vec<ExtXIFrameStreamInf>, i_frame_stream_inf_tags: Vec<ExtXIFrameStreamInf>,
/// Sets all [ExtXSessionData]s.
session_data_tags: Vec<ExtXSessionData>, session_data_tags: Vec<ExtXSessionData>,
/// Sets all [ExtXSessionKey]s.
session_key_tags: Vec<ExtXSessionKey>, session_key_tags: Vec<ExtXSessionKey>,
} }
@ -75,30 +93,100 @@ impl MasterPlaylist {
} }
impl MasterPlaylistBuilder { impl MasterPlaylistBuilder {
pub(crate) fn validate(&self) -> Result<(), String> { fn validate(&self) -> Result<(), String> {
// validate stream inf tags let required_version = self.required_version();
if let Some(stream_inf_tags) = &self.stream_inf_tags { let specified_version = self
.version_tag
.unwrap_or(required_version.into())
.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())?;
self.validate_session_data_tags()
.map_err(|e| e.to_string())?;
self.validate_session_key_tags()
.map_err(|e| e.to_string())?;
Ok(())
}
fn required_version(&self) -> ProtocolVersion {
iter::empty()
.chain(
self.independent_segments_tag
.iter()
.map(|t| t.iter().map(|t| t.requires_version()))
.flatten(),
)
.chain(
self.start_tag
.iter()
.map(|t| t.iter().map(|t| t.requires_version()))
.flatten(),
)
.chain(
self.media_tags
.iter()
.map(|t| t.iter().map(|t| t.requires_version()))
.flatten(),
)
.chain(
self.stream_inf_tags
.iter()
.map(|t| t.iter().map(|t| t.requires_version()))
.flatten(),
)
.chain(
self.i_frame_stream_inf_tags
.iter()
.map(|t| t.iter().map(|t| t.requires_version()))
.flatten(),
)
.chain(
self.session_data_tags
.iter()
.map(|t| t.iter().map(|t| t.requires_version()))
.flatten(),
)
.chain(
self.session_key_tags
.iter()
.map(|t| t.iter().map(|t| t.requires_version()))
.flatten(),
)
.max()
.unwrap_or(ProtocolVersion::V7)
}
fn validate_stream_inf_tags(&self) -> crate::Result<()> {
if let Some(value) = &self.stream_inf_tags {
let mut has_none_closed_captions = false; let mut has_none_closed_captions = false;
for t in stream_inf_tags {
for t in value {
if let Some(group_id) = t.audio() { if let Some(group_id) = t.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).to_string()); return Err(Error::unmatched_group(group_id));
} }
} }
if let Some(group_id) = t.video() { if let Some(group_id) = t.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).to_string()); return Err(Error::unmatched_group(group_id));
} }
} }
if let Some(group_id) = t.subtitles() { if let Some(group_id) = t.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).to_string()); return Err(Error::unmatched_group(group_id));
} }
} }
match t.closed_captions() { match t.closed_captions() {
Some(&ClosedCaptions::GroupId(ref group_id)) => { Some(&ClosedCaptions::GroupId(ref group_id)) => {
if !self.check_media_group(MediaType::ClosedCaptions, group_id) { if !self.check_media_group(MediaType::ClosedCaptions, group_id) {
return Err(Error::unmatched_group(group_id).to_string()); return Err(Error::unmatched_group(group_id));
} }
} }
Some(&ClosedCaptions::None) => { Some(&ClosedCaptions::None) => {
@ -108,53 +196,57 @@ impl MasterPlaylistBuilder {
} }
} }
if has_none_closed_captions { if has_none_closed_captions {
if !stream_inf_tags if !value
.iter() .iter()
.all(|t| t.closed_captions() == Some(&ClosedCaptions::None)) .all(|t| t.closed_captions() == Some(&ClosedCaptions::None))
{ {
return Err(Error::invalid_input().to_string()); return Err(Error::invalid_input());
} }
} }
} }
Ok(())
}
// validate i_frame_stream_inf_tags fn validate_i_frame_stream_inf_tags(&self) -> crate::Result<()> {
if let Some(i_frame_stream_inf_tags) = &self.i_frame_stream_inf_tags { if let Some(value) = &self.i_frame_stream_inf_tags {
for t in i_frame_stream_inf_tags { for t in value {
if let Some(group_id) = t.video() { if let Some(group_id) = t.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).to_string()); return Err(Error::unmatched_group(group_id));
} }
} }
} }
} }
Ok(())
}
// validate session_data_tags fn validate_session_data_tags(&self) -> crate::Result<()> {
if let Some(session_data_tags) = &self.session_data_tags {
let mut set = HashSet::new(); let mut set = HashSet::new();
if let Some(value) = &self.session_data_tags {
for t in session_data_tags { for t in value {
if !set.insert((t.data_id(), t.language())) { if !set.insert((t.data_id(), t.language())) {
return Err(Error::custom(format!("Conflict: {}", t)).to_string()); return Err(Error::custom(format!("Conflict: {}", t)));
} }
} }
} }
Ok(())
}
// validate session_key_tags fn validate_session_key_tags(&self) -> crate::Result<()> {
if let Some(session_key_tags) = &self.session_key_tags {
let mut set = HashSet::new(); let mut set = HashSet::new();
for t in session_key_tags { if let Some(value) = &self.session_key_tags {
for t in value {
if !set.insert(t.key()) { if !set.insert(t.key()) {
return Err(Error::custom(format!("Conflict: {}", t)).to_string()); return Err(Error::custom(format!("Conflict: {}", t)));
} }
} }
} }
Ok(()) Ok(())
} }
fn check_media_group<T: ToString>(&self, media_type: MediaType, group_id: T) -> bool { fn check_media_group<T: ToString>(&self, media_type: MediaType, group_id: T) -> bool {
if let Some(media_tags) = &self.media_tags { if let Some(value) = &self.media_tags {
media_tags value
.iter() .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() == &group_id.to_string())
} else { } else {
@ -220,7 +312,7 @@ impl FromStr for MasterPlaylist {
return Err(Error::invalid_input()); return Err(Error::invalid_input());
} }
Tag::ExtXVersion(t) => { Tag::ExtXVersion(t) => {
builder.version_tag(t.version()); builder.version(t.version());
} }
Tag::ExtInf(_) Tag::ExtInf(_)
| Tag::ExtXByteRange(_) | Tag::ExtXByteRange(_)
@ -235,7 +327,10 @@ impl FromStr for MasterPlaylist {
| Tag::ExtXEndList(_) | Tag::ExtXEndList(_)
| Tag::ExtXPlaylistType(_) | Tag::ExtXPlaylistType(_)
| Tag::ExtXIFramesOnly(_) => { | Tag::ExtXIFramesOnly(_) => {
return Err(Error::invalid_input()); // TODO: why? return Err(Error::custom(format!(
"This tag isn't allowed in a master playlist: {}",
tag
)));
} }
Tag::ExtXMedia(t) => { Tag::ExtXMedia(t) => {
media_tags.push(t); media_tags.push(t);
@ -258,7 +353,7 @@ impl FromStr for MasterPlaylist {
Tag::ExtXStart(t) => { Tag::ExtXStart(t) => {
builder.start_tag(t); builder.start_tag(t);
} }
Tag::Unknown(_) => { _ => {
// [6.3.1. General Client Responsibilities] // [6.3.1. General Client Responsibilities]
// > ignore any unrecognized tags. // > ignore any unrecognized tags.
// TODO: collect custom tags // TODO: collect custom tags

View file

@ -3,123 +3,96 @@ use std::iter;
use std::str::FromStr; use std::str::FromStr;
use std::time::Duration; use std::time::Duration;
use derive_builder::Builder;
use crate::line::{Line, Lines, Tag}; use crate::line::{Line, Lines, Tag};
use crate::media_segment::{MediaSegment, MediaSegmentBuilder}; use crate::media_segment::{MediaSegment, MediaSegmentBuilder};
use crate::tags::{ use crate::tags::{
ExtM3u, ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly, ExtXIndependentSegments, ExtM3u, ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly, ExtXIndependentSegments,
ExtXMediaSequence, ExtXPlaylistType, ExtXStart, ExtXTargetDuration, ExtXVersion, ExtXMediaSequence, ExtXPlaylistType, ExtXStart, ExtXTargetDuration, ExtXVersion,
MediaPlaylistTag,
}; };
use crate::types::ProtocolVersion; use crate::types::ProtocolVersion;
use crate::Error; use crate::Error;
/// Media playlist builder. /// Media playlist.
#[derive(Debug, Clone)] #[derive(Debug, Clone, Builder)]
pub struct MediaPlaylistBuilder { #[builder(build_fn(validate = "Self::validate"))]
version: Option<ProtocolVersion>, #[builder(setter(into, strip_option))]
target_duration_tag: Option<ExtXTargetDuration>, pub struct MediaPlaylist {
/// 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(setter(name = "version"))]
version_tag: ExtXVersion,
/// Sets the [ExtXTargetDuration] tag.
target_duration_tag: ExtXTargetDuration,
#[builder(default)]
/// Sets the [ExtXMediaSequence] tag.
media_sequence_tag: Option<ExtXMediaSequence>, media_sequence_tag: Option<ExtXMediaSequence>,
#[builder(default)]
/// Sets the [ExtXDiscontinuitySequence] tag.
discontinuity_sequence_tag: Option<ExtXDiscontinuitySequence>, discontinuity_sequence_tag: Option<ExtXDiscontinuitySequence>,
#[builder(default)]
/// Sets the [ExtXPlaylistType] tag.
playlist_type_tag: Option<ExtXPlaylistType>, playlist_type_tag: Option<ExtXPlaylistType>,
#[builder(default)]
/// Sets the [ExtXIFramesOnly] tag.
i_frames_only_tag: Option<ExtXIFramesOnly>, i_frames_only_tag: Option<ExtXIFramesOnly>,
#[builder(default)]
/// Sets the [ExtXIndependentSegments] tag.
independent_segments_tag: Option<ExtXIndependentSegments>, independent_segments_tag: Option<ExtXIndependentSegments>,
#[builder(default)]
/// Sets the [ExtXStart] tag.
start_tag: Option<ExtXStart>, start_tag: Option<ExtXStart>,
#[builder(default)]
/// Sets the [ExtXEndList] tag.
end_list_tag: Option<ExtXEndList>, end_list_tag: Option<ExtXEndList>,
/// Sets all [MediaSegment]s.
segments: Vec<MediaSegment>, segments: Vec<MediaSegment>,
options: MediaPlaylistOptions, /// Sets the allowable excess duration of each media segment in the associated playlist.
///
/// # Error
/// If there is a media segment of which duration exceeds
/// `#EXT-X-TARGETDURATION + allowable_excess_duration`,
/// the invocation of `MediaPlaylistBuilder::build()` method will fail.
///
/// The default value is `Duration::from_secs(0)`.
#[builder(default = "Duration::from_secs(0)")]
allowable_excess_duration: Duration,
} }
impl MediaPlaylistBuilder { impl MediaPlaylistBuilder {
/// Makes a new `MediaPlaylistBuilder` instance. fn validate(&self) -> Result<(), String> {
pub fn new() -> Self {
MediaPlaylistBuilder {
version: None,
target_duration_tag: None,
media_sequence_tag: None,
discontinuity_sequence_tag: None,
playlist_type_tag: None,
i_frames_only_tag: None,
independent_segments_tag: None,
start_tag: None,
end_list_tag: None,
segments: Vec::new(),
options: MediaPlaylistOptions::new(),
}
}
/// Sets the protocol compatibility version of the resulting playlist.
///
/// If the resulting playlist has tags which requires a compatibility version greater than `version`,
/// `finish()` method will fail with an `ErrorKind::InvalidInput` error.
///
/// The default is the maximum version among the tags in the playlist.
pub fn version(&mut self, version: ProtocolVersion) -> &mut Self {
self.version = Some(version);
self
}
/// Sets the given tag to the resulting playlist.
pub fn tag<T: Into<MediaPlaylistTag>>(&mut self, tag: T) -> &mut Self {
match tag.into() {
MediaPlaylistTag::ExtXTargetDuration(t) => self.target_duration_tag = Some(t),
MediaPlaylistTag::ExtXMediaSequence(t) => self.media_sequence_tag = Some(t),
MediaPlaylistTag::ExtXDiscontinuitySequence(t) => {
self.discontinuity_sequence_tag = Some(t)
}
MediaPlaylistTag::ExtXPlaylistType(t) => self.playlist_type_tag = Some(t),
MediaPlaylistTag::ExtXIFramesOnly(t) => self.i_frames_only_tag = Some(t),
MediaPlaylistTag::ExtXIndependentSegments(t) => self.independent_segments_tag = Some(t),
MediaPlaylistTag::ExtXStart(t) => self.start_tag = Some(t),
MediaPlaylistTag::ExtXEndList(t) => self.end_list_tag = Some(t),
}
self
}
/// Adds a media segment to the resulting playlist.
pub fn segment(&mut self, segment: MediaSegment) -> &mut Self {
self.segments.push(segment);
self
}
/// Sets the options that will be associated to the resulting playlist.
///
/// The default value is `MediaPlaylistOptions::default()`.
pub fn options(&mut self, options: MediaPlaylistOptions) -> &mut Self {
self.options = options;
self
}
/// Builds a `MediaPlaylist` instance.
pub fn finish(self) -> crate::Result<MediaPlaylist> {
let required_version = self.required_version(); let required_version = self.required_version();
let specified_version = self.version.unwrap_or(required_version); let specified_version = self
if !(required_version <= specified_version) { .version_tag
.unwrap_or(required_version.into())
.version();
if required_version > specified_version {
return Err(Error::custom(format!( return Err(Error::custom(format!(
"required_version:{}, specified_version:{}", "required_version: {}, specified_version: {}",
required_version, specified_version required_version, specified_version
))); ))
.to_string());
} }
let target_duration_tag = self.target_duration_tag.ok_or(Error::invalid_input())?; if let Some(target_duration) = &self.target_duration_tag {
self.validate_media_segments(target_duration_tag.duration())?; self.validate_media_segments(target_duration.duration())
.map_err(|e| e.to_string())?;
}
Ok(MediaPlaylist { Ok(())
version_tag: ExtXVersion::new(specified_version),
target_duration_tag,
media_sequence_tag: self.media_sequence_tag,
discontinuity_sequence_tag: self.discontinuity_sequence_tag,
playlist_type_tag: self.playlist_type_tag,
i_frames_only_tag: self.i_frames_only_tag,
independent_segments_tag: self.independent_segments_tag,
start_tag: self.start_tag,
end_list_tag: self.end_list_tag,
segments: self.segments,
})
} }
fn validate_media_segments(&self, target_duration: Duration) -> crate::Result<()> { fn validate_media_segments(&self, target_duration: Duration) -> crate::Result<()> {
let mut last_range_uri = None; let mut last_range_uri = None;
for s in &self.segments { if let Some(segments) = &self.segments {
for s in segments {
// CHECK: `#EXT-X-TARGETDURATION` // CHECK: `#EXT-X-TARGETDURATION`
let segment_duration = s.inf_tag().duration(); let segment_duration = s.inf_tag().duration();
let rounded_segment_duration = if segment_duration.subsec_nanos() < 500_000_000 { let rounded_segment_duration = if segment_duration.subsec_nanos() < 500_000_000 {
@ -127,7 +100,14 @@ impl MediaPlaylistBuilder {
} else { } else {
Duration::from_secs(segment_duration.as_secs() + 1) Duration::from_secs(segment_duration.as_secs() + 1)
}; };
let max_segment_duration = target_duration + self.options.allowable_excess_duration;
let max_segment_duration = {
if let Some(value) = &self.allowable_excess_duration {
target_duration + *value
} else {
target_duration
}
};
if !(rounded_segment_duration <= max_segment_duration) { if !(rounded_segment_duration <= max_segment_duration) {
return Err(Error::custom(format!( return Err(Error::custom(format!(
@ -153,6 +133,7 @@ impl MediaPlaylistBuilder {
last_range_uri = None; last_range_uri = None;
} }
} }
}
Ok(()) Ok(())
} }
@ -163,49 +144,86 @@ impl MediaPlaylistBuilder {
.iter() .iter()
.map(|t| t.requires_version()), .map(|t| t.requires_version()),
) )
.chain(self.media_sequence_tag.iter().map(|t| t.requires_version())) .chain(self.media_sequence_tag.iter().map(|t| {
.chain( if let Some(p) = t {
self.discontinuity_sequence_tag p.requires_version()
.iter() } else {
.map(|t| t.requires_version()), ProtocolVersion::V1
) }
.chain(self.playlist_type_tag.iter().map(|t| t.requires_version())) }))
.chain(self.i_frames_only_tag.iter().map(|t| t.requires_version())) .chain(self.discontinuity_sequence_tag.iter().map(|t| {
.chain( if let Some(p) = t {
self.independent_segments_tag p.requires_version()
.iter() } else {
.map(|t| t.requires_version()), ProtocolVersion::V1
) }
.chain(self.start_tag.iter().map(|t| t.requires_version())) }))
.chain(self.end_list_tag.iter().map(|t| t.requires_version())) .chain(self.playlist_type_tag.iter().map(|t| {
.chain(self.segments.iter().map(|s| s.requires_version())) if let Some(p) = t {
p.requires_version()
} else {
ProtocolVersion::V1
}
}))
.chain(self.i_frames_only_tag.iter().map(|t| {
if let Some(p) = t {
p.requires_version()
} else {
ProtocolVersion::V1
}
}))
.chain(self.independent_segments_tag.iter().map(|t| {
if let Some(p) = t {
p.requires_version()
} else {
ProtocolVersion::V1
}
}))
.chain(self.start_tag.iter().map(|t| {
if let Some(p) = t {
p.requires_version()
} else {
ProtocolVersion::V1
}
}))
.chain(self.end_list_tag.iter().map(|t| {
if let Some(p) = t {
p.requires_version()
} else {
ProtocolVersion::V1
}
}))
.chain(self.segments.iter().map(|t| {
t.iter()
.map(|s| s.requires_version())
.max()
.unwrap_or(ProtocolVersion::V1)
}))
.max() .max()
.unwrap_or(ProtocolVersion::V1) .unwrap_or(ProtocolVersion::V1)
} }
}
impl Default for MediaPlaylistBuilder { /// Adds a media segment to the resulting playlist.
fn default() -> Self { pub fn push_segment<VALUE: Into<MediaSegment>>(&mut self, value: VALUE) -> &mut Self {
Self::new() if let Some(segments) = &mut self.segments {
segments.push(value.into());
} else {
self.segments = Some(vec![value.into()]);
}
self
}
/// Parse the rest of the [MediaPlaylist] from an m3u8 file.
pub fn parse(&mut self, input: &str) -> crate::Result<MediaPlaylist> {
parse_media_playlist(input, self)
} }
} }
/// Media playlist.
#[derive(Debug, Clone)]
pub struct MediaPlaylist {
version_tag: ExtXVersion,
target_duration_tag: ExtXTargetDuration,
media_sequence_tag: Option<ExtXMediaSequence>,
discontinuity_sequence_tag: Option<ExtXDiscontinuitySequence>,
playlist_type_tag: Option<ExtXPlaylistType>,
i_frames_only_tag: Option<ExtXIFramesOnly>,
independent_segments_tag: Option<ExtXIndependentSegments>,
start_tag: Option<ExtXStart>,
end_list_tag: Option<ExtXEndList>,
segments: Vec<MediaSegment>,
}
impl MediaPlaylist { impl MediaPlaylist {
/// Creates a [MediaPlaylistBuilder].
pub fn builder() -> MediaPlaylistBuilder {
MediaPlaylistBuilder::default()
}
/// Returns the `EXT-X-VERSION` tag contained in the playlist. /// Returns the `EXT-X-VERSION` tag contained in the playlist.
pub const fn version_tag(&self) -> ExtXVersion { pub const fn version_tag(&self) -> ExtXVersion {
self.version_tag self.version_tag
@ -292,66 +310,28 @@ impl fmt::Display for MediaPlaylist {
} }
} }
impl FromStr for MediaPlaylist { fn parse_media_playlist(
type Err = Error; input: &str,
builder: &mut MediaPlaylistBuilder,
fn from_str(input: &str) -> Result<Self, Self::Err> { ) -> crate::Result<MediaPlaylist> {
MediaPlaylistOptions::new().parse(input)
}
}
/// Media playlist options.
#[derive(Debug, Clone)]
pub struct MediaPlaylistOptions {
allowable_excess_duration: Duration,
}
impl MediaPlaylistOptions {
/// Makes a new `MediaPlaylistOptions` with the default settings.
pub const fn new() -> Self {
MediaPlaylistOptions {
allowable_excess_duration: Duration::from_secs(0),
}
}
/// Sets the allowable excess duration of each media segment in the associated playlist.
///
/// If there is a media segment of which duration exceeds
/// `#EXT-X-TARGETDURATION + allowable_excess_duration`,
/// the invocation of `MediaPlaylistBuilder::finish()` method will fail.
///
/// The default value is `Duration::from_secs(0)`.
pub fn allowable_excess_segment_duration(
&mut self,
allowable_excess_duration: Duration,
) -> &mut Self {
self.allowable_excess_duration = allowable_excess_duration;
self
}
/// Parses the given M3U8 text with the specified settings.
pub fn parse(&self, m3u8: &str) -> crate::Result<MediaPlaylist> {
let mut builder = MediaPlaylistBuilder::new();
builder.options(self.clone());
let mut segment = MediaSegmentBuilder::new(); let mut segment = MediaSegmentBuilder::new();
let mut segments = vec![];
let mut has_partial_segment = false; let mut has_partial_segment = false;
let mut has_discontinuity_tag = false; let mut has_discontinuity_tag = false;
for (i, line) in m3u8.parse::<Lines>()?.into_iter().enumerate() {
for (i, line) in input.parse::<Lines>()?.into_iter().enumerate() {
match line { match line {
Line::Tag(tag) => { Line::Tag(tag) => {
if i == 0 { if i == 0 {
if tag != Tag::ExtM3u(ExtM3u) { if tag != Tag::ExtM3u(ExtM3u) {
return Err(Error::invalid_input()); return Err(Error::custom("m3u8 doesn't start with #EXTM3U"));
} }
continue; continue;
} }
match tag { match tag {
Tag::ExtM3u(_) => return Err(Error::invalid_input()), Tag::ExtM3u(_) => return Err(Error::invalid_input()),
Tag::ExtXVersion(t) => { Tag::ExtXVersion(t) => {
if builder.version.is_some() {
return Err(Error::invalid_input());
}
builder.version(t.version()); builder.version(t.version());
} }
Tag::ExtInf(t) => { Tag::ExtInf(t) => {
@ -384,31 +364,28 @@ impl MediaPlaylistOptions {
segment.tag(t); segment.tag(t);
} }
Tag::ExtXTargetDuration(t) => { Tag::ExtXTargetDuration(t) => {
builder.tag(t); builder.target_duration_tag(t);
} }
Tag::ExtXMediaSequence(t) => { Tag::ExtXMediaSequence(t) => {
if builder.segments.is_empty() { builder.media_sequence_tag(t);
return Err(Error::invalid_input());
}
builder.tag(t);
} }
Tag::ExtXDiscontinuitySequence(t) => { Tag::ExtXDiscontinuitySequence(t) => {
if builder.segments.is_empty() { if segments.is_empty() {
return Err(Error::invalid_input()); return Err(Error::invalid_input());
} }
if has_discontinuity_tag { if has_discontinuity_tag {
return Err(Error::invalid_input()); return Err(Error::invalid_input());
} }
builder.tag(t); builder.discontinuity_sequence_tag(t);
} }
Tag::ExtXEndList(t) => { Tag::ExtXEndList(t) => {
builder.tag(t); builder.end_list_tag(t);
} }
Tag::ExtXPlaylistType(t) => { Tag::ExtXPlaylistType(t) => {
builder.tag(t); builder.playlist_type_tag(t);
} }
Tag::ExtXIFramesOnly(t) => { Tag::ExtXIFramesOnly(t) => {
builder.tag(t); builder.i_frames_only_tag(t);
} }
Tag::ExtXMedia(_) Tag::ExtXMedia(_)
| Tag::ExtXStreamInf(_) | Tag::ExtXStreamInf(_)
@ -418,10 +395,10 @@ impl MediaPlaylistOptions {
return Err(Error::custom(tag)); return Err(Error::custom(tag));
} }
Tag::ExtXIndependentSegments(t) => { Tag::ExtXIndependentSegments(t) => {
builder.tag(t); builder.independent_segments_tag(t);
} }
Tag::ExtXStart(t) => { Tag::ExtXStart(t) => {
builder.tag(t); builder.start_tag(t);
} }
Tag::Unknown(_) => { Tag::Unknown(_) => {
// [6.3.1. General Client Responsibilities] // [6.3.1. General Client Responsibilities]
@ -431,7 +408,7 @@ impl MediaPlaylistOptions {
} }
Line::Uri(uri) => { Line::Uri(uri) => {
segment.uri(uri); segment.uri(uri);
builder.segment((segment.finish())?); segments.push(segment.finish()?);
segment = MediaSegmentBuilder::new(); segment = MediaSegmentBuilder::new();
has_partial_segment = false; has_partial_segment = false;
} }
@ -440,13 +417,16 @@ impl MediaPlaylistOptions {
if has_partial_segment { if has_partial_segment {
return Err(Error::invalid_input()); return Err(Error::invalid_input());
} }
builder.finish()
} builder.segments(segments);
builder.build().map_err(Error::builder_error)
} }
impl Default for MediaPlaylistOptions { impl FromStr for MediaPlaylist {
fn default() -> Self { type Err = Error;
Self::new()
fn from_str(input: &str) -> Result<Self, Self::Err> {
parse_media_playlist(input, &mut Self::builder())
} }
} }
@ -471,20 +451,20 @@ mod tests {
assert!(m3u8.parse::<MediaPlaylist>().is_err()); assert!(m3u8.parse::<MediaPlaylist>().is_err());
// Error (allowable segment duration = 9) // Error (allowable segment duration = 9)
assert!(MediaPlaylistOptions::new() assert!(MediaPlaylist::builder()
.allowable_excess_segment_duration(Duration::from_secs(1)) .allowable_excess_duration(Duration::from_secs(1))
.parse(m3u8) .parse(m3u8)
.is_err()); .is_err());
// Ok (allowable segment duration = 10) // Ok (allowable segment duration = 10)
assert!(MediaPlaylistOptions::new() MediaPlaylist::builder()
.allowable_excess_segment_duration(Duration::from_secs(2)) .allowable_excess_duration(Duration::from_secs(2))
.parse(m3u8) .parse(m3u8)
.is_ok()); .unwrap();
} }
#[test] #[test]
fn empty_m3u8_parse_test() { fn test_parser() {
let m3u8 = ""; let m3u8 = "";
assert!(m3u8.parse::<MediaPlaylist>().is_err()); assert!(m3u8.parse::<MediaPlaylist>().is_err());
} }

View file

@ -31,9 +31,9 @@ impl ExtXStreamInf {
pub(crate) const PREFIX: &'static str = "#EXT-X-STREAM-INF:"; pub(crate) const PREFIX: &'static str = "#EXT-X-STREAM-INF:";
/// Makes a new `ExtXStreamInf` tag. /// Makes a new `ExtXStreamInf` tag.
pub const fn new(uri: SingleLineString, bandwidth: u64) -> Self { pub fn new<T: ToString>(uri: T, bandwidth: u64) -> Self {
ExtXStreamInf { ExtXStreamInf {
uri, uri: SingleLineString::new(uri.to_string()).unwrap(),
bandwidth, bandwidth,
average_bandwidth: None, average_bandwidth: None,
codecs: None, codecs: None,
@ -209,11 +209,27 @@ mod test {
use super::*; use super::*;
#[test] #[test]
fn ext_x_stream_inf() { fn test_parser() {
let tag = ExtXStreamInf::new(SingleLineString::new("foo").unwrap(), 1000); let stream_inf = "#EXT-X-STREAM-INF:BANDWIDTH=1000\nfoo"
let text = "#EXT-X-STREAM-INF:BANDWIDTH=1000\nfoo"; .parse::<ExtXStreamInf>()
assert_eq!(text.parse().ok(), Some(tag.clone())); .unwrap();
assert_eq!(tag.to_string(), text);
assert_eq!(tag.requires_version(), ProtocolVersion::V1); assert_eq!(stream_inf, ExtXStreamInf::new("foo", 1000));
}
#[test]
fn test_requires_version() {
assert_eq!(
ProtocolVersion::V1,
ExtXStreamInf::new("foo", 1000).requires_version()
);
}
#[test]
fn test_display() {
assert_eq!(
ExtXStreamInf::new("foo", 1000).to_string(),
"#EXT-X-STREAM-INF:BANDWIDTH=1000\nfoo".to_string()
);
} }
} }

View file

@ -9,7 +9,7 @@ use crate::Error;
/// [4.3.3.1. EXT-X-TARGETDURATION] /// [4.3.3.1. EXT-X-TARGETDURATION]
/// ///
/// [4.3.3.1. EXT-X-TARGETDURATION]: https://tools.ietf.org/html/rfc8216#section-4.3.3.1 /// [4.3.3.1. EXT-X-TARGETDURATION]: https://tools.ietf.org/html/rfc8216#section-4.3.3.1
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct ExtXTargetDuration { pub struct ExtXTargetDuration {
duration: Duration, duration: Duration,
} }