mirror of
https://github.com/sile/hls_m3u8.git
synced 2024-11-25 00:20:59 +00:00
added media_playlist builder
This commit is contained in:
parent
b1aa512679
commit
dd1a40abc9
6 changed files with 462 additions and 389 deletions
64
src/error.rs
64
src/error.rs
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 value {
|
||||||
for t in session_data_tags {
|
|
||||||
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();
|
if let Some(value) = &self.session_key_tags {
|
||||||
for t in 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
|
||||||
|
|
|
@ -3,154 +3,135 @@ 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 {
|
||||||
// CHECK: `#EXT-X-TARGETDURATION`
|
for s in segments {
|
||||||
let segment_duration = s.inf_tag().duration();
|
// CHECK: `#EXT-X-TARGETDURATION`
|
||||||
let rounded_segment_duration = if segment_duration.subsec_nanos() < 500_000_000 {
|
let segment_duration = s.inf_tag().duration();
|
||||||
Duration::from_secs(segment_duration.as_secs())
|
let rounded_segment_duration = if segment_duration.subsec_nanos() < 500_000_000 {
|
||||||
} else {
|
Duration::from_secs(segment_duration.as_secs())
|
||||||
Duration::from_secs(segment_duration.as_secs() + 1)
|
} else {
|
||||||
};
|
Duration::from_secs(segment_duration.as_secs() + 1)
|
||||||
let max_segment_duration = target_duration + self.options.allowable_excess_duration;
|
};
|
||||||
|
|
||||||
if !(rounded_segment_duration <= max_segment_duration) {
|
let max_segment_duration = {
|
||||||
return Err(Error::custom(format!(
|
if let Some(value) = &self.allowable_excess_duration {
|
||||||
"Too large segment duration: actual={:?}, max={:?}, target_duration={:?}, uri={:?}",
|
target_duration + *value
|
||||||
segment_duration,
|
} else {
|
||||||
max_segment_duration,
|
target_duration
|
||||||
target_duration,
|
}
|
||||||
s.uri()
|
};
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// CHECK: `#EXT-X-BYTE-RANGE`
|
if !(rounded_segment_duration <= max_segment_duration) {
|
||||||
if let Some(tag) = s.byte_range_tag() {
|
return Err(Error::custom(format!(
|
||||||
if tag.to_range().start().is_none() {
|
"Too large segment duration: actual={:?}, max={:?}, target_duration={:?}, uri={:?}",
|
||||||
let last_uri = last_range_uri.ok_or(Error::invalid_input())?;
|
segment_duration,
|
||||||
if last_uri != s.uri() {
|
max_segment_duration,
|
||||||
return Err(Error::invalid_input());
|
target_duration,
|
||||||
|
s.uri()
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// CHECK: `#EXT-X-BYTE-RANGE`
|
||||||
|
if let Some(tag) = s.byte_range_tag() {
|
||||||
|
if tag.to_range().start().is_none() {
|
||||||
|
let last_uri = last_range_uri.ok_or(Error::invalid_input())?;
|
||||||
|
if last_uri != s.uri() {
|
||||||
|
return Err(Error::invalid_input());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
last_range_uri = Some(s.uri());
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
last_range_uri = Some(s.uri());
|
last_range_uri = None;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
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,161 +310,123 @@ impl fmt::Display for MediaPlaylist {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_media_playlist(
|
||||||
|
input: &str,
|
||||||
|
builder: &mut MediaPlaylistBuilder,
|
||||||
|
) -> crate::Result<MediaPlaylist> {
|
||||||
|
let mut segment = MediaSegmentBuilder::new();
|
||||||
|
let mut segments = vec![];
|
||||||
|
|
||||||
|
let mut has_partial_segment = false;
|
||||||
|
let mut has_discontinuity_tag = false;
|
||||||
|
|
||||||
|
for (i, line) in input.parse::<Lines>()?.into_iter().enumerate() {
|
||||||
|
match line {
|
||||||
|
Line::Tag(tag) => {
|
||||||
|
if i == 0 {
|
||||||
|
if tag != Tag::ExtM3u(ExtM3u) {
|
||||||
|
return Err(Error::custom("m3u8 doesn't start with #EXTM3U"));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match tag {
|
||||||
|
Tag::ExtM3u(_) => return Err(Error::invalid_input()),
|
||||||
|
Tag::ExtXVersion(t) => {
|
||||||
|
builder.version(t.version());
|
||||||
|
}
|
||||||
|
Tag::ExtInf(t) => {
|
||||||
|
has_partial_segment = true;
|
||||||
|
segment.tag(t);
|
||||||
|
}
|
||||||
|
Tag::ExtXByteRange(t) => {
|
||||||
|
has_partial_segment = true;
|
||||||
|
segment.tag(t);
|
||||||
|
}
|
||||||
|
Tag::ExtXDiscontinuity(t) => {
|
||||||
|
has_discontinuity_tag = true;
|
||||||
|
has_partial_segment = true;
|
||||||
|
segment.tag(t);
|
||||||
|
}
|
||||||
|
Tag::ExtXKey(t) => {
|
||||||
|
has_partial_segment = true;
|
||||||
|
segment.tag(t);
|
||||||
|
}
|
||||||
|
Tag::ExtXMap(t) => {
|
||||||
|
has_partial_segment = true;
|
||||||
|
segment.tag(t);
|
||||||
|
}
|
||||||
|
Tag::ExtXProgramDateTime(t) => {
|
||||||
|
has_partial_segment = true;
|
||||||
|
segment.tag(t);
|
||||||
|
}
|
||||||
|
Tag::ExtXDateRange(t) => {
|
||||||
|
has_partial_segment = true;
|
||||||
|
segment.tag(t);
|
||||||
|
}
|
||||||
|
Tag::ExtXTargetDuration(t) => {
|
||||||
|
builder.target_duration_tag(t);
|
||||||
|
}
|
||||||
|
Tag::ExtXMediaSequence(t) => {
|
||||||
|
builder.media_sequence_tag(t);
|
||||||
|
}
|
||||||
|
Tag::ExtXDiscontinuitySequence(t) => {
|
||||||
|
if segments.is_empty() {
|
||||||
|
return Err(Error::invalid_input());
|
||||||
|
}
|
||||||
|
if has_discontinuity_tag {
|
||||||
|
return Err(Error::invalid_input());
|
||||||
|
}
|
||||||
|
builder.discontinuity_sequence_tag(t);
|
||||||
|
}
|
||||||
|
Tag::ExtXEndList(t) => {
|
||||||
|
builder.end_list_tag(t);
|
||||||
|
}
|
||||||
|
Tag::ExtXPlaylistType(t) => {
|
||||||
|
builder.playlist_type_tag(t);
|
||||||
|
}
|
||||||
|
Tag::ExtXIFramesOnly(t) => {
|
||||||
|
builder.i_frames_only_tag(t);
|
||||||
|
}
|
||||||
|
Tag::ExtXMedia(_)
|
||||||
|
| Tag::ExtXStreamInf(_)
|
||||||
|
| Tag::ExtXIFrameStreamInf(_)
|
||||||
|
| Tag::ExtXSessionData(_)
|
||||||
|
| Tag::ExtXSessionKey(_) => {
|
||||||
|
return Err(Error::custom(tag));
|
||||||
|
}
|
||||||
|
Tag::ExtXIndependentSegments(t) => {
|
||||||
|
builder.independent_segments_tag(t);
|
||||||
|
}
|
||||||
|
Tag::ExtXStart(t) => {
|
||||||
|
builder.start_tag(t);
|
||||||
|
}
|
||||||
|
Tag::Unknown(_) => {
|
||||||
|
// [6.3.1. General Client Responsibilities]
|
||||||
|
// > ignore any unrecognized tags.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Line::Uri(uri) => {
|
||||||
|
segment.uri(uri);
|
||||||
|
segments.push(segment.finish()?);
|
||||||
|
segment = MediaSegmentBuilder::new();
|
||||||
|
has_partial_segment = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if has_partial_segment {
|
||||||
|
return Err(Error::invalid_input());
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.segments(segments);
|
||||||
|
builder.build().map_err(Error::builder_error)
|
||||||
|
}
|
||||||
|
|
||||||
impl FromStr for MediaPlaylist {
|
impl FromStr for MediaPlaylist {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
MediaPlaylistOptions::new().parse(input)
|
parse_media_playlist(input, &mut Self::builder())
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 has_partial_segment = false;
|
|
||||||
let mut has_discontinuity_tag = false;
|
|
||||||
for (i, line) in m3u8.parse::<Lines>()?.into_iter().enumerate() {
|
|
||||||
match line {
|
|
||||||
Line::Tag(tag) => {
|
|
||||||
if i == 0 {
|
|
||||||
if tag != Tag::ExtM3u(ExtM3u) {
|
|
||||||
return Err(Error::invalid_input());
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
match tag {
|
|
||||||
Tag::ExtM3u(_) => return Err(Error::invalid_input()),
|
|
||||||
Tag::ExtXVersion(t) => {
|
|
||||||
if builder.version.is_some() {
|
|
||||||
return Err(Error::invalid_input());
|
|
||||||
}
|
|
||||||
builder.version(t.version());
|
|
||||||
}
|
|
||||||
Tag::ExtInf(t) => {
|
|
||||||
has_partial_segment = true;
|
|
||||||
segment.tag(t);
|
|
||||||
}
|
|
||||||
Tag::ExtXByteRange(t) => {
|
|
||||||
has_partial_segment = true;
|
|
||||||
segment.tag(t);
|
|
||||||
}
|
|
||||||
Tag::ExtXDiscontinuity(t) => {
|
|
||||||
has_discontinuity_tag = true;
|
|
||||||
has_partial_segment = true;
|
|
||||||
segment.tag(t);
|
|
||||||
}
|
|
||||||
Tag::ExtXKey(t) => {
|
|
||||||
has_partial_segment = true;
|
|
||||||
segment.tag(t);
|
|
||||||
}
|
|
||||||
Tag::ExtXMap(t) => {
|
|
||||||
has_partial_segment = true;
|
|
||||||
segment.tag(t);
|
|
||||||
}
|
|
||||||
Tag::ExtXProgramDateTime(t) => {
|
|
||||||
has_partial_segment = true;
|
|
||||||
segment.tag(t);
|
|
||||||
}
|
|
||||||
Tag::ExtXDateRange(t) => {
|
|
||||||
has_partial_segment = true;
|
|
||||||
segment.tag(t);
|
|
||||||
}
|
|
||||||
Tag::ExtXTargetDuration(t) => {
|
|
||||||
builder.tag(t);
|
|
||||||
}
|
|
||||||
Tag::ExtXMediaSequence(t) => {
|
|
||||||
if builder.segments.is_empty() {
|
|
||||||
return Err(Error::invalid_input());
|
|
||||||
}
|
|
||||||
builder.tag(t);
|
|
||||||
}
|
|
||||||
Tag::ExtXDiscontinuitySequence(t) => {
|
|
||||||
if builder.segments.is_empty() {
|
|
||||||
return Err(Error::invalid_input());
|
|
||||||
}
|
|
||||||
if has_discontinuity_tag {
|
|
||||||
return Err(Error::invalid_input());
|
|
||||||
}
|
|
||||||
builder.tag(t);
|
|
||||||
}
|
|
||||||
Tag::ExtXEndList(t) => {
|
|
||||||
builder.tag(t);
|
|
||||||
}
|
|
||||||
Tag::ExtXPlaylistType(t) => {
|
|
||||||
builder.tag(t);
|
|
||||||
}
|
|
||||||
Tag::ExtXIFramesOnly(t) => {
|
|
||||||
builder.tag(t);
|
|
||||||
}
|
|
||||||
Tag::ExtXMedia(_)
|
|
||||||
| Tag::ExtXStreamInf(_)
|
|
||||||
| Tag::ExtXIFrameStreamInf(_)
|
|
||||||
| Tag::ExtXSessionData(_)
|
|
||||||
| Tag::ExtXSessionKey(_) => {
|
|
||||||
return Err(Error::custom(tag));
|
|
||||||
}
|
|
||||||
Tag::ExtXIndependentSegments(t) => {
|
|
||||||
builder.tag(t);
|
|
||||||
}
|
|
||||||
Tag::ExtXStart(t) => {
|
|
||||||
builder.tag(t);
|
|
||||||
}
|
|
||||||
Tag::Unknown(_) => {
|
|
||||||
// [6.3.1. General Client Responsibilities]
|
|
||||||
// > ignore any unrecognized tags.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Line::Uri(uri) => {
|
|
||||||
segment.uri(uri);
|
|
||||||
builder.segment((segment.finish())?);
|
|
||||||
segment = MediaSegmentBuilder::new();
|
|
||||||
has_partial_segment = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if has_partial_segment {
|
|
||||||
return Err(Error::invalid_input());
|
|
||||||
}
|
|
||||||
builder.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for MediaPlaylistOptions {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue