A batch transcoding GUI for Linux built with PySide6.
Go to file
Jeremy Anderson 1df68fe785 A batch transcoding GUI for Linux built with PySide6 2026-07-25 17:32:24 -04:00
logs A batch transcoding GUI for Linux built with PySide6 2026-07-25 17:32:24 -04:00
opentranscode A batch transcoding GUI for Linux built with PySide6 2026-07-25 17:32:24 -04:00
tests A batch transcoding GUI for Linux built with PySide6 2026-07-25 17:32:24 -04:00
CHANGELOG.md A batch transcoding GUI for Linux built with PySide6 2026-07-25 17:32:24 -04:00
LICENSE A batch transcoding GUI for Linux built with PySide6 2026-07-25 17:32:24 -04:00
README.md A batch transcoding GUI for Linux built with PySide6 2026-07-25 17:32:24 -04:00
open-transcode.py A batch transcoding GUI for Linux built with PySide6 2026-07-25 17:32:24 -04:00
pyproject.toml A batch transcoding GUI for Linux built with PySide6 2026-07-25 17:32:24 -04:00
pytest.ini A batch transcoding GUI for Linux built with PySide6 2026-07-25 17:32:24 -04:00

README.md

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

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)

python open-transcode.py             # launches the GUI

Verify environment without encoding

opentranscode --dry-run              # probe + smoke test, no encode
opentranscode --verify-only FILE.mkv # re-verify an existing output

Usage

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.

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):

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
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)
  • POSIXstart_new_session=True for process-group signaling, signal.SIGTERMSIGKILL 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

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.

Third-party tools invoked (not bundled): ffmpeg, ffprobe, av1an, VapourSynth, SvtAv1EncApp, vpxenc, x265, mkvmerge. Licenses flow through from upstream.

Project