2026-09-19 15:07:33 -04:00
|
|
|
# avplan
|
|
|
|
|
|
|
|
|
|
Says whether a recording plays as it is, and prints the one `ffmpeg` call that
|
|
|
|
|
fixes it when it does not.
|
|
|
|
|
|
|
|
|
|
Part of [catena](../../README.md).
|
|
|
|
|
|
|
|
|
|
## What it does
|
|
|
|
|
|
|
|
|
|
Reads what a file's streams actually are and does the least that fixes them.
|
|
|
|
|
An already-H.264 picture is copied rather than re-encoded, which is the
|
|
|
|
|
difference between forty minutes and forty seconds.
|
|
|
|
|
|
|
|
|
|
It runs nothing. What comes out is a command to read, pipe, or run — which is
|
|
|
|
|
the point: a re-encode is expensive and the original is irreplaceable.
|
|
|
|
|
|
|
|
|
|
## When you would use it
|
|
|
|
|
|
|
|
|
|
- **Before a flight.** Convert the six files that need it rather than the
|
|
|
|
|
ninety in the directory.
|
|
|
|
|
- **Sorting a download folder.** A file that plays needs nothing; knowing
|
|
|
|
|
which ones those are saves the rest of the work.
|
|
|
|
|
- **Diagnosing a video that opens to a black screen** — or plays with no
|
|
|
|
|
sound at all, which is what Opus in an MP4 looks like on a device that
|
|
|
|
|
cannot decode it.
|
|
|
|
|
|
|
|
|
|
## Install
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
cargo install --git https://git.pumpin.online/funkpower/catena avplan
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Needs `ffprobe` and `ffmpeg` on `PATH`. They ship together.
|
|
|
|
|
|
|
|
|
|
## Example
|
|
|
|
|
|
|
|
|
|
A VP9/Opus recording in a container nothing will open:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
$ avplan lecture.webm
|
|
|
|
|
ffmpeg -nostdin -y -i lecture.webm -c:v h264_videotoolbox -q:v 60 -profile:v high -pix_fmt yuv420p -c:a aac -b:a 160k -ac 2 -movflags +faststart lecture.h264.mp4
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Nothing to do:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
$ avplan talk.mp4
|
|
|
|
|
$ echo $?
|
|
|
|
|
1
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
An H.264 picture in a Matroska container, with the sound in the wrong codec.
|
|
|
|
|
The picture is copied — only the sound is re-encoded, and the file is rewritten
|
|
|
|
|
into a container that opens:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
$ avplan film.mkv
|
|
|
|
|
ffmpeg -nostdin -y -i film.mkv -c:v copy -c:a aac -b:a 160k -ac 2 -movflags +faststart film.h264.mp4
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Why, rather than what:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
$ avplan --json film.mkv | jq -c '{playable, reasons, encoder}'
|
|
|
|
|
{"playable":false,"reasons":["the sound is opus","a .mkv container cannot be opened"],"encoder":"copy"}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Converting everything in a directory that needs it:
|
|
|
|
|
|
|
|
|
|
```sh
|
2026-09-19 15:31:17 -04:00
|
|
|
for f in *.webm *.mkv; do
|
|
|
|
|
plan=$(avplan "$f") && eval "$plan"
|
|
|
|
|
done
|
2026-09-19 15:07:33 -04:00
|
|
|
```
|
|
|
|
|
|
2026-09-19 15:31:17 -04:00
|
|
|
The line is quoted for a shell, so `eval` runs it as written. A file that
|
|
|
|
|
already plays produces no line and a non-zero status, so `&&` skips it.
|
2026-09-19 15:07:33 -04:00
|
|
|
|
|
|
|
|
## Options
|
|
|
|
|
|
|
|
|
|
| Option | Effect |
|
|
|
|
|
|---|---|
|
|
|
|
|
| `--json` | Emit a record — the streams, the reasons, the arguments |
|
|
|
|
|
| `--output <PATH>` | Where the converted copy goes. Defaults to `<name>.h264.mp4` beside the original |
|
|
|
|
|
| `--encoder <NAME>` | Use this encoder rather than the best one installed |
|
|
|
|
|
| `--vaapi-device <PATH>` | The render node to encode on. Defaults to `/dev/dri/renderD128` |
|
2026-09-19 17:06:17 -04:00
|
|
|
| `--man` | Write this page as a manual page, in roff |
|
2026-09-19 15:07:33 -04:00
|
|
|
|
|
|
|
|
## Exit codes
|
|
|
|
|
|
|
|
|
|
| Code | Meaning |
|
|
|
|
|
|---|---|
|
|
|
|
|
| `0` | It needs converting, and the command is on stdout |
|
|
|
|
|
| `1` | It plays as it is |
|
|
|
|
|
| `2` | `ffprobe` could not read it |
|
|
|
|
|
| `64` | The command line was wrong |
|
|
|
|
|
| `69` | `ffmpeg` or `ffprobe` is not installed, or has no H.264 encoder |
|
|
|
|
|
| `74` | The file is not there |
|
|
|
|
|
|
|
|
|
|
Exit `1` is the good answer, not a failure.
|
|
|
|
|
|
|
|
|
|
## How it decides
|
|
|
|
|
|
|
|
|
|
Three questions, and any one of them failing means the file needs work.
|
|
|
|
|
|
|
|
|
|
**The picture.** H.264 or HEVC plays; anything else — VP9, AV1, VP8, MPEG-4 —
|
|
|
|
|
does not. This is the only expensive question: a picture that already plays is
|
|
|
|
|
copied through untouched.
|
|
|
|
|
|
|
|
|
|
**The sound.** AAC plays. Opus in an MP4 is the quiet failure worth naming,
|
|
|
|
|
because the file opens, the picture runs, and there is simply no sound.
|
|
|
|
|
|
|
|
|
|
**The container.** `.mp4`, `.m4v` and `.mov` can be opened. Matroska cannot,
|
|
|
|
|
whatever is inside it — which is why a `.mkv` full of H.264 and AAC still has
|
|
|
|
|
to be rewritten, though rewriting it is nearly free.
|
|
|
|
|
|
|
|
|
|
## Encoders
|
|
|
|
|
|
|
|
|
|
The encoder is found rather than configured: `ffmpeg -encoders` is asked what
|
|
|
|
|
this machine has, and the best of them is taken.
|
|
|
|
|
|
|
|
|
|
| Encoder | Where |
|
|
|
|
|
|---|---|
|
|
|
|
|
| `h264_nvenc` | NVIDIA |
|
|
|
|
|
| `h264_videotoolbox` | Apple silicon and recent Macs |
|
|
|
|
|
| `h264_vaapi` | AMD and Intel graphics |
|
|
|
|
|
| `libx264` | Software. Always there, and slower by a wide margin |
|
|
|
|
|
|
|
|
|
|
Hardware first, because the difference on a two-hour recording is minutes
|
|
|
|
|
against most of an hour. Each gets asked for roughly the same quality in
|
|
|
|
|
whichever of the four quality dialects it speaks:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
$ avplan --encoder libx264 lecture.webm
|
|
|
|
|
ffmpeg -nostdin -y -i lecture.webm -c:v libx264 -preset veryfast -crf 23 -pix_fmt yuv420p -c:a aac -b:a 160k -ac 2 -movflags +faststart lecture.h264.mp4
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
An `ffmpeg` built without any of the four cannot produce H.264 at all, and
|
|
|
|
|
`avplan` says so with exit `69` rather than printing a command that would
|
|
|
|
|
fail.
|
|
|
|
|
|
|
|
|
|
## As a library
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
let streams = avplan::Streams {
|
|
|
|
|
video: Some("vp9".into()),
|
|
|
|
|
audio: Some("opus".into()),
|
|
|
|
|
container: avplan::container_of("lecture.webm"),
|
|
|
|
|
};
|
|
|
|
|
let plan = avplan::plan(
|
|
|
|
|
"lecture.webm",
|
|
|
|
|
&streams,
|
|
|
|
|
Some(avplan::Encoder::Libx264),
|
|
|
|
|
"lecture.h264.mp4",
|
|
|
|
|
avplan::DEFAULT_VAAPI_DEVICE,
|
|
|
|
|
);
|
|
|
|
|
assert!(!plan.playable);
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`video_plays`, `audio_plays` and `container_plays` answer the three questions
|
|
|
|
|
on their own, for anything that needs one of them without the rest.
|
|
|
|
|
|
2026-09-19 15:31:17 -04:00
|
|
|
## The record
|
|
|
|
|
|
|
|
|
|
`--json` writes one of these, whatever the verdict:
|
|
|
|
|
|
|
|
|
|
| Field | |
|
|
|
|
|
|---|---|
|
|
|
|
|
| `path` | The file it is about |
|
|
|
|
|
| `playable` | Whether it plays as it stands |
|
|
|
|
|
| `streams` | `video`, `audio` and `container`, as they were found |
|
|
|
|
|
| `reasons` | Why it does not play, in the order the reasons were found. Empty when it does |
|
|
|
|
|
| `encoder` | Which encoder the picture would go through, or `copy`. Absent when nothing needs doing |
|
|
|
|
|
| `copies_audio` | Whether the sound is copied rather than re-encoded |
|
|
|
|
|
| `argv` | The call that would fix it, argument by argument. Empty when nothing needs doing |
|
|
|
|
|
|
|
|
|
|
It is a shape of its own rather than one of [the records](../../docs/records.md)
|
|
|
|
|
the tools pass between them, because nothing downstream consumes it.
|
|
|
|
|
|
2026-09-19 15:07:33 -04:00
|
|
|
## See also
|
|
|
|
|
|
|
|
|
|
- [`cuenorm`](../cuenorm/README.md) — the subtitles that go with the recording.
|