video_press/README.md
2026-03-09 17:42:26 -05:00

3.7 KiB

VideoPress — FFmpeg Video Compressor

A web-based video compression tool served via Gunicorn + gevent (WSGI), containerised with Docker. FFmpeg compresses video files to approximately 1/3 their original size. All file-system access is restricted to a single configurable media root for security.


# 1. Build the image
docker build -t videopress .

# 2. Run — replace /your/video/path with the real path on your host
docker run -d \
  --name videopress \
  --restart unless-stopped \
  -p 8080:8080 \
  -v /your/video/path:/media \
  videopress

# 3. Open http://localhost:8080

With docker compose

# Edit the volume path in docker-compose.yml  (or export the env var):
export MEDIA_HOST_PATH=/your/video/path

docker compose up -d

Bare-metal (without Docker)

Requirements

Tool Version
Python 3.8+
FFmpeg + FFprobe any recent
pip packages see requirements.txt
# Install Python dependencies
pip install -r requirements.txt

# Development server (Flask built-in — not for production)
./start.sh

# Production server (Gunicorn + gevent)
MEDIA_ROOT=/your/video/path ./start.sh --prod
# or on a custom port:
MEDIA_ROOT=/your/video/path ./start.sh --prod 9000

Security model

Every API call that accepts a path validates it with safe_path() before any OS operation. safe_path() resolves symlinks and asserts the result is inside MEDIA_ROOT. Requests that attempt directory traversal are rejected with HTTP 403. The container runs as a non-root user (UID 1000).


Architecture

Browser  ──HTTP──▶  Gunicorn (gevent worker)
                        │
                        ├─ GET  /                 → index.html
                        ├─ GET  /api/config        → {"media_root": ...}
                        ├─ GET  /api/browse        → directory listing
                        ├─ POST /api/scan          → ffprobe metadata
                        ├─ POST /api/compress/start → launch ffmpeg thread
                        ├─ GET  /api/compress/progress/<id>  ← SSE stream
                        └─ POST /api/compress/cancel/<id>

Why gevent? SSE (/api/compress/progress) is a long-lived streaming response. Standard Gunicorn sync workers block for its entire duration. Gevent workers use cooperative greenlets so a single worker process can handle many concurrent SSE streams and normal requests simultaneously.

Why workers=1? Job state lives in an in-process Python dict. Multiple worker processes would not share it. One gevent worker with many greenlets is sufficient for this workload. To scale to multiple workers, replace the in-process job store with Redis.


Environment variables

Variable Default Description
MEDIA_ROOT /media Root directory the app may access
PORT 8080 Port Gunicorn listens on
LOG_LEVEL info Gunicorn log level

File layout

videocompressor/
├── app.py              ← Flask application + all API routes
├── wsgi.py             ← Gunicorn entry point  (imports app from app.py)
├── gunicorn.conf.py    ← Gunicorn configuration (gevent, timeout, logging)
├── requirements.txt    ← Python dependencies
├── Dockerfile          ← Two-stage Docker build
├── docker-compose.yml  ← Volume mapping, port, env vars
├── start.sh            ← Helper script (dev + prod modes)
├── README.md
├── templates/
│   └── index.html
└── static/
    ├── css/main.css
    └── js/app.js