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]
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 = []

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 {
//! 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::*;

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.
///
/// # 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)
}

View file

@ -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,

View file

@ -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![],
});