diff --git a/src/parser.rs b/src/parser.rs index 03633d0..f071c4d 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -285,14 +285,14 @@ fn master_playlist_from_tags(mut tags: Vec) -> MasterPlaylist } fn variant_stream_tag(i: &[u8]) -> IResult<&[u8], VariantStream> { - map( + map_res( pair(tag("#EXT-X-STREAM-INF:"), key_value_pairs), |(_, attributes)| VariantStream::from_hashmap(attributes, false), )(i) } fn variant_i_frame_stream_tag(i: &[u8]) -> IResult<&[u8], VariantStream> { - map( + map_res( pair(tag("#EXT-X-I-FRAME-STREAM-INF:"), key_value_pairs), |(_, attributes)| VariantStream::from_hashmap(attributes, true), )(i) @@ -780,7 +780,7 @@ mod tests { VariantStream { is_i_frame: false, uri: "".into(), - bandwidth: "300000".into(), + bandwidth: 300000, average_bandwidth: None, codecs: Some("xxx".into()), resolution: None, diff --git a/src/playlist.rs b/src/playlist.rs index 42ec69b..50e9fc5 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -5,6 +5,7 @@ use crate::QuotedOrUnquoted; use std::collections::HashMap; +use std::convert::{TryFrom, TryInto}; use std::f32; use std::fmt; use std::fmt::Display; @@ -69,7 +70,7 @@ impl Playlist { /// A [Master Playlist](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4) /// provides a set of Variant Streams, each of which /// describes a different version of the same content. -#[derive(Debug, Default, PartialEq, Eq, Clone)] +#[derive(Debug, Default, PartialEq, Clone)] pub struct MasterPlaylist { pub version: Option, pub variants: Vec, @@ -134,22 +135,22 @@ impl MasterPlaylist { /// Clients should switch between different Variant Streams to adapt to /// network conditions. Clients should choose Renditions based on user /// preferences. -#[derive(Debug, Default, PartialEq, Eq, Clone)] +#[derive(Debug, Default, PartialEq, Clone)] pub struct VariantStream { pub is_i_frame: bool, pub uri: String, // - pub bandwidth: String, - pub average_bandwidth: Option, + pub bandwidth: u64, + pub average_bandwidth: Option, pub codecs: Option, - pub resolution: Option, - pub frame_rate: Option, - pub hdcp_level: Option, + pub resolution: Option, + pub frame_rate: Option, + pub hdcp_level: Option, pub audio: Option, pub video: Option, pub subtitles: Option, - pub closed_captions: Option, + pub closed_captions: Option, // PROGRAM-ID tag was removed in protocol version 6 } @@ -157,21 +158,118 @@ impl VariantStream { pub fn from_hashmap( mut attrs: HashMap, is_i_frame: bool, - ) -> VariantStream { - VariantStream { + ) -> Result { + let uri = attrs + .remove("URI") + .map(|c| { + c.as_quoted() + .ok_or_else(|| format!("URI attribute is an unquoted string")) + .map(|s| s.to_string()) + }) + .transpose()? + .unwrap_or_default(); + let bandwidth = attrs + .remove("BANDWIDTH") + .ok_or_else(|| String::from("Mandatory bandwidth attribute not included")) + .and_then(|s| { + s.as_unquoted() + .ok_or_else(|| String::from("Bandwidth attribute is a quoted string")) + .and_then(|s| { + s.trim() + .parse::() + .map_err(|err| format!("Failed to parse bandwidth attribute: {}", err)) + }) + })?; + let average_bandwidth = attrs + .remove("AVERAGE-BANDWIDTH") + .map(|s| { + s.as_unquoted() + .ok_or_else(|| String::from("Average bandwidth attribute is a quoted string")) + .and_then(|s| { + s.trim().parse::().map_err(|err| { + format!("Failed to parse average bandwidth attribute: {}", err) + }) + }) + }) + .transpose()?; + let codecs = attrs + .remove("CODECS") + .map(|c| { + c.as_quoted() + .ok_or_else(|| format!("Codecs attribute is an unquoted string")) + .map(|s| s.to_string()) + }) + .transpose()?; + let resolution = attrs + .remove("RESOLUTION") + .map(|r| { + r.as_unquoted() + .ok_or_else(|| format!("Resolution attribute is a quoted string")) + .and_then(|s| s.parse::()) + }) + .transpose()?; + let frame_rate = attrs + .remove("FRAME-RATE") + .map(|f| { + f.as_unquoted() + .ok_or_else(|| format!("Framerate attribute is a quoted string")) + .and_then(|s| { + s.parse::() + .map_err(|err| format!("Failed to parse framerate: {}", err)) + }) + }) + .transpose()?; + let hdcp_level = attrs + .remove("HDCP-LEVEL") + .map(|r| { + r.as_unquoted() + .ok_or_else(|| format!("HDCP level attribute is a quoted string")) + .and_then(|s| s.parse::()) + }) + .transpose()?; + let audio = attrs + .remove("AUDIO") + .map(|c| { + c.as_quoted() + .ok_or_else(|| format!("Audio attribute is an unquoted string")) + .map(|s| s.to_string()) + }) + .transpose()?; + let video = attrs + .remove("VIDEO") + .map(|c| { + c.as_quoted() + .ok_or_else(|| format!("Video attribute is an unquoted string")) + .map(|s| s.to_string()) + }) + .transpose()?; + let subtitles = attrs + .remove("SUBTITLES") + .map(|c| { + c.as_quoted() + .ok_or_else(|| format!("Subtitles attribute is an unquoted string")) + .map(|s| s.to_string()) + }) + .transpose()?; + let closed_captions = attrs + .remove("CLOSED-CAPTIONS") + .map(|c| c.try_into()) + .transpose()?; + + Ok(VariantStream { is_i_frame, - uri: attrs.remove("URI").unwrap_or_default().to_string(), - bandwidth: attrs.remove("BANDWIDTH").unwrap_or_default().to_string(), - average_bandwidth: attrs.remove("AVERAGE-BANDWIDTH").map(|a| a.to_string()), - codecs: attrs.remove("CODECS").map(|c| c.to_string()), - resolution: attrs.remove("RESOLUTION").map(|r| r.to_string()), - frame_rate: attrs.remove("FRAME-RATE").map(|f| f.to_string()), - hdcp_level: attrs.remove("HDCP-LEVEL"), - audio: attrs.remove("AUDIO").map(|a| a.to_string()), - video: attrs.remove("VIDEO").map(|v| v.to_string()), - subtitles: attrs.remove("SUBTITLES").map(|s| s.to_string()), - closed_captions: attrs.remove("CLOSED-CAPTIONS"), - } + uri, + bandwidth, + average_bandwidth, + codecs, + resolution, + frame_rate, + hdcp_level, + audio, + video, + subtitles, + closed_captions, + }) } pub fn write_to(&self, w: &mut T) -> std::io::Result<()> { @@ -184,7 +282,12 @@ impl VariantStream { self.write_stream_inf_common_attributes(w)?; write_some_attribute_quoted!(w, ",AUDIO", &self.audio)?; write_some_attribute_quoted!(w, ",SUBTITLES", &self.subtitles)?; - write_some_attribute!(w, ",CLOSED-CAPTIONS", &self.closed_captions)?; + 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)?, + } + } writeln!(w)?; writeln!(w, "{}", self.uri) } @@ -201,6 +304,92 @@ impl VariantStream { } } +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)] +pub struct Resolution { + pub width: u64, + pub height: u64, +} + +impl fmt::Display for Resolution { + 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 { + match s.split_once('x') { + Some((width, height)) => { + let width = width + .parse::() + .map_err(|err| format!("Can't parse resolution attribute: {}", err))?; + let height = height + .parse::() + .map_err(|err| format!("Can't parse resolution attribute: {}", err))?; + Ok(Resolution { width, height }) + } + None => Err(String::from("Invalid resolution attribute")), + } + } +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)] +pub enum HDCPLevel { + Type0, + Type1, + None, +} + +impl FromStr for HDCPLevel { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "TYPE-0" => Ok(HDCPLevel::Type0), + "TYPE-1" => Ok(HDCPLevel::Type1), + "NONE" => Ok(HDCPLevel::None), + _ => Err(format!("Unable to create HDCPLevel from {:?}", s)), + } + } +} + +impl fmt::Display for HDCPLevel { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}", + match *self { + HDCPLevel::Type0 => "TYPE-0", + HDCPLevel::Type1 => "TYPE-1", + HDCPLevel::None => "NONE", + } + ) + } +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)] +pub enum ClosedCaptionGroupId { + None, + GroupId(String), +} + +impl TryFrom for ClosedCaptionGroupId { + type Error = String; + + fn try_from(s: QuotedOrUnquoted) -> Result { + match s { + QuotedOrUnquoted::Unquoted(s) if s == "NONE" => Ok(ClosedCaptionGroupId::None), + QuotedOrUnquoted::Quoted(s) => Ok(ClosedCaptionGroupId::GroupId(s)), + _ => Err(format!( + "Unable to create ClosedCaptionGroupId from {:?}", + s + )), + } + } +} + /// [`#EXT-X-MEDIA:`](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.1) /// /// The EXT-X-MEDIA tag is used to relate Media Playlists that contain diff --git a/tests/lib.rs b/tests/lib.rs index 8838bfa..e858fed 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -217,16 +217,19 @@ fn create_and_parse_master_playlist_full() { variants: vec![VariantStream { is_i_frame: false, uri: "masterplaylist-uri".into(), - bandwidth: "10010010".into(), - average_bandwidth: Some("10010010".into()), + bandwidth: 10010010, + average_bandwidth: Some(10010010), codecs: Some("TheCODEC".into()), - resolution: Some("1000x3000".into()), - frame_rate: Some("60".into()), - hdcp_level: Some("NONE".into()), + resolution: Some(Resolution { + width: 1000, + height: 3000, + }), + frame_rate: Some(60.0), + hdcp_level: Some(HDCPLevel::None), audio: Some("audio".into()), video: Some("video".into()), subtitles: Some("subtitles".into()), - closed_captions: Some("closed_captions".into()), + closed_captions: Some(ClosedCaptionGroupId::GroupId("closed_captions".into())), }], session_data: vec![SessionData { data_id: "****".into(),