mirror of
https://github.com/sile/hls_m3u8.git
synced 2024-09-25 21:19:59 +00:00
Merge pull request #12 from Luro02/declutter
Added some tests and moved types + tags into separate folders
This commit is contained in:
commit
861e7e4b74
52 changed files with 3358 additions and 2748 deletions
|
@ -38,6 +38,7 @@ mod line;
|
||||||
mod master_playlist;
|
mod master_playlist;
|
||||||
mod media_playlist;
|
mod media_playlist;
|
||||||
mod media_segment;
|
mod media_segment;
|
||||||
|
mod utils;
|
||||||
|
|
||||||
/// This crate specific `Result` type.
|
/// This crate specific `Result` type.
|
||||||
pub type Result<T> = std::result::Result<T, Error>;
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
42
src/tags/basic/m3u.rs
Normal file
42
src/tags/basic/m3u.rs
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
use crate::types::ProtocolVersion;
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// [4.3.1.1. EXTM3U]
|
||||||
|
///
|
||||||
|
/// [4.3.1.1. EXTM3U]: https://tools.ietf.org/html/rfc8216#section-4.3.1.1
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ExtM3u;
|
||||||
|
impl ExtM3u {
|
||||||
|
pub(crate) const PREFIX: &'static str = "#EXTM3U";
|
||||||
|
|
||||||
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
|
pub fn requires_version(self) -> ProtocolVersion {
|
||||||
|
ProtocolVersion::V1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl fmt::Display for ExtM3u {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
Self::PREFIX.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for ExtM3u {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput);
|
||||||
|
Ok(ExtM3u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extm3u() {
|
||||||
|
assert_eq!("#EXTM3U".parse::<ExtM3u>().ok(), Some(ExtM3u));
|
||||||
|
assert_eq!(ExtM3u.to_string(), "#EXTM3U");
|
||||||
|
assert_eq!(ExtM3u.requires_version(), ProtocolVersion::V1);
|
||||||
|
}
|
||||||
|
}
|
5
src/tags/basic/mod.rs
Normal file
5
src/tags/basic/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
mod m3u;
|
||||||
|
mod version;
|
||||||
|
|
||||||
|
pub use m3u::*;
|
||||||
|
pub use version::*;
|
|
@ -3,32 +3,6 @@ use crate::{Error, ErrorKind, Result};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
/// [4.3.1.1. EXTM3U]
|
|
||||||
///
|
|
||||||
/// [4.3.1.1. EXTM3U]: https://tools.ietf.org/html/rfc8216#section-4.3.1.1
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub struct ExtM3u;
|
|
||||||
impl ExtM3u {
|
|
||||||
pub(crate) const PREFIX: &'static str = "#EXTM3U";
|
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
|
||||||
pub fn requires_version(self) -> ProtocolVersion {
|
|
||||||
ProtocolVersion::V1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl fmt::Display for ExtM3u {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
Self::PREFIX.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for ExtM3u {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput);
|
|
||||||
Ok(ExtM3u)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [4.3.1.2. EXT-X-VERSION]
|
/// [4.3.1.2. EXT-X-VERSION]
|
||||||
///
|
///
|
||||||
/// [4.3.1.2. EXT-X-VERSION]: https://tools.ietf.org/html/rfc8216#section-4.3.1.2
|
/// [4.3.1.2. EXT-X-VERSION]: https://tools.ietf.org/html/rfc8216#section-4.3.1.2
|
||||||
|
@ -36,6 +10,7 @@ impl FromStr for ExtM3u {
|
||||||
pub struct ExtXVersion {
|
pub struct ExtXVersion {
|
||||||
version: ProtocolVersion,
|
version: ProtocolVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExtXVersion {
|
impl ExtXVersion {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-VERSION:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-VERSION:";
|
||||||
|
|
||||||
|
@ -54,11 +29,13 @@ impl ExtXVersion {
|
||||||
ProtocolVersion::V1
|
ProtocolVersion::V1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXVersion {
|
impl fmt::Display for ExtXVersion {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(f, "{}{}", Self::PREFIX, self.version)
|
write!(f, "{}{}", Self::PREFIX, self.version)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXVersion {
|
impl FromStr for ExtXVersion {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
@ -73,13 +50,6 @@ impl FromStr for ExtXVersion {
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn extm3u() {
|
|
||||||
assert_eq!("#EXTM3U".parse::<ExtM3u>().ok(), Some(ExtM3u));
|
|
||||||
assert_eq!(ExtM3u.to_string(), "#EXTM3U");
|
|
||||||
assert_eq!(ExtM3u.requires_version(), ProtocolVersion::V1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ext_x_version() {
|
fn ext_x_version() {
|
||||||
let tag = ExtXVersion::new(ProtocolVersion::V6);
|
let tag = ExtXVersion::new(ProtocolVersion::V6);
|
|
@ -1,918 +0,0 @@
|
||||||
use super::{parse_u64, parse_yes_or_no};
|
|
||||||
use crate::attribute::AttributePairs;
|
|
||||||
use crate::types::{
|
|
||||||
ClosedCaptions, DecimalFloatingPoint, DecimalResolution, DecryptionKey, HdcpLevel, InStreamId,
|
|
||||||
MediaType, ProtocolVersion, QuotedString, SessionData, SingleLineString,
|
|
||||||
};
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
/// `ExtXMedia` builder.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ExtXMediaBuilder {
|
|
||||||
media_type: Option<MediaType>,
|
|
||||||
uri: Option<QuotedString>,
|
|
||||||
group_id: Option<QuotedString>,
|
|
||||||
language: Option<QuotedString>,
|
|
||||||
assoc_language: Option<QuotedString>,
|
|
||||||
name: Option<QuotedString>,
|
|
||||||
default: bool,
|
|
||||||
autoselect: Option<bool>,
|
|
||||||
forced: Option<bool>,
|
|
||||||
instream_id: Option<InStreamId>,
|
|
||||||
characteristics: Option<QuotedString>,
|
|
||||||
channels: Option<QuotedString>,
|
|
||||||
}
|
|
||||||
impl ExtXMediaBuilder {
|
|
||||||
/// Makes a `ExtXMediaBuilder` instance.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
ExtXMediaBuilder {
|
|
||||||
media_type: None,
|
|
||||||
uri: None,
|
|
||||||
group_id: None,
|
|
||||||
language: None,
|
|
||||||
assoc_language: None,
|
|
||||||
name: None,
|
|
||||||
default: false,
|
|
||||||
autoselect: None,
|
|
||||||
forced: None,
|
|
||||||
instream_id: None,
|
|
||||||
characteristics: None,
|
|
||||||
channels: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the media type of the rendition.
|
|
||||||
pub fn media_type(&mut self, media_type: MediaType) -> &mut Self {
|
|
||||||
self.media_type = Some(media_type);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the identifier that specifies the group to which the rendition belongs.
|
|
||||||
pub fn group_id(&mut self, group_id: QuotedString) -> &mut Self {
|
|
||||||
self.group_id = Some(group_id);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets a human-readable description of the rendition.
|
|
||||||
pub fn name(&mut self, name: QuotedString) -> &mut Self {
|
|
||||||
self.name = Some(name);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the URI that identifies the media playlist.
|
|
||||||
pub fn uri(&mut self, uri: QuotedString) -> &mut Self {
|
|
||||||
self.uri = Some(uri);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the name of the primary language used in the rendition.
|
|
||||||
pub fn language(&mut self, language: QuotedString) -> &mut Self {
|
|
||||||
self.language = Some(language);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the name of a language associated with the rendition.
|
|
||||||
pub fn assoc_language(&mut self, language: QuotedString) -> &mut Self {
|
|
||||||
self.assoc_language = Some(language);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the value of the `default` flag.
|
|
||||||
pub fn default(&mut self, b: bool) -> &mut Self {
|
|
||||||
self.default = b;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the value of the `autoselect` flag.
|
|
||||||
pub fn autoselect(&mut self, b: bool) -> &mut Self {
|
|
||||||
self.autoselect = Some(b);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the value of the `forced` flag.
|
|
||||||
pub fn forced(&mut self, b: bool) -> &mut Self {
|
|
||||||
self.forced = Some(b);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the identifier that specifies a rendition within the segments in the media playlist.
|
|
||||||
pub fn instream_id(&mut self, id: InStreamId) -> &mut Self {
|
|
||||||
self.instream_id = Some(id);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the string that represents uniform type identifiers (UTI).
|
|
||||||
pub fn characteristics(&mut self, characteristics: QuotedString) -> &mut Self {
|
|
||||||
self.characteristics = Some(characteristics);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the string that represents the parameters of the rendition.
|
|
||||||
pub fn channels(&mut self, channels: QuotedString) -> &mut Self {
|
|
||||||
self.channels = Some(channels);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Builds a `ExtXMedia` instance.
|
|
||||||
pub fn finish(self) -> Result<ExtXMedia> {
|
|
||||||
let media_type = track_assert_some!(self.media_type, ErrorKind::InvalidInput);
|
|
||||||
let group_id = track_assert_some!(self.group_id, ErrorKind::InvalidInput);
|
|
||||||
let name = track_assert_some!(self.name, ErrorKind::InvalidInput);
|
|
||||||
if MediaType::ClosedCaptions == media_type {
|
|
||||||
track_assert_ne!(self.uri, None, ErrorKind::InvalidInput);
|
|
||||||
track_assert!(self.instream_id.is_some(), ErrorKind::InvalidInput);
|
|
||||||
} else {
|
|
||||||
track_assert!(self.instream_id.is_none(), ErrorKind::InvalidInput);
|
|
||||||
}
|
|
||||||
if self.default && self.autoselect.is_some() {
|
|
||||||
track_assert_eq!(self.autoselect, Some(true), ErrorKind::InvalidInput);
|
|
||||||
}
|
|
||||||
if MediaType::Subtitles != media_type {
|
|
||||||
track_assert_eq!(self.forced, None, ErrorKind::InvalidInput);
|
|
||||||
}
|
|
||||||
Ok(ExtXMedia {
|
|
||||||
media_type,
|
|
||||||
uri: self.uri,
|
|
||||||
group_id,
|
|
||||||
language: self.language,
|
|
||||||
assoc_language: self.assoc_language,
|
|
||||||
name,
|
|
||||||
default: self.default,
|
|
||||||
autoselect: self.autoselect.unwrap_or(false),
|
|
||||||
forced: self.forced.unwrap_or(false),
|
|
||||||
instream_id: self.instream_id,
|
|
||||||
characteristics: self.characteristics,
|
|
||||||
channels: self.channels,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Default for ExtXMediaBuilder {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [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
|
|
||||||
#[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: bool,
|
|
||||||
autoselect: bool,
|
|
||||||
forced: bool,
|
|
||||||
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: false,
|
|
||||||
autoselect: false,
|
|
||||||
forced: false,
|
|
||||||
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) -> bool {
|
|
||||||
self.default
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether the client may choose to
|
|
||||||
/// play this rendition in the absence of explicit user preference.
|
|
||||||
pub fn autoselect(&self) -> bool {
|
|
||||||
self.autoselect
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns whether the rendition contains content that is considered essential to play.
|
|
||||||
pub fn forced(&self) -> bool {
|
|
||||||
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 self.default {
|
|
||||||
write!(f, ",DEFAULT=YES")?;
|
|
||||||
}
|
|
||||||
if self.autoselect {
|
|
||||||
write!(f, ",AUTOSELECT=YES")?;
|
|
||||||
}
|
|
||||||
if 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 builder = ExtXMediaBuilder::new();
|
|
||||||
let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1);
|
|
||||||
for attr in attrs {
|
|
||||||
let (key, value) = track!(attr)?;
|
|
||||||
match key {
|
|
||||||
"TYPE" => {
|
|
||||||
builder.media_type(track!(value.parse())?);
|
|
||||||
}
|
|
||||||
"URI" => {
|
|
||||||
builder.uri(track!(value.parse())?);
|
|
||||||
}
|
|
||||||
"GROUP-ID" => {
|
|
||||||
builder.group_id(track!(value.parse())?);
|
|
||||||
}
|
|
||||||
"LANGUAGE" => {
|
|
||||||
builder.language(track!(value.parse())?);
|
|
||||||
}
|
|
||||||
"ASSOC-LANGUAGE" => {
|
|
||||||
builder.assoc_language(track!(value.parse())?);
|
|
||||||
}
|
|
||||||
"NAME" => {
|
|
||||||
builder.name(track!(value.parse())?);
|
|
||||||
}
|
|
||||||
"DEFAULT" => {
|
|
||||||
builder.default(track!(parse_yes_or_no(value))?);
|
|
||||||
}
|
|
||||||
"AUTOSELECT" => {
|
|
||||||
builder.autoselect(track!(parse_yes_or_no(value))?);
|
|
||||||
}
|
|
||||||
"FORCED" => {
|
|
||||||
builder.forced(track!(parse_yes_or_no(value))?);
|
|
||||||
}
|
|
||||||
"INSTREAM-ID" => {
|
|
||||||
let s: QuotedString = track!(value.parse())?;
|
|
||||||
builder.instream_id(track!(s.parse())?);
|
|
||||||
}
|
|
||||||
"CHARACTERISTICS" => {
|
|
||||||
builder.characteristics(track!(value.parse())?);
|
|
||||||
}
|
|
||||||
"CHANNELS" => {
|
|
||||||
builder.channels(track!(value.parse())?);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// [6.3.1. General Client Responsibilities]
|
|
||||||
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
track!(builder.finish())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [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: SingleLineString,
|
|
||||||
bandwidth: u64,
|
|
||||||
average_bandwidth: Option<u64>,
|
|
||||||
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: SingleLineString, bandwidth: u64) -> 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) -> &SingleLineString {
|
|
||||||
&self.uri
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the peak segment bit rate of the variant stream.
|
|
||||||
pub fn bandwidth(&self) -> u64 {
|
|
||||||
self.bandwidth
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the average segment bit rate of the variant stream.
|
|
||||||
pub fn average_bandwidth(&self) -> Option<u64> {
|
|
||||||
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');
|
|
||||||
let first_line = lines.next().expect("Never fails").trim_end_matches('\r');
|
|
||||||
let second_line = track_assert_some!(lines.next(), ErrorKind::InvalidInput);
|
|
||||||
|
|
||||||
track_assert!(
|
|
||||||
first_line.starts_with(Self::PREFIX),
|
|
||||||
ErrorKind::InvalidInput
|
|
||||||
);
|
|
||||||
let uri = track!(SingleLineString::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!(parse_u64(value))?),
|
|
||||||
"AVERAGE-BANDWIDTH" => average_bandwidth = Some(track!(parse_u64(value))?),
|
|
||||||
"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: u64,
|
|
||||||
average_bandwidth: Option<u64>,
|
|
||||||
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: u64) -> 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) -> u64 {
|
|
||||||
self.bandwidth
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the average segment bit rate of the variant stream.
|
|
||||||
pub fn average_bandwidth(&self) -> Option<u64> {
|
|
||||||
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!(parse_u64(value))?),
|
|
||||||
"AVERAGE-BANDWIDTH" => average_bandwidth = Some(track!(parse_u64(value))?),
|
|
||||||
"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 super::*;
|
|
||||||
use crate::types::{EncryptionMethod, InitializationVector};
|
|
||||||
|
|
||||||
#[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(SingleLineString::new("foo").unwrap(), 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"), 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(InitializationVector([
|
|
||||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
|
|
||||||
])),
|
|
||||||
key_format: None,
|
|
||||||
key_format_versions: None,
|
|
||||||
});
|
|
||||||
let text =
|
|
||||||
r#"#EXT-X-SESSION-KEY:METHOD=AES-128,URI="foo",IV=0x000102030405060708090a0b0c0d0e0f"#;
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
163
src/tags/master_playlist/i_frame_stream_inf.rs
Normal file
163
src/tags/master_playlist/i_frame_stream_inf.rs
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
use crate::attribute::AttributePairs;
|
||||||
|
use crate::types::{DecimalResolution, HdcpLevel, ProtocolVersion, QuotedString};
|
||||||
|
use crate::utils::parse_u64;
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// [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: u64,
|
||||||
|
average_bandwidth: Option<u64>,
|
||||||
|
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: u64) -> 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) -> u64 {
|
||||||
|
self.bandwidth
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the average segment bit rate of the variant stream.
|
||||||
|
pub fn average_bandwidth(&self) -> Option<u64> {
|
||||||
|
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!(parse_u64(value))?),
|
||||||
|
"AVERAGE-BANDWIDTH" => average_bandwidth = Some(track!(parse_u64(value))?),
|
||||||
|
"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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ext_x_i_frame_stream_inf() {
|
||||||
|
let tag = ExtXIFrameStreamInf::new(quoted_string("foo"), 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quoted_string(s: &str) -> QuotedString {
|
||||||
|
QuotedString::new(s).unwrap()
|
||||||
|
}
|
||||||
|
}
|
382
src/tags/master_playlist/media.rs
Normal file
382
src/tags/master_playlist/media.rs
Normal file
|
@ -0,0 +1,382 @@
|
||||||
|
use crate::attribute::AttributePairs;
|
||||||
|
use crate::types::{InStreamId, MediaType, ProtocolVersion, QuotedString};
|
||||||
|
use crate::utils::parse_yes_or_no;
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// `ExtXMedia` builder.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ExtXMediaBuilder {
|
||||||
|
media_type: Option<MediaType>,
|
||||||
|
uri: Option<QuotedString>,
|
||||||
|
group_id: Option<QuotedString>,
|
||||||
|
language: Option<QuotedString>,
|
||||||
|
assoc_language: Option<QuotedString>,
|
||||||
|
name: Option<QuotedString>,
|
||||||
|
default: bool,
|
||||||
|
autoselect: Option<bool>,
|
||||||
|
forced: Option<bool>,
|
||||||
|
instream_id: Option<InStreamId>,
|
||||||
|
characteristics: Option<QuotedString>,
|
||||||
|
channels: Option<QuotedString>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtXMediaBuilder {
|
||||||
|
/// Makes a `ExtXMediaBuilder` instance.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
ExtXMediaBuilder {
|
||||||
|
media_type: None,
|
||||||
|
uri: None,
|
||||||
|
group_id: None,
|
||||||
|
language: None,
|
||||||
|
assoc_language: None,
|
||||||
|
name: None,
|
||||||
|
default: false,
|
||||||
|
autoselect: None,
|
||||||
|
forced: None,
|
||||||
|
instream_id: None,
|
||||||
|
characteristics: None,
|
||||||
|
channels: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the media type of the rendition.
|
||||||
|
pub fn media_type(&mut self, media_type: MediaType) -> &mut Self {
|
||||||
|
self.media_type = Some(media_type);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the identifier that specifies the group to which the rendition belongs.
|
||||||
|
pub fn group_id(&mut self, group_id: QuotedString) -> &mut Self {
|
||||||
|
self.group_id = Some(group_id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a human-readable description of the rendition.
|
||||||
|
pub fn name(&mut self, name: QuotedString) -> &mut Self {
|
||||||
|
self.name = Some(name);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the URI that identifies the media playlist.
|
||||||
|
pub fn uri(&mut self, uri: QuotedString) -> &mut Self {
|
||||||
|
self.uri = Some(uri);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the name of the primary language used in the rendition.
|
||||||
|
pub fn language(&mut self, language: QuotedString) -> &mut Self {
|
||||||
|
self.language = Some(language);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the name of a language associated with the rendition.
|
||||||
|
pub fn assoc_language(&mut self, language: QuotedString) -> &mut Self {
|
||||||
|
self.assoc_language = Some(language);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the value of the `default` flag.
|
||||||
|
pub fn default(&mut self, b: bool) -> &mut Self {
|
||||||
|
self.default = b;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the value of the `autoselect` flag.
|
||||||
|
pub fn autoselect(&mut self, b: bool) -> &mut Self {
|
||||||
|
self.autoselect = Some(b);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the value of the `forced` flag.
|
||||||
|
pub fn forced(&mut self, b: bool) -> &mut Self {
|
||||||
|
self.forced = Some(b);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the identifier that specifies a rendition within the segments in the media playlist.
|
||||||
|
pub fn instream_id(&mut self, id: InStreamId) -> &mut Self {
|
||||||
|
self.instream_id = Some(id);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the string that represents uniform type identifiers (UTI).
|
||||||
|
pub fn characteristics(&mut self, characteristics: QuotedString) -> &mut Self {
|
||||||
|
self.characteristics = Some(characteristics);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the string that represents the parameters of the rendition.
|
||||||
|
pub fn channels(&mut self, channels: QuotedString) -> &mut Self {
|
||||||
|
self.channels = Some(channels);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a `ExtXMedia` instance.
|
||||||
|
pub fn finish(self) -> Result<ExtXMedia> {
|
||||||
|
let media_type = track_assert_some!(self.media_type, ErrorKind::InvalidInput);
|
||||||
|
let group_id = track_assert_some!(self.group_id, ErrorKind::InvalidInput);
|
||||||
|
let name = track_assert_some!(self.name, ErrorKind::InvalidInput);
|
||||||
|
if MediaType::ClosedCaptions == media_type {
|
||||||
|
track_assert_ne!(self.uri, None, ErrorKind::InvalidInput);
|
||||||
|
track_assert!(self.instream_id.is_some(), ErrorKind::InvalidInput);
|
||||||
|
} else {
|
||||||
|
track_assert!(self.instream_id.is_none(), ErrorKind::InvalidInput);
|
||||||
|
}
|
||||||
|
if self.default && self.autoselect.is_some() {
|
||||||
|
track_assert_eq!(self.autoselect, Some(true), ErrorKind::InvalidInput);
|
||||||
|
}
|
||||||
|
if MediaType::Subtitles != media_type {
|
||||||
|
track_assert_eq!(self.forced, None, ErrorKind::InvalidInput);
|
||||||
|
}
|
||||||
|
Ok(ExtXMedia {
|
||||||
|
media_type,
|
||||||
|
uri: self.uri,
|
||||||
|
group_id,
|
||||||
|
language: self.language,
|
||||||
|
assoc_language: self.assoc_language,
|
||||||
|
name,
|
||||||
|
default: self.default,
|
||||||
|
autoselect: self.autoselect.unwrap_or(false),
|
||||||
|
forced: self.forced.unwrap_or(false),
|
||||||
|
instream_id: self.instream_id,
|
||||||
|
characteristics: self.characteristics,
|
||||||
|
channels: self.channels,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ExtXMediaBuilder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [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
|
||||||
|
#[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: bool,
|
||||||
|
autoselect: bool,
|
||||||
|
forced: bool,
|
||||||
|
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: false,
|
||||||
|
autoselect: false,
|
||||||
|
forced: false,
|
||||||
|
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) -> bool {
|
||||||
|
self.default
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether the client may choose to
|
||||||
|
/// play this rendition in the absence of explicit user preference.
|
||||||
|
pub fn autoselect(&self) -> bool {
|
||||||
|
self.autoselect
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether the rendition contains content that is considered essential to play.
|
||||||
|
pub fn forced(&self) -> bool {
|
||||||
|
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 self.default {
|
||||||
|
write!(f, ",DEFAULT=YES")?;
|
||||||
|
}
|
||||||
|
if self.autoselect {
|
||||||
|
write!(f, ",AUTOSELECT=YES")?;
|
||||||
|
}
|
||||||
|
if 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 builder = ExtXMediaBuilder::new();
|
||||||
|
let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1);
|
||||||
|
for attr in attrs {
|
||||||
|
let (key, value) = track!(attr)?;
|
||||||
|
match key {
|
||||||
|
"TYPE" => {
|
||||||
|
builder.media_type(track!(value.parse())?);
|
||||||
|
}
|
||||||
|
"URI" => {
|
||||||
|
builder.uri(track!(value.parse())?);
|
||||||
|
}
|
||||||
|
"GROUP-ID" => {
|
||||||
|
builder.group_id(track!(value.parse())?);
|
||||||
|
}
|
||||||
|
"LANGUAGE" => {
|
||||||
|
builder.language(track!(value.parse())?);
|
||||||
|
}
|
||||||
|
"ASSOC-LANGUAGE" => {
|
||||||
|
builder.assoc_language(track!(value.parse())?);
|
||||||
|
}
|
||||||
|
"NAME" => {
|
||||||
|
builder.name(track!(value.parse())?);
|
||||||
|
}
|
||||||
|
"DEFAULT" => {
|
||||||
|
builder.default(track!(parse_yes_or_no(value))?);
|
||||||
|
}
|
||||||
|
"AUTOSELECT" => {
|
||||||
|
builder.autoselect(track!(parse_yes_or_no(value))?);
|
||||||
|
}
|
||||||
|
"FORCED" => {
|
||||||
|
builder.forced(track!(parse_yes_or_no(value))?);
|
||||||
|
}
|
||||||
|
"INSTREAM-ID" => {
|
||||||
|
let s: QuotedString = track!(value.parse())?;
|
||||||
|
builder.instream_id(track!(s.parse())?);
|
||||||
|
}
|
||||||
|
"CHARACTERISTICS" => {
|
||||||
|
builder.characteristics(track!(value.parse())?);
|
||||||
|
}
|
||||||
|
"CHANNELS" => {
|
||||||
|
builder.channels(track!(value.parse())?);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// [6.3.1. General Client Responsibilities]
|
||||||
|
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
track!(builder.finish())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quoted_string(s: &str) -> QuotedString {
|
||||||
|
QuotedString::new(s).unwrap()
|
||||||
|
}
|
||||||
|
}
|
11
src/tags/master_playlist/mod.rs
Normal file
11
src/tags/master_playlist/mod.rs
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
mod i_frame_stream_inf;
|
||||||
|
mod media;
|
||||||
|
mod session_data;
|
||||||
|
mod session_key;
|
||||||
|
mod stream_inf;
|
||||||
|
|
||||||
|
pub use i_frame_stream_inf::*;
|
||||||
|
pub use media::*;
|
||||||
|
pub use session_data::*;
|
||||||
|
pub use session_key::*;
|
||||||
|
pub use stream_inf::*;
|
151
src/tags/master_playlist/session_data.rs
Normal file
151
src/tags/master_playlist/session_data.rs
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
use crate::attribute::AttributePairs;
|
||||||
|
use crate::types::{ProtocolVersion, QuotedString, SessionData};
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// [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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[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);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quoted_string(s: &str) -> QuotedString {
|
||||||
|
QuotedString::new(s).unwrap()
|
||||||
|
}
|
||||||
|
}
|
75
src/tags/master_playlist/session_key.rs
Normal file
75
src/tags/master_playlist/session_key.rs
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
use crate::types::{DecryptionKey, ProtocolVersion};
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// [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 super::*;
|
||||||
|
use crate::types::{EncryptionMethod, InitializationVector, QuotedString};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ext_x_session_key() {
|
||||||
|
let tag = ExtXSessionKey::new(DecryptionKey {
|
||||||
|
method: EncryptionMethod::Aes128,
|
||||||
|
uri: quoted_string("foo"),
|
||||||
|
iv: Some(InitializationVector([
|
||||||
|
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
|
||||||
|
])),
|
||||||
|
key_format: None,
|
||||||
|
key_format_versions: None,
|
||||||
|
});
|
||||||
|
let text =
|
||||||
|
r#"#EXT-X-SESSION-KEY:METHOD=AES-128,URI="foo",IV=0x000102030405060708090a0b0c0d0e0f"#;
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
217
src/tags/master_playlist/stream_inf.rs
Normal file
217
src/tags/master_playlist/stream_inf.rs
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
use crate::attribute::AttributePairs;
|
||||||
|
use crate::types::{
|
||||||
|
ClosedCaptions, DecimalFloatingPoint, DecimalResolution, HdcpLevel, ProtocolVersion,
|
||||||
|
QuotedString, SingleLineString,
|
||||||
|
};
|
||||||
|
use crate::utils::parse_u64;
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// [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: SingleLineString,
|
||||||
|
bandwidth: u64,
|
||||||
|
average_bandwidth: Option<u64>,
|
||||||
|
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: SingleLineString, bandwidth: u64) -> 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) -> &SingleLineString {
|
||||||
|
&self.uri
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the peak segment bit rate of the variant stream.
|
||||||
|
pub fn bandwidth(&self) -> u64 {
|
||||||
|
self.bandwidth
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the average segment bit rate of the variant stream.
|
||||||
|
pub fn average_bandwidth(&self) -> Option<u64> {
|
||||||
|
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');
|
||||||
|
let first_line = lines.next().expect("Never fails").trim_end_matches('\r');
|
||||||
|
let second_line = track_assert_some!(lines.next(), ErrorKind::InvalidInput);
|
||||||
|
|
||||||
|
track_assert!(
|
||||||
|
first_line.starts_with(Self::PREFIX),
|
||||||
|
ErrorKind::InvalidInput
|
||||||
|
);
|
||||||
|
let uri = track!(SingleLineString::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!(parse_u64(value))?),
|
||||||
|
"AVERAGE-BANDWIDTH" => average_bandwidth = Some(track!(parse_u64(value))?),
|
||||||
|
"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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ext_x_stream_inf() {
|
||||||
|
let tag = ExtXStreamInf::new(SingleLineString::new("foo").unwrap(), 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,279 +0,0 @@
|
||||||
use crate::types::{PlaylistType, ProtocolVersion};
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use std::time::Duration;
|
|
||||||
use trackable::error::ErrorKindExt;
|
|
||||||
|
|
||||||
/// [4.3.3.1. EXT-X-TARGETDURATION]
|
|
||||||
///
|
|
||||||
/// [4.3.3.1. EXT-X-TARGETDURATION]: https://tools.ietf.org/html/rfc8216#section-4.3.3.1
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub struct ExtXTargetDuration {
|
|
||||||
duration: Duration,
|
|
||||||
}
|
|
||||||
impl ExtXTargetDuration {
|
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-TARGETDURATION:";
|
|
||||||
|
|
||||||
/// Makes a new `ExtXTargetduration` tag.
|
|
||||||
///
|
|
||||||
/// Note that the nanoseconds part of the `duration` will be discarded.
|
|
||||||
pub fn new(duration: Duration) -> Self {
|
|
||||||
let duration = Duration::from_secs(duration.as_secs());
|
|
||||||
ExtXTargetDuration { duration }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the maximum media segment duration in the associated playlist.
|
|
||||||
pub fn duration(&self) -> Duration {
|
|
||||||
self.duration
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
|
||||||
pub fn requires_version(&self) -> ProtocolVersion {
|
|
||||||
ProtocolVersion::V1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl fmt::Display for ExtXTargetDuration {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}{}", Self::PREFIX, self.duration.as_secs())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for ExtXTargetDuration {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
|
||||||
let duration = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?;
|
|
||||||
Ok(ExtXTargetDuration {
|
|
||||||
duration: Duration::from_secs(duration),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [4.3.3.2. EXT-X-MEDIA-SEQUENCE]
|
|
||||||
///
|
|
||||||
/// [4.3.3.2. EXT-X-MEDIA-SEQUENCE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.2
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub struct ExtXMediaSequence {
|
|
||||||
seq_num: u64,
|
|
||||||
}
|
|
||||||
impl ExtXMediaSequence {
|
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA-SEQUENCE:";
|
|
||||||
|
|
||||||
/// Makes a new `ExtXMediaSequence` tag.
|
|
||||||
pub fn new(seq_num: u64) -> Self {
|
|
||||||
ExtXMediaSequence { seq_num }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the sequence number of the first media segment that appears in the associated playlist.
|
|
||||||
pub fn seq_num(self) -> u64 {
|
|
||||||
self.seq_num
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
|
||||||
pub fn requires_version(self) -> ProtocolVersion {
|
|
||||||
ProtocolVersion::V1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl fmt::Display for ExtXMediaSequence {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}{}", Self::PREFIX, self.seq_num)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for ExtXMediaSequence {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
|
||||||
let seq_num = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?;
|
|
||||||
Ok(ExtXMediaSequence { seq_num })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [4.3.3.3. EXT-X-DISCONTINUITY-SEQUENCE]
|
|
||||||
///
|
|
||||||
/// [4.3.3.3. EXT-X-DISCONTINUITY-SEQUENCE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.3
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub struct ExtXDiscontinuitySequence {
|
|
||||||
seq_num: u64,
|
|
||||||
}
|
|
||||||
impl ExtXDiscontinuitySequence {
|
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY-SEQUENCE:";
|
|
||||||
|
|
||||||
/// Makes a new `ExtXDiscontinuitySequence` tag.
|
|
||||||
pub fn new(seq_num: u64) -> Self {
|
|
||||||
ExtXDiscontinuitySequence { seq_num }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the discontinuity sequence number of
|
|
||||||
/// the first media segment that appears in the associated playlist.
|
|
||||||
pub fn seq_num(self) -> u64 {
|
|
||||||
self.seq_num
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
|
||||||
pub fn requires_version(self) -> ProtocolVersion {
|
|
||||||
ProtocolVersion::V1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl fmt::Display for ExtXDiscontinuitySequence {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}{}", Self::PREFIX, self.seq_num)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for ExtXDiscontinuitySequence {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
|
||||||
let seq_num = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?;
|
|
||||||
Ok(ExtXDiscontinuitySequence { seq_num })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [4.3.3.4. EXT-X-ENDLIST]
|
|
||||||
///
|
|
||||||
/// [4.3.3.4. EXT-X-ENDLIST]: https://tools.ietf.org/html/rfc8216#section-4.3.3.4
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub struct ExtXEndList;
|
|
||||||
impl ExtXEndList {
|
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-ENDLIST";
|
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
|
||||||
pub fn requires_version(self) -> ProtocolVersion {
|
|
||||||
ProtocolVersion::V1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl fmt::Display for ExtXEndList {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
Self::PREFIX.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for ExtXEndList {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput);
|
|
||||||
Ok(ExtXEndList)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [4.3.3.5. EXT-X-PLAYLIST-TYPE]
|
|
||||||
///
|
|
||||||
/// [4.3.3.5. EXT-X-PLAYLIST-TYPE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.5
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub struct ExtXPlaylistType {
|
|
||||||
playlist_type: PlaylistType,
|
|
||||||
}
|
|
||||||
impl ExtXPlaylistType {
|
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-PLAYLIST-TYPE:";
|
|
||||||
|
|
||||||
/// Makes a new `ExtXPlaylistType` tag.
|
|
||||||
pub fn new(playlist_type: PlaylistType) -> Self {
|
|
||||||
ExtXPlaylistType { playlist_type }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the type of the associated media playlist.
|
|
||||||
pub fn playlist_type(self) -> PlaylistType {
|
|
||||||
self.playlist_type
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
|
||||||
pub fn requires_version(self) -> ProtocolVersion {
|
|
||||||
ProtocolVersion::V1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl fmt::Display for ExtXPlaylistType {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}{}", Self::PREFIX, self.playlist_type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for ExtXPlaylistType {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
|
||||||
let playlist_type = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?;
|
|
||||||
Ok(ExtXPlaylistType { playlist_type })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [4.3.3.6. EXT-X-I-FRAMES-ONLY]
|
|
||||||
///
|
|
||||||
/// [4.3.3.6. EXT-X-I-FRAMES-ONLY]: https://tools.ietf.org/html/rfc8216#section-4.3.3.6
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub struct ExtXIFramesOnly;
|
|
||||||
impl ExtXIFramesOnly {
|
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAMES-ONLY";
|
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
|
||||||
pub fn requires_version(self) -> ProtocolVersion {
|
|
||||||
ProtocolVersion::V4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl fmt::Display for ExtXIFramesOnly {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
Self::PREFIX.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for ExtXIFramesOnly {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput);
|
|
||||||
Ok(ExtXIFramesOnly)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn ext_x_targetduration() {
|
|
||||||
let tag = ExtXTargetDuration::new(Duration::from_secs(5));
|
|
||||||
let text = "#EXT-X-TARGETDURATION:5";
|
|
||||||
assert_eq!(text.parse().ok(), Some(tag));
|
|
||||||
assert_eq!(tag.to_string(), text);
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn ext_x_media_sequence() {
|
|
||||||
let tag = ExtXMediaSequence::new(123);
|
|
||||||
let text = "#EXT-X-MEDIA-SEQUENCE:123";
|
|
||||||
assert_eq!(text.parse().ok(), Some(tag));
|
|
||||||
assert_eq!(tag.to_string(), text);
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn ext_x_discontinuity_sequence() {
|
|
||||||
let tag = ExtXDiscontinuitySequence::new(123);
|
|
||||||
let text = "#EXT-X-DISCONTINUITY-SEQUENCE:123";
|
|
||||||
assert_eq!(text.parse().ok(), Some(tag));
|
|
||||||
assert_eq!(tag.to_string(), text);
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn ext_x_endlist() {
|
|
||||||
let tag = ExtXEndList;
|
|
||||||
let text = "#EXT-X-ENDLIST";
|
|
||||||
assert_eq!(text.parse().ok(), Some(tag));
|
|
||||||
assert_eq!(tag.to_string(), text);
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn ext_x_playlist_type() {
|
|
||||||
let tag = ExtXPlaylistType::new(PlaylistType::Vod);
|
|
||||||
let text = "#EXT-X-PLAYLIST-TYPE:VOD";
|
|
||||||
assert_eq!(text.parse().ok(), Some(tag));
|
|
||||||
assert_eq!(tag.to_string(), text);
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn ext_i_frames_only() {
|
|
||||||
let tag = ExtXIFramesOnly;
|
|
||||||
let text = "#EXT-X-I-FRAMES-ONLY";
|
|
||||||
assert_eq!(text.parse().ok(), Some(tag));
|
|
||||||
assert_eq!(tag.to_string(), text);
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V4);
|
|
||||||
}
|
|
||||||
}
|
|
62
src/tags/media_playlist/discontinuity_sequence.rs
Normal file
62
src/tags/media_playlist/discontinuity_sequence.rs
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
use crate::types::ProtocolVersion;
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use trackable::error::ErrorKindExt;
|
||||||
|
|
||||||
|
/// [4.3.3.3. EXT-X-DISCONTINUITY-SEQUENCE]
|
||||||
|
///
|
||||||
|
/// [4.3.3.3. EXT-X-DISCONTINUITY-SEQUENCE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.3
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ExtXDiscontinuitySequence {
|
||||||
|
seq_num: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtXDiscontinuitySequence {
|
||||||
|
pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY-SEQUENCE:";
|
||||||
|
|
||||||
|
/// Makes a new `ExtXDiscontinuitySequence` tag.
|
||||||
|
pub fn new(seq_num: u64) -> Self {
|
||||||
|
ExtXDiscontinuitySequence { seq_num }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the discontinuity sequence number of
|
||||||
|
/// the first media segment that appears in the associated playlist.
|
||||||
|
pub fn seq_num(self) -> u64 {
|
||||||
|
self.seq_num
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
|
pub fn requires_version(self) -> ProtocolVersion {
|
||||||
|
ProtocolVersion::V1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ExtXDiscontinuitySequence {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}{}", Self::PREFIX, self.seq_num)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ExtXDiscontinuitySequence {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||||
|
let seq_num = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?;
|
||||||
|
Ok(ExtXDiscontinuitySequence { seq_num })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ext_x_discontinuity_sequence() {
|
||||||
|
let tag = ExtXDiscontinuitySequence::new(123);
|
||||||
|
let text = "#EXT-X-DISCONTINUITY-SEQUENCE:123";
|
||||||
|
assert_eq!(text.parse().ok(), Some(tag));
|
||||||
|
assert_eq!(tag.to_string(), text);
|
||||||
|
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||||
|
}
|
||||||
|
}
|
44
src/tags/media_playlist/end_list.rs
Normal file
44
src/tags/media_playlist/end_list.rs
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
use crate::types::ProtocolVersion;
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// [4.3.3.4. EXT-X-ENDLIST]
|
||||||
|
///
|
||||||
|
/// [4.3.3.4. EXT-X-ENDLIST]: https://tools.ietf.org/html/rfc8216#section-4.3.3.4
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ExtXEndList;
|
||||||
|
impl ExtXEndList {
|
||||||
|
pub(crate) const PREFIX: &'static str = "#EXT-X-ENDLIST";
|
||||||
|
|
||||||
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
|
pub fn requires_version(self) -> ProtocolVersion {
|
||||||
|
ProtocolVersion::V1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl fmt::Display for ExtXEndList {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
Self::PREFIX.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for ExtXEndList {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput);
|
||||||
|
Ok(ExtXEndList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ext_x_endlist() {
|
||||||
|
let tag = ExtXEndList;
|
||||||
|
let text = "#EXT-X-ENDLIST";
|
||||||
|
assert_eq!(text.parse().ok(), Some(tag));
|
||||||
|
assert_eq!(tag.to_string(), text);
|
||||||
|
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||||
|
}
|
||||||
|
}
|
47
src/tags/media_playlist/i_frames_only.rs
Normal file
47
src/tags/media_playlist/i_frames_only.rs
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
use crate::types::ProtocolVersion;
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// [4.3.3.6. EXT-X-I-FRAMES-ONLY]
|
||||||
|
///
|
||||||
|
/// [4.3.3.6. EXT-X-I-FRAMES-ONLY]: https://tools.ietf.org/html/rfc8216#section-4.3.3.6
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ExtXIFramesOnly;
|
||||||
|
|
||||||
|
impl ExtXIFramesOnly {
|
||||||
|
pub(crate) const PREFIX: &'static str = "#EXT-X-I-FRAMES-ONLY";
|
||||||
|
|
||||||
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
|
pub fn requires_version(self) -> ProtocolVersion {
|
||||||
|
ProtocolVersion::V4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ExtXIFramesOnly {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
Self::PREFIX.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ExtXIFramesOnly {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput);
|
||||||
|
Ok(ExtXIFramesOnly)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ext_i_frames_only() {
|
||||||
|
let tag = ExtXIFramesOnly;
|
||||||
|
let text = "#EXT-X-I-FRAMES-ONLY";
|
||||||
|
assert_eq!(text.parse().ok(), Some(tag));
|
||||||
|
assert_eq!(tag.to_string(), text);
|
||||||
|
assert_eq!(tag.requires_version(), ProtocolVersion::V4);
|
||||||
|
}
|
||||||
|
}
|
61
src/tags/media_playlist/media_sequence.rs
Normal file
61
src/tags/media_playlist/media_sequence.rs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
use crate::types::ProtocolVersion;
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use trackable::error::ErrorKindExt;
|
||||||
|
|
||||||
|
/// [4.3.3.2. EXT-X-MEDIA-SEQUENCE]
|
||||||
|
///
|
||||||
|
/// [4.3.3.2. EXT-X-MEDIA-SEQUENCE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.2
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ExtXMediaSequence {
|
||||||
|
seq_num: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtXMediaSequence {
|
||||||
|
pub(crate) const PREFIX: &'static str = "#EXT-X-MEDIA-SEQUENCE:";
|
||||||
|
|
||||||
|
/// Makes a new `ExtXMediaSequence` tag.
|
||||||
|
pub fn new(seq_num: u64) -> Self {
|
||||||
|
ExtXMediaSequence { seq_num }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the sequence number of the first media segment that appears in the associated playlist.
|
||||||
|
pub fn seq_num(self) -> u64 {
|
||||||
|
self.seq_num
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
|
pub fn requires_version(self) -> ProtocolVersion {
|
||||||
|
ProtocolVersion::V1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ExtXMediaSequence {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}{}", Self::PREFIX, self.seq_num)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ExtXMediaSequence {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||||
|
let seq_num = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?;
|
||||||
|
Ok(ExtXMediaSequence { seq_num })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ext_x_media_sequence() {
|
||||||
|
let tag = ExtXMediaSequence::new(123);
|
||||||
|
let text = "#EXT-X-MEDIA-SEQUENCE:123";
|
||||||
|
assert_eq!(text.parse().ok(), Some(tag));
|
||||||
|
assert_eq!(tag.to_string(), text);
|
||||||
|
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||||
|
}
|
||||||
|
}
|
13
src/tags/media_playlist/mod.rs
Normal file
13
src/tags/media_playlist/mod.rs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
mod discontinuity_sequence;
|
||||||
|
mod end_list;
|
||||||
|
mod i_frames_only;
|
||||||
|
mod media_sequence;
|
||||||
|
mod playlist_type;
|
||||||
|
mod target_duration;
|
||||||
|
|
||||||
|
pub use discontinuity_sequence::*;
|
||||||
|
pub use end_list::*;
|
||||||
|
pub use i_frames_only::*;
|
||||||
|
pub use media_sequence::*;
|
||||||
|
pub use playlist_type::*;
|
||||||
|
pub use target_duration::*;
|
61
src/tags/media_playlist/playlist_type.rs
Normal file
61
src/tags/media_playlist/playlist_type.rs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
use crate::types::{PlaylistType, ProtocolVersion};
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use trackable::error::ErrorKindExt;
|
||||||
|
|
||||||
|
/// [4.3.3.5. EXT-X-PLAYLIST-TYPE]
|
||||||
|
///
|
||||||
|
/// [4.3.3.5. EXT-X-PLAYLIST-TYPE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.5
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ExtXPlaylistType {
|
||||||
|
playlist_type: PlaylistType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtXPlaylistType {
|
||||||
|
pub(crate) const PREFIX: &'static str = "#EXT-X-PLAYLIST-TYPE:";
|
||||||
|
|
||||||
|
/// Makes a new `ExtXPlaylistType` tag.
|
||||||
|
pub fn new(playlist_type: PlaylistType) -> Self {
|
||||||
|
ExtXPlaylistType { playlist_type }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the type of the associated media playlist.
|
||||||
|
pub fn playlist_type(self) -> PlaylistType {
|
||||||
|
self.playlist_type
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
|
pub fn requires_version(self) -> ProtocolVersion {
|
||||||
|
ProtocolVersion::V1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ExtXPlaylistType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}{}", Self::PREFIX, self.playlist_type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ExtXPlaylistType {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||||
|
let playlist_type = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?;
|
||||||
|
Ok(ExtXPlaylistType { playlist_type })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ext_x_playlist_type() {
|
||||||
|
let tag = ExtXPlaylistType::new(PlaylistType::Vod);
|
||||||
|
let text = "#EXT-X-PLAYLIST-TYPE:VOD";
|
||||||
|
assert_eq!(text.parse().ok(), Some(tag));
|
||||||
|
assert_eq!(tag.to_string(), text);
|
||||||
|
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||||
|
}
|
||||||
|
}
|
67
src/tags/media_playlist/target_duration.rs
Normal file
67
src/tags/media_playlist/target_duration.rs
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
use crate::types::ProtocolVersion;
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::time::Duration;
|
||||||
|
use trackable::error::ErrorKindExt;
|
||||||
|
|
||||||
|
/// [4.3.3.1. EXT-X-TARGETDURATION]
|
||||||
|
///
|
||||||
|
/// [4.3.3.1. EXT-X-TARGETDURATION]: https://tools.ietf.org/html/rfc8216#section-4.3.3.1
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ExtXTargetDuration {
|
||||||
|
duration: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtXTargetDuration {
|
||||||
|
pub(crate) const PREFIX: &'static str = "#EXT-X-TARGETDURATION:";
|
||||||
|
|
||||||
|
/// Makes a new `ExtXTargetduration` tag.
|
||||||
|
///
|
||||||
|
/// Note that the nanoseconds part of the `duration` will be discarded.
|
||||||
|
pub fn new(duration: Duration) -> Self {
|
||||||
|
let duration = Duration::from_secs(duration.as_secs());
|
||||||
|
ExtXTargetDuration { duration }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the maximum media segment duration in the associated playlist.
|
||||||
|
pub fn duration(&self) -> Duration {
|
||||||
|
self.duration
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
|
pub fn requires_version(&self) -> ProtocolVersion {
|
||||||
|
ProtocolVersion::V1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ExtXTargetDuration {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}{}", Self::PREFIX, self.duration.as_secs())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ExtXTargetDuration {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||||
|
let duration = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?;
|
||||||
|
Ok(ExtXTargetDuration {
|
||||||
|
duration: Duration::from_secs(duration),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ext_x_targetduration() {
|
||||||
|
let tag = ExtXTargetDuration::new(Duration::from_secs(5));
|
||||||
|
let text = "#EXT-X-TARGETDURATION:5";
|
||||||
|
assert_eq!(text.parse().ok(), Some(tag));
|
||||||
|
assert_eq!(tag.to_string(), text);
|
||||||
|
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,613 +0,0 @@
|
||||||
use crate::attribute::AttributePairs;
|
|
||||||
use crate::types::{
|
|
||||||
ByteRange, DecimalFloatingPoint, DecryptionKey, ProtocolVersion, QuotedString, SingleLineString,
|
|
||||||
};
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::fmt;
|
|
||||||
use std::str::FromStr;
|
|
||||||
use std::time::Duration;
|
|
||||||
use trackable::error::ErrorKindExt;
|
|
||||||
|
|
||||||
/// [4.3.2.1. EXTINF]
|
|
||||||
///
|
|
||||||
/// [4.3.2.1. EXTINF]: https://tools.ietf.org/html/rfc8216#section-4.3.2.1
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
||||||
pub struct ExtInf {
|
|
||||||
duration: Duration,
|
|
||||||
title: Option<SingleLineString>,
|
|
||||||
}
|
|
||||||
impl ExtInf {
|
|
||||||
pub(crate) const PREFIX: &'static str = "#EXTINF:";
|
|
||||||
|
|
||||||
/// Makes a new `ExtInf` tag.
|
|
||||||
pub fn new(duration: Duration) -> Self {
|
|
||||||
ExtInf {
|
|
||||||
duration,
|
|
||||||
title: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Makes a new `ExtInf` tag with the given title.
|
|
||||||
pub fn with_title(duration: Duration, title: SingleLineString) -> Self {
|
|
||||||
ExtInf {
|
|
||||||
duration,
|
|
||||||
title: Some(title),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the duration of the associated media segment.
|
|
||||||
pub fn duration(&self) -> Duration {
|
|
||||||
self.duration
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the title of the associated media segment.
|
|
||||||
pub fn title(&self) -> Option<&SingleLineString> {
|
|
||||||
self.title.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
|
||||||
pub fn requires_version(&self) -> ProtocolVersion {
|
|
||||||
if self.duration.subsec_nanos() == 0 {
|
|
||||||
ProtocolVersion::V1
|
|
||||||
} else {
|
|
||||||
ProtocolVersion::V3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl fmt::Display for ExtInf {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
|
||||||
|
|
||||||
let duration = (self.duration.as_secs() as f64)
|
|
||||||
+ (f64::from(self.duration.subsec_nanos()) / 1_000_000_000.0);
|
|
||||||
write!(f, "{}", duration)?;
|
|
||||||
|
|
||||||
if let Some(ref title) = self.title {
|
|
||||||
write!(f, ",{}", title)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for ExtInf {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
|
||||||
let mut tokens = s.split_at(Self::PREFIX.len()).1.splitn(2, ',');
|
|
||||||
|
|
||||||
let seconds: DecimalFloatingPoint =
|
|
||||||
may_invalid!(tokens.next().expect("Never fails").parse())?;
|
|
||||||
let duration = seconds.to_duration();
|
|
||||||
|
|
||||||
let title = if let Some(title) = tokens.next() {
|
|
||||||
Some(track!(SingleLineString::new(title))?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
Ok(ExtInf { duration, title })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [4.3.2.2. EXT-X-BYTERANGE]
|
|
||||||
///
|
|
||||||
/// [4.3.2.2. EXT-X-BYTERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.2
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub struct ExtXByteRange {
|
|
||||||
range: ByteRange,
|
|
||||||
}
|
|
||||||
impl ExtXByteRange {
|
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-BYTERANGE:";
|
|
||||||
|
|
||||||
/// Makes a new `ExtXByteRange` tag.
|
|
||||||
pub fn new(range: ByteRange) -> Self {
|
|
||||||
ExtXByteRange { range }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the range of the associated media segment.
|
|
||||||
pub fn range(&self) -> ByteRange {
|
|
||||||
self.range
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
|
||||||
pub fn requires_version(&self) -> ProtocolVersion {
|
|
||||||
ProtocolVersion::V4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl fmt::Display for ExtXByteRange {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}{}", Self::PREFIX, self.range)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for ExtXByteRange {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
|
||||||
let range = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?;
|
|
||||||
Ok(ExtXByteRange { range })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [4.3.2.3. EXT-X-DISCONTINUITY]
|
|
||||||
///
|
|
||||||
/// [4.3.2.3. EXT-X-DISCONTINUITY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.3
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub struct ExtXDiscontinuity;
|
|
||||||
impl ExtXDiscontinuity {
|
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY";
|
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
|
||||||
pub fn requires_version(self) -> ProtocolVersion {
|
|
||||||
ProtocolVersion::V1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl fmt::Display for ExtXDiscontinuity {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
Self::PREFIX.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for ExtXDiscontinuity {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput);
|
|
||||||
Ok(ExtXDiscontinuity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [4.3.2.4. EXT-X-KEY]
|
|
||||||
///
|
|
||||||
/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
||||||
pub struct ExtXKey {
|
|
||||||
key: Option<DecryptionKey>,
|
|
||||||
}
|
|
||||||
impl ExtXKey {
|
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-KEY:";
|
|
||||||
|
|
||||||
/// Makes a new `ExtXKey` tag.
|
|
||||||
pub fn new(key: DecryptionKey) -> Self {
|
|
||||||
ExtXKey { key: Some(key) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Makes a new `ExtXKey` tag without a decryption key.
|
|
||||||
///
|
|
||||||
/// This tag has the `METHDO=NONE` attribute.
|
|
||||||
pub fn new_without_key() -> Self {
|
|
||||||
ExtXKey { key: None }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the decryption key for the following media segments and media initialization sections.
|
|
||||||
pub fn key(&self) -> Option<&DecryptionKey> {
|
|
||||||
self.key.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
|
||||||
pub fn requires_version(&self) -> ProtocolVersion {
|
|
||||||
self.key
|
|
||||||
.as_ref()
|
|
||||||
.map_or(ProtocolVersion::V1, |k| k.requires_version())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl fmt::Display for ExtXKey {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
|
||||||
if let Some(ref key) = self.key {
|
|
||||||
write!(f, "{}", key)?;
|
|
||||||
} else {
|
|
||||||
write!(f, "METHOD=NONE")?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for ExtXKey {
|
|
||||||
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;
|
|
||||||
|
|
||||||
if AttributePairs::parse(suffix).any(|a| a.as_ref().ok() == Some(&("METHOD", "NONE"))) {
|
|
||||||
for attr in AttributePairs::parse(suffix) {
|
|
||||||
let (key, _) = track!(attr)?;
|
|
||||||
track_assert_ne!(key, "URI", ErrorKind::InvalidInput);
|
|
||||||
track_assert_ne!(key, "IV", ErrorKind::InvalidInput);
|
|
||||||
track_assert_ne!(key, "KEYFORMAT", ErrorKind::InvalidInput);
|
|
||||||
track_assert_ne!(key, "KEYFORMATVERSIONS", ErrorKind::InvalidInput);
|
|
||||||
}
|
|
||||||
Ok(ExtXKey { key: None })
|
|
||||||
} else {
|
|
||||||
let key = track!(suffix.parse())?;
|
|
||||||
Ok(ExtXKey { key: Some(key) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [4.3.2.5. EXT-X-MAP]
|
|
||||||
///
|
|
||||||
/// [4.3.2.5. EXT-X-MAP]: https://tools.ietf.org/html/rfc8216#section-4.3.2.5
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
||||||
pub struct ExtXMap {
|
|
||||||
uri: QuotedString,
|
|
||||||
range: Option<ByteRange>,
|
|
||||||
}
|
|
||||||
impl ExtXMap {
|
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-MAP:";
|
|
||||||
|
|
||||||
/// Makes a new `ExtXMap` tag.
|
|
||||||
pub fn new(uri: QuotedString) -> Self {
|
|
||||||
ExtXMap { uri, range: None }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Makes a new `ExtXMap` tag with the given range.
|
|
||||||
pub fn with_range(uri: QuotedString, range: ByteRange) -> Self {
|
|
||||||
ExtXMap {
|
|
||||||
uri,
|
|
||||||
range: Some(range),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the URI that identifies a resource that contains the media initialization section.
|
|
||||||
pub fn uri(&self) -> &QuotedString {
|
|
||||||
&self.uri
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the range of the media initialization section.
|
|
||||||
pub fn range(&self) -> Option<ByteRange> {
|
|
||||||
self.range
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
|
||||||
pub fn requires_version(&self) -> ProtocolVersion {
|
|
||||||
ProtocolVersion::V6
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl fmt::Display for ExtXMap {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
|
||||||
write!(f, "URI={}", self.uri)?;
|
|
||||||
if let Some(ref x) = self.range {
|
|
||||||
write!(f, ",BYTERANGE=\"{}\"", x)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for ExtXMap {
|
|
||||||
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 range = 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())?),
|
|
||||||
"BYTERANGE" => {
|
|
||||||
let s: QuotedString = track!(value.parse())?;
|
|
||||||
range = Some(track!(s.parse())?);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// [6.3.1. General Client Responsibilities]
|
|
||||||
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let uri = track_assert_some!(uri, ErrorKind::InvalidInput);
|
|
||||||
Ok(ExtXMap { uri, range })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]
|
|
||||||
///
|
|
||||||
/// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]: https://tools.ietf.org/html/rfc8216#section-4.3.2.6
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
||||||
pub struct ExtXProgramDateTime {
|
|
||||||
date_time: SingleLineString,
|
|
||||||
}
|
|
||||||
impl ExtXProgramDateTime {
|
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-PROGRAM-DATE-TIME:";
|
|
||||||
|
|
||||||
/// Makes a new `ExtXProgramDateTime` tag.
|
|
||||||
pub fn new(date_time: SingleLineString) -> Self {
|
|
||||||
ExtXProgramDateTime { date_time }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the date-time of the first sample of the associated media segment.
|
|
||||||
pub fn date_time(&self) -> &SingleLineString {
|
|
||||||
&self.date_time
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
|
||||||
pub fn requires_version(&self) -> ProtocolVersion {
|
|
||||||
ProtocolVersion::V1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl fmt::Display for ExtXProgramDateTime {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}{}", Self::PREFIX, self.date_time)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for ExtXProgramDateTime {
|
|
||||||
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;
|
|
||||||
Ok(ExtXProgramDateTime {
|
|
||||||
date_time: track!(SingleLineString::new(suffix))?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [4.3.2.7. EXT-X-DATERANGE]
|
|
||||||
///
|
|
||||||
/// [4.3.2.7. EXT-X-DATERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.7
|
|
||||||
///
|
|
||||||
/// TODO: Implement properly
|
|
||||||
#[allow(missing_docs)]
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
||||||
pub struct ExtXDateRange {
|
|
||||||
pub id: QuotedString,
|
|
||||||
pub class: Option<QuotedString>,
|
|
||||||
pub start_date: QuotedString,
|
|
||||||
pub end_date: Option<QuotedString>,
|
|
||||||
pub duration: Option<Duration>,
|
|
||||||
pub planned_duration: Option<Duration>,
|
|
||||||
pub scte35_cmd: Option<QuotedString>,
|
|
||||||
pub scte35_out: Option<QuotedString>,
|
|
||||||
pub scte35_in: Option<QuotedString>,
|
|
||||||
pub end_on_next: bool,
|
|
||||||
pub client_attributes: BTreeMap<String, String>,
|
|
||||||
}
|
|
||||||
impl ExtXDateRange {
|
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-DATERANGE:";
|
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
|
||||||
pub fn requires_version(&self) -> ProtocolVersion {
|
|
||||||
ProtocolVersion::V1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl fmt::Display for ExtXDateRange {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
|
||||||
write!(f, "ID={}", self.id)?;
|
|
||||||
if let Some(ref x) = self.class {
|
|
||||||
write!(f, ",CLASS={}", x)?;
|
|
||||||
}
|
|
||||||
write!(f, ",START-DATE={}", self.start_date)?;
|
|
||||||
if let Some(ref x) = self.end_date {
|
|
||||||
write!(f, ",END-DATE={}", x)?;
|
|
||||||
}
|
|
||||||
if let Some(x) = self.duration {
|
|
||||||
write!(f, ",DURATION={}", DecimalFloatingPoint::from_duration(x))?;
|
|
||||||
}
|
|
||||||
if let Some(x) = self.planned_duration {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
",PLANNED-DURATION={}",
|
|
||||||
DecimalFloatingPoint::from_duration(x)
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
if let Some(ref x) = self.scte35_cmd {
|
|
||||||
write!(f, ",SCTE35-CMD={}", x)?;
|
|
||||||
}
|
|
||||||
if let Some(ref x) = self.scte35_out {
|
|
||||||
write!(f, ",SCTE35-OUT={}", x)?;
|
|
||||||
}
|
|
||||||
if let Some(ref x) = self.scte35_in {
|
|
||||||
write!(f, ",SCTE35-IN={}", x)?;
|
|
||||||
}
|
|
||||||
if self.end_on_next {
|
|
||||||
write!(f, ",END-ON-NEXT=YES",)?;
|
|
||||||
}
|
|
||||||
for (k, v) in &self.client_attributes {
|
|
||||||
write!(f, ",{}={}", k, v)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for ExtXDateRange {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
|
||||||
|
|
||||||
let mut id = None;
|
|
||||||
let mut class = None;
|
|
||||||
let mut start_date = None;
|
|
||||||
let mut end_date = None;
|
|
||||||
let mut duration = None;
|
|
||||||
let mut planned_duration = None;
|
|
||||||
let mut scte35_cmd = None;
|
|
||||||
let mut scte35_out = None;
|
|
||||||
let mut scte35_in = None;
|
|
||||||
let mut end_on_next = false;
|
|
||||||
let mut client_attributes = BTreeMap::new();
|
|
||||||
let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1);
|
|
||||||
for attr in attrs {
|
|
||||||
let (key, value) = track!(attr)?;
|
|
||||||
match key {
|
|
||||||
"ID" => id = Some(track!(value.parse())?),
|
|
||||||
"CLASS" => class = Some(track!(value.parse())?),
|
|
||||||
"START-DATE" => start_date = Some(track!(value.parse())?),
|
|
||||||
"END-DATE" => end_date = Some(track!(value.parse())?),
|
|
||||||
"DURATION" => {
|
|
||||||
let seconds: DecimalFloatingPoint = track!(value.parse())?;
|
|
||||||
duration = Some(seconds.to_duration());
|
|
||||||
}
|
|
||||||
"PLANNED-DURATION" => {
|
|
||||||
let seconds: DecimalFloatingPoint = track!(value.parse())?;
|
|
||||||
planned_duration = Some(seconds.to_duration());
|
|
||||||
}
|
|
||||||
"SCTE35-CMD" => scte35_cmd = Some(track!(value.parse())?),
|
|
||||||
"SCTE35-OUT" => scte35_out = Some(track!(value.parse())?),
|
|
||||||
"SCTE35-IN" => scte35_in = Some(track!(value.parse())?),
|
|
||||||
"END-ON-NEXT" => {
|
|
||||||
track_assert_eq!(value, "YES", ErrorKind::InvalidInput);
|
|
||||||
end_on_next = true;
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
if key.starts_with("X-") {
|
|
||||||
client_attributes.insert(key.split_at(2).1.to_owned(), value.to_owned());
|
|
||||||
} else {
|
|
||||||
// [6.3.1. General Client Responsibilities]
|
|
||||||
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let id = track_assert_some!(id, ErrorKind::InvalidInput);
|
|
||||||
let start_date = track_assert_some!(start_date, ErrorKind::InvalidInput);
|
|
||||||
if end_on_next {
|
|
||||||
track_assert!(class.is_some(), ErrorKind::InvalidInput);
|
|
||||||
}
|
|
||||||
Ok(ExtXDateRange {
|
|
||||||
id,
|
|
||||||
class,
|
|
||||||
start_date,
|
|
||||||
end_date,
|
|
||||||
duration,
|
|
||||||
planned_duration,
|
|
||||||
scte35_cmd,
|
|
||||||
scte35_out,
|
|
||||||
scte35_in,
|
|
||||||
end_on_next,
|
|
||||||
client_attributes,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
use crate::types::{EncryptionMethod, InitializationVector};
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn extinf() {
|
|
||||||
let tag = ExtInf::new(Duration::from_secs(5));
|
|
||||||
assert_eq!("#EXTINF:5".parse().ok(), Some(tag.clone()));
|
|
||||||
assert_eq!(tag.to_string(), "#EXTINF:5");
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
|
||||||
|
|
||||||
let tag = ExtInf::with_title(
|
|
||||||
Duration::from_secs(5),
|
|
||||||
SingleLineString::new("foo").unwrap(),
|
|
||||||
);
|
|
||||||
assert_eq!("#EXTINF:5,foo".parse().ok(), Some(tag.clone()));
|
|
||||||
assert_eq!(tag.to_string(), "#EXTINF:5,foo");
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
|
||||||
|
|
||||||
let tag = ExtInf::new(Duration::from_millis(1234));
|
|
||||||
assert_eq!("#EXTINF:1.234".parse().ok(), Some(tag.clone()));
|
|
||||||
assert_eq!(tag.to_string(), "#EXTINF:1.234");
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn ext_x_byterange() {
|
|
||||||
let tag = ExtXByteRange::new(ByteRange {
|
|
||||||
length: 3,
|
|
||||||
start: None,
|
|
||||||
});
|
|
||||||
assert_eq!("#EXT-X-BYTERANGE:3".parse().ok(), Some(tag));
|
|
||||||
assert_eq!(tag.to_string(), "#EXT-X-BYTERANGE:3");
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V4);
|
|
||||||
|
|
||||||
let tag = ExtXByteRange::new(ByteRange {
|
|
||||||
length: 3,
|
|
||||||
start: Some(5),
|
|
||||||
});
|
|
||||||
assert_eq!("#EXT-X-BYTERANGE:3@5".parse().ok(), Some(tag));
|
|
||||||
assert_eq!(tag.to_string(), "#EXT-X-BYTERANGE:3@5");
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V4);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn ext_x_discontinuity() {
|
|
||||||
let tag = ExtXDiscontinuity;
|
|
||||||
assert_eq!("#EXT-X-DISCONTINUITY".parse().ok(), Some(tag));
|
|
||||||
assert_eq!(tag.to_string(), "#EXT-X-DISCONTINUITY");
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn ext_x_key() {
|
|
||||||
let tag = ExtXKey::new_without_key();
|
|
||||||
let text = "#EXT-X-KEY:METHOD=NONE";
|
|
||||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
|
||||||
assert_eq!(tag.to_string(), text);
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
|
||||||
|
|
||||||
let tag = ExtXKey::new(DecryptionKey {
|
|
||||||
method: EncryptionMethod::Aes128,
|
|
||||||
uri: QuotedString::new("foo").unwrap(),
|
|
||||||
iv: None,
|
|
||||||
key_format: None,
|
|
||||||
key_format_versions: None,
|
|
||||||
});
|
|
||||||
let text = r#"#EXT-X-KEY:METHOD=AES-128,URI="foo""#;
|
|
||||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
|
||||||
assert_eq!(tag.to_string(), text);
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
|
||||||
|
|
||||||
let tag = ExtXKey::new(DecryptionKey {
|
|
||||||
method: EncryptionMethod::Aes128,
|
|
||||||
uri: QuotedString::new("foo").unwrap(),
|
|
||||||
iv: Some(InitializationVector([
|
|
||||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
|
|
||||||
])),
|
|
||||||
key_format: None,
|
|
||||||
key_format_versions: None,
|
|
||||||
});
|
|
||||||
let text = r#"#EXT-X-KEY:METHOD=AES-128,URI="foo",IV=0x000102030405060708090a0b0c0d0e0f"#;
|
|
||||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
|
||||||
assert_eq!(tag.to_string(), text);
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V2);
|
|
||||||
|
|
||||||
let tag = ExtXKey::new(DecryptionKey {
|
|
||||||
method: EncryptionMethod::Aes128,
|
|
||||||
uri: QuotedString::new("foo").unwrap(),
|
|
||||||
iv: Some(InitializationVector([
|
|
||||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
|
|
||||||
])),
|
|
||||||
key_format: Some(QuotedString::new("baz").unwrap()),
|
|
||||||
key_format_versions: None,
|
|
||||||
});
|
|
||||||
let text = r#"#EXT-X-KEY:METHOD=AES-128,URI="foo",IV=0x000102030405060708090a0b0c0d0e0f,KEYFORMAT="baz""#;
|
|
||||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
|
||||||
assert_eq!(tag.to_string(), text);
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V5);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn ext_x_map() {
|
|
||||||
let tag = ExtXMap::new(QuotedString::new("foo").unwrap());
|
|
||||||
let text = r#"#EXT-X-MAP:URI="foo""#;
|
|
||||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
|
||||||
assert_eq!(tag.to_string(), text);
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V6);
|
|
||||||
|
|
||||||
let tag = ExtXMap::with_range(
|
|
||||||
QuotedString::new("foo").unwrap(),
|
|
||||||
ByteRange {
|
|
||||||
length: 9,
|
|
||||||
start: Some(2),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
let text = r#"#EXT-X-MAP:URI="foo",BYTERANGE="9@2""#;
|
|
||||||
track_try_unwrap!(ExtXMap::from_str(text));
|
|
||||||
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
|
||||||
assert_eq!(tag.to_string(), text);
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V6);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn ext_x_program_date_time() {
|
|
||||||
let text = "#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00";
|
|
||||||
assert!(text.parse::<ExtXProgramDateTime>().is_ok());
|
|
||||||
|
|
||||||
let tag = text.parse::<ExtXProgramDateTime>().unwrap();
|
|
||||||
assert_eq!(tag.to_string(), text);
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
|
||||||
}
|
|
||||||
}
|
|
71
src/tags/media_segment/byte_range.rs
Normal file
71
src/tags/media_segment/byte_range.rs
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
use crate::types::{ByteRange, ProtocolVersion};
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use trackable::error::ErrorKindExt;
|
||||||
|
|
||||||
|
/// [4.3.2.2. EXT-X-BYTERANGE]
|
||||||
|
///
|
||||||
|
/// [4.3.2.2. EXT-X-BYTERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.2
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ExtXByteRange {
|
||||||
|
range: ByteRange,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtXByteRange {
|
||||||
|
pub(crate) const PREFIX: &'static str = "#EXT-X-BYTERANGE:";
|
||||||
|
|
||||||
|
/// Makes a new `ExtXByteRange` tag.
|
||||||
|
pub fn new(range: ByteRange) -> Self {
|
||||||
|
ExtXByteRange { range }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the range of the associated media segment.
|
||||||
|
pub fn range(&self) -> ByteRange {
|
||||||
|
self.range
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
|
pub fn requires_version(&self) -> ProtocolVersion {
|
||||||
|
ProtocolVersion::V4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ExtXByteRange {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}{}", Self::PREFIX, self.range)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ExtXByteRange {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||||
|
let range = may_invalid!(s.split_at(Self::PREFIX.len()).1.parse())?;
|
||||||
|
Ok(ExtXByteRange { range })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ext_x_byterange() {
|
||||||
|
let tag = ExtXByteRange::new(ByteRange {
|
||||||
|
length: 3,
|
||||||
|
start: None,
|
||||||
|
});
|
||||||
|
assert_eq!("#EXT-X-BYTERANGE:3".parse().ok(), Some(tag));
|
||||||
|
assert_eq!(tag.to_string(), "#EXT-X-BYTERANGE:3");
|
||||||
|
assert_eq!(tag.requires_version(), ProtocolVersion::V4);
|
||||||
|
|
||||||
|
let tag = ExtXByteRange::new(ByteRange {
|
||||||
|
length: 3,
|
||||||
|
start: Some(5),
|
||||||
|
});
|
||||||
|
assert_eq!("#EXT-X-BYTERANGE:3@5".parse().ok(), Some(tag));
|
||||||
|
assert_eq!(tag.to_string(), "#EXT-X-BYTERANGE:3@5");
|
||||||
|
assert_eq!(tag.requires_version(), ProtocolVersion::V4);
|
||||||
|
}
|
||||||
|
}
|
156
src/tags/media_segment/date_range.rs
Normal file
156
src/tags/media_segment/date_range.rs
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
use crate::attribute::AttributePairs;
|
||||||
|
use crate::types::{DecimalFloatingPoint, ProtocolVersion, QuotedString};
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// [4.3.2.7. EXT-X-DATERANGE]
|
||||||
|
///
|
||||||
|
/// [4.3.2.7. EXT-X-DATERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.7
|
||||||
|
///
|
||||||
|
/// TODO: Implement properly
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ExtXDateRange {
|
||||||
|
pub id: QuotedString,
|
||||||
|
pub class: Option<QuotedString>,
|
||||||
|
pub start_date: QuotedString,
|
||||||
|
pub end_date: Option<QuotedString>,
|
||||||
|
pub duration: Option<Duration>,
|
||||||
|
pub planned_duration: Option<Duration>,
|
||||||
|
pub scte35_cmd: Option<QuotedString>,
|
||||||
|
pub scte35_out: Option<QuotedString>,
|
||||||
|
pub scte35_in: Option<QuotedString>,
|
||||||
|
pub end_on_next: bool,
|
||||||
|
pub client_attributes: BTreeMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtXDateRange {
|
||||||
|
pub(crate) const PREFIX: &'static str = "#EXT-X-DATERANGE:";
|
||||||
|
|
||||||
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
|
pub fn requires_version(&self) -> ProtocolVersion {
|
||||||
|
ProtocolVersion::V1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ExtXDateRange {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
|
write!(f, "ID={}", self.id)?;
|
||||||
|
if let Some(ref x) = self.class {
|
||||||
|
write!(f, ",CLASS={}", x)?;
|
||||||
|
}
|
||||||
|
write!(f, ",START-DATE={}", self.start_date)?;
|
||||||
|
if let Some(ref x) = self.end_date {
|
||||||
|
write!(f, ",END-DATE={}", x)?;
|
||||||
|
}
|
||||||
|
if let Some(x) = self.duration {
|
||||||
|
write!(f, ",DURATION={}", DecimalFloatingPoint::from_duration(x))?;
|
||||||
|
}
|
||||||
|
if let Some(x) = self.planned_duration {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
",PLANNED-DURATION={}",
|
||||||
|
DecimalFloatingPoint::from_duration(x)
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
if let Some(ref x) = self.scte35_cmd {
|
||||||
|
write!(f, ",SCTE35-CMD={}", x)?;
|
||||||
|
}
|
||||||
|
if let Some(ref x) = self.scte35_out {
|
||||||
|
write!(f, ",SCTE35-OUT={}", x)?;
|
||||||
|
}
|
||||||
|
if let Some(ref x) = self.scte35_in {
|
||||||
|
write!(f, ",SCTE35-IN={}", x)?;
|
||||||
|
}
|
||||||
|
if self.end_on_next {
|
||||||
|
write!(f, ",END-ON-NEXT=YES",)?;
|
||||||
|
}
|
||||||
|
for (k, v) in &self.client_attributes {
|
||||||
|
write!(f, ",{}={}", k, v)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ExtXDateRange {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||||
|
|
||||||
|
let mut id = None;
|
||||||
|
let mut class = None;
|
||||||
|
let mut start_date = None;
|
||||||
|
let mut end_date = None;
|
||||||
|
let mut duration = None;
|
||||||
|
let mut planned_duration = None;
|
||||||
|
let mut scte35_cmd = None;
|
||||||
|
let mut scte35_out = None;
|
||||||
|
let mut scte35_in = None;
|
||||||
|
let mut end_on_next = false;
|
||||||
|
let mut client_attributes = BTreeMap::new();
|
||||||
|
let attrs = AttributePairs::parse(s.split_at(Self::PREFIX.len()).1);
|
||||||
|
for attr in attrs {
|
||||||
|
let (key, value) = track!(attr)?;
|
||||||
|
match key {
|
||||||
|
"ID" => id = Some(track!(value.parse())?),
|
||||||
|
"CLASS" => class = Some(track!(value.parse())?),
|
||||||
|
"START-DATE" => start_date = Some(track!(value.parse())?),
|
||||||
|
"END-DATE" => end_date = Some(track!(value.parse())?),
|
||||||
|
"DURATION" => {
|
||||||
|
let seconds: DecimalFloatingPoint = track!(value.parse())?;
|
||||||
|
duration = Some(seconds.to_duration());
|
||||||
|
}
|
||||||
|
"PLANNED-DURATION" => {
|
||||||
|
let seconds: DecimalFloatingPoint = track!(value.parse())?;
|
||||||
|
planned_duration = Some(seconds.to_duration());
|
||||||
|
}
|
||||||
|
"SCTE35-CMD" => scte35_cmd = Some(track!(value.parse())?),
|
||||||
|
"SCTE35-OUT" => scte35_out = Some(track!(value.parse())?),
|
||||||
|
"SCTE35-IN" => scte35_in = Some(track!(value.parse())?),
|
||||||
|
"END-ON-NEXT" => {
|
||||||
|
track_assert_eq!(value, "YES", ErrorKind::InvalidInput);
|
||||||
|
end_on_next = true;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if key.starts_with("X-") {
|
||||||
|
client_attributes.insert(key.split_at(2).1.to_owned(), value.to_owned());
|
||||||
|
} else {
|
||||||
|
// [6.3.1. General Client Responsibilities]
|
||||||
|
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = track_assert_some!(id, ErrorKind::InvalidInput);
|
||||||
|
let start_date = track_assert_some!(start_date, ErrorKind::InvalidInput);
|
||||||
|
if end_on_next {
|
||||||
|
track_assert!(class.is_some(), ErrorKind::InvalidInput);
|
||||||
|
}
|
||||||
|
Ok(ExtXDateRange {
|
||||||
|
id,
|
||||||
|
class,
|
||||||
|
start_date,
|
||||||
|
end_date,
|
||||||
|
duration,
|
||||||
|
planned_duration,
|
||||||
|
scte35_cmd,
|
||||||
|
scte35_out,
|
||||||
|
scte35_in,
|
||||||
|
end_on_next,
|
||||||
|
client_attributes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test] // TODO; write some tests
|
||||||
|
fn it_works() {}
|
||||||
|
}
|
43
src/tags/media_segment/discontinuity.rs
Normal file
43
src/tags/media_segment/discontinuity.rs
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
use crate::types::ProtocolVersion;
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// [4.3.2.3. EXT-X-DISCONTINUITY]
|
||||||
|
///
|
||||||
|
/// [4.3.2.3. EXT-X-DISCONTINUITY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.3
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ExtXDiscontinuity;
|
||||||
|
impl ExtXDiscontinuity {
|
||||||
|
pub(crate) const PREFIX: &'static str = "#EXT-X-DISCONTINUITY";
|
||||||
|
|
||||||
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
|
pub fn requires_version(self) -> ProtocolVersion {
|
||||||
|
ProtocolVersion::V1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl fmt::Display for ExtXDiscontinuity {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
Self::PREFIX.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for ExtXDiscontinuity {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput);
|
||||||
|
Ok(ExtXDiscontinuity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ext_x_discontinuity() {
|
||||||
|
let tag = ExtXDiscontinuity;
|
||||||
|
assert_eq!("#EXT-X-DISCONTINUITY".parse().ok(), Some(tag));
|
||||||
|
assert_eq!(tag.to_string(), "#EXT-X-DISCONTINUITY");
|
||||||
|
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||||
|
}
|
||||||
|
}
|
114
src/tags/media_segment/inf.rs
Normal file
114
src/tags/media_segment/inf.rs
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
use crate::types::{DecimalFloatingPoint, ProtocolVersion, SingleLineString};
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use std::time::Duration;
|
||||||
|
use trackable::error::ErrorKindExt;
|
||||||
|
|
||||||
|
/// [4.3.2.1. EXTINF]
|
||||||
|
///
|
||||||
|
/// [4.3.2.1. EXTINF]: https://tools.ietf.org/html/rfc8216#section-4.3.2.1
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ExtInf {
|
||||||
|
duration: Duration,
|
||||||
|
title: Option<SingleLineString>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtInf {
|
||||||
|
pub(crate) const PREFIX: &'static str = "#EXTINF:";
|
||||||
|
|
||||||
|
/// Makes a new `ExtInf` tag.
|
||||||
|
pub fn new(duration: Duration) -> Self {
|
||||||
|
ExtInf {
|
||||||
|
duration,
|
||||||
|
title: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes a new `ExtInf` tag with the given title.
|
||||||
|
pub fn with_title(duration: Duration, title: SingleLineString) -> Self {
|
||||||
|
ExtInf {
|
||||||
|
duration,
|
||||||
|
title: Some(title),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the duration of the associated media segment.
|
||||||
|
pub fn duration(&self) -> Duration {
|
||||||
|
self.duration
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the title of the associated media segment.
|
||||||
|
pub fn title(&self) -> Option<&SingleLineString> {
|
||||||
|
self.title.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
|
pub fn requires_version(&self) -> ProtocolVersion {
|
||||||
|
if self.duration.subsec_nanos() == 0 {
|
||||||
|
ProtocolVersion::V1
|
||||||
|
} else {
|
||||||
|
ProtocolVersion::V3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ExtInf {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
|
|
||||||
|
let duration = (self.duration.as_secs() as f64)
|
||||||
|
+ (f64::from(self.duration.subsec_nanos()) / 1_000_000_000.0);
|
||||||
|
write!(f, "{}", duration)?;
|
||||||
|
|
||||||
|
if let Some(ref title) = self.title {
|
||||||
|
write!(f, ",{}", title)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ExtInf {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert!(s.starts_with(Self::PREFIX), ErrorKind::InvalidInput);
|
||||||
|
let mut tokens = s.split_at(Self::PREFIX.len()).1.splitn(2, ',');
|
||||||
|
|
||||||
|
let seconds: DecimalFloatingPoint =
|
||||||
|
may_invalid!(tokens.next().expect("Never fails").parse())?;
|
||||||
|
let duration = seconds.to_duration();
|
||||||
|
|
||||||
|
let title = if let Some(title) = tokens.next() {
|
||||||
|
Some(track!(SingleLineString::new(title))?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
Ok(ExtInf { duration, title })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extinf() {
|
||||||
|
let tag = ExtInf::new(Duration::from_secs(5));
|
||||||
|
assert_eq!("#EXTINF:5".parse().ok(), Some(tag.clone()));
|
||||||
|
assert_eq!(tag.to_string(), "#EXTINF:5");
|
||||||
|
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||||
|
|
||||||
|
let tag = ExtInf::with_title(
|
||||||
|
Duration::from_secs(5),
|
||||||
|
SingleLineString::new("foo").unwrap(),
|
||||||
|
);
|
||||||
|
assert_eq!("#EXTINF:5,foo".parse().ok(), Some(tag.clone()));
|
||||||
|
assert_eq!(tag.to_string(), "#EXTINF:5,foo");
|
||||||
|
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||||
|
|
||||||
|
let tag = ExtInf::new(Duration::from_millis(1234));
|
||||||
|
assert_eq!("#EXTINF:1.234".parse().ok(), Some(tag.clone()));
|
||||||
|
assert_eq!(tag.to_string(), "#EXTINF:1.234");
|
||||||
|
assert_eq!(tag.requires_version(), ProtocolVersion::V3);
|
||||||
|
}
|
||||||
|
}
|
130
src/tags/media_segment/key.rs
Normal file
130
src/tags/media_segment/key.rs
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
use crate::attribute::AttributePairs;
|
||||||
|
use crate::types::{DecryptionKey, ProtocolVersion};
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// [4.3.2.4. EXT-X-KEY]
|
||||||
|
///
|
||||||
|
/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ExtXKey {
|
||||||
|
key: Option<DecryptionKey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtXKey {
|
||||||
|
pub(crate) const PREFIX: &'static str = "#EXT-X-KEY:";
|
||||||
|
|
||||||
|
/// Makes a new `ExtXKey` tag.
|
||||||
|
pub fn new(key: DecryptionKey) -> Self {
|
||||||
|
ExtXKey { key: Some(key) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes a new `ExtXKey` tag without a decryption key.
|
||||||
|
///
|
||||||
|
/// This tag has the `METHDO=NONE` attribute.
|
||||||
|
pub fn new_without_key() -> Self {
|
||||||
|
ExtXKey { key: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the decryption key for the following media segments and media initialization sections.
|
||||||
|
pub fn key(&self) -> Option<&DecryptionKey> {
|
||||||
|
self.key.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
|
pub fn requires_version(&self) -> ProtocolVersion {
|
||||||
|
self.key
|
||||||
|
.as_ref()
|
||||||
|
.map_or(ProtocolVersion::V1, |k| k.requires_version())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ExtXKey {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
|
if let Some(ref key) = self.key {
|
||||||
|
write!(f, "{}", key)?;
|
||||||
|
} else {
|
||||||
|
write!(f, "METHOD=NONE")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ExtXKey {
|
||||||
|
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;
|
||||||
|
|
||||||
|
if AttributePairs::parse(suffix).any(|a| a.as_ref().ok() == Some(&("METHOD", "NONE"))) {
|
||||||
|
for attr in AttributePairs::parse(suffix) {
|
||||||
|
let (key, _) = track!(attr)?;
|
||||||
|
track_assert_ne!(key, "URI", ErrorKind::InvalidInput);
|
||||||
|
track_assert_ne!(key, "IV", ErrorKind::InvalidInput);
|
||||||
|
track_assert_ne!(key, "KEYFORMAT", ErrorKind::InvalidInput);
|
||||||
|
track_assert_ne!(key, "KEYFORMATVERSIONS", ErrorKind::InvalidInput);
|
||||||
|
}
|
||||||
|
Ok(ExtXKey { key: None })
|
||||||
|
} else {
|
||||||
|
let key = track!(suffix.parse())?;
|
||||||
|
Ok(ExtXKey { key: Some(key) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use crate::types::{EncryptionMethod, InitializationVector, QuotedString};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ext_x_key() {
|
||||||
|
let tag = ExtXKey::new_without_key();
|
||||||
|
let text = "#EXT-X-KEY:METHOD=NONE";
|
||||||
|
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||||
|
assert_eq!(tag.to_string(), text);
|
||||||
|
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||||
|
|
||||||
|
let tag = ExtXKey::new(DecryptionKey {
|
||||||
|
method: EncryptionMethod::Aes128,
|
||||||
|
uri: QuotedString::new("foo").unwrap(),
|
||||||
|
iv: None,
|
||||||
|
key_format: None,
|
||||||
|
key_format_versions: None,
|
||||||
|
});
|
||||||
|
let text = r#"#EXT-X-KEY:METHOD=AES-128,URI="foo""#;
|
||||||
|
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||||
|
assert_eq!(tag.to_string(), text);
|
||||||
|
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||||
|
|
||||||
|
let tag = ExtXKey::new(DecryptionKey {
|
||||||
|
method: EncryptionMethod::Aes128,
|
||||||
|
uri: QuotedString::new("foo").unwrap(),
|
||||||
|
iv: Some(InitializationVector([
|
||||||
|
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
|
||||||
|
])),
|
||||||
|
key_format: None,
|
||||||
|
key_format_versions: None,
|
||||||
|
});
|
||||||
|
let text = r#"#EXT-X-KEY:METHOD=AES-128,URI="foo",IV=0x000102030405060708090a0b0c0d0e0f"#;
|
||||||
|
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||||
|
assert_eq!(tag.to_string(), text);
|
||||||
|
assert_eq!(tag.requires_version(), ProtocolVersion::V2);
|
||||||
|
|
||||||
|
let tag = ExtXKey::new(DecryptionKey {
|
||||||
|
method: EncryptionMethod::Aes128,
|
||||||
|
uri: QuotedString::new("foo").unwrap(),
|
||||||
|
iv: Some(InitializationVector([
|
||||||
|
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
|
||||||
|
])),
|
||||||
|
key_format: Some(QuotedString::new("baz").unwrap()),
|
||||||
|
key_format_versions: None,
|
||||||
|
});
|
||||||
|
let text = r#"#EXT-X-KEY:METHOD=AES-128,URI="foo",IV=0x000102030405060708090a0b0c0d0e0f,KEYFORMAT="baz""#;
|
||||||
|
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||||
|
assert_eq!(tag.to_string(), text);
|
||||||
|
assert_eq!(tag.requires_version(), ProtocolVersion::V5);
|
||||||
|
}
|
||||||
|
}
|
112
src/tags/media_segment/map.rs
Normal file
112
src/tags/media_segment/map.rs
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
use crate::attribute::AttributePairs;
|
||||||
|
use crate::types::{ByteRange, ProtocolVersion, QuotedString};
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// [4.3.2.5. EXT-X-MAP]
|
||||||
|
///
|
||||||
|
/// [4.3.2.5. EXT-X-MAP]: https://tools.ietf.org/html/rfc8216#section-4.3.2.5
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ExtXMap {
|
||||||
|
uri: QuotedString,
|
||||||
|
range: Option<ByteRange>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtXMap {
|
||||||
|
pub(crate) const PREFIX: &'static str = "#EXT-X-MAP:";
|
||||||
|
|
||||||
|
/// Makes a new `ExtXMap` tag.
|
||||||
|
pub fn new(uri: QuotedString) -> Self {
|
||||||
|
ExtXMap { uri, range: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Makes a new `ExtXMap` tag with the given range.
|
||||||
|
pub fn with_range(uri: QuotedString, range: ByteRange) -> Self {
|
||||||
|
ExtXMap {
|
||||||
|
uri,
|
||||||
|
range: Some(range),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the URI that identifies a resource that contains the media initialization section.
|
||||||
|
pub fn uri(&self) -> &QuotedString {
|
||||||
|
&self.uri
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the range of the media initialization section.
|
||||||
|
pub fn range(&self) -> Option<ByteRange> {
|
||||||
|
self.range
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
|
pub fn requires_version(&self) -> ProtocolVersion {
|
||||||
|
ProtocolVersion::V6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ExtXMap {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
|
write!(f, "URI={}", self.uri)?;
|
||||||
|
if let Some(ref x) = self.range {
|
||||||
|
write!(f, ",BYTERANGE=\"{}\"", x)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ExtXMap {
|
||||||
|
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 range = 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())?),
|
||||||
|
"BYTERANGE" => {
|
||||||
|
let s: QuotedString = track!(value.parse())?;
|
||||||
|
range = Some(track!(s.parse())?);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// [6.3.1. General Client Responsibilities]
|
||||||
|
// > ignore any attribute/value pair with an unrecognized AttributeName.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let uri = track_assert_some!(uri, ErrorKind::InvalidInput);
|
||||||
|
Ok(ExtXMap { uri, range })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ext_x_map() {
|
||||||
|
let tag = ExtXMap::new(QuotedString::new("foo").unwrap());
|
||||||
|
let text = r#"#EXT-X-MAP:URI="foo""#;
|
||||||
|
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||||
|
assert_eq!(tag.to_string(), text);
|
||||||
|
assert_eq!(tag.requires_version(), ProtocolVersion::V6);
|
||||||
|
|
||||||
|
let tag = ExtXMap::with_range(
|
||||||
|
QuotedString::new("foo").unwrap(),
|
||||||
|
ByteRange {
|
||||||
|
length: 9,
|
||||||
|
start: Some(2),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
let text = r#"#EXT-X-MAP:URI="foo",BYTERANGE="9@2""#;
|
||||||
|
track_try_unwrap!(ExtXMap::from_str(text));
|
||||||
|
assert_eq!(text.parse().ok(), Some(tag.clone()));
|
||||||
|
assert_eq!(tag.to_string(), text);
|
||||||
|
assert_eq!(tag.requires_version(), ProtocolVersion::V6);
|
||||||
|
}
|
||||||
|
}
|
15
src/tags/media_segment/mod.rs
Normal file
15
src/tags/media_segment/mod.rs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
mod byte_range;
|
||||||
|
mod date_range;
|
||||||
|
mod discontinuity;
|
||||||
|
mod inf;
|
||||||
|
mod key;
|
||||||
|
mod map;
|
||||||
|
mod program_date_time;
|
||||||
|
|
||||||
|
pub use byte_range::*;
|
||||||
|
pub use date_range::*;
|
||||||
|
pub use discontinuity::*;
|
||||||
|
pub use inf::*;
|
||||||
|
pub use key::*;
|
||||||
|
pub use map::*;
|
||||||
|
pub use program_date_time::*;
|
63
src/tags/media_segment/program_date_time.rs
Normal file
63
src/tags/media_segment/program_date_time.rs
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
use crate::types::{ProtocolVersion, SingleLineString};
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]
|
||||||
|
///
|
||||||
|
/// [4.3.2.6. EXT-X-PROGRAM-DATE-TIME]: https://tools.ietf.org/html/rfc8216#section-4.3.2.6
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ExtXProgramDateTime {
|
||||||
|
date_time: SingleLineString,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExtXProgramDateTime {
|
||||||
|
pub(crate) const PREFIX: &'static str = "#EXT-X-PROGRAM-DATE-TIME:";
|
||||||
|
|
||||||
|
/// Makes a new `ExtXProgramDateTime` tag.
|
||||||
|
pub fn new(date_time: SingleLineString) -> Self {
|
||||||
|
ExtXProgramDateTime { date_time }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the date-time of the first sample of the associated media segment.
|
||||||
|
pub fn date_time(&self) -> &SingleLineString {
|
||||||
|
&self.date_time
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
|
pub fn requires_version(&self) -> ProtocolVersion {
|
||||||
|
ProtocolVersion::V1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ExtXProgramDateTime {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}{}", Self::PREFIX, self.date_time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ExtXProgramDateTime {
|
||||||
|
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;
|
||||||
|
Ok(ExtXProgramDateTime {
|
||||||
|
date_time: track!(SingleLineString::new(suffix))?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ext_x_program_date_time() {
|
||||||
|
let text = "#EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00";
|
||||||
|
assert!(text.parse::<ExtXProgramDateTime>().is_ok());
|
||||||
|
|
||||||
|
let tag = text.parse::<ExtXProgramDateTime>().unwrap();
|
||||||
|
assert_eq!(tag.to_string(), text);
|
||||||
|
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,6 @@
|
||||||
//! [4.3. Playlist Tags]
|
//! [4.3. Playlist Tags]
|
||||||
//!
|
//!
|
||||||
//! [4.3. Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3
|
//! [4.3. Playlist Tags]: https://tools.ietf.org/html/rfc8216#section-4.3
|
||||||
use crate::{ErrorKind, Result};
|
|
||||||
use trackable::error::ErrorKindExt;
|
|
||||||
|
|
||||||
macro_rules! may_invalid {
|
macro_rules! may_invalid {
|
||||||
($expr:expr) => {
|
($expr:expr) => {
|
||||||
|
@ -20,24 +18,17 @@ macro_rules! impl_from {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub use self::basic::{ExtM3u, ExtXVersion};
|
|
||||||
pub use self::master_playlist::{
|
|
||||||
ExtXIFrameStreamInf, ExtXMedia, ExtXSessionData, ExtXSessionKey, ExtXStreamInf,
|
|
||||||
};
|
|
||||||
pub use self::media_or_master_playlist::{ExtXIndependentSegments, ExtXStart};
|
|
||||||
pub use self::media_playlist::{
|
|
||||||
ExtXDiscontinuitySequence, ExtXEndList, ExtXIFramesOnly, ExtXMediaSequence, ExtXPlaylistType,
|
|
||||||
ExtXTargetDuration,
|
|
||||||
};
|
|
||||||
pub use self::media_segment::{
|
|
||||||
ExtInf, ExtXByteRange, ExtXDateRange, ExtXDiscontinuity, ExtXKey, ExtXMap, ExtXProgramDateTime,
|
|
||||||
};
|
|
||||||
|
|
||||||
mod basic;
|
mod basic;
|
||||||
mod master_playlist;
|
mod master_playlist;
|
||||||
mod media_or_master_playlist;
|
|
||||||
mod media_playlist;
|
mod media_playlist;
|
||||||
mod media_segment;
|
mod media_segment;
|
||||||
|
mod shared;
|
||||||
|
|
||||||
|
pub use basic::*;
|
||||||
|
pub use master_playlist::*;
|
||||||
|
pub use media_playlist::*;
|
||||||
|
pub use media_segment::*;
|
||||||
|
pub use shared::*;
|
||||||
|
|
||||||
/// [4.3.4. Master Playlist Tags]
|
/// [4.3.4. Master Playlist Tags]
|
||||||
///
|
///
|
||||||
|
@ -114,16 +105,3 @@ impl_from!(MediaSegmentTag, ExtXDiscontinuity);
|
||||||
impl_from!(MediaSegmentTag, ExtXKey);
|
impl_from!(MediaSegmentTag, ExtXKey);
|
||||||
impl_from!(MediaSegmentTag, ExtXMap);
|
impl_from!(MediaSegmentTag, ExtXMap);
|
||||||
impl_from!(MediaSegmentTag, ExtXProgramDateTime);
|
impl_from!(MediaSegmentTag, ExtXProgramDateTime);
|
||||||
|
|
||||||
fn parse_yes_or_no(s: &str) -> Result<bool> {
|
|
||||||
match s {
|
|
||||||
"YES" => Ok(true),
|
|
||||||
"NO" => Ok(false),
|
|
||||||
_ => track_panic!(ErrorKind::InvalidInput, "Unexpected value: {:?}", s),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_u64(s: &str) -> Result<u64> {
|
|
||||||
let n = track!(s.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?;
|
|
||||||
Ok(n)
|
|
||||||
}
|
|
||||||
|
|
46
src/tags/shared/independent_segments.rs
Normal file
46
src/tags/shared/independent_segments.rs
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
use crate::types::ProtocolVersion;
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS]
|
||||||
|
///
|
||||||
|
/// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS]: https://tools.ietf.org/html/rfc8216#section-4.3.5.1
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ExtXIndependentSegments;
|
||||||
|
impl ExtXIndependentSegments {
|
||||||
|
pub(crate) const PREFIX: &'static str = "#EXT-X-INDEPENDENT-SEGMENTS";
|
||||||
|
|
||||||
|
/// Returns the protocol compatibility version that this tag requires.
|
||||||
|
pub fn requires_version(self) -> ProtocolVersion {
|
||||||
|
ProtocolVersion::V1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ExtXIndependentSegments {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
Self::PREFIX.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ExtXIndependentSegments {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput);
|
||||||
|
Ok(ExtXIndependentSegments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ext_x_independent_segments() {
|
||||||
|
let tag = ExtXIndependentSegments;
|
||||||
|
let text = "#EXT-X-INDEPENDENT-SEGMENTS";
|
||||||
|
assert_eq!(text.parse().ok(), Some(tag));
|
||||||
|
assert_eq!(tag.to_string(), text);
|
||||||
|
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
||||||
|
}
|
||||||
|
}
|
5
src/tags/shared/mod.rs
Normal file
5
src/tags/shared/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
mod independent_segments;
|
||||||
|
mod start;
|
||||||
|
|
||||||
|
pub use independent_segments::*;
|
||||||
|
pub use start::*;
|
|
@ -1,36 +1,10 @@
|
||||||
use super::parse_yes_or_no;
|
|
||||||
use crate::attribute::AttributePairs;
|
use crate::attribute::AttributePairs;
|
||||||
use crate::types::{ProtocolVersion, SignedDecimalFloatingPoint};
|
use crate::types::{ProtocolVersion, SignedDecimalFloatingPoint};
|
||||||
|
use crate::utils::parse_yes_or_no;
|
||||||
use crate::{Error, ErrorKind, Result};
|
use crate::{Error, ErrorKind, Result};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
/// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS]
|
|
||||||
///
|
|
||||||
/// [4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS]: https://tools.ietf.org/html/rfc8216#section-4.3.5.1
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub struct ExtXIndependentSegments;
|
|
||||||
impl ExtXIndependentSegments {
|
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-INDEPENDENT-SEGMENTS";
|
|
||||||
|
|
||||||
/// Returns the protocol compatibility version that this tag requires.
|
|
||||||
pub fn requires_version(self) -> ProtocolVersion {
|
|
||||||
ProtocolVersion::V1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl fmt::Display for ExtXIndependentSegments {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
Self::PREFIX.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for ExtXIndependentSegments {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert_eq!(s, Self::PREFIX, ErrorKind::InvalidInput);
|
|
||||||
Ok(ExtXIndependentSegments)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [4.3.5.2. EXT-X-START]
|
/// [4.3.5.2. EXT-X-START]
|
||||||
///
|
///
|
||||||
/// [4.3.5.2. EXT-X-START]: https://tools.ietf.org/html/rfc8216#section-4.3.5.2
|
/// [4.3.5.2. EXT-X-START]: https://tools.ietf.org/html/rfc8216#section-4.3.5.2
|
||||||
|
@ -39,6 +13,7 @@ pub struct ExtXStart {
|
||||||
time_offset: SignedDecimalFloatingPoint,
|
time_offset: SignedDecimalFloatingPoint,
|
||||||
precise: bool,
|
precise: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExtXStart {
|
impl ExtXStart {
|
||||||
pub(crate) const PREFIX: &'static str = "#EXT-X-START:";
|
pub(crate) const PREFIX: &'static str = "#EXT-X-START:";
|
||||||
|
|
||||||
|
@ -74,6 +49,7 @@ impl ExtXStart {
|
||||||
ProtocolVersion::V1
|
ProtocolVersion::V1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ExtXStart {
|
impl fmt::Display for ExtXStart {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
write!(f, "{}", Self::PREFIX)?;
|
write!(f, "{}", Self::PREFIX)?;
|
||||||
|
@ -84,6 +60,7 @@ impl fmt::Display for ExtXStart {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ExtXStart {
|
impl FromStr for ExtXStart {
|
||||||
type Err = Error;
|
type Err = Error;
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
@ -116,15 +93,6 @@ impl FromStr for ExtXStart {
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn ext_x_independent_segments() {
|
|
||||||
let tag = ExtXIndependentSegments;
|
|
||||||
let text = "#EXT-X-INDEPENDENT-SEGMENTS";
|
|
||||||
assert_eq!(text.parse().ok(), Some(tag));
|
|
||||||
assert_eq!(tag.to_string(), text);
|
|
||||||
assert_eq!(tag.requires_version(), ProtocolVersion::V1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ext_x_start() {
|
fn ext_x_start() {
|
||||||
let tag = ExtXStart::new(SignedDecimalFloatingPoint::new(-1.23).unwrap());
|
let tag = ExtXStart::new(SignedDecimalFloatingPoint::new(-1.23).unwrap());
|
840
src/types.rs
840
src/types.rs
|
@ -1,840 +0,0 @@
|
||||||
//! Miscellaneous types.
|
|
||||||
use crate::attribute::AttributePairs;
|
|
||||||
use crate::{Error, ErrorKind, Result};
|
|
||||||
use std::fmt;
|
|
||||||
use std::ops::Deref;
|
|
||||||
use std::str::{self, FromStr};
|
|
||||||
use std::time::Duration;
|
|
||||||
use trackable::error::ErrorKindExt;
|
|
||||||
|
|
||||||
/// String that represents a single line in a playlist file.
|
|
||||||
///
|
|
||||||
/// See: [4.1. Definition of a Playlist]
|
|
||||||
///
|
|
||||||
/// [4.1. Definition of a Playlist]: https://tools.ietf.org/html/rfc8216#section-4.1
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
pub struct SingleLineString(String);
|
|
||||||
impl SingleLineString {
|
|
||||||
/// Makes a new `SingleLineString` instance.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// If the given string contains any control characters,
|
|
||||||
/// this function will return an error which has the kind `ErrorKind::InvalidInput`.
|
|
||||||
pub fn new<T: Into<String>>(s: T) -> Result<Self> {
|
|
||||||
let s = s.into();
|
|
||||||
track_assert!(!s.chars().any(|c| c.is_control()), ErrorKind::InvalidInput);
|
|
||||||
Ok(SingleLineString(s))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Deref for SingleLineString {
|
|
||||||
type Target = str;
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl AsRef<str> for SingleLineString {
|
|
||||||
fn as_ref(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl fmt::Display for SingleLineString {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
self.0.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Quoted string.
|
|
||||||
///
|
|
||||||
/// See: [4.2. Attribute Lists]
|
|
||||||
///
|
|
||||||
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
pub struct QuotedString(String);
|
|
||||||
impl QuotedString {
|
|
||||||
/// Makes a new `QuotedString` instance.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// If the given string contains any control characters or double-quote character,
|
|
||||||
/// this function will return an error which has the kind `ErrorKind::InvalidInput`.
|
|
||||||
pub fn new<T: Into<String>>(s: T) -> Result<Self> {
|
|
||||||
let s = s.into();
|
|
||||||
track_assert!(
|
|
||||||
!s.chars().any(|c| c.is_control() || c == '"'),
|
|
||||||
ErrorKind::InvalidInput
|
|
||||||
);
|
|
||||||
Ok(QuotedString(s))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Deref for QuotedString {
|
|
||||||
type Target = str;
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl AsRef<str> for QuotedString {
|
|
||||||
fn as_ref(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl fmt::Display for QuotedString {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{:?}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for QuotedString {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
let len = s.len();
|
|
||||||
let bytes = s.as_bytes();
|
|
||||||
track_assert!(len >= 2, ErrorKind::InvalidInput);
|
|
||||||
track_assert_eq!(bytes[0], b'"', ErrorKind::InvalidInput);
|
|
||||||
track_assert_eq!(bytes[len - 1], b'"', ErrorKind::InvalidInput);
|
|
||||||
|
|
||||||
let s = unsafe { str::from_utf8_unchecked(&bytes[1..len - 1]) };
|
|
||||||
track!(QuotedString::new(s))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decimal resolution.
|
|
||||||
///
|
|
||||||
/// See: [4.2. Attribute Lists]
|
|
||||||
///
|
|
||||||
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub struct DecimalResolution {
|
|
||||||
/// Horizontal pixel dimension.
|
|
||||||
pub width: usize,
|
|
||||||
|
|
||||||
/// Vertical pixel dimension.
|
|
||||||
pub height: usize,
|
|
||||||
}
|
|
||||||
impl fmt::Display for DecimalResolution {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}x{}", self.width, self.height)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for DecimalResolution {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
let mut tokens = s.splitn(2, 'x');
|
|
||||||
let width = tokens.next().expect("Never fails");
|
|
||||||
let height = track_assert_some!(tokens.next(), ErrorKind::InvalidInput);
|
|
||||||
Ok(DecimalResolution {
|
|
||||||
width: track!(width.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?,
|
|
||||||
height: track!(height.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Non-negative decimal floating-point number.
|
|
||||||
///
|
|
||||||
/// See: [4.2. Attribute Lists]
|
|
||||||
///
|
|
||||||
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
|
|
||||||
pub struct DecimalFloatingPoint(f64);
|
|
||||||
impl DecimalFloatingPoint {
|
|
||||||
/// Makes a new `DecimalFloatingPoint` instance.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// The given value must have a positive sign and be finite,
|
|
||||||
/// otherwise this function will return an error that has the kind `ErrorKind::InvalidInput`.
|
|
||||||
pub fn new(n: f64) -> Result<Self> {
|
|
||||||
track_assert!(n.is_sign_positive(), ErrorKind::InvalidInput);
|
|
||||||
track_assert!(n.is_finite(), ErrorKind::InvalidInput);
|
|
||||||
Ok(DecimalFloatingPoint(n))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts `DecimalFloatingPoint` to `f64`.
|
|
||||||
pub fn as_f64(self) -> f64 {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn to_duration(self) -> Duration {
|
|
||||||
let secs = self.0 as u64;
|
|
||||||
let nanos = (self.0.fract() * 1_000_000_000.0) as u32;
|
|
||||||
Duration::new(secs, nanos)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn from_duration(duration: Duration) -> Self {
|
|
||||||
let n =
|
|
||||||
(duration.as_secs() as f64) + (f64::from(duration.subsec_nanos()) / 1_000_000_000.0);
|
|
||||||
DecimalFloatingPoint(n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<u32> for DecimalFloatingPoint {
|
|
||||||
fn from(f: u32) -> Self {
|
|
||||||
DecimalFloatingPoint(f64::from(f))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Eq for DecimalFloatingPoint {}
|
|
||||||
impl fmt::Display for DecimalFloatingPoint {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
self.0.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for DecimalFloatingPoint {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(
|
|
||||||
s.chars().all(|c| c.is_digit(10) || c == '.'),
|
|
||||||
ErrorKind::InvalidInput
|
|
||||||
);
|
|
||||||
let n = track!(s.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?;
|
|
||||||
Ok(DecimalFloatingPoint(n))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Signed decimal floating-point number.
|
|
||||||
///
|
|
||||||
/// See: [4.2. Attribute Lists]
|
|
||||||
///
|
|
||||||
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
|
|
||||||
pub struct SignedDecimalFloatingPoint(f64);
|
|
||||||
impl SignedDecimalFloatingPoint {
|
|
||||||
/// Makes a new `SignedDecimalFloatingPoint` instance.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// The given value must be finite,
|
|
||||||
/// otherwise this function will return an error that has the kind `ErrorKind::InvalidInput`.
|
|
||||||
pub fn new(n: f64) -> Result<Self> {
|
|
||||||
track_assert!(n.is_finite(), ErrorKind::InvalidInput);
|
|
||||||
Ok(SignedDecimalFloatingPoint(n))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts `DecimalFloatingPoint` to `f64`.
|
|
||||||
pub fn as_f64(self) -> f64 {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl From<i32> for SignedDecimalFloatingPoint {
|
|
||||||
fn from(f: i32) -> Self {
|
|
||||||
SignedDecimalFloatingPoint(f64::from(f))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Eq for SignedDecimalFloatingPoint {}
|
|
||||||
impl fmt::Display for SignedDecimalFloatingPoint {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
self.0.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for SignedDecimalFloatingPoint {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(
|
|
||||||
s.chars().all(|c| c.is_digit(10) || c == '.' || c == '-'),
|
|
||||||
ErrorKind::InvalidInput
|
|
||||||
);
|
|
||||||
let n = track!(s.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?;
|
|
||||||
Ok(SignedDecimalFloatingPoint(n))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Hexadecimal sequence.
|
|
||||||
///
|
|
||||||
/// See: [4.2. Attribute Lists]
|
|
||||||
///
|
|
||||||
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
pub struct HexadecimalSequence(Vec<u8>);
|
|
||||||
impl HexadecimalSequence {
|
|
||||||
/// Makes a new `HexadecimalSequence` instance.
|
|
||||||
pub fn new<T: Into<Vec<u8>>>(v: T) -> Self {
|
|
||||||
HexadecimalSequence(v.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts into the underlying byte sequence.
|
|
||||||
pub fn into_bytes(self) -> Vec<u8> {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Deref for HexadecimalSequence {
|
|
||||||
type Target = [u8];
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl AsRef<[u8]> for HexadecimalSequence {
|
|
||||||
fn as_ref(&self) -> &[u8] {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl fmt::Display for HexadecimalSequence {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "0x")?;
|
|
||||||
for b in &self.0 {
|
|
||||||
write!(f, "{:02x}", b)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for HexadecimalSequence {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(
|
|
||||||
s.starts_with("0x") || s.starts_with("0X"),
|
|
||||||
ErrorKind::InvalidInput
|
|
||||||
);
|
|
||||||
track_assert!(s.len() % 2 == 0, ErrorKind::InvalidInput);
|
|
||||||
|
|
||||||
let mut v = Vec::with_capacity(s.len() / 2 - 1);
|
|
||||||
for c in s.as_bytes().chunks(2).skip(1) {
|
|
||||||
let d = track!(str::from_utf8(c).map_err(|e| ErrorKind::InvalidInput.cause(e)))?;
|
|
||||||
let b =
|
|
||||||
track!(u8::from_str_radix(d, 16).map_err(|e| ErrorKind::InvalidInput.cause(e)))?;
|
|
||||||
v.push(b);
|
|
||||||
}
|
|
||||||
Ok(HexadecimalSequence(v))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initialization vector.
|
|
||||||
///
|
|
||||||
/// See: [4.3.2.4. EXT-X-KEY]
|
|
||||||
///
|
|
||||||
/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
pub struct InitializationVector(pub [u8; 16]);
|
|
||||||
impl Deref for InitializationVector {
|
|
||||||
type Target = [u8];
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl AsRef<[u8]> for InitializationVector {
|
|
||||||
fn as_ref(&self) -> &[u8] {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl fmt::Display for InitializationVector {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "0x")?;
|
|
||||||
for b in &self.0 {
|
|
||||||
write!(f, "{:02x}", b)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for InitializationVector {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
track_assert!(
|
|
||||||
s.starts_with("0x") || s.starts_with("0X"),
|
|
||||||
ErrorKind::InvalidInput
|
|
||||||
);
|
|
||||||
track_assert_eq!(s.len() - 2, 32, ErrorKind::InvalidInput);
|
|
||||||
|
|
||||||
let mut v = [0; 16];
|
|
||||||
for (i, c) in s.as_bytes().chunks(2).skip(1).enumerate() {
|
|
||||||
let d = track!(str::from_utf8(c).map_err(|e| ErrorKind::InvalidInput.cause(e)))?;
|
|
||||||
let b =
|
|
||||||
track!(u8::from_str_radix(d, 16).map_err(|e| ErrorKind::InvalidInput.cause(e)))?;
|
|
||||||
v[i] = b;
|
|
||||||
}
|
|
||||||
Ok(InitializationVector(v))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// [7. Protocol Version Compatibility]
|
|
||||||
///
|
|
||||||
/// [7. Protocol Version Compatibility]: https://tools.ietf.org/html/rfc8216#section-7
|
|
||||||
#[allow(missing_docs)]
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
|
||||||
pub enum ProtocolVersion {
|
|
||||||
V1,
|
|
||||||
V2,
|
|
||||||
V3,
|
|
||||||
V4,
|
|
||||||
V5,
|
|
||||||
V6,
|
|
||||||
V7,
|
|
||||||
}
|
|
||||||
impl fmt::Display for ProtocolVersion {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
let n = match *self {
|
|
||||||
ProtocolVersion::V1 => 1,
|
|
||||||
ProtocolVersion::V2 => 2,
|
|
||||||
ProtocolVersion::V3 => 3,
|
|
||||||
ProtocolVersion::V4 => 4,
|
|
||||||
ProtocolVersion::V5 => 5,
|
|
||||||
ProtocolVersion::V6 => 6,
|
|
||||||
ProtocolVersion::V7 => 7,
|
|
||||||
};
|
|
||||||
write!(f, "{}", n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for ProtocolVersion {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
Ok(match s {
|
|
||||||
"1" => ProtocolVersion::V1,
|
|
||||||
"2" => ProtocolVersion::V2,
|
|
||||||
"3" => ProtocolVersion::V3,
|
|
||||||
"4" => ProtocolVersion::V4,
|
|
||||||
"5" => ProtocolVersion::V5,
|
|
||||||
"6" => ProtocolVersion::V6,
|
|
||||||
"7" => ProtocolVersion::V7,
|
|
||||||
_ => track_panic!(ErrorKind::InvalidInput, "Unknown protocol version: {:?}", s),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Byte range.
|
|
||||||
///
|
|
||||||
/// See: [4.3.2.2. EXT-X-BYTERANGE]
|
|
||||||
///
|
|
||||||
/// [4.3.2.2. EXT-X-BYTERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.2
|
|
||||||
#[allow(missing_docs)]
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub struct ByteRange {
|
|
||||||
pub length: usize,
|
|
||||||
pub start: Option<usize>,
|
|
||||||
}
|
|
||||||
impl fmt::Display for ByteRange {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}", self.length)?;
|
|
||||||
if let Some(x) = self.start {
|
|
||||||
write!(f, "@{}", x)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for ByteRange {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
let mut tokens = s.splitn(2, '@');
|
|
||||||
let length = tokens.next().expect("Never fails");
|
|
||||||
let start = if let Some(start) = tokens.next() {
|
|
||||||
Some(track!(start
|
|
||||||
.parse()
|
|
||||||
.map_err(|e| ErrorKind::InvalidInput.cause(e)))?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
Ok(ByteRange {
|
|
||||||
length: track!(length.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?,
|
|
||||||
start,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decryption key.
|
|
||||||
///
|
|
||||||
/// See: [4.3.2.4. EXT-X-KEY]
|
|
||||||
///
|
|
||||||
/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4
|
|
||||||
#[allow(missing_docs)]
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
||||||
pub struct DecryptionKey {
|
|
||||||
pub method: EncryptionMethod,
|
|
||||||
pub uri: QuotedString,
|
|
||||||
pub iv: Option<InitializationVector>,
|
|
||||||
pub key_format: Option<QuotedString>,
|
|
||||||
pub key_format_versions: Option<QuotedString>,
|
|
||||||
}
|
|
||||||
impl DecryptionKey {
|
|
||||||
pub(crate) fn requires_version(&self) -> ProtocolVersion {
|
|
||||||
if self.key_format.is_some() | self.key_format_versions.is_some() {
|
|
||||||
ProtocolVersion::V5
|
|
||||||
} else if self.iv.is_some() {
|
|
||||||
ProtocolVersion::V2
|
|
||||||
} else {
|
|
||||||
ProtocolVersion::V1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl fmt::Display for DecryptionKey {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
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 DecryptionKey {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
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);
|
|
||||||
for attr in attrs {
|
|
||||||
let (key, value) = track!(attr)?;
|
|
||||||
match key {
|
|
||||||
"METHOD" => method = Some(track!(value.parse())?),
|
|
||||||
"URI" => uri = Some(track!(value.parse())?),
|
|
||||||
"IV" => iv = Some(track!(value.parse())?),
|
|
||||||
"KEYFORMAT" => key_format = Some(track!(value.parse())?),
|
|
||||||
"KEYFORMATVERSIONS" => key_format_versions = Some(track!(value.parse())?),
|
|
||||||
_ => {
|
|
||||||
// [6.3.1. General Client Responsibilities]
|
|
||||||
// > 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(DecryptionKey {
|
|
||||||
method,
|
|
||||||
uri,
|
|
||||||
iv,
|
|
||||||
key_format,
|
|
||||||
key_format_versions,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Encryption method.
|
|
||||||
///
|
|
||||||
/// See: [4.3.2.4. EXT-X-KEY]
|
|
||||||
///
|
|
||||||
/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4
|
|
||||||
#[allow(missing_docs)]
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub enum EncryptionMethod {
|
|
||||||
Aes128,
|
|
||||||
SampleAes,
|
|
||||||
}
|
|
||||||
impl fmt::Display for EncryptionMethod {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match *self {
|
|
||||||
EncryptionMethod::Aes128 => "AES-128".fmt(f),
|
|
||||||
EncryptionMethod::SampleAes => "SAMPLE-AES".fmt(f),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for EncryptionMethod {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
match s {
|
|
||||||
"AES-128" => Ok(EncryptionMethod::Aes128),
|
|
||||||
"SAMPLE-AES" => Ok(EncryptionMethod::SampleAes),
|
|
||||||
_ => track_panic!(
|
|
||||||
ErrorKind::InvalidInput,
|
|
||||||
"Unknown encryption method: {:?}",
|
|
||||||
s
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Playlist type.
|
|
||||||
///
|
|
||||||
/// See: [4.3.3.5. EXT-X-PLAYLIST-TYPE]
|
|
||||||
///
|
|
||||||
/// [4.3.3.5. EXT-X-PLAYLIST-TYPE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.5
|
|
||||||
#[allow(missing_docs)]
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
||||||
pub enum PlaylistType {
|
|
||||||
Event,
|
|
||||||
Vod,
|
|
||||||
}
|
|
||||||
impl fmt::Display for PlaylistType {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
match *self {
|
|
||||||
PlaylistType::Event => write!(f, "EVENT"),
|
|
||||||
PlaylistType::Vod => write!(f, "VOD"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl FromStr for PlaylistType {
|
|
||||||
type Err = Error;
|
|
||||||
fn from_str(s: &str) -> Result<Self> {
|
|
||||||
match s {
|
|
||||||
"EVENT" => Ok(PlaylistType::Event),
|
|
||||||
"VOD" => Ok(PlaylistType::Vod),
|
|
||||||
_ => track_panic!(ErrorKind::InvalidInput, "Unknown playlist type: {:?}", s),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Media type.
|
|
||||||
///
|
|
||||||
/// See: [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
|
|
||||||
#[allow(missing_docs)]
|
|
||||||
#[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),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Identifier of a rendition within the segments in a media playlist.
|
|
||||||
///
|
|
||||||
/// See: [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
|
|
||||||
#[allow(missing_docs)]
|
|
||||||
#[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),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// HDCP level.
|
|
||||||
///
|
|
||||||
/// See: [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
|
|
||||||
#[allow(missing_docs)]
|
|
||||||
#[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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The identifier of a closed captions group or its absence.
|
|
||||||
///
|
|
||||||
/// See: [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
|
|
||||||
#[allow(missing_docs)]
|
|
||||||
#[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())?))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Session data.
|
|
||||||
///
|
|
||||||
/// See: [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
|
|
||||||
#[allow(missing_docs)]
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
||||||
pub enum SessionData {
|
|
||||||
Value(QuotedString),
|
|
||||||
Uri(QuotedString),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn single_line_string() {
|
|
||||||
assert!(SingleLineString::new("foo").is_ok());
|
|
||||||
assert!(SingleLineString::new("b\rar").is_err());
|
|
||||||
}
|
|
||||||
}
|
|
92
src/types/byte_range.rs
Normal file
92
src/types/byte_range.rs
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::{self, FromStr};
|
||||||
|
use trackable::error::ErrorKindExt;
|
||||||
|
|
||||||
|
/// Byte range.
|
||||||
|
///
|
||||||
|
/// See: [4.3.2.2. EXT-X-BYTERANGE]
|
||||||
|
///
|
||||||
|
/// [4.3.2.2. EXT-X-BYTERANGE]: https://tools.ietf.org/html/rfc8216#section-4.3.2.2
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ByteRange {
|
||||||
|
pub length: usize,
|
||||||
|
pub start: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ByteRange {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}", self.length)?;
|
||||||
|
if let Some(x) = self.start {
|
||||||
|
write!(f, "@{}", x)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for ByteRange {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
let mut tokens = s.splitn(2, '@');
|
||||||
|
let length = tokens.next().expect("Never fails");
|
||||||
|
let start = if let Some(start) = tokens.next() {
|
||||||
|
Some(track!(start
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| ErrorKind::InvalidInput.cause(e)))?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
Ok(ByteRange {
|
||||||
|
length: track!(length.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?,
|
||||||
|
start,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_display() {
|
||||||
|
let byte_range = ByteRange {
|
||||||
|
length: 0,
|
||||||
|
start: Some(5),
|
||||||
|
};
|
||||||
|
assert_eq!(byte_range.to_string(), "0@5".to_string());
|
||||||
|
|
||||||
|
let byte_range = ByteRange {
|
||||||
|
length: 99999,
|
||||||
|
start: Some(2),
|
||||||
|
};
|
||||||
|
assert_eq!(byte_range.to_string(), "99999@2".to_string());
|
||||||
|
|
||||||
|
let byte_range = ByteRange {
|
||||||
|
length: 99999,
|
||||||
|
start: None,
|
||||||
|
};
|
||||||
|
assert_eq!(byte_range.to_string(), "99999".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse() {
|
||||||
|
let byte_range = ByteRange {
|
||||||
|
length: 99999,
|
||||||
|
start: Some(2),
|
||||||
|
};
|
||||||
|
assert_eq!(byte_range, "99999@2".parse::<ByteRange>().unwrap());
|
||||||
|
|
||||||
|
let byte_range = ByteRange {
|
||||||
|
length: 99999,
|
||||||
|
start: Some(2),
|
||||||
|
};
|
||||||
|
assert_eq!(byte_range, "99999@2".parse::<ByteRange>().unwrap());
|
||||||
|
|
||||||
|
let byte_range = ByteRange {
|
||||||
|
length: 99999,
|
||||||
|
start: None,
|
||||||
|
};
|
||||||
|
assert_eq!(byte_range, "99999".parse::<ByteRange>().unwrap());
|
||||||
|
}
|
||||||
|
}
|
62
src/types/closed_captions.rs
Normal file
62
src/types/closed_captions.rs
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
use crate::types::QuotedString;
|
||||||
|
use crate::{Error, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::{self, FromStr};
|
||||||
|
|
||||||
|
/// The identifier of a closed captions group or its absence.
|
||||||
|
///
|
||||||
|
/// See: [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
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[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())?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_display() {
|
||||||
|
let closed_captions = ClosedCaptions::None;
|
||||||
|
assert_eq!(closed_captions.to_string(), "NONE".to_string());
|
||||||
|
|
||||||
|
let closed_captions = ClosedCaptions::GroupId(QuotedString::new("value").unwrap());
|
||||||
|
assert_eq!(closed_captions.to_string(), "\"value\"".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse() {
|
||||||
|
let closed_captions = ClosedCaptions::None;
|
||||||
|
assert_eq!(closed_captions, "NONE".parse::<ClosedCaptions>().unwrap());
|
||||||
|
|
||||||
|
let closed_captions = ClosedCaptions::GroupId(QuotedString::new("value").unwrap());
|
||||||
|
assert_eq!(
|
||||||
|
closed_captions,
|
||||||
|
"\"value\"".parse::<ClosedCaptions>().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
99
src/types/decimal_floating_point.rs
Normal file
99
src/types/decimal_floating_point.rs
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::{self, FromStr};
|
||||||
|
use std::time::Duration;
|
||||||
|
use trackable::error::ErrorKindExt;
|
||||||
|
|
||||||
|
/// Non-negative decimal floating-point number.
|
||||||
|
///
|
||||||
|
/// See: [4.2. Attribute Lists]
|
||||||
|
///
|
||||||
|
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
|
||||||
|
pub struct DecimalFloatingPoint(f64);
|
||||||
|
|
||||||
|
impl DecimalFloatingPoint {
|
||||||
|
/// Makes a new `DecimalFloatingPoint` instance.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// The given value must have a positive sign and be finite,
|
||||||
|
/// otherwise this function will return an error that has the kind `ErrorKind::InvalidInput`.
|
||||||
|
pub fn new(n: f64) -> Result<Self> {
|
||||||
|
track_assert!(n.is_sign_positive(), ErrorKind::InvalidInput);
|
||||||
|
track_assert!(n.is_finite(), ErrorKind::InvalidInput);
|
||||||
|
Ok(DecimalFloatingPoint(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts `DecimalFloatingPoint` to `f64`.
|
||||||
|
pub fn as_f64(self) -> f64 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn to_duration(self) -> Duration {
|
||||||
|
let secs = self.0 as u64;
|
||||||
|
let nanos = (self.0.fract() * 1_000_000_000.0) as u32;
|
||||||
|
Duration::new(secs, nanos)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn from_duration(duration: Duration) -> Self {
|
||||||
|
let n =
|
||||||
|
(duration.as_secs() as f64) + (f64::from(duration.subsec_nanos()) / 1_000_000_000.0);
|
||||||
|
DecimalFloatingPoint(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u32> for DecimalFloatingPoint {
|
||||||
|
fn from(f: u32) -> Self {
|
||||||
|
DecimalFloatingPoint(f64::from(f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for DecimalFloatingPoint {}
|
||||||
|
|
||||||
|
impl fmt::Display for DecimalFloatingPoint {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for DecimalFloatingPoint {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert!(
|
||||||
|
s.chars().all(|c| c.is_digit(10) || c == '.'),
|
||||||
|
ErrorKind::InvalidInput
|
||||||
|
);
|
||||||
|
let n = track!(s.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?;
|
||||||
|
Ok(DecimalFloatingPoint(n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_display() {
|
||||||
|
let decimal_floating_point = DecimalFloatingPoint::new(22.0).unwrap();
|
||||||
|
assert_eq!(decimal_floating_point.to_string(), "22".to_string());
|
||||||
|
|
||||||
|
let decimal_floating_point = DecimalFloatingPoint::new(4.1).unwrap();
|
||||||
|
assert_eq!(decimal_floating_point.to_string(), "4.1".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_parse() {
|
||||||
|
let decimal_floating_point = DecimalFloatingPoint::new(22.0).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
decimal_floating_point,
|
||||||
|
"22".parse::<DecimalFloatingPoint>().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
let decimal_floating_point = DecimalFloatingPoint::new(4.1).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
decimal_floating_point,
|
||||||
|
"4.1".parse::<DecimalFloatingPoint>().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
78
src/types/decimal_resolution.rs
Normal file
78
src/types/decimal_resolution.rs
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::{self, FromStr};
|
||||||
|
use trackable::error::ErrorKindExt;
|
||||||
|
|
||||||
|
/// Decimal resolution.
|
||||||
|
///
|
||||||
|
/// See: [4.2. Attribute Lists]
|
||||||
|
///
|
||||||
|
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct DecimalResolution {
|
||||||
|
/// Horizontal pixel dimension.
|
||||||
|
pub width: usize,
|
||||||
|
|
||||||
|
/// Vertical pixel dimension.
|
||||||
|
pub height: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for DecimalResolution {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{}x{}", self.width, self.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for DecimalResolution {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
let mut tokens = s.splitn(2, 'x');
|
||||||
|
let width = tokens.next().expect("Never fails");
|
||||||
|
let height = track_assert_some!(tokens.next(), ErrorKind::InvalidInput);
|
||||||
|
Ok(DecimalResolution {
|
||||||
|
width: track!(width.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?,
|
||||||
|
height: track!(height.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_display() {
|
||||||
|
let decimal_resolution = DecimalResolution {
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
};
|
||||||
|
assert_eq!(decimal_resolution.to_string(), "1920x1080".to_string());
|
||||||
|
|
||||||
|
let decimal_resolution = DecimalResolution {
|
||||||
|
width: 1280,
|
||||||
|
height: 720,
|
||||||
|
};
|
||||||
|
assert_eq!(decimal_resolution.to_string(), "1280x720".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse() {
|
||||||
|
let decimal_resolution = DecimalResolution {
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
decimal_resolution,
|
||||||
|
"1920x1080".parse::<DecimalResolution>().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
let decimal_resolution = DecimalResolution {
|
||||||
|
width: 1280,
|
||||||
|
height: 720,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
decimal_resolution,
|
||||||
|
"1280x720".parse::<DecimalResolution>().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
84
src/types/decryption_key.rs
Normal file
84
src/types/decryption_key.rs
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
use crate::attribute::AttributePairs;
|
||||||
|
use crate::types::{EncryptionMethod, InitializationVector, ProtocolVersion, QuotedString};
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::{self, FromStr};
|
||||||
|
|
||||||
|
/// Decryption key.
|
||||||
|
///
|
||||||
|
/// See: [4.3.2.4. EXT-X-KEY]
|
||||||
|
///
|
||||||
|
/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub struct DecryptionKey {
|
||||||
|
pub method: EncryptionMethod,
|
||||||
|
pub uri: QuotedString,
|
||||||
|
pub iv: Option<InitializationVector>,
|
||||||
|
pub key_format: Option<QuotedString>,
|
||||||
|
pub key_format_versions: Option<QuotedString>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DecryptionKey {
|
||||||
|
pub(crate) fn requires_version(&self) -> ProtocolVersion {
|
||||||
|
if self.key_format.is_some() | self.key_format_versions.is_some() {
|
||||||
|
ProtocolVersion::V5
|
||||||
|
} else if self.iv.is_some() {
|
||||||
|
ProtocolVersion::V2
|
||||||
|
} else {
|
||||||
|
ProtocolVersion::V1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for DecryptionKey {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
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 DecryptionKey {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
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);
|
||||||
|
for attr in attrs {
|
||||||
|
let (key, value) = track!(attr)?;
|
||||||
|
match key {
|
||||||
|
"METHOD" => method = Some(track!(value.parse())?),
|
||||||
|
"URI" => uri = Some(track!(value.parse())?),
|
||||||
|
"IV" => iv = Some(track!(value.parse())?),
|
||||||
|
"KEYFORMAT" => key_format = Some(track!(value.parse())?),
|
||||||
|
"KEYFORMATVERSIONS" => key_format_versions = Some(track!(value.parse())?),
|
||||||
|
_ => {
|
||||||
|
// [6.3.1. General Client Responsibilities]
|
||||||
|
// > 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(DecryptionKey {
|
||||||
|
method,
|
||||||
|
uri,
|
||||||
|
iv,
|
||||||
|
key_format,
|
||||||
|
key_format_versions,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
68
src/types/encryption_method.rs
Normal file
68
src/types/encryption_method.rs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::{self, FromStr};
|
||||||
|
|
||||||
|
/// Encryption method.
|
||||||
|
///
|
||||||
|
/// See: [4.3.2.4. EXT-X-KEY]
|
||||||
|
///
|
||||||
|
/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum EncryptionMethod {
|
||||||
|
Aes128,
|
||||||
|
SampleAes,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for EncryptionMethod {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
EncryptionMethod::Aes128 => "AES-128".fmt(f),
|
||||||
|
EncryptionMethod::SampleAes => "SAMPLE-AES".fmt(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for EncryptionMethod {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
match s {
|
||||||
|
"AES-128" => Ok(EncryptionMethod::Aes128),
|
||||||
|
"SAMPLE-AES" => Ok(EncryptionMethod::SampleAes),
|
||||||
|
_ => track_panic!(
|
||||||
|
ErrorKind::InvalidInput,
|
||||||
|
"Unknown encryption method: {:?}",
|
||||||
|
s
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_display() {
|
||||||
|
let encryption_method = EncryptionMethod::Aes128;
|
||||||
|
assert_eq!(encryption_method.to_string(), "AES-128".to_string());
|
||||||
|
|
||||||
|
let encryption_method = EncryptionMethod::SampleAes;
|
||||||
|
assert_eq!(encryption_method.to_string(), "SAMPLE-AES".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse() {
|
||||||
|
let encryption_method = EncryptionMethod::Aes128;
|
||||||
|
assert_eq!(
|
||||||
|
encryption_method,
|
||||||
|
"AES-128".parse::<EncryptionMethod>().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
let encryption_method = EncryptionMethod::SampleAes;
|
||||||
|
assert_eq!(
|
||||||
|
encryption_method,
|
||||||
|
"SAMPLE-AES".parse::<EncryptionMethod>().unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
58
src/types/hdcp_level.rs
Normal file
58
src/types/hdcp_level.rs
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::{self, FromStr};
|
||||||
|
|
||||||
|
/// HDCP level.
|
||||||
|
///
|
||||||
|
/// See: [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
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_display() {
|
||||||
|
let level = HdcpLevel::Type0;
|
||||||
|
assert_eq!(level.to_string(), "TYPE-0".to_string());
|
||||||
|
|
||||||
|
let level = HdcpLevel::None;
|
||||||
|
assert_eq!(level.to_string(), "NONE".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse() {
|
||||||
|
let level = HdcpLevel::Type0;
|
||||||
|
assert_eq!(level, "TYPE-0".parse::<HdcpLevel>().unwrap());
|
||||||
|
|
||||||
|
let level = HdcpLevel::None;
|
||||||
|
assert_eq!(level, "NONE".parse::<HdcpLevel>().unwrap());
|
||||||
|
}
|
||||||
|
}
|
68
src/types/hexadecimal_sequence.rs
Normal file
68
src/types/hexadecimal_sequence.rs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::str::{self, FromStr};
|
||||||
|
use trackable::error::ErrorKindExt;
|
||||||
|
|
||||||
|
/// Hexadecimal sequence.
|
||||||
|
///
|
||||||
|
/// See: [4.2. Attribute Lists]
|
||||||
|
///
|
||||||
|
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct HexadecimalSequence(Vec<u8>);
|
||||||
|
|
||||||
|
impl HexadecimalSequence {
|
||||||
|
/// Makes a new `HexadecimalSequence` instance.
|
||||||
|
pub fn new<T: Into<Vec<u8>>>(v: T) -> Self {
|
||||||
|
HexadecimalSequence(v.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts into the underlying byte sequence.
|
||||||
|
pub fn into_bytes(self) -> Vec<u8> {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for HexadecimalSequence {
|
||||||
|
type Target = [u8];
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<[u8]> for HexadecimalSequence {
|
||||||
|
fn as_ref(&self) -> &[u8] {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for HexadecimalSequence {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "0x")?;
|
||||||
|
for b in &self.0 {
|
||||||
|
write!(f, "{:02x}", b)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for HexadecimalSequence {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert!(
|
||||||
|
s.starts_with("0x") || s.starts_with("0X"),
|
||||||
|
ErrorKind::InvalidInput
|
||||||
|
);
|
||||||
|
track_assert!(s.len() % 2 == 0, ErrorKind::InvalidInput);
|
||||||
|
|
||||||
|
let mut v = Vec::with_capacity(s.len() / 2 - 1);
|
||||||
|
for c in s.as_bytes().chunks(2).skip(1) {
|
||||||
|
let d = track!(str::from_utf8(c).map_err(|e| ErrorKind::InvalidInput.cause(e)))?;
|
||||||
|
let b =
|
||||||
|
track!(u8::from_str_radix(d, 16).map_err(|e| ErrorKind::InvalidInput.cause(e)))?;
|
||||||
|
v.push(b);
|
||||||
|
}
|
||||||
|
Ok(HexadecimalSequence(v))
|
||||||
|
}
|
||||||
|
}
|
162
src/types/in_stream_id.rs
Normal file
162
src/types/in_stream_id.rs
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::{self, FromStr};
|
||||||
|
|
||||||
|
/// Identifier of a rendition within the segments in a media playlist.
|
||||||
|
///
|
||||||
|
/// See: [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
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[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),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
56
src/types/initialization_vector.rs
Normal file
56
src/types/initialization_vector.rs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::str::{self, FromStr};
|
||||||
|
use trackable::error::ErrorKindExt;
|
||||||
|
|
||||||
|
/// Initialization vector.
|
||||||
|
///
|
||||||
|
/// See: [4.3.2.4. EXT-X-KEY]
|
||||||
|
///
|
||||||
|
/// [4.3.2.4. EXT-X-KEY]: https://tools.ietf.org/html/rfc8216#section-4.3.2.4
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct InitializationVector(pub [u8; 16]);
|
||||||
|
|
||||||
|
impl Deref for InitializationVector {
|
||||||
|
type Target = [u8];
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<[u8]> for InitializationVector {
|
||||||
|
fn as_ref(&self) -> &[u8] {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for InitializationVector {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "0x")?;
|
||||||
|
for b in &self.0 {
|
||||||
|
write!(f, "{:02x}", b)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for InitializationVector {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert!(
|
||||||
|
s.starts_with("0x") || s.starts_with("0X"),
|
||||||
|
ErrorKind::InvalidInput
|
||||||
|
);
|
||||||
|
track_assert_eq!(s.len() - 2, 32, ErrorKind::InvalidInput);
|
||||||
|
|
||||||
|
let mut v = [0; 16];
|
||||||
|
for (i, c) in s.as_bytes().chunks(2).skip(1).enumerate() {
|
||||||
|
let d = track!(str::from_utf8(c).map_err(|e| ErrorKind::InvalidInput.cause(e)))?;
|
||||||
|
let b =
|
||||||
|
track!(u8::from_str_radix(d, 16).map_err(|e| ErrorKind::InvalidInput.cause(e)))?;
|
||||||
|
v[i] = b;
|
||||||
|
}
|
||||||
|
Ok(InitializationVector(v))
|
||||||
|
}
|
||||||
|
}
|
41
src/types/media_type.rs
Normal file
41
src/types/media_type.rs
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::{self, FromStr};
|
||||||
|
|
||||||
|
/// Media type.
|
||||||
|
///
|
||||||
|
/// See: [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
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[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),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
36
src/types/mod.rs
Normal file
36
src/types/mod.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
//! Miscellaneous types.
|
||||||
|
mod byte_range;
|
||||||
|
mod closed_captions;
|
||||||
|
mod decimal_floating_point;
|
||||||
|
mod decimal_resolution;
|
||||||
|
mod decryption_key;
|
||||||
|
mod encryption_method;
|
||||||
|
mod hdcp_level;
|
||||||
|
mod hexadecimal_sequence;
|
||||||
|
mod in_stream_id;
|
||||||
|
mod initialization_vector;
|
||||||
|
mod media_type;
|
||||||
|
mod playlist_type;
|
||||||
|
mod protocol_version;
|
||||||
|
mod quoted_string;
|
||||||
|
mod session_data;
|
||||||
|
mod signed_decimal_floating_point;
|
||||||
|
mod single_line_string;
|
||||||
|
|
||||||
|
pub use byte_range::*;
|
||||||
|
pub use closed_captions::*;
|
||||||
|
pub use decimal_floating_point::*;
|
||||||
|
pub use decimal_resolution::*;
|
||||||
|
pub use decryption_key::*;
|
||||||
|
pub use encryption_method::*;
|
||||||
|
pub use hdcp_level::*;
|
||||||
|
pub use hexadecimal_sequence::*;
|
||||||
|
pub use in_stream_id::*;
|
||||||
|
pub use initialization_vector::*;
|
||||||
|
pub use media_type::*;
|
||||||
|
pub use playlist_type::*;
|
||||||
|
pub use protocol_version::*;
|
||||||
|
pub use quoted_string::*;
|
||||||
|
pub use session_data::*;
|
||||||
|
pub use signed_decimal_floating_point::*;
|
||||||
|
pub use single_line_string::*;
|
35
src/types/playlist_type.rs
Normal file
35
src/types/playlist_type.rs
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::{self, FromStr};
|
||||||
|
|
||||||
|
/// Playlist type.
|
||||||
|
///
|
||||||
|
/// See: [4.3.3.5. EXT-X-PLAYLIST-TYPE]
|
||||||
|
///
|
||||||
|
/// [4.3.3.5. EXT-X-PLAYLIST-TYPE]: https://tools.ietf.org/html/rfc8216#section-4.3.3.5
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub enum PlaylistType {
|
||||||
|
Event,
|
||||||
|
Vod,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for PlaylistType {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
PlaylistType::Event => write!(f, "EVENT"),
|
||||||
|
PlaylistType::Vod => write!(f, "VOD"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for PlaylistType {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
match s {
|
||||||
|
"EVENT" => Ok(PlaylistType::Event),
|
||||||
|
"VOD" => Ok(PlaylistType::Vod),
|
||||||
|
_ => track_panic!(ErrorKind::InvalidInput, "Unknown playlist type: {:?}", s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
47
src/types/protocol_version.rs
Normal file
47
src/types/protocol_version.rs
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::{self, FromStr};
|
||||||
|
|
||||||
|
/// [7. Protocol Version Compatibility]
|
||||||
|
///
|
||||||
|
/// [7. Protocol Version Compatibility]: https://tools.ietf.org/html/rfc8216#section-7
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub enum ProtocolVersion {
|
||||||
|
V1,
|
||||||
|
V2,
|
||||||
|
V3,
|
||||||
|
V4,
|
||||||
|
V5,
|
||||||
|
V6,
|
||||||
|
V7,
|
||||||
|
}
|
||||||
|
impl fmt::Display for ProtocolVersion {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
let n = match *self {
|
||||||
|
ProtocolVersion::V1 => 1,
|
||||||
|
ProtocolVersion::V2 => 2,
|
||||||
|
ProtocolVersion::V3 => 3,
|
||||||
|
ProtocolVersion::V4 => 4,
|
||||||
|
ProtocolVersion::V5 => 5,
|
||||||
|
ProtocolVersion::V6 => 6,
|
||||||
|
ProtocolVersion::V7 => 7,
|
||||||
|
};
|
||||||
|
write!(f, "{}", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl FromStr for ProtocolVersion {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
Ok(match s {
|
||||||
|
"1" => ProtocolVersion::V1,
|
||||||
|
"2" => ProtocolVersion::V2,
|
||||||
|
"3" => ProtocolVersion::V3,
|
||||||
|
"4" => ProtocolVersion::V4,
|
||||||
|
"5" => ProtocolVersion::V5,
|
||||||
|
"6" => ProtocolVersion::V6,
|
||||||
|
"7" => ProtocolVersion::V7,
|
||||||
|
_ => track_panic!(ErrorKind::InvalidInput, "Unknown protocol version: {:?}", s),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
62
src/types/quoted_string.rs
Normal file
62
src/types/quoted_string.rs
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::str::{self, FromStr};
|
||||||
|
|
||||||
|
/// Quoted string.
|
||||||
|
///
|
||||||
|
/// See: [4.2. Attribute Lists]
|
||||||
|
///
|
||||||
|
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct QuotedString(String);
|
||||||
|
|
||||||
|
impl QuotedString {
|
||||||
|
/// Makes a new `QuotedString` instance.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// If the given string contains any control characters or double-quote character,
|
||||||
|
/// this function will return an error which has the kind `ErrorKind::InvalidInput`.
|
||||||
|
pub fn new<T: Into<String>>(s: T) -> Result<Self> {
|
||||||
|
let s = s.into();
|
||||||
|
track_assert!(
|
||||||
|
!s.chars().any(|c| c.is_control() || c == '"'),
|
||||||
|
ErrorKind::InvalidInput
|
||||||
|
);
|
||||||
|
Ok(QuotedString(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for QuotedString {
|
||||||
|
type Target = str;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for QuotedString {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for QuotedString {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
write!(f, "{:?}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for QuotedString {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
let len = s.len();
|
||||||
|
let bytes = s.as_bytes();
|
||||||
|
track_assert!(len >= 2, ErrorKind::InvalidInput);
|
||||||
|
track_assert_eq!(bytes[0], b'"', ErrorKind::InvalidInput);
|
||||||
|
track_assert_eq!(bytes[len - 1], b'"', ErrorKind::InvalidInput);
|
||||||
|
|
||||||
|
let s = unsafe { str::from_utf8_unchecked(&bytes[1..len - 1]) };
|
||||||
|
track!(QuotedString::new(s))
|
||||||
|
}
|
||||||
|
}
|
13
src/types/session_data.rs
Normal file
13
src/types/session_data.rs
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
use crate::types::QuotedString;
|
||||||
|
|
||||||
|
/// Session data.
|
||||||
|
///
|
||||||
|
/// See: [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
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
|
pub enum SessionData {
|
||||||
|
Value(QuotedString),
|
||||||
|
Uri(QuotedString),
|
||||||
|
}
|
56
src/types/signed_decimal_floating_point.rs
Normal file
56
src/types/signed_decimal_floating_point.rs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
use crate::{Error, ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::str::{self, FromStr};
|
||||||
|
use trackable::error::ErrorKindExt;
|
||||||
|
|
||||||
|
/// Signed decimal floating-point number.
|
||||||
|
///
|
||||||
|
/// See: [4.2. Attribute Lists]
|
||||||
|
///
|
||||||
|
/// [4.2. Attribute Lists]: https://tools.ietf.org/html/rfc8216#section-4.2
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
|
||||||
|
pub struct SignedDecimalFloatingPoint(f64);
|
||||||
|
|
||||||
|
impl SignedDecimalFloatingPoint {
|
||||||
|
/// Makes a new `SignedDecimalFloatingPoint` instance.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// The given value must be finite,
|
||||||
|
/// otherwise this function will return an error that has the kind `ErrorKind::InvalidInput`.
|
||||||
|
pub fn new(n: f64) -> Result<Self> {
|
||||||
|
track_assert!(n.is_finite(), ErrorKind::InvalidInput);
|
||||||
|
Ok(SignedDecimalFloatingPoint(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts `DecimalFloatingPoint` to `f64`.
|
||||||
|
pub fn as_f64(self) -> f64 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<i32> for SignedDecimalFloatingPoint {
|
||||||
|
fn from(f: i32) -> Self {
|
||||||
|
SignedDecimalFloatingPoint(f64::from(f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for SignedDecimalFloatingPoint {}
|
||||||
|
|
||||||
|
impl fmt::Display for SignedDecimalFloatingPoint {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for SignedDecimalFloatingPoint {
|
||||||
|
type Err = Error;
|
||||||
|
fn from_str(s: &str) -> Result<Self> {
|
||||||
|
track_assert!(
|
||||||
|
s.chars().all(|c| c.is_digit(10) || c == '.' || c == '-'),
|
||||||
|
ErrorKind::InvalidInput
|
||||||
|
);
|
||||||
|
let n = track!(s.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?;
|
||||||
|
Ok(SignedDecimalFloatingPoint(n))
|
||||||
|
}
|
||||||
|
}
|
55
src/types/single_line_string.rs
Normal file
55
src/types/single_line_string.rs
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
use crate::{ErrorKind, Result};
|
||||||
|
use std::fmt;
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
/// String that represents a single line in a playlist file.
|
||||||
|
///
|
||||||
|
/// See: [4.1. Definition of a Playlist]
|
||||||
|
///
|
||||||
|
/// [4.1. Definition of a Playlist]: https://tools.ietf.org/html/rfc8216#section-4.1
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
|
pub struct SingleLineString(String);
|
||||||
|
|
||||||
|
impl SingleLineString {
|
||||||
|
/// Makes a new `SingleLineString` instance.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// If the given string contains any control characters,
|
||||||
|
/// this function will return an error which has the kind `ErrorKind::InvalidInput`.
|
||||||
|
pub fn new<T: Into<String>>(s: T) -> Result<Self> {
|
||||||
|
let s = s.into();
|
||||||
|
track_assert!(!s.chars().any(|c| c.is_control()), ErrorKind::InvalidInput);
|
||||||
|
Ok(SingleLineString(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for SingleLineString {
|
||||||
|
type Target = str;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<str> for SingleLineString {
|
||||||
|
fn as_ref(&self) -> &str {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for SingleLineString {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn single_line_string() {
|
||||||
|
assert!(SingleLineString::new("foo").is_ok());
|
||||||
|
assert!(SingleLineString::new("b\rar").is_err());
|
||||||
|
}
|
||||||
|
}
|
15
src/utils.rs
Normal file
15
src/utils.rs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
use crate::{ErrorKind, Result};
|
||||||
|
use trackable::error::ErrorKindExt;
|
||||||
|
|
||||||
|
pub fn parse_yes_or_no(s: &str) -> Result<bool> {
|
||||||
|
match s {
|
||||||
|
"YES" => Ok(true),
|
||||||
|
"NO" => Ok(false),
|
||||||
|
_ => track_panic!(ErrorKind::InvalidInput, "Unexpected value: {:?}", s),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_u64(s: &str) -> Result<u64> {
|
||||||
|
let n = track!(s.parse().map_err(|e| ErrorKind::InvalidInput.cause(e)))?;
|
||||||
|
Ok(n)
|
||||||
|
}
|
Loading…
Reference in a new issue