README.md

April 16, 2026 · View on GitHub

jellyfin-encoder banner

Automatic video transcoding service for Jellyfin media streaming

License Docker Pulls GitHub Release Docker Image Size codecov listed on awesome-jellyfin


jellyfin-encoder monitors your media library and automatically transcodes videos to optimized 720p HEVC or AV1 for bandwidth-efficient mobile and remote streaming. It runs as a Docker container, supports NVIDIA NVENC and Intel QSV hardware acceleration with automatic software fallback, and uses polling-based observation compatible with NFS, CIFS, and other network filesystems.

Features

  • Automatic folder monitoring -- watches source directories for new and deleted files using polling (NFS/CIFS compatible)
  • Hardware-accelerated encoding -- NVIDIA NVENC and Intel Quick Sync Video (QSV), with transparent software fallback (libx265 / libsvtav1)
  • Smart skip logic -- detects files already at 720p or lower via filename heuristics and ffprobe resolution analysis
  • Jellyfin multi-version support -- creates version symlinks so Jellyfin presents both original and transcoded copies to the user
  • Audio normalization -- re-encodes all audio tracks to stereo AC3 at 192 kbps for consistent mobile playback
  • Subtitle preservation -- copies MKV-native subtitle codecs and converts incompatible ones (MOV text, WebVTT) to SRT
  • Guarded automatic cleanup -- periodically removes orphaned encodes and stale symlinks with mount-health checks to prevent mass deletion (see Safety & Cleanup below)
  • Temp-file workflow -- encodes to .tmp and atomically renames on success, so Jellyfin never indexes incomplete files (note: no cross-container locking — avoid pointing two encoders at the same destination subfolder)
  • Configurable quality presets -- LOW, MEDIUM, and HIGH profiles with per-codec CQ/CRF tuning

Quick Start

Docker Compose

services:
  jellyfin-encoder:
    image: drumsergio/jellyfin-encoder:1.1.4
    container_name: jellyfin-encoder
    devices:
      - /dev/dri:/dev/dri  # Intel QSV -- remove if using NVIDIA or software encoding
    volumes:
      - /path/to/source:/app/source
      - /path/to/destination:/app/destination
    environment:
      ENABLE_HW_ACCEL: "true"
      HW_ENCODING_TYPE: "intel"   # nvidia | intel
      ENCODING_QUALITY: "LOW"     # LOW | MEDIUM | HIGH
      ENCODING_CODEC: "hevc"      # hevc | av1
    restart: always

    # For NVIDIA GPU support, replace the devices block above with:
    # deploy:
    #   resources:
    #     reservations:
    #       devices:
    #         - capabilities: [gpu]

Docker CLI

docker run -d \
  --name jellyfin-encoder \
  --device /dev/dri:/dev/dri \
  -v /path/to/source:/app/source \
  -v /path/to/destination:/app/destination \
  -e ENABLE_HW_ACCEL=true \
  -e HW_ENCODING_TYPE=intel \
  -e ENCODING_CODEC=hevc \
  -e ENCODING_QUALITY=LOW \
  --restart always \
  drumsergio/jellyfin-encoder:1.1.4

Configuration

All settings are controlled via environment variables.

VariableDefaultDescription
SOURCE_FOLDER/app/sourcePath to the directory containing original videos
DEST_FOLDER/app/destinationPath to the directory for encoded output
ENABLE_HW_ACCELtrueEnable hardware-accelerated encoding
HW_ENCODING_TYPEnvidiaHardware encoder: nvidia or intel
ENCODING_CODEChevcOutput codec: hevc or av1
ENCODING_QUALITYLOWQuality preset: LOW, MEDIUM, or HIGH
SYMLINK_TARGET_PREFIX(empty)Absolute path prefix for Jellyfin version symlinks (same-host mode)
SYMLINK_MANIFEST_TARGET(empty)Path prefix for cross-host manifest-based symlinks (see Cross-Host Setup)
SYMLINK_VERSION_SUFFIX - 720pSuffix appended to symlink filenames
CLEANUP_INTERVAL_HOURS6Hours between automatic orphan cleanup runs

Quality Presets

Each preset defines constant-quality (CQ) values for hardware encoding and constant rate factor (CRF) values for software fallback.

PresetHEVC CQ / CRFAV1 CQ / CRFIntended Use
LOW32 / 3045 / 40Mobile devices, minimal storage footprint
MEDIUM26 / 2635 / 35Balanced quality and file size
HIGH22 / 2228 / 28Higher fidelity, larger files

Hardware Acceleration

NVIDIA (NVENC)

Requires the NVIDIA Container Toolkit. Add a GPU reservation to your Compose file:

deploy:
  resources:
    reservations:
      devices:
        - capabilities: [gpu]

Set HW_ENCODING_TYPE: "nvidia". Supported encoders: hevc_nvenc, av1_nvenc.

Intel (Quick Sync Video)

Pass the render device into the container:

devices:
  - /dev/dri:/dev/dri

Set HW_ENCODING_TYPE: "intel". Supported encoders: hevc_qsv, av1_qsv.

Software Fallback

If hardware acceleration is disabled or unavailable, the encoder falls back to libx265 (HEVC) or libsvtav1 (AV1) using CRF-based quality control. Worker count scales to the number of available CPU cores.

Safety & Cleanup

The encoder periodically removes orphaned encodes (files in DEST_FOLDER with no matching source) and stale version symlinks. Several safety rails prevent accidental mass deletion:

GuardScopeBehavior
Source not accessiblecleanup_destination, cleanup_orphaned_symlinksAborts if SOURCE_FOLDER is not a directory
Empty sourcecleanup_destinationAborts if zero video files are found in source
Persisted count (primary)cleanup_destination, cleanup_orphaned_symlinksAfter each successful cleanup, the source video count is written to DEST_FOLDER/.encoder_source_count. If the current count drops below 50% of the persisted value, cleanup is refused. To reset after intentionally shrinking the library, delete the .encoder_source_count file.
Source vs destination ratio (secondary)cleanup_destination, cleanup_orphaned_symlinksIf source video count is less than 50% of destination encode count, cleanup is refused
Mount health on delete eventsVideoHandler.on_deletedBefore trusting a file-delete event from the polling observer, the handler verifies the source mount is responsive. If not, the event is ignored.
Growing tmp filescleanup_destination.tmp files are kept if they are still being written

Same-folder mode (SOURCE_FOLDER == DEST_FOLDER): versioned output filenames (e.g., Movie - 720p.mkv) are recognized as valid encodes and excluded from orphan cleanup.

Delete-event rate limiter: If more than 50 delete events fire within 60 seconds, further deletes are suppressed. This prevents mount outages from cascading into mass encode deletion. The limit resets automatically after the window expires.

Limitations: The persisted-count and ratio guards use a 50% threshold. A mount that exposes more than half its files will pass both guards, potentially allowing cleanup of files in invisible subtrees. After bulk intentional deletions, you may need to delete DEST_FOLDER/.encoder_source_count to reset the baseline — cleanup will refuse to run until the persisted count is reset or the source count recovers above 50%.

Upgrading from < 1.1.0

Starting with v1.1.0, encoded outputs always include the version suffix (e.g., Movie - 720p.mkv instead of Movie.mkv). Existing encodes without the suffix will be re-encoded. To avoid this, rename them before upgrading:

# Dry-run (shows what would be renamed)
docker exec jellyfin-encoder python /app/scripts/migrate_encode_names.py

# Apply renames
docker exec jellyfin-encoder python /app/scripts/migrate_encode_names.py --apply

Cross-Host Manifest Mode

When the encoder and Jellyfin run on different hosts (e.g., encoder on a NAS, Jellyfin on another server connected via CIFS/SMB), real symlinks cannot be created over the network mount. The manifest mode solves this:

  1. Encoder writes a .symlink-manifest.json to DEST_FOLDER listing all encoded files and their Jellyfin container target paths.
  2. Jellyfin host reads the manifest via a CIFS mount and creates real local symlinks.

Encoder Configuration

Set SYMLINK_MANIFEST_TARGET to the path prefix as seen inside the Jellyfin container:

services:
  jellyfin-encoder:
    image: drumsergio/jellyfin-encoder:1.1.4
    environment:
      SYMLINK_MANIFEST_TARGET: "/media-720/Peliculas"  # Jellyfin container path
      # ...other settings

The manifest is updated on encode, delete, and cleanup, and fully rebuilt at startup.

Jellyfin Host

Install scripts/symlink-from-manifest.sh on the Jellyfin host and run it via cron:

# Copy script to Jellyfin host
cp scripts/symlink-from-manifest.sh /boot/config/symlink-from-manifest.sh
chmod +x /boot/config/symlink-from-manifest.sh

# Add cron (runs every 5 minutes)
echo '*/5 * * * * /boot/config/symlink-from-manifest.sh' | crontab -

Edit the script's configuration variables (REMOTE_ROOT, MEDIA_ROOT, LIBRARIES) to match your setup. The script creates symlinks in MEDIA_ROOT pointing to the Jellyfin container path from the manifest, and removes orphaned symlinks not present in the manifest.

Manifest Format

{
  "version": 1,
  "symlinks": {
    "Movie (2024)/Movie (2024) - 720p.mkv": "/media-720/Peliculas/Movie (2024)/Movie (2024) - 720p.mkv"
  }
}

Same-Host vs Cross-Host

ModeVariableUse Case
Same-hostSYMLINK_TARGET_PREFIXEncoder and Jellyfin share a filesystem — encoder creates real symlinks directly
Cross-hostSYMLINK_MANIFEST_TARGETEncoder and Jellyfin on different hosts — encoder writes manifest, Jellyfin host creates symlinks

Both modes can coexist. If only SYMLINK_MANIFEST_TARGET is set, symlinks are managed exclusively via the manifest.

Architecture

Source folder (polling observer)
        |
        v
  New file detected ──> Wait for file completion (size-stable for 60s)
        |
        v
  Resolution check ──> Skip if <= 720p
        |
        v
  FFmpeg transcode ──> scale to 720p, encode video, stereo AC3 audio, copy/convert subtitles
        |
        v
  Verify output (ffprobe duration check)
        |
        v
  Atomic rename .tmp -> .mkv ──> Create Jellyfin version symlink (optional)

Key design decisions:

  • Polling observer (watchdog.PollingObserver) instead of inotify, ensuring compatibility with NFS, CIFS, and other network filesystems.
  • Temp-file workflow -- encodes to a .tmp file first and atomically renames on success, preventing Jellyfin from indexing incomplete files.
  • File-growth detection -- before deleting stale .tmp files, the cleanup routine checks whether the file is still being written by another instance.
  • ProcessPoolExecutor -- one worker for hardware encoding (GPU is the bottleneck), multiple workers for software encoding (CPU-bound).

Utilities

compare_encodes.py

A standalone diagnostic script that compares source and destination folders to report encoding coverage.

# Command-line usage
python scripts/compare_encodes.py --source /media/movies --dest /media/movies-720p

# Inside a running container
docker exec jellyfin-encoder python /app/scripts/compare_encodes.py

# Output as JSON or CSV
python scripts/compare_encodes.py -s /media/movies -d /media/movies-720p --format json
python scripts/compare_encodes.py -s /media/movies -d /media/movies-720p --format csv

# Include files that were skipped (already 720p or lower)
python scripts/compare_encodes.py -s /media/movies -d /media/movies-720p --show-skipped
OptionEnv VariableDescription
-s, --sourceSOURCE_FOLDERSource folder with original videos
-d, --destDEST_FOLDERDestination folder with encoded videos
-f, --formatOUTPUT_FORMATOutput format: text, json, csv
--show-skippedSHOW_SKIPPEDInclude skipped low-quality files in the report
--ignoreIGNORE_PATTERNSAdditional regex patterns to ignore (comma-separated)
Example output
================================================================================
ENCODING COMPARISON REPORT
================================================================================

Source folder:      /media/movies
Destination folder: /media/movies-720p

----------------------------------------
SUMMARY
----------------------------------------
Total source files:     4,463
Total destination files: 4,440
Matched (encoded):      4,420
Missing encodes:        23
Orphaned encodes:       20
Skipped (low quality):  20

----------------------------------------
MISSING ENCODES (23 files, 45.2 GiB total)
----------------------------------------
  [   2.1 GiB] Movie Title (2024) [BDRemux 1080p].mkv
  [   1.8 GiB] Another Movie (2023) [UHD 2160p].mkv
  ...

================================================================================
STATUS: Issues found - 23 missing encodes, 20 orphaned files
================================================================================

Other Jellyfin Projects by GeiserX

  • quality-gate — Restrict users to specific media versions based on filename regex patterns
  • smart-covers — Cover extraction for books, audiobooks, comics, magazines, and music libraries with online fallback
  • whisper-subs — Automatically generates subtitles using local AI models powered by Whisper
  • jellyfin-telegram-channel-sync — Sync Jellyfin access with Telegram channel membership

Contributing

Contributions are welcome. Please open an issue to discuss proposed changes before submitting a pull request.

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/my-change)
  3. Commit your changes
  4. Open a pull request against main

License

This project is licensed under the GPL-3.0 License.