1
0
Fork 0
mirror of https://github.com/sile/hls_m3u8.git synced 2024-05-21 01:38:07 +00:00
hls_m3u8/src/media_playlist.rs

478 lines
16 KiB
Rust
Raw Normal View History

2019-09-13 14:06:52 +00:00
use std::fmt;
use std::iter;
use std::str::FromStr;
use std::time::Duration;
2019-09-14 19:08:35 +00:00
use derive_builder::Builder;
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-09-22 08:57:28 +00:00
use crate::types::{ProtocolVersion, RequiredVersion};
2019-09-13 14:06:52 +00:00
use crate::Error;
2018-02-13 15:25:33 +00:00
2019-09-14 19:08:35 +00:00
/// Media playlist.
#[derive(Debug, Clone, Builder)]
#[builder(build_fn(validate = "Self::validate"))]
#[builder(setter(into, strip_option))]
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.
2018-02-14 19:18:02 +00:00
media_sequence_tag: Option<ExtXMediaSequence>,
2019-09-14 19:08:35 +00:00
#[builder(default)]
/// Sets the [ExtXDiscontinuitySequence] tag.
2018-02-14 19:18:02 +00:00
discontinuity_sequence_tag: Option<ExtXDiscontinuitySequence>,
2019-09-14 19:08:35 +00:00
#[builder(default)]
/// Sets the [ExtXPlaylistType] tag.
2018-02-14 19:18:02 +00:00
playlist_type_tag: Option<ExtXPlaylistType>,
2019-09-14 19:08:35 +00:00
#[builder(default)]
/// Sets the [ExtXIFramesOnly] tag.
2018-02-14 19:18:02 +00:00
i_frames_only_tag: Option<ExtXIFramesOnly>,
2019-09-14 19:08:35 +00:00
#[builder(default)]
/// Sets the [ExtXIndependentSegments] tag.
2018-02-14 19:18:02 +00:00
independent_segments_tag: Option<ExtXIndependentSegments>,
2019-09-14 19:08:35 +00:00
#[builder(default)]
/// Sets the [ExtXStart] tag.
2018-02-14 19:18:02 +00:00
start_tag: Option<ExtXStart>,
2019-09-14 19:08:35 +00:00
#[builder(default)]
/// Sets the [ExtXEndList] tag.
2018-02-14 19:18:02 +00:00
end_list_tag: Option<ExtXEndList>,
2019-09-14 19:08:35 +00:00
/// Sets all [MediaSegment]s.
2018-02-14 19:18:02 +00:00
segments: Vec<MediaSegment>,
2019-09-14 19:08:35 +00:00
/// Sets the allowable excess duration of each media segment in the associated playlist.
2018-02-14 19:18:02 +00:00
///
2019-09-14 19:08:35 +00:00
/// # 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.
2018-10-04 13:59:23 +00:00
///
2019-09-14 19:08:35 +00:00
/// The default value is `Duration::from_secs(0)`.
#[builder(default = "Duration::from_secs(0)")]
allowable_excess_duration: Duration,
}
2018-10-04 13:59:23 +00:00
2019-09-14 19:08:35 +00:00
impl MediaPlaylistBuilder {
fn validate(&self) -> Result<(), String> {
2018-02-14 19:18:02 +00:00
let required_version = self.required_version();
2019-09-14 19:08:35 +00:00
let specified_version = self
.version_tag
2019-09-22 18:33:40 +00:00
.unwrap_or_else(|| required_version.into())
2019-09-14 19:08:35 +00:00
.version();
if required_version > specified_version {
2019-09-13 14:06:52 +00:00
return Err(Error::custom(format!(
2019-09-14 19:08:35 +00:00
"required_version: {}, specified_version: {}",
2019-09-13 14:06:52 +00:00
required_version, specified_version
2019-09-14 19:08:35 +00:00
))
.to_string());
2019-09-13 14:06:52 +00:00
}
2019-09-14 19:08:35 +00:00
if let Some(target_duration) = &self.target_duration_tag {
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;
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_tag().duration();
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)
};
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(tag) = s.byte_range_tag() {
if tag.to_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
}
}
}
Ok(())
}
fn required_version(&self) -> ProtocolVersion {
iter::empty()
.chain(
self.target_duration_tag
.iter()
2019-09-22 08:57:28 +00:00
.map(|t| t.required_version()),
2019-03-31 09:54:21 +00:00
)
2019-09-14 19:08:35 +00:00
.chain(self.media_sequence_tag.iter().map(|t| {
if let Some(p) = t {
2019-09-22 08:57:28 +00:00
p.required_version()
2019-09-14 19:08:35 +00:00
} else {
ProtocolVersion::V1
}
}))
.chain(self.discontinuity_sequence_tag.iter().map(|t| {
if let Some(p) = t {
2019-09-22 08:57:28 +00:00
p.required_version()
2019-09-14 19:08:35 +00:00
} else {
ProtocolVersion::V1
}
}))
.chain(self.playlist_type_tag.iter().map(|t| {
if let Some(p) = t {
2019-09-22 08:57:28 +00:00
p.required_version()
2019-09-14 19:08:35 +00:00
} else {
ProtocolVersion::V1
}
}))
.chain(self.i_frames_only_tag.iter().map(|t| {
if let Some(p) = t {
2019-09-22 08:57:28 +00:00
p.required_version()
2019-09-14 19:08:35 +00:00
} else {
ProtocolVersion::V1
}
}))
.chain(self.independent_segments_tag.iter().map(|t| {
if let Some(p) = t {
2019-09-22 08:57:28 +00:00
p.required_version()
2019-09-14 19:08:35 +00:00
} else {
ProtocolVersion::V1
}
}))
.chain(self.start_tag.iter().map(|t| {
if let Some(p) = t {
2019-09-22 08:57:28 +00:00
p.required_version()
2019-09-14 19:08:35 +00:00
} else {
ProtocolVersion::V1
}
}))
.chain(self.end_list_tag.iter().map(|t| {
if let Some(p) = t {
2019-09-22 08:57:28 +00:00
p.required_version()
2019-09-14 19:08:35 +00:00
} else {
ProtocolVersion::V1
}
}))
.chain(self.segments.iter().map(|t| {
t.iter()
2019-09-22 08:57:28 +00:00
.map(|s| s.required_version())
2019-09-14 19:08:35 +00:00
.max()
.unwrap_or(ProtocolVersion::V1)
}))
2018-02-14 19:18:02 +00:00
.max()
2019-09-22 18:33:40 +00:00
.unwrap_or_else(ProtocolVersion::latest)
2018-02-14 19:18:02 +00:00
}
2019-09-08 10:23:33 +00:00
2019-09-14 19:08:35 +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
2018-02-14 19:18:02 +00:00
}
2019-09-14 19:08:35 +00:00
/// 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)
}
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-09-14 19:08:35 +00:00
/// Creates a [MediaPlaylistBuilder].
pub fn builder() -> MediaPlaylistBuilder {
MediaPlaylistBuilder::default()
}
2018-02-14 19:18:02 +00:00
/// Returns the `EXT-X-VERSION` tag contained in the playlist.
2019-09-08 10:23:33 +00:00
pub const fn version_tag(&self) -> ExtXVersion {
2018-02-14 19:18:02 +00:00
self.version_tag
}
2018-02-13 15:25:33 +00:00
2018-02-14 19:18:02 +00:00
/// Returns the `EXT-X-TARGETDURATION` tag contained in the playlist.
2019-09-08 10:23:33 +00:00
pub const fn target_duration_tag(&self) -> ExtXTargetDuration {
2018-02-14 19:18:02 +00:00
self.target_duration_tag
}
2018-02-13 15:25:33 +00:00
2018-02-14 19:18:02 +00:00
/// Returns the `EXT-X-MEDIA-SEQUENCE` tag contained in the playlist.
2019-09-08 10:23:33 +00:00
pub const fn media_sequence_tag(&self) -> Option<ExtXMediaSequence> {
2018-02-14 19:18:02 +00:00
self.media_sequence_tag
}
2018-02-13 15:25:33 +00:00
2018-02-14 19:18:02 +00:00
/// Returns the `EXT-X-DISCONTINUITY-SEQUENCE` tag contained in the playlist.
2019-09-08 10:23:33 +00:00
pub const fn discontinuity_sequence_tag(&self) -> Option<ExtXDiscontinuitySequence> {
2018-02-14 19:18:02 +00:00
self.discontinuity_sequence_tag
}
2018-02-13 15:25:33 +00:00
2018-02-14 19:18:02 +00:00
/// Returns the `EXT-X-PLAYLIST-TYPE` tag contained in the playlist.
2019-09-08 10:23:33 +00:00
pub const fn playlist_type_tag(&self) -> Option<ExtXPlaylistType> {
2018-02-14 19:18:02 +00:00
self.playlist_type_tag
}
2018-02-13 15:25:33 +00:00
2018-02-14 19:18:02 +00:00
/// Returns the `EXT-X-I-FRAMES-ONLY` tag contained in the playlist.
2019-09-08 10:23:33 +00:00
pub const fn i_frames_only_tag(&self) -> Option<ExtXIFramesOnly> {
2018-02-14 19:18:02 +00:00
self.i_frames_only_tag
}
/// Returns the `EXT-X-INDEPENDENT-SEGMENTS` tag contained in the playlist.
2019-09-08 10:23:33 +00:00
pub const fn independent_segments_tag(&self) -> Option<ExtXIndependentSegments> {
2018-02-14 19:18:02 +00:00
self.independent_segments_tag
}
/// Returns the `EXT-X-START` tag contained in the playlist.
2019-09-08 10:23:33 +00:00
pub const fn start_tag(&self) -> Option<ExtXStart> {
2018-02-14 19:18:02 +00:00
self.start_tag
}
/// Returns the `EXT-X-ENDLIST` tag contained in the playlist.
2019-09-08 10:23:33 +00:00
pub const fn end_list_tag(&self) -> Option<ExtXEndList> {
2018-02-14 19:18:02 +00:00
self.end_list_tag
}
2018-02-13 15:25:33 +00:00
2018-02-14 19:18:02 +00:00
/// Returns the media segments contained in the playlist.
pub fn segments(&self) -> &[MediaSegment] {
&self.segments
}
2018-02-13 15:25:33 +00:00
}
2019-09-08 10:23:33 +00:00
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)?;
2018-02-14 19:18:02 +00:00
if self.version_tag.version() != ProtocolVersion::V1 {
writeln!(f, "{}", self.version_tag)?;
2018-02-13 15:25:33 +00:00
}
2018-02-14 19:18:02 +00:00
writeln!(f, "{}", self.target_duration_tag)?;
2019-09-14 19:42:06 +00:00
if let Some(value) = &self.media_sequence_tag {
writeln!(f, "{}", value)?;
2018-02-13 15:25:33 +00:00
}
2019-09-14 19:42:06 +00:00
if let Some(value) = &self.discontinuity_sequence_tag {
writeln!(f, "{}", value)?;
2018-02-13 15:25:33 +00:00
}
2019-09-14 19:42:06 +00:00
if let Some(value) = &self.playlist_type_tag {
writeln!(f, "{}", value)?;
2018-02-13 15:25:33 +00:00
}
2019-09-14 19:42:06 +00:00
if let Some(value) = &self.i_frames_only_tag {
writeln!(f, "{}", value)?;
2018-02-13 15:25:33 +00:00
}
2019-09-14 19:42:06 +00:00
if let Some(value) = &self.independent_segments_tag {
writeln!(f, "{}", value)?;
2018-02-13 15:25:33 +00:00
}
2019-09-14 19:42:06 +00:00
if let Some(value) = &self.start_tag {
writeln!(f, "{}", value)?;
2018-02-13 15:25:33 +00:00
}
for segment in &self.segments {
2019-05-20 07:01:01 +00:00
write!(f, "{}", segment)?;
2018-02-13 15:25:33 +00:00
}
2019-09-14 19:42:06 +00:00
if let Some(value) = &self.end_list_tag {
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> {
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;
2019-09-15 08:40:45 +00:00
let mut has_version = false; // m3u8 files without ExtXVersion tags are ProtocolVersion::V1
2019-09-14 19:08:35 +00:00
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"));
2018-02-13 15:25:33 +00:00
}
2019-09-14 19:08:35 +00:00
continue;
}
match tag {
Tag::ExtM3u(_) => return Err(Error::invalid_input()),
Tag::ExtXVersion(t) => {
builder.version(t.version());
2019-09-15 08:40:45 +00:00
has_version = true;
2019-09-14 19:08:35 +00:00
}
Tag::ExtInf(t) => {
has_partial_segment = true;
2019-09-14 19:21:44 +00:00
segment.inf_tag(t);
2019-09-14 19:08:35 +00:00
}
Tag::ExtXByteRange(t) => {
has_partial_segment = true;
2019-09-14 19:21:44 +00:00
segment.byte_range_tag(t);
2019-09-14 19:08:35 +00:00
}
Tag::ExtXDiscontinuity(t) => {
has_discontinuity_tag = true;
has_partial_segment = true;
2019-09-14 19:21:44 +00:00
segment.discontinuity_tag(t);
2019-09-14 19:08:35 +00:00
}
Tag::ExtXKey(t) => {
has_partial_segment = true;
2019-09-14 19:21:44 +00:00
segment.push_key_tag(t);
2019-09-14 19:08:35 +00:00
}
Tag::ExtXMap(t) => {
has_partial_segment = true;
2019-09-14 19:21:44 +00:00
segment.map_tag(t);
2019-09-14 19:08:35 +00:00
}
Tag::ExtXProgramDateTime(t) => {
has_partial_segment = true;
2019-09-14 19:21:44 +00:00
segment.program_date_time_tag(t);
2019-09-14 19:08:35 +00:00
}
Tag::ExtXDateRange(t) => {
has_partial_segment = true;
2019-09-14 19:21:44 +00:00
segment.date_range_tag(t);
2019-09-14 19:08:35 +00:00
}
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());
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
}
2019-09-14 19:08:35 +00:00
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.
2018-02-13 15:25:33 +00:00
}
}
}
2019-09-14 19:08:35 +00:00
Line::Uri(uri) => {
segment.uri(uri);
2019-09-14 19:21:44 +00:00
segments.push(segment.build().map_err(Error::builder_error)?);
segment = MediaSegment::builder();
2019-09-14 19:08:35 +00:00
has_partial_segment = false;
}
2018-02-13 15:25:33 +00:00
}
}
2019-09-14 19:08:35 +00:00
if has_partial_segment {
return Err(Error::invalid_input());
}
2019-09-15 08:40:45 +00:00
if !has_version {
builder.version(ProtocolVersion::V1);
}
2019-09-14 19:08:35 +00:00
builder.segments(segments);
builder.build().map_err(Error::builder_error)
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::*;
#[test]
fn too_large_segment_duration_test() {
2019-09-15 08:40:45 +00:00
let playlist = r#"
#EXTM3U
#EXT-X-TARGETDURATION:8
#EXT-X-VERSION:3
#EXTINF:9.009,
http://media.example.com/first.ts
#EXTINF:9.509,
http://media.example.com/second.ts
#EXTINF:3.003,
http://media.example.com/third.ts
#EXT-X-ENDLIST"#;
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
}