Skip to main content

rff_cli/
ffprobe.rs

1//! The `ffprobe` binary's logic: inspect one input and print its container +
2//! streams, in human-readable form or as JSON (`-of json`).
3//!
4//! `-show_targets` adds the other half of the question: not just what the file
5//! *is*, but every format this build can convert it *into*, and what each
6//! conversion would cost. It is computed from the engine's own registries by
7//! `rff::targets`, so it never promises an output the transcoder would refuse.
8
9use std::process::ExitCode;
10
11use rff::probe::MediaInfo;
12use rff::Engine;
13
14/// Entry point used by `src/bin/ffprobe.rs`.
15pub fn run(args: Vec<String>) -> ExitCode {
16    let mut hide_banner = false;
17    let mut want_version = false;
18    let mut as_json = false;
19    let mut show_targets = false;
20    let mut input: Option<String> = None;
21
22    let mut i = 0;
23    while i < args.len() {
24        let arg = &args[i];
25        match arg.as_str() {
26            "-hide_banner" => hide_banner = true,
27            "-version" => want_version = true,
28            // Presence-only flags we accept for compatibility.
29            "-show_format" | "-show_streams" | "-show_entries" => {}
30            "-show_targets" => show_targets = true,
31            "-of" | "-print_format" => {
32                i += 1;
33                as_json = args.get(i).map(|v| v == "json").unwrap_or(false);
34            }
35            "-v" | "-loglevel" => {
36                i += 1; // consume and ignore the level
37            }
38            "-i" => {
39                i += 1;
40                input = args.get(i).cloned();
41            }
42            other if !other.starts_with('-') => input = Some(other.to_string()),
43            other => eprintln!("[warning] unrecognized option `{other}` (ignored)"),
44        }
45        i += 1;
46    }
47
48    if !hide_banner {
49        eprint!("{}", crate::ffmpeg::banner());
50    }
51    if want_version {
52        crate::ffmpeg::print_version();
53        return ExitCode::SUCCESS;
54    }
55
56    let Some(input) = input else {
57        eprintln!("[error] no input file specified. Usage: ffprobe [options] INPUT");
58        return ExitCode::FAILURE;
59    };
60
61    let engine = Engine::new();
62    if show_targets {
63        return match rff::targets::targets(&engine, &input) {
64            Ok(plan) => {
65                if as_json {
66                    println!("{}", plan.to_json());
67                } else {
68                    print_targets(&input, &plan);
69                }
70                ExitCode::SUCCESS
71            }
72            Err(err) => {
73                eprintln!("[error] {input}: {err}");
74                ExitCode::FAILURE
75            }
76        };
77    }
78    match rff::probe::probe(&engine, &input) {
79        Ok(info) => {
80            if as_json {
81                print_json(&input, &info);
82            } else {
83                print_human(&input, &info);
84            }
85            ExitCode::SUCCESS
86        }
87        Err(err) => {
88            eprintln!("[error] {input}: {err}");
89            ExitCode::FAILURE
90        }
91    }
92}
93
94/// `-show_targets`: every output this build can write for the input, grouped by
95/// kind, with the command that produces each one.
96///
97/// `!` lines are the caveats (a dropped stream, a second lossy generation);
98/// `$` lines are runnable as printed.
99fn print_targets(input: &str, plan: &rff::targets::Plan) {
100    let stem = std::path::Path::new(input)
101        .file_stem()
102        .and_then(|s| s.to_str())
103        .unwrap_or("out");
104
105    println!(
106        "Input #0, {}, from '{input}':",
107        plan.source_format.as_deref().unwrap_or("?")
108    );
109    for s in &plan.source {
110        println!("  Stream #0:{}: {}: {}", s.index, s.media_type, s.codec_id);
111    }
112
113    println!("
114Convert to ({} targets):", plan.targets.len());
115    let mut kind = None;
116    for t in &plan.targets {
117        if kind != Some(t.kind) {
118            println!("
119  {}", t.kind);
120            kind = Some(t.kind);
121        }
122        println!(
123            "    .{:<6} {:<9} {}",
124            t.extension,
125            t.fidelity,
126            t.stream_summary()
127        );
128        for note in &t.notes {
129            println!("               ! {note}");
130        }
131        println!("               $ {}", t.command(input, stem));
132    }
133}
134
135/// FFprobe-style human output.
136fn print_human(input: &str, info: &MediaInfo) {
137    println!("Input #0, {}, from '{}':", info.format_name, input);
138    for stream in &info.streams {
139        let detail = match stream.media_type {
140            rff_core::MediaType::Video => {
141                format!(
142                    "Video: {}, {}x{}",
143                    stream.codec_id, stream.width, stream.height
144                )
145            }
146            rff_core::MediaType::Audio => format!(
147                "Audio: {}, {} Hz, {} ch",
148                stream.codec_id, stream.sample_rate, stream.channels
149            ),
150            other => format!("{other}: {}", stream.codec_id),
151        };
152        println!("  Stream #0:{}: {detail}", stream.index);
153    }
154}
155
156/// Minimal JSON output (hand-rolled to keep the CLI dependency-light).
157fn print_json(input: &str, info: &MediaInfo) {
158    println!("{{");
159    println!("  \"format\": {{");
160    println!("    \"filename\": {},", json_str(input));
161    println!("    \"format_name\": {},", json_str(&info.format_name));
162    println!("    \"nb_streams\": {}", info.streams.len());
163    println!("  }},");
164    println!("  \"streams\": [");
165    for (idx, stream) in info.streams.iter().enumerate() {
166        let comma = if idx + 1 < info.streams.len() {
167            ","
168        } else {
169            ""
170        };
171        println!("    {{");
172        println!("      \"index\": {},", stream.index);
173        println!(
174            "      \"codec_type\": {},",
175            json_str(&stream.media_type.to_string())
176        );
177        println!(
178            "      \"codec_name\": {},",
179            json_str(stream.codec_id.name())
180        );
181        println!("      \"width\": {},", stream.width);
182        println!("      \"height\": {},", stream.height);
183        println!("      \"sample_rate\": {},", stream.sample_rate);
184        println!("      \"channels\": {}", stream.channels);
185        println!("    }}{comma}");
186    }
187    println!("  ]");
188    println!("}}");
189}
190
191/// Escape a string as a JSON string literal (quotes + backslashes).
192fn json_str(s: &str) -> String {
193    let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
194    format!("\"{escaped}\"")
195}