mcpField
F
ffmpeg-webassembly-workers

ffmpeg-webassembly-workers

github.com/josiahsiegel开发5974 次安装19 周前更新
代码仓库

关于 ffmpeg-webassembly-workers

这是一份开发技能指南,介绍如何在浏览器和边缘环境中通过 WebAssembly 运行 FFmpeg。内容涵盖浏览器转码、单线程与多线程设置、部署响应头、常见操作和 Cloudflare Workers 限制。

主要功能

  1. 01在浏览器中通过 WebAssembly 运行 FFmpeg
  2. 02配置单线程和多线程核心
  3. 03设置 COOP/COEP 响应头
  4. 04转换、裁剪、缩放和提取媒体
  5. 05了解 Cloudflare Workers 的限制

适用场景

  1. 01在 Web 应用中加入客户端视频转换
  2. 02为 React 或 Next.js 集成 FFmpeg 处理
  3. 03通过跨源隔离启用多线程
  4. 04规划 Cloudflare Workers 视频任务方案

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/josiahsiegel/claude-plugin-marketplace --skill ffmpeg-webassembly-workers

安装后在 Claude Code 中输入/ffmpeg-webassembly-workers确认已加载。

详细介绍

关键指南

Windows 文件路径要求

强制要求:在 Windows 上始终使用反斜杠表示文件路径

在 Windows 上使用 Edit 或 Write 工具时,文件路径必须使用反斜杠(\),而不是正斜杠(/)。


快速参考

软件包大小线程安装
@ffmpeg/core~31MB单线程npm install @ffmpeg/ffmpeg @ffmpeg/util
@ffmpeg/core-mt~31MB多线程需要 COOP/COEP 标头
标头值用途
Cross-Origin-Embedder-Policyrequire-corpSharedArrayBuffer
Cross-Origin-Opener-Policysame-origin多线程
操作命令
转换 WebM→MP4await ffmpeg.exec(['-i', 'input.webm', 'output.mp4'])
提取帧await ffmpeg.exec(['-i', 'video.mp4', '-ss', '5', '-vframes', '1', 'thumb.jpg'])

何时使用此技能

适用于基于浏览器的视频处理:

  • 无需服务器的客户端转码
  • React/Vue/Next.js FFmpeg 集成
  • 设置 COOP/COEP 标头
  • Cloudflare Workers 的 FFmpeg 限制
  • 内存管理与清理

FFmpeg WebAssembly 与 Cloudflare Workers(2025)

介绍如何使用 WebAssembly 在浏览器和边缘环境中运行 FFmpeg。

ffmpeg.wasm 概述

ffmpeg.wasm 是一个纯 WebAssembly/JavaScript 版 FFmpeg,可直接在浏览器中运行,无需服务器端处理。

主要功能
  • 基于浏览器:视频处理无需服务器
  • 跨平台:适用于任何现代浏览器
  • 单线程:@ffmpeg/core(约 31MB)
  • 多线程:@ffmpeg/core-mt(需要 SharedArrayBuffer)
  • 可定制:使用特定编解码器构建自己的核心
限制
  • 性能:比原生 FFmpeg 慢约 10–100 倍
  • 内存:受浏览器内存限制
  • 文件大小:核心约 31MB(可通过自定义构建缩小)
  • 线程:多线程需要特定标头(COOP/COEP)
  • 编解码器:并非所有编解码器都可用(受许可限制)

安装

npm
npm install @ffmpeg/ffmpeg @ffmpeg/util
CDN
<script src="https://cdn.jsdelivr.net/npm/@ffmpeg/[email protected]/dist/umd/ffmpeg.min.js"></script>

基本用法

单线程(浏览器)
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/[email protected]/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' })
);
多线程(需要 COOP/COEP 标头)
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/[email protected]/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']);

跨源隔离(SharedArrayBuffer)

多线程 ffmpeg.wasm 需要 SharedArrayBuffer,而这需要跨源隔离标头。

Vite 配置
// 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 配置
// 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 中间件
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 配置
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 集成

React 组件
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/[email protected]/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>
  );
}

常见操作

将 WebM 转换为 MP4
await ffmpeg.exec(['-i', 'input.webm', '-c:v', 'libx264', 'output.mp4']);
提取音频
await ffmpeg.exec(['-i', 'video.mp4', '-vn', '-c:a', 'libmp3lame', 'audio.mp3']);
创建缩略图
await ffmpeg.exec(['-i', 'video.mp4', '-ss', '00:00:05', '-vframes', '1', 'thumb.jpg']);
剪辑视频
await ffmpeg.exec([
  '-i', 'input.mp4',
  '-ss', '00:00:10',
  '-t', '00:00:30',
  '-c', 'copy',
  'output.mp4'
]);
添加水印
await ffmpeg.writeFile('logo.png', await fetchFile(logoFile));
await ffmpeg.exec([
  '-i', 'input.mp4',
  '-i', 'logo.png',
  '-filter_complex', 'overlay=10:10',
  'output.mp4'
]);
缩放视频
await ffmpeg.exec([
  '-i', 'input.mp4',
  '-vf', 'scale=1280:720',
  '-c:a', 'copy',
  'output.mp4'
]);

Cloudflare Workers

当前限制(2025 年 12 月)

在 Cloudflare Workers 上运行 ffmpeg.wasm 面临重大挑战:

  1. 大小限制:Workers 的压缩大小上限为 10MB(付费计划)
  2. 不支持 Web Workers:无法在 CF Worker 内部创建 Worker
  3. SharedArrayBuffer:Workers 环境中不可用
  4. 环境检测:ffmpeg.wasm 的环境检查可能会失败
变通方案
1. 使用外部 FFmpeg 服务
// 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. 将 Durable Objects 与 R2 存储搭配使用
// 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. 自定义 ffmpeg.wasm 构建(实验性)

对于简单操作,精简的自定义构建可能符合限制:

# 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
替代方案:Cloudflare Stream

如需在 Cloudflare 生态系统中处理视频,可考虑使用 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;
  },
};

构建自定义 ffmpeg.wasm 核心

缩小核心体积
# 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)
构建脚本
#!/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)

性能优化

浏览器最佳实践
  1. 延迟加载核心——按需加载
  2. 使用 Web Workers——保持主线程响应
  3. 流式处理——分块处理大文件
  4. 缓存核心——存储在 IndexedDB 或 Cache API 中
  5. 显示进度——使用进度事件改善用户体验
缓存 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/[email protected]/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),
  });
}
内存管理
// 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();
  }
}

故障排除

“SharedArrayBuffer is not defined”

原因:缺少跨源隔离标头

解决方案:在服务器配置中添加 COOP/COEP 标头

“Worker is not defined”(Cloudflare Workers)

原因:ffmpeg.wasm 尝试启动 Web Workers,但 CF Workers 不支持

解决方案:使用单线程核心或外部 FFmpeg 服务

内存不足

原因:大型视频文件耗尽浏览器内存

解决方案:

  1. 分块处理
  2. 使用较低的分辨率/画质
  3. 使用多线程核心(效率更高)
  4. 实现流式处理
编码速度慢

原因:WebAssembly 比原生程序慢

解决方案:

  1. 使用 -preset ultrafast
  2. 编码前降低分辨率
  3. 如果可用,使用硬件加速(实验性)
  4. 对于繁重的工作负载,可考虑在服务器端处理

本指南介绍了 ffmpeg.wasm 和 WebAssembly 部署。有关原生 FFmpeg,请参阅其他技能文档。

评论

支持 Markdown