From e3b6390186e30bfd40bec6468bb085a8071a66a6 Mon Sep 17 00:00:00 2001 From: Anton Eicher <54324760+ant1eicher@users.noreply.github.com> Date: Fri, 19 Jan 2024 11:29:19 +0200 Subject: [PATCH] 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. --- .../media-playlist-zero-decimal.m3u8 | 30 +++++++++++++++++ src/lib.rs | 26 +++++++++++++++ src/playlist.rs | 16 ++++++++-- tests/lib.rs | 32 +++++++++++++++++++ 4 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 sample-playlists/media-playlist-zero-decimal.m3u8 diff --git a/sample-playlists/media-playlist-zero-decimal.m3u8 b/sample-playlists/media-playlist-zero-decimal.m3u8 new file mode 100644 index 0000000..b52f251 --- /dev/null +++ b/sample-playlists/media-playlist-zero-decimal.m3u8 @@ -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 diff --git a/src/lib.rs b/src/lib.rs index 1b740c1..ec65acf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 = 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::*; diff --git a/src/playlist.rs b/src/playlist.rs index 8ba747b..8b3fa0c 100644 --- a/src/playlist.rs +++ b/src/playlist.rs @@ -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) => { @@ -884,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)?; diff --git a/tests/lib.rs b/tests/lib.rs index 3b95f2d..a388365 100644 --- a/tests/lib.rs +++ b/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 { @@ -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 = 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 { @@ -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![], });