1
0
Fork 0
mirror of https://github.com/sile/hls_m3u8.git synced 2024-06-02 13:39:27 +00:00
hls_m3u8/src/media_playlist.rs

491 lines
15 KiB
Rust
Raw Normal View History

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,
/// 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`
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`
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`].
2020-02-24 15:30:43 +00:00
#[must_use]
#[inline]
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
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;
let mut unknown_tags = vec![];
2019-09-14 19:08:35 +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;
segment.inf(t);
2019-09-14 19:08:35 +00:00
}
Tag::ExtXByteRange(t) => {
has_partial_segment = true;
segment.byte_range(t);
2019-09-14 19:08:35 +00:00
}
Tag::ExtXDiscontinuity(t) => {
has_discontinuity_tag = true;
has_partial_segment = true;
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
t.set_keys(available_keys.clone());
segment.map(t);
2019-09-14 19:08:35 +00:00
}
Tag::ExtXProgramDateTime(t) => {
has_partial_segment = true;
segment.program_date_time(t);
2019-09-14 19:08:35 +00:00
}
Tag::ExtXDateRange(t) => {
has_partial_segment = true;
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
}
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-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
}
Tag::ExtXVersion(_) => {}
Tag::Unknown(_) => {
2019-09-14 19:08:35 +00:00
// [6.3.1. General Client Responsibilities]
// > ignore any unrecognized tags.
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);
segment.keys(available_keys.clone());
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
builder.unknown_tags(unknown_tags);
2019-09-14 19:08:35 +00:00
builder.segments(segments);
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::*;
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
}
#[test]
2019-09-15 08:40:45 +00:00
fn test_empty_playlist() {
let playlist = "";
assert!(playlist.parse::<MediaPlaylist>().is_err());
}
2018-10-04 13:59:23 +00:00
}