mirror of
https://github.com/rutgersc/m3u8-rs.git
synced 2025-02-16 14:45:14 +00:00
Store VariantStream attributes in more specific types and validate them
This commit is contained in:
parent
0789098d7d
commit
6559e45b49
3 changed files with 224 additions and 32 deletions
|
@ -285,14 +285,14 @@ fn master_playlist_from_tags(mut tags: Vec<MasterPlaylistTag>) -> 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,
|
||||
|
|
235
src/playlist.rs
235
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<usize>,
|
||||
pub variants: Vec<VariantStream>,
|
||||
|
@ -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,
|
||||
|
||||
// <attribute-list>
|
||||
pub bandwidth: String,
|
||||
pub average_bandwidth: Option<String>,
|
||||
pub bandwidth: u64,
|
||||
pub average_bandwidth: Option<u64>,
|
||||
pub codecs: Option<String>,
|
||||
pub resolution: Option<String>,
|
||||
pub frame_rate: Option<String>,
|
||||
pub hdcp_level: Option<QuotedOrUnquoted>,
|
||||
pub resolution: Option<Resolution>,
|
||||
pub frame_rate: Option<f64>,
|
||||
pub hdcp_level: Option<HDCPLevel>,
|
||||
pub audio: Option<String>,
|
||||
pub video: Option<String>,
|
||||
pub subtitles: Option<String>,
|
||||
pub closed_captions: Option<QuotedOrUnquoted>,
|
||||
pub closed_captions: Option<ClosedCaptionGroupId>,
|
||||
// PROGRAM-ID tag was removed in protocol version 6
|
||||
}
|
||||
|
||||
|
@ -157,21 +158,118 @@ impl VariantStream {
|
|||
pub fn from_hashmap(
|
||||
mut attrs: HashMap<String, QuotedOrUnquoted>,
|
||||
is_i_frame: bool,
|
||||
) -> VariantStream {
|
||||
VariantStream {
|
||||
) -> Result<VariantStream, String> {
|
||||
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::<u64>()
|
||||
.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::<u64>().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::<Resolution>())
|
||||
})
|
||||
.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::<f64>()
|
||||
.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::<HDCPLevel>())
|
||||
})
|
||||
.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<T: Write>(&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<Resolution, String> {
|
||||
match s.split_once('x') {
|
||||
Some((width, height)) => {
|
||||
let width = width
|
||||
.parse::<u64>()
|
||||
.map_err(|err| format!("Can't parse resolution attribute: {}", err))?;
|
||||
let height = height
|
||||
.parse::<u64>()
|
||||
.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<HDCPLevel, String> {
|
||||
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<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),
|
||||
QuotedOrUnquoted::Quoted(s) => Ok(ClosedCaptionGroupId::GroupId(s)),
|
||||
_ => Err(format!(
|
||||
"Unable to create ClosedCaptionGroupId from {:?}",
|
||||
s
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// [`#EXT-X-MEDIA:<attribute-list>`](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
|
||||
|
|
15
tests/lib.rs
15
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(),
|
||||
|
|
Loading…
Reference in a new issue