- Python 98.1%
- Shell 1.4%
- Dockerfile 0.5%
| nginx | ||
| producer | ||
| scripts | ||
| .env.dist | ||
| .gitignore | ||
| docker-compose.yml | ||
| LICENSE | ||
| README.md | ||
Docker HLS Video Service
This project runs a live-like HLS channel from a directory of video files. A Python producer picks files in a shuffled cycle, runs FFmpeg, writes HLS output, and NGINX serves the playlist, segments, and a web health endpoint.
The stream URL is http://localhost:8080/hls/live.m3u8.
The web health URL is http://localhost:8080/health.
Docker verification is required in a Docker-enabled environment. This workspace currently lacks Docker, so Docker commands here are documented for the target environment and can't be completed locally in this workspace.
Requirements
- Docker with the Compose plugin for running the service.
- FFmpeg and ffprobe inside the producer image, installed by
producer/Dockerfile. - Optional host FFmpeg for local fixture generation with
scripts/generate-test-media.sh. - curl for CLI health and playlist checks.
Directory Layout
docker-compose.yml defines three services.
producer/ contains the Python producer. The runtime command is python -m app.supervisor. The producer healthcheck is python -m app.health --check.
nginx/nginx.conf serves /hls/ and /health from the web container.
scripts/generate-test-media.sh creates short local media files for QA.
sample-media/ is a suggested local input directory. It is generated during QA and should stay uncommitted.
The producer mounts the host media directory read-only at /media. The producer and web containers share the hls-output volume at /hls and /usr/share/nginx/html/hls.
Quick Start
Generate sample media:
bash scripts/generate-test-media.sh ./sample-media
Start the service:
MEDIA_DIR=./sample-media docker compose up --build -d
Check web health:
curl -fsS http://localhost:8080/health
Fetch the live playlist:
curl -fsS http://localhost:8080/hls/live.m3u8
Stop the service:
docker compose down
Configuration
Set these variables in the environment or copy .env.dist to .env.
| Variable | Default | Purpose |
|---|---|---|
MEDIA_DIR |
./media |
Host directory mounted read-only into the producer as /media. |
HTTP_PORT |
8080 |
Host port mapped to the NGINX web container. |
HLS_PLAYLIST |
live.m3u8 |
Playlist filename written under /hls. |
SEGMENT_SECONDS |
4 |
Target HLS segment duration. |
PLAYLIST_SIZE |
6 |
Number of segments kept in the rolling playlist. |
RESCAN_SECONDS |
30 |
Wait time before rescanning when no media is found. |
OUTPUT_WIDTH |
1920 |
Output video width. |
OUTPUT_HEIGHT |
1080 |
Output video height. |
OUTPUT_FPS |
30 |
Output frame rate. |
SUPPORTED_EXTENSIONS |
.mp4,.mkv,.mov,.avi,.webm,.m4v |
Comma-separated file extensions accepted by the producer. |
SHUFFLE_SEED |
empty | Optional integer seed for deterministic shuffle order in tests. |
LOG_LEVEL |
INFO |
Producer log level. |
Supported input extensions are .mp4, .mkv, .mov, .avi, .webm, and .m4v. Matching is case-insensitive.
Runtime Behavior
The producer scans MEDIA_DIR, filters supported files, shuffles the queue, and streams one file at a time. It rescans between full playlist cycles so newly added files can be picked up later.
All output is fixed to 1920x1080 H.264 video at 4000k with AAC audio at 48000 Hz and 192k, at 30 fps. Inputs are scaled to fit inside 1920x1080 and padded with black bars when needed.
If an input has no audio track, the producer adds silent AAC audio so the HLS output still has an audio stream.
If no media is present, the producer writes a no_media status, reports unhealthy through python -m app.health --check, waits RESCAN_SECONDS, and tries again. The web container can still answer http://localhost:8080/health; the HLS playlist may be missing until media exists.
If a file is corrupt, unsupported, missing, or deleted before playback, the producer skips it or records an FFmpeg failure status, then continues with later scans instead of crashing permanently.
During a media cycle, status.json includes played_files and pending_files arrays for the current shuffled queue. The currently streaming file stays in pending_files until it completes, fails, or is skipped.
To skip the currently streaming file without stopping the supervisor, run:
docker compose exec -T producer python -m app.control skip-current
The command writes a skip-current.request marker under the shared HLS output directory. The running supervisor consumes that marker, terminates only the current FFmpeg child process, and continues with the next queued media file.
Exact QA Commands
Run these from the repository root.
Local sample media check:
rm -rf ./sample-media
bash scripts/generate-test-media.sh ./sample-media
ls ./sample-media
Compose syntax check, requires Docker:
docker compose config
Build containers, requires Docker:
docker compose build
Run producer tests in the container, requires Docker:
docker compose run --rm producer-test pytest -q
Start the stream, requires Docker:
MEDIA_DIR=./sample-media docker compose up --build -d
Check web health, requires the running web container:
curl -fsS http://localhost:8080/health
Check the HLS playlist, requires the running web container:
curl -fsS http://localhost:8080/hls/live.m3u8 -o /tmp/live.m3u8
grep '#EXTM3U' /tmp/live.m3u8
! grep '#EXT-X-ENDLIST' /tmp/live.m3u8
Fetch one referenced segment, requires the running web container:
python -c "import urllib.request; p=open('/tmp/live.m3u8').read(); seg=next(x for x in p.splitlines() if x and not x.startswith('#')); urllib.request.urlretrieve('http://localhost:8080/hls/'+seg, '/tmp/hls-segment.ts')"
Probe video output:
ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,width,height -of csv=p=0 /tmp/hls-segment.ts
Expected video output:
h264,1920,1080
Probe audio output:
ffprobe -v error -select_streams a:0 -show_entries stream=codec_name -of csv=p=0 /tmp/hls-segment.ts
Expected audio output:
aac
Check producer health inside the container, requires Docker:
docker compose exec -T producer python -m app.health --check
Skip the current track inside the producer container, requires a running producer:
docker compose exec -T producer python -m app.control skip-current
Inspect logs:
docker compose logs producer web
No-media runtime QA, requires Docker:
docker compose down
rm -rf ./empty-media
mkdir -p ./empty-media
MEDIA_DIR=./empty-media docker compose up --build -d
curl -fsS http://localhost:8080/health
docker compose ps producer web
for attempt in $(seq 1 30); do
if docker compose exec -T producer test -f /hls/status.json; then
break
fi
sleep 1
done
if docker compose exec -T producer python -m app.health --check; then
echo "expected producer health check to fail with no_media"
exit 1
else
echo "producer health check failed as expected for no_media"
fi
docker compose exec -T producer python - <<'PY'
import json
from pathlib import Path
status = json.loads(Path("/hls/status.json").read_text())
assert status["state"] == "no_media", status
print(status)
PY
docker compose logs producer
curl -fsS http://localhost:8080/health
Expected no-media result: the web health request stays successful, while the producer health command fails and status.json reports no_media.
Troubleshooting
If docker is not found, run the Docker commands in an environment with Docker and the Compose plugin installed. This workspace currently has that limitation.
If bash scripts/generate-test-media.sh ./sample-media fails with ffmpeg is required to generate sample media, install host FFmpeg or generate fixtures in another environment. The producer container still installs FFmpeg for runtime use.
If http://localhost:8080/health fails, check that docker compose ps shows the web service running and that HTTP_PORT is not already in use.
If http://localhost:8080/hls/live.m3u8 returns 404, confirm MEDIA_DIR points to files with supported extensions and wait for the producer to create the first playlist.
If the producer is unhealthy with no_media, add supported media files or run bash scripts/generate-test-media.sh ./sample-media, then restart with MEDIA_DIR=./sample-media docker compose up --build -d.
If corrupt or unsupported files are present, remove them or leave them in place. The producer is expected to skip unsupported extensions and continue after FFmpeg failures on bad media.