diff --git a/net/hlssink3/Cargo.toml b/net/hlssink3/Cargo.toml index bb3e844b..022ae7cc 100644 --- a/net/hlssink3/Cargo.toml +++ b/net/hlssink3/Cargo.toml @@ -15,8 +15,8 @@ glib = { git = "https://github.com/gtk-rs/gtk-rs-core" } gio = { git = "https://github.com/gtk-rs/gtk-rs-core" } once_cell = "1.7.2" m3u8-rs = "5.0" -regex = "1" chrono = "0.4" +sprintf = "0.1.3" [dev-dependencies] gst-audio = { package = "gstreamer-audio", git = "https://gitlab.freedesktop.org/gstreamer/gstreamer-rs" } diff --git a/net/hlssink3/src/imp.rs b/net/hlssink3/src/imp.rs index 86a03095..9061dd96 100644 --- a/net/hlssink3/src/imp.rs +++ b/net/hlssink3/src/imp.rs @@ -6,7 +6,7 @@ // // SPDX-License-Identifier: MPL-2.0 -use crate::playlist::{Playlist, SegmentFormatter}; +use crate::playlist::Playlist; use crate::HlsSink3PlaylistType; use chrono::{DateTime, Duration, Utc}; use gio::prelude::*; @@ -63,7 +63,6 @@ impl From> for HlsSink3PlaylistType { struct Settings { location: String, - segment_formatter: SegmentFormatter, playlist_location: String, playlist_root: Option, playlist_length: u32, @@ -93,7 +92,6 @@ impl Default for Settings { .expect("Could not make element giostreamsink"); Self { location: String::from(DEFAULT_LOCATION), - segment_formatter: SegmentFormatter::new(DEFAULT_LOCATION).unwrap(), playlist_location: String::from(DEFAULT_PLAYLIST_LOCATION), playlist_root: None, playlist_length: DEFAULT_PLAYLIST_LENGTH, @@ -203,7 +201,18 @@ impl HlsSink3 { }; let settings = self.settings.lock().unwrap(); - let segment_file_location = settings.segment_formatter.segment(fragment_id); + let segment_file_location = match sprintf::sprintf!(&settings.location, fragment_id) { + Ok(file_name) => file_name, + Err(err) => { + gst::error!( + CAT, + imp: self, + "Couldn't build file name, err: {:?}", err, + ); + return Err(String::from("Invalid init segment file pattern")); + } + }; + gst::trace!( CAT, imp: self, @@ -577,9 +586,6 @@ impl ObjectImpl for HlsSink3 { .get::>() .expect("type checked upstream") .unwrap_or_else(|| DEFAULT_LOCATION.into()); - settings.segment_formatter = SegmentFormatter::new(&settings.location).expect( - "A string containing `%03d` pattern must be used (can be any number from 0-9)", - ); settings .splitmuxsink .set_property("location", &settings.location); diff --git a/net/hlssink3/src/playlist.rs b/net/hlssink3/src/playlist.rs index b595bdeb..6dffb054 100644 --- a/net/hlssink3/src/playlist.rs +++ b/net/hlssink3/src/playlist.rs @@ -7,16 +7,12 @@ // SPDX-License-Identifier: MPL-2.0 use chrono::{DateTime, Utc}; -use gst::glib::once_cell::sync::Lazy; use m3u8_rs::{MediaPlaylist, MediaPlaylistType, MediaSegment}; -use regex::Regex; use std::io::Write; const GST_M3U8_PLAYLIST_V3: usize = 3; const GST_M3U8_PLAYLIST_V4: usize = 4; -static SEGMENT_IDX_PATTERN: Lazy = Lazy::new(|| Regex::new(r"(%0(\d+)d)").unwrap()); - /// An HLS playlist. /// /// Controls the changes that needs to happen in the playlist as new segments are added. This @@ -159,119 +155,3 @@ pub enum PlaylistRenderState { Init, Started, } - -/// A formatter for segment locations. -/// -/// The formatting is based on a string that must contain the placeholder `%0Xd` where `X` is a -/// the number of zero prefixes you want to have in the segment name. The placeholder is only -/// replaced once in the string, other placements are not going to be processed. -/// -/// # Examples -/// -/// In this example we want to have segment files with the following names: -/// ```text -/// part001.ts -/// part002.ts -/// part003.ts -/// part004.ts -/// ``` -/// Then we can use the segment pattern value as `"part%03d.ts"`: -/// -/// ```rust,ignore -/// let formatter = SegmentFormatter::new("part%03d.ts").unwrap(); -/// assert_eq!(formatter.segment(1), "part001.ts"); -/// assert_eq!(formatter.segment(2), "part002.ts"); -/// assert_eq!(formatter.segment(3), "part003.ts"); -/// assert_eq!(formatter.segment(4), "part004.ts"); -/// ``` -pub struct SegmentFormatter { - prefix: String, - suffix: String, - padding_len: u32, -} - -impl SegmentFormatter { - /// Processes the segment name containing a placeholder. It can be used - /// repeatedly to format segment names. - /// - /// If an invalid placeholder is provided, then `None` is returned. - pub fn new>(segment_pattern: S) -> Option { - let segment_pattern = segment_pattern.as_ref(); - let caps = SEGMENT_IDX_PATTERN.captures(segment_pattern)?; - let number_placement_match = caps.get(1)?; - let zero_pad_match = caps.get(2)?; - let padding_len = zero_pad_match - .as_str() - .parse::() - .expect("valid number matched by regex"); - let prefix = segment_pattern[..number_placement_match.start()].to_string(); - let suffix = segment_pattern[number_placement_match.end()..].to_string(); - Some(Self { - prefix, - suffix, - padding_len, - }) - } - - /// Returns the segment location formatted for the provided id. - #[inline] - pub fn segment(&self, id: u32) -> String { - let padded_number = left_pad_zeroes(self.padding_len, id); - format!("{}{}{}", self.prefix, padded_number, self.suffix) - } -} - -/// Transforms a number to a zero padded string representation. -/// -/// The zero padding is added to the left of the number which is converted to a string. For the -/// case that the length number converted to string is larger than the requested padding, the -/// number representation is returned and no padding is added. The length of the returned string is -/// the maximum value between the desired padding and the length of the number. -/// -/// # Examples -/// -/// ```rust,ignore -/// let padded_number = left_pad_zeroes(4, 10); -/// assert_eq!(padded_number, "0010"); -/// ``` -#[inline] -pub(crate) fn left_pad_zeroes(padding: u32, number: u32) -> String { - let numerical_repr = number.to_string(); - let mut padded = String::with_capacity(padding.max(numerical_repr.len() as u32) as usize); - let pad_zeroes = padding as i32 - numerical_repr.len() as i32; - if pad_zeroes > 0 { - for _ in 0..pad_zeroes { - padded.push('0'); - } - } - padded.push_str(&numerical_repr); - padded -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn segment_is_correctly_formatted() { - let formatter = SegmentFormatter::new("segment%05d.ts").unwrap(); - - assert_eq!("segment00001.ts", formatter.segment(1)); - assert_eq!("segment00016.ts", formatter.segment(16)); - assert_eq!("segment01827.ts", formatter.segment(1827)); - assert_eq!("segment98765.ts", formatter.segment(98765)); - - let formatter = SegmentFormatter::new("part-%03d.ts").unwrap(); - assert_eq!("part-010.ts", formatter.segment(10)); - assert_eq!("part-9999.ts", formatter.segment(9999)); - } - - #[test] - fn padding_numbers() { - assert_eq!("001", left_pad_zeroes(3, 1)); - assert_eq!("010", left_pad_zeroes(3, 10)); - assert_eq!("100", left_pad_zeroes(3, 100)); - assert_eq!("1000", left_pad_zeroes(3, 1000)); - assert_eq!("987654321", left_pad_zeroes(3, 987654321)); - } -}