Skip to content

Commit 57e08ed

Browse files
feat: add FfmpegTimeDuration type (#94)
* feat: add FfmpegTimeDuration type * fix: fix typo in docs --------- Co-authored-by: nbabcock <nathan.r.babcock@gmail.com>
1 parent 2b3831a commit 57e08ed

3 files changed

Lines changed: 378 additions & 18 deletions

File tree

src/ffmpeg_time_duration.rs

Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
use std::borrow::Cow;
2+
use std::convert::TryFrom;
3+
use std::fmt;
4+
use std::ops::{Add, Sub};
5+
use std::time::Duration;
6+
7+
const MICROS_PER_SEC: f64 = 1_000_000.0;
8+
const MILLIS_PER_SEC: f64 = 1_000.0;
9+
10+
/// A [`FfmpegTimeDuration`] type to represent a ffmpeg time duration described in
11+
/// [FFmpeg documentation](https://ffmpeg.org/ffmpeg-utils.html#Time-duration)
12+
///
13+
/// This type wraps an `i64` internally, representing time in microseconds, similar to how
14+
/// FFmpeg handles time durations internally.
15+
///
16+
/// # Conversions
17+
///
18+
/// The `From` trait implementations for numeric types (such as `f64`, `f32`, `i64`, etc.)
19+
/// convert between `FfmpegTimeDuration` and numeric values in **seconds**:
20+
/// - Converting from a numeric type to `FfmpegTimeDuration` interprets the numeric value as seconds
21+
/// - Converting from `FfmpegTimeDuration` to a numeric type returns the duration in seconds
22+
/// - Adding or subtracting with numeric values adds or subtracts seconds
23+
///
24+
/// # Traits
25+
///
26+
/// [`FfmpegTimeDuration`] implement many common traits, including [`Add`], [`Sub`].
27+
/// It implements [`Default`] by returning a zero-length `FfmpegTimeDuration`.
28+
///
29+
/// # Examples
30+
///
31+
/// ```rust
32+
/// use ffmpeg_sidecar::ffmpeg_time_duration::FfmpegTimeDuration;
33+
/// use ffmpeg_sidecar::command::FfmpegCommand;
34+
///
35+
/// let second = FfmpegTimeDuration::from_str("00:00:01").unwrap();
36+
/// let hundred_milliseconds = FfmpegTimeDuration::from_str("100ms").unwrap();
37+
///
38+
/// assert_eq!(second.as_seconds(), 1.0);
39+
/// assert_eq!(hundred_milliseconds.as_seconds(), 0.1);
40+
///
41+
/// FfmpegCommand::new()
42+
/// .arg("-ss")
43+
/// .arg(second.to_string());
44+
/// ```
45+
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)]
46+
pub struct FfmpegTimeDuration(i64);
47+
48+
impl FfmpegTimeDuration {
49+
#[must_use]
50+
#[inline]
51+
pub fn new(microseconds: i64) -> Self {
52+
Self::from_micros(microseconds)
53+
}
54+
55+
#[must_use]
56+
#[inline]
57+
pub fn as_micros(&self) -> i64 {
58+
self.0
59+
}
60+
61+
#[must_use]
62+
#[inline]
63+
pub fn from_micros(microseconds: i64) -> Self {
64+
Self(microseconds)
65+
}
66+
67+
#[must_use]
68+
#[inline]
69+
pub fn as_seconds(self) -> f64 {
70+
self.0 as f64 / MICROS_PER_SEC
71+
}
72+
73+
#[must_use]
74+
#[inline]
75+
pub fn from_seconds(seconds: f64) -> Self {
76+
Self::from_micros((seconds * MICROS_PER_SEC) as i64)
77+
}
78+
79+
#[must_use]
80+
pub fn from_str(str: &str) -> Option<Self> {
81+
let str = str.trim();
82+
83+
// Handle negative values
84+
let (is_negative, str) = if str.starts_with('-') {
85+
(true, &str[1..])
86+
} else {
87+
(false, str)
88+
};
89+
90+
let mut micros: i64;
91+
92+
// Check for microseconds suffix
93+
if str.ends_with("us") {
94+
let value = str.trim_end_matches("us").trim().parse::<f64>().ok()?;
95+
micros = value as i64;
96+
}
97+
// Check for milliseconds suffix
98+
else if str.ends_with("ms") {
99+
let value = str.trim_end_matches("ms").trim().parse::<f64>().ok()?;
100+
micros = (value * MILLIS_PER_SEC) as i64;
101+
}
102+
// Check for HH:MM:SS format
103+
else if str.contains(':') {
104+
let mut seconds = 0.0;
105+
let mut smh = str.split(':').rev();
106+
if let Some(sec) = smh.next() {
107+
seconds += sec.parse::<f64>().ok()?;
108+
}
109+
110+
if let Some(min) = smh.next() {
111+
seconds += min.parse::<f64>().ok()? * 60.0;
112+
}
113+
114+
if let Some(hrs) = smh.next() {
115+
seconds += hrs.parse::<f64>().ok()? * 60.0 * 60.0;
116+
}
117+
micros = (seconds * MICROS_PER_SEC) as i64;
118+
}
119+
// Plain numeric value (seconds)
120+
else {
121+
let seconds = str.parse::<f64>().ok()?;
122+
micros = (seconds * MICROS_PER_SEC) as i64;
123+
}
124+
125+
if is_negative {
126+
micros = -micros;
127+
}
128+
129+
Some(Self::from_micros(micros))
130+
}
131+
132+
#[must_use]
133+
#[inline]
134+
pub fn as_duration(&self) -> Duration {
135+
Duration::from_micros(self.0.unsigned_abs())
136+
}
137+
138+
#[must_use]
139+
#[inline]
140+
pub fn from_duration(duration: Duration) -> Self {
141+
Self::from_micros(duration.as_micros() as i64)
142+
}
143+
144+
/// Returns a string representation of the duration in microseconds with "us" suffix.
145+
#[must_use]
146+
#[inline]
147+
pub fn to_alt_string(&self) -> String {
148+
format!("{:#}", self)
149+
}
150+
}
151+
152+
impl fmt::Display for FfmpegTimeDuration {
153+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
154+
if !f.alternate() {
155+
let seconds = self.as_seconds();
156+
let is_negative = seconds < 0.0;
157+
let abs_seconds = seconds.abs();
158+
159+
let hours = (abs_seconds / 3600.0).floor() as i64;
160+
let minutes = ((abs_seconds / 60.0) % 60.0).floor() as i64;
161+
let secs = abs_seconds % 60.0;
162+
163+
if is_negative {
164+
write!(f, "-{:02}:{:02}:{:06.3}", hours, minutes, secs)
165+
} else {
166+
write!(f, "{:02}:{:02}:{:06.3}", hours, minutes, secs)
167+
}
168+
} else {
169+
write!(f, "{}us", self.as_micros())
170+
}
171+
}
172+
}
173+
174+
#[derive(Debug, Clone)]
175+
pub struct ParseFfmpegTimeStrError;
176+
177+
impl fmt::Display for ParseFfmpegTimeStrError {
178+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179+
write!(f, "Failed to parse FFmpeg time string")
180+
}
181+
}
182+
183+
impl std::error::Error for ParseFfmpegTimeStrError {}
184+
185+
impl TryFrom<String> for FfmpegTimeDuration {
186+
type Error = ParseFfmpegTimeStrError;
187+
188+
fn try_from(value: String) -> Result<Self, Self::Error> {
189+
FfmpegTimeDuration::from_str(&value).ok_or(ParseFfmpegTimeStrError)
190+
}
191+
}
192+
193+
impl TryFrom<&str> for FfmpegTimeDuration {
194+
type Error = ParseFfmpegTimeStrError;
195+
196+
fn try_from(value: &str) -> Result<Self, Self::Error> {
197+
FfmpegTimeDuration::from_str(value).ok_or(ParseFfmpegTimeStrError)
198+
}
199+
}
200+
201+
macro_rules! impl_from_to_numeric {
202+
($($t:ty),*) => {
203+
$(
204+
impl From<$t> for FfmpegTimeDuration {
205+
fn from(value: $t) -> Self {
206+
Self::from_seconds(value as f64)
207+
}
208+
}
209+
210+
impl From<FfmpegTimeDuration> for $t {
211+
fn from(value: FfmpegTimeDuration) -> Self {
212+
value.as_seconds() as $t
213+
}
214+
}
215+
216+
impl Add<$t> for FfmpegTimeDuration {
217+
type Output = Self;
218+
219+
fn add(self, rhs: $t) -> Self::Output {
220+
FfmpegTimeDuration::from_micros(self.as_micros() + (rhs * MICROS_PER_SEC as $t) as i64)
221+
}
222+
}
223+
)*
224+
};
225+
}
226+
227+
impl_from_to_numeric!(f64, f32, i64, i32, i16, i8, u64, u32, u16, u8, isize, usize);
228+
229+
impl Add for FfmpegTimeDuration {
230+
type Output = Self;
231+
232+
fn add(self, rhs: Self) -> Self::Output {
233+
FfmpegTimeDuration::from_micros(self.as_micros() + rhs.as_micros())
234+
}
235+
}
236+
237+
impl Sub for FfmpegTimeDuration {
238+
type Output = Self;
239+
240+
fn sub(self, rhs: Self) -> Self::Output {
241+
FfmpegTimeDuration::from_micros(self.as_micros() - rhs.as_micros())
242+
}
243+
}
244+
245+
impl From<FfmpegTimeDuration> for String {
246+
fn from(value: FfmpegTimeDuration) -> Self {
247+
value.to_string()
248+
}
249+
}
250+
251+
impl From<FfmpegTimeDuration> for Cow<'static, str> {
252+
fn from(value: FfmpegTimeDuration) -> Self {
253+
Cow::Owned(value.to_string())
254+
}
255+
}
256+
257+
#[cfg(test)]
258+
mod tests {
259+
use super::*;
260+
261+
#[test]
262+
fn test_parse_string() {
263+
assert_eq!(
264+
FfmpegTimeDuration::from_str("00:00:00.00"),
265+
Some(FfmpegTimeDuration::from_seconds(0.0))
266+
);
267+
assert_eq!(
268+
FfmpegTimeDuration::from_str("5"),
269+
Some(FfmpegTimeDuration::from_seconds(5.0))
270+
);
271+
assert_eq!(
272+
FfmpegTimeDuration::from_str("2.5"),
273+
Some(FfmpegTimeDuration::from_seconds(2.5))
274+
);
275+
assert_eq!(
276+
FfmpegTimeDuration::from_str("0.123"),
277+
Some(FfmpegTimeDuration::from_seconds(0.123))
278+
);
279+
assert_eq!(
280+
FfmpegTimeDuration::from_str("1:00.0"),
281+
Some(FfmpegTimeDuration::from_seconds(60.0))
282+
);
283+
assert_eq!(
284+
FfmpegTimeDuration::from_str("1:01.0"),
285+
Some(FfmpegTimeDuration::from_seconds(61.0))
286+
);
287+
assert_eq!(
288+
FfmpegTimeDuration::from_str("1:01:01.123"),
289+
Some(FfmpegTimeDuration::from_seconds(3661.123))
290+
);
291+
assert_eq!(FfmpegTimeDuration::from_str("N/A"), None);
292+
}
293+
294+
#[test]
295+
fn test_parse_negative_value() {
296+
assert_eq!(
297+
FfmpegTimeDuration::from_str("-00:00:01.00"),
298+
Some(FfmpegTimeDuration::from_seconds(-1.0))
299+
);
300+
assert_eq!(
301+
FfmpegTimeDuration::from_str("-00:01.00"),
302+
Some(FfmpegTimeDuration::from_seconds(-1.0))
303+
);
304+
assert_eq!(
305+
FfmpegTimeDuration::from_str("-01.00"),
306+
Some(FfmpegTimeDuration::from_seconds(-1.0))
307+
);
308+
assert_eq!(
309+
FfmpegTimeDuration::from_str("-01"),
310+
Some(FfmpegTimeDuration::from_seconds(-1.0))
311+
);
312+
assert_eq!(
313+
FfmpegTimeDuration::from_str("-1"),
314+
Some(FfmpegTimeDuration::from_seconds(-1.0))
315+
);
316+
assert_eq!(
317+
FfmpegTimeDuration::from_str("-1000ms"),
318+
Some(FfmpegTimeDuration::from_seconds(-1.0))
319+
);
320+
}
321+
322+
#[test]
323+
fn test_parse_string_with_suffix() {
324+
assert_eq!(
325+
FfmpegTimeDuration::from_str("400ms"),
326+
Some(FfmpegTimeDuration::from_seconds(0.4))
327+
);
328+
assert_eq!(
329+
FfmpegTimeDuration::from_str("3000us"),
330+
Some(FfmpegTimeDuration::from_seconds(0.003))
331+
);
332+
}
333+
334+
#[test]
335+
fn test_format() {
336+
assert_eq!(
337+
format!("{}", FfmpegTimeDuration::from_seconds(0.0)),
338+
"00:00:00.000"
339+
);
340+
assert_eq!(
341+
format!("{}", FfmpegTimeDuration::from_seconds(-1.0)),
342+
"-00:00:01.000"
343+
);
344+
assert_eq!(
345+
format!("{}", FfmpegTimeDuration::from_seconds(3661.123)),
346+
"01:01:01.123"
347+
);
348+
assert_eq!(
349+
format!("{}", FfmpegTimeDuration::from_seconds(0.547)),
350+
"00:00:00.547"
351+
);
352+
}
353+
354+
#[test]
355+
fn test_alternative_format() {
356+
assert_eq!(
357+
format!("{:#}", FfmpegTimeDuration::from_seconds(0.0)),
358+
"0us"
359+
);
360+
assert_eq!(
361+
format!("{:#}", FfmpegTimeDuration::from_seconds(-0.000001)),
362+
"-1us"
363+
);
364+
assert_eq!(
365+
format!("{:#}", FfmpegTimeDuration::from_seconds(3661.123)),
366+
"3661123000us"
367+
);
368+
assert_eq!(
369+
format!("{:#}", FfmpegTimeDuration::from_seconds(0.547)),
370+
"547000us"
371+
);
372+
}
373+
}

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ pub mod paths;
4040
pub mod pix_fmt;
4141
pub mod read_until_any;
4242
pub mod version;
43-
43+
pub mod ffmpeg_time_duration;
4444
#[cfg(feature = "named_pipes")]
4545
#[cfg_attr(docsrs, doc(cfg(feature = "named_pipes")))]
4646
pub mod named_pipes;

0 commit comments

Comments
 (0)