From idea to release in 3 weeks, using Claude Code to build ClipForge — a cross-platform desktop app powered by Electron and FFmpeg.
Most video processing tools force you to:
I wanted a fully local, feature-rich, good-looking video processing tool. So I built ClipForge.
ClipForge is a desktop app that handles 20+ video/audio operations locally:
Three modes: Single operation, Stack (chain multiple ops), Batch processing.
Built with Electron, React, FFmpeg, and Zustand. Ships for Windows, macOS, and Linux.
| Layer | Tech | Why |
|---|---|---|
| Desktop | Electron 42 | Cross-platform, Node.js for FFmpeg |
| UI | React 18 + TypeScript | Component ecosystem |
| Build | Vite 5 + Electron Forge | Fast HMR, clean packaging |
| State | Zustand | Simple, no boilerplate |
| Styling | Tailwind CSS | Rapid UI development |
| Video | FFmpeg (bundled) | Industry-standard processing |
| AI | Claude Code | Pair programming assistant |
npm create electron-app clipforge
Electron Forge generated the boilerplate: main process, preload script, renderer with Vite.
Built a 4-panel layout:
Dark theme with Tailwind CSS.
This is the core challenge — wrapping FFmpeg's CLI into visual operations.
Architecture:
Renderer (React)
│ invoke('process:start', request)
▼
Preload (IPC bridge)
│
▼
Main Process (Node.js)
│ composeArgs(request) → ffmpeg args array
▼
FFmpeg (child_process.spawn)
│ progress parsing from stderr
▼
Events back to renderer
Example: Watermark Removal
Instead of FFmpeg's delogo
filter (which has boundary restrictions — x≥1, y≥1, no edge support), I used a crop + blur + overlay approach:
case 'delogo': {
const x = Math.max(0, Math.round(Number(p.x) || 0));
const y = Math.max(0, Math.round(Number(p.y) || 0));
const w = Math.max(10, Math.round(Number(p.w) || 10));
const h = Math.max(10, Math.round(Number(p.h) || 10));
args.push('-filter_complex',
`[0:v]split[a][b];` +
`[b]crop=${w}:${h}:${x}:${y},gblur=sigma=30,format=rgba,colorchannelmixer=aa=0.7[b2];` +
`[a][b2]overlay=${x}:${y}[out]`
);
args.push('-map', '[out]', '-map', '0:a?');
args.push(...videoCodec(outExt));
break;
}
The filter graph:
crop
— extract the watermark regiongblur
— Gaussian blur (more natural than boxblur)colorchannelmixer=aa=0.7
— semi-transparent blend for smooth integrationUsers need to see changes immediately, not after processing completes.
Solution: Canvas-based preview simulation. Instead of running FFmpeg, read frames from the <video>
element and apply operations on a <canvas>
:
useEffect(() => {
const render = () => {
drawPreview(ctx, video, previewOps, { width: rect.width, height: rect.height });
};
render(); // immediate draw
if (playing) {
const loop = () => { render(); raf = requestAnimationFrame(loop); };
raf = requestAnimationFrame(loop);
}
return () => cancelAnimationFrame(raf);
}, [playing, playhead, JSON.stringify(previewOps)]);
Adjusting brightness, crop region, or rotation shows instant feedback.
For watermark removal, users drag to select the area. Screen coordinates must convert to video pixel coordinates (accounting for letterbox scaling):
function screenToVideo(localX, localY, container, videoW, videoH) {
const { scale, ox, oy } = getVideoMapping(container, videoW, videoH);
return {
x: Math.max(0, Math.min(Math.round((localX - ox) / scale), videoW)),
y: Math.max(0, Math.min(Math.round((localY - oy) / scale), videoH)),
};
}
Bug I hit: The onUp
callback captured stale state from useState
. Fixed by using useRef
for live coordinates during drag.
Electron packaging is tricky — FFmpeg binaries can't go inside the asar archive, and Linux needs lowercase executable names.
forge.config.ts:
packagerConfig: {
asar: { unpackDir: 'src/main/ffmpeg' },
extraResource: ['src/main/ffmpeg'],
executableName: 'clipforge',
}
GitHub Actions builds all three platforms in parallel:
jobs:
build:
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npm run make
Push a tag → auto-build → auto-publish to GitHub Releases.
Claude Code handled tedious work (Electron packaging, FFmpeg arg mapping, IPC boilerplate), but I still needed to:
Got "open file → select operation → process → output" working before adding preview, batch mode, or i18n.
Binary files, asar compression, platform-specific naming — expect to spend time here. Automate with CI early.
License: MIT + Commons Clause (free for personal use, commercial use requires authorization).
Built with Electron, FFmpeg, and a lot of help from AI. The future of indie development is here.