ffmpeg_common/
types.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::path::PathBuf;
4use std::str::FromStr;
5use std::time::Duration as StdDuration;
6
7use crate::error::{Error, Result};
8
9/// Represents a duration in FFmpeg format (HH:MM:SS.MS or seconds)
10#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
11pub struct Duration(StdDuration);
12
13impl Duration {
14    /// Create a new duration from seconds
15    pub fn from_secs(secs: u64) -> Self {
16        Self(StdDuration::from_secs(secs))
17    }
18
19    /// Create a new duration from milliseconds
20    pub fn from_millis(millis: u64) -> Self {
21        Self(StdDuration::from_millis(millis))
22    }
23
24    /// Get the duration as seconds
25    pub fn as_secs(&self) -> u64 {
26        self.0.as_secs()
27    }
28
29    /// Get the duration as milliseconds
30    pub fn as_millis(&self) -> u128 {
31        self.0.as_millis()
32    }
33
34    /// Convert to FFmpeg time format (HH:MM:SS.MS)
35    pub fn to_ffmpeg_format(&self) -> String {
36        let total_secs = self.0.as_secs();
37        let hours = total_secs / 3600;
38        let minutes = (total_secs % 3600) / 60;
39        let seconds = total_secs % 60;
40        let millis = self.0.subsec_millis();
41
42        if millis > 0 {
43            format!("{:02}:{:02}:{:02}.{:03}", hours, minutes, seconds, millis)
44        } else {
45            format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
46        }
47    }
48
49    /// Parse from FFmpeg time format
50    pub fn from_ffmpeg_format(s: &str) -> Result<Self> {
51        // Handle pure seconds format
52        if let Ok(secs) = s.parse::<f64>() {
53            return Ok(Self(StdDuration::from_secs_f64(secs)));
54        }
55
56        // Handle HH:MM:SS[.MS] format
57        let parts: Vec<&str> = s.split(':').collect();
58        if parts.len() != 3 {
59            return Err(Error::ParseError(format!("Invalid time format: {}", s)));
60        }
61
62        let hours: u64 = parts[0].parse()
63            .map_err(|_| Error::ParseError(format!("Invalid hours: {}", parts[0])))?;
64        let minutes: u64 = parts[1].parse()
65            .map_err(|_| Error::ParseError(format!("Invalid minutes: {}", parts[1])))?;
66
67        let (seconds, millis) = if parts[2].contains('.') {
68            let sec_parts: Vec<&str> = parts[2].split('.').collect();
69            let secs: u64 = sec_parts[0].parse()
70                .map_err(|_| Error::ParseError(format!("Invalid seconds: {}", sec_parts[0])))?;
71            let ms: u64 = sec_parts[1].parse()
72                .map_err(|_| Error::ParseError(format!("Invalid milliseconds: {}", sec_parts[1])))?;
73            (secs, ms)
74        } else {
75            let secs: u64 = parts[2].parse()
76                .map_err(|_| Error::ParseError(format!("Invalid seconds: {}", parts[2])))?;
77            (secs, 0)
78        };
79
80        let total_millis = (hours * 3600 + minutes * 60 + seconds) * 1000 + millis;
81        Ok(Self(StdDuration::from_millis(total_millis)))
82    }
83}
84
85impl From<StdDuration> for Duration {
86    fn from(d: StdDuration) -> Self {
87        Self(d)
88    }
89}
90
91impl From<Duration> for StdDuration {
92    fn from(d: Duration) -> Self {
93        d.0
94    }
95}
96
97impl fmt::Display for Duration {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        write!(f, "{}", self.to_ffmpeg_format())
100    }
101}
102
103impl FromStr for Duration {
104    type Err = Error;
105
106    fn from_str(s: &str) -> Result<Self> {
107        Self::from_ffmpeg_format(s)
108    }
109}
110
111/// Represents a size in bytes with SI/binary prefixes support
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
113pub struct Size(u64);
114
115impl Size {
116    /// Create a new size in bytes
117    pub fn from_bytes(bytes: u64) -> Self {
118        Self(bytes)
119    }
120
121    /// Create from kilobytes
122    pub fn from_kb(kb: u64) -> Self {
123        Self(kb * 1000)
124    }
125
126    /// Create from megabytes
127    pub fn from_mb(mb: u64) -> Self {
128        Self(mb * 1_000_000)
129    }
130
131    /// Create from gigabytes
132    pub fn from_gb(gb: u64) -> Self {
133        Self(gb * 1_000_000_000)
134    }
135
136    /// Create from kibibytes
137    pub fn from_kib(kib: u64) -> Self {
138        Self(kib * 1024)
139    }
140
141    /// Create from mebibytes
142    pub fn from_mib(mib: u64) -> Self {
143        Self(mib * 1024 * 1024)
144    }
145
146    /// Create from gibibytes
147    pub fn from_gib(gib: u64) -> Self {
148        Self(gib * 1024 * 1024 * 1024)
149    }
150
151    /// Get size in bytes
152    pub fn as_bytes(&self) -> u64 {
153        self.0
154    }
155
156    /// Parse size from string with optional suffix
157    pub fn parse(s: &str) -> Result<Self> {
158        let s = s.trim();
159
160        // Extract number and suffix
161        let (num_str, suffix) = s
162            .find(|c: char| c.is_alphabetic())
163            .map(|i| s.split_at(i))
164            .unwrap_or((s, ""));
165
166        let number: f64 = num_str.parse()
167            .map_err(|_| Error::ParseError(format!("Invalid number: {}", num_str)))?;
168
169        let multiplier = match suffix.to_uppercase().as_str() {
170            "" | "B" => 1.0,
171            "K" | "KB" => 1_000.0,
172            "M" | "MB" => 1_000_000.0,
173            "G" | "GB" => 1_000_000_000.0,
174            "KI" | "KIB" => 1_024.0,
175            "MI" | "MIB" => 1_048_576.0,
176            "GI" | "GIB" => 1_073_741_824.0,
177            _ => return Err(Error::ParseError(format!("Invalid size suffix: {}", suffix))),
178        };
179
180        Ok(Self((number * multiplier) as u64))
181    }
182}
183
184impl fmt::Display for Size {
185    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186        write!(f, "{}", self.0)
187    }
188}
189
190impl FromStr for Size {
191    type Err = Error;
192
193    fn from_str(s: &str) -> Result<Self> {
194        Self::parse(s)
195    }
196}
197
198/// Represents a stream specifier in FFmpeg
199#[derive(Debug, Clone, PartialEq, Eq)]
200pub enum StreamSpecifier {
201    /// Stream by index
202    Index(usize),
203    /// Stream by type (v, a, s, d, t)
204    Type(StreamType),
205    /// Stream by type and index
206    TypeIndex(StreamType, usize),
207    /// All streams
208    All,
209    /// Program ID
210    Program(usize),
211    /// Stream ID
212    StreamId(String),
213    /// Metadata key/value
214    Metadata { key: String, value: Option<String> },
215    /// Usable streams
216    Usable,
217}
218
219impl StreamSpecifier {
220    /// Convert to FFmpeg command-line format
221    pub fn to_string(&self) -> String {
222        match self {
223            Self::Index(i) => i.to_string(),
224            Self::Type(t) => t.to_string(),
225            Self::TypeIndex(t, i) => format!("{}:{}", t, i),
226            Self::All => String::new(),
227            Self::Program(id) => format!("p:{}", id),
228            Self::StreamId(id) => format!("#{}", id),
229            Self::Metadata { key, value } => {
230                if let Some(val) = value {
231                    format!("m:{}:{}", key, val)
232                } else {
233                    format!("m:{}", key)
234                }
235            }
236            Self::Usable => "u".to_string(),
237        }
238    }
239}
240
241impl fmt::Display for StreamSpecifier {
242    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243        write!(f, "{}", self.to_string())
244    }
245}
246
247/// Stream type
248#[derive(Debug, Clone, Copy, PartialEq, Eq)]
249pub enum StreamType {
250    Video,
251    VideoNoAttached,
252    Audio,
253    Subtitle,
254    Data,
255    Attachment,
256}
257
258impl StreamType {
259    pub fn as_str(&self) -> &'static str {
260        match self {
261            Self::Video => "v",
262            Self::VideoNoAttached => "V",
263            Self::Audio => "a",
264            Self::Subtitle => "s",
265            Self::Data => "d",
266            Self::Attachment => "t",
267        }
268    }
269}
270
271impl fmt::Display for StreamType {
272    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273        write!(f, "{}", self.as_str())
274    }
275}
276
277/// Log level for FFmpeg tools
278#[derive(Debug, Clone, Copy, PartialEq, Eq)]
279pub enum LogLevel {
280    Quiet,
281    Panic,
282    Fatal,
283    Error,
284    Warning,
285    Info,
286    Verbose,
287    Debug,
288    Trace,
289}
290
291impl LogLevel {
292    pub fn as_str(&self) -> &'static str {
293        match self {
294            Self::Quiet => "quiet",
295            Self::Panic => "panic",
296            Self::Fatal => "fatal",
297            Self::Error => "error",
298            Self::Warning => "warning",
299            Self::Info => "info",
300            Self::Verbose => "verbose",
301            Self::Debug => "debug",
302            Self::Trace => "trace",
303        }
304    }
305
306    pub fn as_number(&self) -> i32 {
307        match self {
308            Self::Quiet => -8,
309            Self::Panic => 0,
310            Self::Fatal => 8,
311            Self::Error => 16,
312            Self::Warning => 24,
313            Self::Info => 32,
314            Self::Verbose => 40,
315            Self::Debug => 48,
316            Self::Trace => 56,
317        }
318    }
319}
320
321impl fmt::Display for LogLevel {
322    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
323        write!(f, "{}", self.as_str())
324    }
325}
326
327/// Pixel format
328#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
329pub struct PixelFormat(String);
330
331impl PixelFormat {
332    pub fn new(format: impl Into<String>) -> Self {
333        Self(format.into())
334    }
335
336    pub fn as_str(&self) -> &str {
337        &self.0
338    }
339
340    // Common pixel formats
341    pub fn yuv420p() -> Self {
342        Self("yuv420p".to_string())
343    }
344
345    pub fn yuv422p() -> Self {
346        Self("yuv422p".to_string())
347    }
348
349    pub fn yuv444p() -> Self {
350        Self("yuv444p".to_string())
351    }
352
353    pub fn rgb24() -> Self {
354        Self("rgb24".to_string())
355    }
356
357    pub fn bgr24() -> Self {
358        Self("bgr24".to_string())
359    }
360
361    pub fn rgba() -> Self {
362        Self("rgba".to_string())
363    }
364
365    pub fn bgra() -> Self {
366        Self("bgra".to_string())
367    }
368
369    pub fn gray() -> Self {
370        Self("gray".to_string())
371    }
372
373    pub fn nv12() -> Self {
374        Self("nv12".to_string())
375    }
376}
377
378impl fmt::Display for PixelFormat {
379    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
380        write!(f, "{}", self.0)
381    }
382}
383
384/// Audio sample format
385#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
386pub struct SampleFormat(String);
387
388impl SampleFormat {
389    pub fn new(format: impl Into<String>) -> Self {
390        Self(format.into())
391    }
392
393    pub fn as_str(&self) -> &str {
394        &self.0
395    }
396
397    // Common sample formats
398    pub fn u8() -> Self {
399        Self("u8".to_string())
400    }
401
402    pub fn s16() -> Self {
403        Self("s16".to_string())
404    }
405
406    pub fn s32() -> Self {
407        Self("s32".to_string())
408    }
409
410    pub fn flt() -> Self {
411        Self("flt".to_string())
412    }
413
414    pub fn dbl() -> Self {
415        Self("dbl".to_string())
416    }
417
418    pub fn u8p() -> Self {
419        Self("u8p".to_string())
420    }
421
422    pub fn s16p() -> Self {
423        Self("s16p".to_string())
424    }
425
426    pub fn s32p() -> Self {
427        Self("s32p".to_string())
428    }
429
430    pub fn fltp() -> Self {
431        Self("fltp".to_string())
432    }
433
434    pub fn dblp() -> Self {
435        Self("dblp".to_string())
436    }
437}
438
439impl fmt::Display for SampleFormat {
440    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
441        write!(f, "{}", self.0)
442    }
443}
444
445/// Codec name
446#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
447pub struct Codec(String);
448
449impl Codec {
450    pub fn new(codec: impl Into<String>) -> Self {
451        Self(codec.into())
452    }
453
454    pub fn as_str(&self) -> &str {
455        &self.0
456    }
457
458    // Common video codecs
459    pub fn h264() -> Self {
460        Self("h264".to_string())
461    }
462
463    pub fn h265() -> Self {
464        Self("h265".to_string())
465    }
466
467    pub fn vp9() -> Self {
468        Self("vp9".to_string())
469    }
470
471    pub fn av1() -> Self {
472        Self("av1".to_string())
473    }
474
475    pub fn mpeg2video() -> Self {
476        Self("mpeg2video".to_string())
477    }
478
479    pub fn mpeg4() -> Self {
480        Self("mpeg4".to_string())
481    }
482
483    // Common audio codecs
484    pub fn aac() -> Self {
485        Self("aac".to_string())
486    }
487
488    pub fn mp3() -> Self {
489        Self("mp3".to_string())
490    }
491
492    pub fn opus() -> Self {
493        Self("opus".to_string())
494    }
495
496    pub fn flac() -> Self {
497        Self("flac".to_string())
498    }
499
500    pub fn ac3() -> Self {
501        Self("ac3".to_string())
502    }
503
504    pub fn pcm_s16le() -> Self {
505        Self("pcm_s16le".to_string())
506    }
507
508    // Copy codec
509    pub fn copy() -> Self {
510        Self("copy".to_string())
511    }
512}
513
514impl fmt::Display for Codec {
515    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
516        write!(f, "{}", self.0)
517    }
518}
519
520/// Input or output file/URL
521#[derive(Debug, Clone, PartialEq, Eq)]
522pub struct MediaPath {
523    path: PathBuf,
524    is_url: bool,
525}
526
527impl MediaPath {
528    /// Create from a file path
529    pub fn from_path(path: impl Into<PathBuf>) -> Self {
530        Self {
531            path: path.into(),
532            is_url: false,
533        }
534    }
535
536    /// Create from a URL
537    pub fn from_url(url: impl Into<String>) -> Self {
538        Self {
539            path: PathBuf::from(url.into()),
540            is_url: true,
541        }
542    }
543
544    /// Parse from string, auto-detecting URLs
545    pub fn parse(s: impl AsRef<str>) -> Self {
546        let s = s.as_ref();
547        if s.contains("://") || s.starts_with("rtmp") || s.starts_with("rtsp") {
548            Self::from_url(s)
549        } else {
550            Self::from_path(s)
551        }
552    }
553
554    /// Get as string for command line
555    pub fn as_str(&self) -> &str {
556        self.path.to_str().unwrap_or("")
557    }
558
559    /// Check if this is a URL
560    pub fn is_url(&self) -> bool {
561        self.is_url
562    }
563
564    /// Check if this is a file path
565    pub fn is_file(&self) -> bool {
566        !self.is_url
567    }
568
569    /// Get the path
570    pub fn path(&self) -> &PathBuf {
571        &self.path
572    }
573}
574
575impl fmt::Display for MediaPath {
576    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
577        write!(f, "{}", self.as_str())
578    }
579}
580
581impl From<PathBuf> for MediaPath {
582    fn from(path: PathBuf) -> Self {
583        Self::from_path(path)
584    }
585}
586
587impl From<&str> for MediaPath {
588    fn from(s: &str) -> Self {
589        Self::parse(s)
590    }
591}
592
593impl From<String> for MediaPath {
594    fn from(s: String) -> Self {
595        Self::parse(s)
596    }
597}
598
599#[cfg(test)]
600mod tests {
601    use super::*;
602
603    #[test]
604    fn test_duration_parsing() {
605        assert_eq!(Duration::from_ffmpeg_format("10").unwrap().as_secs(), 10);
606        assert_eq!(Duration::from_ffmpeg_format("01:30:00").unwrap().as_secs(), 5400);
607        assert_eq!(Duration::from_ffmpeg_format("00:00:30.500").unwrap().as_millis(), 30500);
608    }
609
610    #[test]
611    fn test_duration_formatting() {
612        assert_eq!(Duration::from_secs(90).to_ffmpeg_format(), "00:01:30");
613        assert_eq!(Duration::from_millis(30500).to_ffmpeg_format(), "00:00:30.500");
614    }
615
616    #[test]
617    fn test_size_parsing() {
618        assert_eq!(Size::parse("1024").unwrap().as_bytes(), 1024);
619        assert_eq!(Size::parse("10K").unwrap().as_bytes(), 10_000);
620        assert_eq!(Size::parse("10KB").unwrap().as_bytes(), 10_000);
621        assert_eq!(Size::parse("10KiB").unwrap().as_bytes(), 10_240);
622        assert_eq!(Size::parse("1.5M").unwrap().as_bytes(), 1_500_000);
623    }
624
625    #[test]
626    fn test_stream_specifier() {
627        assert_eq!(StreamSpecifier::Index(1).to_string(), "1");
628        assert_eq!(StreamSpecifier::Type(StreamType::Audio).to_string(), "a");
629        assert_eq!(StreamSpecifier::TypeIndex(StreamType::Video, 0).to_string(), "v:0");
630        assert_eq!(StreamSpecifier::Program(1).to_string(), "p:1");
631    }
632
633    #[test]
634    fn test_media_path() {
635        let file = MediaPath::parse("/path/to/file.mp4");
636        assert!(file.is_file());
637        assert!(!file.is_url());
638
639        let url = MediaPath::parse("https://example.com/video.mp4");
640        assert!(url.is_url());
641        assert!(!url.is_file());
642
643        let rtmp = MediaPath::parse("rtmp://server/live/stream");
644        assert!(rtmp.is_url());
645    }
646}