2016-06-03 18:56:45 +00:00
|
|
|
//! Contains all the structs used for parsing.
|
|
|
|
//!
|
|
|
|
//! The main type here is the `Playlist` enum.
|
|
|
|
//! Which is either a `MasterPlaylist` or a `MediaPlaylist`.
|
|
|
|
|
2022-04-14 09:07:05 +00:00
|
|
|
use crate::QuotedOrUnquoted;
|
2016-06-03 18:56:45 +00:00
|
|
|
use std::collections::HashMap;
|
2022-04-17 06:20:01 +00:00
|
|
|
use std::convert::{TryFrom, TryInto};
|
2016-06-03 18:56:45 +00:00
|
|
|
use std::f32;
|
2021-10-18 09:41:28 +00:00
|
|
|
use std::fmt;
|
2021-10-14 19:21:03 +00:00
|
|
|
use std::fmt::Display;
|
2021-10-18 09:41:28 +00:00
|
|
|
use std::io::Write;
|
|
|
|
use std::str::FromStr;
|
2016-06-03 18:56:45 +00:00
|
|
|
|
2017-02-17 13:50:50 +00:00
|
|
|
macro_rules! write_some_attribute_quoted {
|
2021-10-18 09:41:28 +00:00
|
|
|
($w:expr, $tag:expr, $o:expr) => {
|
|
|
|
if let &Some(ref v) = $o {
|
|
|
|
write!($w, "{}=\"{}\"", $tag, v)
|
|
|
|
} else {
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
};
|
2020-02-27 01:17:13 +00:00
|
|
|
}
|
2017-02-17 13:50:50 +00:00
|
|
|
|
|
|
|
macro_rules! write_some_attribute {
|
2021-10-18 09:41:28 +00:00
|
|
|
($w:expr, $tag:expr, $o:expr) => {
|
|
|
|
if let &Some(ref v) = $o {
|
|
|
|
write!($w, "{}={}", $tag, v)
|
|
|
|
} else {
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
};
|
2020-02-27 01:17:13 +00:00
|
|
|
}
|
2017-02-17 13:50:50 +00:00
|
|
|
|
2022-06-30 07:41:58 +00:00
|
|
|
macro_rules! write_some_other_attributes {
|
|
|
|
($w:expr, $attr:expr) => {
|
|
|
|
if let &Some(ref attributes) = $attr {
|
|
|
|
let mut status = std::io::Result::Ok(());
|
|
|
|
for (name, val) in attributes {
|
|
|
|
let res = write!($w, ",{}={}", name, val);
|
|
|
|
if res.is_err() {
|
|
|
|
status = res;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
status
|
|
|
|
} else {
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
macro_rules! is_yes {
|
|
|
|
($attrs:expr, $attr:expr) => {
|
|
|
|
match $attrs.remove($attr) {
|
|
|
|
Some(QuotedOrUnquoted::Unquoted(ref s)) if s == "YES" => true,
|
|
|
|
Some(QuotedOrUnquoted::Unquoted(ref s)) if s == "NO" => false,
|
|
|
|
Some(ref s) => {
|
|
|
|
return Err(format!(
|
|
|
|
"Can't create bool from {} for {} attribute",
|
|
|
|
s, $attr
|
|
|
|
))
|
|
|
|
}
|
|
|
|
None => false,
|
2017-02-17 13:50:50 +00:00
|
|
|
}
|
2021-10-18 09:41:28 +00:00
|
|
|
};
|
2020-02-27 01:17:13 +00:00
|
|
|
}
|
2017-02-17 13:50:50 +00:00
|
|
|
|
2022-06-30 07:41:58 +00:00
|
|
|
macro_rules! quoted_string {
|
|
|
|
($attrs:expr, $attr:expr) => {
|
|
|
|
match $attrs.remove($attr) {
|
|
|
|
Some(QuotedOrUnquoted::Quoted(s)) => Some(s),
|
|
|
|
Some(QuotedOrUnquoted::Unquoted(_)) => {
|
|
|
|
return Err(format!(
|
|
|
|
"Can't create {} attribute from unquoted string",
|
|
|
|
$attr
|
|
|
|
))
|
|
|
|
}
|
|
|
|
None => None,
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
macro_rules! unquoted_string {
|
|
|
|
($attrs:expr, $attr:expr) => {
|
|
|
|
match $attrs.remove($attr) {
|
|
|
|
Some(QuotedOrUnquoted::Unquoted(s)) => Some(s),
|
|
|
|
Some(QuotedOrUnquoted::Quoted(_)) => {
|
|
|
|
return Err(format!(
|
|
|
|
"Can't create {} attribute from quoted string",
|
|
|
|
$attr
|
|
|
|
))
|
|
|
|
}
|
|
|
|
None => None,
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
macro_rules! unquoted_string_parse {
|
|
|
|
($attrs:expr, $attr:expr, $parse:expr) => {
|
|
|
|
match $attrs.remove($attr) {
|
|
|
|
Some(QuotedOrUnquoted::Unquoted(s)) => Some(
|
|
|
|
($parse(s.as_str())).map_err(|_| format!("Can't create attribute {}", $attr))?,
|
|
|
|
),
|
|
|
|
Some(QuotedOrUnquoted::Quoted(_)) => {
|
|
|
|
return Err(format!(
|
|
|
|
"Can't create {} attribute from quoted string",
|
|
|
|
$attr
|
|
|
|
))
|
|
|
|
}
|
|
|
|
None => None,
|
|
|
|
}
|
|
|
|
};
|
|
|
|
($attrs:expr, $attr:expr) => {
|
|
|
|
unquoted_string_parse!($attrs, $attr, |s: &str| s.parse())
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
macro_rules! quoted_string_parse {
|
|
|
|
($attrs:expr, $attr:expr, $parse:expr) => {
|
|
|
|
match $attrs.remove($attr) {
|
|
|
|
Some(QuotedOrUnquoted::Quoted(s)) => Some(
|
|
|
|
($parse(s.as_str())).map_err(|_| format!("Can't create attribute {}", $attr))?,
|
|
|
|
),
|
|
|
|
Some(QuotedOrUnquoted::Unquoted(_)) => {
|
|
|
|
return Err(format!(
|
|
|
|
"Can't create {} attribute from unquoted string",
|
|
|
|
$attr
|
|
|
|
))
|
|
|
|
}
|
|
|
|
None => None,
|
|
|
|
}
|
|
|
|
};
|
|
|
|
($attrs:expr, $attr:expr) => {
|
|
|
|
quoted_string_parse!($attrs, $attr, |s: &str| s.parse())
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2016-06-03 18:56:45 +00:00
|
|
|
/// [Playlist](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.1),
|
|
|
|
/// can either be a `MasterPlaylist` or a `MediaPlaylist`.
|
|
|
|
///
|
|
|
|
/// A Playlist is a Media Playlist if all URI lines in the Playlist
|
|
|
|
/// identify Media Segments. A Playlist is a Master Playlist if all URI
|
|
|
|
/// lines in the Playlist identify Media Playlists. A Playlist MUST be
|
|
|
|
/// either a Media Playlist or a Master Playlist; all other Playlists are invalid.
|
2018-11-26 01:43:42 +00:00
|
|
|
#[derive(Debug, PartialEq, Clone)]
|
2016-06-03 18:56:45 +00:00
|
|
|
pub enum Playlist {
|
|
|
|
MasterPlaylist(MasterPlaylist),
|
|
|
|
MediaPlaylist(MediaPlaylist),
|
|
|
|
}
|
|
|
|
|
2017-02-17 13:50:50 +00:00
|
|
|
impl Playlist {
|
|
|
|
pub fn write_to<T: Write>(&self, writer: &mut T) -> std::io::Result<()> {
|
2021-10-18 09:48:30 +00:00
|
|
|
match *self {
|
|
|
|
Playlist::MasterPlaylist(ref pl) => pl.write_to(writer),
|
|
|
|
Playlist::MediaPlaylist(ref pl) => pl.write_to(writer),
|
2017-02-17 13:50:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-03 18:56:45 +00:00
|
|
|
// -----------------------------------------------------------------------------------------------
|
|
|
|
// Master Playlist
|
|
|
|
// -----------------------------------------------------------------------------------------------
|
|
|
|
|
2021-11-18 13:04:17 +00:00
|
|
|
/// A [Master Playlist](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4)
|
2016-06-03 18:56:45 +00:00
|
|
|
/// provides a set of Variant Streams, each of which
|
|
|
|
/// describes a different version of the same content.
|
2022-04-17 06:20:01 +00:00
|
|
|
#[derive(Debug, Default, PartialEq, Clone)]
|
2016-06-03 18:56:45 +00:00
|
|
|
pub struct MasterPlaylist {
|
2022-04-17 08:08:07 +00:00
|
|
|
pub version: Option<usize>,
|
2016-06-03 18:56:45 +00:00
|
|
|
pub variants: Vec<VariantStream>,
|
2021-04-19 15:55:02 +00:00
|
|
|
pub session_data: Vec<SessionData>,
|
|
|
|
pub session_key: Vec<SessionKey>,
|
2016-06-03 18:56:45 +00:00
|
|
|
pub start: Option<Start>,
|
|
|
|
pub independent_segments: bool,
|
2021-03-11 20:15:22 +00:00
|
|
|
pub alternatives: Vec<AlternativeMedia>, // EXT-X-MEDIA tags
|
2021-10-14 19:21:03 +00:00
|
|
|
pub unknown_tags: Vec<ExtTag>,
|
2016-06-03 18:56:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl MasterPlaylist {
|
2021-04-18 16:51:29 +00:00
|
|
|
pub fn get_newest_variant(&mut self) -> Option<&mut VariantStream> {
|
2016-06-03 18:56:45 +00:00
|
|
|
self.variants.iter_mut().rev().find(|v| !v.is_i_frame)
|
|
|
|
}
|
2017-02-17 13:50:50 +00:00
|
|
|
|
|
|
|
pub fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
|
2022-04-18 08:03:54 +00:00
|
|
|
writeln!(w, "#EXTM3U")?;
|
2022-04-17 08:08:07 +00:00
|
|
|
|
|
|
|
if let Some(ref v) = self.version {
|
|
|
|
writeln!(w, "#EXT-X-VERSION:{}", v)?;
|
|
|
|
}
|
2022-07-21 05:04:20 +00:00
|
|
|
if self.independent_segments {
|
|
|
|
writeln!(w, "#EXT-X-INDEPENDENT-SEGMENTS")?;
|
|
|
|
}
|
2017-02-17 13:50:50 +00:00
|
|
|
|
2021-04-16 19:04:03 +00:00
|
|
|
for alternative in &self.alternatives {
|
|
|
|
alternative.write_to(w)?;
|
|
|
|
}
|
|
|
|
|
2017-02-17 13:50:50 +00:00
|
|
|
for variant in &self.variants {
|
|
|
|
variant.write_to(w)?;
|
|
|
|
}
|
2021-04-19 15:55:02 +00:00
|
|
|
for session_data in &self.session_data {
|
2017-02-17 13:50:50 +00:00
|
|
|
session_data.write_to(w)?;
|
|
|
|
}
|
2021-04-19 15:55:02 +00:00
|
|
|
for session_key in &self.session_key {
|
2017-02-17 13:50:50 +00:00
|
|
|
session_key.write_to(w)?;
|
|
|
|
}
|
|
|
|
if let Some(ref start) = self.start {
|
|
|
|
start.write_to(w)?;
|
|
|
|
}
|
2021-10-14 19:21:03 +00:00
|
|
|
for unknown_tag in &self.unknown_tags {
|
|
|
|
writeln!(w, "{}", unknown_tag)?;
|
|
|
|
}
|
2017-02-17 13:50:50 +00:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2016-06-03 18:56:45 +00:00
|
|
|
}
|
|
|
|
|
2021-11-18 13:04:17 +00:00
|
|
|
/// [`#EXT-X-STREAM-INF:<attribute-list> <URI>`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.2)
|
|
|
|
/// [`#EXT-X-I-FRAME-STREAM-INF:<attribute-list>`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.3)
|
2016-06-03 18:56:45 +00:00
|
|
|
///
|
|
|
|
/// A Variant Stream includes a Media Playlist that specifies media
|
|
|
|
/// encoded at a particular bit rate, in a particular format, and at a
|
|
|
|
/// particular resolution for media containing video.
|
|
|
|
///
|
|
|
|
/// A Variant Stream can also specify a set of Renditions. Renditions
|
|
|
|
/// are alternate versions of the content, such as audio produced in
|
|
|
|
/// different languages or video recorded from different camera angles.
|
|
|
|
///
|
|
|
|
/// Clients should switch between different Variant Streams to adapt to
|
|
|
|
/// network conditions. Clients should choose Renditions based on user
|
|
|
|
/// preferences.
|
2022-04-17 06:20:01 +00:00
|
|
|
#[derive(Debug, Default, PartialEq, Clone)]
|
2016-06-03 18:56:45 +00:00
|
|
|
pub struct VariantStream {
|
|
|
|
pub is_i_frame: bool,
|
|
|
|
pub uri: String,
|
|
|
|
|
|
|
|
// <attribute-list>
|
2022-04-17 06:20:01 +00:00
|
|
|
pub bandwidth: u64,
|
|
|
|
pub average_bandwidth: Option<u64>,
|
2020-02-27 01:17:13 +00:00
|
|
|
pub codecs: Option<String>,
|
2022-04-17 06:20:01 +00:00
|
|
|
pub resolution: Option<Resolution>,
|
|
|
|
pub frame_rate: Option<f64>,
|
|
|
|
pub hdcp_level: Option<HDCPLevel>,
|
2016-06-03 18:56:45 +00:00
|
|
|
pub audio: Option<String>,
|
|
|
|
pub video: Option<String>,
|
|
|
|
pub subtitles: Option<String>,
|
2022-04-17 06:20:01 +00:00
|
|
|
pub closed_captions: Option<ClosedCaptionGroupId>,
|
2016-06-03 18:56:45 +00:00
|
|
|
// PROGRAM-ID tag was removed in protocol version 6
|
2022-06-30 07:41:58 +00:00
|
|
|
pub other_attributes: Option<HashMap<String, QuotedOrUnquoted>>,
|
2016-06-03 18:56:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl VariantStream {
|
2022-06-30 07:41:58 +00:00
|
|
|
pub(crate) fn from_hashmap(
|
2022-04-14 09:07:05 +00:00
|
|
|
mut attrs: HashMap<String, QuotedOrUnquoted>,
|
|
|
|
is_i_frame: bool,
|
2022-04-17 06:20:01 +00:00
|
|
|
) -> Result<VariantStream, String> {
|
2022-06-30 07:41:58 +00:00
|
|
|
let uri = quoted_string!(attrs, "URI").unwrap_or_default();
|
|
|
|
// TODO: keep in attrs if parsing optional attributes fails
|
|
|
|
let bandwidth = unquoted_string_parse!(attrs, "BANDWIDTH", |s: &str| s
|
|
|
|
.parse::<u64>()
|
|
|
|
.map_err(|err| format!("Failed to parse BANDWIDTH attribute: {}", err)))
|
2022-07-20 08:30:34 +00:00
|
|
|
.ok_or_else(|| String::from("EXT-X-STREAM-INF without mandatory BANDWIDTH attribute"))?;
|
2022-06-30 07:41:58 +00:00
|
|
|
let average_bandwidth = unquoted_string_parse!(attrs, "AVERAGE-BANDWIDTH", |s: &str| s
|
|
|
|
.parse::<u64>()
|
|
|
|
.map_err(|err| format!("Failed to parse AVERAGE-BANDWIDTH: {}", err)));
|
|
|
|
let codecs = quoted_string!(attrs, "CODECS");
|
|
|
|
let resolution = unquoted_string_parse!(attrs, "RESOLUTION");
|
|
|
|
let frame_rate = unquoted_string_parse!(attrs, "FRAME-RATE", |s: &str| s
|
|
|
|
.parse::<f64>()
|
|
|
|
.map_err(|err| format!("Failed to parse FRAME-RATE attribute: {}", err)));
|
|
|
|
let hdcp_level = unquoted_string_parse!(attrs, "HDCP-LEVEL");
|
|
|
|
let audio = quoted_string!(attrs, "AUDIO");
|
|
|
|
let video = quoted_string!(attrs, "VIDEO");
|
|
|
|
let subtitles = quoted_string!(attrs, "SUBTITLES");
|
2022-04-17 06:20:01 +00:00
|
|
|
let closed_captions = attrs
|
|
|
|
.remove("CLOSED-CAPTIONS")
|
|
|
|
.map(|c| c.try_into())
|
|
|
|
.transpose()?;
|
2022-06-30 07:41:58 +00:00
|
|
|
let other_attributes = if attrs.is_empty() { None } else { Some(attrs) };
|
2022-04-17 06:20:01 +00:00
|
|
|
|
|
|
|
Ok(VariantStream {
|
2021-10-18 09:48:30 +00:00
|
|
|
is_i_frame,
|
2022-04-17 06:20:01 +00:00
|
|
|
uri,
|
|
|
|
bandwidth,
|
|
|
|
average_bandwidth,
|
|
|
|
codecs,
|
|
|
|
resolution,
|
|
|
|
frame_rate,
|
|
|
|
hdcp_level,
|
|
|
|
audio,
|
|
|
|
video,
|
|
|
|
subtitles,
|
|
|
|
closed_captions,
|
2022-06-30 07:41:58 +00:00
|
|
|
other_attributes,
|
2022-04-17 06:20:01 +00:00
|
|
|
})
|
2016-06-03 18:56:45 +00:00
|
|
|
}
|
2017-02-17 13:50:50 +00:00
|
|
|
|
2022-06-30 07:41:58 +00:00
|
|
|
pub(crate) fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
|
2017-02-17 13:50:50 +00:00
|
|
|
if self.is_i_frame {
|
|
|
|
write!(w, "#EXT-X-I-FRAME-STREAM-INF:")?;
|
|
|
|
self.write_stream_inf_common_attributes(w)?;
|
2020-02-11 20:43:06 +00:00
|
|
|
writeln!(w, ",URI=\"{}\"", self.uri)
|
2021-10-18 09:41:28 +00:00
|
|
|
} else {
|
2017-02-17 13:50:50 +00:00
|
|
|
write!(w, "#EXT-X-STREAM-INF:")?;
|
|
|
|
self.write_stream_inf_common_attributes(w)?;
|
|
|
|
write_some_attribute_quoted!(w, ",AUDIO", &self.audio)?;
|
|
|
|
write_some_attribute_quoted!(w, ",SUBTITLES", &self.subtitles)?;
|
2022-04-17 06:20:01 +00:00
|
|
|
if let Some(ref closed_captions) = self.closed_captions {
|
|
|
|
match closed_captions {
|
|
|
|
ClosedCaptionGroupId::None => write!(w, ",CLOSED-CAPTIONS=NONE")?,
|
|
|
|
ClosedCaptionGroupId::GroupId(s) => write!(w, ",CLOSED-CAPTIONS=\"{}\"", s)?,
|
2022-06-30 07:41:58 +00:00
|
|
|
ClosedCaptionGroupId::Other(s) => write!(w, ",CLOSED-CAPTIONS={}", s)?,
|
2022-04-17 06:20:01 +00:00
|
|
|
}
|
|
|
|
}
|
2021-10-18 09:48:30 +00:00
|
|
|
writeln!(w)?;
|
2017-02-17 13:50:50 +00:00
|
|
|
writeln!(w, "{}", self.uri)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn write_stream_inf_common_attributes<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
|
|
|
|
write!(w, "BANDWIDTH={}", &self.bandwidth)?;
|
|
|
|
write_some_attribute!(w, ",AVERAGE-BANDWIDTH", &self.average_bandwidth)?;
|
2020-02-27 01:17:13 +00:00
|
|
|
write_some_attribute_quoted!(w, ",CODECS", &self.codecs)?;
|
2017-02-17 13:50:50 +00:00
|
|
|
write_some_attribute!(w, ",RESOLUTION", &self.resolution)?;
|
|
|
|
write_some_attribute!(w, ",FRAME-RATE", &self.frame_rate)?;
|
2021-01-20 10:25:52 +00:00
|
|
|
write_some_attribute!(w, ",HDCP-LEVEL", &self.hdcp_level)?;
|
2022-06-30 07:41:58 +00:00
|
|
|
write_some_attribute_quoted!(w, ",VIDEO", &self.video)?;
|
|
|
|
write_some_other_attributes!(w, &self.other_attributes)?;
|
|
|
|
Ok(())
|
2017-02-17 13:50:50 +00:00
|
|
|
}
|
2016-06-03 18:56:45 +00:00
|
|
|
}
|
|
|
|
|
2022-04-17 06:20:01 +00:00
|
|
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
|
|
|
|
pub struct Resolution {
|
|
|
|
pub width: u64,
|
|
|
|
pub height: u64,
|
|
|
|
}
|
|
|
|
|
2022-06-30 07:41:58 +00:00
|
|
|
impl Display for Resolution {
|
2022-04-17 06:20:01 +00:00
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
write!(f, "{}x{}", self.width, self.height)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl FromStr for Resolution {
|
|
|
|
type Err = String;
|
|
|
|
|
|
|
|
fn from_str(s: &str) -> Result<Resolution, String> {
|
|
|
|
match s.split_once('x') {
|
|
|
|
Some((width, height)) => {
|
|
|
|
let width = width
|
|
|
|
.parse::<u64>()
|
2022-06-30 07:41:58 +00:00
|
|
|
.map_err(|err| format!("Can't parse RESOLUTION attribute width: {}", err))?;
|
2022-04-17 06:20:01 +00:00
|
|
|
let height = height
|
|
|
|
.parse::<u64>()
|
2022-06-30 07:41:58 +00:00
|
|
|
.map_err(|err| format!("Can't parse RESOLUTION attribute height: {}", err))?;
|
2022-04-17 06:20:01 +00:00
|
|
|
Ok(Resolution { width, height })
|
|
|
|
}
|
2022-06-30 07:41:58 +00:00
|
|
|
None => Err(String::from("Invalid RESOLUTION attribute")),
|
2022-04-17 06:20:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-30 07:41:58 +00:00
|
|
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
2022-04-17 06:20:01 +00:00
|
|
|
pub enum HDCPLevel {
|
|
|
|
Type0,
|
|
|
|
Type1,
|
|
|
|
None,
|
2022-06-30 07:41:58 +00:00
|
|
|
Other(String),
|
2022-04-17 06:20:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl FromStr for HDCPLevel {
|
|
|
|
type Err = String;
|
|
|
|
|
|
|
|
fn from_str(s: &str) -> Result<HDCPLevel, String> {
|
|
|
|
match s {
|
|
|
|
"TYPE-0" => Ok(HDCPLevel::Type0),
|
|
|
|
"TYPE-1" => Ok(HDCPLevel::Type1),
|
|
|
|
"NONE" => Ok(HDCPLevel::None),
|
2022-09-06 15:21:10 +00:00
|
|
|
_ => Ok(HDCPLevel::Other(String::from(s))),
|
2022-04-17 06:20:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-30 07:41:58 +00:00
|
|
|
impl Display for HDCPLevel {
|
2022-04-17 06:20:01 +00:00
|
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
|
|
write!(
|
|
|
|
f,
|
|
|
|
"{}",
|
2022-06-30 07:41:58 +00:00
|
|
|
match self {
|
2022-04-17 06:20:01 +00:00
|
|
|
HDCPLevel::Type0 => "TYPE-0",
|
|
|
|
HDCPLevel::Type1 => "TYPE-1",
|
|
|
|
HDCPLevel::None => "NONE",
|
2022-06-30 07:41:58 +00:00
|
|
|
HDCPLevel::Other(s) => s,
|
2022-04-17 06:20:01 +00:00
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-30 07:41:58 +00:00
|
|
|
/// TODO docs
|
2022-04-17 06:20:01 +00:00
|
|
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
|
|
|
pub enum ClosedCaptionGroupId {
|
|
|
|
None,
|
|
|
|
GroupId(String),
|
2022-06-30 07:41:58 +00:00
|
|
|
Other(String),
|
2022-04-17 06:20:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl TryFrom<QuotedOrUnquoted> for ClosedCaptionGroupId {
|
|
|
|
type Error = String;
|
|
|
|
|
|
|
|
fn try_from(s: QuotedOrUnquoted) -> Result<ClosedCaptionGroupId, String> {
|
|
|
|
match s {
|
|
|
|
QuotedOrUnquoted::Unquoted(s) if s == "NONE" => Ok(ClosedCaptionGroupId::None),
|
2022-09-06 15:21:10 +00:00
|
|
|
QuotedOrUnquoted::Unquoted(s) => Ok(ClosedCaptionGroupId::Other(String::from(s))),
|
2022-04-17 06:20:01 +00:00
|
|
|
QuotedOrUnquoted::Quoted(s) => Ok(ClosedCaptionGroupId::GroupId(s)),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-18 13:04:17 +00:00
|
|
|
/// [`#EXT-X-MEDIA:<attribute-list>`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.1)
|
2016-06-03 18:56:45 +00:00
|
|
|
///
|
|
|
|
/// The EXT-X-MEDIA tag is used to relate Media Playlists that contain
|
|
|
|
/// alternative Renditions (Section 4.3.4.2.1) of the same content. For
|
|
|
|
/// example, three EXT-X-MEDIA tags can be used to identify audio-only
|
|
|
|
/// Media Playlists that contain English, French and Spanish Renditions
|
|
|
|
/// of the same presentation. Or two EXT-X-MEDIA tags can be used to
|
|
|
|
/// identify video-only Media Playlists that show two different camera angles.
|
2022-01-07 10:44:56 +00:00
|
|
|
#[derive(Debug, Default, PartialEq, Eq, Clone)]
|
2016-06-03 18:56:45 +00:00
|
|
|
pub struct AlternativeMedia {
|
|
|
|
// <attribute-list>
|
|
|
|
pub media_type: AlternativeMediaType,
|
|
|
|
pub uri: Option<String>,
|
|
|
|
pub group_id: String,
|
|
|
|
pub language: Option<String>,
|
|
|
|
pub assoc_language: Option<String>,
|
|
|
|
pub name: String, // All EXT-X-MEDIA tags in the same Group MUST have different NAME attributes.
|
|
|
|
pub default: bool, // Its absence indicates an implicit value of NO
|
|
|
|
pub autoselect: bool, // Its absence indicates an implicit value of NO
|
|
|
|
pub forced: bool, // Its absence indicates an implicit value of NO
|
2022-06-30 07:41:58 +00:00
|
|
|
pub instream_id: Option<InstreamId>,
|
2016-06-03 18:56:45 +00:00
|
|
|
pub characteristics: Option<String>,
|
2021-01-20 10:25:52 +00:00
|
|
|
pub channels: Option<String>,
|
2022-06-30 07:41:58 +00:00
|
|
|
pub other_attributes: Option<HashMap<String, QuotedOrUnquoted>>,
|
2016-06-03 18:56:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl AlternativeMedia {
|
2022-06-30 07:41:58 +00:00
|
|
|
pub(crate) fn from_hashmap(
|
|
|
|
mut attrs: HashMap<String, QuotedOrUnquoted>,
|
|
|
|
) -> Result<AlternativeMedia, String> {
|
|
|
|
let media_type = unquoted_string_parse!(attrs, "TYPE")
|
|
|
|
.ok_or_else(|| String::from("EXT-X-MEDIA without mandatory TYPE attribute"))?;
|
|
|
|
let uri = quoted_string!(attrs, "URI");
|
|
|
|
|
|
|
|
if media_type == AlternativeMediaType::ClosedCaptions && uri.is_some() {
|
|
|
|
return Err(String::from(
|
|
|
|
"URI attribute must not be included in CLOSED-CAPTIONS Alternative Medias",
|
|
|
|
));
|
2016-06-03 18:56:45 +00:00
|
|
|
}
|
2022-06-30 07:41:58 +00:00
|
|
|
|
|
|
|
let group_id = quoted_string!(attrs, "GROUP-ID")
|
|
|
|
.ok_or_else(|| String::from("EXT-X-MEDIA without mandatory GROUP-ID attribute"))?;
|
|
|
|
let language = quoted_string!(attrs, "LANGUAGE");
|
|
|
|
let assoc_language = quoted_string!(attrs, "ASSOC-LANGUAGE");
|
|
|
|
let name = quoted_string!(attrs, "NAME")
|
|
|
|
.ok_or_else(|| String::from("EXT-X-MEDIA without mandatory NAME attribute"))?;
|
|
|
|
let default = is_yes!(attrs, "DEFAULT");
|
|
|
|
let autoselect = is_yes!(attrs, "AUTOSELECT");
|
|
|
|
|
|
|
|
if media_type != AlternativeMediaType::Subtitles && attrs.contains_key("FORCED") {
|
|
|
|
return Err(String::from(
|
|
|
|
"FORCED attribute must not be included in non-SUBTITLE Alternative Medias",
|
|
|
|
));
|
|
|
|
}
|
|
|
|
let forced = is_yes!(attrs, "FORCED");
|
|
|
|
|
|
|
|
if media_type != AlternativeMediaType::ClosedCaptions && attrs.contains_key("INSTREAM-ID") {
|
|
|
|
return Err(String::from("INSTREAM-ID attribute must not be included in non-CLOSED-CAPTIONS Alternative Medias"));
|
|
|
|
} else if media_type == AlternativeMediaType::ClosedCaptions
|
|
|
|
&& !attrs.contains_key("INSTREAM-ID")
|
|
|
|
{
|
|
|
|
return Err(String::from(
|
|
|
|
"INSTREAM-ID attribute must be included in CLOSED-CAPTIONS Alternative Medias",
|
|
|
|
));
|
|
|
|
}
|
|
|
|
let instream_id = quoted_string_parse!(attrs, "INSTREAM-ID");
|
|
|
|
let characteristics = quoted_string!(attrs, "CHARACTERISTICS");
|
|
|
|
let channels = quoted_string!(attrs, "CHANNELS");
|
|
|
|
let other_attributes = if attrs.is_empty() { None } else { Some(attrs) };
|
|
|
|
|
|
|
|
Ok(AlternativeMedia {
|
|
|
|
media_type,
|
|
|
|
uri,
|
|
|
|
group_id,
|
|
|
|
language,
|
|
|
|
assoc_language,
|
|
|
|
name,
|
|
|
|
default,
|
|
|
|
autoselect,
|
|
|
|
forced,
|
|
|
|
instream_id,
|
|
|
|
characteristics,
|
|
|
|
channels,
|
|
|
|
other_attributes,
|
|
|
|
})
|
2016-06-03 18:56:45 +00:00
|
|
|
}
|
2017-02-17 13:50:50 +00:00
|
|
|
|
2022-06-30 07:41:58 +00:00
|
|
|
pub(crate) fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
|
2017-02-17 13:50:50 +00:00
|
|
|
write!(w, "#EXT-X-MEDIA:")?;
|
|
|
|
write!(w, "TYPE={}", self.media_type)?;
|
2022-06-30 07:41:58 +00:00
|
|
|
if self.media_type != AlternativeMediaType::ClosedCaptions {
|
|
|
|
write_some_attribute_quoted!(w, ",URI", &self.uri)?;
|
|
|
|
}
|
2017-02-17 13:50:50 +00:00
|
|
|
write!(w, ",GROUP-ID=\"{}\"", self.group_id)?;
|
|
|
|
write_some_attribute_quoted!(w, ",LANGUAGE", &self.language)?;
|
|
|
|
write_some_attribute_quoted!(w, ",ASSOC-LANGUAGE", &self.assoc_language)?;
|
|
|
|
write!(w, ",NAME=\"{}\"", self.name)?;
|
2021-10-18 09:41:28 +00:00
|
|
|
if self.default {
|
|
|
|
write!(w, ",DEFAULT=YES")?;
|
|
|
|
}
|
|
|
|
if self.autoselect {
|
|
|
|
write!(w, ",AUTOSELECT=YES")?;
|
|
|
|
}
|
2022-06-30 07:41:58 +00:00
|
|
|
if self.forced && self.media_type == AlternativeMediaType::Subtitles {
|
2021-10-18 09:41:28 +00:00
|
|
|
write!(w, ",FORCED=YES")?;
|
|
|
|
}
|
2022-06-30 07:41:58 +00:00
|
|
|
if self.media_type == AlternativeMediaType::ClosedCaptions {
|
|
|
|
// FIXME: Mandatory for closed captions
|
|
|
|
write_some_attribute_quoted!(w, ",INSTREAM-ID", &self.instream_id)?;
|
|
|
|
}
|
2017-02-17 13:50:50 +00:00
|
|
|
write_some_attribute_quoted!(w, ",CHARACTERISTICS", &self.characteristics)?;
|
2021-01-20 10:25:52 +00:00
|
|
|
write_some_attribute_quoted!(w, ",CHANNELS", &self.channels)?;
|
2022-06-30 07:41:58 +00:00
|
|
|
write_some_other_attributes!(w, &self.other_attributes)?;
|
2021-10-18 09:48:30 +00:00
|
|
|
writeln!(w)
|
2017-02-17 13:50:50 +00:00
|
|
|
}
|
2016-06-03 18:56:45 +00:00
|
|
|
}
|
|
|
|
|
2022-06-30 07:41:58 +00:00
|
|
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
2016-06-03 18:56:45 +00:00
|
|
|
pub enum AlternativeMediaType {
|
|
|
|
Audio,
|
|
|
|
Video,
|
|
|
|
Subtitles,
|
|
|
|
ClosedCaptions,
|
2022-06-30 07:41:58 +00:00
|
|
|
Other(String),
|
2016-06-03 18:56:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl FromStr for AlternativeMediaType {
|
|
|
|
type Err = String;
|
|
|
|
|
|
|
|
fn from_str(s: &str) -> Result<AlternativeMediaType, String> {
|
|
|
|
match s {
|
|
|
|
"AUDIO" => Ok(AlternativeMediaType::Audio),
|
|
|
|
"VIDEO" => Ok(AlternativeMediaType::Video),
|
|
|
|
"SUBTITLES" => Ok(AlternativeMediaType::Subtitles),
|
2022-01-05 17:11:42 +00:00
|
|
|
"CLOSED-CAPTIONS" => Ok(AlternativeMediaType::ClosedCaptions),
|
2022-09-06 15:21:10 +00:00
|
|
|
_ => Ok(AlternativeMediaType::Other(String::from(s))),
|
2016-06-03 18:56:45 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for AlternativeMediaType {
|
|
|
|
fn default() -> AlternativeMediaType {
|
|
|
|
AlternativeMediaType::Video
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-30 07:41:58 +00:00
|
|
|
impl Display for AlternativeMediaType {
|
2017-02-17 13:50:50 +00:00
|
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
2021-10-18 09:41:28 +00:00
|
|
|
write!(
|
|
|
|
f,
|
|
|
|
"{}",
|
2022-06-30 07:41:58 +00:00
|
|
|
match self {
|
2021-10-18 09:48:30 +00:00
|
|
|
AlternativeMediaType::Audio => "AUDIO",
|
|
|
|
AlternativeMediaType::Video => "VIDEO",
|
|
|
|
AlternativeMediaType::Subtitles => "SUBTITLES",
|
2022-01-05 17:11:42 +00:00
|
|
|
AlternativeMediaType::ClosedCaptions => "CLOSED-CAPTIONS",
|
2022-06-30 07:41:58 +00:00
|
|
|
AlternativeMediaType::Other(s) => s.as_str(),
|
2021-10-18 09:41:28 +00:00
|
|
|
}
|
|
|
|
)
|
2020-02-27 01:17:13 +00:00
|
|
|
}
|
2017-02-17 13:50:50 +00:00
|
|
|
}
|
|
|
|
|
2022-06-30 07:41:58 +00:00
|
|
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
|
|
|
pub enum InstreamId {
|
|
|
|
CC(u8),
|
|
|
|
Service(u8),
|
|
|
|
Other(String),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl FromStr for InstreamId {
|
|
|
|
type Err = String;
|
|
|
|
|
|
|
|
fn from_str(s: &str) -> Result<InstreamId, String> {
|
|
|
|
if let Some(cc) = s.strip_prefix("CC") {
|
|
|
|
let cc = cc
|
|
|
|
.parse::<u8>()
|
|
|
|
.map_err(|err| format!("Unable to create InstreamId from {:?}: {}", s, err))?;
|
|
|
|
Ok(InstreamId::CC(cc))
|
|
|
|
} else if let Some(service) = s.strip_prefix("SERVICE") {
|
|
|
|
let service = service
|
|
|
|
.parse::<u8>()
|
|
|
|
.map_err(|err| format!("Unable to create InstreamId from {:?}: {}", s, err))?;
|
|
|
|
Ok(InstreamId::Service(service))
|
|
|
|
} else {
|
2022-09-06 15:21:10 +00:00
|
|
|
Ok(InstreamId::Other(String::from(s)))
|
2022-06-30 07:41:58 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Display for InstreamId {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
|
|
match self {
|
|
|
|
InstreamId::CC(cc) => write!(f, "CC{}", cc),
|
|
|
|
InstreamId::Service(service) => write!(f, "SERVICE{}", service),
|
|
|
|
InstreamId::Other(s) => write!(f, "{}", s),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-18 13:04:17 +00:00
|
|
|
/// [`#EXT-X-SESSION-KEY:<attribute-list>`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.5)
|
2021-04-19 16:35:45 +00:00
|
|
|
/// The EXT-X-SESSION-KEY tag allows encryption keys from Media Playlists
|
|
|
|
/// to be specified in a Master Playlist. This allows the client to
|
|
|
|
/// preload these keys without having to read the Media Playlist(s) first.
|
2022-01-07 10:44:56 +00:00
|
|
|
#[derive(Debug, Default, PartialEq, Eq, Clone)]
|
2016-06-03 18:56:45 +00:00
|
|
|
pub struct SessionKey(pub Key);
|
|
|
|
|
2017-02-17 13:50:50 +00:00
|
|
|
impl SessionKey {
|
2022-06-30 07:41:58 +00:00
|
|
|
pub(crate) fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
|
2017-02-17 13:50:50 +00:00
|
|
|
write!(w, "#EXT-X-SESSION-KEY:")?;
|
|
|
|
self.0.write_attributes_to(w)?;
|
2021-10-18 09:48:30 +00:00
|
|
|
writeln!(w)
|
2017-02-17 13:50:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-07 10:44:56 +00:00
|
|
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
2021-04-19 16:35:45 +00:00
|
|
|
pub enum SessionDataField {
|
|
|
|
Value(String),
|
2021-10-18 09:41:28 +00:00
|
|
|
Uri(String),
|
2021-04-19 16:35:45 +00:00
|
|
|
}
|
|
|
|
|
2021-11-18 13:04:17 +00:00
|
|
|
/// [`#EXT-X-SESSION-DATA:<attribute-list>`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.4)
|
2021-04-19 16:35:45 +00:00
|
|
|
/// The EXT-X-SESSION-DATA tag allows arbitrary session data to be carried
|
|
|
|
/// in a Master Playlist.
|
2022-01-07 10:44:56 +00:00
|
|
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
2016-06-03 18:56:45 +00:00
|
|
|
pub struct SessionData {
|
|
|
|
pub data_id: String,
|
2021-04-19 16:35:45 +00:00
|
|
|
pub field: SessionDataField,
|
2016-06-03 18:56:45 +00:00
|
|
|
pub language: Option<String>,
|
2022-06-30 07:41:58 +00:00
|
|
|
pub other_attributes: Option<HashMap<String, QuotedOrUnquoted>>,
|
2016-06-03 18:56:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl SessionData {
|
2022-06-30 07:41:58 +00:00
|
|
|
pub(crate) fn from_hashmap(
|
2022-04-14 09:07:05 +00:00
|
|
|
mut attrs: HashMap<String, QuotedOrUnquoted>,
|
|
|
|
) -> Result<SessionData, String> {
|
2022-06-30 07:41:58 +00:00
|
|
|
let data_id = quoted_string!(attrs, "DATA-ID")
|
|
|
|
.ok_or_else(|| String::from("EXT-X-SESSION-DATA field without DATA-ID attribute"))?;
|
2021-04-19 16:35:45 +00:00
|
|
|
|
2022-06-30 07:41:58 +00:00
|
|
|
let value = quoted_string!(attrs, "VALUE");
|
|
|
|
let uri = quoted_string!(attrs, "URI");
|
2021-04-19 16:35:45 +00:00
|
|
|
|
|
|
|
// SessionData must contain either a VALUE or a URI,
|
|
|
|
// but not both https://tools.ietf.org/html/rfc8216#section-4.3.4.4
|
|
|
|
let field = match (value, uri) {
|
|
|
|
(Some(value), None) => SessionDataField::Value(value),
|
|
|
|
(None, Some(uri)) => SessionDataField::Uri(uri),
|
2021-10-18 09:41:28 +00:00
|
|
|
(Some(_), Some(_)) => {
|
|
|
|
return Err(format![
|
2022-06-30 07:41:58 +00:00
|
|
|
"EXT-X-SESSION-DATA tag {} contains both a value and an URI",
|
2021-10-18 09:41:28 +00:00
|
|
|
data_id
|
|
|
|
])
|
|
|
|
}
|
|
|
|
(None, None) => {
|
|
|
|
return Err(format![
|
2022-06-30 07:41:58 +00:00
|
|
|
"EXT-X-SESSION-DATA tag {} must contain either a value or an URI",
|
2021-10-18 09:41:28 +00:00
|
|
|
data_id
|
|
|
|
])
|
|
|
|
}
|
2021-04-19 16:35:45 +00:00
|
|
|
};
|
|
|
|
|
2022-06-30 07:41:58 +00:00
|
|
|
let language = quoted_string!(attrs, "LANGUAGE");
|
|
|
|
let other_attributes = if attrs.is_empty() { None } else { Some(attrs) };
|
|
|
|
|
2021-04-19 16:35:45 +00:00
|
|
|
Ok(SessionData {
|
2022-06-30 07:41:58 +00:00
|
|
|
data_id,
|
2021-04-19 16:35:45 +00:00
|
|
|
field,
|
2022-06-30 07:41:58 +00:00
|
|
|
language,
|
|
|
|
other_attributes,
|
2021-04-19 16:35:45 +00:00
|
|
|
})
|
2016-06-03 18:56:45 +00:00
|
|
|
}
|
2017-02-17 13:50:50 +00:00
|
|
|
|
2022-06-30 07:41:58 +00:00
|
|
|
pub(crate) fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
|
2017-02-17 13:50:50 +00:00
|
|
|
write!(w, "#EXT-X-SESSION-DATA:")?;
|
|
|
|
write!(w, "DATA-ID=\"{}\"", self.data_id)?;
|
2021-04-19 16:35:45 +00:00
|
|
|
match &self.field {
|
2021-10-18 09:41:28 +00:00
|
|
|
SessionDataField::Value(value) => write!(w, ",VALUE=\"{}\"", value)?,
|
|
|
|
SessionDataField::Uri(uri) => write!(w, ",URI=\"{}\"", uri)?,
|
2021-04-19 16:35:45 +00:00
|
|
|
};
|
2017-02-17 13:50:50 +00:00
|
|
|
write_some_attribute_quoted!(w, ",LANGUAGE", &self.language)?;
|
2022-06-30 07:41:58 +00:00
|
|
|
write_some_other_attributes!(w, &self.other_attributes)?;
|
2021-10-18 09:48:30 +00:00
|
|
|
writeln!(w)
|
2017-02-17 13:50:50 +00:00
|
|
|
}
|
2016-06-03 18:56:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------------------------------
|
|
|
|
// Media Playlist
|
|
|
|
// -----------------------------------------------------------------------------------------------
|
|
|
|
|
2021-11-18 13:04:17 +00:00
|
|
|
/// A [Media Playlist](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.3)
|
2016-06-03 18:56:45 +00:00
|
|
|
/// contains a list of Media Segments, which when played
|
|
|
|
/// sequentially will play the multimedia presentation.
|
2018-11-26 01:43:42 +00:00
|
|
|
#[derive(Debug, Default, PartialEq, Clone)]
|
2016-06-03 18:56:45 +00:00
|
|
|
pub struct MediaPlaylist {
|
2022-04-17 08:08:07 +00:00
|
|
|
pub version: Option<usize>,
|
2016-06-03 18:56:45 +00:00
|
|
|
/// `#EXT-X-TARGETDURATION:<s>`
|
|
|
|
pub target_duration: f32,
|
|
|
|
/// `#EXT-X-MEDIA-SEQUENCE:<number>`
|
2022-01-07 10:47:03 +00:00
|
|
|
pub media_sequence: u64,
|
2016-06-03 18:56:45 +00:00
|
|
|
pub segments: Vec<MediaSegment>,
|
|
|
|
/// `#EXT-X-DISCONTINUITY-SEQUENCE:<number>`
|
2022-01-07 10:47:03 +00:00
|
|
|
pub discontinuity_sequence: u64,
|
2016-06-03 18:56:45 +00:00
|
|
|
/// `#EXT-X-ENDLIST`
|
|
|
|
pub end_list: bool,
|
|
|
|
/// `#EXT-X-PLAYLIST-TYPE`
|
2017-02-17 13:50:50 +00:00
|
|
|
pub playlist_type: Option<MediaPlaylistType>,
|
2016-06-03 18:56:45 +00:00
|
|
|
/// `#EXT-X-I-FRAMES-ONLY`
|
|
|
|
pub i_frames_only: bool,
|
|
|
|
/// `#EXT-X-START`
|
|
|
|
pub start: Option<Start>,
|
|
|
|
/// `#EXT-X-INDEPENDENT-SEGMENTS`
|
|
|
|
pub independent_segments: bool,
|
2022-06-30 07:41:58 +00:00
|
|
|
/// Unknown tags before the first media segment
|
|
|
|
pub unknown_tags: Vec<ExtTag>,
|
2016-06-03 18:56:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl MediaPlaylist {
|
2017-02-17 13:50:50 +00:00
|
|
|
pub fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
|
2022-04-18 08:03:54 +00:00
|
|
|
writeln!(w, "#EXTM3U")?;
|
2022-04-17 08:08:07 +00:00
|
|
|
|
|
|
|
if let Some(ref v) = self.version {
|
|
|
|
writeln!(w, "#EXT-X-VERSION:{}", v)?;
|
|
|
|
}
|
2022-07-21 05:04:20 +00:00
|
|
|
if self.independent_segments {
|
|
|
|
writeln!(w, "#EXT-X-INDEPENDENT-SEGMENTS")?;
|
|
|
|
}
|
2017-02-17 13:50:50 +00:00
|
|
|
writeln!(w, "#EXT-X-TARGETDURATION:{}", self.target_duration)?;
|
|
|
|
|
2020-02-27 01:17:13 +00:00
|
|
|
if self.media_sequence != 0 {
|
2017-02-17 13:50:50 +00:00
|
|
|
writeln!(w, "#EXT-X-MEDIA-SEQUENCE:{}", self.media_sequence)?;
|
|
|
|
}
|
|
|
|
if self.discontinuity_sequence != 0 {
|
2021-10-18 09:41:28 +00:00
|
|
|
writeln!(
|
|
|
|
w,
|
|
|
|
"#EXT-X-DISCONTINUITY-SEQUENCE:{}",
|
|
|
|
self.discontinuity_sequence
|
|
|
|
)?;
|
2017-02-17 13:50:50 +00:00
|
|
|
}
|
|
|
|
if let Some(ref v) = self.playlist_type {
|
|
|
|
writeln!(w, "#EXT-X-PLAYLIST-TYPE:{}", v)?;
|
|
|
|
}
|
2020-02-27 01:17:13 +00:00
|
|
|
if self.i_frames_only {
|
2017-02-17 13:50:50 +00:00
|
|
|
writeln!(w, "#EXT-X-I-FRAMES-ONLY")?;
|
|
|
|
}
|
|
|
|
if let Some(ref start) = self.start {
|
|
|
|
start.write_to(w)?;
|
|
|
|
}
|
|
|
|
for segment in &self.segments {
|
|
|
|
segment.write_to(w)?;
|
|
|
|
}
|
2022-04-08 23:27:04 +00:00
|
|
|
if self.end_list {
|
|
|
|
writeln!(w, "#EXT-X-ENDLIST")?;
|
|
|
|
}
|
2017-02-17 13:50:50 +00:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2016-06-03 18:56:45 +00:00
|
|
|
}
|
|
|
|
|
2021-11-18 13:04:17 +00:00
|
|
|
/// [`#EXT-X-PLAYLIST-TYPE:<EVENT|VOD>`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.3.5)
|
2022-06-30 07:41:58 +00:00
|
|
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
2016-06-03 18:56:45 +00:00
|
|
|
pub enum MediaPlaylistType {
|
|
|
|
Event,
|
|
|
|
Vod,
|
2022-06-30 07:41:58 +00:00
|
|
|
Other(String),
|
2016-06-03 18:56:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl FromStr for MediaPlaylistType {
|
|
|
|
type Err = String;
|
|
|
|
|
|
|
|
fn from_str(s: &str) -> Result<MediaPlaylistType, String> {
|
|
|
|
match s {
|
|
|
|
"EVENT" => Ok(MediaPlaylistType::Event),
|
|
|
|
"VOD" => Ok(MediaPlaylistType::Vod),
|
2022-09-06 15:21:10 +00:00
|
|
|
_ => Ok(MediaPlaylistType::Other(String::from(s))),
|
2016-06-03 18:56:45 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-30 07:41:58 +00:00
|
|
|
impl Display for MediaPlaylistType {
|
2017-02-17 13:50:50 +00:00
|
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
2021-10-18 09:41:28 +00:00
|
|
|
write!(
|
|
|
|
f,
|
|
|
|
"{}",
|
2022-06-30 07:41:58 +00:00
|
|
|
match self {
|
2021-10-18 09:48:30 +00:00
|
|
|
MediaPlaylistType::Event => "EVENT",
|
|
|
|
MediaPlaylistType::Vod => "VOD",
|
2022-06-30 07:41:58 +00:00
|
|
|
MediaPlaylistType::Other(s) => s,
|
2021-10-18 09:41:28 +00:00
|
|
|
}
|
|
|
|
)
|
2020-02-27 01:17:13 +00:00
|
|
|
}
|
2017-02-17 13:50:50 +00:00
|
|
|
}
|
|
|
|
|
2016-06-03 18:56:45 +00:00
|
|
|
impl Default for MediaPlaylistType {
|
|
|
|
fn default() -> MediaPlaylistType {
|
|
|
|
MediaPlaylistType::Event
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------------------------------
|
|
|
|
// Media Segment
|
|
|
|
// -----------------------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
/// A [Media Segment](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-3)
|
|
|
|
/// is specified by a URI and optionally a byte range.
|
2018-11-26 01:43:42 +00:00
|
|
|
#[derive(Debug, Default, PartialEq, Clone)]
|
2016-06-03 18:56:45 +00:00
|
|
|
pub struct MediaSegment {
|
|
|
|
pub uri: String,
|
|
|
|
/// `#EXTINF:<duration>,[<title>]`
|
|
|
|
pub duration: f32,
|
|
|
|
/// `#EXTINF:<duration>,[<title>]`
|
|
|
|
pub title: Option<String>,
|
|
|
|
/// `#EXT-X-BYTERANGE:<n>[@<o>]`
|
|
|
|
pub byte_range: Option<ByteRange>,
|
|
|
|
/// `#EXT-X-DISCONTINUITY`
|
|
|
|
pub discontinuity: bool,
|
|
|
|
/// `#EXT-X-KEY:<attribute-list>`
|
|
|
|
pub key: Option<Key>,
|
|
|
|
/// `#EXT-X-MAP:<attribute-list>`
|
|
|
|
pub map: Option<Map>,
|
|
|
|
/// `#EXT-X-PROGRAM-DATE-TIME:<YYYY-MM-DDThh:mm:ssZ>`
|
2022-07-20 08:30:34 +00:00
|
|
|
pub program_date_time: Option<chrono::DateTime<chrono::FixedOffset>>,
|
2016-06-03 18:56:45 +00:00
|
|
|
/// `#EXT-X-DATERANGE:<attribute-list>`
|
2022-06-30 07:41:58 +00:00
|
|
|
pub daterange: Option<DateRange>,
|
2021-10-12 21:06:47 +00:00
|
|
|
/// `#EXT-`
|
|
|
|
pub unknown_tags: Vec<ExtTag>,
|
2016-06-03 18:56:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl MediaSegment {
|
2017-02-17 13:50:50 +00:00
|
|
|
pub fn empty() -> MediaSegment {
|
2016-06-03 18:56:45 +00:00
|
|
|
Default::default()
|
|
|
|
}
|
2017-02-17 13:50:50 +00:00
|
|
|
|
2022-06-30 07:41:58 +00:00
|
|
|
pub(crate) fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
|
2017-02-17 13:50:50 +00:00
|
|
|
if let Some(ref byte_range) = self.byte_range {
|
|
|
|
write!(w, "#EXT-X-BYTERANGE:")?;
|
|
|
|
byte_range.write_value_to(w)?;
|
2021-10-18 09:48:30 +00:00
|
|
|
writeln!(w)?;
|
2017-02-17 13:50:50 +00:00
|
|
|
}
|
2020-02-27 01:17:13 +00:00
|
|
|
if self.discontinuity {
|
2021-10-18 09:48:30 +00:00
|
|
|
writeln!(w, "#EXT-X-DISCONTINUITY")?;
|
2017-02-17 13:50:50 +00:00
|
|
|
}
|
|
|
|
if let Some(ref key) = self.key {
|
|
|
|
write!(w, "#EXT-X-KEY:")?;
|
|
|
|
key.write_attributes_to(w)?;
|
2021-10-18 09:48:30 +00:00
|
|
|
writeln!(w)?;
|
2017-02-17 13:50:50 +00:00
|
|
|
}
|
|
|
|
if let Some(ref map) = self.map {
|
|
|
|
write!(w, "#EXT-X-MAP:")?;
|
|
|
|
map.write_attributes_to(w)?;
|
2021-10-18 09:48:30 +00:00
|
|
|
writeln!(w)?;
|
2017-02-17 13:50:50 +00:00
|
|
|
}
|
|
|
|
if let Some(ref v) = self.program_date_time {
|
2022-07-20 08:30:34 +00:00
|
|
|
writeln!(w, "#EXT-X-PROGRAM-DATE-TIME:{}", v.to_rfc3339())?;
|
2017-02-17 13:50:50 +00:00
|
|
|
}
|
|
|
|
if let Some(ref v) = self.daterange {
|
2022-06-30 07:41:58 +00:00
|
|
|
write!(w, "#EXT-X-DATERANGE:")?;
|
|
|
|
v.write_attributes_to(w)?;
|
|
|
|
writeln!(w)?;
|
2020-02-27 01:17:13 +00:00
|
|
|
}
|
2021-10-12 21:06:47 +00:00
|
|
|
for unknown_tag in &self.unknown_tags {
|
2021-10-14 19:21:03 +00:00
|
|
|
writeln!(w, "{}", unknown_tag)?;
|
2021-10-12 21:06:47 +00:00
|
|
|
}
|
2017-02-17 13:50:50 +00:00
|
|
|
|
|
|
|
write!(w, "#EXTINF:{},", self.duration)?;
|
|
|
|
|
|
|
|
if let Some(ref v) = self.title {
|
|
|
|
writeln!(w, "{}", v)?;
|
2020-03-07 05:52:22 +00:00
|
|
|
} else {
|
2021-10-18 09:48:30 +00:00
|
|
|
writeln!(w)?;
|
2017-02-17 13:50:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
writeln!(w, "{}", self.uri)
|
|
|
|
}
|
2016-06-03 18:56:45 +00:00
|
|
|
}
|
|
|
|
|
2022-06-30 07:41:58 +00:00
|
|
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
|
|
|
|
pub enum KeyMethod {
|
|
|
|
None,
|
|
|
|
AES128,
|
|
|
|
SampleAES,
|
|
|
|
Other(String),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for KeyMethod {
|
|
|
|
fn default() -> Self {
|
|
|
|
KeyMethod::None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl FromStr for KeyMethod {
|
|
|
|
type Err = String;
|
|
|
|
|
|
|
|
fn from_str(s: &str) -> Result<KeyMethod, String> {
|
|
|
|
match s {
|
|
|
|
"NONE" => Ok(KeyMethod::None),
|
|
|
|
"AES-128" => Ok(KeyMethod::AES128),
|
|
|
|
"SAMPLE-AES" => Ok(KeyMethod::SampleAES),
|
2022-09-06 15:21:10 +00:00
|
|
|
_ => Ok(KeyMethod::Other(String::from(s))),
|
2022-06-30 07:41:58 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Display for KeyMethod {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
|
|
write!(
|
|
|
|
f,
|
|
|
|
"{}",
|
|
|
|
match self {
|
|
|
|
KeyMethod::None => "NONE",
|
|
|
|
KeyMethod::AES128 => "AES-128",
|
|
|
|
KeyMethod::SampleAES => "SAMPLE-AES",
|
|
|
|
KeyMethod::Other(s) => s,
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-18 13:04:17 +00:00
|
|
|
/// [`#EXT-X-KEY:<attribute-list>`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.2.4)
|
2016-06-03 18:56:45 +00:00
|
|
|
///
|
|
|
|
/// Media Segments MAY be encrypted. The EXT-X-KEY tag specifies how to
|
|
|
|
/// decrypt them. It applies to every Media Segment that appears between
|
|
|
|
/// it and the next EXT-X-KEY tag in the Playlist file with the same
|
|
|
|
/// KEYFORMAT attribute (or the end of the Playlist file). Two or more
|
|
|
|
/// EXT-X-KEY tags with different KEYFORMAT attributes MAY apply to the
|
|
|
|
/// same Media Segment if they ultimately produce the same decryption key.
|
2022-01-07 10:44:56 +00:00
|
|
|
#[derive(Debug, Default, PartialEq, Eq, Clone)]
|
2016-06-03 18:56:45 +00:00
|
|
|
pub struct Key {
|
2022-06-30 07:41:58 +00:00
|
|
|
pub method: KeyMethod,
|
2016-06-03 18:56:45 +00:00
|
|
|
pub uri: Option<String>,
|
|
|
|
pub iv: Option<String>,
|
|
|
|
pub keyformat: Option<String>,
|
|
|
|
pub keyformatversions: Option<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Key {
|
2022-06-30 07:41:58 +00:00
|
|
|
pub(crate) fn from_hashmap(
|
|
|
|
mut attrs: HashMap<String, QuotedOrUnquoted>,
|
|
|
|
) -> Result<Key, String> {
|
|
|
|
let method: KeyMethod = unquoted_string_parse!(attrs, "METHOD")
|
|
|
|
.ok_or_else(|| String::from("EXT-X-KEY without mandatory METHOD attribute"))?;
|
|
|
|
|
|
|
|
let uri = quoted_string!(attrs, "URI");
|
|
|
|
let iv = unquoted_string!(attrs, "IV");
|
|
|
|
if method == KeyMethod::None && iv.is_none() {
|
|
|
|
return Err("IV is required unless METHOD is NONE".parse().unwrap());
|
2016-06-03 18:56:45 +00:00
|
|
|
}
|
2022-06-30 07:41:58 +00:00
|
|
|
let keyformat = quoted_string!(attrs, "KEYFORMAT");
|
|
|
|
let keyformatversions = quoted_string!(attrs, "KEYFORMATVERSIONS");
|
|
|
|
|
|
|
|
Ok(Key {
|
|
|
|
method,
|
|
|
|
uri,
|
|
|
|
iv,
|
|
|
|
keyformat,
|
|
|
|
keyformatversions,
|
|
|
|
})
|
2016-06-03 18:56:45 +00:00
|
|
|
}
|
2017-02-17 13:50:50 +00:00
|
|
|
|
2020-02-27 01:17:13 +00:00
|
|
|
pub fn write_attributes_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
|
2017-02-17 13:50:50 +00:00
|
|
|
write!(w, "METHOD={}", self.method)?;
|
|
|
|
write_some_attribute_quoted!(w, ",URI", &self.uri)?;
|
|
|
|
write_some_attribute!(w, ",IV", &self.iv)?;
|
2022-06-30 07:41:58 +00:00
|
|
|
write_some_attribute_quoted!(w, ",KEYFORMAT", &self.keyformat)?;
|
|
|
|
write_some_attribute_quoted!(w, ",KEYFORMATVERSIONS", &self.keyformatversions)
|
2017-02-17 13:50:50 +00:00
|
|
|
}
|
2016-06-03 18:56:45 +00:00
|
|
|
}
|
|
|
|
|
2021-11-18 13:04:17 +00:00
|
|
|
/// [`#EXT-X-MAP:<attribute-list>`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.2.5)
|
2016-06-03 18:56:45 +00:00
|
|
|
///
|
|
|
|
/// The EXT-X-MAP tag specifies how to obtain the Media Initialization Section
|
2021-11-18 13:04:17 +00:00
|
|
|
/// [(Section 3)](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-3)
|
2016-06-03 18:56:45 +00:00
|
|
|
/// required to parse the applicable Media Segments.
|
|
|
|
/// It applies to every Media Segment that appears after it in the
|
|
|
|
/// Playlist until the next EXT-X-MAP tag or until the end of the
|
|
|
|
/// playlist.
|
2022-01-07 10:44:56 +00:00
|
|
|
#[derive(Debug, Default, PartialEq, Eq, Clone)]
|
2016-06-03 18:56:45 +00:00
|
|
|
pub struct Map {
|
|
|
|
pub uri: String,
|
2017-02-17 13:50:50 +00:00
|
|
|
pub byte_range: Option<ByteRange>,
|
2022-06-30 07:41:58 +00:00
|
|
|
pub other_attributes: HashMap<String, QuotedOrUnquoted>,
|
2016-06-03 18:56:45 +00:00
|
|
|
}
|
|
|
|
|
2017-02-17 13:50:50 +00:00
|
|
|
impl Map {
|
|
|
|
pub fn write_attributes_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
|
|
|
|
write!(w, "URI=\"{}\"", self.uri)?;
|
|
|
|
if let Some(ref byte_range) = self.byte_range {
|
|
|
|
write!(w, ",BYTERANGE=")?;
|
|
|
|
byte_range.write_value_to(w)?;
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-18 13:04:17 +00:00
|
|
|
/// [`#EXT-X-BYTERANGE:<n>[@<o>]`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.2.2)
|
2016-06-03 18:56:45 +00:00
|
|
|
///
|
|
|
|
/// The EXT-X-BYTERANGE tag indicates that a Media Segment is a sub-range
|
|
|
|
/// of the resource identified by its URI. It applies only to the next
|
|
|
|
/// URI line that follows it in the Playlist.
|
2022-01-07 10:44:56 +00:00
|
|
|
#[derive(Debug, Default, PartialEq, Eq, Clone)]
|
2016-06-03 18:56:45 +00:00
|
|
|
pub struct ByteRange {
|
2022-01-07 10:47:03 +00:00
|
|
|
pub length: u64,
|
|
|
|
pub offset: Option<u64>,
|
2016-06-03 18:56:45 +00:00
|
|
|
}
|
|
|
|
|
2017-02-17 13:50:50 +00:00
|
|
|
impl ByteRange {
|
|
|
|
pub fn write_value_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
|
|
|
|
write!(w, "{}", self.length)?;
|
|
|
|
if let Some(offset) = self.offset {
|
|
|
|
write!(w, "@{}", offset)?;
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-18 13:04:17 +00:00
|
|
|
/// [`#EXT-X-DATERANGE:<attribute-list>`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.2.7)
|
2016-06-03 18:56:45 +00:00
|
|
|
///
|
|
|
|
/// The EXT-X-DATERANGE tag associates a Date Range (i.e. a range of time
|
|
|
|
/// defined by a starting and ending date) with a set of attribute /
|
|
|
|
/// value pairs.
|
2022-07-20 08:30:34 +00:00
|
|
|
#[derive(Debug, PartialEq, Clone)]
|
2016-06-03 18:56:45 +00:00
|
|
|
pub struct DateRange {
|
|
|
|
pub id: String,
|
|
|
|
pub class: Option<String>,
|
2022-07-20 08:30:34 +00:00
|
|
|
pub start_date: chrono::DateTime<chrono::FixedOffset>,
|
|
|
|
pub end_date: Option<chrono::DateTime<chrono::FixedOffset>>,
|
2022-06-30 07:41:58 +00:00
|
|
|
pub duration: Option<f64>,
|
|
|
|
pub planned_duration: Option<f64>,
|
|
|
|
pub x_prefixed: Option<HashMap<String, QuotedOrUnquoted>>, // X-<client-attribute>
|
2016-06-03 18:56:45 +00:00
|
|
|
pub end_on_next: bool,
|
2022-06-30 07:41:58 +00:00
|
|
|
pub other_attributes: Option<HashMap<String, QuotedOrUnquoted>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl DateRange {
|
|
|
|
pub fn from_hashmap(mut attrs: HashMap<String, QuotedOrUnquoted>) -> Result<DateRange, String> {
|
|
|
|
let id = quoted_string!(attrs, "ID")
|
|
|
|
.ok_or_else(|| String::from("EXT-X-DATERANGE without mandatory ID attribute"))?;
|
|
|
|
let class = quoted_string!(attrs, "CLASS");
|
2022-07-20 08:30:34 +00:00
|
|
|
let start_date =
|
|
|
|
quoted_string_parse!(attrs, "START-DATE", chrono::DateTime::parse_from_rfc3339)
|
|
|
|
.ok_or_else(|| {
|
|
|
|
String::from("EXT-X-DATERANGE without mandatory START-DATE attribute")
|
|
|
|
})?;
|
|
|
|
let end_date =
|
|
|
|
quoted_string_parse!(attrs, "END-DATE", chrono::DateTime::parse_from_rfc3339);
|
2022-06-30 07:41:58 +00:00
|
|
|
let duration = unquoted_string_parse!(attrs, "DURATION", |s: &str| s
|
|
|
|
.parse::<f64>()
|
|
|
|
.map_err(|err| format!("Failed to parse DURATION attribute: {}", err)));
|
|
|
|
let planned_duration = unquoted_string_parse!(attrs, "PLANNED-DURATION", |s: &str| s
|
|
|
|
.parse::<f64>()
|
|
|
|
.map_err(|err| format!("Failed to parse PLANNED-DURATION attribute: {}", err)));
|
|
|
|
let end_on_next = is_yes!(attrs, "END-ON-NEXT");
|
|
|
|
let mut x_prefixed = HashMap::new();
|
|
|
|
let mut other_attributes = HashMap::new();
|
|
|
|
for (k, v) in attrs.into_iter() {
|
|
|
|
if k.starts_with("X-") {
|
|
|
|
x_prefixed.insert(k, v);
|
|
|
|
} else {
|
|
|
|
other_attributes.insert(k, v);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(DateRange {
|
|
|
|
id,
|
|
|
|
class,
|
|
|
|
start_date,
|
|
|
|
end_date,
|
|
|
|
duration,
|
|
|
|
planned_duration,
|
|
|
|
x_prefixed: if x_prefixed.is_empty() {
|
|
|
|
None
|
|
|
|
} else {
|
|
|
|
Some(x_prefixed)
|
|
|
|
},
|
|
|
|
end_on_next,
|
|
|
|
other_attributes: if other_attributes.is_empty() {
|
|
|
|
None
|
|
|
|
} else {
|
|
|
|
Some(other_attributes)
|
|
|
|
},
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn write_attributes_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
|
|
|
|
write_some_attribute_quoted!(w, "ID", &Some(&self.id))?;
|
|
|
|
write_some_attribute_quoted!(w, ",CLASS", &self.class)?;
|
2022-07-20 08:30:34 +00:00
|
|
|
write_some_attribute_quoted!(w, ",START-DATE", &Some(&self.start_date.to_rfc3339()))?;
|
|
|
|
write_some_attribute_quoted!(
|
|
|
|
w,
|
|
|
|
",END-DATE",
|
|
|
|
&self.end_date.as_ref().map(|dt| dt.to_rfc3339())
|
|
|
|
)?;
|
2022-06-30 07:41:58 +00:00
|
|
|
write_some_attribute!(w, ",DURATION", &self.duration)?;
|
|
|
|
write_some_attribute!(w, ",PLANNED-DURATION", &self.planned_duration)?;
|
|
|
|
if let Some(x_prefixed) = &self.x_prefixed {
|
|
|
|
for (name, attr) in x_prefixed {
|
|
|
|
write!(w, ",{}={}", name, attr)?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if self.end_on_next {
|
|
|
|
write!(w, ",END-ON-NEXT=YES")?;
|
|
|
|
}
|
|
|
|
if let Some(other_attributes) = &self.other_attributes {
|
|
|
|
for (name, attr) in other_attributes {
|
|
|
|
write!(w, ",{}={}", name, attr)?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
2016-06-03 18:56:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------------------------------
|
|
|
|
// Rest
|
|
|
|
// -----------------------------------------------------------------------------------------------
|
|
|
|
|
2021-11-18 13:04:17 +00:00
|
|
|
/// [`#EXT-X-START:<attribute-list>`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.5.2)
|
2016-06-03 18:56:45 +00:00
|
|
|
///
|
|
|
|
/// The EXT-X-START tag indicates a preferred point at which to start
|
|
|
|
/// playing a Playlist. By default, clients SHOULD start playback at
|
|
|
|
/// this point when beginning a playback session.
|
2022-06-30 07:41:58 +00:00
|
|
|
#[derive(Debug, Default, PartialEq, Clone)]
|
2016-06-03 18:56:45 +00:00
|
|
|
pub struct Start {
|
2022-06-30 07:41:58 +00:00
|
|
|
pub time_offset: f64,
|
|
|
|
pub precise: Option<bool>,
|
|
|
|
pub other_attributes: HashMap<String, QuotedOrUnquoted>,
|
2016-06-03 18:56:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Start {
|
2022-06-30 07:41:58 +00:00
|
|
|
pub(crate) fn from_hashmap(
|
|
|
|
mut attrs: HashMap<String, QuotedOrUnquoted>,
|
|
|
|
) -> Result<Start, String> {
|
|
|
|
let time_offset = unquoted_string_parse!(attrs, "TIME-OFFSET", |s: &str| s
|
|
|
|
.parse::<f64>()
|
|
|
|
.map_err(|err| format!("Failed to parse TIME-OFFSET attribute: {}", err)))
|
2022-07-20 08:30:34 +00:00
|
|
|
.ok_or_else(|| String::from("EXT-X-START without mandatory TIME-OFFSET attribute"))?;
|
2022-06-30 07:41:58 +00:00
|
|
|
Ok(Start {
|
|
|
|
time_offset,
|
|
|
|
precise: is_yes!(attrs, "PRECISE").into(),
|
|
|
|
other_attributes: attrs,
|
|
|
|
})
|
2016-06-03 18:56:45 +00:00
|
|
|
}
|
2017-02-17 13:50:50 +00:00
|
|
|
|
2022-06-30 07:41:58 +00:00
|
|
|
pub(crate) fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
|
2017-02-17 13:50:50 +00:00
|
|
|
write!(w, "#EXT-X-START:TIME-OFFSET={}", self.time_offset)?;
|
2022-06-30 07:41:58 +00:00
|
|
|
if let Some(precise) = self.precise {
|
|
|
|
if precise {
|
|
|
|
write!(w, ",PRECISE=YES")?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
writeln!(w)?;
|
|
|
|
|
|
|
|
Ok(())
|
2017-02-17 13:50:50 +00:00
|
|
|
}
|
2016-06-03 18:56:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// A simple `#EXT-` tag
|
2022-01-07 10:44:56 +00:00
|
|
|
#[derive(Debug, Default, PartialEq, Eq, Clone)]
|
2016-06-03 18:56:45 +00:00
|
|
|
pub struct ExtTag {
|
|
|
|
pub tag: String,
|
2021-10-12 21:06:47 +00:00
|
|
|
pub rest: Option<String>,
|
2020-02-11 20:43:06 +00:00
|
|
|
}
|
2021-03-11 20:15:22 +00:00
|
|
|
|
2021-10-14 19:21:03 +00:00
|
|
|
impl Display for ExtTag {
|
2022-06-30 07:41:58 +00:00
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
2021-10-14 19:21:03 +00:00
|
|
|
write!(f, "#EXT-{}", self.tag)?;
|
|
|
|
if let Some(v) = &self.rest {
|
|
|
|
write!(f, ":{}", v)?;
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod test {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn ext_tag_with_value_is_printable() {
|
|
|
|
let cue_out_tag = ExtTag {
|
|
|
|
tag: "X-CUE-OUT".into(),
|
|
|
|
rest: Some("DURATION=30".into()),
|
|
|
|
};
|
|
|
|
|
|
|
|
let mut output = Vec::new();
|
2021-10-14 19:35:35 +00:00
|
|
|
write!(output, "{}", cue_out_tag).unwrap();
|
2021-10-14 19:21:03 +00:00
|
|
|
|
2021-10-18 09:41:28 +00:00
|
|
|
assert_eq!(
|
|
|
|
std::str::from_utf8(output.as_slice()).unwrap(),
|
|
|
|
"#EXT-X-CUE-OUT:DURATION=30"
|
|
|
|
)
|
2021-10-14 19:21:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn ext_tag_without_value_is_printable() {
|
|
|
|
let cue_in_tag = ExtTag {
|
|
|
|
tag: "X-CUE-IN".into(),
|
|
|
|
rest: None,
|
|
|
|
};
|
|
|
|
|
|
|
|
let mut output = Vec::new();
|
2021-10-14 19:35:35 +00:00
|
|
|
write!(output, "{}", cue_in_tag).unwrap();
|
2021-10-14 19:21:03 +00:00
|
|
|
|
2021-10-18 09:41:28 +00:00
|
|
|
assert_eq!(
|
|
|
|
std::str::from_utf8(output.as_slice()).unwrap(),
|
|
|
|
"#EXT-X-CUE-IN"
|
|
|
|
)
|
2021-10-14 19:21:03 +00:00
|
|
|
}
|
|
|
|
}
|