OpenTranscode/README.md

244 lines
9.3 KiB
Markdown
Executable File

# OpenTranscode
**Open-source batch video transcoder for Linux.** Encodes folders of video
files to AV1 / VP9 / HEVC with configurable audio codecs, resolution
scaling, and source-file management. Built on ffmpeg (default) with an
optional av1an chunk-parallel path for users with a working VapourSynth
setup.
- **Default encoder**: ffmpeg + libsvtav1 (reliable across distros)
- **Optional encoder**: av1an chunk-parallel (opt-in via UI toggle)
- **Codecs**: AV1 (SVT-AV1), VP9, x265 (HEVC) video; Opus, Vorbis, FLAC, IAMF audio
- **Containers**: MKV, WebM, MP4
- **Resolution**: Original or scaled (16:9, 21:9, 32:9 presets from 480p to 4K)
- **Skip-existing**: Probes output with ffprobe; skips files whose codec matches
- **Audio normalization**: Per-file loudness analysis with volume gain
- **Subtitle mux**: Optional English subtitle passthrough
- **Source management**: Optional verified-source deletion after encode
## Requirements
- Linux (POSIX)
- Python ≥ 3.12
- ffmpeg (with libsvtav1, libvpx, libx265, libopus, libvorbis, flac)
- ffprobe
- PySide6 (for the GUI)
- Optional: av1an + VapourSynth (only if using the av1an toggle)
## Quick start
### Install as a package (recommended)
```bash
cd /path/to/opentranscode
pip install -e .
opentranscode # launch the GUI
python -m opentranscode --version # → opentranscode 4.4.3
python -m opentranscode --help
```
### Run the launcher script (backwards compat)
```bash
python open-transcode.py # launches the GUI
```
### Verify environment without encoding
```bash
opentranscode --dry-run # probe + smoke test, no encode
opentranscode --verify-only FILE.mkv # re-verify an existing output
```
## Usage
### Default (ffmpeg-only, recommended)
```bash
opentranscode
```
- Encodes with `ffmpeg -c:v libsvtav1` (or libvpx-vp9 / libx265 based on selection)
- Single-pass per file
- Reliable across distros; no VapourSynth dependency
### Skip-existing (default ON)
Files whose output already exists with a matching video+audio codec are
skipped. Detection uses ffprobe — verifies `codec_name` for both video
and audio streams, plus resolution when scaling is requested.
```bash
opentranscode # skip-existing ON (default)
opentranscode --force-reencode # re-encode everything
```
### Verbose logging
Default log output is minimal — two lines per file (start + finish):
```
Found 180 file(s) to process.
[1/180] filename.mkv
[1/180] filename.mkv — OK: 1.6MB -> 1.3MB (81%)
[2/180] already_done.mkv — SKIP (already av1/opus)
[3/180] next.mkv
[3/180] next.mkv — OK: 2.4MB -> 1.8MB (75%)
QUEUE COMPLETE. Success: 178, Failed: 0, Skipped: 2.
```
For diagnostics (CMD lines, live tail of ffmpeg/av1an stderr, disk-space
warnings, heartbeats):
```bash
opentranscode --verbose
```
### Massive-file support
For 30GB+ BluRay rips:
- **24-hour per-file timeout** (configurable via `--timeout SECONDS`)
- **1KB absolute integrity minimum** (no false "output too small" failures
on high-bitrate sources — duration check is the real gate)
- **Disk-space warnings** (verbose only) for output and temp partitions
```bash
opentranscode --timeout 36000 # 10h per-file timeout
```
### av1an chunk-parallel (opt-in)
For users with a working VapourSynth + source plugin (lsmash, ffms2,
bestsource) setup who want scene-detection-based chunk-parallel encoding:
- **UI**: Check the "av1an (chunk-parallel)" checkbox
- **CLI**: `opentranscode --use-av1an`
When av1an fails per-file (concat failures, scene-detection panics),
the code automatically falls back to ffmpeg for that file. When av1an
fails systematically (VSScript API mismatch, missing encoder), the queue
aborts with an actionable diagnostic.
## CLI reference
```
opentranscode [--version] [--dry-run] [--verify-only PATH] [--force]
[--chunk-method METHOD] [--max-workers N] [--threads-per-worker N]
[--use-av1an] [--verbose] [--skip-existing | --force-reencode]
[--timeout SECONDS]
```
| Flag | Default | Description |
|------|---------|-------------|
| `--version` | — | Print version and exit |
| `--dry-run` | — | Probe environment + smoke test, no encode |
| `--verify-only PATH` | — | Re-verify an existing output file |
| `--force` | off | Skip ffprobe pre-validation |
| `--chunk-method METHOD` | auto | Force av1an chunk method (select, hybrid, ffms2, lsmash, bestsource, dgdecnv) |
| `--max-workers N` | auto | Cap chunk-parallel worker count |
| `--threads-per-worker N` | auto | Per-encoder thread cap |
| `--use-av1an` | off | Use av1an chunk-parallel (UI toggle also available) |
| `--verbose` | off | Full tech-detail log output |
| `--skip-existing` | on | Skip files whose output has matching codec |
| `--force-reencode` | off | Re-encode everything |
| `--timeout SECONDS` | 86400 | Per-file encode timeout (24h default) |
## Architecture
```
opentranscode/
├── __init__.py # Package metadata + lazy launch_gui wrapper
├── __main__.py # python -m opentranscode entry point
├── cli.py # argparse + dry-run + verify-only
├── codec_profiles.py # VideoCodecProfile, AudioProfile, ContainerProfile tables
├── encoder_worker.py # QThread-based per-file encode pipeline
├── env_probe.py # Distro + binary + library + av1an detection
├── ffprobe_utils.py # ffprobe_validate, ffprobe_duration, file-type ID
├── temp_manager.py # Per-worker temp directory isolation
├── cpu_topology.py # Physical core / logical thread detection
├── distro_probe.py # Distro family + package manager detection
├── keepawake.py # systemd-inhibit + optional mouse nudge
├── source_builder.py # From-git rebuild for VapourSynth/av1an ABI mismatches
├── license_registry.py # Third-party license attribution
├── ui_window.py # PySide6 main window + launch_gui
├── ui_theme.py # Retro-futuristic QSS theme
└── widgets/ # Custom Qt widgets (radio_knob, etc.)
open-transcode.py # Launcher script (mirrors package, test target)
pyproject.toml # PEP 621 build config
tests/ # 149 tests across 14 files
```
### Encode pipeline
1. **Environment probe** — detects distro, ffmpeg/ffprobe/av1an paths,
encoder library availability, VapourSynth + source plugins, CPU topology
2. **Pre-flight validation** — ffprobe scans all input files; reports valid
vs invalid counts before encoding starts
3. **Per-file pipeline**:
- `_validate_file` — ffprobe pre-check (skip if invalid)
- `_check_disk_space` — warn (verbose) if free space < source size
- `_output_already_encoded` skip if output exists with matching codec
- `_prepare_input` pre-scale (if requested) or symlink to temp
- `_encode_one` dispatch to ffmpeg (default) or av1an (opt-in)
- `_run_with_stop_check` subprocess with STOP-button interrupt support
- `_verify_and_finalize` duration check (≥95%), subtitle mux, source deletion
4. **Final cleanup** sweep per-worker temp dir, delete verified sources
### Thread safety
- Each `EncoderWorker` runs in its own `QThread`
- Per-worker temp subdirectory (`~/.cache/OpenTranscode/tmp/worker-<pid>/`)
created with mode 0700 (SEI CERT FIO09-C)
- Process-group signaling (`start_new_session=True` + `os.killpg`) reaches
av1an's child encoders (SvtAv1EncApp / vpxenc / x265)
- Drainer threads read stdout/stderr continuously to prevent pipe-buffer
deadlock (same pattern as `subprocess.run._communicate`)
### Coding standards
The codebase follows:
- **PEP 868** — parameterized type hints (`dict[str, object]`, not `Dict[str, object]`)
- **SEI CERT** — MSC04-C (single source of truth for diagnostics), FIO09-C
(secure temp directory), ERR01-C (narrow exception scope), STR09-C (no
substring matches for encoder names)
- **POSIX** — `start_new_session=True` for process-group signaling,
`signal.SIGTERM``SIGKILL` escalation, `os.killpg` for child cleanup
- **MISRA** (where applicable to Python) — single exit point per function
where practical, no early returns from `try` blocks without cleanup
## Testing
```bash
python -m pytest tests/ -q # 149 tests, ~10s
python -m pytest tests/ -v # verbose
python -m pytest tests/ -k "skip_existing" # subset
```
Test categories:
- **Smoke tests** — av1an VSScript compatibility probe
- **Encoder pipeline** — `_validate_file`, `_prepare_input`, `_verify_and_finalize`
- **Real-encode e2e** — generates real test videos with ffmpeg, runs the full pipeline
- **Chunk-method retry** — y4m pipe break recovery
- **Stop button** — SIGTERM/SIGKILL on process group
- **Concurrent workers** — per-PID temp directory isolation
- **Skip-existing** — codec matching, ffprobe failure, resolution mismatch
- **Massive files** — timeout flag, 1KB integrity threshold, disk-space checks
- **Package structure** — public API surface, submodule imports, CLI parser
## License
AGPL-3.0-or-later. See [LICENSE](LICENSE).
Third-party tools invoked (not bundled): ffmpeg, ffprobe, av1an,
VapourSynth, SvtAv1EncApp, vpxenc, x265, mkvmerge. Licenses flow
through from upstream.
## Project
- **Repository**: https://git.dcos.net/dcosnet/OpenTranscode
- **Issues**: https://git.dcos.net/dcosnet/OpenTranscode/issues
- **Changelog**: [CHANGELOG.md](CHANGELOG.md)