Claude-skill-registry ffmpeg-webassembly-workers
Complete browser-based FFmpeg system. PROACTIVELY activate for: (1) ffmpeg.wasm setup and loading, (2) Browser video transcoding, (3) React/Vue/Next.js integration, (4) SharedArrayBuffer and COOP/COEP headers, (5) Multi-threaded ffmpeg-core-mt, (6) Cloudflare Workers limitations, (7) Custom ffmpeg.wasm builds, (8) Memory management and cleanup, (9) Progress tracking and UI, (10) IndexedDB core caching. Provides: Framework-specific examples, header configuration, common operation recipes, performance optimization, troubleshooting guides. Ensures: Client-side video processing without server dependencies.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/ffmpeg-webassembly-workers" ~/.claude/skills/majiayu000-claude-skill-registry-ffmpeg-webassembly-workers && rm -rf "$T"
skills/data/ffmpeg-webassembly-workers/SKILL.mdCRITICAL GUIDELINES
Windows File Path Requirements
MANDATORY: Always Use Backslashes on Windows for File Paths
When using Edit or Write tools on Windows, you MUST use backslashes (
\) in file paths, NOT forward slashes (/).
Quick Reference
| Package | Size | Threading | Install |
|---|---|---|---|
| ~31MB | Single | |
| ~31MB | Multi | Requires COOP/COEP headers |
| Header | Value | Purpose |
|---|---|---|
| Cross-Origin-Embedder-Policy | | SharedArrayBuffer |
| Cross-Origin-Opener-Policy | | Multi-threading |
| Operation | Command |
|---|---|
| Convert WebM→MP4 | |
| Extract frame | |
When to Use This Skill
Use for browser-based video processing:
- Client-side transcoding without server
- React/Vue/Next.js FFmpeg integration
- Setting up COOP/COEP headers
- Cloudflare Workers FFmpeg limitations
- Memory management and cleanup
FFmpeg WebAssembly & Cloudflare Workers (2025)
Guide to running FFmpeg in browsers and edge environments using WebAssembly.
ffmpeg.wasm Overview
ffmpeg.wasm is a pure WebAssembly/JavaScript port of FFmpeg that runs directly in browsers without server-side processing.
Key Features
- Browser-based: No server required for video processing
- Cross-platform: Works on any modern browser
- Single-thread: @ffmpeg/core (~31MB)
- Multi-thread: @ffmpeg/core-mt (requires SharedArrayBuffer)
- Customizable: Build your own core with specific codecs
Limitations
- Performance: ~10-100x slower than native FFmpeg
- Memory: Limited by browser memory constraints
- File size: Core is ~31MB (can be reduced with custom builds)
- Threading: Multi-thread requires specific headers (COOP/COEP)
- Codecs: Not all codecs available (licensing restrictions)
Installation
npm
npm install @ffmpeg/ffmpeg @ffmpeg/util
CDN
<script src="https://cdn.jsdelivr.net/npm/@ffmpeg/ffmpeg@0.12.10/dist/umd/ffmpeg.min.js"></script>
Basic Usage
Single-Thread (Browser)
import { FFmpeg } from '@ffmpeg/ffmpeg'; import { fetchFile, toBlobURL } from '@ffmpeg/util'; const ffmpeg = new FFmpeg(); // Load FFmpeg core const baseURL = 'https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.10/dist/umd'; await ffmpeg.load({ coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'), wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'), }); // Transcode video await ffmpeg.writeFile('input.webm', await fetchFile(videoFile)); await ffmpeg.exec(['-i', 'input.webm', 'output.mp4']); const data = await ffmpeg.readFile('output.mp4'); // Create blob URL for playback const videoURL = URL.createObjectURL( new Blob([data.buffer], { type: 'video/mp4' }) );
Multi-Thread (Requires COOP/COEP Headers)
import { FFmpeg } from '@ffmpeg/ffmpeg'; import { fetchFile, toBlobURL } from '@ffmpeg/util'; const ffmpeg = new FFmpeg(); // Load multi-threaded core const baseURL = 'https://cdn.jsdelivr.net/npm/@ffmpeg/core-mt@0.12.10/dist/umd'; await ffmpeg.load({ coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'), wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'), workerURL: await toBlobURL(`${baseURL}/ffmpeg-core.worker.js`, 'text/javascript'), }); // Use multi-threaded encoding await ffmpeg.exec(['-i', 'input.webm', '-threads', '4', 'output.mp4']);
Cross-Origin Isolation (SharedArrayBuffer)
Multi-threaded ffmpeg.wasm requires SharedArrayBuffer, which needs Cross-Origin Isolation headers.
Vite Configuration
// vite.config.js export default { server: { headers: { "Cross-Origin-Embedder-Policy": "require-corp", "Cross-Origin-Opener-Policy": "same-origin", }, }, optimizeDeps: { exclude: ["@ffmpeg/ffmpeg", "@ffmpeg/util"], }, };
Next.js Configuration
// next.config.js module.exports = { async headers() { return [ { source: "/:path*", headers: [ { key: "Cross-Origin-Embedder-Policy", value: "require-corp" }, { key: "Cross-Origin-Opener-Policy", value: "same-origin" }, ], }, ]; }, };
Express.js Middleware
import express from 'express'; const app = express(); app.use((req, res, next) => { res.setHeader("Cross-Origin-Embedder-Policy", "require-corp"); res.setHeader("Cross-Origin-Opener-Policy", "same-origin"); res.setHeader("Cross-Origin-Resource-Policy", "cross-origin"); next(); }); app.use(express.static('public')); app.listen(3000);
Nginx Configuration
server { listen 443 ssl; add_header Cross-Origin-Embedder-Policy "require-corp" always; add_header Cross-Origin-Opener-Policy "same-origin" always; location / { root /var/www/html; } }
React Integration
React Component
import { useState, useRef } from 'react'; import { FFmpeg } from '@ffmpeg/ffmpeg'; import { fetchFile, toBlobURL } from '@ffmpeg/util'; function VideoTranscoder() { const [loaded, setLoaded] = useState(false); const [progress, setProgress] = useState(0); const [outputURL, setOutputURL] = useState(null); const ffmpegRef = useRef(new FFmpeg()); const load = async () => { const ffmpeg = ffmpegRef.current; // Progress handler ffmpeg.on('progress', ({ progress }) => { setProgress(Math.round(progress * 100)); }); const baseURL = 'https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.10/dist/umd'; await ffmpeg.load({ coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'), wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'), }); setLoaded(true); }; const transcode = async (file) => { const ffmpeg = ffmpegRef.current; await ffmpeg.writeFile('input.webm', await fetchFile(file)); await ffmpeg.exec([ '-i', 'input.webm', '-c:v', 'libx264', '-preset', 'ultrafast', '-c:a', 'aac', 'output.mp4' ]); const data = await ffmpeg.readFile('output.mp4'); const url = URL.createObjectURL( new Blob([data.buffer], { type: 'video/mp4' }) ); setOutputURL(url); }; return ( <div> {!loaded ? ( <button onClick={load}>Load FFmpeg (~31MB)</button> ) : ( <> <input type="file" accept="video/*" onChange={(e) => transcode(e.target.files[0])} /> <p>Progress: {progress}%</p> {outputURL && <video src={outputURL} controls />} </> )} </div> ); }
Common Operations
Convert WebM to MP4
await ffmpeg.exec(['-i', 'input.webm', '-c:v', 'libx264', 'output.mp4']);
Extract Audio
await ffmpeg.exec(['-i', 'video.mp4', '-vn', '-c:a', 'libmp3lame', 'audio.mp3']);
Create Thumbnail
await ffmpeg.exec(['-i', 'video.mp4', '-ss', '00:00:05', '-vframes', '1', 'thumb.jpg']);
Trim Video
await ffmpeg.exec([ '-i', 'input.mp4', '-ss', '00:00:10', '-t', '00:00:30', '-c', 'copy', 'output.mp4' ]);
Add Watermark
await ffmpeg.writeFile('logo.png', await fetchFile(logoFile)); await ffmpeg.exec([ '-i', 'input.mp4', '-i', 'logo.png', '-filter_complex', 'overlay=10:10', 'output.mp4' ]);
Scale Video
await ffmpeg.exec([ '-i', 'input.mp4', '-vf', 'scale=1280:720', '-c:a', 'copy', 'output.mp4' ]);
Cloudflare Workers
Current Limitations (December 2025)
Running ffmpeg.wasm on Cloudflare Workers faces significant challenges:
- Size limit: Workers have a 10MB compressed limit (paid plan)
- No Web Workers: Cannot spawn workers from within a CF Worker
- SharedArrayBuffer: Not available in Workers environment
- Environment detection: ffmpeg.wasm environment checks may fail
Workarounds
1. Use External FFmpeg Service
// Cloudflare Worker calling external FFmpeg API export default { async fetch(request) { const formData = await request.formData(); const video = formData.get('video'); // Send to external FFmpeg service const response = await fetch('https://your-ffmpeg-api.com/transcode', { method: 'POST', body: video, }); return response; }, };
2. Use Durable Objects with R2 Storage
// Store video in R2, process with external service export default { async fetch(request, env) { const video = await request.arrayBuffer(); // Store in R2 await env.BUCKET.put('input.mp4', video); // Trigger external processing await env.QUEUE.send({ bucket: 'BUCKET', key: 'input.mp4', }); return new Response('Processing started'); }, };
3. Custom ffmpeg.wasm Build (Experimental)
For simple operations, a minimal custom build may fit within limits:
# Build minimal ffmpeg.wasm (~8MB compressed) # Only include essential codecs git clone https://github.com/ffmpegwasm/ffmpeg.wasm cd ffmpeg.wasm # Modify build scripts to include only needed codecs npm run build:core -- --disable-all --enable-libx264
Alternative: Cloudflare Stream
For video processing in Cloudflare ecosystem, consider Cloudflare Stream:
// Upload to Cloudflare Stream for processing export default { async fetch(request, env) { const formData = new FormData(); formData.append('file', await request.arrayBuffer()); const response = await fetch( `https://api.cloudflare.com/client/v4/accounts/${env.ACCOUNT_ID}/stream`, { method: 'POST', headers: { 'Authorization': `Bearer ${env.API_TOKEN}`, }, body: formData, } ); return response; }, };
Building Custom ffmpeg.wasm Core
Reduce Core Size
# Custom Dockerfile for minimal build FROM emscripten/emsdk:3.1.50 WORKDIR /src # Clone FFmpeg RUN git clone https://github.com/FFmpeg/FFmpeg.git ffmpeg WORKDIR /src/ffmpeg # Configure with minimal codecs RUN emconfigure ./configure \ --target-os=none \ --arch=x86_32 \ --enable-cross-compile \ --disable-x86asm \ --disable-inline-asm \ --disable-stripping \ --disable-programs \ --disable-doc \ --disable-debug \ --disable-runtime-cpudetect \ --disable-autodetect \ --enable-small \ --enable-gpl \ --enable-libx264 \ --extra-cflags="-O3 -s USE_PTHREADS=1" \ --extra-ldflags="-O3 -s USE_PTHREADS=1" RUN emmake make -j$(nproc)
Build Script
#!/bin/bash # build-minimal.sh # Configure build ./configure \ --disable-all \ --enable-avcodec \ --enable-avformat \ --enable-avutil \ --enable-swresample \ --enable-swscale \ --enable-decoder=h264 \ --enable-decoder=aac \ --enable-encoder=libx264 \ --enable-encoder=aac \ --enable-muxer=mp4 \ --enable-demuxer=mov \ --enable-protocol=file \ --enable-gpl \ --enable-libx264 make -j$(nproc)
Performance Optimization
Browser Best Practices
- Load core lazily - Don't load until needed
- Use Web Workers - Keep main thread responsive
- Stream processing - Process chunks for large files
- Cache core - Store in IndexedDB or Cache API
- Show progress - Use progress events for UX
Caching ffmpeg-core
// Cache ffmpeg core in IndexedDB async function loadCachedFFmpeg() { const cacheKey = 'ffmpeg-core-0.12.10'; // Check cache const cached = await getCachedCore(cacheKey); if (cached) { await ffmpeg.load({ coreURL: URL.createObjectURL(cached.core), wasmURL: URL.createObjectURL(cached.wasm), }); return; } // Download and cache const baseURL = 'https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.10/dist/umd'; const [core, wasm] = await Promise.all([ fetch(`${baseURL}/ffmpeg-core.js`).then(r => r.blob()), fetch(`${baseURL}/ffmpeg-core.wasm`).then(r => r.blob()), ]); await cacheCore(cacheKey, { core, wasm }); await ffmpeg.load({ coreURL: URL.createObjectURL(core), wasmURL: URL.createObjectURL(wasm), }); }
Memory Management
// Clean up after processing async function processAndCleanup(inputFile) { const ffmpeg = new FFmpeg(); await ffmpeg.load({ ... }); try { await ffmpeg.writeFile('input.mp4', await fetchFile(inputFile)); await ffmpeg.exec(['-i', 'input.mp4', 'output.mp4']); const data = await ffmpeg.readFile('output.mp4'); // Clean up virtual filesystem await ffmpeg.deleteFile('input.mp4'); await ffmpeg.deleteFile('output.mp4'); return data; } finally { // Terminate FFmpeg to free memory ffmpeg.terminate(); } }
Troubleshooting
"SharedArrayBuffer is not defined"
Cause: Missing Cross-Origin Isolation headers
Solution: Add COOP/COEP headers to server configuration
"Worker is not defined" (Cloudflare Workers)
Cause: ffmpeg.wasm tries to spawn Web Workers, unavailable in CF Workers
Solution: Use single-thread core or external FFmpeg service
Out of memory
Cause: Large video files exhaust browser memory
Solutions:
- Process in chunks
- Use lower resolution/quality
- Use multi-thread core (more efficient)
- Implement streaming processing
Slow encoding
Cause: WebAssembly is slower than native
Solutions:
- Use
-preset ultrafast - Reduce resolution before encoding
- Use hardware acceleration if available (experimental)
- Consider server-side processing for heavy workloads
This guide covers ffmpeg.wasm and WebAssembly deployment. For native FFmpeg, see other skill documents.