cd /news/developer-tools/i-built-a-local-video-processing-wor… · home topics developer-tools article
[ARTICLE · art-40540] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=↑ positive

I Built a Local Video Processing Workstation with AI — Here's the Complete Journey

A developer built ClipForge, a cross-platform desktop video processing app using Electron, React, FFmpeg, and Claude Code. The app handles 20+ video/audio operations locally with single, stack, and batch modes. The developer solved challenges like FFmpeg integration, real-time preview via canvas, and Electron packaging across Windows, macOS, and Linux.

read3 min views1 publishedJun 26, 2026

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.

── more in #developer-tools 4 stories · sorted by recency
── more on @clipforge 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/i-built-a-local-vide…] indexed:0 read:3min 2026-06-26 ·