Compare commits

...

13 commits

Author SHA1 Message Date
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
Sebastian Dröge 487d63da4d Udpate version to 5.0.5 2023-12-05 08:33:58 +02:00
Vadim Getmanshchuk f6af8acbfe ran rustfmt 2023-12-04 21:25:16 +02:00
Vadim Getmanshchuk 46622345d1 added test 2023-12-04 21:25:16 +02:00
Vadim Getmanshchuk d8e0283ddb allowing empty comments 2023-12-04 21:25:16 +02:00
Sebastian Dröge b9cf88b7ec Update version to 5.0.4 2023-05-08 10:24:06 +03:00
clitic a1970192ff Write BYTERANGE inside quotes 2023-04-12 10:39:44 +03:00
clitic ae31a2741f Parse #EXT-X-MAP BYTERANGE attr from quoted string 2023-04-12 10:39:44 +03:00
mmason e7a6cf943c allow millisecond accuracy for EXT-X-PROGRAM-DATE-TIME 2023-04-11 18:05:54 +03:00
6 changed files with 135 additions and 22 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "m3u8-rs"
version = "5.0.3"
version = "6.0.0"
authors = ["Rutger"]
readme = "README.md"
repository = "https://github.com/rutgersc/m3u8-rs"

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

@ -218,7 +218,7 @@ enum MasterPlaylistTag {
SessionKey(SessionKey),
Start(Start),
IndependentSegments,
Comment(String),
Comment(Option<String>),
Uri(String),
Unknown(ExtTag),
}
@ -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(
@ -487,7 +487,7 @@ enum SegmentTag {
ProgramDateTime(chrono::DateTime<chrono::FixedOffset>),
DateRange(DateRange),
Unknown(ExtTag),
Comment(String),
Comment(Option<String>),
Uri(String),
}
@ -555,13 +555,16 @@ fn extmap(i: &[u8]) -> IResult<&[u8], Map> {
}
None => Err("URI is empty"),
}?;
let byte_range = attrs
.remove("BYTERANGE")
.map(|range| match byte_range_val(range.to_string().as_bytes()) {
IResult::Ok((_, range)) => Ok(range),
let byte_range = match attrs.remove("BYTERANGE") {
Some(QuotedOrUnquoted::Quoted(s)) => match byte_range_val(s.as_bytes()) {
IResult::Ok((_, range)) => Ok(Some(range)),
IResult::Err(_) => Err("Invalid byte range"),
})
.transpose()?;
},
Some(QuotedOrUnquoted::Unquoted(_)) => {
Err("Can't create BYTERANGE attribute from unquoted string")
}
None => Ok(None),
}?;
Ok(Map {
uri,
@ -606,10 +609,10 @@ fn ext_tag(i: &[u8]) -> IResult<&[u8], ExtTag> {
)(i)
}
fn comment_tag(i: &[u8]) -> IResult<&[u8], String> {
fn comment_tag(i: &[u8]) -> IResult<&[u8], Option<String>> {
map(
pair(
preceded(char('#'), map_res(is_not("\r\n"), from_utf8_slice)),
preceded(char('#'), opt(map_res(is_not("\r\n"), from_utf8_slice))),
take(1usize),
),
|(text, _)| text,
@ -926,10 +929,15 @@ mod tests {
fn comment() {
assert_eq!(
comment_tag(b"#Hello\nxxx"),
Result::Ok(("xxx".as_bytes(), "Hello".to_string()))
Result::Ok(("xxx".as_bytes(), Some("Hello".to_string())))
);
}
#[test]
fn empty_comment() {
assert_eq!(comment_tag(b"#\nxxx"), Result::Ok(("xxx".as_bytes(), None)));
}
#[test]
fn quotes() {
assert_eq!(

View file

@ -6,11 +6,16 @@
use crate::QuotedOrUnquoted;
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) => {
@ -713,7 +718,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>,
@ -869,7 +874,11 @@ impl MediaSegment {
writeln!(w)?;
}
if let Some(ref v) = self.program_date_time {
writeln!(w, "#EXT-X-PROGRAM-DATE-TIME:{}", v.to_rfc3339())?;
writeln!(
w,
"#EXT-X-PROGRAM-DATE-TIME:{}",
v.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
)?;
}
if let Some(ref v) = self.daterange {
write!(w, "#EXT-X-DATERANGE:")?;
@ -880,7 +889,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)?;
@ -1003,8 +1019,9 @@ impl Map {
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=")?;
write!(w, ",BYTERANGE=\"")?;
byte_range.write_value_to(w)?;
write!(w, "\"")?;
}
Ok(())
}

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,
@ -382,6 +413,7 @@ fn create_and_parse_media_playlist_full() {
tag: "X-CUE-OUT".into(),
rest: Some("DURATION=2.002".into()),
}],
..Default::default()
}],
unknown_tags: vec![],
});