1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
|
# transcode.sh
> Batch transcode helper for media files using [ffmpeg](https://ffmpeg.org/).
`transcode.sh` re-encodes media files in place using user-defined *presets* —
small shell snippets that supply the ffmpeg arguments. It probes each input
file to detect the video codec and pixel format, skips files whose codec is on
the skip list, selects an appropriate output pixel format automatically, and
replaces the original atomically on success.
## Features
- **Preset-driven** — encoding parameters live in plain shell files, not
hard-coded flags.
- **Batch processing** — pass files on the command line or via a list file
(`-f`).
- **Codec skip list** — per-user and per-invocation lists of codecs to leave
untouched (e.g. skip files already encoded as AV1).
- **Only-if-smaller mode** (`-l`) — discard the re-encoded file if it is
larger than the original.
- **Savings log** (`-s`) — append filename, original size, new size and
percentage saved to a log file after each successful encode.
- **Dry-run** (`-n`) — preview what would happen without touching any files.
- **Nice** — runs ffmpeg at niceness 19 by default to avoid starving other
processes; configurable with `-N`.
- **Respects [NO\_COLOR](https://no-color.org/)**.
## Requirements
| Tool | Required | Notes |
|------|----------|-------|
| `ffmpeg` | always | encoding |
| `ffprobe` | always | codec/format detection |
| `nice` | always | process priority control |
| `bc` | only with `--saving` | percentage calculation |
| `tomlq` | only when `config.toml` is present | TOML config parsing |
## Installation
```sh
# Clone the repo
git clone https://codeberg.org/yourname/transcode.sh.git
cd transcode.sh
# Make the script executable
chmod +x transcode.sh
# Optional: install to a directory in your PATH
install -Dm755 transcode.sh ~/.local/bin/transcode.sh
# Optional: install the man page
install -Dm644 transcode.1 ~/.local/share/man/man1/transcode.1
# Optional: install bash completion
install -Dm644 transcode.sh.bash-completion \
/etc/bash_completion.d/transcode.sh
# or for your user only (requires bash-completion ≥ 2.2):
install -Dm644 transcode.sh.bash-completion \
~/.local/share/bash-completion/completions/transcode.sh
```
> **PATH note:** the script resets `PATH` to `/bin:/usr/bin:/usr/local/bin`
> for security. If your `ffmpeg` lives elsewhere (e.g. Homebrew on macOS,
> Nix), prepend its directory:
>
> ```sh
> PATH="/opt/homebrew/bin:$PATH" transcode.sh input.mp4
> ```
## Configuration
### Presets
Presets live in:
```
${XDG_CONFIG_HOME:-$HOME/.config}/transcode.sh/presets/<name>.sh
```
Each preset is sourced as Bash and must define an array named `ffargs`.
The following variables are available to presets at source time:
| Variable | Description |
|----------|-------------|
| `input_codec` | Video codec of the input file (e.g. `h264`, `hevc`) |
| `input_pixel_format` | Pixel format of the input after `yuvj*` normalisation (e.g. `yuv420p`) |
| `output_pixel_format` | Recommended output pixel format, derived from the input to preserve chroma subsampling and bit depth |
| `input_frame_rate` | Raw frame rate fraction of the input file as reported by `ffprobe` (e.g. `30000/1001`, `25/1`) |
| `input_fps` | `input_frame_rate` rounded to the nearest integer (e.g. `30`, `25`, `60`) |
| `output_gop_size` | Recommended GOP size derived from `input_fps` (≈ 5 s of video, capped at 300 frames) |
Before the preset's `ffargs` are appended, the script always passes
these base arguments to `ffmpeg`:
```
-map 0 # include all streams from the input
-map_metadata 0 # copy all container-level metadata
-map_chapters 0 # copy all chapter markers
-c copy # default to stream copy for every stream
```
A preset therefore only needs to specify the streams it wishes to
re-encode (typically `-c:v:0`) and any associated encoder options.
All other streams — audio, subtitles, attachments — are passed through
unchanged unless the preset explicitly overrides them.
**Example — `~/.config/transcode.sh/presets/default.sh`:**
```sh
ffargs=(
-c:v:0 libsvtav1
-crf 30
-preset 6
-pix_fmt "$output_pixel_format"
)
```
> **Security:** presets are executed as shell code. Only use presets from
> trusted sources.
### Configuration file
An optional TOML configuration file can be placed at:
```
${XDG_CONFIG_HOME:-$HOME/.config}/transcode.sh/config.toml
```
Use `--config-file FILE` to load an alternative path. CLI flags always take
precedence over values in the config file. The file must be valid TOML — an
unparseable file is a hard error. Requires `tomlq` when the file is present.
**Example `config.toml`:**
```toml
[encoding]
preset = "av1"
nice = 10
[skip]
codecs = ["av1", "hevc"]
[saving]
enabled = true
file = "transcode_savings"
[output]
verbose = false
quiet = false
```
| Section | Key | Type | CLI equivalent |
|---------|-----|------|----------------|
| `[encoding]` | `preset` | string | `-p` |
| `[encoding]` | `nice` | integer | `-N` |
| `[encoding]` | `verify_output` | boolean | `--verify-output` |
| `[skip]` | `codecs` | array of strings | `-S` (merged with CLI) |
| `[saving]` | `enabled` | boolean | `-s` |
| `[saving]` | `file` | string | `--saving-file` |
| `[output]` | `verbose` | boolean | `-v` |
| `[output]` | `quiet` | boolean | `-q` |
> **Note:** `[skip] codecs` from the config file and `--skip-codec` from the
> CLI are **merged** — both sources contribute to the skip list.
### Codec skip list (deprecated)
The legacy `skip.conf` file is no longer read. If it still exists, a
deprecation warning is printed at startup. Migrate its contents to
`[skip] codecs` in `config.toml` and delete the old file.
## Usage
```
transcode.sh [OPTION] [--] FILE...
```
| Option | Description |
|--------|-------------|
| `-c`, `--continue` | Continue to next file if ffmpeg fails |
| `--no-continue` | Do not continue with the next file if ffmpeg fails |
| `--verify-output` | Probe output with ffprobe before replacing original (default) |
| `--no-verify-output` | Skip the post-encode integrity check |
| `--config-file FILE`, `--config-file=FILE` | Load configuration from FILE instead of the default path |
| `-f PATH`, `--encode-file PATH`, `--encodefile=PATH` | Read file list from PATH (one per line) |
| `-n`, `--dry-run` | Show what would be done; don't run ffmpeg |
| `-N VALUE`, `--nice VALUE`, `--nice=VALUE` | Niceness value for ffmpeg (default: 19) |
| `-s`, `--saving` | Log filesize savings after each encode |
| `--saving-file PATH`, `--saving-file=PATH` | Savings log path (default: `transcode_savings`) |
| `-S LIST`, `--skip-codec LIST`, `--skip-codec=LIST` | Codecs to skip (comma/space/colon separated) |
| `-l`, `--only-if-smaller` | Only replace original if new file is smaller |
| `--no-only-if-smaller` | Do replace original even if new file is bigger |
| `-p NAME`, `--preset NAME`, `--preset=NAME` | Preset to use (default: `default`) |
| `-q`, `--quiet` | Suppress all output (overrides `-v`) |
| `-v`, `--verbose` | More detailed output |
| `--color` / `--no-color` | Force or disable colored output |
| `--version` | Print version information |
| `-h`, `-?`, `--help` | Show help and exit |
### Examples
```sh
# Transcode a single file with the default preset
transcode.sh video.mp4
# Transcode a list of files and record size savings
transcode.sh --saving --encodefile list.txt
# Dry-run with verbose output to inspect detected formats
transcode.sh --dry-run --verbose my_movie.mkv
# Use a custom preset, only keep result if smaller, skip AV1 inputs
transcode.sh --preset av1_fast --only-if-smaller --skip-codec av1 /media/films/*.mkv
# Run at a moderate priority and continue past failures
transcode.sh --nice 10 --continue *.mp4
# Load an alternative config file
transcode.sh --config-file ~/profiles/fast.toml input.mp4
```
## Debugging
```sh
# High-level state messages
DEBUG=1 transcode.sh input.mkv
# Full bash execution trace
TRACE=1 transcode.sh input.mkv
```
## Exit codes
| Code | Meaning |
|------|---------|
| 0 | Success |
| 1 | Runtime failure (ffmpeg error on one or more files) |
| 2 | Usage error (unknown option, missing argument) |
| 3 | Configuration error (missing or invalid preset) |
| 127 | Missing dependency |
## License
[BSD-3-Clause](LICENSES/BSD-3-Clause.txt) © 2025 Dennis Fink \<me+coding@dennisfink.me\>
|