# 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.5.0 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-/`) 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)