mirror of
https://github.com/rutgersc/m3u8-rs.git
synced 2024-12-22 14:36:28 +00:00
Added feature: writing playlists back to file
This commit is contained in:
parent
6286cddc04
commit
c336b89981
6 changed files with 515 additions and 242 deletions
|
@ -7,7 +7,7 @@ To use this library, add the following dependency to `Cargo.toml`:
|
|||
|
||||
```toml
|
||||
[dependencies]
|
||||
m3u8-rs = "1.0.0"
|
||||
m3u8-rs = "1.0.2"
|
||||
```
|
||||
|
||||
And add the crate to `lib.rs`
|
||||
|
|
|
@ -12,8 +12,8 @@ fn main() {
|
|||
let parsed = m3u8_rs::parse_playlist_res(&bytes);
|
||||
|
||||
match parsed {
|
||||
Ok(Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{}", pl),
|
||||
Ok(Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{}", pl),
|
||||
Ok(Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{:?}", pl),
|
||||
Ok(Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{:?}", pl),
|
||||
Err(e) => println!("Error: {:?}", e)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,8 +19,8 @@ fn main() {
|
|||
};
|
||||
|
||||
match playlist {
|
||||
Playlist::MasterPlaylist(pl) => println!("Master playlist:\n{}", pl),
|
||||
Playlist::MediaPlaylist(pl) => println!("Media playlist:\n{}", pl),
|
||||
Playlist::MasterPlaylist(pl) => println!("Master playlist:\n{:?}", pl),
|
||||
Playlist::MediaPlaylist(pl) => println!("Media playlist:\n{:?}", pl),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,8 +32,8 @@ fn main_alt() {
|
|||
let parsed = m3u8_rs::parse_playlist(&bytes);
|
||||
|
||||
match parsed {
|
||||
IResult::Done(i, Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{}", pl),
|
||||
IResult::Done(i, Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{}", pl),
|
||||
IResult::Done(i, Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{:?}", pl),
|
||||
IResult::Done(i, Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{:?}", pl),
|
||||
IResult::Error(e) => panic!("Parsing error: \n{}", e),
|
||||
IResult::Incomplete(e) => panic!("Parsing error: \n{:?}", e),
|
||||
}
|
||||
|
|
63
src/lib.rs
63
src/lib.rs
|
@ -6,8 +6,8 @@
|
|||
//! Parsing a playlist and let the parser figure out if it's a media or master playlist.
|
||||
//!
|
||||
//! ```
|
||||
//! extern crate m3u8_rs;
|
||||
//! extern crate nom;
|
||||
//! extern crate m3u8_rs;
|
||||
//! use m3u8_rs::playlist::Playlist;
|
||||
//! use nom::IResult;
|
||||
//! use std::io::Read;
|
||||
|
@ -18,15 +18,15 @@
|
|||
//!
|
||||
//! // Option 1: fn parse_playlist_res(input) -> Result<Playlist, _>
|
||||
//! match m3u8_rs::parse_playlist_res(&bytes) {
|
||||
//! Ok(Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{}", pl),
|
||||
//! Ok(Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{}", pl),
|
||||
//! Ok(Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{:?}", pl),
|
||||
//! Ok(Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{:?}", pl),
|
||||
//! Err(e) => println!("Error: {:?}", e)
|
||||
//! }
|
||||
//!
|
||||
//! // Option 2: fn parse_playlist(input) -> IResult<_, Playlist, _>
|
||||
//! match m3u8_rs::parse_playlist(&bytes) {
|
||||
//! IResult::Done(i, Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{}", pl),
|
||||
//! IResult::Done(i, Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{}", pl),
|
||||
//! IResult::Done(i, Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{:?}", pl),
|
||||
//! IResult::Done(i, Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{:?}", pl),
|
||||
//! IResult::Error(e) => panic!("Parsing error: \n{}", e),
|
||||
//! IResult::Incomplete(e) => panic!("Parsing error: \n{:?}", e),
|
||||
//! }
|
||||
|
@ -35,8 +35,8 @@
|
|||
//! Parsing a master playlist directly
|
||||
//!
|
||||
//! ```
|
||||
//! extern crate m3u8_rs;
|
||||
//! extern crate nom;
|
||||
//! extern crate m3u8_rs;
|
||||
//! use std::io::Read;
|
||||
//! use nom::IResult;
|
||||
//!
|
||||
|
@ -85,8 +85,8 @@ use playlist::*;
|
|||
/// };
|
||||
///
|
||||
/// match playlist {
|
||||
/// Playlist::MasterPlaylist(pl) => println!("Master playlist:\n{}", pl),
|
||||
/// Playlist::MediaPlaylist(pl) => println!("Media playlist:\n{}", pl),
|
||||
/// Playlist::MasterPlaylist(pl) => println!("Master playlist:\n{:?}", pl),
|
||||
/// Playlist::MediaPlaylist(pl) => println!("Media playlist:\n{:?}", pl),
|
||||
/// }
|
||||
pub fn parse_playlist(input: &[u8]) -> IResult<&[u8], Playlist> {
|
||||
match is_master_playlist(input) {
|
||||
|
@ -110,8 +110,8 @@ pub fn parse_playlist(input: &[u8]) -> IResult<&[u8], Playlist> {
|
|||
/// let parsed = m3u8_rs::parse_playlist_res(&bytes);
|
||||
///
|
||||
/// match parsed {
|
||||
/// Ok(Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{}", pl),
|
||||
/// Ok(Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{}", pl),
|
||||
/// Ok(Playlist::MasterPlaylist(pl)) => println!("Master playlist:\n{:?}", pl),
|
||||
/// Ok(Playlist::MediaPlaylist(pl)) => println!("Media playlist:\n{:?}", pl),
|
||||
/// Err(e) => println!("Error: {:?}", e)
|
||||
/// }
|
||||
/// ```
|
||||
|
@ -128,11 +128,29 @@ pub fn parse_master_playlist(input: &[u8]) -> IResult<&[u8], MasterPlaylist> {
|
|||
parse_master_playlist_tags(input).map(MasterPlaylist::from_tags)
|
||||
}
|
||||
|
||||
/// Parse input as a master playlist
|
||||
pub fn parse_master_playlist_res(input: &[u8]) -> Result<MasterPlaylist, IResult<&[u8], MasterPlaylist>> {
|
||||
let parse_result = parse_master_playlist(input);
|
||||
match parse_result {
|
||||
IResult::Done(_, playlist) => Ok(playlist),
|
||||
_ => Err(parse_result),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse input as a media playlist
|
||||
pub fn parse_media_playlist(input: &[u8]) -> IResult<&[u8], MediaPlaylist> {
|
||||
parse_media_playlist_tags(input).map(MediaPlaylist::from_tags)
|
||||
}
|
||||
|
||||
/// Parse input as a media playlist
|
||||
pub fn parse_media_playlist_res(input: &[u8]) -> Result<MediaPlaylist, IResult<&[u8], MediaPlaylist>> {
|
||||
let parse_result = parse_media_playlist(input);
|
||||
match parse_result {
|
||||
IResult::Done(_, playlist) => Ok(playlist),
|
||||
_ => Err(parse_result),
|
||||
}
|
||||
}
|
||||
|
||||
/// When a media tag or no master tag is found, this returns false.
|
||||
pub fn is_master_playlist(input: &[u8]) -> bool {
|
||||
// Assume it's not a master playlist
|
||||
|
@ -302,19 +320,21 @@ pub fn media_playlist_tag(input: &[u8]) -> IResult<&[u8], MediaPlaylistTag> {
|
|||
| map!(chain!(tag!("#EXT-X-TARGETDURATION:") ~ n:float,||n), MediaPlaylistTag::TargetDuration)
|
||||
| map!(chain!(tag!("#EXT-X-MEDIA-SEQUENCE:") ~ n:number,||n), MediaPlaylistTag::MediaSequence)
|
||||
| map!(chain!(tag!("#EXT-X-DISCONTINUITY-SEQUENCE:") ~ n:number,||n), MediaPlaylistTag::DiscontinuitySequence)
|
||||
| map!(playlist_type_tag, MediaPlaylistTag::PlaylistType)
|
||||
| map!(chain!(tag!("#EXT-X-PLAYLIST-TYPE:") ~ t:playlist_type, ||t), MediaPlaylistTag::PlaylistType)
|
||||
| map!(tag!("#EXT-X-I-FRAMES-ONLY"), |_| MediaPlaylistTag::IFramesOnly)
|
||||
| map!(start_tag, MediaPlaylistTag::Start)
|
||||
| map!(tag!("#EXT-X-INDEPENDENT-SEGMENTS"), |_| MediaPlaylistTag::IndependentSegments)
|
||||
| map!(tag!("#EXT-X-ENDLIST"), |_| MediaPlaylistTag::EndList)
|
||||
|
||||
| map!(media_segment_tag, MediaPlaylistTag::Segment)
|
||||
)
|
||||
}
|
||||
|
||||
named!(pub playlist_type_tag<MediaPlaylistType>,
|
||||
named!(pub playlist_type<MediaPlaylistType>,
|
||||
map_res!(
|
||||
map_res!(tag!("#EXT-X-PLAYLIST-TYPE:"), str::from_utf8),
|
||||
MediaPlaylistType::from_str)
|
||||
map_res!(take_until_either_and_consume!("\r\n"), str::from_utf8),
|
||||
MediaPlaylistType::from_str
|
||||
)
|
||||
);
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
@ -339,7 +359,7 @@ pub enum SegmentTag {
|
|||
pub fn media_segment_tag(input: &[u8]) -> IResult<&[u8], SegmentTag> {
|
||||
alt!(input,
|
||||
map!(chain!(tag!("#EXTINF:") ~ e:duration_title_tag,||e), |(a,b)| SegmentTag::Extinf(a,b))
|
||||
| map!(chain!(tag!("#EXT-X-BYTERANGE:") ~ r:byterange_val, || r), SegmentTag::ByteRange)
|
||||
| map!(chain!(tag!("#EXT-X-BYTERANGE:") ~ r:byte_range_val, || r), SegmentTag::ByteRange)
|
||||
| map!(tag!("#EXT-X-DISCONTINUITY"), |_| SegmentTag::Discontinuity)
|
||||
| map!(chain!(tag!("#EXT-X-KEY:") ~ k:key, || k), SegmentTag::Key)
|
||||
| map!(chain!(tag!("#EXT-X-MAP:") ~ m:map, || m), SegmentTag::Map)
|
||||
|
@ -357,7 +377,7 @@ named!(pub duration_title_tag<(f32, Option<String>)>,
|
|||
chain!(
|
||||
duration: float
|
||||
~ tag!(",")?
|
||||
~ title: opt!(map_res!(take_until_and_consume!("\r\n"), from_utf8_slice))
|
||||
~ title: opt!(map_res!(take_until_either_and_consume!("\r\n,"), from_utf8_slice))
|
||||
~ tag!(",")?
|
||||
,
|
||||
|| (duration, title)
|
||||
|
@ -366,12 +386,7 @@ named!(pub duration_title_tag<(f32, Option<String>)>,
|
|||
|
||||
named!(pub key<Key>, map!(key_value_pairs, Key::from_hashmap));
|
||||
|
||||
named!(pub map<Map>,
|
||||
chain!(
|
||||
uri: quoted ~ range: opt!(chain!(char!(',') ~ b:byterange_val,||b )),
|
||||
|| Map { uri: uri, byterange: range }
|
||||
)
|
||||
);
|
||||
named!(pub map<Map>, map!(key_value_pairs, Map::from_hashmap));
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// Basic tags
|
||||
|
@ -450,7 +465,7 @@ named!(pub number<i32>,
|
|||
map_res!(map_res!(digit, str::from_utf8), str::FromStr::from_str)
|
||||
);
|
||||
|
||||
named!(pub byterange_val<ByteRange>,
|
||||
named!(pub byte_range_val<ByteRange>,
|
||||
chain!(
|
||||
n: number
|
||||
~ o: opt!(chain!(char!('@') ~ n:number,||n))
|
||||
|
@ -462,7 +477,7 @@ named!(pub byterange_val<ByteRange>,
|
|||
named!(pub float<f32>,
|
||||
chain!(
|
||||
left: map_res!(digit, str::from_utf8)
|
||||
~ right_opt: opt!(chain!(char!('.') ~ d:map_res!(digit, str::from_utf8),|| d )),
|
||||
~ right_opt: opt!(chain!(char!('.') ~ d:map_res!(digit, str::from_utf8),|| d)),
|
||||
||
|
||||
match right_opt {
|
||||
Some(right) => {
|
||||
|
|
511
src/playlist.rs
511
src/playlist.rs
|
@ -3,12 +3,34 @@
|
|||
//! The main type here is the `Playlist` enum.
|
||||
//! Which is either a `MasterPlaylist` or a `MediaPlaylist`.
|
||||
|
||||
use std::io::Write;
|
||||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use std::fmt;
|
||||
use super::*;
|
||||
use std::f32;
|
||||
|
||||
macro_rules! write_some_attribute_quoted {
|
||||
($w:expr, $tag:expr, $o:expr) => (
|
||||
if let &Some(ref v) = $o { write!($w, "{}=\"{}\"", $tag, v) } else { Ok(()) }
|
||||
);
|
||||
}
|
||||
|
||||
macro_rules! write_some_attribute {
|
||||
($w:expr, $tag:expr, $o:expr) => (
|
||||
if let &Some(ref v) = $o { write!($w, "{}={}", $tag, v) } else { Ok(()) }
|
||||
);
|
||||
}
|
||||
|
||||
macro_rules! bool_default_false {
|
||||
($optional:expr) => (
|
||||
match $optional {
|
||||
Some(ref s) if s == "YES" => true,
|
||||
Some(_) | None => false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/// [Playlist](https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.1),
|
||||
/// can either be a `MasterPlaylist` or a `MediaPlaylist`.
|
||||
///
|
||||
|
@ -16,12 +38,21 @@ use std::f32;
|
|||
/// 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.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Playlist {
|
||||
MasterPlaylist(MasterPlaylist),
|
||||
MediaPlaylist(MediaPlaylist),
|
||||
}
|
||||
|
||||
impl Playlist {
|
||||
pub fn write_to<T: Write>(&self, writer: &mut T) -> std::io::Result<()> {
|
||||
match self {
|
||||
&Playlist::MasterPlaylist(ref pl) => pl.write_to(writer),
|
||||
&Playlist::MediaPlaylist(ref pl) => pl.write_to(writer),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// Master Playlist
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
@ -30,7 +61,7 @@ pub enum 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)]
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct MasterPlaylist {
|
||||
pub version: usize,
|
||||
pub variants: Vec<VariantStream>,
|
||||
|
@ -41,13 +72,13 @@ pub struct MasterPlaylist {
|
|||
}
|
||||
|
||||
impl MasterPlaylist {
|
||||
|
||||
pub fn from_tags(mut tags: Vec<MasterPlaylistTag>) -> MasterPlaylist {
|
||||
let mut master_playlist = MasterPlaylist::default();
|
||||
let mut alternatives = vec![];
|
||||
|
||||
// println!("Creating master playlist from:", );
|
||||
while let Some(tag) = tags.pop() {
|
||||
// println!(" {:?}", tag );
|
||||
|
||||
match tag {
|
||||
MasterPlaylistTag::Version(v) => {
|
||||
master_playlist.version = v;
|
||||
|
@ -90,9 +121,33 @@ impl MasterPlaylist {
|
|||
fn get_newest_variant(&mut self) -> Option<&mut VariantStream> {
|
||||
self.variants.iter_mut().rev().find(|v| !v.is_i_frame)
|
||||
}
|
||||
|
||||
pub fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
|
||||
writeln!(w, "{}" ,"#EXTM3U")?;
|
||||
writeln!(w, "#EXT-X-VERSION:{}", self.version)?;
|
||||
|
||||
for variant in &self.variants {
|
||||
variant.write_to(w)?;
|
||||
}
|
||||
if let Some(ref session_data) = self.session_data {
|
||||
session_data.write_to(w)?;
|
||||
}
|
||||
if let Some(ref session_key) = self.session_key {
|
||||
session_key.write_to(w)?;
|
||||
}
|
||||
if let Some(ref start) = self.start {
|
||||
start.write_to(w)?;
|
||||
}
|
||||
if self.independent_segments {
|
||||
writeln!(w, "#EXT-X-INDEPENDENT-SEGMENTS")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// [`#EXT-X-STREAM-INF:<attribute-list>`]
|
||||
/// [`#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)
|
||||
|
@ -108,7 +163,7 @@ 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)]
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct VariantStream {
|
||||
pub is_i_frame: bool,
|
||||
pub uri: String,
|
||||
|
@ -128,6 +183,7 @@ pub struct VariantStream {
|
|||
}
|
||||
|
||||
impl VariantStream {
|
||||
|
||||
pub fn from_hashmap(mut attrs: HashMap<String, String>, is_i_frame: bool) -> VariantStream {
|
||||
VariantStream {
|
||||
is_i_frame: is_i_frame,
|
||||
|
@ -144,6 +200,37 @@ impl VariantStream {
|
|||
alternatives: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
|
||||
|
||||
for alternative in &self.alternatives {
|
||||
alternative.write_to(w)?;
|
||||
}
|
||||
|
||||
if self.is_i_frame {
|
||||
write!(w, "#EXT-X-I-FRAME-STREAM-INF:")?;
|
||||
self.write_stream_inf_common_attributes(w)?;
|
||||
writeln!(w, "URI=\"{}\"", self.uri)
|
||||
}
|
||||
else {
|
||||
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)?;
|
||||
write_some_attribute_quoted!(w, ",CLOSED-CAPTIONS", &self.closed_captions)?;
|
||||
write!(w, "\n")?;
|
||||
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)?;
|
||||
write!(w, ",CODECS=\"{}\"", &self.codecs)?;
|
||||
write_some_attribute!(w, ",RESOLUTION", &self.resolution)?;
|
||||
write_some_attribute!(w, ",FRAME-RATE", &self.frame_rate)?;
|
||||
write_some_attribute_quoted!(w, ",VIDEO", &self.video)
|
||||
}
|
||||
}
|
||||
|
||||
/// [`#EXT-X-MEDIA:<attribute-list>`]
|
||||
|
@ -155,7 +242,7 @@ impl VariantStream {
|
|||
/// 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.
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct AlternativeMedia {
|
||||
// <attribute-list>
|
||||
pub media_type: AlternativeMediaType,
|
||||
|
@ -172,6 +259,7 @@ pub struct AlternativeMedia {
|
|||
}
|
||||
|
||||
impl AlternativeMedia {
|
||||
|
||||
pub fn from_hashmap(mut attrs: HashMap<String, String>) -> AlternativeMedia {
|
||||
AlternativeMedia {
|
||||
media_type: attrs.get("TYPE")
|
||||
|
@ -182,16 +270,32 @@ impl AlternativeMedia {
|
|||
language: attrs.remove("LANGUAGE"),
|
||||
assoc_language: attrs.remove("ASSOC-LANGUAGE"),
|
||||
name: attrs.remove("NAME").unwrap_or(String::new()),
|
||||
default: bool_default_false(attrs.remove("DEFAULT")),
|
||||
autoselect: bool_default_false(attrs.remove("ASSOC-LANGUAGE")),
|
||||
forced: bool_default_false(attrs.remove("ASSOC-LANGUAGE")),
|
||||
default: bool_default_false!(attrs.remove("DEFAULT")),
|
||||
autoselect: bool_default_false!(attrs.remove("AUTOSELECT")),
|
||||
forced: bool_default_false!(attrs.remove("FORCED")),
|
||||
instream_id: attrs.remove("INSTREAM-ID"),
|
||||
characteristics: attrs.remove("CHARACTERISTICS"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
|
||||
write!(w, "#EXT-X-MEDIA:")?;
|
||||
write!(w, "TYPE={}", self.media_type)?;
|
||||
write_some_attribute_quoted!(w, ",URI", &self.uri)?;
|
||||
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)?;
|
||||
if self.default { write!(w, ",DEFAULT=YES")?; }
|
||||
if self.autoselect { write!(w, ",AUTOSELECT=YES")?; }
|
||||
if self.forced { write!(w, ",FORCED=YES")?; }
|
||||
write_some_attribute_quoted!(w, ",INSTREAM-ID", &self.instream_id)?;
|
||||
write_some_attribute_quoted!(w, ",CHARACTERISTICS", &self.characteristics)?;
|
||||
write!(w, "\n")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum AlternativeMediaType {
|
||||
Audio,
|
||||
Video,
|
||||
|
@ -219,17 +323,37 @@ impl Default for AlternativeMediaType {
|
|||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for AlternativeMediaType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", match self {
|
||||
&AlternativeMediaType::Audio => "AUDIO",
|
||||
&AlternativeMediaType::Video => "VIDEO",
|
||||
&AlternativeMediaType::Subtitles => "SUBTITLES",
|
||||
&AlternativeMediaType::ClosedCaptions => "CLOSEDCAPTIONS",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// [`#EXT-X-SESSION-KEY:<attribute-list>`]
|
||||
/// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.5)
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct SessionKey(pub Key);
|
||||
|
||||
impl SessionKey {
|
||||
pub fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
|
||||
write!(w, "#EXT-X-SESSION-KEY:")?;
|
||||
self.0.write_attributes_to(w)?;
|
||||
write!(w, "\n")
|
||||
}
|
||||
}
|
||||
|
||||
/// [`#EXT-X-SESSION-DATA:<attribute-list>`]
|
||||
/// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.4.4)
|
||||
/// 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.
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct SessionData {
|
||||
pub data_id: String,
|
||||
pub value: String,
|
||||
|
@ -243,9 +367,18 @@ impl SessionData {
|
|||
data_id: attrs.remove("DATA-ID").unwrap_or_else(String::new),
|
||||
value: attrs.remove("VALUE").unwrap_or_else(String::new),
|
||||
uri: attrs.remove("URI").unwrap_or_else(String::new),
|
||||
language: attrs.remove("SUBTITLES"),
|
||||
language: attrs.remove("LANGUAGE"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
|
||||
write!(w, "#EXT-X-SESSION-DATA:")?;
|
||||
write!(w, "DATA-ID=\"{}\"", self.data_id)?;
|
||||
write!(w, ",VALUE=\"{}\"", self.value)?;
|
||||
write!(w, ",URI=\"{}\"", self.uri)?;
|
||||
write_some_attribute_quoted!(w, ",LANGUAGE", &self.language)?;
|
||||
write!(w, "\n")
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
@ -256,7 +389,7 @@ impl SessionData {
|
|||
/// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.3)
|
||||
/// contains a list of Media Segments, which when played
|
||||
/// sequentially will play the multimedia presentation.
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct MediaPlaylist {
|
||||
pub version: usize,
|
||||
/// `#EXT-X-TARGETDURATION:<s>`
|
||||
|
@ -269,7 +402,7 @@ pub struct MediaPlaylist {
|
|||
/// `#EXT-X-ENDLIST`
|
||||
pub end_list: bool,
|
||||
/// `#EXT-X-PLAYLIST-TYPE`
|
||||
pub playlist_type: MediaPlaylistType,
|
||||
pub playlist_type: Option<MediaPlaylistType>,
|
||||
/// `#EXT-X-I-FRAMES-ONLY`
|
||||
pub i_frames_only: bool,
|
||||
/// `#EXT-X-START`
|
||||
|
@ -279,13 +412,15 @@ pub struct MediaPlaylist {
|
|||
}
|
||||
|
||||
impl MediaPlaylist {
|
||||
|
||||
pub fn from_tags(mut tags: Vec<MediaPlaylistTag>) -> MediaPlaylist {
|
||||
let mut media_playlist = MediaPlaylist::default();
|
||||
let mut next_segment = MediaSegment::new();
|
||||
let mut next_segment = MediaSegment::empty();
|
||||
let mut encryption_key = None;
|
||||
let mut map = None;
|
||||
|
||||
while let Some(tag) = tags.pop() {
|
||||
println!("Tag: {:?}\n", &tag);
|
||||
match tag {
|
||||
MediaPlaylistTag::Version(v) => {
|
||||
media_playlist.version = v;
|
||||
|
@ -303,7 +438,7 @@ impl MediaPlaylist {
|
|||
media_playlist.end_list = true;
|
||||
}
|
||||
MediaPlaylistTag::PlaylistType(t) => {
|
||||
media_playlist.playlist_type = t;
|
||||
media_playlist.playlist_type = Some(t);
|
||||
}
|
||||
MediaPlaylistTag::IFramesOnly => {
|
||||
media_playlist.i_frames_only = true;
|
||||
|
@ -343,7 +478,7 @@ impl MediaPlaylist {
|
|||
next_segment.map = map.clone();
|
||||
next_segment.uri = u;
|
||||
media_playlist.segments.push(next_segment);
|
||||
next_segment = MediaSegment::new();
|
||||
next_segment = MediaSegment::empty();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
@ -353,11 +488,44 @@ impl MediaPlaylist {
|
|||
}
|
||||
media_playlist
|
||||
}
|
||||
|
||||
pub fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
|
||||
writeln!(w, "{}" ,"#EXTM3U")?;
|
||||
writeln!(w, "#EXT-X-VERSION:{}", self.version)?;
|
||||
writeln!(w, "#EXT-X-TARGETDURATION:{}", self.target_duration)?;
|
||||
|
||||
if self.media_sequence != 0 {
|
||||
writeln!(w, "#EXT-X-MEDIA-SEQUENCE:{}", self.media_sequence)?;
|
||||
}
|
||||
if self.discontinuity_sequence != 0 {
|
||||
writeln!(w, "#EXT-X-DISCONTINUITY-SEQUENCE:{}", self.discontinuity_sequence)?;
|
||||
}
|
||||
if self.end_list {
|
||||
writeln!(w, "#EXT-X-ENDLIST")?;
|
||||
}
|
||||
if let Some(ref v) = self.playlist_type {
|
||||
writeln!(w, "#EXT-X-PLAYLIST-TYPE:{}", v)?;
|
||||
}
|
||||
if self.i_frames_only {
|
||||
writeln!(w, "#EXT-X-I-FRAMES-ONLY")?;
|
||||
}
|
||||
if let Some(ref start) = self.start {
|
||||
start.write_to(w)?;
|
||||
}
|
||||
if self.independent_segments {
|
||||
writeln!(w, "#EXT-X-INDEPENDENT-SEGMENTS")?;
|
||||
}
|
||||
for segment in &self.segments {
|
||||
segment.write_to(w)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// [`#EXT-X-PLAYLIST-TYPE:<EVENT|VOD>`]
|
||||
/// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.3.5)
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum MediaPlaylistType {
|
||||
Event,
|
||||
Vod,
|
||||
|
@ -375,6 +543,15 @@ impl FromStr for MediaPlaylistType {
|
|||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for MediaPlaylistType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{}", match self {
|
||||
&MediaPlaylistType::Event => "EVENT",
|
||||
&MediaPlaylistType::Vod => "VOD",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MediaPlaylistType {
|
||||
fn default() -> MediaPlaylistType {
|
||||
MediaPlaylistType::Event
|
||||
|
@ -387,7 +564,7 @@ impl Default for MediaPlaylistType {
|
|||
|
||||
/// 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.
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct MediaSegment {
|
||||
pub uri: String,
|
||||
/// `#EXTINF:<duration>,[<title>]`
|
||||
|
@ -409,9 +586,45 @@ pub struct MediaSegment {
|
|||
}
|
||||
|
||||
impl MediaSegment {
|
||||
pub fn new() -> MediaSegment {
|
||||
pub fn empty() -> MediaSegment {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
pub fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
|
||||
|
||||
if let Some(ref byte_range) = self.byte_range {
|
||||
write!(w, "#EXT-X-BYTERANGE:")?;
|
||||
byte_range.write_value_to(w)?;
|
||||
write!(w, "\n")?;
|
||||
}
|
||||
if self.discontinuity {
|
||||
writeln!(w, "{}", "#EXT-X-DISCONTINUITY")?;
|
||||
}
|
||||
if let Some(ref key) = self.key {
|
||||
write!(w, "#EXT-X-KEY:")?;
|
||||
key.write_attributes_to(w)?;
|
||||
write!(w, "\n")?;
|
||||
}
|
||||
if let Some(ref map) = self.map {
|
||||
write!(w, "#EXT-X-MAP:")?;
|
||||
map.write_attributes_to(w)?;
|
||||
write!(w, "\n")?;
|
||||
}
|
||||
if let Some(ref v) = self.program_date_time {
|
||||
writeln!(w, "#EXT-X-PROGRAM-DATE-TIME:{}", v)?;
|
||||
}
|
||||
if let Some(ref v) = self.daterange {
|
||||
writeln!(w, "#EXT-X-DATERANGE:{}", v)?;
|
||||
}
|
||||
|
||||
write!(w, "#EXTINF:{},", self.duration)?;
|
||||
|
||||
if let Some(ref v) = self.title {
|
||||
writeln!(w, "{}", v)?;
|
||||
}
|
||||
|
||||
writeln!(w, "{}", self.uri)
|
||||
}
|
||||
}
|
||||
|
||||
/// [`#EXT-X-KEY:<attribute-list>`]
|
||||
|
@ -423,7 +636,7 @@ impl MediaSegment {
|
|||
/// 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.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct Key {
|
||||
pub method: String,
|
||||
pub uri: Option<String>,
|
||||
|
@ -442,6 +655,14 @@ impl Key {
|
|||
keyformatversions: attrs.remove("KEYFORMATVERSIONS"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_attributes_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
|
||||
write!(w, "METHOD={}", self.method)?;
|
||||
write_some_attribute_quoted!(w, ",URI", &self.uri)?;
|
||||
write_some_attribute!(w, ",IV", &self.iv)?;
|
||||
write_some_attribute!(w, ",KEYFORMAT", &self.keyformat)?;
|
||||
write_some_attribute!(w, ",KEYFORMATVERSIONS", &self.keyformatversions)
|
||||
}
|
||||
}
|
||||
|
||||
/// [`#EXT-X-MAP:<attribute-list>`]
|
||||
|
@ -454,31 +675,77 @@ impl Key {
|
|||
/// 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.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct Map {
|
||||
pub uri: String,
|
||||
pub byterange: Option<ByteRange>,
|
||||
pub byte_range: Option<ByteRange>,
|
||||
}
|
||||
|
||||
impl Map {
|
||||
pub fn from_hashmap(mut attrs: HashMap<String, String>) -> Map {
|
||||
Map {
|
||||
uri: attrs.remove("URI").unwrap_or_default(),
|
||||
byte_range: attrs.remove("BYTERANGE").map(ByteRange::from),
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// [`#EXT-X-BYTERANGE:<n>[@<o>]`]
|
||||
/// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.2.2)
|
||||
///
|
||||
/// 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.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
pub struct ByteRange {
|
||||
pub length: i32,
|
||||
pub offset: Option<i32>,
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ByteRange {
|
||||
fn from(s: String) -> Self {
|
||||
let w: &str = &s;
|
||||
ByteRange::from(w)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for ByteRange {
|
||||
fn from(s: &'a str) -> Self {
|
||||
match byte_range_val(s.as_bytes()) {
|
||||
IResult::Done(_, br) => br,
|
||||
_ => panic!("Should not happen"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// [`#EXT-X-DATERANGE:<attribute-list>`]
|
||||
/// (https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-4.3.2.7)
|
||||
///
|
||||
/// 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.
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct DateRange {
|
||||
pub id: String,
|
||||
pub class: Option<String>,
|
||||
|
@ -500,7 +767,7 @@ pub struct DateRange {
|
|||
/// 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.
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct Start {
|
||||
pub time_offset: String,
|
||||
pub precise: Option<String>,
|
||||
|
@ -513,6 +780,12 @@ impl Start {
|
|||
precise: attrs.remove("PRECISE").or(Some("NO".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
|
||||
write!(w, "#EXT-X-START:TIME-OFFSET={}", self.time_offset)?;
|
||||
write_some_attribute!(w, ",PRECISE", &self.precise)?;
|
||||
write!(w, "\n")
|
||||
}
|
||||
}
|
||||
|
||||
/// A simple `#EXT-` tag
|
||||
|
@ -520,184 +793,4 @@ impl Start {
|
|||
pub struct ExtTag {
|
||||
pub tag: String,
|
||||
pub rest: String,
|
||||
}
|
||||
|
||||
fn bool_default_false(o: Option<String>) -> bool {
|
||||
if let Some(str) = o {
|
||||
if str == "YES" {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// Display
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
|
||||
impl fmt::Display for Playlist {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
&Playlist::MasterPlaylist(ref p) => write!(f, "{}", p),
|
||||
&Playlist::MediaPlaylist(ref p) => write!(f, "{}", p),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for MasterPlaylist {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
try!(writeln!(f,
|
||||
"[Master Playlist, version: {} | {} Streams]\n",
|
||||
self.version,
|
||||
self.variants.len()));
|
||||
|
||||
for (i, stream) in self.variants.iter().enumerate() {
|
||||
try!(write!(f, " {} -> {}", i + 1, stream))
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for MediaPlaylist {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
try!(write!(f, "[Media Playlist | duration: {:?} ~ seq: {:?} ~ type: {:?} ~ segments: {}",
|
||||
self.target_duration,
|
||||
self.media_sequence,
|
||||
self.playlist_type,
|
||||
self.segments.len(),
|
||||
));
|
||||
|
||||
if self.i_frames_only {
|
||||
try!(write!(f, " [iframes only]"));
|
||||
}
|
||||
if self.independent_segments {
|
||||
try!(write!(f, " [independent segments]"));
|
||||
}
|
||||
|
||||
try!(writeln!(f, "]"));
|
||||
|
||||
for (i, segment) in self.segments.iter().enumerate() {
|
||||
try!(write!(f, " {} -> {}", i + 1, segment));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for MediaSegment {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
try!(write!(f, "[Segment |"));
|
||||
|
||||
if let &Some(ref v) = &self.title {
|
||||
try!(write!(f, " title: {:?}", v));
|
||||
}
|
||||
|
||||
try!(write!(f, " ~ duration: {:?}", self.duration));
|
||||
|
||||
if let &Some(ref v) = &self.byte_range {
|
||||
try!(write!(f, " ~ byterange: {:?}", v));
|
||||
}
|
||||
|
||||
if self.discontinuity {
|
||||
try!(write!(f, " [discontinuity]"));
|
||||
}
|
||||
|
||||
if let &Some(ref v) = &self.program_date_time {
|
||||
try!(write!(f, " ~ datetime: {:?}", v));
|
||||
}
|
||||
|
||||
if let &Some(ref v) = &self.daterange {
|
||||
try!(write!(f, " ~ daterange: {:?}", v));
|
||||
}
|
||||
|
||||
writeln!(f, " ~ uri: {:?}]", self.uri)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for VariantStream {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
|
||||
match self.is_i_frame {
|
||||
true => try!(write!(f, "[VariantIFrame |")),
|
||||
false => try!(write!(f, "[Variant |")),
|
||||
};
|
||||
|
||||
try!(write!(f, " uri: {:?}", self.uri));
|
||||
|
||||
try!(write!(f, " ~ bandwidth: {}", self.bandwidth));
|
||||
if let &Some(ref v) = &self.resolution {
|
||||
try!(write!(f, " ~ res: {}", v));
|
||||
}
|
||||
|
||||
try!(write!(f, " ~ alts: {}", self.alternatives.len()));
|
||||
|
||||
if let &Some(ref v) = &self.frame_rate {
|
||||
try!(write!(f, " ~ fps: {}", v));
|
||||
}
|
||||
|
||||
if let &Some(ref v) = &self.audio {
|
||||
try!(write!(f, " ~ audio: {}", v));
|
||||
}
|
||||
|
||||
if let &Some(ref v) = &self.video {
|
||||
try!(write!(f, " ~ video: {}", v));
|
||||
}
|
||||
|
||||
if let &Some(ref v) = &self.subtitles {
|
||||
try!(write!(f, " ~ subs: {}", v));
|
||||
}
|
||||
|
||||
if let &Some(ref v) = &self.closed_captions {
|
||||
try!(write!(f, " ~ closed_captions: {}", v));
|
||||
}
|
||||
|
||||
try!(write!(f, "]"));
|
||||
try!(write!(f, "\n"));
|
||||
|
||||
for (_, alt) in self.alternatives.iter().enumerate() {
|
||||
try!(write!(f, "{}", alt));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for AlternativeMedia {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
|
||||
try!(write!(f,
|
||||
"[AlternativeMedia | type: {:?} ~ group: {} ~ name: {:?}",
|
||||
self.media_type,
|
||||
self.group_id,
|
||||
self.name));
|
||||
|
||||
if let &Some(ref v) = &self.uri {
|
||||
try!(write!(f, " ~ uri: {:?}", v));
|
||||
}
|
||||
|
||||
try!(write!(f, " ~ default: {}", self.default));
|
||||
|
||||
if let &Some(ref v) = &self.language {
|
||||
try!(write!(f, " ~ lang: {}", v));
|
||||
}
|
||||
|
||||
if let &Some(ref v) = &self.assoc_language {
|
||||
try!(write!(f, " ~ assoc_language: {}", v));
|
||||
}
|
||||
|
||||
try!(write!(f, " ~ autoselect: {}", self.default));
|
||||
|
||||
try!(write!(f, " ~ forced: {}", self.default));
|
||||
|
||||
if let &Some(ref v) = &self.instream_id {
|
||||
try!(write!(f, " ~ instream_id: {}", v));
|
||||
}
|
||||
|
||||
if let &Some(ref v) = &self.characteristics {
|
||||
try!(write!(f, " ~ characteristics: {}", v));
|
||||
}
|
||||
|
||||
writeln!(f, "]")
|
||||
}
|
||||
}
|
||||
}
|
169
tests/lib.rs
169
tests/lib.rs
|
@ -7,7 +7,9 @@ extern crate m3u8_rs;
|
|||
use std::fs;
|
||||
use std::path;
|
||||
use m3u8_rs::*;
|
||||
use m3u8_rs::playlist::*;
|
||||
use std::io::Read;
|
||||
use std::fs::File;
|
||||
use nom::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
|
@ -34,12 +36,12 @@ fn get_sample_playlist(name: &str) -> String {
|
|||
// Playlist
|
||||
|
||||
fn print_parse_playlist_test(playlist_name: &str) -> bool {
|
||||
let input = get_sample_playlist(playlist_name);
|
||||
let input: String = get_sample_playlist(playlist_name);
|
||||
println!("Parsing playlist file: {:?}", playlist_name);
|
||||
let parsed = parse_playlist(input.as_bytes());
|
||||
|
||||
if let IResult::Done(i,o) = parsed {
|
||||
println!("{}", o);
|
||||
println!("{:?}", o);
|
||||
true
|
||||
}
|
||||
else {
|
||||
|
@ -225,3 +227,166 @@ fn float_should_ignore_trailing_dot() {
|
|||
IResult::Done(".rest".as_bytes(), 33f32)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_duration_title() {
|
||||
assert_eq!(
|
||||
duration_title_tag(b"2.002,title\nrest"),
|
||||
IResult::Done("rest".as_bytes(), (2.002f32, Some("title".to_string())))
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// Creating playlists
|
||||
|
||||
fn print_create_and_parse_playlist(playlist_original: &mut Playlist) -> Playlist {
|
||||
let mut utf8: Vec<u8> = Vec::new();
|
||||
playlist_original.write_to(&mut utf8).unwrap();
|
||||
|
||||
let m3u8_str: &str = std::str::from_utf8(&utf8).unwrap();
|
||||
|
||||
let playlist_parsed = match *playlist_original {
|
||||
Playlist::MasterPlaylist(_) =>
|
||||
Playlist::MasterPlaylist(parse_master_playlist_res(m3u8_str.as_bytes()).unwrap()),
|
||||
Playlist::MediaPlaylist(_) =>
|
||||
Playlist::MediaPlaylist(parse_media_playlist_res(m3u8_str.as_bytes()).unwrap()),
|
||||
};
|
||||
|
||||
print!("\n\n---- utf8 result\n\n{}", m3u8_str);
|
||||
print!("\n---- Original\n\n{:?}", playlist_original);
|
||||
print!("\n\n---- Parsed\n\n{:?}\n\n", playlist_parsed);
|
||||
|
||||
playlist_parsed
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_and_parse_master_playlist_empty() {
|
||||
let mut playlist_original = Playlist::MasterPlaylist(MasterPlaylist { ..Default::default() });
|
||||
let playlist_parsed = print_create_and_parse_playlist(&mut playlist_original);
|
||||
assert_eq!(playlist_original, playlist_parsed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_and_parse_master_playlist_full() {
|
||||
|
||||
let mut playlist_original = Playlist::MasterPlaylist(MasterPlaylist {
|
||||
version: 6,
|
||||
variants: vec![
|
||||
VariantStream {
|
||||
is_i_frame: false,
|
||||
uri: "masterplaylist-uri".into(),
|
||||
bandwidth: "10010010".into(),
|
||||
average_bandwidth: Some("10010010".into()),
|
||||
codecs: "TheCODEC".into(),
|
||||
resolution: Some("1000x3000".into()),
|
||||
frame_rate: Some("60".into()),
|
||||
audio: Some("audio".into()),
|
||||
video: Some("video".into()),
|
||||
subtitles: Some("subtitles".into()),
|
||||
closed_captions: Some("closed_captions".into()),
|
||||
alternatives: vec! [
|
||||
AlternativeMedia {
|
||||
media_type: AlternativeMediaType::Audio,
|
||||
uri: Some("alt-media-uri".into()),
|
||||
group_id: "group-id".into(),
|
||||
language: Some("language".into()),
|
||||
assoc_language: Some("assoc-language".into()),
|
||||
name: "Xmedia".into(),
|
||||
default: true, // Its absence indicates an implicit value of NO
|
||||
autoselect: true, // Its absence indicates an implicit value of NO
|
||||
forced: true, // Its absence indicates an implicit value of NO
|
||||
instream_id: Some("instream_id".into()),
|
||||
characteristics: Some("characteristics".into()),
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
session_data: Some(SessionData {
|
||||
data_id: "****".into(),
|
||||
value: "%%%%".into(),
|
||||
uri: "++++".into(),
|
||||
language: Some("SessionDataLanguage".into()),
|
||||
}),
|
||||
session_key: Some(SessionKey(Key {
|
||||
method: "AES-128".into(),
|
||||
uri: Some("https://secure.domain.com".into()),
|
||||
iv: Some("0xb059217aa2649ce170b734".into()),
|
||||
keyformat: Some("xXkeyformatXx".into()),
|
||||
keyformatversions: Some("xXFormatVers".into()),
|
||||
})),
|
||||
start: Some(Start {
|
||||
time_offset: "123123123".into(),
|
||||
precise: Some("YES".into()),
|
||||
}),
|
||||
independent_segments: true,
|
||||
});
|
||||
let playlist_parsed = print_create_and_parse_playlist(&mut playlist_original);
|
||||
assert_eq!(playlist_original, playlist_parsed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_and_parse_media_playlist_empty() {
|
||||
let mut playlist_original = Playlist::MediaPlaylist(MediaPlaylist { ..Default::default() });
|
||||
let playlist_parsed = print_create_and_parse_playlist(&mut playlist_original);
|
||||
assert_eq!(playlist_original, playlist_parsed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_and_parse_media_playlist_single_segment() {
|
||||
let mut playlist_original = Playlist::MediaPlaylist(MediaPlaylist {
|
||||
segments: vec![
|
||||
MediaSegment {
|
||||
uri: "20140311T113819-01-338559live.ts".into(),
|
||||
duration: 2.002,
|
||||
title: Some("hey".into()),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
});
|
||||
let playlist_parsed = print_create_and_parse_playlist(&mut playlist_original);
|
||||
assert_eq!(playlist_original, playlist_parsed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_and_parse_media_playlist_full() {
|
||||
|
||||
let mut playlist_original = Playlist::MediaPlaylist(MediaPlaylist {
|
||||
version: 4,
|
||||
target_duration: 3.0,
|
||||
media_sequence: 338559,
|
||||
discontinuity_sequence: 1234,
|
||||
end_list: true,
|
||||
playlist_type: Some(MediaPlaylistType::Vod),
|
||||
i_frames_only: true,
|
||||
start: Some(Start {
|
||||
time_offset: "9999".into(),
|
||||
precise: Some("YES".into()),
|
||||
}),
|
||||
independent_segments: true,
|
||||
segments: vec![
|
||||
MediaSegment {
|
||||
uri: "20140311T113819-01-338559live.ts".into(),
|
||||
duration: 2.002,
|
||||
title: Some("338559".into()),
|
||||
byte_range: Some(ByteRange::from("137116@497036")),
|
||||
discontinuity: true,
|
||||
key: Some(Key {
|
||||
method: "AES-128".into(),
|
||||
uri: Some("https://secure.domain.com".into()),
|
||||
iv: Some("0xb059217aa2649ce170b734".into()),
|
||||
keyformat: Some("xXkeyformatXx".into()),
|
||||
keyformatversions: Some("xXFormatVers".into()),
|
||||
}),
|
||||
map: Some(Map {
|
||||
uri: "www.map-uri.com".into(),
|
||||
byte_range: Some(ByteRange::from("137116@497036")),
|
||||
}),
|
||||
program_date_time: Some("broodlordinfestorgg".into()),
|
||||
daterange: None,
|
||||
},
|
||||
],
|
||||
});
|
||||
let playlist_parsed = print_create_and_parse_playlist(&mut playlist_original);
|
||||
assert_eq!(playlist_original, playlist_parsed);
|
||||
}
|
Loading…
Reference in a new issue