mirror of
https://git.pumpin.online/funkpower/catena.git
synced 2026-10-23 03:07:13 +00:00
avplan: paths an encoder can actually read, and a pipeline that runs
This commit is contained in:
@@ -55,6 +55,13 @@ impl Encoder {
|
||||
/// Only VAAPI needs any: the device has to be opened before the file is,
|
||||
/// so a filter can upload frames to it.
|
||||
pub fn before_input(self, vaapi_device: &str) -> Vec<String> {
|
||||
// No device named is the default device, not a device with no name:
|
||||
// `vaapi=va:` is rejected, and an empty argument is how a shell
|
||||
// passes on a variable nobody set.
|
||||
let vaapi_device = match vaapi_device.is_empty() {
|
||||
true => crate::DEFAULT_VAAPI_DEVICE,
|
||||
false => vaapi_device,
|
||||
};
|
||||
match self {
|
||||
Encoder::H264Vaapi => vec![
|
||||
"-init_hw_device".into(),
|
||||
@@ -216,6 +223,16 @@ Encoders:
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_device_named_means_the_usual_one() {
|
||||
// A shell passing on an unset variable gives an empty argument, and
|
||||
// an encoder asked to open a device with no name refuses to start.
|
||||
assert_eq!(
|
||||
Encoder::H264Vaapi.before_input(""),
|
||||
Encoder::H264Vaapi.before_input(crate::DEFAULT_VAAPI_DEVICE)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copying_is_not_encoding() {
|
||||
assert!(!Encoder::Copy.is_hardware());
|
||||
|
||||
@@ -110,10 +110,25 @@ pub fn container_plays(extension: Option<&str>) -> bool {
|
||||
}
|
||||
|
||||
/// The extension of a path, lowercased, without the dot.
|
||||
///
|
||||
/// Read off the last segment of the path as written, rather than by asking
|
||||
/// the path library. The library folds `.` and `..` away and then reports the
|
||||
/// extension of whatever they were attached to, which makes `film.mp4/.` —
|
||||
/// a directory — a playable MP4.
|
||||
pub fn container_of(path: &str) -> Option<String> {
|
||||
std::path::Path::new(path)
|
||||
.extension()
|
||||
.map(|extension| extension.to_string_lossy().to_lowercase())
|
||||
let trimmed = path.trim_end_matches(std::path::is_separator);
|
||||
let segment = trimmed.rsplit(std::path::is_separator).next().unwrap_or("");
|
||||
if matches!(segment, "" | "." | "..") {
|
||||
return None;
|
||||
}
|
||||
// A leading dot is a hidden file, not an extension: `.mp4` is a name.
|
||||
match segment.rfind('.') {
|
||||
None | Some(0) => None,
|
||||
Some(at) => match &segment[at + 1..] {
|
||||
"" => None,
|
||||
extension => Some(extension.to_lowercase()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a converted copy goes: beside the original, named for what it is.
|
||||
@@ -122,13 +137,33 @@ pub fn output_beside(path: &str) -> String {
|
||||
let stem = path.file_stem().map(|stem| stem.to_string_lossy());
|
||||
match (path.parent(), stem) {
|
||||
(Some(parent), Some(stem)) if !parent.as_os_str().is_empty() => {
|
||||
format!("{}/{stem}.h264.mp4", parent.display())
|
||||
// The root is the one directory that already ends in a separator,
|
||||
// and a name beginning with two of them means something else
|
||||
// again on some systems.
|
||||
let parent = parent.display().to_string();
|
||||
match parent.ends_with(std::path::MAIN_SEPARATOR) {
|
||||
true => format!("{parent}{stem}.h264.mp4"),
|
||||
false => format!("{parent}/{stem}.h264.mp4"),
|
||||
}
|
||||
}
|
||||
(_, Some(stem)) => format!("{stem}.h264.mp4"),
|
||||
_ => format!("{}.h264.mp4", path.display()),
|
||||
}
|
||||
}
|
||||
|
||||
/// A path an encoder will read as a path.
|
||||
///
|
||||
/// The destination is the one argument with no flag in front of it, so a name
|
||||
/// beginning with a dash would be read as an option and the call would fail
|
||||
/// on a file that is otherwise fine. `./` in front of it names the same file
|
||||
/// and cannot be mistaken for anything.
|
||||
fn runnable(path: &str) -> String {
|
||||
match path.starts_with('-') {
|
||||
true => format!("./{path}"),
|
||||
false => path.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Judge a file and, if it needs converting, say how.
|
||||
///
|
||||
/// `available` is the best encoder the machine has; it is only consulted when
|
||||
@@ -199,7 +234,7 @@ pub fn plan(
|
||||
}
|
||||
// So a player can start before the whole file has arrived.
|
||||
argv.extend(["-movflags".to_string(), "+faststart".to_string()]);
|
||||
argv.push(output.to_string());
|
||||
argv.push(runnable(output));
|
||||
argv
|
||||
}
|
||||
};
|
||||
@@ -220,6 +255,52 @@ pub fn plan(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_directory_that_looks_like_a_file_is_not_one() {
|
||||
// `Path` folds a trailing `.` away and would otherwise read the
|
||||
// extension of the directory it was attached to, which makes a
|
||||
// directory a playable MP4.
|
||||
assert_eq!(container_of("film.mp4/."), None);
|
||||
assert_eq!(container_of("film.mp4/./"), None);
|
||||
assert_eq!(container_of("film.mp4/.."), None);
|
||||
assert_eq!(container_of("film.mp4"), Some("mp4".to_string()));
|
||||
assert_eq!(container_of("film.MP4"), Some("mp4".to_string()));
|
||||
assert_eq!(container_of("film.mp4//"), Some("mp4".to_string()));
|
||||
assert_eq!(container_of("film"), None);
|
||||
// A leading dot is a hidden file rather than an extension.
|
||||
assert_eq!(container_of(".mp4"), None);
|
||||
assert_eq!(container_of("/media/.hidden.mp4"), Some("mp4".to_string()));
|
||||
assert_eq!(container_of("talk."), None);
|
||||
assert_eq!(container_of(""), None);
|
||||
assert_eq!(container_of("/"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_destination_beginning_with_a_dash_is_named_so_it_is_read_as_one() {
|
||||
let found = plan(
|
||||
"-clip.webm",
|
||||
&streams("vp9", "opus", "webm"),
|
||||
Some(Encoder::Libx264),
|
||||
"-clip.h264.mp4",
|
||||
DEFAULT_VAAPI_DEVICE,
|
||||
);
|
||||
assert_eq!(
|
||||
found.argv.last().map(String::as_str),
|
||||
Some("./-clip.h264.mp4")
|
||||
);
|
||||
// The source needs no such help: it comes after `-i`.
|
||||
assert!(found.argv.contains(&"-clip.webm".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_file_at_the_root_does_not_get_two_leading_separators() {
|
||||
// A name beginning with exactly two separators is left to the system
|
||||
// to interpret, and some interpret it as something other than a path.
|
||||
assert_eq!(output_beside("/talk.webm"), "/talk.h264.mp4");
|
||||
assert_eq!(output_beside("/media/talk.webm"), "/media/talk.h264.mp4");
|
||||
assert_eq!(output_beside("talk.webm"), "talk.h264.mp4");
|
||||
}
|
||||
|
||||
fn streams(video: &str, audio: &str, container: &str) -> Streams {
|
||||
Streams {
|
||||
video: Some(video.into()),
|
||||
|
||||
@@ -26,7 +26,7 @@ Exit codes: 0 it needs converting, 1 it plays as it is, 2 could not tell,
|
||||
|
||||
Example:
|
||||
|
||||
for f in *.webm; do avplan \"$f\" | xargs -d '\\n' ffmpeg; done
|
||||
for f in *.webm; do plan=$(avplan \"$f\") && eval \"$plan\"; done
|
||||
avplan --json talk.mkv | jq -r '.reasons[]'"
|
||||
)]
|
||||
struct Args {
|
||||
@@ -67,7 +67,16 @@ fn main() -> ExitCode {
|
||||
}
|
||||
}
|
||||
|
||||
let path = args.path.display().to_string();
|
||||
// The record and the command line both carry the path as text, so a name
|
||||
// that is not text cannot be answered about. Saying so beats printing a
|
||||
// confident command that names a file which does not exist.
|
||||
let Some(path) = args.path.to_str().map(str::to_string) else {
|
||||
eprintln!(
|
||||
"avplan: {}: the name is not valid text",
|
||||
args.path.display()
|
||||
);
|
||||
return ExitCode::from(exit::USAGE);
|
||||
};
|
||||
let Some(video) = probe(&args.path, "v:0") else {
|
||||
eprintln!("avplan: ffprobe could not read {path}");
|
||||
return ExitCode::from(exit::INDETERMINATE);
|
||||
@@ -162,6 +171,9 @@ fn probe(path: &std::path::Path, stream: &str) -> Option<String> {
|
||||
"-of",
|
||||
"default=noprint_wrappers=1:nokey=1",
|
||||
])
|
||||
// Named after `-i` rather than left as a trailing argument, so that a
|
||||
// file whose name begins with a dash is a file and not an option.
|
||||
.arg("-i")
|
||||
.arg(path)
|
||||
.output()
|
||||
.ok()?;
|
||||
|
||||
Reference in New Issue
Block a user