Skip to content

Repository files navigation

Video Merger

A self-hosted web app to merge, trim, and crop videos — right on your own machine. Drag and drop your clips, arrange them in any order, trim or crop any of them, and download the result. A single video can also be trimmed or cropped on its own.

  • Smart merging — when all inputs share the same codec, resolution, frame rate, and audio format, the merge is a near-instant stream copy with no quality loss. Mismatched inputs are automatically normalized (letterboxed to the largest canvas, re-encoded to H.264/AAC).
  • Trimming — set a start/end point per clip before merging, or trim a single video standalone. Trims are frame-accurate: any job containing a trim is re-encoded so the cut lands exactly where you set it.
  • Cropping — drag a crop rectangle on any clip (with 16:9 / 9:16 / 1:1 / 4:3 aspect presets or free-form). Like trims, any crop forces a re-encode; untouched jobs keep the fast stream-copy path.
  • Fully offline — no CDNs, no external fonts, no outbound requests. Designed to run in air-gapped environments.
  • Any machine — ships as a single Docker container.

Screenshots

Drag & drop your clips:

Landing page with drag-and-drop zone

Arrange, reorder, and edit clips before merging — the timeline strip shows how they combine:

Workspace with clips, merge button, and timeline

Trim and crop in one dialog — drag the crop box (with aspect presets) and set start/end on the filmstrip:

Edit dialog with crop rectangle, aspect presets, and trim track

Preview and download the result:

Finished merge with preview player and download button

Quick start (Docker)

Requires Docker with the compose plugin.

git clone https://github.com/mkamranr/videomerger.git
cd videomerger
docker compose up --build -d

Then open http://localhost:8000 — drop in your videos and go.

To stop: docker compose down. Uploaded files and outputs live in a named Docker volume (videomerger-data) and are cleaned up automatically after each job expires.

How the image works

The Docker image contains only the runtime: Python, FFmpeg, and all Python packages, installed at build time. The application code (app/ and static/) is not baked into the image — it is volume-mounted into the container at run time. This means you can update the app on a server by copying new files, without rebuilding or re-transferring the image. The container never downloads anything at runtime. See DEPLOYMENT.md for the update procedure.

Running without compose:

docker build -t videomerger:1.0 .
docker run -d -p 8000:8000 \
  -v "$PWD/app:/app/app:ro" \
  -v "$PWD/static:/app/static:ro" \
  -v videomerger-data:/data \
  --name videomerger videomerger:1.0

Air-gapped deployment

Build the image once on an internet-connected machine, then move it as a tar archive:

# On the connected machine
git clone https://github.com/mkamranr/videomerger.git
cd videomerger
docker build -t videomerger:1.0 .
docker save videomerger:1.0 -o videomerger-1.0.tar

Copy to the air-gapped host (USB drive, internal file share, ...):

  1. videomerger-1.0.tar — the runtime image
  2. The project's app/ and static/ directories (and docker-compose.airgap_v1.yml)
# On the air-gapped host, from the directory containing app/ and static/
docker load -i videomerger-1.0.tar
docker compose -f docker-compose.airgap_v1.yml up -d

(The air-gapped compose file has no build: entry, so an offline rebuild can never be attempted; the loaded image is used as-is.)

To update the app later, just replace the files in app/ or static/ and restart the container (docker restart videomerger) — no rebuild, no internet. Full procedure in DEPLOYMENT.md.

The app makes no outbound network requests at runtime: FFmpeg and Python packages live in the image, and all frontend assets (including SortableJS) are served locally.

Run locally for development (Windows/Linux/macOS)

Requires Python 3.10+ and ffmpeg/ffprobe on PATH.

git clone https://github.com/mkamranr/videomerger.git
cd videomerger
python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\Activate.ps1
pip install -r requirements.txt
uvicorn app.main:app --reload --port 8000

Configuration

Environment variable Default Description
DATA_DIR ./data (/data in Docker) Where uploads and outputs are stored
JOB_TTL_SECONDS 3600 How long finished jobs are kept before cleanup
MAX_FILES 10 Maximum number of videos per merge

API

Method Path Description
POST /api/merge Multipart upload, repeated files field in merge order; optional trims field — JSON list parallel to files, each entry null or {"start": s, "end": e} in seconds; optional crops field — JSON list parallel to files, each entry null or {"x", "y", "w", "h"} in source pixels; a single file requires at least one trim or crop; returns {job_id}
GET /api/jobs/{id} Job status: state, mode, progress, message
GET /api/jobs/{id}/download Download the finished MP4

Example:

curl -F "files=@clip1.mp4" -F "files=@clip2.mp4" \
     -F 'trims=[{"start":2,"end":10},null]' \
     -F 'crops=[null,{"x":0,"y":0,"w":1280,"h":720}]' \
     http://localhost:8000/api/merge

Project layout

app/       FastAPI backend: routes (main.py), ffprobe planning (probe.py),
           ffmpeg execution (merge.py)
static/    Frontend: single page, vanilla JS/CSS, vendored SortableJS
Dockerfile, docker-compose.yml, docker-compose.airgap_v1.yml

About

Self-hosted web app to merge, trim, and crop videos with FFmpeg. Fully offline, Docker-ready, air-gap friendly.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages