mirror of
https://github.com/sile/hls_m3u8.git
synced 2024-11-25 08:31:00 +00:00
added master_playlist builder
This commit is contained in:
parent
a2614b5aca
commit
b1aa512679
5 changed files with 177 additions and 232 deletions
|
@ -18,6 +18,7 @@ codecov = {repository = "sile/hls_m3u8"}
|
||||||
[dependencies]
|
[dependencies]
|
||||||
getset = "0.0.8"
|
getset = "0.0.8"
|
||||||
failure = "0.1.5"
|
failure = "0.1.5"
|
||||||
|
derive_builder = "0.7.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
clap = "2"
|
clap = "2"
|
||||||
|
|
|
@ -55,6 +55,9 @@ pub enum ErrorKind {
|
||||||
)]
|
)]
|
||||||
VersionError(String, String),
|
VersionError(String, String),
|
||||||
|
|
||||||
|
#[fail(display = "BuilderError: {}", _0)]
|
||||||
|
BuilderError(String),
|
||||||
|
|
||||||
/// Hints that destructuring should not be exhaustive.
|
/// Hints that destructuring should not be exhaustive.
|
||||||
///
|
///
|
||||||
/// This enum may grow additional variants, so this makes sure clients
|
/// This enum may grow additional variants, so this makes sure clients
|
||||||
|
@ -188,6 +191,10 @@ impl Error {
|
||||||
specified_version.to_string(),
|
specified_version.to_string(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn builder_error<T: ToString>(value: T) -> Self {
|
||||||
|
Self::from(ErrorKind::BuilderError(value.to_string()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<std::num::ParseIntError> for Error {
|
impl From<std::num::ParseIntError> for Error {
|
||||||
|
|
|
@ -1,212 +1,21 @@
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::iter;
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use derive_builder::Builder;
|
||||||
|
|
||||||
use crate::line::{Line, Lines, Tag};
|
use crate::line::{Line, Lines, Tag};
|
||||||
use crate::tags::{
|
use crate::tags::{
|
||||||
ExtM3u, ExtXIFrameStreamInf, ExtXIndependentSegments, ExtXMedia, ExtXSessionData,
|
ExtM3u, ExtXIFrameStreamInf, ExtXIndependentSegments, ExtXMedia, ExtXSessionData,
|
||||||
ExtXSessionKey, ExtXStart, ExtXStreamInf, ExtXVersion, MasterPlaylistTag,
|
ExtXSessionKey, ExtXStart, ExtXStreamInf, ExtXVersion,
|
||||||
};
|
};
|
||||||
use crate::types::{ClosedCaptions, MediaType, ProtocolVersion};
|
use crate::types::{ClosedCaptions, MediaType, ProtocolVersion};
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
|
||||||
/// Master playlist builder.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct MasterPlaylistBuilder {
|
|
||||||
version: Option<ProtocolVersion>,
|
|
||||||
independent_segments_tag: Option<ExtXIndependentSegments>,
|
|
||||||
start_tag: Option<ExtXStart>,
|
|
||||||
media_tags: Vec<ExtXMedia>,
|
|
||||||
stream_inf_tags: Vec<ExtXStreamInf>,
|
|
||||||
i_frame_stream_inf_tags: Vec<ExtXIFrameStreamInf>,
|
|
||||||
session_data_tags: Vec<ExtXSessionData>,
|
|
||||||
session_key_tags: Vec<ExtXSessionKey>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MasterPlaylistBuilder {
|
|
||||||
/// Makes a new `MasterPlaylistBuilder` instance.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
MasterPlaylistBuilder {
|
|
||||||
version: None,
|
|
||||||
independent_segments_tag: None,
|
|
||||||
start_tag: None,
|
|
||||||
media_tags: Vec::new(),
|
|
||||||
stream_inf_tags: Vec::new(),
|
|
||||||
i_frame_stream_inf_tags: Vec::new(),
|
|
||||||
session_data_tags: Vec::new(),
|
|
||||||
session_key_tags: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the protocol compatibility version of the resulting playlist.
|
|
||||||
///
|
|
||||||
/// If the resulting playlist has tags which requires a compatibility version greater than `version`,
|
|
||||||
/// `finish()` method will fail with an `ErrorKind::InvalidInput` error.
|
|
||||||
///
|
|
||||||
/// The default is the maximum version among the tags in the playlist.
|
|
||||||
pub fn version(&mut self, version: ProtocolVersion) -> &mut Self {
|
|
||||||
self.version = Some(version);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Adds the given tag to the resulting playlist.
|
|
||||||
///
|
|
||||||
/// If it is forbidden to have multiple instance of the tag, the existing one will be overwritten.
|
|
||||||
pub fn tag<T: Into<MasterPlaylistTag>>(&mut self, tag: T) -> &mut Self {
|
|
||||||
match tag.into() {
|
|
||||||
MasterPlaylistTag::ExtXIndependentSegments(t) => {
|
|
||||||
self.independent_segments_tag = Some(t);
|
|
||||||
}
|
|
||||||
MasterPlaylistTag::ExtXStart(t) => self.start_tag = Some(t),
|
|
||||||
MasterPlaylistTag::ExtXMedia(t) => self.media_tags.push(t),
|
|
||||||
MasterPlaylistTag::ExtXStreamInf(t) => self.stream_inf_tags.push(t),
|
|
||||||
MasterPlaylistTag::ExtXIFrameStreamInf(t) => self.i_frame_stream_inf_tags.push(t),
|
|
||||||
MasterPlaylistTag::ExtXSessionData(t) => self.session_data_tags.push(t),
|
|
||||||
MasterPlaylistTag::ExtXSessionKey(t) => self.session_key_tags.push(t),
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds a `MasterPlaylist` instance.
|
|
||||||
pub fn finish(self) -> crate::Result<MasterPlaylist> {
|
|
||||||
let required_version = self.required_version();
|
|
||||||
let specified_version = self.version.unwrap_or(required_version);
|
|
||||||
|
|
||||||
if required_version < specified_version {
|
|
||||||
return Err(Error::required_version(required_version, specified_version));
|
|
||||||
}
|
|
||||||
|
|
||||||
(self.validate_stream_inf_tags())?;
|
|
||||||
(self.validate_i_frame_stream_inf_tags())?;
|
|
||||||
(self.validate_session_data_tags())?;
|
|
||||||
(self.validate_session_key_tags())?;
|
|
||||||
|
|
||||||
Ok(MasterPlaylist {
|
|
||||||
version_tag: ExtXVersion::new(specified_version),
|
|
||||||
independent_segments_tag: self.independent_segments_tag,
|
|
||||||
start_tag: self.start_tag,
|
|
||||||
media_tags: self.media_tags,
|
|
||||||
stream_inf_tags: self.stream_inf_tags,
|
|
||||||
i_frame_stream_inf_tags: self.i_frame_stream_inf_tags,
|
|
||||||
session_data_tags: self.session_data_tags,
|
|
||||||
session_key_tags: self.session_key_tags,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn required_version(&self) -> ProtocolVersion {
|
|
||||||
iter::empty()
|
|
||||||
.chain(
|
|
||||||
self.independent_segments_tag
|
|
||||||
.iter()
|
|
||||||
.map(|t| t.requires_version()),
|
|
||||||
)
|
|
||||||
.chain(self.start_tag.iter().map(|t| t.requires_version()))
|
|
||||||
.chain(self.media_tags.iter().map(|t| t.requires_version()))
|
|
||||||
.chain(self.stream_inf_tags.iter().map(|t| t.requires_version()))
|
|
||||||
.chain(
|
|
||||||
self.i_frame_stream_inf_tags
|
|
||||||
.iter()
|
|
||||||
.map(|t| t.requires_version()),
|
|
||||||
)
|
|
||||||
.chain(self.session_data_tags.iter().map(|t| t.requires_version()))
|
|
||||||
.chain(self.session_key_tags.iter().map(|t| t.requires_version()))
|
|
||||||
.max()
|
|
||||||
.expect("Never fails")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_stream_inf_tags(&self) -> crate::Result<()> {
|
|
||||||
let mut has_none_closed_captions = false;
|
|
||||||
for t in &self.stream_inf_tags {
|
|
||||||
if let Some(group_id) = t.audio() {
|
|
||||||
if !self.check_media_group(MediaType::Audio, group_id) {
|
|
||||||
return Err(Error::unmatched_group(group_id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(group_id) = t.video() {
|
|
||||||
if !self.check_media_group(MediaType::Video, group_id) {
|
|
||||||
return Err(Error::unmatched_group(group_id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(group_id) = t.subtitles() {
|
|
||||||
if !self.check_media_group(MediaType::Subtitles, group_id) {
|
|
||||||
return Err(Error::unmatched_group(group_id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
match t.closed_captions() {
|
|
||||||
Some(&ClosedCaptions::GroupId(ref group_id)) => {
|
|
||||||
if !self.check_media_group(MediaType::ClosedCaptions, group_id) {
|
|
||||||
return Err(Error::unmatched_group(group_id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(&ClosedCaptions::None) => {
|
|
||||||
has_none_closed_captions = true;
|
|
||||||
}
|
|
||||||
None => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if has_none_closed_captions {
|
|
||||||
if !self
|
|
||||||
.stream_inf_tags
|
|
||||||
.iter()
|
|
||||||
.all(|t| t.closed_captions() == Some(&ClosedCaptions::None))
|
|
||||||
{
|
|
||||||
return Err(Error::invalid_input());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_i_frame_stream_inf_tags(&self) -> crate::Result<()> {
|
|
||||||
for t in &self.i_frame_stream_inf_tags {
|
|
||||||
if let Some(group_id) = t.video() {
|
|
||||||
if !self.check_media_group(MediaType::Video, group_id) {
|
|
||||||
return Err(Error::unmatched_group(group_id));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_session_data_tags(&self) -> crate::Result<()> {
|
|
||||||
let mut set = HashSet::new();
|
|
||||||
for t in &self.session_data_tags {
|
|
||||||
if !set.insert((t.data_id(), t.language())) {
|
|
||||||
return Err(Error::custom(format!("Conflict: {}", t)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_session_key_tags(&self) -> crate::Result<()> {
|
|
||||||
let mut set = HashSet::new();
|
|
||||||
for t in &self.session_key_tags {
|
|
||||||
if !set.insert(t.key()) {
|
|
||||||
return Err(Error::custom(format!("Conflict: {}", t)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_media_group<T: ToString>(&self, media_type: MediaType, group_id: T) -> bool {
|
|
||||||
self.media_tags
|
|
||||||
.iter()
|
|
||||||
.any(|t| t.media_type() == media_type && t.group_id() == &group_id.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for MasterPlaylistBuilder {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Master playlist.
|
/// Master playlist.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Builder, Default)]
|
||||||
|
#[builder(build_fn(validate = "Self::validate"))]
|
||||||
|
#[builder(setter(into, strip_option), default)]
|
||||||
pub struct MasterPlaylist {
|
pub struct MasterPlaylist {
|
||||||
version_tag: ExtXVersion,
|
version_tag: ExtXVersion,
|
||||||
independent_segments_tag: Option<ExtXIndependentSegments>,
|
independent_segments_tag: Option<ExtXIndependentSegments>,
|
||||||
|
@ -219,6 +28,11 @@ pub struct MasterPlaylist {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MasterPlaylist {
|
impl MasterPlaylist {
|
||||||
|
/// Returns a Builder for a MasterPlaylist.
|
||||||
|
pub fn builder() -> MasterPlaylistBuilder {
|
||||||
|
MasterPlaylistBuilder::default()
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the `EXT-X-VERSION` tag contained in the playlist.
|
/// Returns the `EXT-X-VERSION` tag contained in the playlist.
|
||||||
pub const fn version_tag(&self) -> ExtXVersion {
|
pub const fn version_tag(&self) -> ExtXVersion {
|
||||||
self.version_tag
|
self.version_tag
|
||||||
|
@ -260,6 +74,95 @@ impl MasterPlaylist {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl MasterPlaylistBuilder {
|
||||||
|
pub(crate) fn validate(&self) -> Result<(), String> {
|
||||||
|
// validate stream inf tags
|
||||||
|
if let Some(stream_inf_tags) = &self.stream_inf_tags {
|
||||||
|
let mut has_none_closed_captions = false;
|
||||||
|
for t in stream_inf_tags {
|
||||||
|
if let Some(group_id) = t.audio() {
|
||||||
|
if !self.check_media_group(MediaType::Audio, group_id) {
|
||||||
|
return Err(Error::unmatched_group(group_id).to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(group_id) = t.video() {
|
||||||
|
if !self.check_media_group(MediaType::Video, group_id) {
|
||||||
|
return Err(Error::unmatched_group(group_id).to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(group_id) = t.subtitles() {
|
||||||
|
if !self.check_media_group(MediaType::Subtitles, group_id) {
|
||||||
|
return Err(Error::unmatched_group(group_id).to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match t.closed_captions() {
|
||||||
|
Some(&ClosedCaptions::GroupId(ref group_id)) => {
|
||||||
|
if !self.check_media_group(MediaType::ClosedCaptions, group_id) {
|
||||||
|
return Err(Error::unmatched_group(group_id).to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(&ClosedCaptions::None) => {
|
||||||
|
has_none_closed_captions = true;
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if has_none_closed_captions {
|
||||||
|
if !stream_inf_tags
|
||||||
|
.iter()
|
||||||
|
.all(|t| t.closed_captions() == Some(&ClosedCaptions::None))
|
||||||
|
{
|
||||||
|
return Err(Error::invalid_input().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate i_frame_stream_inf_tags
|
||||||
|
if let Some(i_frame_stream_inf_tags) = &self.i_frame_stream_inf_tags {
|
||||||
|
for t in i_frame_stream_inf_tags {
|
||||||
|
if let Some(group_id) = t.video() {
|
||||||
|
if !self.check_media_group(MediaType::Video, group_id) {
|
||||||
|
return Err(Error::unmatched_group(group_id).to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate session_data_tags
|
||||||
|
if let Some(session_data_tags) = &self.session_data_tags {
|
||||||
|
let mut set = HashSet::new();
|
||||||
|
|
||||||
|
for t in session_data_tags {
|
||||||
|
if !set.insert((t.data_id(), t.language())) {
|
||||||
|
return Err(Error::custom(format!("Conflict: {}", t)).to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate session_key_tags
|
||||||
|
if let Some(session_key_tags) = &self.session_key_tags {
|
||||||
|
let mut set = HashSet::new();
|
||||||
|
for t in session_key_tags {
|
||||||
|
if !set.insert(t.key()) {
|
||||||
|
return Err(Error::custom(format!("Conflict: {}", t)).to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_media_group<T: ToString>(&self, media_type: MediaType, group_id: T) -> bool {
|
||||||
|
if let Some(media_tags) = &self.media_tags {
|
||||||
|
media_tags
|
||||||
|
.iter()
|
||||||
|
.any(|t| t.media_type() == media_type && t.group_id() == &group_id.to_string())
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Display for MasterPlaylist {
|
impl fmt::Display for MasterPlaylist {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
writeln!(f, "{}", ExtM3u)?;
|
writeln!(f, "{}", ExtM3u)?;
|
||||||
|
@ -295,7 +198,14 @@ impl FromStr for MasterPlaylist {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
|
|
||||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||||
let mut builder = MasterPlaylistBuilder::new();
|
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![];
|
||||||
|
|
||||||
for (i, line) in input.parse::<Lines>()?.into_iter().enumerate() {
|
for (i, line) in input.parse::<Lines>()?.into_iter().enumerate() {
|
||||||
match line {
|
match line {
|
||||||
Line::Tag(tag) => {
|
Line::Tag(tag) => {
|
||||||
|
@ -310,10 +220,7 @@ impl FromStr for MasterPlaylist {
|
||||||
return Err(Error::invalid_input());
|
return Err(Error::invalid_input());
|
||||||
}
|
}
|
||||||
Tag::ExtXVersion(t) => {
|
Tag::ExtXVersion(t) => {
|
||||||
if builder.version.is_some() {
|
builder.version_tag(t.version());
|
||||||
return Err(Error::invalid_input());
|
|
||||||
}
|
|
||||||
builder.version(t.version());
|
|
||||||
}
|
}
|
||||||
Tag::ExtInf(_)
|
Tag::ExtInf(_)
|
||||||
| Tag::ExtXByteRange(_)
|
| Tag::ExtXByteRange(_)
|
||||||
|
@ -331,31 +238,25 @@ impl FromStr for MasterPlaylist {
|
||||||
return Err(Error::invalid_input()); // TODO: why?
|
return Err(Error::invalid_input()); // TODO: why?
|
||||||
}
|
}
|
||||||
Tag::ExtXMedia(t) => {
|
Tag::ExtXMedia(t) => {
|
||||||
builder.tag(t);
|
media_tags.push(t);
|
||||||
}
|
}
|
||||||
Tag::ExtXStreamInf(t) => {
|
Tag::ExtXStreamInf(t) => {
|
||||||
builder.tag(t);
|
stream_inf_tags.push(t);
|
||||||
}
|
}
|
||||||
Tag::ExtXIFrameStreamInf(t) => {
|
Tag::ExtXIFrameStreamInf(t) => {
|
||||||
builder.tag(t);
|
i_frame_stream_inf_tags.push(t);
|
||||||
}
|
}
|
||||||
Tag::ExtXSessionData(t) => {
|
Tag::ExtXSessionData(t) => {
|
||||||
builder.tag(t);
|
session_data_tags.push(t);
|
||||||
}
|
}
|
||||||
Tag::ExtXSessionKey(t) => {
|
Tag::ExtXSessionKey(t) => {
|
||||||
builder.tag(t);
|
session_key_tags.push(t);
|
||||||
}
|
}
|
||||||
Tag::ExtXIndependentSegments(t) => {
|
Tag::ExtXIndependentSegments(t) => {
|
||||||
if builder.independent_segments_tag.is_some() {
|
builder.independent_segments_tag(t);
|
||||||
return Err(Error::invalid_input());
|
|
||||||
}
|
|
||||||
builder.tag(t);
|
|
||||||
}
|
}
|
||||||
Tag::ExtXStart(t) => {
|
Tag::ExtXStart(t) => {
|
||||||
if builder.start_tag.is_some() {
|
builder.start_tag(t);
|
||||||
return Err(Error::invalid_input());
|
|
||||||
}
|
|
||||||
builder.tag(t);
|
|
||||||
}
|
}
|
||||||
Tag::Unknown(_) => {
|
Tag::Unknown(_) => {
|
||||||
// [6.3.1. General Client Responsibilities]
|
// [6.3.1. General Client Responsibilities]
|
||||||
|
@ -369,7 +270,14 @@ impl FromStr for MasterPlaylist {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
builder.finish()
|
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -379,20 +287,37 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parser() {
|
fn test_parser() {
|
||||||
let playlist = r#"
|
r#"#EXTM3U
|
||||||
#EXTM3U
|
#EXT-X-STREAM-INF:BANDWIDTH=150000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=150000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
|
http://example.com/low/index.m3u8
|
||||||
http://example.com/low/index.m3u8
|
#EXT-X-STREAM-INF:BANDWIDTH=240000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=240000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
|
http://example.com/lo_mid/index.m3u8
|
||||||
http://example.com/lo_mid/index.m3u8
|
#EXT-X-STREAM-INF:BANDWIDTH=440000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=440000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
|
http://example.com/hi_mid/index.m3u8
|
||||||
http://example.com/hi_mid/index.m3u8
|
#EXT-X-STREAM-INF:BANDWIDTH=640000,RESOLUTION=640x360,CODECS="avc1.42e00a,mp4a.40.2"
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=640000,RESOLUTION=640x360,CODECS="avc1.42e00a,mp4a.40.2"
|
http://example.com/high/index.m3u8
|
||||||
http://example.com/high/index.m3u8
|
#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.5"
|
||||||
#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.5"
|
http://example.com/audio/index.m3u8
|
||||||
http://example.com/audio/index.m3u8
|
"#
|
||||||
"#
|
|
||||||
.parse::<MasterPlaylist>()
|
.parse::<MasterPlaylist>()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_display() {
|
||||||
|
let input = r#"#EXTM3U
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=150000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
|
||||||
|
http://example.com/low/index.m3u8
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=240000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
|
||||||
|
http://example.com/lo_mid/index.m3u8
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=440000,RESOLUTION=416x234,CODECS="avc1.42e00a,mp4a.40.2"
|
||||||
|
http://example.com/hi_mid/index.m3u8
|
||||||
|
#EXT-X-STREAM-INF:BANDWIDTH=640000,RESOLUTION=640x360,CODECS="avc1.42e00a,mp4a.40.2"
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,18 @@ impl fmt::Display for ExtXVersion {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for ExtXVersion {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(ProtocolVersion::V1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ProtocolVersion> for ExtXVersion {
|
||||||
|
fn from(value: ProtocolVersion) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXVersion {
|
impl FromStr for ExtXVersion {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
|
|
||||||
|
|
|
@ -115,12 +115,12 @@ impl fmt::Display for ExtXStreamInf {
|
||||||
if let Some(ref x) = self.average_bandwidth {
|
if let Some(ref x) = self.average_bandwidth {
|
||||||
write!(f, ",AVERAGE-BANDWIDTH={}", x)?;
|
write!(f, ",AVERAGE-BANDWIDTH={}", x)?;
|
||||||
}
|
}
|
||||||
if let Some(ref x) = self.codecs {
|
|
||||||
write!(f, ",CODECS={}", quote(x))?;
|
|
||||||
}
|
|
||||||
if let Some(ref x) = self.resolution {
|
if let Some(ref x) = self.resolution {
|
||||||
write!(f, ",RESOLUTION={}", x)?;
|
write!(f, ",RESOLUTION={}", x)?;
|
||||||
}
|
}
|
||||||
|
if let Some(ref x) = self.codecs {
|
||||||
|
write!(f, ",CODECS={}", quote(x))?;
|
||||||
|
}
|
||||||
if let Some(ref x) = self.frame_rate {
|
if let Some(ref x) = self.frame_rate {
|
||||||
write!(f, ",FRAME-RATE={:.3}", x.as_f64())?;
|
write!(f, ",FRAME-RATE={:.3}", x.as_f64())?;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue