DEV Community

nareshipme
nareshipme

Posted on

framewebworker: Browser-Native Video Rendering with OffscreenCanvas, Web Workers, and ffmpeg.wasm

Server-side video rendering is expensive, operationally painful, and doesn't scale well. You need GPU instances, media processing queues, temp storage, CDN delivery pipelines — a whole infrastructure just to clip a video. We hit every one of those walls building ClipCrafter, and eventually asked: what if the browser just did it?

The result is framewebworker — an open-source library that runs video export entirely in the browser using OffscreenCanvas, Web Workers, and ffmpeg.wasm. No server. No queue. No infra.

The Problem with Server-Side Video Rendering

Traditional video rendering pipelines: user selects clips → API queues job → worker spins up ffmpeg → uploads to S3 → returns signed URL. This works, but every concurrent render ties up server resources. Peak usage means queuing delays. Cold starts on serverless are brutal for long-running ffmpeg processes.

The browser is now powerful enough to do this locally. For most use cases — trimming, merging, exporting clips — it's a better fit.

Introducing framewebworker

npm install framewebworker
Enter fullscreen mode Exit fullscreen mode

GitHub: https://github.com/nareshipme/framewebworker | npm: https://www.npmjs.com/package/framewebworker

Core API

exportClips(videoUrl, segments[])

import { exportClips } from 'framewebworker';

const results = await exportClips('https://example.com/video.mp4', [
  { start: 0,  end: 10, label: 'intro' },
  { start: 45, end: 60, label: 'highlight' },
]);

results.forEach(({ blob, label, metrics }) => {
  console.log(label + ': ' + metrics.totalMs + 'ms @ ' + metrics.framesPerSecond + 'fps');
});
Enter fullscreen mode Exit fullscreen mode

mergeClips(clips[])

import { mergeClips } from 'framewebworker';

const merged = await mergeClips([
  'https://example.com/clip-a.mp4',
  'https://example.com/clip-b.mp4',
]);
console.log('Merged in', merged.metrics.totalMs, 'ms');
Enter fullscreen mode Exit fullscreen mode

useExportClips() — React hook

import { useExportClips } from 'framewebworker';

function ExportButton({ videoUrl, segments }) {
  const { exportClips, progress, results, isExporting } = useExportClips();
  return (
    <button onClick={() => exportClips(videoUrl, segments)} disabled={isExporting}>
      {isExporting ? 'Exporting... ' + progress + '%' : 'Export Clips'}
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

Architecture

Workers are spawned based on navigator.hardwareConcurrency (capped at 4). Each segment dispatches to the next available worker — multi-clip exports run in parallel. Each worker uses OffscreenCanvas for frame extraction and ffmpeg.wasm for encoding.

RenderMetrics

interface RenderMetrics {
  extractionMs: number;    // Frame extraction time
  encodingMs: number;      // ffmpeg encoding time  
  totalMs: number;         // End-to-end wall clock
  framesPerSecond: number; // Effective fps
}
Enter fullscreen mode Exit fullscreen mode

COOP/COEP Headers Required

ffmpeg.wasm needs SharedArrayBuffer, which requires cross-origin isolation:

// next.config.js
module.exports = {
  async headers() {
    return [{
      source: '/(.*)',
      headers: [
        { key: 'Cross-Origin-Opener-Policy', value: 'same-origin' },
        { key: 'Cross-Origin-Embedder-Policy', value: 'require-corp' },
      ],
    }];
  },
};
Enter fullscreen mode Exit fullscreen mode

Note: these headers can break cross-origin iframes and some OAuth popups. Audit your third-party dependencies before shipping.

Bundle Size

ffmpeg.wasm is ~30MB but loaded lazily — only triggered when the user initiates their first export. Initial page load is completely unaffected. First-export cold start is ~2-3s. After that, rendering is fast and stays in memory for the session.

When to Use framewebworker

Good fit: Web apps where users clip/trim/merge short videos, products eliminating backend rendering infra, controlled deployments where you can set COOP/COEP headers.

Not ideal: Very long videos (>30min), environments where you can't control response headers, apps with heavy third-party embeds that break under cross-origin isolation.

Get Started

npm install framewebworker
Enter fullscreen mode Exit fullscreen mode

PRs, issues, and stars welcome. Multi-track audio mixing and subtitle burn-in are next on the roadmap.

Top comments (0)