A web-based ffmpeg wrapper. Select a file once, stack edits (convert, trim) across tabs, and apply them all in a single ffmpeg pass while you watch live progress.
Note
This project was vibecoded. Nearly all of the code, architecture, and docs were produced iteratively through AI pair-programming with Claude Code (Anthropic), driven by natural-language prompts and verified by testing against real ffmpeg output. It's a hobby project — read the code and review it yourself before relying on it for anything serious.
- Backend — Java 21 / Spring Boot 3.5 REST API that spawns ffmpeg as a subprocess and tracks progress asynchronously. Jobs are persisted to SQLite and old jobs/files are purged by a scheduled cleanup task.
- Frontend — Angular single-page app: select once, edit, process, download.
- Select once, edit everywhere — pick a file at the top; the Convert and Trim tabs configure edits that are stacked into one job. Each tab has an Apply toggle, and the Process button shows a summary of what will run.
- One-pass processing — trim, resize, fps, compression, codec/format change and audio selection all run as a single ffmpeg command. Trim-only uses fast stream copy; when you also convert, the trim becomes frame-accurate.
- Encode controls — target resolution (down to 360p), frame rate, video/audio codec, and a compression slider (Lossless → Low → Medium → High → Ultra, mapped to CRF for software encoders and QP for GPU encoders).
- Target file size — set a max size (MB) and the server computes a constrained video bitrate (90% of the cap, after reserving audio) so the output lands under the limit. Overrides the compression slider.
- Audio tracks — keep all, select specific tracks, or merge (mix) tracks into one. Track list shows codec, channels, and language.
- Platform presets — one-click, size-compliant:
- Discord — HEVC + Opus in MKV, 720p, ≤ 15 MB.
- Twitter/X — H.264 + AAC in MP4, 1080p, ≤ 500 MB.
- Original media info — selecting a file uploads it and runs a fast
ffprobe(no transcode), showing container, codec, resolution, fps, pixel format, bitrate, audio channels/sample-rate, and size beside the preview. - Preview any codec — if the browser can't decode the source (e.g. HEVC/MKV),
the server generates a downscaled H.264 preview proxy (range-streamed for
seeking). You scrub the proxy; edits run on the original file. Proxies are
built with the GPU H.264 encoder (
h264_amf/h264_nvenc/h264_qsv) when one is available, falling back tolibx264if the hardware encode fails. - Switch preview audio track — for multi-track files, a Preview audio
selector swaps which track you hear. Browsers can't switch tracks in
<video>, so the server builds a small per-track proxy on demand (cached, position preserved across switches); the natively-playable default track stays instant. - Async jobs with live progress (frontend polls the job status).
- Hardware-accelerated encoding — detected per-machine at startup via a trial
encode, so the UI only offers GPU encoders that actually work. On an AMD GPU
(AMF) that means H.264 (
h264_amf), HEVC (hevc_amf), and AV1 (av1_amf); NVENC/QSV appear automatically on NVIDIA/Intel hardware. - Persistence — jobs survive restarts (SQLite). Jobs left running when the
server stops are marked
FAILEDon the next boot. - Cleanup — a scheduled task deletes jobs and their files once they pass a configurable TTL.
- ffmpeg and ffprobe on the
PATH(or setmpegado.ffmpeg-path/mpegado.ffprobe-path). - JDK 21+ (the backend uses the bundled Maven wrapper, so no global Maven needed).
- Node.js 20+ for the Angular frontend.
cd backend
./mvnw spring-boot:run # Windows: .\mvnw.cmd spring-boot:runcd frontend
npm startOpen http://localhost:4200. The dev server proxies API calls, and CORS is open
to http://localhost:4200 by default.
| Method | Path | Description |
|---|---|---|
| GET | /api/capabilities |
available formats + video/audio codecs (with a hardware flag) detected on this machine |
| POST | /api/sources |
upload an original; server ffprobes it. Returns source id + mediaInfo |
| GET | /api/sources/{id} |
source status (NONE/PREPARING/READY/FAILED), duration, mediaInfo |
| POST | /api/sources/{id}/proxy?track=N |
lazily build the H.264 preview proxy for audio track N (cached) |
| GET | /api/sources/{id}/preview?track=N |
range-streamed H.264 preview proxy for audio track N |
| POST | /api/jobs/process |
JSON { sourceId, trim?: {startSeconds,endSeconds}, convert?: {targetFormat,videoCodec,audioCodec,scaleHeight,fps,compression}, audio?: {mode,tracks} } — applies all edits in one job |
| GET | /api/jobs |
list jobs (newest first) |
| GET | /api/jobs/{id} |
job status + progress (0–100) |
| GET | /api/jobs/{id}/download |
download the output once COMPLETED |
Codecs and formats in a request are validated against /api/capabilities, so
unknown values are rejected (400) before ffmpeg ever runs.
Override in backend/src/main/resources/application.properties or via env vars:
mpegado.ffmpeg-path/mpegado.ffprobe-path— binary locationsmpegado.work-dir— where uploads/outputs are stored (default: temp dir)mpegado.worker-threads— concurrent conversions (default: 2)mpegado.cors.allowed-origins— comma-separated allowed originsmpegado.db-path— SQLite database file (default:mpegado.db)mpegado.cleanup.ttl-hours— age after which jobs/files are deleted (default: 24)mpegado.cleanup.interval-ms— how often cleanup runs (default: 3600000)spring.servlet.multipart.max-file-size— upload limit (default: 2GB)
mpegado/
├── backend/ # Spring Boot REST API
│ ├── src/main/java/com/mpegado/
│ │ ├── model/ # JPA entities + enums (ConversionJob, Source, Compression…)
│ │ ├── repository/ # Spring Data JPA repositories
│ │ ├── service/ # ffmpeg orchestration, codec probing, cleanup
│ │ │ ├── ConversionService.java # builds + runs the one-pass edit job
│ │ │ ├── FfmpegCommandBuilder.java # assembles the ffmpeg command line
│ │ │ ├── SourceService.java # uploads, ffprobe, preview proxies
│ │ │ └── CodecService.java # detects working (incl. GPU) encoders
│ │ └── web/ # REST controllers + DTOs
│ └── src/main/resources/application.properties
└── frontend/ # Angular single-page app
└── src/app/
├── editor-store.ts # shared edit state (the "select once" model)
├── file-selector.* # upload + preview + media info
├── convert-form.* # format/codec/resolution/fps/size/compression
├── trim-form.* # in/out points
└── audio-form.* # keep / select / merge audio tracks
MIT © 0xVacent