1
0
Fork 0
mirror of https://github.com/sile/hls_m3u8.git synced 2024-05-17 16:02:59 +00:00
hls_m3u8/src/master_playlist.rs

410 lines
14 KiB
Rust
Raw Normal View History

2019-09-13 14:06:52 +00:00
use std::collections::HashSet;
use std::fmt;
2019-09-14 19:08:35 +00:00
use std::iter;
2019-09-13 14:06:52 +00:00
use std::str::FromStr;
2019-09-14 11:26:16 +00:00
use derive_builder::Builder;
2019-03-31 09:58:11 +00:00
use crate::line::{Line, Lines, Tag};
use crate::tags::{
ExtM3u, ExtXIFrameStreamInf, ExtXIndependentSegments, ExtXMedia, ExtXSessionData,
2019-09-14 11:26:16 +00:00
ExtXSessionKey, ExtXStart, ExtXStreamInf, ExtXVersion,
2019-03-31 09:58:11 +00:00
};
2019-09-22 08:57:28 +00:00
use crate::types::{ClosedCaptions, MediaType, ProtocolVersion, RequiredVersion};
2019-09-14 09:57:56 +00:00
use crate::Error;
2018-02-14 01:31:24 +00:00
2018-02-14 17:52:56 +00:00
/// Master playlist.
2019-09-14 19:08:35 +00:00
#[derive(Debug, Clone, Builder)]
2019-09-14 11:26:16 +00:00
#[builder(build_fn(validate = "Self::validate"))]
2019-09-14 19:08:35 +00:00
#[builder(setter(into, strip_option))]
2018-02-14 01:31:24 +00:00
pub struct MasterPlaylist {
2019-09-14 19:08:35 +00:00
#[builder(default, setter(name = "version"))]
/// 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.
2018-02-14 17:52:56 +00:00
version_tag: ExtXVersion,
2019-09-14 19:08:35 +00:00
#[builder(default)]
/// Sets the [ExtXIndependentSegments] tag.
2018-02-14 17:52:56 +00:00
independent_segments_tag: Option<ExtXIndependentSegments>,
2019-09-14 19:08:35 +00:00
#[builder(default)]
/// Sets the [ExtXStart] tag.
2018-02-14 17:52:56 +00:00
start_tag: Option<ExtXStart>,
2019-09-14 19:08:35 +00:00
/// Sets the [ExtXMedia] tag.
2018-02-14 17:52:56 +00:00
media_tags: Vec<ExtXMedia>,
2019-09-14 19:08:35 +00:00
/// Sets all [ExtXStreamInf]s.
2018-02-14 17:52:56 +00:00
stream_inf_tags: Vec<ExtXStreamInf>,
2019-09-14 19:08:35 +00:00
/// Sets all [ExtXIFrameStreamInf]s.
2018-02-14 17:52:56 +00:00
i_frame_stream_inf_tags: Vec<ExtXIFrameStreamInf>,
2019-09-14 19:08:35 +00:00
/// Sets all [ExtXSessionData]s.
2018-02-14 17:52:56 +00:00
session_data_tags: Vec<ExtXSessionData>,
2019-09-14 19:08:35 +00:00
/// Sets all [ExtXSessionKey]s.
2018-02-14 17:52:56 +00:00
session_key_tags: Vec<ExtXSessionKey>,
}
2019-09-08 09:30:52 +00:00
2018-02-14 17:52:56 +00:00
impl MasterPlaylist {
2019-09-14 11:26:16 +00:00
/// Returns a Builder for a MasterPlaylist.
pub fn builder() -> MasterPlaylistBuilder {
MasterPlaylistBuilder::default()
}
2018-02-14 17:52:56 +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 17:52:56 +00:00
self.version_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 17:52:56 +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 17:52:56 +00:00
self.start_tag
}
/// Returns the `EXT-X-MEDIA` tags contained in the playlist.
pub fn media_tags(&self) -> &[ExtXMedia] {
&self.media_tags
}
/// Returns the `EXT-X-STREAM-INF` tags contained in the playlist.
pub fn stream_inf_tags(&self) -> &[ExtXStreamInf] {
&self.stream_inf_tags
}
/// Returns the `EXT-X-I-FRAME-STREAM-INF` tags contained in the playlist.
pub fn i_frame_stream_inf_tags(&self) -> &[ExtXIFrameStreamInf] {
2018-02-14 17:52:56 +00:00
&self.i_frame_stream_inf_tags
}
/// Returns the `EXT-X-SESSION-DATA` tags contained in the playlist.
pub fn session_data_tags(&self) -> &[ExtXSessionData] {
&self.session_data_tags
}
/// Returns the `EXT-X-SESSION-KEY` tags contained in the playlist.
pub fn session_key_tags(&self) -> &[ExtXSessionKey] {
&self.session_key_tags
}
2018-02-14 01:31:24 +00:00
}
2019-09-08 09:30:52 +00:00
2019-09-22 08:57:28 +00:00
impl RequiredVersion for MasterPlaylist {
fn required_version(&self) -> ProtocolVersion {
self.version_tag.version()
}
}
2019-09-14 11:26:16 +00:00
impl MasterPlaylistBuilder {
2019-09-14 19:08:35 +00:00
fn validate(&self) -> Result<(), String> {
let required_version = self.required_version();
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 {
return Err(Error::required_version(required_version, specified_version).to_string());
}
self.validate_stream_inf_tags().map_err(|e| e.to_string())?;
self.validate_i_frame_stream_inf_tags()
.map_err(|e| e.to_string())?;
self.validate_session_data_tags()
.map_err(|e| e.to_string())?;
Ok(())
}
fn required_version(&self) -> ProtocolVersion {
iter::empty()
.chain(
self.independent_segments_tag
.iter()
2019-09-22 08:57:28 +00:00
.map(|t| t.iter().map(|t| t.required_version()))
2019-09-14 19:08:35 +00:00
.flatten(),
)
.chain(
self.start_tag
.iter()
2019-09-22 08:57:28 +00:00
.map(|t| t.iter().map(|t| t.required_version()))
2019-09-14 19:08:35 +00:00
.flatten(),
)
.chain(
self.media_tags
.iter()
2019-09-22 08:57:28 +00:00
.map(|t| t.iter().map(|t| t.required_version()))
2019-09-14 19:08:35 +00:00
.flatten(),
)
.chain(
self.stream_inf_tags
.iter()
2019-09-22 08:57:28 +00:00
.map(|t| t.iter().map(|t| t.required_version()))
2019-09-14 19:08:35 +00:00
.flatten(),
)
.chain(
self.i_frame_stream_inf_tags
.iter()
2019-09-22 08:57:28 +00:00
.map(|t| t.iter().map(|t| t.required_version()))
2019-09-14 19:08:35 +00:00
.flatten(),
)
.chain(
self.session_data_tags
.iter()
2019-09-22 08:57:28 +00:00
.map(|t| t.iter().map(|t| t.required_version()))
2019-09-14 19:08:35 +00:00
.flatten(),
)
.chain(
self.session_key_tags
.iter()
2019-09-22 08:57:28 +00:00
.map(|t| t.iter().map(|t| t.required_version()))
2019-09-14 19:08:35 +00:00
.flatten(),
)
.max()
2019-09-22 18:33:40 +00:00
.unwrap_or_else(ProtocolVersion::latest)
2019-09-14 19:08:35 +00:00
}
fn validate_stream_inf_tags(&self) -> crate::Result<()> {
if let Some(value) = &self.stream_inf_tags {
2019-09-14 11:26:16 +00:00
let mut has_none_closed_captions = false;
2019-09-14 19:08:35 +00:00
for t in value {
2019-09-14 11:26:16 +00:00
if let Some(group_id) = t.audio() {
if !self.check_media_group(MediaType::Audio, group_id) {
2019-09-14 19:08:35 +00:00
return Err(Error::unmatched_group(group_id));
2019-09-14 11:26:16 +00:00
}
}
if let Some(group_id) = t.video() {
if !self.check_media_group(MediaType::Video, group_id) {
2019-09-14 19:08:35 +00:00
return Err(Error::unmatched_group(group_id));
2019-09-14 11:26:16 +00:00
}
}
if let Some(group_id) = t.subtitles() {
if !self.check_media_group(MediaType::Subtitles, group_id) {
2019-09-14 19:08:35 +00:00
return Err(Error::unmatched_group(group_id));
2019-09-14 11:26:16 +00:00
}
}
match t.closed_captions() {
2019-09-22 18:33:40 +00:00
&Some(ClosedCaptions::GroupId(ref group_id)) => {
2019-09-14 11:26:16 +00:00
if !self.check_media_group(MediaType::ClosedCaptions, group_id) {
2019-09-14 19:08:35 +00:00
return Err(Error::unmatched_group(group_id));
2019-09-14 11:26:16 +00:00
}
}
2019-09-22 18:33:40 +00:00
&Some(ClosedCaptions::None) => {
2019-09-14 11:26:16 +00:00
has_none_closed_captions = true;
}
None => {}
}
}
2019-09-22 18:33:40 +00:00
if has_none_closed_captions
&& !value
2019-09-14 11:26:16 +00:00
.iter()
2019-09-22 18:33:40 +00:00
.all(|t| t.closed_captions() == &Some(ClosedCaptions::None))
{
return Err(Error::invalid_input());
2019-09-14 11:26:16 +00:00
}
}
2019-09-14 19:08:35 +00:00
Ok(())
}
2019-09-14 11:26:16 +00:00
2019-09-14 19:08:35 +00:00
fn validate_i_frame_stream_inf_tags(&self) -> crate::Result<()> {
if let Some(value) = &self.i_frame_stream_inf_tags {
for t in value {
2019-09-14 11:26:16 +00:00
if let Some(group_id) = t.video() {
if !self.check_media_group(MediaType::Video, group_id) {
2019-09-14 19:08:35 +00:00
return Err(Error::unmatched_group(group_id));
2019-09-14 11:26:16 +00:00
}
}
}
}
2019-09-14 19:08:35 +00:00
Ok(())
}
2019-09-14 11:26:16 +00:00
2019-09-14 19:08:35 +00:00
fn validate_session_data_tags(&self) -> crate::Result<()> {
let mut set = HashSet::new();
if let Some(value) = &self.session_data_tags {
for t in value {
2019-09-14 11:26:16 +00:00
if !set.insert((t.data_id(), t.language())) {
2019-09-14 19:08:35 +00:00
return Err(Error::custom(format!("Conflict: {}", t)));
2019-09-14 11:26:16 +00:00
}
}
}
2019-09-14 19:08:35 +00:00
Ok(())
}
2019-09-14 11:26:16 +00:00
fn check_media_group<T: ToString>(&self, media_type: MediaType, group_id: T) -> bool {
2019-09-14 19:08:35 +00:00
if let Some(value) = &self.media_tags {
value
2019-09-14 11:26:16 +00:00
.iter()
.any(|t| t.media_type() == media_type && t.group_id() == &group_id.to_string())
} else {
false
}
}
}
2018-02-14 01:31:24 +00:00
impl fmt::Display for MasterPlaylist {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "{}", ExtM3u)?;
2018-02-14 17:52:56 +00:00
if self.version_tag.version() != ProtocolVersion::V1 {
writeln!(f, "{}", self.version_tag)?;
2018-02-14 01:31:24 +00:00
}
for t in &self.media_tags {
writeln!(f, "{}", t)?;
}
2018-02-14 17:52:56 +00:00
for t in &self.stream_inf_tags {
writeln!(f, "{}", t)?;
2018-02-14 01:31:24 +00:00
}
2018-02-14 17:52:56 +00:00
for t in &self.i_frame_stream_inf_tags {
2018-02-14 01:31:24 +00:00
writeln!(f, "{}", t)?;
}
for t in &self.session_data_tags {
writeln!(f, "{}", t)?;
}
2018-02-14 17:52:56 +00:00
for t in &self.session_key_tags {
2018-02-14 01:31:24 +00:00
writeln!(f, "{}", t)?;
}
2019-09-14 19:42:06 +00:00
if let Some(value) = &self.independent_segments_tag {
writeln!(f, "{}", value)?;
2018-02-14 01:31:24 +00:00
}
2019-09-14 19:42:06 +00:00
if let Some(value) = &self.start_tag {
writeln!(f, "{}", value)?;
2018-02-14 01:31:24 +00:00
}
Ok(())
}
}
2019-09-13 14:06:52 +00:00
2018-02-14 01:31:24 +00:00
impl FromStr for MasterPlaylist {
type Err = Error;
2019-09-14 09:57:56 +00:00
fn from_str(input: &str) -> Result<Self, Self::Err> {
2019-09-14 11:26:16 +00:00
let mut builder = MasterPlaylist::builder();
let mut media_tags = vec![];
let mut stream_inf_tags = vec![];
let mut i_frame_stream_inf_tags = vec![];
let mut session_data_tags = vec![];
let mut session_key_tags = vec![];
2019-09-14 09:57:56 +00:00
for (i, line) in input.parse::<Lines>()?.into_iter().enumerate() {
match line {
2018-02-14 01:31:24 +00:00
Line::Tag(tag) => {
if i == 0 {
2019-09-13 14:06:52 +00:00
if tag != Tag::ExtM3u(ExtM3u) {
return Err(Error::invalid_input());
}
2018-02-14 01:31:24 +00:00
continue;
}
match tag {
2018-02-14 17:52:56 +00:00
Tag::ExtM3u(_) => {
2019-09-13 14:06:52 +00:00
return Err(Error::invalid_input());
2018-02-14 17:52:56 +00:00
}
2018-02-14 01:31:24 +00:00
Tag::ExtXVersion(t) => {
2019-09-14 19:08:35 +00:00
builder.version(t.version());
2018-02-14 01:31:24 +00:00
}
Tag::ExtInf(_)
| Tag::ExtXByteRange(_)
| Tag::ExtXDiscontinuity(_)
| Tag::ExtXKey(_)
| Tag::ExtXMap(_)
| Tag::ExtXProgramDateTime(_)
| Tag::ExtXDateRange(_)
| Tag::ExtXTargetDuration(_)
| Tag::ExtXMediaSequence(_)
| Tag::ExtXDiscontinuitySequence(_)
| Tag::ExtXEndList(_)
| Tag::ExtXPlaylistType(_)
| Tag::ExtXIFramesOnly(_) => {
2019-09-14 19:08:35 +00:00
return Err(Error::custom(format!(
"This tag isn't allowed in a master playlist: {}",
tag
)));
2018-02-14 01:31:24 +00:00
}
Tag::ExtXMedia(t) => {
2019-09-14 11:26:16 +00:00
media_tags.push(t);
2018-02-14 01:31:24 +00:00
}
Tag::ExtXStreamInf(t) => {
2019-09-14 11:26:16 +00:00
stream_inf_tags.push(t);
2018-02-14 01:31:24 +00:00
}
Tag::ExtXIFrameStreamInf(t) => {
2019-09-14 11:26:16 +00:00
i_frame_stream_inf_tags.push(t);
2018-02-14 01:31:24 +00:00
}
Tag::ExtXSessionData(t) => {
2019-09-14 11:26:16 +00:00
session_data_tags.push(t);
2018-02-14 01:31:24 +00:00
}
Tag::ExtXSessionKey(t) => {
2019-09-14 11:26:16 +00:00
session_key_tags.push(t);
2018-02-14 01:31:24 +00:00
}
Tag::ExtXIndependentSegments(t) => {
2019-09-14 11:26:16 +00:00
builder.independent_segments_tag(t);
2018-02-14 01:31:24 +00:00
}
Tag::ExtXStart(t) => {
2019-09-14 11:26:16 +00:00
builder.start_tag(t);
2018-02-14 01:31:24 +00:00
}
2019-09-14 19:08:35 +00:00
_ => {
2018-02-14 15:50:57 +00:00
// [6.3.1. General Client Responsibilities]
// > ignore any unrecognized tags.
2019-09-13 14:06:52 +00:00
// TODO: collect custom tags
2018-02-14 15:50:57 +00:00
}
2018-02-14 01:31:24 +00:00
}
}
Line::Uri(uri) => {
2019-09-13 14:06:52 +00:00
return Err(Error::custom(format!("Unexpected URI: {:?}", uri)));
2018-02-14 01:31:24 +00:00
}
}
}
2019-09-14 11:26:16 +00:00
builder.media_tags(media_tags);
builder.stream_inf_tags(stream_inf_tags);
builder.i_frame_stream_inf_tags(i_frame_stream_inf_tags);
builder.session_data_tags(session_data_tags);
builder.session_key_tags(session_key_tags);
builder.build().map_err(Error::builder_error)
2018-02-14 01:31:24 +00:00
}
}
2019-09-14 09:57:56 +00:00
#[cfg(test)]
mod tests {
use super::*;
#[test]
2019-09-14 10:34:34 +00:00
fn test_parser() {
2019-09-14 11:26:16 +00:00
r#"#EXTM3U
2019-09-21 13:20:19 +00:00
#EXT-X-STREAM-INF:BANDWIDTH=150000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234
2019-09-14 11:26:16 +00:00
http://example.com/low/index.m3u8
2019-09-21 13:20:19 +00:00
#EXT-X-STREAM-INF:BANDWIDTH=240000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234
2019-09-14 11:26:16 +00:00
http://example.com/lo_mid/index.m3u8
2019-09-21 13:20:19 +00:00
#EXT-X-STREAM-INF:BANDWIDTH=440000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234
2019-09-14 11:26:16 +00:00
http://example.com/hi_mid/index.m3u8
2019-09-21 13:20:19 +00:00
#EXT-X-STREAM-INF:BANDWIDTH=640000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=640x360
2019-09-14 11:26:16 +00:00
http://example.com/high/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.5"
http://example.com/audio/index.m3u8
"#
2019-09-14 10:34:34 +00:00
.parse::<MasterPlaylist>()
.unwrap();
}
2019-09-14 11:26:16 +00:00
#[test]
fn test_display() {
let input = r#"#EXTM3U
2019-09-21 13:20:19 +00:00
#EXT-X-STREAM-INF:BANDWIDTH=150000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234
2019-09-14 11:26:16 +00:00
http://example.com/low/index.m3u8
2019-09-21 13:20:19 +00:00
#EXT-X-STREAM-INF:BANDWIDTH=240000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234
2019-09-14 11:26:16 +00:00
http://example.com/lo_mid/index.m3u8
2019-09-21 13:20:19 +00:00
#EXT-X-STREAM-INF:BANDWIDTH=440000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=416x234
2019-09-14 11:26:16 +00:00
http://example.com/hi_mid/index.m3u8
2019-09-21 13:20:19 +00:00
#EXT-X-STREAM-INF:BANDWIDTH=640000,CODECS="avc1.42e00a,mp4a.40.2",RESOLUTION=640x360
2019-09-14 11:26:16 +00:00
http://example.com/high/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.5"
http://example.com/audio/index.m3u8
"#;
let playlist = input.parse::<MasterPlaylist>().unwrap();
assert_eq!(playlist.to_string(), input);
}
2019-09-14 09:57:56 +00:00
}