# 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. --- ## Quick start with Docker (recommended) ```bash # 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 ```bash # 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 | ```bash # 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/ ← SSE stream └─ POST /api/compress/cancel/ ``` **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 ```