mirror of
https://github.com/sile/hls_m3u8.git
synced 2024-11-29 02:00:59 +00:00
Add tag::master_playlist
module
This commit is contained in:
parent
27d152fc53
commit
93f6db8904
6 changed files with 1072 additions and 632 deletions
|
@ -141,8 +141,9 @@ impl FromStr for HexadecimalSequence {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: delete
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub struct DecimalInteger(u64);
|
pub struct DecimalInteger(pub u64);
|
||||||
impl fmt::Display for DecimalInteger {
|
impl fmt::Display for DecimalInteger {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(f, "{}", self.0)
|
write!(f, "{}", self.0)
|
||||||
|
@ -168,6 +169,9 @@ impl DecimalFloatingPoint {
|
||||||
let n = (duration.as_secs() as f64) + (duration.subsec_nanos() as f64 / 1_000_000_000.0);
|
let n = (duration.as_secs() as f64) + (duration.subsec_nanos() as f64 / 1_000_000_000.0);
|
||||||
DecimalFloatingPoint(n)
|
DecimalFloatingPoint(n)
|
||||||
}
|
}
|
||||||
|
pub fn as_f64(&self) -> f64 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
impl Eq for DecimalFloatingPoint {}
|
impl Eq for DecimalFloatingPoint {}
|
||||||
impl fmt::Display for DecimalFloatingPoint {
|
impl fmt::Display for DecimalFloatingPoint {
|
||||||
|
|
|
@ -110,10 +110,12 @@ impl FromStr for MasterPlaylist {
|
||||||
media_tags.push(t);
|
media_tags.push(t);
|
||||||
}
|
}
|
||||||
Tag::ExtXStreamInf(t) => {
|
Tag::ExtXStreamInf(t) => {
|
||||||
|
// TODO: It MUST match the value of the GROUP-ID attribute of an EXT-X-MEDIA tag
|
||||||
track_assert_eq!(last_stream_inf, None, ErrorKind::InvalidInput);
|
track_assert_eq!(last_stream_inf, None, ErrorKind::InvalidInput);
|
||||||
last_stream_inf = Some((i, t));
|
last_stream_inf = Some((i, t));
|
||||||
}
|
}
|
||||||
Tag::ExtXIFrameStreamInf(t) => {
|
Tag::ExtXIFrameStreamInf(t) => {
|
||||||
|
// TODO: It MUST match the value of the GROUP-ID attribute of an EXT-X-MEDIA tag
|
||||||
i_frame_stream_infs.push(t);
|
i_frame_stream_infs.push(t);
|
||||||
}
|
}
|
||||||
Tag::ExtXSessionData(t) => {
|
Tag::ExtXSessionData(t) => {
|
||||||
|
|
788
src/tag/master_playlist.rs
Normal file
788
src/tag/master_playlist.rs
Normal file
|
@ -0,0 +1,788 @@
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use {Error, ErrorKind, Result};
|
||||||
|
use attribute::{AttributePairs, DecimalFloatingPoint, DecimalInteger, DecimalResolution,
|
||||||
|
QuotedString};
|
||||||
|
use types::{ClosedCaptions, DecryptionKey, HdcpLevel, InStreamId, M3u8String, MediaType,
|
||||||
|
ProtocolVersion, SessionData, YesOrNo};
|
||||||
|
|
||||||
|
/// [4.3.4.1. EXT-X-MEDIA]
|
||||||
|
///
|
||||||
|
/// [4.3.4.1. EXT-X-MEDIA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.1
|
||||||
|
///
|
||||||
|
/// TODO: Add `ExtXMediaBuilder`
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ExtXMedia {
|
||||||
|
media_type: MediaType,
|
||||||
|
uri: Option<QuotedString>,
|
||||||
|
group_id: QuotedString,
|
||||||
|
language: Option<QuotedString>,
|
||||||
|
assoc_language: Option<QuotedString>,
|
||||||
|
name: QuotedString,
|
||||||
|
default: YesOrNo,
|
||||||
|
autoselect: YesOrNo,
|
||||||
|
forced: YesOrNo,
|
||||||
|
instream_id: Option<InStreamId>,
|
||||||
|
characteristics: Option<QuotedString>,
|
||||||
|
channels: Option<QuotedString>,
|
||||||
|
}
|
||||||
|
impl ExtXMedia {
|
||||||
|
pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA:";
|
||||||
|
|
||||||
|
/// Makes a new `ExtXMedia` tag.
|
||||||
|
pub fn new(media_type: MediaType, group_id: QuotedString, name: QuotedString) -> Self {
|
||||||
|
ExtXMedia {
|
||||||
|
media_type,
|
||||||
|
uri: None,
|
||||||
|
group_id,
|
||||||
|
language: None,
|
||||||
|
assoc_language: None,
|
||||||
|
name,
|
||||||
|
default: YesOrNo::No,
|
||||||
|
autoselect: YesOrNo::No,
|
||||||
|
forced: YesOrNo::No,
|
||||||
|
instream_id: None,
|
||||||
|
characteristics: None,
|
||||||
|
channels: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the type of the media associated with this tag.
|
||||||
|
pub fn media_type(&self) -> MediaType {
|
||||||
|
self.media_type
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the identifier that specifies the group to which the rendition belongs.
|
||||||
|
pub fn group_id(&self) -> &QuotedString {
|
||||||
|
&self.group_id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a human-readable description of the rendition.
|
||||||
|
pub fn name(&self) -> &QuotedString {
|
||||||
|
&self.name
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the URI that identifies the media playlist.
|
||||||
|
pub fn uri(&self) -> Option<&QuotedString> {
|
||||||
|
self.uri.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the name of the primary language used in the rendition.
|
||||||
|
pub fn language(&self) -> Option<&QuotedString> {
|
||||||
|
self.language.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the name of a language associated with the rendition.
|
||||||
|
pub fn assoc_language(&self) -> Option<&QuotedString> {
|
||||||
|
self.assoc_language.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether this is the default rendition.
|
||||||
|
pub fn default(&self) -> YesOrNo {
|
||||||
|
self.default
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether the client may choose to
|
||||||
|
/// play this rendition in the absence of explicit user preference.
|
||||||
|
pub fn autoselect(&self) -> YesOrNo {
|
||||||
|
self.autoselect
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether the rendition contains content that is considered essential to play.
|
||||||
|
pub fn forced(&self) -> YesOrNo {
|
||||||
|
self.forced
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the identifier that specifies a rendition within the segments in the media playlist.
|
||||||
|
pub fn instream_id(&self) -> Option<InStreamId> {
|
||||||
|
self.instream_id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a string that represents uniform type identifiers (UTI).
|
||||||
|
///
|
||||||
|
/// Each UTI indicates an individual characteristic of the rendition.
|
||||||
|
pub fn characteristics(&self) -> Option<&QuotedString> {
|
||||||
|
self.characteristics.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a string that represents the parameters of the rendition.
|
||||||
|
pub fn channels(&self) -> Option<&QuotedString> {
|
||||||
|
self.channels.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
|
pub fn requires_version(&self) -> ProtocolVersion {
|
||||||
|
match self.instream_id {
|
||||||
|
None
|
||||||
|
| Some(InStreamId::Cc1)
|
||||||
|
| Some(InStreamId::Cc2)
|
||||||
|
| Some(InStreamId::Cc3)
|
||||||
|
| Some(InStreamId::Cc4) => ProtocolVersion::V1,
|
||||||
|
_ => ProtocolVersion::V7,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl fmt::Display for ExtXMedia {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
|
write!(f, "TYPE={}", self.media_type)?;
|
||||||
|
if let Some(ref x) = self.uri {
|
||||||
|
write!(f, ",URI={}", x)?;
|
||||||
|
}
|
||||||
|
write!(f, ",GROUP-ID={}", self.group_id)?;
|
||||||
|
if let Some(ref x) = self.language {
|
||||||
|
write!(f, ",LANGUAGE={}", x)?;
|
||||||
|
}
|
||||||
|
if let Some(ref x) = self.assoc_language {
|
||||||
|
write!(f, ",ASSOC-LANGUAGE={}", x)?;
|
||||||
|
}
|
||||||
|
write!(f, ",NAME={}", self.name)?;
|
||||||
|
if YesOrNo::Yes == self.default {
|
||||||
|
write!(f, ",DEFAULT=YES")?;
|
||||||
|
}
|
||||||
|
if YesOrNo::Yes == self.autoselect {
|
||||||
|
write!(f, ",AUTOSELECT=YES")?;
|
||||||
|
}
|
||||||
|
if YesOrNo::Yes == self.forced {
|
||||||
|
write!(f, ",FORCED=YES")?;
|
||||||
|
}
|
||||||
|
if let Some(ref x) = self.instream_id {
|
||||||
|
write!(f, ",INSTREAM-ID=\"{}\"", x)?;
|
||||||
|
}
|
||||||
|
if let Some(ref x) = self.characteristics {
|
||||||
|
write!(f, ",CHARACTERISTICS={}", x)?;
|
||||||
|
}
|
||||||
|
if let Some(ref x) = self.channels {
|
||||||
|
write!(f, ",CHANNELS={}", x)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for ExtXMedia {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||||
|
|
||||||
|
let mut media_type = None;
|
||||||
|
let mut uri = None;
|
||||||
|
let mut group_id = None;
|
||||||
|
let mut language = None;
|
||||||
|
let mut assoc_language = None;
|
||||||
|
let mut name = None;
|
||||||
|
let mut default = None;
|
||||||
|
let mut autoselect = None;
|
||||||
|
let mut forced = None;
|
||||||
|
let mut instream_id = None;
|
||||||
|
let mut characteristics = None;
|
||||||
|
let mut channels = None;
|
||||||
|
let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1);
|
||||||
|
for attr in attrs {
|
||||||
|
let (key, value) = track!(attr)?;
|
||||||
|
match key {
|
||||||
|
"TYPE" => media_type = Some(track!(value.parse())?),
|
||||||
|
"URI" => uri = Some(track!(value.parse())?),
|
||||||
|
"GROUP-ID" => group_id = Some(track!(value.parse())?),
|
||||||
|
"LANGUAGE" => language = Some(track!(value.parse())?),
|
||||||
|
"ASSOC-LANGUAGE" => assoc_language = Some(track!(value.parse())?),
|
||||||
|
"NAME" => name = Some(track!(value.parse())?),
|
||||||
|
"DEFAULT" => default = Some(track!(value.parse())?),
|
||||||
|
"AUTOSELECT" => autoselect = Some(track!(value.parse())?),
|
||||||
|
"FORCED" => forced = Some(track!(value.parse())?),
|
||||||
|
"INSTREAM-ID" => {
|
||||||
|
let s: QuotedString = track!(value.parse())?;
|
||||||
|
instream_id = Some(track!(s.as_str().parse())?);
|
||||||
|
}
|
||||||
|
"CHARACTERISTICS" => characteristics = Some(track!(value.parse())?),
|
||||||
|
"CHANNELS" => channels = Some(track!(value.parse())?),
|
||||||
|
_ => {
|
||||||
|
// [6.3.1. General Client Responsibilities]
|
||||||
|
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let media_type = track_assert_some!(media_type, ErrorKind::InvalidInput);
|
||||||
|
let group_id = track_assert_some!(group_id, ErrorKind::InvalidInput);
|
||||||
|
let name = track_assert_some!(name, ErrorKind::InvalidInput);
|
||||||
|
if MediaType::ClosedCaptions == media_type {
|
||||||
|
track_assert_ne!(uri, None, ErrorKind::InvalidInput);
|
||||||
|
track_assert!(instream_id.is_some(), ErrorKind::InvalidInput);
|
||||||
|
} else {
|
||||||
|
track_assert!(instream_id.is_none(), ErrorKind::InvalidInput);
|
||||||
|
}
|
||||||
|
if default == Some(YesOrNo::Yes) && autoselect.is_some() {
|
||||||
|
track_assert_eq!(autoselect, Some(YesOrNo::Yes), ErrorKind::InvalidInput);
|
||||||
|
}
|
||||||
|
if MediaType::Subtitles != media_type {
|
||||||
|
track_assert_eq!(forced, None, ErrorKind::InvalidInput);
|
||||||
|
}
|
||||||
|
Ok(ExtXMedia {
|
||||||
|
media_type,
|
||||||
|
uri,
|
||||||
|
group_id,
|
||||||
|
language,
|
||||||
|
assoc_language,
|
||||||
|
name,
|
||||||
|
default: default.unwrap_or(YesOrNo::No),
|
||||||
|
autoselect: autoselect.unwrap_or(YesOrNo::No),
|
||||||
|
forced: forced.unwrap_or(YesOrNo::No),
|
||||||
|
instream_id,
|
||||||
|
characteristics,
|
||||||
|
channels,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [4.3.4.2. EXT-X-STREAM-INF]
|
||||||
|
///
|
||||||
|
/// [4.3.4.2. EXT-X-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.2
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ExtXStreamInf {
|
||||||
|
uri: M3u8String,
|
||||||
|
bandwidth: DecimalInteger,
|
||||||
|
average_bandwidth: Option<DecimalInteger>,
|
||||||
|
codecs: Option<QuotedString>,
|
||||||
|
resolution: Option<DecimalResolution>,
|
||||||
|
frame_rate: Option<DecimalFloatingPoint>,
|
||||||
|
hdcp_level: Option<HdcpLevel>,
|
||||||
|
audio: Option<QuotedString>,
|
||||||
|
video: Option<QuotedString>,
|
||||||
|
subtitles: Option<QuotedString>,
|
||||||
|
closed_captions: Option<ClosedCaptions>,
|
||||||
|
}
|
||||||
|
impl ExtXStreamInf {
|
||||||
|
pub(crate) const PREFIX: &'static str = "#EXT-X-STREAM-INF:";
|
||||||
|
|
||||||
|
/// Makes a new `ExtXStreamInf` tag.
|
||||||
|
pub fn new(uri: M3u8String, bandwidth: DecimalInteger) -> Self {
|
||||||
|
ExtXStreamInf {
|
||||||
|
uri,
|
||||||
|
bandwidth,
|
||||||
|
average_bandwidth: None,
|
||||||
|
codecs: None,
|
||||||
|
resolution: None,
|
||||||
|
frame_rate: None,
|
||||||
|
hdcp_level: None,
|
||||||
|
audio: None,
|
||||||
|
video: None,
|
||||||
|
subtitles: None,
|
||||||
|
closed_captions: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the URI that identifies the associated media playlist.
|
||||||
|
pub fn uri(&self) -> &M3u8String {
|
||||||
|
&self.uri
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the peak segment bit rate of the variant stream.
|
||||||
|
pub fn bandwidth(&self) -> DecimalInteger {
|
||||||
|
self.bandwidth
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the average segment bit rate of the variant stream.
|
||||||
|
pub fn average_bandwidth(&self) -> Option<DecimalInteger> {
|
||||||
|
self.average_bandwidth
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a string that represents the list of codec types contained the variant stream.
|
||||||
|
pub fn codecs(&self) -> Option<&QuotedString> {
|
||||||
|
self.codecs.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the optimal pixel resolution at which to display all the video in the variant stream.
|
||||||
|
pub fn resolution(&self) -> Option<DecimalResolution> {
|
||||||
|
self.resolution
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the maximum frame rate for all the video in the variant stream.
|
||||||
|
pub fn frame_rate(&self) -> Option<DecimalFloatingPoint> {
|
||||||
|
self.frame_rate
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the HDCP level of the variant stream.
|
||||||
|
pub fn hdcp_level(&self) -> Option<HdcpLevel> {
|
||||||
|
self.hdcp_level
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the group identifier for the audio in the variant stream.
|
||||||
|
pub fn audio(&self) -> Option<&QuotedString> {
|
||||||
|
self.audio.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the group identifier for the video in the variant stream.
|
||||||
|
pub fn video(&self) -> Option<&QuotedString> {
|
||||||
|
self.video.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the group identifier for the subtitles in the variant stream.
|
||||||
|
pub fn subtitles(&self) -> Option<&QuotedString> {
|
||||||
|
self.subtitles.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the value of `CLOSED-CAPTIONS` attribute.
|
||||||
|
pub fn closed_captions(&self) -> Option<&ClosedCaptions> {
|
||||||
|
self.closed_captions.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
|
pub fn requires_version(&self) -> ProtocolVersion {
|
||||||
|
ProtocolVersion::V1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl fmt::Display for ExtXStreamInf {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
|
write!(f, "BANDWIDTH={}", self.bandwidth)?;
|
||||||
|
if let Some(ref x) = self.average_bandwidth {
|
||||||
|
write!(f, ",AVERAGE-BANDWIDTH={}", x)?;
|
||||||
|
}
|
||||||
|
if let Some(ref x) = self.codecs {
|
||||||
|
write!(f, ",CODECS={}", x)?;
|
||||||
|
}
|
||||||
|
if let Some(ref x) = self.resolution {
|
||||||
|
write!(f, ",RESOLUTION={}", x)?;
|
||||||
|
}
|
||||||
|
if let Some(ref x) = self.frame_rate {
|
||||||
|
write!(f, ",FRAME-RATE={:.3}", x.as_f64())?;
|
||||||
|
}
|
||||||
|
if let Some(ref x) = self.hdcp_level {
|
||||||
|
write!(f, ",HDCP-LEVEL={}", x)?;
|
||||||
|
}
|
||||||
|
if let Some(ref x) = self.audio {
|
||||||
|
write!(f, ",AUDIO={}", x)?;
|
||||||
|
}
|
||||||
|
if let Some(ref x) = self.video {
|
||||||
|
write!(f, ",VIDEO={}", x)?;
|
||||||
|
}
|
||||||
|
if let Some(ref x) = self.subtitles {
|
||||||
|
write!(f, ",SUBTITLES={}", x)?;
|
||||||
|
}
|
||||||
|
if let Some(ref x) = self.closed_captions {
|
||||||
|
write!(f, ",CLOSED-CAPTIONS={}", x)?;
|
||||||
|
}
|
||||||
|
write!(f, "\n{}", self.uri)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for ExtXStreamInf {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
let mut lines = s.splitn(2, '\n'); // TODO:
|
||||||
|
let first_line = lines.next().expect("Never fails");
|
||||||
|
let second_line = track_assert_some!(lines.next(), ErrorKind::InvalidInput);
|
||||||
|
|
||||||
|
track_assert!(
|
||||||
|
first_line.starts_with(Self::PREFIX),
|
||||||
|
ErrorKind::InvalidInput
|
||||||
|
);
|
||||||
|
let uri = track!(M3u8String::new(second_line))?;
|
||||||
|
let mut bandwidth = None;
|
||||||
|
let mut average_bandwidth = None;
|
||||||
|
let mut codecs = None;
|
||||||
|
let mut resolution = None;
|
||||||
|
let mut frame_rate = None;
|
||||||
|
let mut hdcp_level = None;
|
||||||
|
let mut audio = None;
|
||||||
|
let mut video = None;
|
||||||
|
let mut subtitles = None;
|
||||||
|
let mut closed_captions = None;
|
||||||
|
let attrs = AttributePairs::parse(first_line.split_at(Self::PREFIX.len()).1);
|
||||||
|
for attr in attrs {
|
||||||
|
let (key, value) = track!(attr)?;
|
||||||
|
match key {
|
||||||
|
"BANDWIDTH" => bandwidth = Some(track!(value.parse())?),
|
||||||
|
"AVERAGE-BANDWIDTH" => average_bandwidth = Some(track!(value.parse())?),
|
||||||
|
"CODECS" => codecs = Some(track!(value.parse())?),
|
||||||
|
"RESOLUTION" => resolution = Some(track!(value.parse())?),
|
||||||
|
"FRAME-RATE" => frame_rate = Some(track!(value.parse())?),
|
||||||
|
"HDCP-LEVEL" => hdcp_level = Some(track!(value.parse())?),
|
||||||
|
"AUDIO" => audio = Some(track!(value.parse())?),
|
||||||
|
"VIDEO" => video = Some(track!(value.parse())?),
|
||||||
|
"SUBTITLES" => subtitles = Some(track!(value.parse())?),
|
||||||
|
"CLOSED-CAPTIONS" => closed_captions = Some(track!(value.parse())?),
|
||||||
|
_ => {
|
||||||
|
// [6.3.1. General Client Responsibilities]
|
||||||
|
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let bandwidth = track_assert_some!(bandwidth, ErrorKind::InvalidInput);
|
||||||
|
Ok(ExtXStreamInf {
|
||||||
|
uri,
|
||||||
|
bandwidth,
|
||||||
|
average_bandwidth,
|
||||||
|
codecs,
|
||||||
|
resolution,
|
||||||
|
frame_rate,
|
||||||
|
hdcp_level,
|
||||||
|
audio,
|
||||||
|
video,
|
||||||
|
subtitles,
|
||||||
|
closed_captions,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [4.3.4.3. EXT-X-I-FRAME-STREAM-INF]
|
||||||
|
///
|
||||||
|
/// [4.3.4.3. EXT-X-I-FRAME-STREAM-INF]: https://tools.ietf.org/html/rfc8216#section-4.3.4.3
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ExtXIFrameStreamInf {
|
||||||
|
uri: QuotedString,
|
||||||
|
bandwidth: DecimalInteger,
|
||||||
|
average_bandwidth: Option<DecimalInteger>,
|
||||||
|
codecs: Option<QuotedString>,
|
||||||
|
resolution: Option<DecimalResolution>,
|
||||||
|
hdcp_level: Option<HdcpLevel>,
|
||||||
|
video: Option<QuotedString>,
|
||||||
|
}
|
||||||
|
impl ExtXIFrameStreamInf {
|
||||||
|
pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAME-STREAM-INF:";
|
||||||
|
|
||||||
|
/// Makes a new `ExtXIFrameStreamInf` tag.
|
||||||
|
pub fn new(uri: QuotedString, bandwidth: DecimalInteger) -> Self {
|
||||||
|
ExtXIFrameStreamInf {
|
||||||
|
uri,
|
||||||
|
bandwidth,
|
||||||
|
average_bandwidth: None,
|
||||||
|
codecs: None,
|
||||||
|
resolution: None,
|
||||||
|
hdcp_level: None,
|
||||||
|
video: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the URI that identifies the associated media playlist.
|
||||||
|
pub fn uri(&self) -> &QuotedString {
|
||||||
|
&self.uri
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the peak segment bit rate of the variant stream.
|
||||||
|
pub fn bandwidth(&self) -> DecimalInteger {
|
||||||
|
self.bandwidth
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the average segment bit rate of the variant stream.
|
||||||
|
pub fn average_bandwidth(&self) -> Option<DecimalInteger> {
|
||||||
|
self.average_bandwidth
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a string that represents the list of codec types contained the variant stream.
|
||||||
|
pub fn codecs(&self) -> Option<&QuotedString> {
|
||||||
|
self.codecs.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the optimal pixel resolution at which to display all the video in the variant stream.
|
||||||
|
pub fn resolution(&self) -> Option<DecimalResolution> {
|
||||||
|
self.resolution
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the HDCP level of the variant stream.
|
||||||
|
pub fn hdcp_level(&self) -> Option<HdcpLevel> {
|
||||||
|
self.hdcp_level
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the group identifier for the video in the variant stream.
|
||||||
|
pub fn video(&self) -> Option<&QuotedString> {
|
||||||
|
self.video.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
|
pub fn requires_version(&self) -> ProtocolVersion {
|
||||||
|
ProtocolVersion::V1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl fmt::Display for ExtXIFrameStreamInf {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
|
write!(f, "URI={}", self.uri)?;
|
||||||
|
write!(f, ",BANDWIDTH={}", self.bandwidth)?;
|
||||||
|
if let Some(ref x) = self.average_bandwidth {
|
||||||
|
write!(f, ",AVERAGE-BANDWIDTH={}", x)?;
|
||||||
|
}
|
||||||
|
if let Some(ref x) = self.codecs {
|
||||||
|
write!(f, ",CODECS={}", x)?;
|
||||||
|
}
|
||||||
|
if let Some(ref x) = self.resolution {
|
||||||
|
write!(f, ",RESOLUTION={}", x)?;
|
||||||
|
}
|
||||||
|
if let Some(ref x) = self.hdcp_level {
|
||||||
|
write!(f, ",HDCP-LEVEL={}", x)?;
|
||||||
|
}
|
||||||
|
if let Some(ref x) = self.video {
|
||||||
|
write!(f, ",VIDEO={}", x)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for ExtXIFrameStreamInf {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||||
|
|
||||||
|
let mut uri = None;
|
||||||
|
let mut bandwidth = None;
|
||||||
|
let mut average_bandwidth = None;
|
||||||
|
let mut codecs = None;
|
||||||
|
let mut resolution = None;
|
||||||
|
let mut hdcp_level = None;
|
||||||
|
let mut video = None;
|
||||||
|
let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1);
|
||||||
|
for attr in attrs {
|
||||||
|
let (key, value) = track!(attr)?;
|
||||||
|
match key {
|
||||||
|
"URI" => uri = Some(track!(value.parse())?),
|
||||||
|
"BANDWIDTH" => bandwidth = Some(track!(value.parse())?),
|
||||||
|
"AVERAGE-BANDWIDTH" => average_bandwidth = Some(track!(value.parse())?),
|
||||||
|
"CODECS" => codecs = Some(track!(value.parse())?),
|
||||||
|
"RESOLUTION" => resolution = Some(track!(value.parse())?),
|
||||||
|
"HDCP-LEVEL" => hdcp_level = Some(track!(value.parse())?),
|
||||||
|
"VIDEO" => video = Some(track!(value.parse())?),
|
||||||
|
_ => {
|
||||||
|
// [6.3.1. General Client Responsibilities]
|
||||||
|
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let uri = track_assert_some!(uri, ErrorKind::InvalidInput);
|
||||||
|
let bandwidth = track_assert_some!(bandwidth, ErrorKind::InvalidInput);
|
||||||
|
Ok(ExtXIFrameStreamInf {
|
||||||
|
uri,
|
||||||
|
bandwidth,
|
||||||
|
average_bandwidth,
|
||||||
|
codecs,
|
||||||
|
resolution,
|
||||||
|
hdcp_level,
|
||||||
|
video,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [4.3.4.4. EXT-X-SESSION-DATA]
|
||||||
|
///
|
||||||
|
/// [4.3.4.4. EXT-X-SESSION-DATA]: https://tools.ietf.org/html/rfc8216#section-4.3.4.4
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ExtXSessionData {
|
||||||
|
data_id: QuotedString,
|
||||||
|
data: SessionData,
|
||||||
|
language: Option<QuotedString>,
|
||||||
|
}
|
||||||
|
impl ExtXSessionData {
|
||||||
|
pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-DATA:";
|
||||||
|
|
||||||
|
/// Makes a new `ExtXSessionData` tag.
|
||||||
|
pub fn new(data_id: QuotedString, data: SessionData) -> Self {
|
||||||
|
ExtXSessionData {
|
||||||
|
data_id,
|
||||||
|
data,
|
||||||
|
language: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes a new `ExtXSessionData` with the given language.
|
||||||
|
pub fn with_language(data_id: QuotedString, data: SessionData, language: QuotedString) -> Self {
|
||||||
|
ExtXSessionData {
|
||||||
|
data_id,
|
||||||
|
data,
|
||||||
|
language: Some(language),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the identifier of the data.
|
||||||
|
pub fn data_id(&self) -> &QuotedString {
|
||||||
|
&self.data_id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the session data.
|
||||||
|
pub fn data(&self) -> &SessionData {
|
||||||
|
&self.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the language of the data.
|
||||||
|
pub fn language(&self) -> Option<&QuotedString> {
|
||||||
|
self.language.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
|
pub fn requires_version(&self) -> ProtocolVersion {
|
||||||
|
ProtocolVersion::V1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl fmt::Display for ExtXSessionData {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
|
write!(f, "DATA-ID={}", self.data_id)?;
|
||||||
|
match self.data {
|
||||||
|
SessionData::Value(ref x) => write!(f, ",VALUE={}", x)?,
|
||||||
|
SessionData::Uri(ref x) => write!(f, ",URI={}", x)?,
|
||||||
|
}
|
||||||
|
if let Some(ref x) = self.language {
|
||||||
|
write!(f, ",LANGUAGE={}", x)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for ExtXSessionData {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||||
|
|
||||||
|
let mut data_id = None;
|
||||||
|
let mut session_value = None;
|
||||||
|
let mut uri = None;
|
||||||
|
let mut language = None;
|
||||||
|
let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1);
|
||||||
|
for attr in attrs {
|
||||||
|
let (key, value) = track!(attr)?;
|
||||||
|
match key {
|
||||||
|
"DATA-ID" => data_id = Some(track!(value.parse())?),
|
||||||
|
"VALUE" => session_value = Some(track!(value.parse())?),
|
||||||
|
"URI" => uri = Some(track!(value.parse())?),
|
||||||
|
"LANGUAGE" => language = Some(track!(value.parse())?),
|
||||||
|
_ => {
|
||||||
|
// [6.3.1. General Client Responsibilities]
|
||||||
|
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let data_id = track_assert_some!(data_id, ErrorKind::InvalidInput);
|
||||||
|
let data = if let Some(value) = session_value {
|
||||||
|
track_assert_eq!(uri, None, ErrorKind::InvalidInput);
|
||||||
|
SessionData::Value(value)
|
||||||
|
} else if let Some(uri) = uri {
|
||||||
|
SessionData::Uri(uri)
|
||||||
|
} else {
|
||||||
|
track_panic!(ErrorKind::InvalidInput);
|
||||||
|
};
|
||||||
|
Ok(ExtXSessionData {
|
||||||
|
data_id,
|
||||||
|
data,
|
||||||
|
language,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [4.3.4.5. EXT-X-SESSION-KEY]
|
||||||
|
///
|
||||||
|
/// [4.3.4.5. EXT-X-SESSION-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.4.5
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ExtXSessionKey {
|
||||||
|
key: DecryptionKey,
|
||||||
|
}
|
||||||
|
impl ExtXSessionKey {
|
||||||
|
pub(crate) const PREFIX: &'static str = "#EXT-X-SESSION-KEY:";
|
||||||
|
|
||||||
|
/// Makes a new `ExtXSessionKey` tag.
|
||||||
|
pub fn new(key: DecryptionKey) -> Self {
|
||||||
|
ExtXSessionKey { key }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a decryption key for the playlist.
|
||||||
|
pub fn key(&self) -> &DecryptionKey {
|
||||||
|
&self.key
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
|
pub fn requires_version(&self) -> ProtocolVersion {
|
||||||
|
self.key.requires_version()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl fmt::Display for ExtXSessionKey {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}{}", Self::PREFIX, self.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for ExtXSessionKey {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||||
|
let suffix = s.split_at(Self::PREFIX.len()).1;
|
||||||
|
let key = track!(suffix.parse())?;
|
||||||
|
Ok(ExtXSessionKey { key })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use attribute::HexadecimalSequence;
|
||||||
|
use types::EncryptionMethod;
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ext_x_media() {
|
||||||
|
let tag = ExtXMedia::new(MediaType::Audio, quoted_string("foo"), quoted_string("bar"));
|
||||||
|
let text = r#"#EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="foo",NAME="bar""#;
|
||||||
|
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||||
|
assert_eq!(tag.to_string(), text);
|
||||||
|
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ext_x_stream_inf() {
|
||||||
|
let tag = ExtXStreamInf::new(M3u8String::new("foo").unwrap(), DecimalInteger(1000));
|
||||||
|
let text = "#EXT-X-STREAM-INF:BANDWIDTH=1000\nfoo";
|
||||||
|
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||||
|
assert_eq!(tag.to_string(), text);
|
||||||
|
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ext_x_i_frame_stream_inf() {
|
||||||
|
let tag = ExtXIFrameStreamInf::new(quoted_string("foo"), DecimalInteger(1000));
|
||||||
|
let text = r#"#EXT-X-I-FRAME-STREAM-INF:URI="foo",BANDWIDTH=1000"#;
|
||||||
|
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||||
|
assert_eq!(tag.to_string(), text);
|
||||||
|
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ext_x_session_data() {
|
||||||
|
let tag = ExtXSessionData::new(
|
||||||
|
quoted_string("foo"),
|
||||||
|
SessionData::Value(quoted_string("bar")),
|
||||||
|
);
|
||||||
|
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar""#;
|
||||||
|
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||||
|
assert_eq!(tag.to_string(), text);
|
||||||
|
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||||
|
|
||||||
|
let tag =
|
||||||
|
ExtXSessionData::new(quoted_string("foo"), SessionData::Uri(quoted_string("bar")));
|
||||||
|
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",URI="bar""#;
|
||||||
|
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||||
|
assert_eq!(tag.to_string(), text);
|
||||||
|
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||||
|
|
||||||
|
let tag = ExtXSessionData::with_language(
|
||||||
|
quoted_string("foo"),
|
||||||
|
SessionData::Value(quoted_string("bar")),
|
||||||
|
quoted_string("baz"),
|
||||||
|
);
|
||||||
|
let text = r#"#EXT-X-SESSION-DATA:DATA-ID="foo",VALUE="bar",LANGUAGE="baz""#;
|
||||||
|
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||||
|
assert_eq!(tag.to_string(), text);
|
||||||
|
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ext_x_session_key() {
|
||||||
|
let tag = ExtXSessionKey::new(DecryptionKey {
|
||||||
|
method: EncryptionMethod::Aes128,
|
||||||
|
uri: quoted_string("foo"),
|
||||||
|
iv: Some(HexadecimalSequence::new(vec![0, 1, 2])),
|
||||||
|
key_format: None,
|
||||||
|
key_format_versions: None,
|
||||||
|
});
|
||||||
|
let text = r#"#EXT-X-SESSION-KEY:METHOD=AES-128,URI="foo",IV=0x000102"#;
|
||||||
|
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||||
|
assert_eq!(tag.to_string(), text);
|
||||||
|
assert_eq!(tag.requires_version(), ProtocolVersion::V2);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quoted_string(s: &str) -> QuotedString {
|
||||||
|
QuotedString::new(s).unwrap()
|
||||||
|
}
|
||||||
|
}
|
|
@ -283,14 +283,8 @@ impl FromStr for ExtXMap {
|
||||||
for attr in attrs {
|
for attr in attrs {
|
||||||
let (key, value) = track!(attr)?;
|
let (key, value) = track!(attr)?;
|
||||||
match key {
|
match key {
|
||||||
"URI" => {
|
"URI" => uri = Some(track!(value.parse())?),
|
||||||
track_assert_eq!(uri, None, ErrorKind::InvalidInput);
|
"BYTERANGE" => range = Some(track!(value.parse())?),
|
||||||
uri = Some(track!(value.parse())?);
|
|
||||||
}
|
|
||||||
"BYTERANGE" => {
|
|
||||||
track_assert_eq!(range, None, ErrorKind::InvalidInput);
|
|
||||||
range = Some(track!(value.parse())?);
|
|
||||||
}
|
|
||||||
_ => {
|
_ => {
|
||||||
// [6.3.1. General Client Responsibilities]
|
// [6.3.1. General Client Responsibilities]
|
||||||
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
||||||
|
@ -435,12 +429,8 @@ impl FromStr for ExtXDateRange {
|
||||||
for attr in attrs {
|
for attr in attrs {
|
||||||
let (key, value) = track!(attr)?;
|
let (key, value) = track!(attr)?;
|
||||||
match key {
|
match key {
|
||||||
"ID" => {
|
"ID" => id = Some(track!(value.parse())?),
|
||||||
id = Some(track!(value.parse())?);
|
"CLASS" => class = Some(track!(value.parse())?),
|
||||||
}
|
|
||||||
"CLASS" => {
|
|
||||||
class = Some(track!(value.parse())?);
|
|
||||||
}
|
|
||||||
"START-DATE" => {
|
"START-DATE" => {
|
||||||
let s: QuotedString = track!(value.parse())?;
|
let s: QuotedString = track!(value.parse())?;
|
||||||
start_date = Some(track!(
|
start_date = Some(track!(
|
||||||
|
@ -463,18 +453,10 @@ impl FromStr for ExtXDateRange {
|
||||||
let seconds: DecimalFloatingPoint = track!(value.parse())?;
|
let seconds: DecimalFloatingPoint = track!(value.parse())?;
|
||||||
planned_duration = Some(seconds.to_duration());
|
planned_duration = Some(seconds.to_duration());
|
||||||
}
|
}
|
||||||
"SCTE35-CMD" => {
|
"SCTE35-CMD" => scte35_cmd = Some(track!(value.parse())?),
|
||||||
scte35_cmd = Some(track!(value.parse())?);
|
"SCTE35-OUT" => scte35_out = Some(track!(value.parse())?),
|
||||||
}
|
"SCTE35-IN" => scte35_in = Some(track!(value.parse())?),
|
||||||
"SCTE35-OUT" => {
|
"END-ON-NEXT" => end_on_next = Some(track!(value.parse())?),
|
||||||
scte35_out = Some(track!(value.parse())?);
|
|
||||||
}
|
|
||||||
"SCTE35-IN" => {
|
|
||||||
scte35_in = Some(track!(value.parse())?);
|
|
||||||
}
|
|
||||||
"END-ON-NEXT" => {
|
|
||||||
end_on_next = Some(track!(value.parse())?);
|
|
||||||
}
|
|
||||||
_ => {
|
_ => {
|
||||||
if key.starts_with("X-") {
|
if key.starts_with("X-") {
|
||||||
client_attributes.insert(key.split_at(2).1.to_owned(), value.to_owned());
|
client_attributes.insert(key.split_at(2).1.to_owned(), value.to_owned());
|
||||||
|
|
610
src/tag/mod.rs
610
src/tag/mod.rs
|
@ -5,8 +5,8 @@ use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use {Error, ErrorKind, Result};
|
use {Error, ErrorKind, Result};
|
||||||
use attribute::{AttributePairs, DecimalFloatingPoint, DecimalInteger, DecimalResolution,
|
use attribute::{AttributePairs, SignedDecimalFloatingPoint};
|
||||||
HexadecimalSequence, QuotedString, SignedDecimalFloatingPoint};
|
use types::YesOrNo;
|
||||||
|
|
||||||
macro_rules! may_invalid {
|
macro_rules! may_invalid {
|
||||||
($expr:expr) => {
|
($expr:expr) => {
|
||||||
|
@ -25,12 +25,15 @@ macro_rules! impl_from {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub use self::basic::{ExtM3u, ExtXVersion};
|
pub use self::basic::{ExtM3u, ExtXVersion};
|
||||||
|
pub use self::master_playlist::{ExtXIFrameStreamInf, ExtXMedia, ExtXSessionData, ExtXSessionKey,
|
||||||
|
ExtXStreamInf};
|
||||||
pub use self::media_playlist::{ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly,
|
pub use self::media_playlist::{ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly,
|
||||||
ExtXMediaSequence, ExtXPlaylistType, ExtXTargetDuration};
|
ExtXMediaSequence, ExtXPlaylistType, ExtXTargetDuration};
|
||||||
pub use self::media_segment::{ExtInf, ExtXByteRange, ExtXDateRange, ExtXDiscontinuity, ExtXKey,
|
pub use self::media_segment::{ExtInf, ExtXByteRange, ExtXDateRange, ExtXDiscontinuity, ExtXKey,
|
||||||
ExtXMap, ExtXProgramDateTime};
|
ExtXMap, ExtXProgramDateTime};
|
||||||
|
|
||||||
mod basic;
|
mod basic;
|
||||||
|
mod master_playlist;
|
||||||
mod media_playlist;
|
mod media_playlist;
|
||||||
mod media_segment;
|
mod media_segment;
|
||||||
|
|
||||||
|
@ -227,580 +230,6 @@ impl FromStr for Tag {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub enum MediaType {
|
|
||||||
Audio,
|
|
||||||
Video,
|
|
||||||
Subtitles,
|
|
||||||
ClosedCaptions,
|
|
||||||
}
|
|
||||||
impl fmt::Display for MediaType {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match *self {
|
|
||||||
MediaType::Audio => "AUDIO".fmt(f),
|
|
||||||
MediaType::Video => "VIDEO".fmt(f),
|
|
||||||
MediaType::Subtitles => "SUBTITLES".fmt(f),
|
|
||||||
MediaType::ClosedCaptions => "CLOSED-CAPTIONS".fmt(f),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for MediaType {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
Ok(match s {
|
|
||||||
"AUDIO" => MediaType::Audio,
|
|
||||||
"VIDEO" => MediaType::Video,
|
|
||||||
"SUBTITLES" => MediaType::Subtitles,
|
|
||||||
"CLOSED-CAPTIONS" => MediaType::ClosedCaptions,
|
|
||||||
_ => track_panic!(ErrorKind::InvalidInput, "Unknown media type: {:?}", s),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub enum YesOrNo {
|
|
||||||
Yes,
|
|
||||||
No,
|
|
||||||
}
|
|
||||||
impl fmt::Display for YesOrNo {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match *self {
|
|
||||||
YesOrNo::Yes => "YES".fmt(f),
|
|
||||||
YesOrNo::No => "NO".fmt(f),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for YesOrNo {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
match s {
|
|
||||||
"YES" => Ok(YesOrNo::Yes),
|
|
||||||
"NO" => Ok(YesOrNo::No),
|
|
||||||
_ => track_panic!(
|
|
||||||
ErrorKind::InvalidInput,
|
|
||||||
"Unexpected enumerated-string: {:?}",
|
|
||||||
s
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct ExtXMedia {
|
|
||||||
media_type: MediaType,
|
|
||||||
uri: Option<QuotedString>,
|
|
||||||
group_id: QuotedString,
|
|
||||||
language: Option<QuotedString>,
|
|
||||||
assoc_language: Option<QuotedString>,
|
|
||||||
name: QuotedString,
|
|
||||||
default: YesOrNo,
|
|
||||||
autoselect: YesOrNo,
|
|
||||||
forced: Option<YesOrNo>,
|
|
||||||
instream_id: Option<QuotedString>, // TODO: `InStreamId` type
|
|
||||||
characteristics: Option<QuotedString>,
|
|
||||||
channels: Option<QuotedString>,
|
|
||||||
}
|
|
||||||
impl ExtXMedia {
|
|
||||||
const PREFIX: &'static str = "#EXT-X-MEDIA:";
|
|
||||||
}
|
|
||||||
impl fmt::Display for ExtXMedia {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
|
||||||
write!(f, "TYPE={}", self.media_type)?;
|
|
||||||
if let Some(ref x) = self.uri {
|
|
||||||
write!(f, ",URI={}", x)?;
|
|
||||||
}
|
|
||||||
write!(f, ",GROUP_ID={}", self.group_id)?;
|
|
||||||
if let Some(ref x) = self.language {
|
|
||||||
write!(f, ",LANGUAGE={}", x)?;
|
|
||||||
}
|
|
||||||
if let Some(ref x) = self.assoc_language {
|
|
||||||
write!(f, ",ASSOC-LANGUAGE={}", x)?;
|
|
||||||
}
|
|
||||||
write!(f, ",NAME={}", self.name)?;
|
|
||||||
if YesOrNo::Yes == self.default {
|
|
||||||
write!(f, ",DEFAULT={}", self.default)?;
|
|
||||||
}
|
|
||||||
if YesOrNo::Yes == self.autoselect {
|
|
||||||
write!(f, ",AUTOSELECT={}", self.autoselect)?;
|
|
||||||
}
|
|
||||||
if let Some(ref x) = self.forced {
|
|
||||||
write!(f, ",FORCED={}", x)?;
|
|
||||||
}
|
|
||||||
if let Some(ref x) = self.instream_id {
|
|
||||||
write!(f, ",INSTREAM-ID={}", x)?;
|
|
||||||
}
|
|
||||||
if let Some(ref x) = self.characteristics {
|
|
||||||
write!(f, ",CHARACTERISTICS={}", x)?;
|
|
||||||
}
|
|
||||||
if let Some(ref x) = self.channels {
|
|
||||||
write!(f, ",CHANNELS={}", x)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for ExtXMedia {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
|
||||||
|
|
||||||
let mut media_type = None;
|
|
||||||
let mut uri = None;
|
|
||||||
let mut group_id = None;
|
|
||||||
let mut language = None;
|
|
||||||
let mut assoc_language = None;
|
|
||||||
let mut name = None;
|
|
||||||
let mut default = None;
|
|
||||||
let mut autoselect = None;
|
|
||||||
let mut forced = None;
|
|
||||||
let mut instream_id = None;
|
|
||||||
let mut characteristics = None;
|
|
||||||
let mut channels = None;
|
|
||||||
let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1);
|
|
||||||
for attr in attrs {
|
|
||||||
let (key, value) = track!(attr)?;
|
|
||||||
match key {
|
|
||||||
"TYPE" => media_type = Some(track!(value.parse())?),
|
|
||||||
"URI" => uri = Some(track!(value.parse())?),
|
|
||||||
"GROUP-ID" => group_id = Some(track!(value.parse())?),
|
|
||||||
"LANGUAGE" => language = Some(track!(value.parse())?),
|
|
||||||
"ASSOC-LANGUAGE" => assoc_language = Some(track!(value.parse())?),
|
|
||||||
"NAME" => name = Some(track!(value.parse())?),
|
|
||||||
"DEFAULT" => default = Some(track!(value.parse())?),
|
|
||||||
"AUTOSELECT" => autoselect = Some(track!(value.parse())?),
|
|
||||||
"FORCED" => forced = Some(track!(value.parse())?),
|
|
||||||
"INSTREAM-ID" => instream_id = Some(track!(value.parse())?),
|
|
||||||
"CHARACTERISTICS" => characteristics = Some(track!(value.parse())?),
|
|
||||||
"CHANNELS" => channels = Some(track!(value.parse())?),
|
|
||||||
_ => {
|
|
||||||
// [6.3.1] ignore any attribute/value pair with an unrecognized AttributeName.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let media_type = track_assert_some!(media_type, ErrorKind::InvalidInput);
|
|
||||||
let group_id = track_assert_some!(group_id, ErrorKind::InvalidInput);
|
|
||||||
let name = track_assert_some!(name, ErrorKind::InvalidInput);
|
|
||||||
if MediaType::ClosedCaptions == media_type {
|
|
||||||
track_assert_ne!(uri, None, ErrorKind::InvalidInput);
|
|
||||||
track_assert!(instream_id.is_some(), ErrorKind::InvalidInput);
|
|
||||||
} else {
|
|
||||||
track_assert!(instream_id.is_none(), ErrorKind::InvalidInput);
|
|
||||||
}
|
|
||||||
if MediaType::Subtitles != media_type {
|
|
||||||
track_assert_eq!(forced, None, ErrorKind::InvalidInput);
|
|
||||||
}
|
|
||||||
Ok(ExtXMedia {
|
|
||||||
media_type,
|
|
||||||
uri,
|
|
||||||
group_id,
|
|
||||||
language,
|
|
||||||
assoc_language,
|
|
||||||
name,
|
|
||||||
default: default.unwrap_or(YesOrNo::No),
|
|
||||||
autoselect: autoselect.unwrap_or(YesOrNo::No),
|
|
||||||
forced,
|
|
||||||
instream_id,
|
|
||||||
characteristics,
|
|
||||||
channels,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub enum HdcpLevel {
|
|
||||||
Type0,
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
impl fmt::Display for HdcpLevel {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match *self {
|
|
||||||
HdcpLevel::Type0 => "TYPE-0".fmt(f),
|
|
||||||
HdcpLevel::None => "NONE".fmt(f),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for HdcpLevel {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
match s {
|
|
||||||
"TYPE-0" => Ok(HdcpLevel::Type0),
|
|
||||||
"NONE" => Ok(HdcpLevel::None),
|
|
||||||
_ => track_panic!(ErrorKind::InvalidInput, "Unknown HDCP level: {:?}", s),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
||||||
pub enum ClosedCaptions {
|
|
||||||
GroupId(QuotedString),
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
impl fmt::Display for ClosedCaptions {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match *self {
|
|
||||||
ClosedCaptions::GroupId(ref x) => x.fmt(f),
|
|
||||||
ClosedCaptions::None => "NONE".fmt(f),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for ClosedCaptions {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
if s == "NONE" {
|
|
||||||
Ok(ClosedCaptions::None)
|
|
||||||
} else {
|
|
||||||
Ok(ClosedCaptions::GroupId(track!(s.parse())?))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: The URI line is REQUIRED.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct ExtXStreamInf {
|
|
||||||
bandwidth: DecimalInteger,
|
|
||||||
average_bandwidth: Option<DecimalInteger>,
|
|
||||||
codecs: Option<QuotedString>,
|
|
||||||
resolution: Option<DecimalResolution>,
|
|
||||||
|
|
||||||
// TODO: rounded to three decimal places
|
|
||||||
frame_rate: Option<DecimalFloatingPoint>,
|
|
||||||
hdcp_level: Option<HdcpLevel>,
|
|
||||||
audio: Option<QuotedString>,
|
|
||||||
video: Option<QuotedString>,
|
|
||||||
subtitles: Option<QuotedString>,
|
|
||||||
closed_captions: Option<ClosedCaptions>,
|
|
||||||
}
|
|
||||||
impl ExtXStreamInf {
|
|
||||||
const PREFIX: &'static str = "#EXT-X-STREAM-INF:";
|
|
||||||
}
|
|
||||||
impl fmt::Display for ExtXStreamInf {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
|
||||||
write!(f, "BANDWIDTH={}", self.bandwidth)?;
|
|
||||||
if let Some(ref x) = self.average_bandwidth {
|
|
||||||
write!(f, ",AVERAGE-BANDWIDTH={}", x)?;
|
|
||||||
}
|
|
||||||
if let Some(ref x) = self.codecs {
|
|
||||||
write!(f, ",CODECS={}", x)?;
|
|
||||||
}
|
|
||||||
if let Some(ref x) = self.resolution {
|
|
||||||
write!(f, ",RESOLUTION={}", x)?;
|
|
||||||
}
|
|
||||||
if let Some(ref x) = self.frame_rate {
|
|
||||||
write!(f, ",FRAME-RATE={}", x)?;
|
|
||||||
}
|
|
||||||
if let Some(ref x) = self.hdcp_level {
|
|
||||||
write!(f, ",HDCP-LEVEL={}", x)?;
|
|
||||||
}
|
|
||||||
if let Some(ref x) = self.audio {
|
|
||||||
write!(f, ",AUDIO={}", x)?;
|
|
||||||
}
|
|
||||||
if let Some(ref x) = self.video {
|
|
||||||
write!(f, ",VIDEO={}", x)?;
|
|
||||||
}
|
|
||||||
if let Some(ref x) = self.subtitles {
|
|
||||||
write!(f, ",SUBTITLES={}", x)?;
|
|
||||||
}
|
|
||||||
if let Some(ref x) = self.closed_captions {
|
|
||||||
write!(f, ",CLOSED-CAPTIONS={}", x)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for ExtXStreamInf {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
|
||||||
|
|
||||||
let mut bandwidth = None;
|
|
||||||
let mut average_bandwidth = None;
|
|
||||||
let mut codecs = None;
|
|
||||||
let mut resolution = None;
|
|
||||||
let mut frame_rate = None;
|
|
||||||
let mut hdcp_level = None;
|
|
||||||
let mut audio = None;
|
|
||||||
let mut video = None;
|
|
||||||
let mut subtitles = None;
|
|
||||||
let mut closed_captions = None;
|
|
||||||
let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1);
|
|
||||||
for attr in attrs {
|
|
||||||
let (key, value) = track!(attr)?;
|
|
||||||
match key {
|
|
||||||
"BANDWIDTH" => bandwidth = Some(track!(value.parse())?),
|
|
||||||
"AVERAGE-BANDWIDTH" => average_bandwidth = Some(track!(value.parse())?),
|
|
||||||
"CODECS" => codecs = Some(track!(value.parse())?),
|
|
||||||
"RESOLUTION" => resolution = Some(track!(value.parse())?),
|
|
||||||
"FRAME-RATE" => frame_rate = Some(track!(value.parse())?),
|
|
||||||
"HDCP-LEVEL" => hdcp_level = Some(track!(value.parse())?),
|
|
||||||
"AUDIO" => {
|
|
||||||
// TODO: It MUST match the value of the GROUP-ID attribute of an EXT-X-MEDIA tag
|
|
||||||
audio = Some(track!(value.parse())?);
|
|
||||||
}
|
|
||||||
"VIDEO" => {
|
|
||||||
// TODO: It MUST match the value of the GROUP-ID attribute of an EXT-X-MEDIA tag
|
|
||||||
video = Some(track!(value.parse())?);
|
|
||||||
}
|
|
||||||
"SUBTITLES" => {
|
|
||||||
// TODO: It MUST match the value of the GROUP-ID attribute of an EXT-X-MEDIA tag
|
|
||||||
subtitles = Some(track!(value.parse())?);
|
|
||||||
}
|
|
||||||
"CLOSED-CAPTIONS" => {
|
|
||||||
// TODO: validate
|
|
||||||
closed_captions = Some(track!(value.parse())?);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// [6.3.1] ignore any attribute/value pair with an unrecognized AttributeName.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let bandwidth = track_assert_some!(bandwidth, ErrorKind::InvalidInput);
|
|
||||||
Ok(ExtXStreamInf {
|
|
||||||
bandwidth,
|
|
||||||
average_bandwidth,
|
|
||||||
codecs,
|
|
||||||
resolution,
|
|
||||||
frame_rate,
|
|
||||||
hdcp_level,
|
|
||||||
audio,
|
|
||||||
video,
|
|
||||||
subtitles,
|
|
||||||
closed_captions,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: That Playlist file MUST contain an EXT-X-I-FRAMES-ONLY tag.
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct ExtXIFrameStreamInf {
|
|
||||||
uri: QuotedString,
|
|
||||||
bandwidth: DecimalInteger,
|
|
||||||
average_bandwidth: Option<DecimalInteger>,
|
|
||||||
codecs: Option<QuotedString>,
|
|
||||||
resolution: Option<DecimalResolution>,
|
|
||||||
hdcp_level: Option<HdcpLevel>,
|
|
||||||
video: Option<QuotedString>,
|
|
||||||
}
|
|
||||||
impl ExtXIFrameStreamInf {
|
|
||||||
const PREFIX: &'static str = "#EXT-X-I-FRAME-STREAM-INF:";
|
|
||||||
}
|
|
||||||
impl fmt::Display for ExtXIFrameStreamInf {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
|
||||||
write!(f, "URI={}", self.uri)?;
|
|
||||||
write!(f, ",BANDWIDTH={}", self.bandwidth)?;
|
|
||||||
if let Some(ref x) = self.average_bandwidth {
|
|
||||||
write!(f, ",AVERAGE-BANDWIDTH={}", x)?;
|
|
||||||
}
|
|
||||||
if let Some(ref x) = self.codecs {
|
|
||||||
write!(f, ",CODECS={}", x)?;
|
|
||||||
}
|
|
||||||
if let Some(ref x) = self.resolution {
|
|
||||||
write!(f, ",RESOLUTION={}", x)?;
|
|
||||||
}
|
|
||||||
if let Some(ref x) = self.hdcp_level {
|
|
||||||
write!(f, ",HDCP-LEVEL={}", x)?;
|
|
||||||
}
|
|
||||||
if let Some(ref x) = self.video {
|
|
||||||
write!(f, ",VIDEO={}", x)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for ExtXIFrameStreamInf {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
|
||||||
|
|
||||||
let mut uri = None;
|
|
||||||
let mut bandwidth = None;
|
|
||||||
let mut average_bandwidth = None;
|
|
||||||
let mut codecs = None;
|
|
||||||
let mut resolution = None;
|
|
||||||
let mut hdcp_level = None;
|
|
||||||
let mut video = None;
|
|
||||||
let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1);
|
|
||||||
for attr in attrs {
|
|
||||||
let (key, value) = track!(attr)?;
|
|
||||||
match key {
|
|
||||||
"URI" => uri = Some(track!(value.parse())?),
|
|
||||||
"BANDWIDTH" => bandwidth = Some(track!(value.parse())?),
|
|
||||||
"AVERAGE-BANDWIDTH" => average_bandwidth = Some(track!(value.parse())?),
|
|
||||||
"CODECS" => codecs = Some(track!(value.parse())?),
|
|
||||||
"RESOLUTION" => resolution = Some(track!(value.parse())?),
|
|
||||||
"HDCP-LEVEL" => hdcp_level = Some(track!(value.parse())?),
|
|
||||||
"VIDEO" => {
|
|
||||||
// TODO:
|
|
||||||
video = Some(track!(value.parse())?);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// [6.3.1] ignore any attribute/value pair with an unrecognized AttributeName.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let uri = track_assert_some!(uri, ErrorKind::InvalidInput);
|
|
||||||
let bandwidth = track_assert_some!(bandwidth, ErrorKind::InvalidInput);
|
|
||||||
Ok(ExtXIFrameStreamInf {
|
|
||||||
uri,
|
|
||||||
bandwidth,
|
|
||||||
average_bandwidth,
|
|
||||||
codecs,
|
|
||||||
resolution,
|
|
||||||
hdcp_level,
|
|
||||||
video,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum SessionData {
|
|
||||||
Value(QuotedString),
|
|
||||||
Uri(QuotedString),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct ExtXSessionData {
|
|
||||||
pub data_id: QuotedString,
|
|
||||||
pub data: SessionData,
|
|
||||||
pub language: Option<QuotedString>,
|
|
||||||
}
|
|
||||||
impl ExtXSessionData {
|
|
||||||
const PREFIX: &'static str = "#EXT-X-SESSION-DATA:";
|
|
||||||
}
|
|
||||||
impl fmt::Display for ExtXSessionData {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
|
||||||
write!(f, "DATA-ID={}", self.data_id)?;
|
|
||||||
match self.data {
|
|
||||||
SessionData::Value(ref x) => write!(f, ",VALUE={}", x)?,
|
|
||||||
SessionData::Uri(ref x) => write!(f, ",URI={}", x)?,
|
|
||||||
}
|
|
||||||
if let Some(ref x) = self.language {
|
|
||||||
write!(f, ",LANGUAGE={}", x)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for ExtXSessionData {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
|
||||||
|
|
||||||
let mut data_id = None;
|
|
||||||
let mut session_value = None;
|
|
||||||
let mut uri = None;
|
|
||||||
let mut language = None;
|
|
||||||
let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1);
|
|
||||||
for attr in attrs {
|
|
||||||
let (key, value) = track!(attr)?;
|
|
||||||
match key {
|
|
||||||
"DATA-ID" => data_id = Some(track!(value.parse())?),
|
|
||||||
"VALUE" => session_value = Some(track!(value.parse())?),
|
|
||||||
"URI" => uri = Some(track!(value.parse())?),
|
|
||||||
"LANGUAGE" => language = Some(track!(value.parse())?),
|
|
||||||
_ => {
|
|
||||||
// [6.3.1] ignore any attribute/value pair with an unrecognized AttributeName.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let data_id = track_assert_some!(data_id, ErrorKind::InvalidInput);
|
|
||||||
let data = if let Some(value) = session_value {
|
|
||||||
track_assert_eq!(uri, None, ErrorKind::InvalidInput);
|
|
||||||
SessionData::Value(value)
|
|
||||||
} else if let Some(uri) = uri {
|
|
||||||
SessionData::Uri(uri)
|
|
||||||
} else {
|
|
||||||
track_panic!(ErrorKind::InvalidInput);
|
|
||||||
};
|
|
||||||
Ok(ExtXSessionData {
|
|
||||||
data_id,
|
|
||||||
data,
|
|
||||||
language,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct ExtXSessionKey {
|
|
||||||
pub method: SessionEncryptionMethod,
|
|
||||||
pub uri: QuotedString,
|
|
||||||
pub iv: Option<HexadecimalSequence>,
|
|
||||||
pub key_format: Option<QuotedString>,
|
|
||||||
pub key_format_versions: Option<QuotedString>,
|
|
||||||
}
|
|
||||||
impl ExtXSessionKey {
|
|
||||||
const PREFIX: &'static str = "#EXT-X-SESSION-KEY:";
|
|
||||||
}
|
|
||||||
impl fmt::Display for ExtXSessionKey {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
|
||||||
write!(f, "METHOD={}", self.method)?;
|
|
||||||
write!(f, ",URI={}", self.uri)?;
|
|
||||||
if let Some(ref x) = self.iv {
|
|
||||||
write!(f, ",IV={}", x)?;
|
|
||||||
}
|
|
||||||
if let Some(ref x) = self.key_format {
|
|
||||||
write!(f, ",KEYFORMAT={}", x)?;
|
|
||||||
}
|
|
||||||
if let Some(ref x) = self.key_format_versions {
|
|
||||||
write!(f, ",KEYFORMATVERSIONS={}", x)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for ExtXSessionKey {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
|
||||||
|
|
||||||
let mut method = None;
|
|
||||||
let mut uri = None;
|
|
||||||
let mut iv = None;
|
|
||||||
let mut key_format = None;
|
|
||||||
let mut key_format_versions = None;
|
|
||||||
let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1);
|
|
||||||
for attr in attrs {
|
|
||||||
let (key, value) = track!(attr)?;
|
|
||||||
match key {
|
|
||||||
"METHOD" => {
|
|
||||||
track_assert_eq!(method, None, ErrorKind::InvalidInput);
|
|
||||||
method = Some(track!(value.parse())?);
|
|
||||||
}
|
|
||||||
"URI" => {
|
|
||||||
track_assert_eq!(uri, None, ErrorKind::InvalidInput);
|
|
||||||
uri = Some(track!(value.parse())?);
|
|
||||||
}
|
|
||||||
"IV" => {
|
|
||||||
// TODO: validate length(128-bit)
|
|
||||||
track_assert_eq!(iv, None, ErrorKind::InvalidInput);
|
|
||||||
iv = Some(track!(value.parse())?);
|
|
||||||
}
|
|
||||||
"KEYFORMAT" => {
|
|
||||||
track_assert_eq!(key_format, None, ErrorKind::InvalidInput);
|
|
||||||
key_format = Some(track!(value.parse())?);
|
|
||||||
}
|
|
||||||
"KEYFORMATVERSIONS" => {
|
|
||||||
track_assert_eq!(key_format_versions, None, ErrorKind::InvalidInput);
|
|
||||||
key_format_versions = Some(track!(value.parse())?);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// [6.3.1] ignore any attribute/value pair with an unrecognized AttributeName.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let method = track_assert_some!(method, ErrorKind::InvalidInput);
|
|
||||||
let uri = track_assert_some!(uri, ErrorKind::InvalidInput);
|
|
||||||
Ok(ExtXSessionKey {
|
|
||||||
method,
|
|
||||||
uri,
|
|
||||||
iv,
|
|
||||||
key_format,
|
|
||||||
key_format_versions,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4.3.5. Media or Master Playlist Tags
|
// 4.3.5. Media or Master Playlist Tags
|
||||||
// TODO: A tag that appears in both MUST have the same value; otherwise, clients SHOULD ignore the value in the Media Playlist(s).
|
// TODO: A tag that appears in both MUST have the same value; otherwise, clients SHOULD ignore the value in the Media Playlist(s).
|
||||||
// TODO: These tags MUST NOT appear more than once in a Playlist.
|
// TODO: These tags MUST NOT appear more than once in a Playlist.
|
||||||
|
@ -867,32 +296,3 @@ impl FromStr for ExtXStart {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: move
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub enum SessionEncryptionMethod {
|
|
||||||
Aes128,
|
|
||||||
SampleAes,
|
|
||||||
}
|
|
||||||
impl fmt::Display for SessionEncryptionMethod {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match *self {
|
|
||||||
SessionEncryptionMethod::Aes128 => "AES-128".fmt(f),
|
|
||||||
SessionEncryptionMethod::SampleAes => "SAMPLE-AES".fmt(f),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for SessionEncryptionMethod {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
match s {
|
|
||||||
"AES-128" => Ok(SessionEncryptionMethod::Aes128),
|
|
||||||
"SAMPLE-AES" => Ok(SessionEncryptionMethod::SampleAes),
|
|
||||||
_ => track_panic!(
|
|
||||||
ErrorKind::InvalidInput,
|
|
||||||
"Unknown encryption method: {:?}",
|
|
||||||
s
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
264
src/types.rs
264
src/types.rs
|
@ -261,3 +261,267 @@ impl FromStr for PlaylistType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum MediaType {
|
||||||
|
Audio,
|
||||||
|
Video,
|
||||||
|
Subtitles,
|
||||||
|
ClosedCaptions,
|
||||||
|
}
|
||||||
|
impl fmt::Display for MediaType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
MediaType::Audio => "AUDIO".fmt(f),
|
||||||
|
MediaType::Video => "VIDEO".fmt(f),
|
||||||
|
MediaType::Subtitles => "SUBTITLES".fmt(f),
|
||||||
|
MediaType::ClosedCaptions => "CLOSED-CAPTIONS".fmt(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for MediaType {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
Ok(match s {
|
||||||
|
"AUDIO" => MediaType::Audio,
|
||||||
|
"VIDEO" => MediaType::Video,
|
||||||
|
"SUBTITLES" => MediaType::Subtitles,
|
||||||
|
"CLOSED-CAPTIONS" => MediaType::ClosedCaptions,
|
||||||
|
_ => track_panic!(ErrorKind::InvalidInput, "Unknown media type: {:?}", s),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: remove
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum YesOrNo {
|
||||||
|
Yes,
|
||||||
|
No,
|
||||||
|
}
|
||||||
|
impl fmt::Display for YesOrNo {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
YesOrNo::Yes => "YES".fmt(f),
|
||||||
|
YesOrNo::No => "NO".fmt(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for YesOrNo {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
match s {
|
||||||
|
"YES" => Ok(YesOrNo::Yes),
|
||||||
|
"NO" => Ok(YesOrNo::No),
|
||||||
|
_ => track_panic!(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"Unexpected enumerated-string: {:?}",
|
||||||
|
s
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum InStreamId {
|
||||||
|
Cc1,
|
||||||
|
Cc2,
|
||||||
|
Cc3,
|
||||||
|
Cc4,
|
||||||
|
Service1,
|
||||||
|
Service2,
|
||||||
|
Service3,
|
||||||
|
Service4,
|
||||||
|
Service5,
|
||||||
|
Service6,
|
||||||
|
Service7,
|
||||||
|
Service8,
|
||||||
|
Service9,
|
||||||
|
Service10,
|
||||||
|
Service11,
|
||||||
|
Service12,
|
||||||
|
Service13,
|
||||||
|
Service14,
|
||||||
|
Service15,
|
||||||
|
Service16,
|
||||||
|
Service17,
|
||||||
|
Service18,
|
||||||
|
Service19,
|
||||||
|
Service20,
|
||||||
|
Service21,
|
||||||
|
Service22,
|
||||||
|
Service23,
|
||||||
|
Service24,
|
||||||
|
Service25,
|
||||||
|
Service26,
|
||||||
|
Service27,
|
||||||
|
Service28,
|
||||||
|
Service29,
|
||||||
|
Service30,
|
||||||
|
Service31,
|
||||||
|
Service32,
|
||||||
|
Service33,
|
||||||
|
Service34,
|
||||||
|
Service35,
|
||||||
|
Service36,
|
||||||
|
Service37,
|
||||||
|
Service38,
|
||||||
|
Service39,
|
||||||
|
Service40,
|
||||||
|
Service41,
|
||||||
|
Service42,
|
||||||
|
Service43,
|
||||||
|
Service44,
|
||||||
|
Service45,
|
||||||
|
Service46,
|
||||||
|
Service47,
|
||||||
|
Service48,
|
||||||
|
Service49,
|
||||||
|
Service50,
|
||||||
|
Service51,
|
||||||
|
Service52,
|
||||||
|
Service53,
|
||||||
|
Service54,
|
||||||
|
Service55,
|
||||||
|
Service56,
|
||||||
|
Service57,
|
||||||
|
Service58,
|
||||||
|
Service59,
|
||||||
|
Service60,
|
||||||
|
Service61,
|
||||||
|
Service62,
|
||||||
|
Service63,
|
||||||
|
}
|
||||||
|
impl fmt::Display for InStreamId {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
format!("{:?}", self).to_uppercase().fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for InStreamId {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
Ok(match s {
|
||||||
|
"CC1" => InStreamId::Cc1,
|
||||||
|
"CC2" => InStreamId::Cc2,
|
||||||
|
"CC3" => InStreamId::Cc3,
|
||||||
|
"CC4" => InStreamId::Cc4,
|
||||||
|
"SERVICE1" => InStreamId::Service1,
|
||||||
|
"SERVICE2" => InStreamId::Service2,
|
||||||
|
"SERVICE3" => InStreamId::Service3,
|
||||||
|
"SERVICE4" => InStreamId::Service4,
|
||||||
|
"SERVICE5" => InStreamId::Service5,
|
||||||
|
"SERVICE6" => InStreamId::Service6,
|
||||||
|
"SERVICE7" => InStreamId::Service7,
|
||||||
|
"SERVICE8" => InStreamId::Service8,
|
||||||
|
"SERVICE9" => InStreamId::Service9,
|
||||||
|
"SERVICE10" => InStreamId::Service10,
|
||||||
|
"SERVICE11" => InStreamId::Service11,
|
||||||
|
"SERVICE12" => InStreamId::Service12,
|
||||||
|
"SERVICE13" => InStreamId::Service13,
|
||||||
|
"SERVICE14" => InStreamId::Service14,
|
||||||
|
"SERVICE15" => InStreamId::Service15,
|
||||||
|
"SERVICE16" => InStreamId::Service16,
|
||||||
|
"SERVICE17" => InStreamId::Service17,
|
||||||
|
"SERVICE18" => InStreamId::Service18,
|
||||||
|
"SERVICE19" => InStreamId::Service19,
|
||||||
|
"SERVICE20" => InStreamId::Service20,
|
||||||
|
"SERVICE21" => InStreamId::Service21,
|
||||||
|
"SERVICE22" => InStreamId::Service22,
|
||||||
|
"SERVICE23" => InStreamId::Service23,
|
||||||
|
"SERVICE24" => InStreamId::Service24,
|
||||||
|
"SERVICE25" => InStreamId::Service25,
|
||||||
|
"SERVICE26" => InStreamId::Service26,
|
||||||
|
"SERVICE27" => InStreamId::Service27,
|
||||||
|
"SERVICE28" => InStreamId::Service28,
|
||||||
|
"SERVICE29" => InStreamId::Service29,
|
||||||
|
"SERVICE30" => InStreamId::Service30,
|
||||||
|
"SERVICE31" => InStreamId::Service31,
|
||||||
|
"SERVICE32" => InStreamId::Service32,
|
||||||
|
"SERVICE33" => InStreamId::Service33,
|
||||||
|
"SERVICE34" => InStreamId::Service34,
|
||||||
|
"SERVICE35" => InStreamId::Service35,
|
||||||
|
"SERVICE36" => InStreamId::Service36,
|
||||||
|
"SERVICE37" => InStreamId::Service37,
|
||||||
|
"SERVICE38" => InStreamId::Service38,
|
||||||
|
"SERVICE39" => InStreamId::Service39,
|
||||||
|
"SERVICE40" => InStreamId::Service40,
|
||||||
|
"SERVICE41" => InStreamId::Service41,
|
||||||
|
"SERVICE42" => InStreamId::Service42,
|
||||||
|
"SERVICE43" => InStreamId::Service43,
|
||||||
|
"SERVICE44" => InStreamId::Service44,
|
||||||
|
"SERVICE45" => InStreamId::Service45,
|
||||||
|
"SERVICE46" => InStreamId::Service46,
|
||||||
|
"SERVICE47" => InStreamId::Service47,
|
||||||
|
"SERVICE48" => InStreamId::Service48,
|
||||||
|
"SERVICE49" => InStreamId::Service49,
|
||||||
|
"SERVICE50" => InStreamId::Service50,
|
||||||
|
"SERVICE51" => InStreamId::Service51,
|
||||||
|
"SERVICE52" => InStreamId::Service52,
|
||||||
|
"SERVICE53" => InStreamId::Service53,
|
||||||
|
"SERVICE54" => InStreamId::Service54,
|
||||||
|
"SERVICE55" => InStreamId::Service55,
|
||||||
|
"SERVICE56" => InStreamId::Service56,
|
||||||
|
"SERVICE57" => InStreamId::Service57,
|
||||||
|
"SERVICE58" => InStreamId::Service58,
|
||||||
|
"SERVICE59" => InStreamId::Service59,
|
||||||
|
"SERVICE60" => InStreamId::Service60,
|
||||||
|
"SERVICE61" => InStreamId::Service61,
|
||||||
|
"SERVICE62" => InStreamId::Service62,
|
||||||
|
"SERVICE63" => InStreamId::Service63,
|
||||||
|
_ => track_panic!(ErrorKind::InvalidInput, "Unknown instream id: {:?}", s),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum HdcpLevel {
|
||||||
|
Type0,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
impl fmt::Display for HdcpLevel {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
HdcpLevel::Type0 => "TYPE-0".fmt(f),
|
||||||
|
HdcpLevel::None => "NONE".fmt(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for HdcpLevel {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
match s {
|
||||||
|
"TYPE-0" => Ok(HdcpLevel::Type0),
|
||||||
|
"NONE" => Ok(HdcpLevel::None),
|
||||||
|
_ => track_panic!(ErrorKind::InvalidInput, "Unknown HDCP level: {:?}", s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub enum ClosedCaptions {
|
||||||
|
GroupId(QuotedString),
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
impl fmt::Display for ClosedCaptions {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
ClosedCaptions::GroupId(ref x) => x.fmt(f),
|
||||||
|
ClosedCaptions::None => "NONE".fmt(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for ClosedCaptions {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
if s == "NONE" {
|
||||||
|
Ok(ClosedCaptions::None)
|
||||||
|
} else {
|
||||||
|
Ok(ClosedCaptions::GroupId(track!(s.parse())?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub enum SessionData {
|
||||||
|
Value(QuotedString),
|
||||||
|
Uri(QuotedString),
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue