2020-03-25 13:10:27 +00:00
|
|
|
use std::collections::{BTreeMap, 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;
|
|
|
|
|
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::{
|
2020-03-29 10:58:32 +00:00
|
|
|
ExtM3u, ExtXByteRange, ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly,
|
|
|
|
ExtXIndependentSegments, ExtXKey, ExtXMediaSequence, ExtXStart, ExtXTargetDuration,
|
|
|
|
ExtXVersion,
|
2018-10-04 11:18:56 +00:00
|
|
|
};
|
2020-03-28 09:46:07 +00:00
|
|
|
use crate::types::{
|
|
|
|
DecryptionKey, EncryptionMethod, InitializationVector, KeyFormat, PlaylistType, ProtocolVersion,
|
|
|
|
};
|
|
|
|
use crate::utils::{tag, BoolExt};
|
2020-03-23 12:34:26 +00:00
|
|
|
use crate::{Error, RequiredVersion};
|
2018-02-13 15:25:33 +00:00
|
|
|
|
2019-09-14 19:08:35 +00:00
|
|
|
/// Media playlist.
|
2020-03-25 15:13:40 +00:00
|
|
|
#[derive(Builder, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
2020-03-25 13:10:27 +00:00
|
|
|
#[builder(build_fn(skip), setter(strip_option))]
|
|
|
|
#[non_exhaustive]
|
2019-09-14 19:08:35 +00:00
|
|
|
pub struct MediaPlaylist {
|
2020-03-25 13:10:27 +00:00
|
|
|
/// Specifies the maximum [`MediaSegment::duration`]. A typical target
|
|
|
|
/// duration is 10 seconds.
|
2020-02-02 12:38:11 +00:00
|
|
|
///
|
2020-03-25 13:10:27 +00:00
|
|
|
/// ### Note
|
2020-02-02 12:38:11 +00:00
|
|
|
///
|
|
|
|
/// This field is required.
|
2020-03-25 13:10:27 +00:00
|
|
|
pub target_duration: Duration,
|
|
|
|
/// The [`MediaSegment::number`] of the first [`MediaSegment`] that
|
|
|
|
/// appears in a [`MediaPlaylist`].
|
2020-02-02 12:38:11 +00:00
|
|
|
///
|
2020-03-25 13:10:27 +00:00
|
|
|
/// ### Note
|
2020-02-02 12:38:11 +00:00
|
|
|
///
|
2020-03-25 13:10:27 +00:00
|
|
|
/// This field is optional and by default a value of 0 is assumed.
|
2019-09-14 19:08:35 +00:00
|
|
|
#[builder(default)]
|
2020-03-25 13:10:27 +00:00
|
|
|
pub media_sequence: usize,
|
|
|
|
/// Allows synchronization between different renditions of the same
|
|
|
|
/// [`VariantStream`].
|
2020-02-02 12:38:11 +00:00
|
|
|
///
|
2020-03-25 13:10:27 +00:00
|
|
|
/// ### Note
|
2020-02-02 12:38:11 +00:00
|
|
|
///
|
2020-03-25 13:10:27 +00:00
|
|
|
/// This field is optional and by default a vaule of 0 is assumed.
|
|
|
|
///
|
|
|
|
/// [`VariantStream`]: crate::tags::VariantStream
|
2019-09-14 19:08:35 +00:00
|
|
|
#[builder(default)]
|
2020-03-25 13:10:27 +00:00
|
|
|
pub discontinuity_sequence: usize,
|
|
|
|
/// Provides mutability information about a [`MediaPlaylist`].
|
2020-02-02 12:38:11 +00:00
|
|
|
///
|
2020-03-25 13:10:27 +00:00
|
|
|
/// - [`PlaylistType::Vod`] indicates that the playlist must not change.
|
2020-02-02 12:38:11 +00:00
|
|
|
///
|
2020-03-25 13:10:27 +00:00
|
|
|
/// - [`PlaylistType::Event`] indicates that the server does not change or
|
|
|
|
/// delete any part of the playlist, but may append new lines to it.
|
2020-02-02 12:38:11 +00:00
|
|
|
///
|
2020-03-25 13:10:27 +00:00
|
|
|
/// ### Note
|
2020-02-02 12:38:11 +00:00
|
|
|
///
|
|
|
|
/// This field is optional.
|
2020-03-25 13:10:27 +00:00
|
|
|
#[builder(default, setter(into))]
|
|
|
|
pub playlist_type: Option<PlaylistType>,
|
|
|
|
/// Indicates that each [`MediaSegment`] in the playlist describes a single
|
|
|
|
/// I-frame. I-frames are encoded video frames, whose decoding does not
|
|
|
|
/// depend on any other frame. I-frame Playlists can be used for trick
|
|
|
|
/// play, such as fast forward, rapid reverse, and scrubbing.
|
2020-02-02 12:38:11 +00:00
|
|
|
///
|
2020-03-25 13:10:27 +00:00
|
|
|
/// ### Note
|
2020-02-02 12:38:11 +00:00
|
|
|
///
|
|
|
|
/// This field is optional.
|
2019-09-14 19:08:35 +00:00
|
|
|
#[builder(default)]
|
2020-03-25 13:10:27 +00:00
|
|
|
pub has_i_frames_only: bool,
|
|
|
|
/// This indicates that all media samples in a [`MediaSegment`] can be
|
|
|
|
/// decoded without information from other segments.
|
2020-02-02 12:38:11 +00:00
|
|
|
///
|
2020-03-25 13:10:27 +00:00
|
|
|
/// ### Note
|
2020-02-02 12:38:11 +00:00
|
|
|
///
|
2020-03-25 13:10:27 +00:00
|
|
|
/// This field is optional and by default `false`. If the value is `true` it
|
|
|
|
/// applies to every [`MediaSegment`] in this [`MediaPlaylist`].
|
2019-09-14 19:08:35 +00:00
|
|
|
#[builder(default)]
|
2020-03-25 13:10:27 +00:00
|
|
|
pub has_independent_segments: bool,
|
|
|
|
/// Indicates a preferred point at which to start playing a playlist. By
|
|
|
|
/// default, clients should start playback at this point when beginning a
|
|
|
|
/// playback session.
|
2020-02-02 12:38:11 +00:00
|
|
|
///
|
2020-03-25 13:10:27 +00:00
|
|
|
/// ### Note
|
2020-02-02 12:38:11 +00:00
|
|
|
///
|
|
|
|
/// This field is optional.
|
2020-03-25 13:10:27 +00:00
|
|
|
#[builder(default, setter(into))]
|
|
|
|
pub start: Option<ExtXStart>,
|
|
|
|
/// Indicates that no more [`MediaSegment`]s will be added to the
|
|
|
|
/// [`MediaPlaylist`] file.
|
|
|
|
///
|
|
|
|
/// ### Note
|
|
|
|
///
|
|
|
|
/// This field is optional and by default `false`.
|
|
|
|
/// A `false` indicates that the client should reload the [`MediaPlaylist`]
|
|
|
|
/// from the server, until a playlist is encountered, where this field is
|
|
|
|
/// `true`.
|
2020-02-02 12:38:11 +00:00
|
|
|
#[builder(default)]
|
2020-03-25 13:10:27 +00:00
|
|
|
pub has_end_list: bool,
|
2020-02-02 12:38:11 +00:00
|
|
|
/// A list of all [`MediaSegment`]s.
|
|
|
|
///
|
2020-03-25 13:10:27 +00:00
|
|
|
/// ### Note
|
2020-02-02 12:38:11 +00:00
|
|
|
///
|
|
|
|
/// This field is required.
|
2020-03-25 13:10:27 +00:00
|
|
|
#[builder(setter(custom))]
|
|
|
|
pub segments: BTreeMap<usize, 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
|
|
|
///
|
2020-03-25 13:10:27 +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
|
|
|
///
|
2020-03-25 13:10:27 +00:00
|
|
|
/// ### Note
|
2020-02-02 12:38:11 +00:00
|
|
|
///
|
|
|
|
/// 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)")]
|
2020-03-25 13:10:27 +00:00
|
|
|
pub allowable_excess_duration: Duration,
|
2020-02-02 12:50:56 +00:00
|
|
|
/// A list of unknown tags.
|
|
|
|
///
|
2020-03-25 13:10:27 +00:00
|
|
|
/// ### Note
|
2020-02-02 12:50:56 +00:00
|
|
|
///
|
|
|
|
/// This field is optional.
|
2020-03-25 13:10:27 +00:00
|
|
|
#[builder(default, setter(into))]
|
|
|
|
pub unknown: 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 {
|
2020-03-25 13:10:27 +00:00
|
|
|
self.validate_media_segments(*target_duration)
|
2019-09-14 19:08:35 +00:00
|
|
|
.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 {
|
2020-03-25 13:10:27 +00:00
|
|
|
// verify the independent segments
|
|
|
|
if self.has_independent_segments.unwrap_or(false) {
|
|
|
|
// If the encryption METHOD is AES-128 and the Playlist contains an EXT-
|
|
|
|
// X-I-FRAMES-ONLY tag, the entire resource MUST be encrypted using
|
|
|
|
// AES-128 CBC with PKCS7 padding [RFC5652].
|
|
|
|
//
|
|
|
|
// from the rfc: https://tools.ietf.org/html/rfc8216#section-6.2.3
|
|
|
|
|
|
|
|
let is_aes128 = segments
|
|
|
|
.values()
|
|
|
|
// convert iterator of segments to iterator of keys
|
|
|
|
.flat_map(|s| s.keys.iter())
|
|
|
|
// filter out all empty keys
|
|
|
|
.filter_map(ExtXKey::as_ref)
|
|
|
|
.any(|k| k.method == EncryptionMethod::Aes128);
|
|
|
|
|
|
|
|
if is_aes128 {
|
|
|
|
for key in segments.values().flat_map(|s| s.keys.iter()) {
|
|
|
|
if let ExtXKey(Some(key)) = key {
|
|
|
|
if key.method != EncryptionMethod::Aes128 {
|
|
|
|
return Err(Error::custom(concat!(
|
|
|
|
"if any independent segment is encrypted with Aes128,",
|
|
|
|
" all must be encrypted with Aes128"
|
|
|
|
)));
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return Err(Error::custom(concat!(
|
|
|
|
"if any independent segment is encrypted with Aes128,",
|
|
|
|
" all must be encrypted with Aes128"
|
|
|
|
)));
|
|
|
|
}
|
2019-10-05 14:24:48 +00:00
|
|
|
}
|
2020-03-25 13:10:27 +00:00
|
|
|
}
|
|
|
|
}
|
2019-09-14 19:08:35 +00:00
|
|
|
|
2020-03-25 13:10:27 +00:00
|
|
|
for segment in segments.values() {
|
|
|
|
// CHECK: `#EXT-X-TARGETDURATION`
|
|
|
|
let segment_duration = segment.duration.duration();
|
|
|
|
|
|
|
|
// round the duration if it is .5s
|
|
|
|
let rounded_segment_duration =
|
|
|
|
Duration::from_secs(segment_duration.as_secs_f64().round() as u64);
|
|
|
|
|
|
|
|
let max_segment_duration = self
|
|
|
|
.allowable_excess_duration
|
|
|
|
.as_ref()
|
|
|
|
.map_or(target_duration, |value| target_duration + *value);
|
2019-09-14 19:08:35 +00:00
|
|
|
|
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,
|
2020-03-25 13:10:27 +00:00
|
|
|
segment.uri()
|
2019-09-14 19:08:35 +00:00
|
|
|
)));
|
|
|
|
}
|
2018-02-14 19:18:02 +00:00
|
|
|
|
2019-09-14 19:08:35 +00:00
|
|
|
// CHECK: `#EXT-X-BYTE-RANGE`
|
2020-03-25 13:10:27 +00:00
|
|
|
if let Some(range) = &segment.byte_range {
|
2020-02-23 17:56:41 +00:00
|
|
|
if range.start().is_none() {
|
2020-03-25 13:10:27 +00:00
|
|
|
// TODO: error messages
|
|
|
|
if last_range_uri.ok_or_else(Error::invalid_input)? != segment.uri() {
|
2019-09-14 19:08:35 +00:00
|
|
|
return Err(Error::invalid_input());
|
|
|
|
}
|
|
|
|
} else {
|
2020-03-25 13:10:27 +00:00
|
|
|
last_range_uri = Some(segment.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(())
|
|
|
|
}
|
|
|
|
|
2020-03-25 13:10:27 +00:00
|
|
|
/// Adds a media segment to the resulting playlist and assigns the next free
|
|
|
|
/// [`MediaSegment::number`] to the segment.
|
|
|
|
pub fn push_segment(&mut self, segment: MediaSegment) -> &mut Self {
|
|
|
|
let segments = self.segments.get_or_insert_with(BTreeMap::new);
|
|
|
|
|
|
|
|
let number = {
|
|
|
|
if segment.explicit_number {
|
|
|
|
segment.number
|
|
|
|
} else {
|
|
|
|
segments.keys().last().copied().unwrap_or(0) + 1
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
segments.insert(number, segment);
|
2019-10-04 09:02:21 +00:00
|
|
|
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)
|
|
|
|
}
|
2020-03-25 13:10:27 +00:00
|
|
|
|
|
|
|
/// Adds segments to the resulting playlist and assigns a
|
|
|
|
/// [`MediaSegment::number`] to each segment.
|
|
|
|
///
|
|
|
|
/// ## Note
|
|
|
|
///
|
|
|
|
/// The [`MediaSegment::number`] will be assigned based on the order of the
|
|
|
|
/// input (e.g. the first element will be 0, second element 1, ..) or if a
|
|
|
|
/// number has been set explicitly. This function assumes, that all segments
|
|
|
|
/// will be present in the final media playlist and the following is only
|
|
|
|
/// possible if the segment is marked with `ExtXDiscontinuity`.
|
|
|
|
pub fn segments(&mut self, segments: Vec<MediaSegment>) -> &mut Self {
|
|
|
|
// media segments are numbered starting at either 0 or the discontinuity
|
|
|
|
// sequence, but it might not be available at the moment.
|
|
|
|
//
|
|
|
|
// -> final numbering will be applied in the build function
|
|
|
|
self.segments = Some(segments.into_iter().enumerate().collect());
|
|
|
|
self
|
|
|
|
}
|
|
|
|
|
2020-03-28 09:51:19 +00:00
|
|
|
/// Builds a new `MediaPlaylist`.
|
|
|
|
///
|
|
|
|
/// # Errors
|
|
|
|
///
|
|
|
|
/// If a required field has not been initialized.
|
2020-03-25 13:10:27 +00:00
|
|
|
pub fn build(&self) -> Result<MediaPlaylist, String> {
|
|
|
|
// validate builder
|
|
|
|
self.validate()?;
|
|
|
|
|
|
|
|
let sequence_number = self.media_sequence.unwrap_or(0);
|
|
|
|
|
|
|
|
let segments = self
|
|
|
|
.segments
|
|
|
|
.as_ref()
|
|
|
|
.ok_or_else(|| "missing field `segments`".to_string())?;
|
|
|
|
|
|
|
|
// insert all explictly numbered segments into the result
|
|
|
|
let mut result_segments = segments
|
|
|
|
.iter()
|
2020-03-28 09:46:07 +00:00
|
|
|
.filter_map(|(_, s)| s.explicit_number.athen(|| (s.number, s.clone())))
|
2020-03-25 13:10:27 +00:00
|
|
|
.collect::<BTreeMap<_, _>>();
|
|
|
|
|
|
|
|
// no segment should exist before the sequence_number
|
|
|
|
if let Some(first_segment) = result_segments.keys().min() {
|
|
|
|
if sequence_number > *first_segment {
|
|
|
|
return Err(format!(
|
|
|
|
"there should be no segment ({}) before the sequence_number ({})",
|
|
|
|
first_segment, sequence_number,
|
|
|
|
));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut position = sequence_number;
|
2020-03-29 10:58:32 +00:00
|
|
|
let mut previous_range: Option<ExtXByteRange> = None;
|
|
|
|
|
2020-03-25 13:10:27 +00:00
|
|
|
for segment in segments
|
|
|
|
.iter()
|
|
|
|
.filter_map(|(_, s)| if s.explicit_number { None } else { Some(s) })
|
|
|
|
{
|
|
|
|
while result_segments.contains_key(&position) {
|
|
|
|
position += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut segment = segment.clone();
|
|
|
|
segment.number = position;
|
|
|
|
|
2020-03-28 09:47:52 +00:00
|
|
|
// add the segment number as iv, if the iv is missing:
|
|
|
|
for key in &mut segment.keys {
|
|
|
|
if let ExtXKey(Some(DecryptionKey {
|
|
|
|
method, iv, format, ..
|
|
|
|
})) = key
|
|
|
|
{
|
|
|
|
if *method == EncryptionMethod::Aes128 && *iv == InitializationVector::Missing {
|
|
|
|
if format.is_none() {
|
|
|
|
*iv = InitializationVector::Number(segment.number as u128);
|
|
|
|
} else if let Some(KeyFormat::Identity) = format {
|
|
|
|
*iv = InitializationVector::Number(segment.number as u128);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-29 10:58:32 +00:00
|
|
|
// add the lower bound to the byterange automatically
|
|
|
|
if let Some(range) = &mut segment.byte_range {
|
|
|
|
if range.start().is_none() {
|
|
|
|
if let Some(previous_range) = previous_range {
|
|
|
|
// the end of the previous_range is the start of the next range
|
|
|
|
*range = range.saturating_add(previous_range.end());
|
|
|
|
range.set_start(Some(previous_range.end()));
|
|
|
|
} else {
|
|
|
|
// assume that the byte range starts at zero
|
|
|
|
range.set_start(Some(0));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
previous_range = segment.byte_range;
|
|
|
|
}
|
|
|
|
|
2020-03-25 13:10:27 +00:00
|
|
|
result_segments.insert(segment.number, segment);
|
|
|
|
position += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut previous_n = None;
|
|
|
|
|
|
|
|
for n in result_segments.keys() {
|
|
|
|
if let Some(previous_n) = previous_n {
|
|
|
|
if previous_n + 1 != *n {
|
|
|
|
return Err(format!("missing segment ({})", previous_n + 1));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
previous_n = Some(n);
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(MediaPlaylist {
|
|
|
|
target_duration: self
|
|
|
|
.target_duration
|
|
|
|
.ok_or_else(|| "missing field `target_duration`".to_string())?,
|
|
|
|
media_sequence: self.media_sequence.unwrap_or(0),
|
|
|
|
discontinuity_sequence: self.discontinuity_sequence.unwrap_or(0),
|
|
|
|
playlist_type: self.playlist_type.unwrap_or(None),
|
|
|
|
has_i_frames_only: self.has_i_frames_only.unwrap_or(false),
|
|
|
|
has_independent_segments: self.has_independent_segments.unwrap_or(false),
|
|
|
|
start: self.start.unwrap_or(None),
|
|
|
|
has_end_list: self.has_end_list.unwrap_or(false),
|
|
|
|
segments: result_segments,
|
|
|
|
allowable_excess_duration: self
|
|
|
|
.allowable_excess_duration
|
|
|
|
.unwrap_or_else(|| Duration::from_secs(0)),
|
|
|
|
unknown: self.unknown.clone().unwrap_or_else(Vec::new),
|
|
|
|
})
|
|
|
|
}
|
2019-10-04 09:02:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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-03-25 13:10:27 +00:00
|
|
|
self.target_duration.map(ExtXTargetDuration),
|
2020-03-28 09:46:07 +00:00
|
|
|
(self.media_sequence.unwrap_or(0) != 0)
|
|
|
|
.athen(|| ExtXMediaSequence(self.media_sequence.unwrap_or(0))),
|
|
|
|
(self.discontinuity_sequence.unwrap_or(0) != 0)
|
|
|
|
.athen(|| ExtXDiscontinuitySequence(self.discontinuity_sequence.unwrap_or(0))),
|
2020-02-10 12:20:39 +00:00
|
|
|
self.playlist_type,
|
2020-03-28 09:46:07 +00:00
|
|
|
self.has_i_frames_only
|
|
|
|
.unwrap_or(false)
|
|
|
|
.athen_some(ExtXIFramesOnly),
|
|
|
|
self.has_independent_segments
|
|
|
|
.unwrap_or(false)
|
|
|
|
.athen_some(ExtXIndependentSegments),
|
2020-02-10 12:20:39 +00:00
|
|
|
self.start,
|
2020-03-28 09:46:07 +00:00
|
|
|
self.has_end_list.unwrap_or(false).athen_some(ExtXEndList),
|
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() }
|
2020-03-25 13:10:27 +00:00
|
|
|
|
|
|
|
/// Computes the `Duration` of the [`MediaPlaylist`], by adding each segment
|
|
|
|
/// duration together.
|
2020-03-28 09:48:17 +00:00
|
|
|
#[must_use]
|
2020-03-25 13:10:27 +00:00
|
|
|
pub fn duration(&self) -> Duration {
|
|
|
|
self.segments.values().map(|s| s.duration.duration()).sum()
|
|
|
|
}
|
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-03-25 13:10:27 +00:00
|
|
|
ExtXTargetDuration(self.target_duration),
|
2020-03-28 09:46:07 +00:00
|
|
|
(self.media_sequence != 0).athen(|| ExtXMediaSequence(self.media_sequence)),
|
|
|
|
(self.discontinuity_sequence != 0)
|
|
|
|
.athen(|| ExtXDiscontinuitySequence(self.discontinuity_sequence)),
|
2020-02-10 12:20:39 +00:00
|
|
|
self.playlist_type,
|
2020-03-28 09:46:07 +00:00
|
|
|
self.has_i_frames_only.athen_some(ExtXIFramesOnly),
|
|
|
|
self.has_independent_segments
|
|
|
|
.athen_some(ExtXIndependentSegments),
|
2020-02-10 12:20:39 +00:00
|
|
|
self.start,
|
2020-03-28 09:46:07 +00:00
|
|
|
self.has_end_list.athen_some(ExtXEndList),
|
2019-10-05 14:24:48 +00:00
|
|
|
self.segments
|
|
|
|
]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-02-13 15:25:33 +00:00
|
|
|
impl fmt::Display for MediaPlaylist {
|
2020-04-09 06:43:13 +00:00
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
2018-02-13 15:25:33 +00:00
|
|
|
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-03-25 13:10:27 +00:00
|
|
|
writeln!(f, "{}", ExtXTargetDuration(self.target_duration))?;
|
2020-02-02 12:38:11 +00:00
|
|
|
|
2020-03-25 13:10:27 +00:00
|
|
|
if self.media_sequence != 0 {
|
|
|
|
writeln!(f, "{}", ExtXMediaSequence(self.media_sequence))?;
|
2018-02-13 15:25:33 +00:00
|
|
|
}
|
2020-02-02 12:38:11 +00:00
|
|
|
|
2020-03-25 13:10:27 +00:00
|
|
|
if self.discontinuity_sequence != 0 {
|
|
|
|
writeln!(
|
|
|
|
f,
|
|
|
|
"{}",
|
|
|
|
ExtXDiscontinuitySequence(self.discontinuity_sequence)
|
|
|
|
)?;
|
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-03-25 13:10:27 +00:00
|
|
|
if self.has_i_frames_only {
|
|
|
|
writeln!(f, "{}", ExtXIFramesOnly)?;
|
2018-02-13 15:25:33 +00:00
|
|
|
}
|
2020-02-02 12:38:11 +00:00
|
|
|
|
2020-03-25 13:10:27 +00:00
|
|
|
if self.has_independent_segments {
|
|
|
|
writeln!(f, "{}", ExtXIndependentSegments)?;
|
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-03-28 09:47:52 +00:00
|
|
|
let mut available_keys = HashSet::<ExtXKey>::new();
|
2020-02-16 11:51:49 +00:00
|
|
|
|
2020-03-25 13:10:27 +00:00
|
|
|
for segment in self.segments.values() {
|
|
|
|
for key in &segment.keys {
|
|
|
|
if let ExtXKey(Some(decryption_key)) = key {
|
|
|
|
// next segment will be encrypted, so the segment can not have an empty key
|
|
|
|
available_keys.remove(&ExtXKey::empty());
|
|
|
|
|
2020-03-28 09:47:52 +00:00
|
|
|
let mut decryption_key = decryption_key.clone();
|
|
|
|
let key = {
|
|
|
|
if let InitializationVector::Number(_) = decryption_key.iv {
|
|
|
|
// set the iv from a segment number to missing
|
|
|
|
// this does reduce the output size and the correct iv
|
|
|
|
// is automatically set, when parsing.
|
|
|
|
decryption_key.iv = InitializationVector::Missing;
|
|
|
|
}
|
|
|
|
|
|
|
|
ExtXKey(Some(decryption_key.clone()))
|
|
|
|
};
|
|
|
|
|
2020-03-25 13:10:27 +00:00
|
|
|
// only do something if a key has been overwritten
|
2020-03-28 09:47:52 +00:00
|
|
|
if available_keys.insert(key.clone()) {
|
2020-03-25 13:10:27 +00:00
|
|
|
let mut remove_key = None;
|
|
|
|
|
|
|
|
// an old key might be removed:
|
|
|
|
for k in &available_keys {
|
|
|
|
if let ExtXKey(Some(dk)) = k {
|
2020-03-28 09:47:52 +00:00
|
|
|
if dk.format == decryption_key.format && key != *k {
|
|
|
|
remove_key = Some(k.clone());
|
2020-03-25 13:10:27 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
unreachable!("empty keys should not exist in `available_keys`");
|
|
|
|
}
|
2020-02-16 11:51:49 +00:00
|
|
|
}
|
|
|
|
|
2020-03-25 13:10:27 +00:00
|
|
|
if let Some(k) = remove_key {
|
|
|
|
// this should always be true:
|
2020-03-28 09:47:52 +00:00
|
|
|
let res = available_keys.remove(&k);
|
2020-03-25 13:10:27 +00:00
|
|
|
debug_assert!(res);
|
|
|
|
}
|
2020-02-16 11:51:49 +00:00
|
|
|
|
2020-03-25 13:10:27 +00:00
|
|
|
writeln!(f, "{}", key)?;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// the next segment is not encrypted, so remove all available keys
|
|
|
|
available_keys.clear();
|
2020-03-28 09:47:52 +00:00
|
|
|
available_keys.insert(ExtXKey::empty());
|
2020-02-16 11:51:49 +00:00
|
|
|
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-03-25 13:10:27 +00:00
|
|
|
for value in &self.unknown {
|
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-03-25 13:10:27 +00:00
|
|
|
if self.has_end_list {
|
|
|
|
writeln!(f, "{}", ExtXEndList)?;
|
2020-02-02 12:50:56 +00:00
|
|
|
}
|
|
|
|
|
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-03-25 13:10:27 +00:00
|
|
|
let mut unknown = vec![];
|
|
|
|
let mut available_keys = HashSet::new();
|
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-03-25 13:10:27 +00:00
|
|
|
segment.duration(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
|
|
|
}
|
2020-03-25 13:10:27 +00:00
|
|
|
Tag::ExtXDiscontinuity(_) => {
|
2019-09-14 19:08:35 +00:00
|
|
|
has_discontinuity_tag = true;
|
|
|
|
has_partial_segment = true;
|
2020-03-25 13:10:27 +00:00
|
|
|
segment.has_discontinuity(true);
|
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;
|
2020-03-25 13:10:27 +00:00
|
|
|
let mut remove = None;
|
|
|
|
|
|
|
|
if let ExtXKey(Some(decryption_key)) = &key {
|
|
|
|
for old_key in &available_keys {
|
|
|
|
if let ExtXKey(Some(old_decryption_key)) = &old_key {
|
|
|
|
if old_decryption_key.format == decryption_key.format {
|
|
|
|
// remove the old key
|
|
|
|
remove = Some(old_key.clone());
|
|
|
|
|
|
|
|
// there are no keys with the same format in
|
|
|
|
// available_keys so the loop can stop here:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// remove an empty key
|
|
|
|
remove = Some(ExtXKey::empty());
|
|
|
|
break;
|
|
|
|
}
|
2020-02-16 11:51:49 +00:00
|
|
|
}
|
2020-03-25 13:10:27 +00:00
|
|
|
} else {
|
|
|
|
available_keys.clear();
|
|
|
|
available_keys.insert(ExtXKey::empty());
|
|
|
|
is_new_key = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some(key) = &remove {
|
|
|
|
available_keys.remove(key);
|
2020-02-16 11:51:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if is_new_key {
|
2020-03-25 13:10:27 +00:00
|
|
|
available_keys.insert(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-03-25 13:10:27 +00:00
|
|
|
t.keys = available_keys.iter().cloned().collect();
|
2020-02-16 11:50:52 +00:00
|
|
|
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-03-25 13:10:27 +00:00
|
|
|
builder.target_duration(t.0);
|
2019-09-14 19:08:35 +00:00
|
|
|
}
|
|
|
|
Tag::ExtXMediaSequence(t) => {
|
2020-03-25 13:10:27 +00:00
|
|
|
builder.media_sequence(t.0);
|
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-03-25 13:10:27 +00:00
|
|
|
builder.discontinuity_sequence(t.0);
|
2019-09-14 19:08:35 +00:00
|
|
|
}
|
2020-03-25 13:10:27 +00:00
|
|
|
Tag::ExtXEndList(_) => {
|
|
|
|
builder.has_end_list(true);
|
2019-09-14 19:08:35 +00:00
|
|
|
}
|
2020-03-25 11:17:03 +00:00
|
|
|
Tag::PlaylistType(t) => {
|
2020-02-10 12:20:39 +00:00
|
|
|
builder.playlist_type(t);
|
2019-09-14 19:08:35 +00:00
|
|
|
}
|
2020-03-25 13:10:27 +00:00
|
|
|
Tag::ExtXIFramesOnly(_) => {
|
|
|
|
builder.has_i_frames_only(true);
|
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
|
|
|
}
|
2020-03-25 13:10:27 +00:00
|
|
|
Tag::ExtXIndependentSegments(_) => {
|
|
|
|
builder.has_independent_segments(true);
|
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-03-25 13:10:27 +00:00
|
|
|
unknown.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-03-25 13:10:27 +00:00
|
|
|
segment.keys(available_keys.iter().cloned().collect::<Vec<_>>());
|
2020-01-23 18:13:26 +00:00
|
|
|
segments.push(segment.build().map_err(Error::builder)?);
|
2020-03-25 13:10:27 +00:00
|
|
|
|
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 {
|
2020-03-28 09:47:52 +00:00
|
|
|
return Err(Error::custom("Missing URI for the last `MediaSegment`"));
|
2019-09-14 19:08:35 +00:00
|
|
|
}
|
2019-10-04 09:02:21 +00:00
|
|
|
|
2020-03-25 13:10:27 +00:00
|
|
|
builder.unknown(unknown);
|
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)
|
2020-03-25 13:10:27 +00:00
|
|
|
assert_eq!(
|
|
|
|
MediaPlaylist::builder()
|
|
|
|
.allowable_excess_duration(Duration::from_secs(2))
|
|
|
|
.parse(playlist)
|
|
|
|
.unwrap(),
|
|
|
|
MediaPlaylist::builder()
|
|
|
|
.allowable_excess_duration(Duration::from_secs(2))
|
|
|
|
.target_duration(Duration::from_secs(8))
|
|
|
|
.segments(vec![
|
|
|
|
MediaSegment::builder()
|
|
|
|
.duration(Duration::from_secs_f64(9.009))
|
|
|
|
.uri("http://media.example.com/first.ts")
|
|
|
|
.build()
|
|
|
|
.unwrap(),
|
|
|
|
MediaSegment::builder()
|
|
|
|
.duration(Duration::from_secs_f64(9.509))
|
|
|
|
.uri("http://media.example.com/second.ts")
|
|
|
|
.build()
|
|
|
|
.unwrap(),
|
|
|
|
MediaSegment::builder()
|
|
|
|
.duration(Duration::from_secs_f64(3.003))
|
|
|
|
.uri("http://media.example.com/third.ts")
|
|
|
|
.build()
|
|
|
|
.unwrap(),
|
|
|
|
])
|
|
|
|
.has_end_list(true)
|
|
|
|
.build()
|
|
|
|
.unwrap()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_segment_number_simple() {
|
|
|
|
let playlist = MediaPlaylist::builder()
|
2019-09-14 19:08:35 +00:00
|
|
|
.allowable_excess_duration(Duration::from_secs(2))
|
2020-03-25 13:10:27 +00:00
|
|
|
.target_duration(Duration::from_secs(8))
|
|
|
|
.segments(vec![
|
|
|
|
MediaSegment::builder()
|
|
|
|
.duration(Duration::from_secs_f64(9.009))
|
|
|
|
.uri("http://media.example.com/first.ts")
|
|
|
|
.build()
|
|
|
|
.unwrap(),
|
|
|
|
MediaSegment::builder()
|
|
|
|
.duration(Duration::from_secs_f64(9.509))
|
|
|
|
.uri("http://media.example.com/second.ts")
|
|
|
|
.build()
|
|
|
|
.unwrap(),
|
|
|
|
MediaSegment::builder()
|
|
|
|
.duration(Duration::from_secs_f64(3.003))
|
|
|
|
.uri("http://media.example.com/third.ts")
|
|
|
|
.build()
|
|
|
|
.unwrap(),
|
|
|
|
])
|
|
|
|
.build()
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
let mut segments = playlist.segments.into_iter().map(|(k, v)| (k, v.number));
|
|
|
|
assert_eq!(segments.next(), Some((0, 0)));
|
|
|
|
assert_eq!(segments.next(), Some((1, 1)));
|
|
|
|
assert_eq!(segments.next(), Some((2, 2)));
|
|
|
|
assert_eq!(segments.next(), None);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_segment_number_sequence() {
|
|
|
|
let playlist = MediaPlaylist::builder()
|
|
|
|
.target_duration(Duration::from_secs(8))
|
|
|
|
.media_sequence(2680)
|
|
|
|
.segments(vec![
|
|
|
|
MediaSegment::builder()
|
|
|
|
.duration(Duration::from_secs_f64(7.975))
|
|
|
|
.uri("https://priv.example.com/fileSequence2680.ts")
|
|
|
|
.build()
|
|
|
|
.unwrap(),
|
|
|
|
MediaSegment::builder()
|
|
|
|
.duration(Duration::from_secs_f64(7.941))
|
|
|
|
.uri("https://priv.example.com/fileSequence2681.ts")
|
|
|
|
.build()
|
|
|
|
.unwrap(),
|
|
|
|
MediaSegment::builder()
|
|
|
|
.duration(Duration::from_secs_f64(7.975))
|
|
|
|
.uri("https://priv.example.com/fileSequence2682.ts")
|
|
|
|
.build()
|
|
|
|
.unwrap(),
|
|
|
|
])
|
|
|
|
.build()
|
2019-09-14 19:08:35 +00:00
|
|
|
.unwrap();
|
2020-03-25 13:10:27 +00:00
|
|
|
let mut segments = playlist.segments.into_iter().map(|(k, v)| (k, v.number));
|
|
|
|
assert_eq!(segments.next(), Some((2680, 2680)));
|
|
|
|
assert_eq!(segments.next(), Some((2681, 2681)));
|
|
|
|
assert_eq!(segments.next(), Some((2682, 2682)));
|
|
|
|
assert_eq!(segments.next(), None);
|
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
|
|
|
}
|