Skip to main content

ff_encode/trim/
trimmer.rs

1//! Stream-copy trimming — cut a media file to a time range without re-encoding.
2
3use std::path::PathBuf;
4
5use crate::error::EncodeError;
6
7use super::trim_inner;
8
9/// Trim a media file to a time range using stream copy (no re-encode).
10///
11/// Uses [`avformat_seek_file`] to seek to the start point, then copies packets
12/// until the presentation timestamp exceeds the end point.  All streams
13/// (video, audio, subtitles) are copied verbatim from the input.
14///
15/// # Example
16///
17/// ```ignore
18/// use ff_encode::StreamCopyTrimmer;
19///
20/// StreamCopyTrimmer::new("input.mp4", 2.0, 7.0, "output.mp4")
21///     .run()?;
22/// ```
23///
24/// [`avformat_seek_file`]: https://ffmpeg.org/doxygen/trunk/group__lavf__decoding.html
25pub struct StreamCopyTrimmer {
26    input: PathBuf,
27    output: PathBuf,
28    start_sec: f64,
29    end_sec: f64,
30}
31
32impl StreamCopyTrimmer {
33    /// Create a new `StreamCopyTrimmer`.
34    ///
35    /// `start_sec` and `end_sec` are absolute timestamps in seconds measured
36    /// from the start of the source file.  [`run`](Self::run) returns
37    /// [`EncodeError::InvalidConfig`] if `start_sec >= end_sec`.
38    pub fn new(
39        input: impl Into<PathBuf>,
40        start_sec: f64,
41        end_sec: f64,
42        output: impl Into<PathBuf>,
43    ) -> Self {
44        Self {
45            input: input.into(),
46            output: output.into(),
47            start_sec,
48            end_sec,
49        }
50    }
51
52    /// Execute the trim operation.
53    ///
54    /// # Errors
55    ///
56    /// - [`EncodeError::InvalidConfig`] if `start_sec >= end_sec`.
57    /// - [`EncodeError::Ffmpeg`] if any FFmpeg API call fails.
58    pub fn run(self) -> Result<(), EncodeError> {
59        if self.start_sec >= self.end_sec {
60            return Err(EncodeError::InvalidConfig {
61                reason: format!(
62                    "start_sec ({}) must be less than end_sec ({})",
63                    self.start_sec, self.end_sec
64                ),
65            });
66        }
67        log::debug!(
68            "stream copy trim start input={} output={} start_sec={} end_sec={}",
69            self.input.display(),
70            self.output.display(),
71            self.start_sec,
72            self.end_sec,
73        );
74        trim_inner::run_trim(&self.input, &self.output, self.start_sec, self.end_sec)
75    }
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn stream_copy_trimmer_should_reject_start_greater_than_end() {
84        let result = StreamCopyTrimmer::new("input.mp4", 7.0, 2.0, "output.mp4").run();
85        assert!(
86            matches!(result, Err(EncodeError::InvalidConfig { .. })),
87            "expected InvalidConfig for start > end, got {result:?}"
88        );
89    }
90
91    #[test]
92    fn stream_copy_trimmer_should_reject_equal_start_and_end() {
93        let result = StreamCopyTrimmer::new("input.mp4", 5.0, 5.0, "output.mp4").run();
94        assert!(
95            matches!(result, Err(EncodeError::InvalidConfig { .. })),
96            "expected InvalidConfig for start == end, got {result:?}"
97        );
98    }
99}