No description
  • Rust 61.9%
  • CSS 14.3%
  • HTML 12.4%
  • Nix 6.2%
  • JavaScript 4.8%
  • Other 0.4%
Find a file
2026-07-29 16:04:39 +01:00
migrations feat: add manual settings to encodding jobs 2026-07-29 16:04:39 +01:00
nix feat: add custom theme and better trans theme 2026-07-29 15:03:17 +01:00
src feat: add manual settings to encodding jobs 2026-07-29 16:04:39 +01:00
static feat: add manual settings to encodding jobs 2026-07-29 16:04:39 +01:00
templates feat: add manual settings to encodding jobs 2026-07-29 16:04:39 +01:00
.env.example feat: add custom theme and better trans theme 2026-07-29 15:03:17 +01:00
.envrc init 2026-07-27 20:34:58 +01:00
.gitignore init 2026-07-27 20:34:58 +01:00
Cargo.lock init 2026-07-27 20:34:58 +01:00
Cargo.toml chore: expand ci and docs behond amd 2026-07-29 14:32:49 +01:00
CLAUDE.md feat: add manual settings to encodding jobs 2026-07-29 16:04:39 +01:00
docker-compose.yml feat: add custom theme and better trans theme 2026-07-29 15:03:17 +01:00
Dockerfile chore: expand ci and docs behond amd 2026-07-29 14:32:49 +01:00
flake.lock init 2026-07-27 20:34:58 +01:00
flake.nix chore: expand ci and docs behond amd 2026-07-29 14:32:49 +01:00
LICENSE feat: add license 2026-07-29 14:32:22 +01:00
README.md feat: add manual settings to encodding jobs 2026-07-29 16:04:39 +01:00

trans-coded

A self-hosted web app to manage your Jellyfin video libraries and transcode files to AV1 with hardware acceleration.

  • Backend: Rust (Axum + Tokio), Askama templates + HTMX, single binary
  • Database: PostgreSQL (via sqlx) — tracks libraries, videos, streams, and a transcode job queue
  • Discovery: Jellyfin REST API for items + ffprobe for exact codec/subtitle details
  • Transcode: ffmpeg → AV1, copying audio + subtitle streams; encode to temp → verify (codec + duration) → atomic in-place replace. Bulk actions can also pick quality, strip audio/subtitle languages, convert the container, or skip encoding entirely and just remux
  • Encoders: vaapi (AMD/Intel, e.g. Radeon 7800 XT), nvenc (NVIDIA Ada/Blackwell), software (libsvtav1)
  • Auth: local accounts, Argon2 password hashing, Postgres-backed cookie sessions

Quick start (Docker)

cp .env.example .env   # edit secrets, Jellyfin URL/key, encoder mode
docker compose up -d
# open http://localhost:8080  (admin / <ADMIN_PASSWORD>)

Mount your media at the same path Jellyfin reports (for atomic in-place replace), or set JELLYFIN_PATH_MAP=from:to.

Docker GPU setup

docker-compose.yml ships configured for VA-API (AMD/Intel). The other modes need the edits below — each block is present in the compose file as a comment.

AMD (VA-API, radeonsi) — the default. Nothing to change beyond your media path:

environment:
  ENCODER_MODE: vaapi
  VAAPI_DEVICE: /dev/dri/renderD128
devices:
  - "/dev/dri:/dev/dri"
group_add: [video, render]

AV1 encode on RDNA3 (e.g. Radeon 7800 XT) needs Mesa ≥ 24; the runtime image is Debian trixie (Mesa 25). RDNA2 and older have no AV1 encoder — use ENCODER_MODE=software there.

Intel (VA-API, iHD) — same passthrough as AMD; the image also ships intel-media-va-driver. If libva picks the wrong driver, pin it:

environment:
  ENCODER_MODE: vaapi
  VAAPI_DEVICE: /dev/dri/renderD128
  LIBVA_DRIVER_NAME: iHD
devices:
  - "/dev/dri:/dev/dri"
group_add: [video, render]

AV1 encode needs Arc (DG2/Battlemage) or Meteor Lake and newer iGPUs; earlier Intel parts can decode AV1 but not encode it.

NVIDIA (NVENC) — no /dev/dri; the GPU comes in through the NVIDIA container toolkit, which must be installed and configured on the host. Delete the devices and group_add keys and use:

environment:
  ENCODER_MODE: nvenc
  NVIDIA_VISIBLE_DEVICES: all
  NVIDIA_DRIVER_CAPABILITIES: compute,video,utility
deploy:
  resources:
    reservations:
      devices:
        - driver: nvidia
          count: all
          capabilities: [gpu, video]

video in the driver capabilities is what exposes libnvidia-encode; without it ffmpeg reports no NVENC encoders. AV1 NVENC requires Ada (RTX 40) or newer — Ampere and older can only encode H.264/HEVC, so use software there.

No GPU — set ENCODER_MODE=software and drop devices/group_add entirely. libsvtav1 is much slower but has no hardware requirements, and it is also the encoder used for the DOLBY_VISION_MODE=software lane.

Quick check that the container can see the encoder:

docker compose exec app ffmpeg -hide_banner -encoders | grep av1
# expect av1_vaapi / av1_nvenc / libsvtav1 for your mode
docker compose exec app vainfo   # VA-API only: lists the driver + AV1 profiles

Local dev

nix develop                       # rust toolchain + ffmpeg + psql
docker run -d --name pg -e POSTGRES_USER=transcoded -e POSTGRES_PASSWORD=transcoded \
  -e POSTGRES_DB=transcoded -p 5432:5432 postgres:17-alpine
export DATABASE_URL=postgres://transcoded:transcoded@localhost:5432/transcoded
export ADMIN_USERNAME=admin ADMIN_PASSWORD=secret
export ENCODER_MODE=software      # reliable without VA-API/NVENC encode support
cargo run

NixOS

Add the flake, then configure declaratively:

# flake inputs: trans-coded.url = "path:/path/to/trans-coded";
{
  imports = [ inputs.trans-coded.nixosModules.default ];
  nixpkgs.overlays = [ inputs.trans-coded.overlays.default ];

  services.trans-coded = {
    enable = true;
    bindAddr = "0.0.0.0:8080";
    database.createLocally = true;               # provisions PostgreSQL
    admin.passwordFile = "/run/secrets/tc-admin";
    jellyfin.url = "http://localhost:8096";
    jellyfin.apiKeyFile = "/run/secrets/tc-jf";
    jellyfin.pathMap = "/media:/srv/media";
    encoder.mode = "vaapi";                       # 7800 XT
    encoder.vaapiDevice = "/dev/dri/renderD128";
  };
  # Media must be writable for in-place replace:
  systemd.services.trans-coded.serviceConfig.ReadWritePaths = [ "/srv/media" ];
}

Secrets (*File options) are injected via systemd credentials and never enter the Nix store. Every setting is also available as a plain env var (see .env.example) — the module just sets those.

How transcoding works

  1. Sync pulls items from Jellyfin and ffprobes each file into Postgres.
  2. Queue a job: per-video Transcode, or one of the dashboard's bulk buttons (Transcode all non-AV1 / Re-encode all AV1), which open a settings dialog first — see Bulk queue settings below.
  3. A worker claims one job at a time (FOR UPDATE SKIP LOCKED) and runs ffmpeg: video → AV1, -c copy for the audio/subtitle/attachment streams the job keeps, progress streamed to the DB.
  4. Dolby Vision sources follow DOLBY_VISION_MODE: software uses libsvtav1 to create AV1 Dolby Vision Profile 10, hdr10 drops the Dolby Vision RPU and encodes the HDR10 base layer on the configured (hardware) encoder, and skip keeps the source and records a DB marker so bulk queueing will not retry it. Under software the hardware encoders are not used for DV, because this ffmpeg build ignores -dolbyvision there — that is what makes DV a CPU-lane job. hdr10 avoids the CPU fallback entirely: Profile 8.1 and 10.1 already carry an HDR10-compatible base layer, so nothing has to be converted, and av1_vaapi preserves the colour tags plus the mastering-display and content-light metadata. The trade is losing DV's per-scene dynamic metadata. Profile 5 has no HDR10 base layer and stays skipped under either mode. For ordinary SDR/HDR sources, ffmpeg is told to preserve the source color range, primaries, transfer, matrix, and chroma location tags.
  5. The output is verified (must be AV1, duration within tolerance). If KEEP_ORIGINAL_IF_LARGER=true, a valid output that is not smaller is deleted, the original is kept, and the video is marked so bulk non-AV1 queueing skips it next time.
  6. Smaller accepted outputs start a best-effort VMAF task while the source and temp output still exist. The task hard-links both files, then runs in the background so the transcode job can finish and the worker can claim more encode work.
  7. The accepted output is atomically renamed over the original on the same filesystem. VMAF status/score is saved on the video row when the background metric task finishes.

Bulk queue settings

The dashboard's two "transcode all" buttons open a dialog whose choices apply to every job that action queues. Each control carries a ? with a full explanation in the UI; in short:

Setting What it does
Video Encode to AV1 re-compresses the picture (the slow part, and the only thing that shrinks the video stream). Don't encode — copy the video remuxes instead: the video stream is copied bit-for-bit and only the container and track list change. A remux runs in seconds, skips VMAF, and is always kept — the size gate does not apply.
Quality Constant-quality target on the CRF scale (063): lower is better looking and bigger, ~6 steps roughly halves or doubles the size. Hardware encoders take it ×4 on their own 0255 quantizer. Defaults to AV1_QUALITY.
Audio / subtitle tracks Which languages to carry over. Kept tracks are copied, never re-encoded, so dropping unwanted dubs costs no quality. Matching is on the language tag; untagged tracks are und. Selecting none drops every track of that kind, and an audio filter that matches nothing keeps the first track rather than leaving a silent file.
Container Keep each source's container (replaces in place), or convert to MKV/MP4. MP4 drops image-based subtitles (PGS/VobSub) and font attachments and converts text subtitles to mov_text. Converting renames the file and deletes the original, so Jellyfin needs a rescan.

The per-video Transcode buttons don't prompt — they always use the configured defaults.

Configuration

Environment Variables

Variable Default Description
APP_BIND_ADDR 0.0.0.0:8080 HTTP listen address.
COOKIE_SECURE false Mark session cookies Secure; use true behind HTTPS.
STATIC_DIR static Runtime static asset directory.
DATABASE_URL required PostgreSQL connection string.
DB_MAX_CONNECTIONS 5 Maximum Postgres pool size.
ADMIN_USERNAME unset Bootstrap admin username, created/updated at startup when paired with ADMIN_PASSWORD.
ADMIN_PASSWORD unset Bootstrap admin password.
JELLYFIN_URL unset Base Jellyfin URL.
JELLYFIN_API_KEY unset Jellyfin API key.
JELLYFIN_USER_ID unset Optional Jellyfin user id; first user is used when unset.
JELLYFIN_PATH_MAP unset Optional from:to path remap from Jellyfin paths to local paths.
SCAN_ON_START false Run a Jellyfin sync when the service starts.
ENCODER_MODE vaapi vaapi, nvenc, or software.
VAAPI_DEVICE /dev/dri/renderD128 VA-API render node.
HWACCEL_DECODE true Decode on GPU as well as encode; unsupported inputs may fail. Toggleable from Jobs page.
FFMPEG_BIN ffmpeg ffmpeg binary path.
FFPROBE_BIN ffprobe ffprobe binary path.
VMAF_ENABLED true Run best-effort post-encode VMAF and save status/score on the video row.
VMAF_FFMPEG_BIN FFMPEG_BIN ffmpeg binary used for VMAF. It must include the libvmaf filter; the NixOS module uses pkgs.ffmpeg-full.
VMAF_HWACCEL_DECODE true in vaapi mode, otherwise false Use VA-API decode for VMAF inputs before downloading sampled frames for libvmaf.
VMAF_SAMPLE_INTERVAL 24 Score every Nth frame. 24 is roughly 1 fps for common 23.976/24 fps sources; 1 scores every frame.
VMAF_THREADS 0 libvmaf worker threads; 0 lets libvmaf choose.
VMAF_CONCURRENCY 1 Maximum concurrent VMAF processes. Waiting VMAF tasks do not occupy transcode worker slots.
AV1_QUALITY 28 Software libsvtav1 CRF. Lower is better/larger.
AV1_QP AV1_QUALITY * 4, clamped 1..255 Hardware quality. VA-API uses -global_quality; NVENC uses -qp. Higher is smaller.
KEEP_ORIGINAL_IF_LARGER true Delete valid AV1 outputs that are not smaller, keep the source, and mark the video skipped for bulk queueing.
DOLBY_VISION_MODE software software creates AV1 Dolby Vision Profile 10 with libsvtav1 (CPU lane); hdr10 drops the DV RPU and encodes the HDR10 base layer on the configured encoder, keeping DV on the GPU; skip keeps the source and marks it blocked for bulk queueing. Profile 5 is skipped under software and hdr10 alike.
SVT_PRESET 6 libsvtav1 preset, 0 slowest/best to 13 fastest.
TRANSCODE_TEMP_DIR unset Parsed for future use; current temp outputs stay next to the source so replace is atomic.
TRANSCODE_CONCURRENCY 1 Initial GPU (hardware) encode concurrency; usually single-lane. In software encoder mode it seeds the CPU limit instead.
TRANSCODE_CPU_CONCURRENCY 1 Initial CPU (software / Dolby Vision fallback) encode concurrency, kept separate from the GPU lane so it can't clobber the CPU while GPU jobs run. Only meaningful in a hardware encoder mode. Both limits are live-tunable from the Jobs UI.
WORKER_POLL_SECONDS 5 Worker queue polling interval.
SKIP_CODECS av1 Parsed for future codec-skip behavior; current bulk queueing skips AV1 plus videos with a persisted AV1 transcode block marker.
CUSTOM_THEME_CSS built-in template Starter CSS for the in-app Theme ▸ Custom… editor. See Theming.
CUSTOM_THEME_CSS_FILE unset Path to a .css file used as that starter instead. Wins over CUSTOM_THEME_CSS; unreadable or empty falls back to the built-in template.
RUST_LOG info,sqlx=warn Tracing filter.

NixOS Options

All NixOS module options map to the environment variables above or to systemd service settings.

Option Default Description
services.trans-coded.enable false Enable the service.
services.trans-coded.package pkgs.trans-coded Package to run.
services.trans-coded.ffmpegPackage pkgs.ffmpeg Package providing ffmpeg and ffprobe.
services.trans-coded.vmaf.enable true Sets VMAF_ENABLED.
services.trans-coded.vmaf.ffmpegPackage pkgs.ffmpeg-full Package providing the VMAF ffmpeg binary; sets VMAF_FFMPEG_BIN.
services.trans-coded.vmaf.hwDecode true Sets VMAF_HWACCEL_DECODE.
services.trans-coded.vmaf.sampleInterval 24 Sets VMAF_SAMPLE_INTERVAL.
services.trans-coded.vmaf.threads 0 Sets VMAF_THREADS.
services.trans-coded.vmaf.concurrency 1 Sets VMAF_CONCURRENCY.
services.trans-coded.user trans-coded System user. Added to video and render.
services.trans-coded.group trans-coded Primary group.
services.trans-coded.bindAddr 0.0.0.0:8080 HTTP listen address.
services.trans-coded.cookieSecure false Sets COOKIE_SECURE.
services.trans-coded.theme.customTemplate null Starter CSS for the in-app custom theme editor; written to the store and passed as CUSTOM_THEME_CSS_FILE.
services.trans-coded.theme.customTemplateFile null Path to a CSS file used as that starter instead. Takes precedence over theme.customTemplate.
services.trans-coded.logLevel info,sqlx=warn Sets RUST_LOG.
services.trans-coded.scanOnStart false Sets SCAN_ON_START.
services.trans-coded.database.createLocally true Provision local PostgreSQL database and role.
services.trans-coded.database.name transcoded Local database name.
services.trans-coded.database.url local socket URL Sets DATABASE_URL; required when createLocally = false.
services.trans-coded.admin.username admin Sets ADMIN_USERNAME.
services.trans-coded.admin.passwordFile null File loaded as ADMIN_PASSWORD via systemd credentials.
services.trans-coded.jellyfin.url null Sets JELLYFIN_URL.
services.trans-coded.jellyfin.apiKeyFile null File loaded as JELLYFIN_API_KEY via systemd credentials.
services.trans-coded.jellyfin.userId null Sets JELLYFIN_USER_ID.
services.trans-coded.jellyfin.pathMap null Sets JELLYFIN_PATH_MAP.
services.trans-coded.encoder.mode vaapi Sets ENCODER_MODE; one of vaapi, nvenc, software.
services.trans-coded.encoder.vaapiDevice /dev/dri/renderD128 Sets VAAPI_DEVICE.
services.trans-coded.encoder.quality 28 Sets software AV1_QUALITY.
services.trans-coded.encoder.hardwareQuality encoder.quality * 4, clamped 1..255 Sets AV1_QP; VA-API -global_quality, NVENC -qp.
services.trans-coded.encoder.keepOriginalIfLarger true Sets KEEP_ORIGINAL_IF_LARGER.
services.trans-coded.encoder.dolbyVisionMode software Sets DOLBY_VISION_MODE; one of software, hdr10, skip.
services.trans-coded.encoder.svtPreset 6 Sets SVT_PRESET.
services.trans-coded.encoder.concurrency 1 Sets TRANSCODE_CONCURRENCY.
services.trans-coded.encoder.cpuConcurrency 1 Sets TRANSCODE_CPU_CONCURRENCY.
services.trans-coded.extraEnvironment { } Extra environment entries passed through verbatim.

Media paths must still be made writable with systemd settings, for example systemd.services.trans-coded.serviceConfig.ReadWritePaths = [ "/srv/media" ];.

License

trans-coded is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License v3.0 or later as published by the Free Software Foundation. See LICENSE for the full text.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

SPDX identifier: GPL-3.0-or-later.