Skip to main content

stream_copy_trim/
stream_copy_trim.rs

1//! Trim a media file to a time range without re-encoding.
2//!
3//! Uses [`StreamCopyTrimmer`] to perform a fast stream-copy trim.  Both video
4//! and audio streams are preserved as-is; only the container timestamps are
5//! adjusted.  The output starts at the nearest keyframe before `start`, so
6//! the first few frames of the output may be from slightly before the
7//! requested start time.
8//!
9//! # Usage
10//!
11//! ```bash
12//! cargo run --example stream_copy_trim -- \
13//!     --input  input.mp4  \
14//!     --start  10.0       \
15//!     --end    30.0       \
16//!     --output trimmed.mp4
17//! ```
18
19use std::process;
20
21use ff_remux::StreamCopyTrimmer;
22
23fn main() {
24    let mut args = std::env::args().skip(1);
25    let mut input = None::<String>;
26    let mut output = None::<String>;
27    let mut start_sec = 0.0_f64;
28    let mut end_sec = None::<f64>;
29
30    while let Some(flag) = args.next() {
31        match flag.as_str() {
32            "--input" | "-i" => input = Some(args.next().unwrap_or_default()),
33            "--output" | "-o" => output = Some(args.next().unwrap_or_default()),
34            "--start" => {
35                let raw = args.next().unwrap_or_default();
36                start_sec = raw.parse().unwrap_or_else(|_| {
37                    eprintln!("Invalid start: {raw}");
38                    process::exit(1);
39                });
40            }
41            "--end" => {
42                let raw = args.next().unwrap_or_default();
43                end_sec = Some(raw.parse().unwrap_or_else(|_| {
44                    eprintln!("Invalid end: {raw}");
45                    process::exit(1);
46                }));
47            }
48            other => {
49                eprintln!("Unknown flag: {other}");
50                process::exit(1);
51            }
52        }
53    }
54
55    let input = input.unwrap_or_else(|| {
56        eprintln!(
57            "Usage: stream_copy_trim --input <file> --start <secs> --end <secs> --output <file>"
58        );
59        process::exit(1);
60    });
61    let end_sec = end_sec.unwrap_or_else(|| {
62        eprintln!("--end is required");
63        process::exit(1);
64    });
65    let output = output.unwrap_or_else(|| {
66        eprintln!("--output is required");
67        process::exit(1);
68    });
69
70    if start_sec >= end_sec {
71        eprintln!("Error: --start ({start_sec}) must be less than --end ({end_sec})");
72        process::exit(1);
73    }
74
75    let duration = end_sec - start_sec;
76    println!("Input:    {input}");
77    println!("Range:    {start_sec:.3}s – {end_sec:.3}s  ({duration:.3}s)");
78    println!("Output:   {output}");
79    println!();
80    println!("Trimming (stream-copy, no re-encode)…");
81
82    StreamCopyTrimmer::new(&input, start_sec, end_sec, &output)
83        .run()
84        .unwrap_or_else(|e| {
85            eprintln!("Error: {e}");
86            process::exit(1);
87        });
88
89    let size = match std::fs::metadata(&output) {
90        Ok(m) => {
91            #[allow(clippy::cast_precision_loss)]
92            let kb = m.len() as f64 / 1024.0;
93            if kb < 1024.0 {
94                format!("{kb:.0} KB")
95            } else {
96                format!("{:.1} MB", kb / 1024.0)
97            }
98        }
99        Err(_) => "(unknown size)".to_string(),
100    };
101
102    println!("Done. {output}  {size}");
103}