mirror of
https://github.com/rutgersc/m3u8-rs.git
synced 2024-05-19 08:48:09 +00:00
Compare commits
10 commits
1bf089e671
...
25e0daadcf
Author | SHA1 | Date | |
---|---|---|---|
25e0daadcf | |||
381ac7732f | |||
e3b6390186 | |||
7f322675eb | |||
c5cceeb4f6 | |||
5109753b96 | |||
b7c2cb023d | |||
663e0607cf | |||
4120e1c557 | |||
b84da46e0a |
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "m3u8-rs"
|
||||
version = "5.0.5"
|
||||
version = "6.0.0"
|
||||
authors = ["Rutger"]
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/rutgersc/m3u8-rs"
|
||||
|
@ -16,4 +16,5 @@ chrono = { version = "0.4", default-features = false, features = [ "std" ] }
|
|||
[features]
|
||||
default = ["parser"]
|
||||
parser = ["nom"]
|
||||
lenient = []
|
||||
|
||||
|
|
30
sample-playlists/media-playlist-zero-decimal.m3u8
Normal file
30
sample-playlists/media-playlist-zero-decimal.m3u8
Normal file
|
@ -0,0 +1,30 @@
|
|||
#EXTM3U
|
||||
#EXT-X-TARGETDURATION:11
|
||||
#EXT-X-VERSION:4
|
||||
#EXT-X-MEDIA-SEQUENCE:0
|
||||
#EXT-X-PLAYLIST-TYPE:VOD
|
||||
#EXTINF:9.00000,
|
||||
#EXT-X-BYTERANGE:86920@0
|
||||
main.aac
|
||||
#EXTINF:10.00000,
|
||||
#EXT-X-BYTERANGE:136595@86920
|
||||
main.aac
|
||||
#EXTINF:9.00000,
|
||||
#EXT-X-BYTERANGE:136567@223515
|
||||
main.aac
|
||||
#EXTINF:10.00000,
|
||||
#EXT-X-BYTERANGE:136954@360082
|
||||
main.aac
|
||||
#EXTINF:10.00000,
|
||||
#EXT-X-BYTERANGE:137116@497036
|
||||
main.aac
|
||||
#EXTINF:9.00000,
|
||||
#EXT-X-BYTERANGE:136770@634152
|
||||
main.aac
|
||||
#EXTINF:10.00000,
|
||||
#EXT-X-BYTERANGE:137219@770922
|
||||
main.aac
|
||||
#EXTINF:10.00000,
|
||||
#EXT-X-BYTERANGE:137132@908141
|
||||
main.acc
|
||||
#EXT-X-ENDLIST
|
28
src/lib.rs
28
src/lib.rs
|
@ -42,7 +42,7 @@
|
|||
//!
|
||||
//! let playlist = MediaPlaylist {
|
||||
//! version: Some(6),
|
||||
//! target_duration: 3.0,
|
||||
//! target_duration: 3,
|
||||
//! media_sequence: 338559,
|
||||
//! discontinuity_sequence: 1234,
|
||||
//! end_list: true,
|
||||
|
@ -64,6 +64,32 @@
|
|||
//! //let mut file = std::fs::File::open("playlist.m3u8").unwrap();
|
||||
//! //playlist.write_to(&mut file).unwrap();
|
||||
//! ```
|
||||
//!
|
||||
//! Controlling the output precision for floats, such as #EXTINF (default is unset)
|
||||
//!
|
||||
//! ```
|
||||
//! use std::sync::atomic::Ordering;
|
||||
//! use m3u8_rs::{WRITE_OPT_FLOAT_PRECISION, MediaPlaylist, MediaSegment};
|
||||
//!
|
||||
//! WRITE_OPT_FLOAT_PRECISION.store(5, Ordering::Relaxed);
|
||||
//!
|
||||
//! let playlist = MediaPlaylist {
|
||||
//! target_duration: 3,
|
||||
//! segments: vec![
|
||||
//! MediaSegment {
|
||||
//! duration: 2.9,
|
||||
//! title: Some("title".into()),
|
||||
//! ..Default::default()
|
||||
//! },
|
||||
//! ],
|
||||
//! ..Default::default()
|
||||
//! };
|
||||
//!
|
||||
//! let mut v: Vec<u8> = Vec::new();
|
||||
//!
|
||||
//! playlist.write_to(&mut v).unwrap();
|
||||
//! let m3u8_str: &str = std::str::from_utf8(&v).unwrap();
|
||||
//! assert!(m3u8_str.contains("#EXTINF:2.90000,title"));
|
||||
|
||||
mod playlist;
|
||||
pub use playlist::*;
|
||||
|
|
|
@ -52,7 +52,7 @@ pub fn parse_playlist(input: &[u8]) -> IResult<&[u8], Playlist> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Parses an m3u8 playlist just like `parse_playlist`, except that this returns an [std::result::Result](std::result::Result) instead of a [nom::IResult](https://docs.rs/nom/1.2.3/nom/enum.IResult.html).
|
||||
/// Parses an m3u8 playlist just like `parse_playlist`, except that this returns an [std::result::Result](std::result::Result) instead of a [`nom::IResult`](https://docs.rs/nom/1.2.3/nom/enum.IResult.html).
|
||||
/// However, since [nom::IResult](nom::IResult) is now an [alias to Result](https://github.com/Geal/nom/blob/master/doc/upgrading_to_nom_5.md), this is no longer needed.
|
||||
///
|
||||
/// # Examples
|
||||
|
@ -128,7 +128,7 @@ pub fn parse_media_playlist_res(
|
|||
/// 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
|
||||
contains_master_tag(input).map(|t| t.0).unwrap_or(false)
|
||||
contains_master_tag(input).map_or(false, |t| t.0)
|
||||
}
|
||||
|
||||
/// Scans input looking for either a master or media `#EXT` tag.
|
||||
|
@ -344,7 +344,7 @@ fn parse_media_playlist_tags(i: &[u8]) -> IResult<&[u8], Vec<MediaPlaylistTag>>
|
|||
enum MediaPlaylistTag {
|
||||
Version(usize),
|
||||
Segment(SegmentTag),
|
||||
TargetDuration(f32),
|
||||
TargetDuration(u64),
|
||||
MediaSequence(u64),
|
||||
DiscontinuitySequence(u64),
|
||||
EndList,
|
||||
|
@ -361,7 +361,7 @@ fn media_playlist_tag(i: &[u8]) -> IResult<&[u8], MediaPlaylistTag> {
|
|||
alt((
|
||||
map(version_tag, MediaPlaylistTag::Version),
|
||||
map(
|
||||
pair(tag("#EXT-X-TARGETDURATION:"), float),
|
||||
pair(tag("#EXT-X-TARGETDURATION:"), number),
|
||||
|(_, duration)| MediaPlaylistTag::TargetDuration(duration),
|
||||
),
|
||||
map(
|
||||
|
@ -642,28 +642,27 @@ pub enum QuotedOrUnquoted {
|
|||
|
||||
impl Default for QuotedOrUnquoted {
|
||||
fn default() -> Self {
|
||||
QuotedOrUnquoted::Quoted(String::new())
|
||||
Self::Quoted(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl QuotedOrUnquoted {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
QuotedOrUnquoted::Quoted(s) => s.as_str(),
|
||||
QuotedOrUnquoted::Unquoted(s) => s.as_str(),
|
||||
Self::Quoted(s) | Self::Unquoted(s) => s.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_unquoted(&self) -> Option<&str> {
|
||||
match self {
|
||||
QuotedOrUnquoted::Unquoted(s) => Some(s.as_str()),
|
||||
Self::Unquoted(s) => Some(s.as_str()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_quoted(&self) -> Option<&str> {
|
||||
match self {
|
||||
QuotedOrUnquoted::Quoted(s) => Some(s.as_str()),
|
||||
Self::Quoted(s) => Some(s.as_str()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
@ -672,22 +671,22 @@ impl QuotedOrUnquoted {
|
|||
impl From<&str> for QuotedOrUnquoted {
|
||||
fn from(s: &str) -> Self {
|
||||
if s.starts_with('"') && s.ends_with('"') {
|
||||
return QuotedOrUnquoted::Quoted(
|
||||
return Self::Quoted(
|
||||
s.strip_prefix('"')
|
||||
.and_then(|s| s.strip_suffix('"'))
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
QuotedOrUnquoted::Unquoted(s.to_string())
|
||||
Self::Unquoted(s.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for QuotedOrUnquoted {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
QuotedOrUnquoted::Unquoted(s) => write!(f, "{}", s),
|
||||
QuotedOrUnquoted::Quoted(u) => write!(f, "\"{}\"", u),
|
||||
Self::Unquoted(s) => write!(f, "{}", s),
|
||||
Self::Quoted(u) => write!(f, "\"{}\"", u),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -747,18 +746,20 @@ fn float(i: &[u8]) -> IResult<&[u8], f32> {
|
|||
take_while1(is_digit),
|
||||
opt(preceded(char('.'), take_while1(is_digit))),
|
||||
),
|
||||
|(left, right): (&[u8], Option<&[u8]>)| match right {
|
||||
Some(right) => {
|
||||
let n = &i[..(left.len() + right.len() + 1)];
|
||||
// Can't fail because we validated it above already
|
||||
let n = str::from_utf8(n).unwrap();
|
||||
n.parse()
|
||||
}
|
||||
None => {
|
||||
// Can't fail because we validated it above already
|
||||
let left = str::from_utf8(left).unwrap();
|
||||
left.parse()
|
||||
}
|
||||
|(left, right): (&[u8], Option<&[u8]>)| {
|
||||
right.map_or_else(
|
||||
|| {
|
||||
// Can't fail because we validated it above already
|
||||
let left = str::from_utf8(left).unwrap();
|
||||
left.parse()
|
||||
},
|
||||
|right| {
|
||||
let n = &i[..=(left.len() + right.len())];
|
||||
// Can't fail because we validated it above already
|
||||
let n = str::from_utf8(n).unwrap();
|
||||
n.parse()
|
||||
},
|
||||
)
|
||||
},
|
||||
)(i)
|
||||
}
|
||||
|
|
174
src/playlist.rs
174
src/playlist.rs
|
@ -4,13 +4,19 @@
|
|||
//! Which is either a `MasterPlaylist` or a `MediaPlaylist`.
|
||||
|
||||
use crate::QuotedOrUnquoted;
|
||||
use chrono::DateTime;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::f32;
|
||||
use std::fmt;
|
||||
use std::fmt::Display;
|
||||
use std::io::Write;
|
||||
use std::str::FromStr;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::usize::MAX;
|
||||
use std::{f32, usize};
|
||||
|
||||
/// The output precision for floats, such as #EXTINF (default is unset)
|
||||
pub static WRITE_OPT_FLOAT_PRECISION: AtomicUsize = AtomicUsize::new(MAX);
|
||||
|
||||
macro_rules! write_some_attribute_quoted {
|
||||
($w:expr, $tag:expr, $o:expr) => {
|
||||
|
@ -152,8 +158,8 @@ pub enum Playlist {
|
|||
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),
|
||||
Self::MasterPlaylist(ref pl) => pl.write_to(writer),
|
||||
Self::MediaPlaylist(ref pl) => pl.write_to(writer),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -254,7 +260,7 @@ impl VariantStream {
|
|||
pub(crate) fn from_hashmap(
|
||||
mut attrs: HashMap<String, QuotedOrUnquoted>,
|
||||
is_i_frame: bool,
|
||||
) -> Result<VariantStream, String> {
|
||||
) -> Result<Self, String> {
|
||||
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
|
||||
|
@ -275,11 +281,11 @@ impl VariantStream {
|
|||
let subtitles = quoted_string!(attrs, "SUBTITLES");
|
||||
let closed_captions = attrs
|
||||
.remove("CLOSED-CAPTIONS")
|
||||
.map(|c| c.try_into())
|
||||
.map(TryInto::try_into)
|
||||
.transpose()?;
|
||||
let other_attributes = if attrs.is_empty() { None } else { Some(attrs) };
|
||||
|
||||
Ok(VariantStream {
|
||||
Ok(Self {
|
||||
is_i_frame,
|
||||
uri,
|
||||
bandwidth,
|
||||
|
@ -346,7 +352,7 @@ impl Display for Resolution {
|
|||
impl FromStr for Resolution {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Resolution, String> {
|
||||
fn from_str(s: &str) -> Result<Self, String> {
|
||||
match s.split_once('x') {
|
||||
Some((width, height)) => {
|
||||
let width = width
|
||||
|
@ -355,7 +361,7 @@ impl FromStr for Resolution {
|
|||
let height = height
|
||||
.parse::<u64>()
|
||||
.map_err(|err| format!("Can't parse RESOLUTION attribute height: {}", err))?;
|
||||
Ok(Resolution { width, height })
|
||||
Ok(Self { width, height })
|
||||
}
|
||||
None => Err(String::from("Invalid RESOLUTION attribute")),
|
||||
}
|
||||
|
@ -373,12 +379,12 @@ pub enum HDCPLevel {
|
|||
impl FromStr for HDCPLevel {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<HDCPLevel, String> {
|
||||
fn from_str(s: &str) -> Result<Self, String> {
|
||||
match s {
|
||||
"TYPE-0" => Ok(HDCPLevel::Type0),
|
||||
"TYPE-1" => Ok(HDCPLevel::Type1),
|
||||
"NONE" => Ok(HDCPLevel::None),
|
||||
_ => Ok(HDCPLevel::Other(String::from(s))),
|
||||
"TYPE-0" => Ok(Self::Type0),
|
||||
"TYPE-1" => Ok(Self::Type1),
|
||||
"NONE" => Ok(Self::None),
|
||||
_ => Ok(Self::Other(String::from(s))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -389,10 +395,10 @@ impl Display for HDCPLevel {
|
|||
f,
|
||||
"{}",
|
||||
match self {
|
||||
HDCPLevel::Type0 => "TYPE-0",
|
||||
HDCPLevel::Type1 => "TYPE-1",
|
||||
HDCPLevel::None => "NONE",
|
||||
HDCPLevel::Other(s) => s,
|
||||
Self::Type0 => "TYPE-0",
|
||||
Self::Type1 => "TYPE-1",
|
||||
Self::None => "NONE",
|
||||
Self::Other(s) => s,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -409,11 +415,11 @@ pub enum ClosedCaptionGroupId {
|
|||
impl TryFrom<QuotedOrUnquoted> for ClosedCaptionGroupId {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(s: QuotedOrUnquoted) -> Result<ClosedCaptionGroupId, String> {
|
||||
fn try_from(s: QuotedOrUnquoted) -> Result<Self, String> {
|
||||
match s {
|
||||
QuotedOrUnquoted::Unquoted(s) if s == "NONE" => Ok(ClosedCaptionGroupId::None),
|
||||
QuotedOrUnquoted::Unquoted(s) => Ok(ClosedCaptionGroupId::Other(s)),
|
||||
QuotedOrUnquoted::Quoted(s) => Ok(ClosedCaptionGroupId::GroupId(s)),
|
||||
QuotedOrUnquoted::Unquoted(s) if s == "NONE" => Ok(Self::None),
|
||||
QuotedOrUnquoted::Unquoted(s) => Ok(Self::Other(s)),
|
||||
QuotedOrUnquoted::Quoted(s) => Ok(Self::GroupId(s)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -447,7 +453,7 @@ pub struct AlternativeMedia {
|
|||
impl AlternativeMedia {
|
||||
pub(crate) fn from_hashmap(
|
||||
mut attrs: HashMap<String, QuotedOrUnquoted>,
|
||||
) -> Result<AlternativeMedia, String> {
|
||||
) -> Result<Self, 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");
|
||||
|
@ -467,6 +473,7 @@ impl AlternativeMedia {
|
|||
let default = is_yes!(attrs, "DEFAULT");
|
||||
let autoselect = is_yes!(attrs, "AUTOSELECT");
|
||||
|
||||
#[cfg(not(feature = "lenient"))]
|
||||
if media_type != AlternativeMediaType::Subtitles && attrs.contains_key("FORCED") {
|
||||
return Err(String::from(
|
||||
"FORCED attribute must not be included in non-SUBTITLE Alternative Medias",
|
||||
|
@ -488,7 +495,7 @@ impl AlternativeMedia {
|
|||
let channels = quoted_string!(attrs, "CHANNELS");
|
||||
let other_attributes = if attrs.is_empty() { None } else { Some(attrs) };
|
||||
|
||||
Ok(AlternativeMedia {
|
||||
Ok(Self {
|
||||
media_type,
|
||||
uri,
|
||||
group_id,
|
||||
|
@ -547,20 +554,20 @@ pub enum AlternativeMediaType {
|
|||
impl FromStr for AlternativeMediaType {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<AlternativeMediaType, String> {
|
||||
fn from_str(s: &str) -> Result<Self, String> {
|
||||
match s {
|
||||
"AUDIO" => Ok(AlternativeMediaType::Audio),
|
||||
"VIDEO" => Ok(AlternativeMediaType::Video),
|
||||
"SUBTITLES" => Ok(AlternativeMediaType::Subtitles),
|
||||
"CLOSED-CAPTIONS" => Ok(AlternativeMediaType::ClosedCaptions),
|
||||
_ => Ok(AlternativeMediaType::Other(String::from(s))),
|
||||
"AUDIO" => Ok(Self::Audio),
|
||||
"VIDEO" => Ok(Self::Video),
|
||||
"SUBTITLES" => Ok(Self::Subtitles),
|
||||
"CLOSED-CAPTIONS" => Ok(Self::ClosedCaptions),
|
||||
_ => Ok(Self::Other(String::from(s))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AlternativeMediaType {
|
||||
fn default() -> AlternativeMediaType {
|
||||
AlternativeMediaType::Video
|
||||
fn default() -> Self {
|
||||
Self::Video
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -570,11 +577,11 @@ impl Display for AlternativeMediaType {
|
|||
f,
|
||||
"{}",
|
||||
match self {
|
||||
AlternativeMediaType::Audio => "AUDIO",
|
||||
AlternativeMediaType::Video => "VIDEO",
|
||||
AlternativeMediaType::Subtitles => "SUBTITLES",
|
||||
AlternativeMediaType::ClosedCaptions => "CLOSED-CAPTIONS",
|
||||
AlternativeMediaType::Other(s) => s.as_str(),
|
||||
Self::Audio => "AUDIO",
|
||||
Self::Video => "VIDEO",
|
||||
Self::Subtitles => "SUBTITLES",
|
||||
Self::ClosedCaptions => "CLOSED-CAPTIONS",
|
||||
Self::Other(s) => s.as_str(),
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -590,19 +597,19 @@ pub enum InstreamId {
|
|||
impl FromStr for InstreamId {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<InstreamId, String> {
|
||||
fn from_str(s: &str) -> Result<Self, 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))
|
||||
Ok(Self::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))
|
||||
Ok(Self::Service(service))
|
||||
} else {
|
||||
Ok(InstreamId::Other(String::from(s)))
|
||||
Ok(Self::Other(String::from(s)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -610,9 +617,9 @@ impl FromStr for InstreamId {
|
|||
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),
|
||||
Self::CC(cc) => write!(f, "CC{}", cc),
|
||||
Self::Service(service) => write!(f, "SERVICE{}", service),
|
||||
Self::Other(s) => write!(f, "{}", s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -652,7 +659,7 @@ pub struct SessionData {
|
|||
impl SessionData {
|
||||
pub(crate) fn from_hashmap(
|
||||
mut attrs: HashMap<String, QuotedOrUnquoted>,
|
||||
) -> Result<SessionData, String> {
|
||||
) -> Result<Self, String> {
|
||||
let data_id = quoted_string!(attrs, "DATA-ID")
|
||||
.ok_or_else(|| String::from("EXT-X-SESSION-DATA field without DATA-ID attribute"))?;
|
||||
|
||||
|
@ -665,23 +672,23 @@ impl SessionData {
|
|||
(Some(value), None) => SessionDataField::Value(value),
|
||||
(None, Some(uri)) => SessionDataField::Uri(uri),
|
||||
(Some(_), Some(_)) => {
|
||||
return Err(format![
|
||||
return Err(format!(
|
||||
"EXT-X-SESSION-DATA tag {} contains both a value and an URI",
|
||||
data_id
|
||||
])
|
||||
))
|
||||
}
|
||||
(None, None) => {
|
||||
return Err(format![
|
||||
return Err(format!(
|
||||
"EXT-X-SESSION-DATA tag {} must contain either a value or an URI",
|
||||
data_id
|
||||
])
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let language = quoted_string!(attrs, "LANGUAGE");
|
||||
let other_attributes = if attrs.is_empty() { None } else { Some(attrs) };
|
||||
|
||||
Ok(SessionData {
|
||||
Ok(Self {
|
||||
data_id,
|
||||
field,
|
||||
language,
|
||||
|
@ -713,7 +720,7 @@ impl SessionData {
|
|||
pub struct MediaPlaylist {
|
||||
pub version: Option<usize>,
|
||||
/// `#EXT-X-TARGETDURATION:<s>`
|
||||
pub target_duration: f32,
|
||||
pub target_duration: u64,
|
||||
/// `#EXT-X-MEDIA-SEQUENCE:<number>`
|
||||
pub media_sequence: u64,
|
||||
pub segments: Vec<MediaSegment>,
|
||||
|
@ -786,11 +793,11 @@ pub enum MediaPlaylistType {
|
|||
impl FromStr for MediaPlaylistType {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<MediaPlaylistType, String> {
|
||||
fn from_str(s: &str) -> Result<Self, String> {
|
||||
match s {
|
||||
"EVENT" => Ok(MediaPlaylistType::Event),
|
||||
"VOD" => Ok(MediaPlaylistType::Vod),
|
||||
_ => Ok(MediaPlaylistType::Other(String::from(s))),
|
||||
"EVENT" => Ok(Self::Event),
|
||||
"VOD" => Ok(Self::Vod),
|
||||
_ => Ok(Self::Other(String::from(s))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -801,17 +808,17 @@ impl Display for MediaPlaylistType {
|
|||
f,
|
||||
"{}",
|
||||
match self {
|
||||
MediaPlaylistType::Event => "EVENT",
|
||||
MediaPlaylistType::Vod => "VOD",
|
||||
MediaPlaylistType::Other(s) => s,
|
||||
Self::Event => "EVENT",
|
||||
Self::Vod => "VOD",
|
||||
Self::Other(s) => s,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MediaPlaylistType {
|
||||
fn default() -> MediaPlaylistType {
|
||||
MediaPlaylistType::Event
|
||||
fn default() -> Self {
|
||||
Self::Event
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -845,8 +852,8 @@ pub struct MediaSegment {
|
|||
}
|
||||
|
||||
impl MediaSegment {
|
||||
pub fn empty() -> MediaSegment {
|
||||
Default::default()
|
||||
pub fn empty() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub(crate) fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
|
||||
|
@ -884,7 +891,14 @@ impl MediaSegment {
|
|||
writeln!(w, "{}", unknown_tag)?;
|
||||
}
|
||||
|
||||
write!(w, "#EXTINF:{},", self.duration)?;
|
||||
match WRITE_OPT_FLOAT_PRECISION.load(Ordering::Relaxed) {
|
||||
MAX => {
|
||||
write!(w, "#EXTINF:{},", self.duration)?;
|
||||
}
|
||||
n => {
|
||||
write!(w, "#EXTINF:{:.*},", n, self.duration)?;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(ref v) = self.title {
|
||||
writeln!(w, "{}", v)?;
|
||||
|
@ -906,19 +920,19 @@ pub enum KeyMethod {
|
|||
|
||||
impl Default for KeyMethod {
|
||||
fn default() -> Self {
|
||||
KeyMethod::None
|
||||
Self::None
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for KeyMethod {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<KeyMethod, String> {
|
||||
fn from_str(s: &str) -> Result<Self, String> {
|
||||
match s {
|
||||
"NONE" => Ok(KeyMethod::None),
|
||||
"AES-128" => Ok(KeyMethod::AES128),
|
||||
"SAMPLE-AES" => Ok(KeyMethod::SampleAES),
|
||||
_ => Ok(KeyMethod::Other(String::from(s))),
|
||||
"NONE" => Ok(Self::None),
|
||||
"AES-128" => Ok(Self::AES128),
|
||||
"SAMPLE-AES" => Ok(Self::SampleAES),
|
||||
_ => Ok(Self::Other(String::from(s))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -929,10 +943,10 @@ impl Display for KeyMethod {
|
|||
f,
|
||||
"{}",
|
||||
match self {
|
||||
KeyMethod::None => "NONE",
|
||||
KeyMethod::AES128 => "AES-128",
|
||||
KeyMethod::SampleAES => "SAMPLE-AES",
|
||||
KeyMethod::Other(s) => s,
|
||||
Self::None => "NONE",
|
||||
Self::AES128 => "AES-128",
|
||||
Self::SampleAES => "SAMPLE-AES",
|
||||
Self::Other(s) => s,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -958,7 +972,7 @@ pub struct Key {
|
|||
impl Key {
|
||||
pub(crate) fn from_hashmap(
|
||||
mut attrs: HashMap<String, QuotedOrUnquoted>,
|
||||
) -> Result<Key, String> {
|
||||
) -> Result<Self, String> {
|
||||
let method: KeyMethod = unquoted_string_parse!(attrs, "METHOD")
|
||||
.ok_or_else(|| String::from("EXT-X-KEY without mandatory METHOD attribute"))?;
|
||||
|
||||
|
@ -970,7 +984,7 @@ impl Key {
|
|||
let keyformat = quoted_string!(attrs, "KEYFORMAT");
|
||||
let keyformatversions = quoted_string!(attrs, "KEYFORMATVERSIONS");
|
||||
|
||||
Ok(Key {
|
||||
Ok(Self {
|
||||
method,
|
||||
uri,
|
||||
iv,
|
||||
|
@ -1055,7 +1069,7 @@ pub struct DateRange {
|
|||
}
|
||||
|
||||
impl DateRange {
|
||||
pub fn from_hashmap(mut attrs: HashMap<String, QuotedOrUnquoted>) -> Result<DateRange, String> {
|
||||
pub fn from_hashmap(mut attrs: HashMap<String, QuotedOrUnquoted>) -> Result<Self, 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");
|
||||
|
@ -1075,7 +1089,7 @@ impl DateRange {
|
|||
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() {
|
||||
for (k, v) in attrs {
|
||||
if k.starts_with("X-") {
|
||||
x_prefixed.insert(k, v);
|
||||
} else {
|
||||
|
@ -1083,7 +1097,7 @@ impl DateRange {
|
|||
}
|
||||
}
|
||||
|
||||
Ok(DateRange {
|
||||
Ok(Self {
|
||||
id,
|
||||
class,
|
||||
start_date,
|
||||
|
@ -1111,7 +1125,7 @@ impl DateRange {
|
|||
write_some_attribute_quoted!(
|
||||
w,
|
||||
",END-DATE",
|
||||
&self.end_date.as_ref().map(|dt| dt.to_rfc3339())
|
||||
&self.end_date.as_ref().map(DateTime::to_rfc3339)
|
||||
)?;
|
||||
write_some_attribute!(w, ",DURATION", &self.duration)?;
|
||||
write_some_attribute!(w, ",PLANNED-DURATION", &self.planned_duration)?;
|
||||
|
@ -1151,12 +1165,12 @@ pub struct Start {
|
|||
impl Start {
|
||||
pub(crate) fn from_hashmap(
|
||||
mut attrs: HashMap<String, QuotedOrUnquoted>,
|
||||
) -> Result<Start, String> {
|
||||
) -> Result<Self, 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)))
|
||||
.ok_or_else(|| String::from("EXT-X-START without mandatory TIME-OFFSET attribute"))?;
|
||||
Ok(Start {
|
||||
Ok(Self {
|
||||
time_offset,
|
||||
precise: is_yes!(attrs, "PRECISE").into(),
|
||||
other_attributes: attrs,
|
||||
|
|
50
tests/lib.rs
50
tests/lib.rs
|
@ -8,6 +8,7 @@ use std::collections::HashMap;
|
|||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path;
|
||||
use std::sync::atomic::Ordering;
|
||||
use std::{fs, io};
|
||||
|
||||
fn all_sample_m3u_playlists() -> Vec<path::PathBuf> {
|
||||
|
@ -198,6 +199,36 @@ fn create_and_parse_master_playlist_empty() {
|
|||
assert_eq!(playlist_original, playlist_parsed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_segment_float_inf() {
|
||||
let playlist = Playlist::MediaPlaylist(MediaPlaylist {
|
||||
version: Some(6),
|
||||
target_duration: 3,
|
||||
media_sequence: 338559,
|
||||
discontinuity_sequence: 1234,
|
||||
end_list: true,
|
||||
playlist_type: Some(MediaPlaylistType::Vod),
|
||||
segments: vec![MediaSegment {
|
||||
uri: "20140311T113819-01-338559live.ts".into(),
|
||||
duration: 2.000f32,
|
||||
title: Some("title".into()),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let mut v: Vec<u8> = Vec::new();
|
||||
playlist.write_to(&mut v).unwrap();
|
||||
let m3u8_str: &str = std::str::from_utf8(&v).unwrap();
|
||||
assert!(m3u8_str.contains("#EXTINF:2,title"));
|
||||
|
||||
WRITE_OPT_FLOAT_PRECISION.store(5, Ordering::Relaxed);
|
||||
|
||||
playlist.write_to(&mut v).unwrap();
|
||||
let m3u8_str: &str = std::str::from_utf8(&v).unwrap();
|
||||
assert!(m3u8_str.contains("#EXTINF:2.00000,title"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_and_parse_master_playlist_full() {
|
||||
let mut playlist_original = Playlist::MasterPlaylist(MasterPlaylist {
|
||||
|
@ -304,7 +335,7 @@ fn create_and_parse_media_playlist_empty() {
|
|||
#[test]
|
||||
fn create_and_parse_media_playlist_single_segment() {
|
||||
let mut playlist_original = Playlist::MediaPlaylist(MediaPlaylist {
|
||||
target_duration: 2.0,
|
||||
target_duration: 2,
|
||||
segments: vec![MediaSegment {
|
||||
uri: "20140311T113819-01-338559live.ts".into(),
|
||||
duration: 2.002,
|
||||
|
@ -321,7 +352,7 @@ fn create_and_parse_media_playlist_single_segment() {
|
|||
fn create_and_parse_media_playlist_full() {
|
||||
let mut playlist_original = Playlist::MediaPlaylist(MediaPlaylist {
|
||||
version: Some(4),
|
||||
target_duration: 3.0,
|
||||
target_duration: 3,
|
||||
media_sequence: 338559,
|
||||
discontinuity_sequence: 1234,
|
||||
end_list: true,
|
||||
|
@ -358,16 +389,18 @@ fn create_and_parse_media_playlist_full() {
|
|||
other_attributes: Default::default(),
|
||||
}),
|
||||
program_date_time: Some(
|
||||
chrono::FixedOffset::east(8 * 3600)
|
||||
.ymd(2010, 2, 19)
|
||||
.and_hms_milli(14, 54, 23, 31),
|
||||
chrono::FixedOffset::east_opt(8 * 3600)
|
||||
.unwrap()
|
||||
.with_ymd_and_hms(2010, 2, 19, 14, 54, 23)
|
||||
.unwrap(),
|
||||
),
|
||||
daterange: Some(DateRange {
|
||||
id: "9999".into(),
|
||||
class: Some("class".into()),
|
||||
start_date: chrono::FixedOffset::east(8 * 3600)
|
||||
.ymd(2010, 2, 19)
|
||||
.and_hms_milli(14, 54, 23, 31),
|
||||
start_date: chrono::FixedOffset::east_opt(8 * 3600)
|
||||
.unwrap()
|
||||
.with_ymd_and_hms(2010, 2, 19, 14, 54, 23)
|
||||
.unwrap(),
|
||||
end_date: None,
|
||||
duration: None,
|
||||
planned_duration: Some("40.000".parse().unwrap()),
|
||||
|
@ -382,6 +415,7 @@ fn create_and_parse_media_playlist_full() {
|
|||
tag: "X-CUE-OUT".into(),
|
||||
rest: Some("DURATION=2.002".into()),
|
||||
}],
|
||||
..Default::default()
|
||||
}],
|
||||
unknown_tags: vec![],
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue