Replay video files using ffmpeg and create HLS streams for each directory
This repository has been archived on 2026-06-17. You can view files and clone it, but you cannot make any changes to its state, such as pushing and creating new issues, pull requests or comments.
  • Python 98.7%
  • Dockerfile 0.7%
  • Shell 0.6%
Find a file
2026-06-16 16:05:21 +00:00
app Clean up channel runtime logs 2026-06-16 16:05:21 +00:00
scripts Package replay app entrypoint 2026-06-13 13:40:37 +00:00
tests Clean up channel runtime logs 2026-06-16 16:05:21 +00:00
.dockerignore Add test and ignore baselines 2026-06-13 13:41:09 +00:00
.gitignore Add test and ignore baselines 2026-06-13 13:41:09 +00:00
docker-compose.yml Publish replay service on port 8090 2026-03-09 00:49:33 +00:00
Dockerfile Package replay app entrypoint 2026-06-13 13:40:37 +00:00
README.md Estimate current playback file 2026-06-15 17:38:27 +00:00
requirements-dev.txt Add test and ignore baselines 2026-06-13 13:41:09 +00:00
requirements.txt Add replay service runtime files 2026-03-08 19:16:10 +00:00
variables.env.example Add FFMPEG_THREADS setting to limit per-process CPU usage during transcoding 2026-03-09 02:20:35 +00:00

Replay Service

Replay is a small Quart service that turns each top-level directory in a media library into an endless HLS channel.

Each discovered channel gets its own ffmpeg process, writes HLS output under /tmp/hls/<channel>/, and serves playlists and segments over HTTP.

What it does

  • Discovers channels from subdirectories under LIBRARY_DIR
  • Recursively scans each channel directory for .mp4, .mkv, and .ts files
  • Shuffles files and loops them forever through ffmpeg
  • Serves HLS playlists at /<channel>/playlist.m3u8
  • Polls the library for file or directory changes and restarts affected channels automatically

Repository layout

  • app/main.py - HTTP routes and service lifecycle
  • app/channel.py - channel discovery, file watching, ffmpeg orchestration, and HLS generation
  • app/config.py - environment variables, defaults, logging, and shared in-memory state
  • scripts/run.sh - container entrypoint
  • Dockerfile - container image build
  • docker-compose.yml - compose service definition used by the original stack

Requirements

  • Python 3.11+
  • ffmpeg and ffprobe
  • A media library where each top-level directory is a channel

Quick start with Docker

  1. Copy the example environment file:
cp variables.env.example variables.env

Keep LIBRARY_DIR aligned with the container bind mount. The examples in this repo mount the host library at /library, so variables.env should keep LIBRARY_DIR=/library unless you also change the container mount target.

  1. Create a library with at least one channel directory:
data/
  library/
    movies/
      clip-01.mp4
      clip-02.mp4
    sports/
      match-01.ts
  1. Build and run the container:
docker build -t replay-service .
docker run --rm \
  -p 8090:8090 \
  --env-file variables.env \
  -v "$(pwd)/data/library:/library:ro" \
  replay-service
  1. Check the service:
curl http://localhost:8090/health
  1. Open a playlist:
http://localhost:8090/movies/playlist.m3u8

Docker Compose

The checked-in docker-compose.yml mounts the library, loads variables.env, and publishes 8090 on the host.

For a standalone setup, either:

  • use the docker run command above, or
  • run docker compose up --build

Example:

services:
  replay:
    ports:
      - "8090:8090"

Local development

Install dependencies and run the app directly:

python -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
export LIBRARY_DIR="$(pwd)/data/library"
export REPLAY_PORT=8090
uvicorn app.main:app --host 0.0.0.0 --port "$REPLAY_PORT" --workers 1

Notes:

  • local runs still require ffmpeg and ffprobe on your PATH
  • keep --workers 1; channel state is stored in process memory

Configuration

These environment variables are read in app/config.py:

Variable Default Purpose
LIBRARY_DIR /library Root directory scanned for channel folders
REPLAY_PORT 8090 HTTP listen port
REPLAY_SCAN_INTERVAL 60 Seconds between library scans
HLS_SEGMENT_TIME 4 HLS segment duration in seconds
HLS_LIST_SIZE 20 Number of entries kept in the live playlist
TRANSCODE_ENABLED false Enables re-encoding for all channels when true
VIDEO_BITRATE 4000k Video bitrate used when transcoding
AUDIO_BITRATE 128k Audio bitrate used when transcoding
FFMPEG_THREADS 2 Number of threads each ffmpeg transcode process may use

Channel layout

Each top-level directory inside LIBRARY_DIR becomes a channel name.

Example:

/library
  /movies
    movie-a.mp4
    movie-b.mkv
  /sports
    game-01.ts

Behavior:

  • TRANSCODE_ENABLED=false keeps copy mode enabled
  • TRANSCODE_ENABLED=true forces re-encoding through libx264 and AAC

Copy mode is the default and is the cheapest option, but it only keeps files that are compatible for concat playback. The service uses ffprobe to compare stream signatures and skips incompatible files. If channels contain mixed codecs, frame rates, or audio layouts, set TRANSCODE_ENABLED=true in variables.env.

HTTP endpoints

  • / - basic service metadata and discovered channel names
  • /health - readiness, current source file and recent ffmpeg error summary for each channel
  • /<channel>/playlist.m3u8 - HLS playlist
  • /<channel>/<filename> - HLS segments and related files

Operational notes

  • the service writes generated HLS output to /tmp/hls
  • one ffmpeg process runs per discovered channel
  • library scanning is polling-based and runs every REPLAY_SCAN_INTERVAL seconds
  • channel directory additions and removals are picked up on the next scan
  • channel file additions and removals trigger a channel restart on the next scan
  • changing TRANSCODE_ENABLED requires restarting the service
  • /health always returns a JSON summary; use each channel's ready flag to see whether playlist.m3u8 exists yet, and current_file / last_error_file to identify files involved in playback or recent ffmpeg errors
  • files that fail ffprobe are skipped before playback, including in transcode mode

Local checks

Run python -m compileall app && python -m pytest before shipping changes.