ffmpeg_sidecar/
ffmpeg_time_duration.rs

1use std::borrow::Cow;
2use std::convert::TryFrom;
3use std::fmt;
4use std::ops::{Add, Sub};
5use std::time::Duration;
6
7const MICROS_PER_SEC: f64 = 1_000_000.0;
8const 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)]
46pub struct FfmpegTimeDuration(i64);
47
48impl 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
152impl 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)]
175pub struct ParseFfmpegTimeStrError;
176
177impl 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
183impl std::error::Error for ParseFfmpegTimeStrError {}
184
185impl 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
193impl 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
201macro_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
227impl_from_to_numeric!(f64, f32, i64, i32, i16, i8, u64, u32, u16, u8, isize, usize);
228
229impl 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
237impl 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
245impl From<FfmpegTimeDuration> for String {
246  fn from(value: FfmpegTimeDuration) -> Self {
247    value.to_string()
248  }
249}
250
251impl From<FfmpegTimeDuration> for Cow<'static, str> {
252  fn from(value: FfmpegTimeDuration) -> Self {
253    Cow::Owned(value.to_string())
254  }
255}
256
257#[cfg(test)]
258mod 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}