2020-02-16 11:51:49 +00:00
|
|
|
use std::collections::HashSet;
|
2019-09-13 14:06:52 +00:00
|
|
|
use std::fmt;
|
|
|
|
use std::str::FromStr;
|
|
|
|
use std::time::Duration;
|
|
|
|
|
2019-09-14 19:08:35 +00:00
|
|
|
use derive_builder::Builder;
|
2020-02-02 12:38:11 +00:00
|
|
|
use shorthand::ShortHand;
|
2019-09-14 19:08:35 +00:00
|
|
|
|
2019-03-31 09:58:11 +00:00
|
|
|
use crate::line::{Line, Lines, Tag};
|
2019-09-14 19:21:44 +00:00
|
|
|
use crate::media_segment::MediaSegment;
|
2019-03-31 09:58:11 +00:00
|
|
|
use crate::tags::{
|
2018-10-04 11:18:56 +00:00
|
|
|
ExtM3u, ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly, ExtXIndependentSegments,
|
|
|
|
ExtXMediaSequence, ExtXPlaylistType, ExtXStart, ExtXTargetDuration, ExtXVersion,
|
|
|
|
};
|
2019-10-04 09:02:21 +00:00
|
|
|
use crate::types::ProtocolVersion;
|
2020-02-06 11:27:48 +00:00
|
|
|
use crate::utils::tag;
|
2019-10-04 09:02:21 +00:00
|
|
|
use crate::{Encrypted, Error, RequiredVersion};
|
2018-02-13 15:25:33 +00:00
|
|
|
|
2019-09-14 19:08:35 +00:00
|
|
|
/// Media playlist.
|
2020-02-02 12:38:11 +00:00
|
|
|
#[derive(ShortHand, Debug, Clone, Builder, PartialEq, PartialOrd)]
|
2019-09-14 19:08:35 +00:00
|
|
|
#[builder(build_fn(validate = "Self::validate"))]
|
|
|
|
#[builder(setter(into, strip_option))]
|
2020-02-02 12:38:11 +00:00
|
|
|
#[shorthand(enable(must_use, collection_magic, get_mut))]
|
2019-09-14 19:08:35 +00:00
|
|
|
pub struct MediaPlaylist {
|
2020-02-02 12:38:11 +00:00
|
|
|
/// The [`ExtXTargetDuration`] tag of the playlist.
|
|
|
|
///
|
|
|
|
/// # Note
|
|
|
|
///
|
|
|
|
/// This field is required.
|
|
|
|
#[shorthand(enable(copy))]
|
2020-02-10 12:20:39 +00:00
|
|
|
target_duration: ExtXTargetDuration,
|
2019-10-04 09:02:21 +00:00
|
|
|
/// Sets the [`ExtXMediaSequence`] tag.
|
2020-02-02 12:38:11 +00:00
|
|
|
///
|
|
|
|
/// # Note
|
|
|
|
///
|
|
|
|
/// This field is optional.
|
2019-09-14 19:08:35 +00:00
|
|
|
#[builder(default)]
|
2020-02-10 12:20:39 +00:00
|
|
|
media_sequence: Option<ExtXMediaSequence>,
|
2019-10-04 09:02:21 +00:00
|
|
|
/// Sets the [`ExtXDiscontinuitySequence`] tag.
|
2020-02-02 12:38:11 +00:00
|
|
|
///
|
|
|
|
/// # Note
|
|
|
|
///
|
|
|
|
/// This field is optional.
|
2019-09-14 19:08:35 +00:00
|
|
|
#[builder(default)]
|
2020-02-10 12:20:39 +00:00
|
|
|
discontinuity_sequence: Option<ExtXDiscontinuitySequence>,
|
2019-10-04 09:02:21 +00:00
|
|
|
/// Sets the [`ExtXPlaylistType`] tag.
|
2020-02-02 12:38:11 +00:00
|
|
|
///
|
|
|
|
/// # Note
|
|
|
|
///
|
|
|
|
/// This field is optional.
|
2019-09-14 19:08:35 +00:00
|
|
|
#[builder(default)]
|
2020-02-10 12:20:39 +00:00
|
|
|
playlist_type: Option<ExtXPlaylistType>,
|
2019-10-04 09:02:21 +00:00
|
|
|
/// Sets the [`ExtXIFramesOnly`] tag.
|
2020-02-02 12:38:11 +00:00
|
|
|
///
|
|
|
|
/// # Note
|
|
|
|
///
|
|
|
|
/// This field is optional.
|
2019-09-14 19:08:35 +00:00
|
|
|
#[builder(default)]
|
2020-02-10 12:20:39 +00:00
|
|
|
i_frames_only: Option<ExtXIFramesOnly>,
|
2019-10-04 09:02:21 +00:00
|
|
|
/// Sets the [`ExtXIndependentSegments`] tag.
|
2020-02-02 12:38:11 +00:00
|
|
|
///
|
|
|
|
/// # Note
|
|
|
|
///
|
|
|
|
/// This field is optional.
|
2019-09-14 19:08:35 +00:00
|
|
|
#[builder(default)]
|
2020-02-10 12:20:39 +00:00
|
|
|
independent_segments: Option<ExtXIndependentSegments>,
|
2019-10-04 09:02:21 +00:00
|
|
|
/// Sets the [`ExtXStart`] tag.
|
2020-02-02 12:38:11 +00:00
|
|
|
///
|
|
|
|
/// # Note
|
|
|
|
///
|
|
|
|
/// This field is optional.
|
2019-09-14 19:08:35 +00:00
|
|
|
#[builder(default)]
|
2020-02-10 12:20:39 +00:00
|
|
|
start: Option<ExtXStart>,
|
2019-10-04 09:02:21 +00:00
|
|
|
/// Sets the [`ExtXEndList`] tag.
|
2020-02-02 12:38:11 +00:00
|
|
|
///
|
|
|
|
/// # Note
|
|
|
|
///
|
|
|
|
/// This field is optional.
|
|
|
|
#[builder(default)]
|
2020-02-10 12:20:39 +00:00
|
|
|
end_list: Option<ExtXEndList>,
|
2020-02-02 12:38:11 +00:00
|
|
|
/// A list of all [`MediaSegment`]s.
|
|
|
|
///
|
|
|
|
/// # Note
|
|
|
|
///
|
|
|
|
/// This field is required.
|
2018-02-14 19:18:02 +00:00
|
|
|
segments: Vec<MediaSegment>,
|
2020-02-02 12:38:11 +00:00
|
|
|
/// The allowable excess duration of each media segment in the
|
2019-10-03 15:01:15 +00:00
|
|
|
/// associated playlist.
|
2018-02-14 19:18:02 +00:00
|
|
|
///
|
2019-09-14 19:08:35 +00:00
|
|
|
/// # Error
|
2020-02-02 12:38:11 +00:00
|
|
|
///
|
2019-09-14 19:08:35 +00:00
|
|
|
/// If there is a media segment of which duration exceeds
|
|
|
|
/// `#EXT-X-TARGETDURATION + allowable_excess_duration`,
|
|
|
|
/// the invocation of `MediaPlaylistBuilder::build()` method will fail.
|
2018-10-04 13:59:23 +00:00
|
|
|
///
|
2020-02-02 12:38:11 +00:00
|
|
|
///
|
|
|
|
/// # Note
|
|
|
|
///
|
|
|
|
/// This field is optional and the default value is
|
|
|
|
/// `Duration::from_secs(0)`.
|
2019-09-14 19:08:35 +00:00
|
|
|
#[builder(default = "Duration::from_secs(0)")]
|
|
|
|
allowable_excess_duration: Duration,
|
2020-02-02 12:50:56 +00:00
|
|
|
/// A list of unknown tags.
|
|
|
|
///
|
|
|
|
/// # Note
|
|
|
|
///
|
|
|
|
/// This field is optional.
|
|
|
|
#[builder(default)]
|
|
|
|
unknown_tags: Vec<String>,
|
2019-09-14 19:08:35 +00:00
|
|
|
}
|
2018-10-04 13:59:23 +00:00
|
|
|
|
2019-09-14 19:08:35 +00:00
|
|
|
impl MediaPlaylistBuilder {
|
|
|
|
fn validate(&self) -> Result<(), String> {
|
2020-02-10 12:20:39 +00:00
|
|
|
if let Some(target_duration) = &self.target_duration {
|
2019-09-14 19:08:35 +00:00
|
|
|
self.validate_media_segments(target_duration.duration())
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
2018-02-14 19:18:02 +00:00
|
|
|
}
|
|
|
|
|
2019-09-13 14:06:52 +00:00
|
|
|
fn validate_media_segments(&self, target_duration: Duration) -> crate::Result<()> {
|
2018-02-14 19:18:02 +00:00
|
|
|
let mut last_range_uri = None;
|
2020-02-02 12:38:11 +00:00
|
|
|
|
2019-09-14 19:08:35 +00:00
|
|
|
if let Some(segments) = &self.segments {
|
|
|
|
for s in segments {
|
|
|
|
// CHECK: `#EXT-X-TARGETDURATION`
|
2020-02-16 11:50:52 +00:00
|
|
|
let segment_duration = s.inf().duration();
|
2019-10-05 14:24:48 +00:00
|
|
|
let rounded_segment_duration = {
|
|
|
|
if segment_duration.subsec_nanos() < 500_000_000 {
|
|
|
|
Duration::from_secs(segment_duration.as_secs())
|
|
|
|
} else {
|
|
|
|
Duration::from_secs(segment_duration.as_secs() + 1)
|
|
|
|
}
|
2019-09-14 19:08:35 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
let max_segment_duration = {
|
|
|
|
if let Some(value) = &self.allowable_excess_duration {
|
|
|
|
target_duration + *value
|
|
|
|
} else {
|
|
|
|
target_duration
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2019-09-22 18:33:40 +00:00
|
|
|
if rounded_segment_duration > max_segment_duration {
|
2019-09-14 19:08:35 +00:00
|
|
|
return Err(Error::custom(format!(
|
|
|
|
"Too large segment duration: actual={:?}, max={:?}, target_duration={:?}, uri={:?}",
|
|
|
|
segment_duration,
|
|
|
|
max_segment_duration,
|
|
|
|
target_duration,
|
|
|
|
s.uri()
|
|
|
|
)));
|
|
|
|
}
|
2018-02-14 19:18:02 +00:00
|
|
|
|
2019-09-14 19:08:35 +00:00
|
|
|
// CHECK: `#EXT-X-BYTE-RANGE`
|
2020-02-23 17:56:41 +00:00
|
|
|
if let Some(range) = s.byte_range() {
|
|
|
|
if range.start().is_none() {
|
2019-09-22 18:33:40 +00:00
|
|
|
let last_uri = last_range_uri.ok_or_else(Error::invalid_input)?;
|
2019-09-14 19:08:35 +00:00
|
|
|
if last_uri != s.uri() {
|
|
|
|
return Err(Error::invalid_input());
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
last_range_uri = Some(s.uri());
|
2019-09-13 14:06:52 +00:00
|
|
|
}
|
2018-02-14 19:18:02 +00:00
|
|
|
} else {
|
2019-09-14 19:08:35 +00:00
|
|
|
last_range_uri = None;
|
2018-02-14 19:18:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-02-02 12:38:11 +00:00
|
|
|
|
2018-02-14 19:18:02 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2019-10-04 09:02:21 +00:00
|
|
|
/// Adds a media segment to the resulting playlist.
|
|
|
|
pub fn push_segment<VALUE: Into<MediaSegment>>(&mut self, value: VALUE) -> &mut Self {
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl RequiredVersion for MediaPlaylistBuilder {
|
2018-02-14 19:18:02 +00:00
|
|
|
fn required_version(&self) -> ProtocolVersion {
|
2019-10-05 14:24:48 +00:00
|
|
|
required_version![
|
2020-02-10 12:20:39 +00:00
|
|
|
self.target_duration,
|
|
|
|
self.media_sequence,
|
|
|
|
self.discontinuity_sequence,
|
|
|
|
self.playlist_type,
|
|
|
|
self.i_frames_only,
|
|
|
|
self.independent_segments,
|
|
|
|
self.start,
|
|
|
|
self.end_list,
|
2019-10-05 14:24:48 +00:00
|
|
|
self.segments
|
|
|
|
]
|
2018-02-14 19:18:02 +00:00
|
|
|
}
|
|
|
|
}
|
2019-09-08 10:23:33 +00:00
|
|
|
|
2018-02-14 19:18:02 +00:00
|
|
|
impl MediaPlaylist {
|
2019-10-04 09:02:21 +00:00
|
|
|
/// Returns a builder for [`MediaPlaylist`].
|
2019-10-03 15:01:15 +00:00
|
|
|
pub fn builder() -> MediaPlaylistBuilder { MediaPlaylistBuilder::default() }
|
2018-02-13 15:25:33 +00:00
|
|
|
}
|
2019-09-08 10:23:33 +00:00
|
|
|
|
2019-10-05 14:24:48 +00:00
|
|
|
impl RequiredVersion for MediaPlaylist {
|
|
|
|
fn required_version(&self) -> ProtocolVersion {
|
|
|
|
required_version![
|
2020-02-10 12:20:39 +00:00
|
|
|
self.target_duration,
|
|
|
|
self.media_sequence,
|
|
|
|
self.discontinuity_sequence,
|
|
|
|
self.playlist_type,
|
|
|
|
self.i_frames_only,
|
|
|
|
self.independent_segments,
|
|
|
|
self.start,
|
|
|
|
self.end_list,
|
2019-10-05 14:24:48 +00:00
|
|
|
self.segments
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-13 15:25:33 +00:00
|
|
|
impl fmt::Display for MediaPlaylist {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
|
|
writeln!(f, "{}", ExtM3u)?;
|
2020-02-02 12:38:11 +00:00
|
|
|
|
2019-10-05 14:24:48 +00:00
|
|
|
if self.required_version() != ProtocolVersion::V1 {
|
|
|
|
writeln!(f, "{}", ExtXVersion::new(self.required_version()))?;
|
2018-02-13 15:25:33 +00:00
|
|
|
}
|
2020-02-02 12:38:11 +00:00
|
|
|
|
2020-02-10 12:20:39 +00:00
|
|
|
writeln!(f, "{}", self.target_duration)?;
|
2020-02-02 12:38:11 +00:00
|
|
|
|
2020-02-10 12:20:39 +00:00
|
|
|
if let Some(value) = &self.media_sequence {
|
2019-09-14 19:42:06 +00:00
|
|
|
writeln!(f, "{}", value)?;
|
2018-02-13 15:25:33 +00:00
|
|
|
}
|
2020-02-02 12:38:11 +00:00
|
|
|
|
2020-02-10 12:20:39 +00:00
|
|
|
if let Some(value) = &self.discontinuity_sequence {
|
2019-09-14 19:42:06 +00:00
|
|
|
writeln!(f, "{}", value)?;
|
2018-02-13 15:25:33 +00:00
|
|
|
}
|
2020-02-02 12:38:11 +00:00
|
|
|
|
2020-02-10 12:20:39 +00:00
|
|
|
if let Some(value) = &self.playlist_type {
|
2019-09-14 19:42:06 +00:00
|
|
|
writeln!(f, "{}", value)?;
|
2018-02-13 15:25:33 +00:00
|
|
|
}
|
2020-02-02 12:38:11 +00:00
|
|
|
|
2020-02-10 12:20:39 +00:00
|
|
|
if let Some(value) = &self.i_frames_only {
|
2019-09-14 19:42:06 +00:00
|
|
|
writeln!(f, "{}", value)?;
|
2018-02-13 15:25:33 +00:00
|
|
|
}
|
2020-02-02 12:38:11 +00:00
|
|
|
|
2020-02-10 12:20:39 +00:00
|
|
|
if let Some(value) = &self.independent_segments {
|
2019-09-14 19:42:06 +00:00
|
|
|
writeln!(f, "{}", value)?;
|
2018-02-13 15:25:33 +00:00
|
|
|
}
|
2020-02-02 12:38:11 +00:00
|
|
|
|
2020-02-10 12:20:39 +00:00
|
|
|
if let Some(value) = &self.start {
|
2019-09-14 19:42:06 +00:00
|
|
|
writeln!(f, "{}", value)?;
|
2018-02-13 15:25:33 +00:00
|
|
|
}
|
2020-02-02 12:38:11 +00:00
|
|
|
|
2020-02-16 11:51:49 +00:00
|
|
|
// most likely only 1 ExtXKey will be in the HashSet:
|
|
|
|
let mut available_keys = HashSet::with_capacity(1);
|
|
|
|
|
2018-02-13 15:25:33 +00:00
|
|
|
for segment in &self.segments {
|
2020-02-16 11:51:49 +00:00
|
|
|
for key in segment.keys() {
|
|
|
|
// the key is new:
|
|
|
|
if available_keys.insert(key) {
|
|
|
|
let mut remove_key = None;
|
|
|
|
|
|
|
|
// an old key might be removed:
|
|
|
|
for k in &available_keys {
|
|
|
|
if k.key_format() == key.key_format() && &key != k {
|
2020-02-21 19:44:09 +00:00
|
|
|
remove_key = Some(*k);
|
2020-02-16 11:51:49 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(k) = remove_key {
|
|
|
|
// this should always be true:
|
|
|
|
let res = available_keys.remove(k);
|
|
|
|
debug_assert!(res);
|
|
|
|
}
|
|
|
|
|
|
|
|
writeln!(f, "{}", key)?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-20 07:01:01 +00:00
|
|
|
write!(f, "{}", segment)?;
|
2018-02-13 15:25:33 +00:00
|
|
|
}
|
2020-02-02 12:38:11 +00:00
|
|
|
|
2020-02-10 12:20:39 +00:00
|
|
|
if let Some(value) = &self.end_list {
|
2019-09-14 19:42:06 +00:00
|
|
|
writeln!(f, "{}", value)?;
|
2018-02-13 15:25:33 +00:00
|
|
|
}
|
2020-02-02 12:38:11 +00:00
|
|
|
|
2020-02-02 12:50:56 +00:00
|
|
|
for value in &self.unknown_tags {
|
|
|
|
writeln!(f, "{}", value)?;
|
|
|
|
}
|
|
|
|
|
2018-02-13 15:25:33 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
2019-09-08 10:23:33 +00:00
|
|
|
|
2019-09-14 19:08:35 +00:00
|
|
|
fn parse_media_playlist(
|
|
|
|
input: &str,
|
|
|
|
builder: &mut MediaPlaylistBuilder,
|
|
|
|
) -> crate::Result<MediaPlaylist> {
|
2020-02-06 11:27:48 +00:00
|
|
|
let input = tag(input, "#EXTM3U")?;
|
|
|
|
|
2019-09-14 19:21:44 +00:00
|
|
|
let mut segment = MediaSegment::builder();
|
2019-09-14 19:08:35 +00:00
|
|
|
let mut segments = vec![];
|
|
|
|
|
|
|
|
let mut has_partial_segment = false;
|
|
|
|
let mut has_discontinuity_tag = false;
|
2020-02-02 12:50:56 +00:00
|
|
|
let mut unknown_tags = vec![];
|
2019-09-14 19:08:35 +00:00
|
|
|
|
2020-02-16 11:50:52 +00:00
|
|
|
let mut available_keys: Vec<crate::tags::ExtXKey> = vec![];
|
2019-10-04 09:02:21 +00:00
|
|
|
|
2020-02-06 11:27:48 +00:00
|
|
|
for line in Lines::from(input) {
|
2020-02-02 13:33:57 +00:00
|
|
|
match line? {
|
2019-09-14 19:08:35 +00:00
|
|
|
Line::Tag(tag) => {
|
|
|
|
match tag {
|
|
|
|
Tag::ExtInf(t) => {
|
|
|
|
has_partial_segment = true;
|
2020-02-16 11:50:52 +00:00
|
|
|
segment.inf(t);
|
2019-09-14 19:08:35 +00:00
|
|
|
}
|
|
|
|
Tag::ExtXByteRange(t) => {
|
|
|
|
has_partial_segment = true;
|
2020-02-16 11:50:52 +00:00
|
|
|
segment.byte_range(t);
|
2019-09-14 19:08:35 +00:00
|
|
|
}
|
|
|
|
Tag::ExtXDiscontinuity(t) => {
|
|
|
|
has_discontinuity_tag = true;
|
|
|
|
has_partial_segment = true;
|
2020-02-16 11:50:52 +00:00
|
|
|
segment.discontinuity(t);
|
2019-09-14 19:08:35 +00:00
|
|
|
}
|
2020-02-16 11:51:49 +00:00
|
|
|
Tag::ExtXKey(key) => {
|
2019-09-14 19:08:35 +00:00
|
|
|
has_partial_segment = true;
|
2020-02-16 11:51:49 +00:00
|
|
|
|
|
|
|
// An ExtXKey applies to every MediaSegment and to every Media
|
|
|
|
// Initialization Section declared by an ExtXMap tag, that appears
|
|
|
|
// between it and the next ExtXKey tag in the Playlist file with the
|
|
|
|
// same KEYFORMAT attribute (or the end of the Playlist file).
|
|
|
|
|
|
|
|
let mut is_new_key = true;
|
|
|
|
|
|
|
|
for old_key in &mut available_keys {
|
|
|
|
if old_key.key_format() == key.key_format() {
|
|
|
|
*old_key = key.clone();
|
|
|
|
is_new_key = false;
|
|
|
|
// there are no keys with the same key_format in available_keys
|
|
|
|
// so the loop can stop here:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if is_new_key {
|
|
|
|
available_keys.push(key);
|
2019-10-04 09:02:21 +00:00
|
|
|
}
|
2019-09-14 19:08:35 +00:00
|
|
|
}
|
2019-10-04 09:02:21 +00:00
|
|
|
Tag::ExtXMap(mut t) => {
|
2019-09-14 19:08:35 +00:00
|
|
|
has_partial_segment = true;
|
2019-10-04 09:02:21 +00:00
|
|
|
|
2020-02-16 11:50:52 +00:00
|
|
|
t.set_keys(available_keys.clone());
|
|
|
|
segment.map(t);
|
2019-09-14 19:08:35 +00:00
|
|
|
}
|
|
|
|
Tag::ExtXProgramDateTime(t) => {
|
|
|
|
has_partial_segment = true;
|
2020-02-16 11:50:52 +00:00
|
|
|
segment.program_date_time(t);
|
2019-09-14 19:08:35 +00:00
|
|
|
}
|
|
|
|
Tag::ExtXDateRange(t) => {
|
|
|
|
has_partial_segment = true;
|
2020-02-16 11:50:52 +00:00
|
|
|
segment.date_range(t);
|
2019-09-14 19:08:35 +00:00
|
|
|
}
|
|
|
|
Tag::ExtXTargetDuration(t) => {
|
2020-02-10 12:20:39 +00:00
|
|
|
builder.target_duration(t);
|
2019-09-14 19:08:35 +00:00
|
|
|
}
|
|
|
|
Tag::ExtXMediaSequence(t) => {
|
2020-02-10 12:20:39 +00:00
|
|
|
builder.media_sequence(t);
|
2019-09-14 19:08:35 +00:00
|
|
|
}
|
|
|
|
Tag::ExtXDiscontinuitySequence(t) => {
|
|
|
|
if segments.is_empty() {
|
|
|
|
return Err(Error::invalid_input());
|
2018-02-13 15:25:33 +00:00
|
|
|
}
|
2020-02-16 11:50:52 +00:00
|
|
|
|
2019-09-14 19:08:35 +00:00
|
|
|
if has_discontinuity_tag {
|
|
|
|
return Err(Error::invalid_input());
|
2018-02-14 15:50:57 +00:00
|
|
|
}
|
2020-02-16 11:50:52 +00:00
|
|
|
|
2020-02-10 12:20:39 +00:00
|
|
|
builder.discontinuity_sequence(t);
|
2019-09-14 19:08:35 +00:00
|
|
|
}
|
|
|
|
Tag::ExtXEndList(t) => {
|
2020-02-10 12:20:39 +00:00
|
|
|
builder.end_list(t);
|
2019-09-14 19:08:35 +00:00
|
|
|
}
|
|
|
|
Tag::ExtXPlaylistType(t) => {
|
2020-02-10 12:20:39 +00:00
|
|
|
builder.playlist_type(t);
|
2019-09-14 19:08:35 +00:00
|
|
|
}
|
|
|
|
Tag::ExtXIFramesOnly(t) => {
|
2020-02-10 12:20:39 +00:00
|
|
|
builder.i_frames_only(t);
|
2019-09-14 19:08:35 +00:00
|
|
|
}
|
|
|
|
Tag::ExtXMedia(_)
|
2020-02-10 12:20:39 +00:00
|
|
|
| Tag::VariantStream(_)
|
2019-09-14 19:08:35 +00:00
|
|
|
| Tag::ExtXSessionData(_)
|
|
|
|
| Tag::ExtXSessionKey(_) => {
|
2019-10-04 09:02:21 +00:00
|
|
|
return Err(Error::unexpected_tag(tag));
|
2019-09-14 19:08:35 +00:00
|
|
|
}
|
|
|
|
Tag::ExtXIndependentSegments(t) => {
|
2020-02-10 12:20:39 +00:00
|
|
|
builder.independent_segments(t);
|
2019-09-14 19:08:35 +00:00
|
|
|
}
|
|
|
|
Tag::ExtXStart(t) => {
|
2020-02-10 12:20:39 +00:00
|
|
|
builder.start(t);
|
2019-09-14 19:08:35 +00:00
|
|
|
}
|
2020-02-02 12:50:56 +00:00
|
|
|
Tag::ExtXVersion(_) => {}
|
|
|
|
Tag::Unknown(_) => {
|
2019-09-14 19:08:35 +00:00
|
|
|
// [6.3.1. General Client Responsibilities]
|
|
|
|
// > ignore any unrecognized tags.
|
2020-02-02 12:50:56 +00:00
|
|
|
unknown_tags.push(tag.to_string());
|
2018-02-13 15:25:33 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-09-14 19:08:35 +00:00
|
|
|
Line::Uri(uri) => {
|
|
|
|
segment.uri(uri);
|
2020-02-16 11:50:52 +00:00
|
|
|
segment.keys(available_keys.clone());
|
2020-01-23 18:13:26 +00:00
|
|
|
segments.push(segment.build().map_err(Error::builder)?);
|
2019-09-14 19:21:44 +00:00
|
|
|
segment = MediaSegment::builder();
|
2019-09-14 19:08:35 +00:00
|
|
|
has_partial_segment = false;
|
|
|
|
}
|
2020-02-10 12:21:48 +00:00
|
|
|
_ => {}
|
2018-02-13 15:25:33 +00:00
|
|
|
}
|
|
|
|
}
|
2019-10-04 09:02:21 +00:00
|
|
|
|
2019-09-14 19:08:35 +00:00
|
|
|
if has_partial_segment {
|
|
|
|
return Err(Error::invalid_input());
|
|
|
|
}
|
2019-10-04 09:02:21 +00:00
|
|
|
|
2020-02-02 12:50:56 +00:00
|
|
|
builder.unknown_tags(unknown_tags);
|
2019-09-14 19:08:35 +00:00
|
|
|
builder.segments(segments);
|
2020-01-23 18:13:26 +00:00
|
|
|
builder.build().map_err(Error::builder)
|
2018-02-13 15:25:33 +00:00
|
|
|
}
|
2019-09-08 10:23:33 +00:00
|
|
|
|
2019-09-14 19:08:35 +00:00
|
|
|
impl FromStr for MediaPlaylist {
|
|
|
|
type Err = Error;
|
|
|
|
|
|
|
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
|
|
|
parse_media_playlist(input, &mut Self::builder())
|
2018-10-04 13:59:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
2019-10-08 13:42:33 +00:00
|
|
|
use pretty_assertions::assert_eq;
|
2018-10-04 13:59:23 +00:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn too_large_segment_duration_test() {
|
2020-02-06 11:28:54 +00:00
|
|
|
let playlist = concat!(
|
|
|
|
"#EXTM3U\n",
|
|
|
|
"#EXT-X-TARGETDURATION:8\n",
|
|
|
|
"#EXT-X-VERSION:3\n",
|
|
|
|
"#EXTINF:9.009,\n",
|
|
|
|
"http://media.example.com/first.ts\n",
|
|
|
|
"#EXTINF:9.509,\n",
|
|
|
|
"http://media.example.com/second.ts\n",
|
|
|
|
"#EXTINF:3.003,\n",
|
|
|
|
"http://media.example.com/third.ts\n",
|
|
|
|
"#EXT-X-ENDLIST\n"
|
|
|
|
);
|
2018-10-04 13:59:23 +00:00
|
|
|
|
|
|
|
// Error (allowable segment duration = target duration = 8)
|
2019-09-15 08:40:45 +00:00
|
|
|
assert!(playlist.parse::<MediaPlaylist>().is_err());
|
2018-10-04 13:59:23 +00:00
|
|
|
|
|
|
|
// Error (allowable segment duration = 9)
|
2019-09-14 19:08:35 +00:00
|
|
|
assert!(MediaPlaylist::builder()
|
|
|
|
.allowable_excess_duration(Duration::from_secs(1))
|
2019-09-15 08:40:45 +00:00
|
|
|
.parse(playlist)
|
2019-03-31 09:54:21 +00:00
|
|
|
.is_err());
|
2018-10-04 13:59:23 +00:00
|
|
|
|
|
|
|
// Ok (allowable segment duration = 10)
|
2019-09-14 19:08:35 +00:00
|
|
|
MediaPlaylist::builder()
|
|
|
|
.allowable_excess_duration(Duration::from_secs(2))
|
2019-09-15 08:40:45 +00:00
|
|
|
.parse(playlist)
|
2019-09-14 19:08:35 +00:00
|
|
|
.unwrap();
|
2018-10-04 13:59:23 +00:00
|
|
|
}
|
2018-10-10 15:35:24 +00:00
|
|
|
|
|
|
|
#[test]
|
2019-09-15 08:40:45 +00:00
|
|
|
fn test_empty_playlist() {
|
|
|
|
let playlist = "";
|
|
|
|
assert!(playlist.parse::<MediaPlaylist>().is_err());
|
2018-10-10 15:35:24 +00:00
|
|
|
}
|
2018-10-04 13:59:23 +00:00
|
|
|
}
|