shrivel 0.3.1

Cross-platform FFmpeg orchestrator that transcodes videos to HEVC or AV1
# shrivel

A small cross-platform command-line tool that transcodes videos to
H.265/HEVC or AV1 by orchestrating [FFmpeg](https://ffmpeg.org/). HEVC is the
default; select AV1 with `--codec av1`. Point it at a folder
of videos and shrinks them using a shared compression scale mapped to the
selected encoder.

## Requirements

- `ffmpeg` and `ffprobe` available in your `PATH`.
- Rust (2024 edition) to build from source.

## Installation

```sh
cargo install --path .
```

Or build without installing:

```sh
cargo build --release
# binary: target/release/shrivel
```

## Usage

```sh
# Recursively convert every .mp4 in ./input into ./output
shrivel

# Custom folders and quality
shrivel --input ~/Videos/raw --output ~/Videos/small --cq 28

# Convert one file into the output directory
shrivel --input ~/Videos/clip.mp4 --output ~/Videos/converted

# Resample to 30 fps, denoise, and skip files already using the selected codec
shrivel --fps 30 --denoise --skip-same-codec

# Scale to 1280 pixels width, resample, and denoise
shrivel --scale 1280 --fps 30 --denoise

# Process several container types, forcing the CPU encoder
shrivel --ext mp4,mkv,mov --encoder software

# Encode to AV1 (auto-select hardware when usable, otherwise SVT-AV1 on CPU)
shrivel --codec av1

# Use dynamic CQ to adjust quality based on source bitrate
shrivel --dynamic-cq

# Re-encode audio as Opus, with bitrate chosen from --cq
shrivel --audio-codec opus

# Drop all audio streams
shrivel --no-audio

# Preview FFmpeg commands and estimated output sizes without running them
shrivel --dry-run

# Convert up to two files at once (default: sequential)
shrivel --jobs 2
```

When the input is a directory, it is searched recursively. Converted files
keep their paths relative to the input folder, and the corresponding
subdirectories are created inside the output folder. For example,
`input/trips/day1.mp4` is written to `output/trips/day1.mp4`. When the input is
a single video file, it is written directly inside the output directory with
the same filename.

## Options

| Option | Default | Description |
| --- | --- | --- |
| `--codec <CODEC>` | `hevc` | Output codec: `hevc` or `av1`. |
| `-i, --input <FILE_OR_DIR>` | `input` | Video file or directory containing source videos. |
| `-o, --output <DIR>` | `output` | Directory for converted videos (created if missing). Must differ from the input directory. |
| `--cq <N>` | `26` | Normalized compression value, 0-51. Lower generally means better quality; mapped to the selected encoder's native scale. |
| `--audio-codec <CODEC>` | `copy` | Audio output: `copy`, `mp3`, `aac`, or `opus`. Transcoded bitrates scale with the effective CQ. |
| `--no-audio` | off | Remove audio streams from the output. Takes precedence over `--audio-codec`. |
| `--fps <FPS>` | source | Target frame rate. The source frame rate is kept when omitted. |
| `--skip-same-codec` | off | Skip files already using the selected output codec. |
| `--denoise` | off | Apply a light `hqdn3d=3:3:8:8` denoise filter. |
| `--scale <WIDTH>` | source | Target width for scaling. Height is calculated proportionally. Uses bicubic interpolation. |
| `-e, --encoder <NAME>` | `auto` | One of `auto`, `nvenc`, `qsv`, `amf`, `videotoolbox`, `software`; available choices depend on the codec. |
| `--dynamic-cq` | off | Adjust the compression value dynamically based on the source bitrate to optimize the quality/size ratio. |
| `--ext <EXT,...>` | `mp4` | Comma-separated input extensions, matched case-insensitively. |
| `--skip-existing` | off | Skip files whose output already exists instead of overwriting them. |
| `--dry-run` | off | Print the FFmpeg commands without running them. |
| `--jobs <N>` | `1` | Maximum number of files converted at once. `1` keeps sequential processing. |

The process exits with a non-zero status if at least one file failed to convert.

## How it works

For each input file shrivel:

1. Runs `ffprobe` to read the video codec, bitrate and frame rate. If the video
   stream has no bitrate (common in Matroska files), the container bitrate is
   used instead.
2. Optionally skips the file when it already uses the selected codec (`--skip-same-codec`).
3. Maps `--cq` to the selected encoder's native quality parameter. The mapping
   aligns the usable range endpoints; it does not promise equal perceptual
   quality or file size across codecs and encoders.
4. Optionally adapts the base `--cq` to the average bitrate of the source video (`--dynamic-cq`).
5. Builds the video filter chain (denoise, then frame-rate change). A frame-rate
   change is applied only when the source differs from the target by more than
   0.5 fps, and it forces constant-frame-rate output to avoid leftover
   variable-frame-rate drift.
6. Runs FFmpeg, mapping the first video stream, all subtitle streams, and
   (unless `--no-audio` is set) all audio streams. Audio is copied by default.
   `--audio-codec` can re-encode audio as MP3, AAC, or Opus; its bitrate is
   scaled from the effective CQ (including any `--dynamic-cq` adjustment).
   `--no-audio` removes audio. Subtitles are copied; other streams (data) are
   dropped. Subtitle copying may fail if a subtitle format is not supported by
   the output container. If `--dry-run` is set, it instead prints the commands
   and provides a heuristic estimate of the resulting file size based on source
   bitrate, CQ, codec, and any resolution or FPS changes.
7. Validates the output by comparing its duration with the source. If the
   output is truncated or corrupted, the file is removed and the conversion
   is marked as failed.
8. Reports the input and output sizes. If FFmpeg fails, the partial output file
   is removed and the first lines of FFmpeg's error output are shown.

HEVC files in `.mp4`, `.m4v` and `.mov` outputs are tagged `hvc1` so they play
on Apple devices. AV1 outputs do not receive this HEVC-specific tag.

## Encoders

With `--encoder auto` (the default), shrivel tries each hardware encoder
supported by the selected codec in turn by encoding one tiny synthetic frame,
and uses the first one that works. If none works it uses software x265 for
HEVC or SVT-AV1 for AV1. An encoder being listed by
`ffmpeg -encoders` is not enough, because hardware encoders are often compiled
in but unusable on the current machine.

| Encoder | FFmpeg codec | Native quality range used by `--cq` |
| --- | --- | --- |
| `nvenc` | `hevc_nvenc`, `av1_nvenc` | HEVC 1-51; AV1 1-63. 0 means automatic in NVENC, so shrivel avoids it. |
| `videotoolbox` | `hevc_videotoolbox` | HEVC `-q:v` 100 (best) to 1 (most compression); HEVC only. |
| `qsv` | `hevc_qsv`, `av1_qsv` | 1-51; 1 is best quality in QSV ICQ mode. |
| `amf` | `hevc_amf`, `av1_amf` | HEVC QP 0-51; AV1 QP 0-255. |
| `software` | `libx265`, `libsvtav1` | x265 CRF 0-51; SVT-AV1 CRF 0-63. FFmpeg must include the selected encoder. |

The normalized `--cq` scale runs from 0 (best quality, usually larger output)
to 51 (strongest compression, usually smaller output). shrivel maps it linearly onto each
backend's usable native range, reversing the direction for VideoToolbox. The
same value still does not imply equal quality or output size across codecs and
encoders: rate-control behavior and perceptual quality differ. Check a short
clip when changing backend or codec. SVT-AV1 uses preset 6 by default.
Hardware AV1 encoding requires support from both FFmpeg and the system
hardware/runtime.