use std::borrow::Cow;
use std::collections::HashSet;
use std::convert::TryFrom;
use std::fmt;
use std::str::FromStr;
use std::time::Duration;
use derive_builder::Builder;
use stable_vec::StableVec;
use crate::line::{Line, Lines, Tag};
use crate::media_segment::MediaSegment;
use crate::tags::{
ExtM3u, ExtXByteRange, ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly,
ExtXIndependentSegments, ExtXKey, ExtXMediaSequence, ExtXStart, ExtXTargetDuration,
ExtXVersion,
};
use crate::types::{
DecryptionKey, EncryptionMethod, InitializationVector, KeyFormat, PlaylistType, ProtocolVersion,
};
use crate::utils::{tag, BoolExt};
use crate::{Error, RequiredVersion};
#[derive(Builder, Debug, Clone, PartialEq, Eq)]
#[builder(build_fn(skip), setter(strip_option))]
#[non_exhaustive]
pub struct MediaPlaylist<'a> {
pub target_duration: Duration,
#[builder(default)]
pub media_sequence: usize,
#[builder(default)]
pub discontinuity_sequence: usize,
#[builder(default, setter(into))]
pub playlist_type: Option<PlaylistType>,
#[builder(default)]
pub has_i_frames_only: bool,
#[builder(default)]
pub has_independent_segments: bool,
#[builder(default, setter(into))]
pub start: Option<ExtXStart>,
#[builder(default)]
pub has_end_list: bool,
#[builder(setter(custom))]
pub segments: StableVec<MediaSegment<'a>>,
#[builder(default = "Duration::from_secs(0)")]
pub allowable_excess_duration: Duration,
#[builder(default, setter(into))]
pub unknown: Vec<Cow<'a, str>>,
}
impl<'a> MediaPlaylistBuilder<'a> {
fn validate(&self) -> Result<(), String> {
if let Some(target_duration) = &self.target_duration {
self.validate_media_segments(*target_duration)
.map_err(|e| e.to_string())?;
}
Ok(())
}
fn validate_media_segments(&self, target_duration: Duration) -> crate::Result<()> {
let mut last_range_uri = None;
if let Some(segments) = &self.segments {
if self.has_independent_segments.unwrap_or(false) {
let is_aes128 = segments
.values()
.flat_map(|s| s.keys.iter())
.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"
)));
}
}
}
}
for segment in segments.values() {
let segment_duration = segment.duration.duration();
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);
if rounded_segment_duration > max_segment_duration {
return Err(Error::custom(format!(
"Too large segment duration: actual={:?}, max={:?}, target_duration={:?}, uri={:?}",
segment_duration,
max_segment_duration,
target_duration,
segment.uri()
)));
}
if let Some(range) = &segment.byte_range {
if range.start().is_none() {
if last_range_uri.ok_or_else(Error::invalid_input)? != segment.uri() {
return Err(Error::invalid_input());
}
} else {
last_range_uri = Some(segment.uri());
}
} else {
last_range_uri = None;
}
}
}
Ok(())
}
pub fn push_segment(&mut self, segment: MediaSegment<'a>) -> &mut Self {
let segments = self.segments.get_or_insert_with(StableVec::new);
if segment.explicit_number {
segments.reserve_for(segment.number);
segments.insert(segment.number, segment);
} else {
segments.push(segment);
}
self
}
pub fn parse(&mut self, input: &'a str) -> crate::Result<MediaPlaylist<'a>> {
parse_media_playlist(input, self)
}
pub fn segments(&mut self, segments: Vec<MediaSegment<'a>>) -> &mut Self {
let mut vec = StableVec::<MediaSegment<'a>>::with_capacity(segments.len());
let mut remaining = Vec::with_capacity(segments.len());
for segment in segments {
if segment.explicit_number {
vec.insert(segment.number, segment);
} else {
remaining.push(segment);
}
}
for segment in remaining {
vec.push(segment);
}
self.segments = Some(vec);
self
}
pub fn build(&self) -> Result<MediaPlaylist<'a>, String> {
self.validate()?;
let sequence_number = self.media_sequence.unwrap_or(0);
let mut segments = self
.segments
.clone()
.ok_or_else(|| "missing field `segments`".to_string())?;
if let Some(first_segment) = segments.find_first() {
if sequence_number > first_segment.number && first_segment.explicit_number {
return Err(format!(
"there should be no segment ({}) before the sequence_number ({})",
first_segment, sequence_number,
));
}
}
let mut previous_range: Option<ExtXByteRange> = None;
for (i, segment) in segments.iter_mut() {
if !segment.explicit_number {
segment.number = i + sequence_number;
}
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);
}
}
}
}
if let Some(range) = &mut segment.byte_range {
if range.start().is_none() {
if let Some(previous_range) = previous_range {
*range = range.saturating_add(previous_range.end());
range.set_start(Some(previous_range.end()));
} else {
range.set_start(Some(0));
}
}
previous_range = segment.byte_range;
}
}
if !segments.is_compact() {
return Err("a segment is missing".to_string());
}
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,
allowable_excess_duration: self
.allowable_excess_duration
.unwrap_or_else(|| Duration::from_secs(0)),
unknown: self.unknown.clone().unwrap_or_else(Vec::new),
})
}
}
impl<'a> RequiredVersion for MediaPlaylistBuilder<'a> {
fn required_version(&self) -> ProtocolVersion {
required_version![
self.target_duration.map(ExtXTargetDuration),
(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))),
self.playlist_type,
self.has_i_frames_only
.unwrap_or(false)
.athen_some(ExtXIFramesOnly),
self.has_independent_segments
.unwrap_or(false)
.athen_some(ExtXIndependentSegments),
self.start,
self.has_end_list.unwrap_or(false).athen_some(ExtXEndList),
self.segments
]
}
}
impl<'a> MediaPlaylist<'a> {
#[must_use]
#[inline]
pub fn builder() -> MediaPlaylistBuilder<'a> { MediaPlaylistBuilder::default() }
#[must_use]
pub fn duration(&self) -> Duration {
self.segments.values().map(|s| s.duration.duration()).sum()
}
#[must_use]
pub fn into_owned(self) -> MediaPlaylist<'static> {
MediaPlaylist {
target_duration: self.target_duration,
media_sequence: self.media_sequence,
discontinuity_sequence: self.discontinuity_sequence,
playlist_type: self.playlist_type,
has_i_frames_only: self.has_i_frames_only,
has_independent_segments: self.has_independent_segments,
start: self.start,
has_end_list: self.has_end_list,
segments: {
self.segments
.into_iter()
.map(|(_, s)| s.into_owned())
.collect()
},
allowable_excess_duration: self.allowable_excess_duration,
unknown: {
self.unknown
.into_iter()
.map(|v| Cow::Owned(v.into_owned()))
.collect()
},
}
}
}
impl<'a> RequiredVersion for MediaPlaylist<'a> {
fn required_version(&self) -> ProtocolVersion {
required_version![
ExtXTargetDuration(self.target_duration),
(self.media_sequence != 0).athen(|| ExtXMediaSequence(self.media_sequence)),
(self.discontinuity_sequence != 0)
.athen(|| ExtXDiscontinuitySequence(self.discontinuity_sequence)),
self.playlist_type,
self.has_i_frames_only.athen_some(ExtXIFramesOnly),
self.has_independent_segments
.athen_some(ExtXIndependentSegments),
self.start,
self.has_end_list.athen_some(ExtXEndList),
self.segments
]
}
}
impl<'a> fmt::Display for MediaPlaylist<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "{}", ExtM3u)?;
if self.required_version() != ProtocolVersion::V1 {
writeln!(f, "{}", ExtXVersion::new(self.required_version()))?;
}
writeln!(f, "{}", ExtXTargetDuration(self.target_duration))?;
if self.media_sequence != 0 {
writeln!(f, "{}", ExtXMediaSequence(self.media_sequence))?;
}
if self.discontinuity_sequence != 0 {
writeln!(
f,
"{}",
ExtXDiscontinuitySequence(self.discontinuity_sequence)
)?;
}
if let Some(value) = &self.playlist_type {
writeln!(f, "{}", value)?;
}
if self.has_i_frames_only {
writeln!(f, "{}", ExtXIFramesOnly)?;
}
if self.has_independent_segments {
writeln!(f, "{}", ExtXIndependentSegments)?;
}
if let Some(value) = &self.start {
writeln!(f, "{}", value)?;
}
let mut available_keys = HashSet::<ExtXKey<'_>>::new();
for segment in self.segments.values() {
for key in &segment.keys {
if let ExtXKey(Some(decryption_key)) = key {
available_keys.remove(&ExtXKey::empty());
let mut decryption_key = decryption_key.clone();
let key = {
if let InitializationVector::Number(_) = decryption_key.iv {
decryption_key.iv = InitializationVector::Missing;
}
ExtXKey(Some(decryption_key.clone()))
};
if available_keys.insert(key.clone()) {
let mut remove_key = None;
for k in &available_keys {
if let ExtXKey(Some(dk)) = k {
if dk.format == decryption_key.format && key != *k {
remove_key = Some(k.clone());
break;
}
} else {
unreachable!("empty keys should not exist in `available_keys`");
}
}
if let Some(k) = remove_key {
let res = available_keys.remove(&k);
debug_assert!(res);
}
writeln!(f, "{}", key)?;
}
} else {
available_keys.clear();
available_keys.insert(ExtXKey::empty());
writeln!(f, "{}", key)?;
}
}
write!(f, "{}", segment)?;
}
for value in &self.unknown {
writeln!(f, "{}", value)?;
}
if self.has_end_list {
writeln!(f, "{}", ExtXEndList)?;
}
Ok(())
}
}
fn parse_media_playlist<'a>(
input: &'a str,
builder: &mut MediaPlaylistBuilder<'a>,
) -> crate::Result<MediaPlaylist<'a>> {
let input = tag(input, "#EXTM3U")?;
let mut segment = MediaSegment::builder();
let mut segments = vec![];
let mut has_partial_segment = false;
let mut has_discontinuity_tag = false;
let mut unknown = vec![];
let mut available_keys = HashSet::new();
for line in Lines::from(input) {
match line? {
Line::Tag(tag) => {
match tag {
Tag::ExtInf(t) => {
has_partial_segment = true;
segment.duration(t);
}
Tag::ExtXByteRange(t) => {
has_partial_segment = true;
segment.byte_range(t);
}
Tag::ExtXDiscontinuity(_) => {
has_discontinuity_tag = true;
has_partial_segment = true;
segment.has_discontinuity(true);
}
Tag::ExtXKey(key) => {
has_partial_segment = true;
let mut is_new_key = true;
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 = Some(old_key.clone());
break;
}
} else {
remove = Some(ExtXKey::empty());
break;
}
}
} else {
available_keys.clear();
available_keys.insert(ExtXKey::empty());
is_new_key = false;
}
if let Some(key) = &remove {
available_keys.remove(key);
}
if is_new_key {
available_keys.insert(key);
}
}
Tag::ExtXMap(mut t) => {
has_partial_segment = true;
t.keys = available_keys.iter().cloned().collect();
segment.map(t);
}
Tag::ExtXProgramDateTime(t) => {
has_partial_segment = true;
segment.program_date_time(t);
}
Tag::ExtXDateRange(t) => {
has_partial_segment = true;
segment.date_range(t);
}
Tag::ExtXTargetDuration(t) => {
builder.target_duration(t.0);
}
Tag::ExtXMediaSequence(t) => {
builder.media_sequence(t.0);
}
Tag::ExtXDiscontinuitySequence(t) => {
if !segments.is_empty() {
return Err(Error::custom("discontinuity sequence tag must appear before the first media segment in the playlist"));
}
if has_discontinuity_tag {
return Err(Error::custom("discontinuity sequence tag must appear before any `ExtXDiscontinuity` tag"));
}
builder.discontinuity_sequence(t.0);
}
Tag::ExtXEndList(_) => {
builder.has_end_list(true);
}
Tag::PlaylistType(t) => {
builder.playlist_type(t);
}
Tag::ExtXIFramesOnly(_) => {
builder.has_i_frames_only(true);
}
Tag::ExtXMedia(_)
| Tag::VariantStream(_)
| Tag::ExtXSessionData(_)
| Tag::ExtXSessionKey(_) => {
return Err(Error::unexpected_tag(tag));
}
Tag::ExtXIndependentSegments(_) => {
builder.has_independent_segments(true);
}
Tag::ExtXStart(t) => {
builder.start(t);
}
Tag::ExtXVersion(_) => {}
Tag::Unknown(s) => {
unknown.push(Cow::Borrowed(s));
}
}
}
Line::Uri(uri) => {
segment.uri(uri);
segment.keys(available_keys.iter().cloned().collect::<Vec<_>>());
segments.push(segment.build().map_err(Error::builder)?);
segment = MediaSegment::builder();
has_partial_segment = false;
}
Line::Comment(_) => {}
}
}
if has_partial_segment {
return Err(Error::custom("Missing URI for the last `MediaSegment`"));
}
builder.unknown(unknown);
builder.segments(segments);
builder.build().map_err(Error::builder)
}
impl FromStr for MediaPlaylist<'static> {
type Err = Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
Ok(parse_media_playlist(input, &mut MediaPlaylist::builder())?.into_owned())
}
}
impl<'a> TryFrom<&'a str> for MediaPlaylist<'a> {
type Error = Error;
fn try_from(input: &'a str) -> Result<Self, Self::Error> {
parse_media_playlist(input, &mut Self::builder())
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn too_large_segment_duration_test() {
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"
);
assert!(MediaPlaylist::try_from(playlist).is_err());
assert!(MediaPlaylist::builder()
.allowable_excess_duration(Duration::from_secs(1))
.parse(playlist)
.is_err());
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()
.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(),
])
.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()
.unwrap();
let mut segments = playlist.segments.into_iter().map(|(k, v)| (k, v.number));
assert_eq!(segments.next(), Some((0, 2680)));
assert_eq!(segments.next(), Some((1, 2681)));
assert_eq!(segments.next(), Some((2, 2682)));
assert_eq!(segments.next(), None);
}
#[test]
fn test_empty_playlist() {
let playlist = "";
assert!(MediaPlaylist::try_from(playlist).is_err());
}
}