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!(
114        "
115Convert to ({} targets):",
116        plan.targets.len()
117    );
118    let mut kind = None;
119    for t in &plan.targets {
120        if kind != Some(t.kind) {
121            println!(
122                "
123  {}",
124                t.kind
125            );
126            kind = Some(t.kind);
127        }
128        println!(
129            "    .{:<6} {:<9} {}",
130            t.extension,
131            t.fidelity,
132            t.stream_summary()
133        );
134        for note in &t.notes {
135            println!("               ! {note}");
136        }
137        println!("               $ {}", t.command(input, stem));
138    }
139}
140
141/// FFprobe-style human output.
142fn print_human(input: &str, info: &MediaInfo) {
143    println!("Input #0, {}, from '{}':", info.format_name, input);
144    for stream in &info.streams {
145        let detail = match stream.media_type {
146            rff_core::MediaType::Video => {
147                format!(
148                    "Video: {}, {}x{}",
149                    stream.codec_id, stream.width, stream.height
150                )
151            }
152            rff_core::MediaType::Audio => format!(
153                "Audio: {}, {} Hz, {} ch",
154                stream.codec_id, stream.sample_rate, stream.channels
155            ),
156            other => format!("{other}: {}", stream.codec_id),
157        };
158        println!("  Stream #0:{}: {detail}", stream.index);
159    }
160}
161
162/// Minimal JSON output (hand-rolled to keep the CLI dependency-light).
163fn print_json(input: &str, info: &MediaInfo) {
164    println!("{{");
165    println!("  \"format\": {{");
166    println!("    \"filename\": {},", json_str(input));
167    println!("    \"format_name\": {},", json_str(&info.format_name));
168    println!("    \"nb_streams\": {}", info.streams.len());
169    println!("  }},");
170    println!("  \"streams\": [");
171    for (idx, stream) in info.streams.iter().enumerate() {
172        let comma = if idx + 1 < info.streams.len() {
173            ","
174        } else {
175            ""
176        };
177        println!("    {{");
178        println!("      \"index\": {},", stream.index);
179        println!(
180            "      \"codec_type\": {},",
181            json_str(&stream.media_type.to_string())
182        );
183        println!(
184            "      \"codec_name\": {},",
185            json_str(stream.codec_id.name())
186        );
187        println!("      \"width\": {},", stream.width);
188        println!("      \"height\": {},", stream.height);
189        println!("      \"sample_rate\": {},", stream.sample_rate);
190        println!("      \"channels\": {}", stream.channels);
191        println!("    }}{comma}");
192    }
193    println!("  ]");
194    println!("}}");
195}
196
197/// Escape a string as a JSON string literal (quotes + backslashes).
198fn json_str(s: &str) -> String {
199    let escaped = s.replace('\\', "\\\\").replace('"', "\\\"");
200    format!("\"{escaped}\"")
201}