2026-03-09 17:42:26 -05:00
|
|
|
# VideoPress — FFmpeg Video Compressor
|
|
|
|
|
|
2026-03-09 17:54:34 -05:00
|
|
|
NOTE: This application was built with the help of AI.
|
|
|
|
|
|
|
|
|
|
- [x] The application code has been reviewed by this developer.
|
|
|
|
|
- [x] The application code has been tested by this developer.
|
|
|
|
|
|
|
|
|
|
If you find any issues, please feel free to post an issue, and / or a pull request for a fix.
|
|
|
|
|
|
|
|
|
|
## What is VideoPress?
|
2026-03-09 17:42:26 -05:00
|
|
|
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.
|
|
|
|
|
|
2026-03-13 09:49:05 -05:00
|
|
|

|
2026-03-13 09:46:54 -05:00
|
|
|
|
2026-03-09 17:42:26 -05:00
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Quick start with Docker (recommended)
|
|
|
|
|
|
|
|
|
|
|
2026-03-12 16:51:57 -05:00
|
|
|
Step. 1. You have two options
|
2026-03-12 16:53:21 -05:00
|
|
|
|
2026-03-12 16:51:57 -05:00
|
|
|
A. Build the image
|
2026-03-12 16:53:21 -05:00
|
|
|
|
2026-03-12 16:54:21 -05:00
|
|
|
`docker build -t videopress .`
|
2026-03-12 16:51:57 -05:00
|
|
|
|
|
|
|
|
B. Use the pre-built image
|
2026-03-12 16:53:21 -05:00
|
|
|
|
2026-03-12 16:54:21 -05:00
|
|
|
Already included in the `docker-compose.yml` file witht his project.
|
2026-03-12 16:51:57 -05:00
|
|
|
|
|
|
|
|
Step 2. Run — replace `/your/video/path` with the real path on your host
|
2026-03-12 16:53:21 -05:00
|
|
|
|
2026-03-12 16:51:57 -05:00
|
|
|
A. Using `docker run`:
|
|
|
|
|
|
2026-03-12 16:55:53 -05:00
|
|
|
```
|
|
|
|
|
docker run -d \
|
2026-03-09 17:42:26 -05:00
|
|
|
--name videopress \
|
|
|
|
|
--restart unless-stopped \
|
|
|
|
|
-p 8080:8080 \
|
|
|
|
|
-v /your/video/path:/media \
|
|
|
|
|
videopress
|
|
|
|
|
```
|
2026-03-12 16:51:57 -05:00
|
|
|
B. Using Docker Compose
|
2026-03-12 16:53:21 -05:00
|
|
|
|
2026-03-12 16:51:57 -05:00
|
|
|
`docker compose up -d`
|
2026-03-09 17:42:26 -05:00
|
|
|
|
2026-03-12 16:51:57 -05:00
|
|
|
Step 3. Open http://localhost:8080
|
2026-03-09 17:42:26 -05:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## Bare-metal (without Docker)
|
|
|
|
|
|
|
|
|
|
### Requirements
|
|
|
|
|
|
|
|
|
|
| Tool | Version |
|
|
|
|
|
|------|---------|
|
|
|
|
|
| Python | 3.8+ |
|
|
|
|
|
| FFmpeg + FFprobe | any recent |
|
|
|
|
|
| pip packages | see requirements.txt |
|
|
|
|
|
|
|
|
|
|
```bash
|
2026-03-09 17:54:34 -05:00
|
|
|
# Create a python virtual environment to run in
|
|
|
|
|
python3 -m venv videopress
|
|
|
|
|
|
|
|
|
|
#start the virtual environment
|
|
|
|
|
source ./videopress/bin/activate
|
|
|
|
|
|
2026-03-09 17:42:26 -05:00
|
|
|
# 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
|
|
|
|
|
```
|