Compare commits

...

10 commits

Author SHA1 Message Date
Vadim Getmanshchuk 25e0daadcf
Merge b7c2cb023d into 381ac7732f 2024-02-17 16:42:21 +03:00
rutgersc 381ac7732f
Merge pull request #72 from ant1eicher/master
Support AWS Elemental MediaConvert decimal format.
2024-02-14 18:59:35 +01:00
Anton Eicher e3b6390186 EXTINF tags need to be in floating-point format to work with AWS Elemental MediaConvert
AWS Elemental MediaConvert rejects playlists with EXTINF tags that are not in floating point format. When m3u8 MediaSegment self.duration is an exact number without trailing decimals, writeln cuts off the decimal places and prints it like an integer.

This change adds support for fixed length floating point numbers.
2024-02-14 16:29:47 +02:00
rutgersc 7f322675eb
Merge pull request #73 from rutgersc/fix/targetduration
#EXT-X-TARGETDURATION:<s> is supposed to be a decimal-integer
2024-01-30 20:57:38 +01:00
Rutger Schoorstra c5cceeb4f6 Update version to 6.0.0 2024-01-26 18:56:34 +01:00
Rutger Schoorstra 5109753b96 #EXT-X-TARGETDURATION:<s> is supposed to be a decimal-integer
https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.3.1
2024-01-26 18:55:39 +01:00
Vadim Getmanshchuk b7c2cb023d reverted the feature statement, meant to be a not() 2023-02-14 11:43:51 -06:00
Vadim Getmanshchuk 663e0607cf Added "lenient" feature 2023-02-14 11:21:53 -06:00
Vadim Getmanshchuk 4120e1c557 Implemented some clippy recommendations 2023-02-12 14:28:14 -08:00
Vadim Getmanshchuk b84da46e0a retrofited tests for new Crono 2023-02-10 10:29:03 -08:00
6 changed files with 221 additions and 115 deletions

View file

@ -1,6 +1,6 @@
[package] [package]
name = "m3u8-rs" name = "m3u8-rs"
version = "5.0.5" version = "6.0.0"
authors = ["Rutger"] authors = ["Rutger"]
readme = "README.md" readme = "README.md"
repository = "https://github.com/rutgersc/m3u8-rs" repository = "https://github.com/rutgersc/m3u8-rs"
@ -16,4 +16,5 @@ chrono = { version = "0.4", default-features = false, features = [ "std" ] }
[features] [features]
default = ["parser"] default = ["parser"]
parser = ["nom"] parser = ["nom"]
lenient = []

View 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

View file

@ -42,7 +42,7 @@
//! //!
//! let playlist = MediaPlaylist { //! let playlist = MediaPlaylist {
//! version: Some(6), //! version: Some(6),
//! target_duration: 3.0, //! target_duration: 3,
//! media_sequence: 338559, //! media_sequence: 338559,
//! discontinuity_sequence: 1234, //! discontinuity_sequence: 1234,
//! end_list: true, //! end_list: true,
@ -64,6 +64,32 @@
//! //let mut file = std::fs::File::open("playlist.m3u8").unwrap(); //! //let mut file = std::fs::File::open("playlist.m3u8").unwrap();
//! //playlist.write_to(&mut file).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; mod playlist;
pub use playlist::*; pub use playlist::*;

View file

@ -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. /// 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 /// # Examples
@ -128,7 +128,7 @@ pub fn parse_media_playlist_res(
/// When a media tag or no master tag is found, this returns false. /// When a media tag or no master tag is found, this returns false.
pub fn is_master_playlist(input: &[u8]) -> bool { pub fn is_master_playlist(input: &[u8]) -> bool {
// Assume it's not a master playlist // 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. /// 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 { enum MediaPlaylistTag {
Version(usize), Version(usize),
Segment(SegmentTag), Segment(SegmentTag),
TargetDuration(f32), TargetDuration(u64),
MediaSequence(u64), MediaSequence(u64),
DiscontinuitySequence(u64), DiscontinuitySequence(u64),
EndList, EndList,
@ -361,7 +361,7 @@ fn media_playlist_tag(i: &[u8]) -> IResult<&[u8], MediaPlaylistTag> {
alt(( alt((
map(version_tag, MediaPlaylistTag::Version), map(version_tag, MediaPlaylistTag::Version),
map( map(
pair(tag("#EXT-X-TARGETDURATION:"), float), pair(tag("#EXT-X-TARGETDURATION:"), number),
|(_, duration)| MediaPlaylistTag::TargetDuration(duration), |(_, duration)| MediaPlaylistTag::TargetDuration(duration),
), ),
map( map(
@ -642,28 +642,27 @@ pub enum QuotedOrUnquoted {
impl Default for QuotedOrUnquoted { impl Default for QuotedOrUnquoted {
fn default() -> Self { fn default() -> Self {
QuotedOrUnquoted::Quoted(String::new()) Self::Quoted(String::new())
} }
} }
impl QuotedOrUnquoted { impl QuotedOrUnquoted {
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
match self { match self {
QuotedOrUnquoted::Quoted(s) => s.as_str(), Self::Quoted(s) | Self::Unquoted(s) => s.as_str(),
QuotedOrUnquoted::Unquoted(s) => s.as_str(),
} }
} }
pub fn as_unquoted(&self) -> Option<&str> { pub fn as_unquoted(&self) -> Option<&str> {
match self { match self {
QuotedOrUnquoted::Unquoted(s) => Some(s.as_str()), Self::Unquoted(s) => Some(s.as_str()),
_ => None, _ => None,
} }
} }
pub fn as_quoted(&self) -> Option<&str> { pub fn as_quoted(&self) -> Option<&str> {
match self { match self {
QuotedOrUnquoted::Quoted(s) => Some(s.as_str()), Self::Quoted(s) => Some(s.as_str()),
_ => None, _ => None,
} }
} }
@ -672,22 +671,22 @@ impl QuotedOrUnquoted {
impl From<&str> for QuotedOrUnquoted { impl From<&str> for QuotedOrUnquoted {
fn from(s: &str) -> Self { fn from(s: &str) -> Self {
if s.starts_with('"') && s.ends_with('"') { if s.starts_with('"') && s.ends_with('"') {
return QuotedOrUnquoted::Quoted( return Self::Quoted(
s.strip_prefix('"') s.strip_prefix('"')
.and_then(|s| s.strip_suffix('"')) .and_then(|s| s.strip_suffix('"'))
.unwrap_or_default() .unwrap_or_default()
.to_string(), .to_string(),
); );
} }
QuotedOrUnquoted::Unquoted(s.to_string()) Self::Unquoted(s.to_string())
} }
} }
impl Display for QuotedOrUnquoted { impl Display for QuotedOrUnquoted {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self { match self {
QuotedOrUnquoted::Unquoted(s) => write!(f, "{}", s), Self::Unquoted(s) => write!(f, "{}", s),
QuotedOrUnquoted::Quoted(u) => write!(f, "\"{}\"", u), Self::Quoted(u) => write!(f, "\"{}\"", u),
} }
} }
} }
@ -747,18 +746,20 @@ fn float(i: &[u8]) -> IResult<&[u8], f32> {
take_while1(is_digit), take_while1(is_digit),
opt(preceded(char('.'), take_while1(is_digit))), opt(preceded(char('.'), take_while1(is_digit))),
), ),
|(left, right): (&[u8], Option<&[u8]>)| match right { |(left, right): (&[u8], Option<&[u8]>)| {
Some(right) => { right.map_or_else(
let n = &i[..(left.len() + right.len() + 1)]; || {
// Can't fail because we validated it above already // Can't fail because we validated it above already
let n = str::from_utf8(n).unwrap(); let left = str::from_utf8(left).unwrap();
n.parse() left.parse()
} },
None => { |right| {
// Can't fail because we validated it above already let n = &i[..=(left.len() + right.len())];
let left = str::from_utf8(left).unwrap(); // Can't fail because we validated it above already
left.parse() let n = str::from_utf8(n).unwrap();
} n.parse()
},
)
}, },
)(i) )(i)
} }

View file

@ -4,13 +4,19 @@
//! Which is either a `MasterPlaylist` or a `MediaPlaylist`. //! Which is either a `MasterPlaylist` or a `MediaPlaylist`.
use crate::QuotedOrUnquoted; use crate::QuotedOrUnquoted;
use chrono::DateTime;
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::{TryFrom, TryInto}; use std::convert::{TryFrom, TryInto};
use std::f32;
use std::fmt; use std::fmt;
use std::fmt::Display; use std::fmt::Display;
use std::io::Write; use std::io::Write;
use std::str::FromStr; 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 { macro_rules! write_some_attribute_quoted {
($w:expr, $tag:expr, $o:expr) => { ($w:expr, $tag:expr, $o:expr) => {
@ -152,8 +158,8 @@ pub enum Playlist {
impl Playlist { impl Playlist {
pub fn write_to<T: Write>(&self, writer: &mut T) -> std::io::Result<()> { pub fn write_to<T: Write>(&self, writer: &mut T) -> std::io::Result<()> {
match *self { match *self {
Playlist::MasterPlaylist(ref pl) => pl.write_to(writer), Self::MasterPlaylist(ref pl) => pl.write_to(writer),
Playlist::MediaPlaylist(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( pub(crate) fn from_hashmap(
mut attrs: HashMap<String, QuotedOrUnquoted>, mut attrs: HashMap<String, QuotedOrUnquoted>,
is_i_frame: bool, is_i_frame: bool,
) -> Result<VariantStream, String> { ) -> Result<Self, String> {
let uri = quoted_string!(attrs, "URI").unwrap_or_default(); let uri = quoted_string!(attrs, "URI").unwrap_or_default();
// TODO: keep in attrs if parsing optional attributes fails // TODO: keep in attrs if parsing optional attributes fails
let bandwidth = unquoted_string_parse!(attrs, "BANDWIDTH", |s: &str| s let bandwidth = unquoted_string_parse!(attrs, "BANDWIDTH", |s: &str| s
@ -275,11 +281,11 @@ impl VariantStream {
let subtitles = quoted_string!(attrs, "SUBTITLES"); let subtitles = quoted_string!(attrs, "SUBTITLES");
let closed_captions = attrs let closed_captions = attrs
.remove("CLOSED-CAPTIONS") .remove("CLOSED-CAPTIONS")
.map(|c| c.try_into()) .map(TryInto::try_into)
.transpose()?; .transpose()?;
let other_attributes = if attrs.is_empty() { None } else { Some(attrs) }; let other_attributes = if attrs.is_empty() { None } else { Some(attrs) };
Ok(VariantStream { Ok(Self {
is_i_frame, is_i_frame,
uri, uri,
bandwidth, bandwidth,
@ -346,7 +352,7 @@ impl Display for Resolution {
impl FromStr for Resolution { impl FromStr for Resolution {
type Err = String; type Err = String;
fn from_str(s: &str) -> Result<Resolution, String> { fn from_str(s: &str) -> Result<Self, String> {
match s.split_once('x') { match s.split_once('x') {
Some((width, height)) => { Some((width, height)) => {
let width = width let width = width
@ -355,7 +361,7 @@ impl FromStr for Resolution {
let height = height let height = height
.parse::<u64>() .parse::<u64>()
.map_err(|err| format!("Can't parse RESOLUTION attribute height: {}", err))?; .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")), None => Err(String::from("Invalid RESOLUTION attribute")),
} }
@ -373,12 +379,12 @@ pub enum HDCPLevel {
impl FromStr for HDCPLevel { impl FromStr for HDCPLevel {
type Err = String; type Err = String;
fn from_str(s: &str) -> Result<HDCPLevel, String> { fn from_str(s: &str) -> Result<Self, String> {
match s { match s {
"TYPE-0" => Ok(HDCPLevel::Type0), "TYPE-0" => Ok(Self::Type0),
"TYPE-1" => Ok(HDCPLevel::Type1), "TYPE-1" => Ok(Self::Type1),
"NONE" => Ok(HDCPLevel::None), "NONE" => Ok(Self::None),
_ => Ok(HDCPLevel::Other(String::from(s))), _ => Ok(Self::Other(String::from(s))),
} }
} }
} }
@ -389,10 +395,10 @@ impl Display for HDCPLevel {
f, f,
"{}", "{}",
match self { match self {
HDCPLevel::Type0 => "TYPE-0", Self::Type0 => "TYPE-0",
HDCPLevel::Type1 => "TYPE-1", Self::Type1 => "TYPE-1",
HDCPLevel::None => "NONE", Self::None => "NONE",
HDCPLevel::Other(s) => s, Self::Other(s) => s,
} }
) )
} }
@ -409,11 +415,11 @@ pub enum ClosedCaptionGroupId {
impl TryFrom<QuotedOrUnquoted> for ClosedCaptionGroupId { impl TryFrom<QuotedOrUnquoted> for ClosedCaptionGroupId {
type Error = String; type Error = String;
fn try_from(s: QuotedOrUnquoted) -> Result<ClosedCaptionGroupId, String> { fn try_from(s: QuotedOrUnquoted) -> Result<Self, String> {
match s { match s {
QuotedOrUnquoted::Unquoted(s) if s == "NONE" => Ok(ClosedCaptionGroupId::None), QuotedOrUnquoted::Unquoted(s) if s == "NONE" => Ok(Self::None),
QuotedOrUnquoted::Unquoted(s) => Ok(ClosedCaptionGroupId::Other(s)), QuotedOrUnquoted::Unquoted(s) => Ok(Self::Other(s)),
QuotedOrUnquoted::Quoted(s) => Ok(ClosedCaptionGroupId::GroupId(s)), QuotedOrUnquoted::Quoted(s) => Ok(Self::GroupId(s)),
} }
} }
} }
@ -447,7 +453,7 @@ pub struct AlternativeMedia {
impl AlternativeMedia { impl AlternativeMedia {
pub(crate) fn from_hashmap( pub(crate) fn from_hashmap(
mut attrs: HashMap<String, QuotedOrUnquoted>, mut attrs: HashMap<String, QuotedOrUnquoted>,
) -> Result<AlternativeMedia, String> { ) -> Result<Self, String> {
let media_type = unquoted_string_parse!(attrs, "TYPE") let media_type = unquoted_string_parse!(attrs, "TYPE")
.ok_or_else(|| String::from("EXT-X-MEDIA without mandatory TYPE attribute"))?; .ok_or_else(|| String::from("EXT-X-MEDIA without mandatory TYPE attribute"))?;
let uri = quoted_string!(attrs, "URI"); let uri = quoted_string!(attrs, "URI");
@ -467,6 +473,7 @@ impl AlternativeMedia {
let default = is_yes!(attrs, "DEFAULT"); let default = is_yes!(attrs, "DEFAULT");
let autoselect = is_yes!(attrs, "AUTOSELECT"); let autoselect = is_yes!(attrs, "AUTOSELECT");
#[cfg(not(feature = "lenient"))]
if media_type != AlternativeMediaType::Subtitles && attrs.contains_key("FORCED") { if media_type != AlternativeMediaType::Subtitles && attrs.contains_key("FORCED") {
return Err(String::from( return Err(String::from(
"FORCED attribute must not be included in non-SUBTITLE Alternative Medias", "FORCED attribute must not be included in non-SUBTITLE Alternative Medias",
@ -488,7 +495,7 @@ impl AlternativeMedia {
let channels = quoted_string!(attrs, "CHANNELS"); let channels = quoted_string!(attrs, "CHANNELS");
let other_attributes = if attrs.is_empty() { None } else { Some(attrs) }; let other_attributes = if attrs.is_empty() { None } else { Some(attrs) };
Ok(AlternativeMedia { Ok(Self {
media_type, media_type,
uri, uri,
group_id, group_id,
@ -547,20 +554,20 @@ pub enum AlternativeMediaType {
impl FromStr for AlternativeMediaType { impl FromStr for AlternativeMediaType {
type Err = String; type Err = String;
fn from_str(s: &str) -> Result<AlternativeMediaType, String> { fn from_str(s: &str) -> Result<Self, String> {
match s { match s {
"AUDIO" => Ok(AlternativeMediaType::Audio), "AUDIO" => Ok(Self::Audio),
"VIDEO" => Ok(AlternativeMediaType::Video), "VIDEO" => Ok(Self::Video),
"SUBTITLES" => Ok(AlternativeMediaType::Subtitles), "SUBTITLES" => Ok(Self::Subtitles),
"CLOSED-CAPTIONS" => Ok(AlternativeMediaType::ClosedCaptions), "CLOSED-CAPTIONS" => Ok(Self::ClosedCaptions),
_ => Ok(AlternativeMediaType::Other(String::from(s))), _ => Ok(Self::Other(String::from(s))),
} }
} }
} }
impl Default for AlternativeMediaType { impl Default for AlternativeMediaType {
fn default() -> AlternativeMediaType { fn default() -> Self {
AlternativeMediaType::Video Self::Video
} }
} }
@ -570,11 +577,11 @@ impl Display for AlternativeMediaType {
f, f,
"{}", "{}",
match self { match self {
AlternativeMediaType::Audio => "AUDIO", Self::Audio => "AUDIO",
AlternativeMediaType::Video => "VIDEO", Self::Video => "VIDEO",
AlternativeMediaType::Subtitles => "SUBTITLES", Self::Subtitles => "SUBTITLES",
AlternativeMediaType::ClosedCaptions => "CLOSED-CAPTIONS", Self::ClosedCaptions => "CLOSED-CAPTIONS",
AlternativeMediaType::Other(s) => s.as_str(), Self::Other(s) => s.as_str(),
} }
) )
} }
@ -590,19 +597,19 @@ pub enum InstreamId {
impl FromStr for InstreamId { impl FromStr for InstreamId {
type Err = String; 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") { if let Some(cc) = s.strip_prefix("CC") {
let cc = cc let cc = cc
.parse::<u8>() .parse::<u8>()
.map_err(|err| format!("Unable to create InstreamId from {:?}: {}", s, err))?; .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") { } else if let Some(service) = s.strip_prefix("SERVICE") {
let service = service let service = service
.parse::<u8>() .parse::<u8>()
.map_err(|err| format!("Unable to create InstreamId from {:?}: {}", s, err))?; .map_err(|err| format!("Unable to create InstreamId from {:?}: {}", s, err))?;
Ok(InstreamId::Service(service)) Ok(Self::Service(service))
} else { } 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 { impl Display for InstreamId {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self { match self {
InstreamId::CC(cc) => write!(f, "CC{}", cc), Self::CC(cc) => write!(f, "CC{}", cc),
InstreamId::Service(service) => write!(f, "SERVICE{}", service), Self::Service(service) => write!(f, "SERVICE{}", service),
InstreamId::Other(s) => write!(f, "{}", s), Self::Other(s) => write!(f, "{}", s),
} }
} }
} }
@ -652,7 +659,7 @@ pub struct SessionData {
impl SessionData { impl SessionData {
pub(crate) fn from_hashmap( pub(crate) fn from_hashmap(
mut attrs: HashMap<String, QuotedOrUnquoted>, mut attrs: HashMap<String, QuotedOrUnquoted>,
) -> Result<SessionData, String> { ) -> Result<Self, String> {
let data_id = quoted_string!(attrs, "DATA-ID") let data_id = quoted_string!(attrs, "DATA-ID")
.ok_or_else(|| String::from("EXT-X-SESSION-DATA field without DATA-ID attribute"))?; .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), (Some(value), None) => SessionDataField::Value(value),
(None, Some(uri)) => SessionDataField::Uri(uri), (None, Some(uri)) => SessionDataField::Uri(uri),
(Some(_), Some(_)) => { (Some(_), Some(_)) => {
return Err(format![ return Err(format!(
"EXT-X-SESSION-DATA tag {} contains both a value and an URI", "EXT-X-SESSION-DATA tag {} contains both a value and an URI",
data_id data_id
]) ))
} }
(None, None) => { (None, None) => {
return Err(format![ return Err(format!(
"EXT-X-SESSION-DATA tag {} must contain either a value or an URI", "EXT-X-SESSION-DATA tag {} must contain either a value or an URI",
data_id data_id
]) ))
} }
}; };
let language = quoted_string!(attrs, "LANGUAGE"); let language = quoted_string!(attrs, "LANGUAGE");
let other_attributes = if attrs.is_empty() { None } else { Some(attrs) }; let other_attributes = if attrs.is_empty() { None } else { Some(attrs) };
Ok(SessionData { Ok(Self {
data_id, data_id,
field, field,
language, language,
@ -713,7 +720,7 @@ impl SessionData {
pub struct MediaPlaylist { pub struct MediaPlaylist {
pub version: Option<usize>, pub version: Option<usize>,
/// `#EXT-X-TARGETDURATION:<s>` /// `#EXT-X-TARGETDURATION:<s>`
pub target_duration: f32, pub target_duration: u64,
/// `#EXT-X-MEDIA-SEQUENCE:<number>` /// `#EXT-X-MEDIA-SEQUENCE:<number>`
pub media_sequence: u64, pub media_sequence: u64,
pub segments: Vec<MediaSegment>, pub segments: Vec<MediaSegment>,
@ -786,11 +793,11 @@ pub enum MediaPlaylistType {
impl FromStr for MediaPlaylistType { impl FromStr for MediaPlaylistType {
type Err = String; type Err = String;
fn from_str(s: &str) -> Result<MediaPlaylistType, String> { fn from_str(s: &str) -> Result<Self, String> {
match s { match s {
"EVENT" => Ok(MediaPlaylistType::Event), "EVENT" => Ok(Self::Event),
"VOD" => Ok(MediaPlaylistType::Vod), "VOD" => Ok(Self::Vod),
_ => Ok(MediaPlaylistType::Other(String::from(s))), _ => Ok(Self::Other(String::from(s))),
} }
} }
} }
@ -801,17 +808,17 @@ impl Display for MediaPlaylistType {
f, f,
"{}", "{}",
match self { match self {
MediaPlaylistType::Event => "EVENT", Self::Event => "EVENT",
MediaPlaylistType::Vod => "VOD", Self::Vod => "VOD",
MediaPlaylistType::Other(s) => s, Self::Other(s) => s,
} }
) )
} }
} }
impl Default for MediaPlaylistType { impl Default for MediaPlaylistType {
fn default() -> MediaPlaylistType { fn default() -> Self {
MediaPlaylistType::Event Self::Event
} }
} }
@ -845,8 +852,8 @@ pub struct MediaSegment {
} }
impl MediaSegment { impl MediaSegment {
pub fn empty() -> MediaSegment { pub fn empty() -> Self {
Default::default() Self::default()
} }
pub(crate) fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> { pub(crate) fn write_to<T: Write>(&self, w: &mut T) -> std::io::Result<()> {
@ -884,7 +891,14 @@ impl MediaSegment {
writeln!(w, "{}", unknown_tag)?; 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 { if let Some(ref v) = self.title {
writeln!(w, "{}", v)?; writeln!(w, "{}", v)?;
@ -906,19 +920,19 @@ pub enum KeyMethod {
impl Default for KeyMethod { impl Default for KeyMethod {
fn default() -> Self { fn default() -> Self {
KeyMethod::None Self::None
} }
} }
impl FromStr for KeyMethod { impl FromStr for KeyMethod {
type Err = String; type Err = String;
fn from_str(s: &str) -> Result<KeyMethod, String> { fn from_str(s: &str) -> Result<Self, String> {
match s { match s {
"NONE" => Ok(KeyMethod::None), "NONE" => Ok(Self::None),
"AES-128" => Ok(KeyMethod::AES128), "AES-128" => Ok(Self::AES128),
"SAMPLE-AES" => Ok(KeyMethod::SampleAES), "SAMPLE-AES" => Ok(Self::SampleAES),
_ => Ok(KeyMethod::Other(String::from(s))), _ => Ok(Self::Other(String::from(s))),
} }
} }
} }
@ -929,10 +943,10 @@ impl Display for KeyMethod {
f, f,
"{}", "{}",
match self { match self {
KeyMethod::None => "NONE", Self::None => "NONE",
KeyMethod::AES128 => "AES-128", Self::AES128 => "AES-128",
KeyMethod::SampleAES => "SAMPLE-AES", Self::SampleAES => "SAMPLE-AES",
KeyMethod::Other(s) => s, Self::Other(s) => s,
} }
) )
} }
@ -958,7 +972,7 @@ pub struct Key {
impl Key { impl Key {
pub(crate) fn from_hashmap( pub(crate) fn from_hashmap(
mut attrs: HashMap<String, QuotedOrUnquoted>, mut attrs: HashMap<String, QuotedOrUnquoted>,
) -> Result<Key, String> { ) -> Result<Self, String> {
let method: KeyMethod = unquoted_string_parse!(attrs, "METHOD") let method: KeyMethod = unquoted_string_parse!(attrs, "METHOD")
.ok_or_else(|| String::from("EXT-X-KEY without mandatory METHOD attribute"))?; .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 keyformat = quoted_string!(attrs, "KEYFORMAT");
let keyformatversions = quoted_string!(attrs, "KEYFORMATVERSIONS"); let keyformatversions = quoted_string!(attrs, "KEYFORMATVERSIONS");
Ok(Key { Ok(Self {
method, method,
uri, uri,
iv, iv,
@ -1055,7 +1069,7 @@ pub struct DateRange {
} }
impl 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") let id = quoted_string!(attrs, "ID")
.ok_or_else(|| String::from("EXT-X-DATERANGE without mandatory ID attribute"))?; .ok_or_else(|| String::from("EXT-X-DATERANGE without mandatory ID attribute"))?;
let class = quoted_string!(attrs, "CLASS"); let class = quoted_string!(attrs, "CLASS");
@ -1075,7 +1089,7 @@ impl DateRange {
let end_on_next = is_yes!(attrs, "END-ON-NEXT"); let end_on_next = is_yes!(attrs, "END-ON-NEXT");
let mut x_prefixed = HashMap::new(); let mut x_prefixed = HashMap::new();
let mut other_attributes = 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-") { if k.starts_with("X-") {
x_prefixed.insert(k, v); x_prefixed.insert(k, v);
} else { } else {
@ -1083,7 +1097,7 @@ impl DateRange {
} }
} }
Ok(DateRange { Ok(Self {
id, id,
class, class,
start_date, start_date,
@ -1111,7 +1125,7 @@ impl DateRange {
write_some_attribute_quoted!( write_some_attribute_quoted!(
w, w,
",END-DATE", ",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, ",DURATION", &self.duration)?;
write_some_attribute!(w, ",PLANNED-DURATION", &self.planned_duration)?; write_some_attribute!(w, ",PLANNED-DURATION", &self.planned_duration)?;
@ -1151,12 +1165,12 @@ pub struct Start {
impl Start { impl Start {
pub(crate) fn from_hashmap( pub(crate) fn from_hashmap(
mut attrs: HashMap<String, QuotedOrUnquoted>, mut attrs: HashMap<String, QuotedOrUnquoted>,
) -> Result<Start, String> { ) -> Result<Self, String> {
let time_offset = unquoted_string_parse!(attrs, "TIME-OFFSET", |s: &str| s let time_offset = unquoted_string_parse!(attrs, "TIME-OFFSET", |s: &str| s
.parse::<f64>() .parse::<f64>()
.map_err(|err| format!("Failed to parse TIME-OFFSET attribute: {}", err))) .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_or_else(|| String::from("EXT-X-START without mandatory TIME-OFFSET attribute"))?;
Ok(Start { Ok(Self {
time_offset, time_offset,
precise: is_yes!(attrs, "PRECISE").into(), precise: is_yes!(attrs, "PRECISE").into(),
other_attributes: attrs, other_attributes: attrs,

View file

@ -8,6 +8,7 @@ use std::collections::HashMap;
use std::fs::File; use std::fs::File;
use std::io::Read; use std::io::Read;
use std::path; use std::path;
use std::sync::atomic::Ordering;
use std::{fs, io}; use std::{fs, io};
fn all_sample_m3u_playlists() -> Vec<path::PathBuf> { 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); 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] #[test]
fn create_and_parse_master_playlist_full() { fn create_and_parse_master_playlist_full() {
let mut playlist_original = Playlist::MasterPlaylist(MasterPlaylist { let mut playlist_original = Playlist::MasterPlaylist(MasterPlaylist {
@ -304,7 +335,7 @@ fn create_and_parse_media_playlist_empty() {
#[test] #[test]
fn create_and_parse_media_playlist_single_segment() { fn create_and_parse_media_playlist_single_segment() {
let mut playlist_original = Playlist::MediaPlaylist(MediaPlaylist { let mut playlist_original = Playlist::MediaPlaylist(MediaPlaylist {
target_duration: 2.0, target_duration: 2,
segments: vec![MediaSegment { segments: vec![MediaSegment {
uri: "20140311T113819-01-338559live.ts".into(), uri: "20140311T113819-01-338559live.ts".into(),
duration: 2.002, duration: 2.002,
@ -321,7 +352,7 @@ fn create_and_parse_media_playlist_single_segment() {
fn create_and_parse_media_playlist_full() { fn create_and_parse_media_playlist_full() {
let mut playlist_original = Playlist::MediaPlaylist(MediaPlaylist { let mut playlist_original = Playlist::MediaPlaylist(MediaPlaylist {
version: Some(4), version: Some(4),
target_duration: 3.0, target_duration: 3,
media_sequence: 338559, media_sequence: 338559,
discontinuity_sequence: 1234, discontinuity_sequence: 1234,
end_list: true, end_list: true,
@ -358,16 +389,18 @@ fn create_and_parse_media_playlist_full() {
other_attributes: Default::default(), other_attributes: Default::default(),
}), }),
program_date_time: Some( program_date_time: Some(
chrono::FixedOffset::east(8 * 3600) chrono::FixedOffset::east_opt(8 * 3600)
.ymd(2010, 2, 19) .unwrap()
.and_hms_milli(14, 54, 23, 31), .with_ymd_and_hms(2010, 2, 19, 14, 54, 23)
.unwrap(),
), ),
daterange: Some(DateRange { daterange: Some(DateRange {
id: "9999".into(), id: "9999".into(),
class: Some("class".into()), class: Some("class".into()),
start_date: chrono::FixedOffset::east(8 * 3600) start_date: chrono::FixedOffset::east_opt(8 * 3600)
.ymd(2010, 2, 19) .unwrap()
.and_hms_milli(14, 54, 23, 31), .with_ymd_and_hms(2010, 2, 19, 14, 54, 23)
.unwrap(),
end_date: None, end_date: None,
duration: None, duration: None,
planned_duration: Some("40.000".parse().unwrap()), planned_duration: Some("40.000".parse().unwrap()),
@ -382,6 +415,7 @@ fn create_and_parse_media_playlist_full() {
tag: "X-CUE-OUT".into(), tag: "X-CUE-OUT".into(),
rest: Some("DURATION=2.002".into()), rest: Some("DURATION=2.002".into()),
}], }],
..Default::default()
}], }],
unknown_tags: vec![], unknown_tags: vec![],
}); });