Pretranscode

A small, single-writer Jellyfin compatibility cache.

Immutable source media is the only source of truth. The cache is a disposable, filesystem-derived projection and contains no manifests, inventory database, or migration state.

Compatibility output

A cached rendition contains only:

  • one H.264 High, 8-bit yuv420p video stream;
  • one AAC-LC, 192 kbps stereo audio stream when the source has audio;
  • BT.709 SDR output for HDR10, HLG, and Dolby Vision sources.

Embedded subtitles, attachments, chapters, and inherited metadata are omitted. Sidecar subtitle files are mirrored unchanged. This deliberately small profile prevents EAC3/Atmos, PGS, or stale Dolby Vision metadata from forcing another Jellyfin transcode.

A source needs a compatibility rendition unless it is already H.264 8-bit with AAC stereo audio and no embedded subtitles.

Durable model

  • Originals are mounted read-only at /source.
  • Jellyfin reads /data/library, a symlink mirror under /view.
  • A link targets either /data/original or an immutable cache object.
  • Cache names encode hashes of the logical path, source generation, complete encoding profile, and output bytes.
  • FFmpeg writes only a .partial file.
  • Publication is validation, fsync, an atomic output rename, then an atomic symlink rename.
  • Open clients retain their old inode during publication.
  • A profile change builds a new object while the published old-profile object remains available. The old object is collected only after replacement.
  • LRU updates file access time only; published media modification time and bytes never change.
  • Scheduling, retries, priorities, and cooldowns exist only in process memory.
  • One controller owns the cache directory through flock().

There is no cache migration command. Restarting with a changed profile is enough.

Installation

  1. Copy .env.example to .env and set the host paths and Jellyfin API key.

  2. Provision the cache filesystem:

    sudo scripts/provision-cache.sh .env
    
  3. Mount the same paths in Jellyfin:

    volumes:
      - ${PRETRANSCODE_VIEW_PATH}:/data/library:ro
      - ${MEDIA_LIBRARY_PATH}:/data/original:ro
      - ${PRETRANSCODE_CACHE_PATH}:/data/cache:ro
    
  4. Start the controller:

    docker compose --env-file .env up -d --build
    

For Coolify, import compose.yaml as a Custom Docker Compose resource and enter .env.example values through the environment-variable UI.

Operation

The controller has one worker. FFmpeg and file hashing run with SCHED_IDLE and idle I/O priority so foreground services take precedence.

Playback locking rules:

  • an active movie locks that movie;
  • an active episode locks its series;
  • an encode is cancelled if its item becomes locked;
  • a series remains locked for SERIES_COOLDOWN_SECONDS after playback;
  • recently watched content receives in-memory queue priority.

Commands:

pretranscode run
pretranscode sync
pretranscode active
pretranscode trim BYTES
pretranscode purge
pretranscode --version

purge and trim refuse to operate during active playback.

Failure behaviour

Partial outputs are never published. Startup removes stale partials and unreferenced objects. A missing cache mount marker, second controller, failed probe, failed validation, failed fsync, or unavailable Jellyfin session API causes work to stop safely rather than mutate published media.

Tests

The image build treats compiler warnings as errors and runs the C core tests. The integration test creates incompatible media and verifies canonical streams, atomic publication, refresh retry, and stateless profile replacement:

podman build -t pretranscode-test .
IMAGE=pretranscode-test tests/integration.sh
S
Description
Portable immutable Jellyfin H.264 compatibility cache
Readme MIT
144 KiB
Languages
C 84.3%
Shell 10.1%
Python 3.6%
Dockerfile 1.1%
Makefile 0.9%