244 lines
9.3 KiB
Markdown
Executable File
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)
|