▸_ffmpegtool
~/blog/ffmpeg-in-browser-wasm
WASM10 авг. 2026 г. · 10 min

ffmpeg.wasm: как запустить FFmpeg в браузере

Практические заметки: загрузка ядра, COOP/COEP, память MemFS, однопоточная скорость и случаи, когда браузер объективно не нужен.

FFmpeg — мощный, но это огромный C-проект. Тем не менее ffmpeg.wasm действительно запускает его в браузере, и на этом работают все инструменты сайта с пометкой «файл не загружается на сервер». Ниже — практические заметки о том, как довести такую сборку до приемлемого состояния.

Почему WASM, а не сервер

Схема «загрузил файл на сервер — обработал — скачал обратно» упирается в три вещи: трафик, очередь и приватность. Чтобы отрезать первые десять секунд, не нужно отправлять в облако несколько гигабайт исходника.

WebAssembly даёт третий путь: FFmpeg компилируется в .wasm и исполняется в песочнице браузера. У этого подхода есть цена:

  • ядро весит около 30 МБ, первая загрузка медленная;
  • память ограничена пределами WASM, примерно 2–4 ГБ;
  • для многопоточности нужны заголовки кросс-доменной изоляции.

Для небольших файлов, приватных материалов и одной простой операции — обрезки, конвертации, сжатия, извлечения звука — такой обмен обычно выгоден.

Загрузка ядра: CDN и локальный запас

Ядро большое, поэтому у нас приоритет CDN, а при сбое — локальная копия:

import { FFmpeg } from '@ffmpeg/ffmpeg'
import { toBlobURL } from '@ffmpeg/util'

const CDN_BASE = 'https://unpkg.com/@ffmpeg/core@0.12.10/dist/esm'

async function loadCore(ffmpeg: FFmpeg) {
  try {
    await ffmpeg.load({
      coreURL: await toBlobURL(`${CDN_BASE}/ffmpeg-core.js`, 'text/javascript'),
      wasmURL: await toBlobURL(`${CDN_BASE}/ffmpeg-core.wasm`, 'application/wasm'),
    })
  } catch {
    ffmpeg.terminate()
    await ffmpeg.load({
      coreURL: await toBlobURL('/ffmpeg-core/ffmpeg-core.js', 'text/javascript'),
      wasmURL: await toBlobURL('/ffmpeg-core/ffmpeg-core.wasm', 'application/wasm'),
    })
  }
}

Три детали:

  1. Перед запасной загрузкой вызывайте terminate(). После неудачного load() worker может остаться в полуинициализированном состоянии.
  2. Используйте toBlobURL. Worker загружает скрипт через blob URL, и это обходит кросс-доменные ограничения.
  3. Не смешивайте два вида прогресса. ffmpeg.load() показывает загрузку ядра, ffmpeg.exec() — обработку. В интерфейсе это должно быть разделено.

SharedArrayBuffer и кросс-доменная изоляция

Для многопоточной версии нужен SharedArrayBuffer, а браузер разрешает его только странице с кросс-доменной изоляцией. В nginx обычно добавляют:

add_header Cross-Origin-Opener-Policy same-origin;
add_header Cross-Origin-Embedder-Policy require-corp;

Ошибки вы не увидите: страница может работать, но SharedArrayBuffer окажется undefined, и ffmpeg.wasm тихо перейдёт в один поток. Проверяйте перед релизом:

console.log(typeof SharedArrayBuffer !== 'undefined') // должно быть true

У COEP: require-corp есть побочный эффект: все кросс-доменные ресурсы — картинки, шрифты, скрипты аналитики — должны иметь корректные CORS-заголовки.

MemFS и память

ffmpeg.wasm работает с виртуальной файловой системой в памяти:

await ffmpeg.writeFile('input.mp4', await fetchFile(file))
await ffmpeg.exec(['-i', 'input.mp4', '-ss', '10', '-c', 'copy', 'output.mp4'])
const data = await ffmpeg.readFile('output.mp4')

Посчитайте память на файле 500 МБ: буфер исходного файла, копия в MemFS и обработка внутри WASM-кучи. Пиковое потребление легко упирается в лимит вкладки, после чего приходит AbortError: out of memory.

Практические правила:

  • где можно, используйте -c copy;
  • после exec() удаляйте файлы через deleteFile();
  • не пытайтесь обрабатывать любые размеры — честно называйте границу и предлагайте локальный FFmpeg для больших задач.

Производительность

Однопоточная версия обычно даёт от 20% до 50% скорости нативного FFmpeg. Что реально помогает:

  • многопоточное ядро @ffmpeg/core-mt плюс заголовки изоляции, ускорение примерно в 2–4 раза;
  • явный -threads с числом доступных ядер;
  • входной seek через -ss вместо обработки всего файла;
  • отсутствие лишнего перекодирования: пересборку контейнера и обрезку без перекодирования оставляйте в stream copy.

Короткий список проблем

СимптомПричинаРешение
Второй load() после ошибки CDNWorker в промежуточном состоянииСначала terminate()
В проде медленнее, чем в devНет COOP/COEPДобавить заголовки и проверить SharedArrayBuffer
out of memory на большом файлеКопии в JS-куче и MemFS-c copy, удаление файлов, ограничение размера
Прогресс стоит на 0%Смешаны загрузка ядра и обработкаРазделить прогресс load и exec
Файл 0 байтОшибка аргументов execПроверять код возврата и результат

Когда WASM не нужен

Честная граница есть. Тяжёлое перекодирование длинных видео больше 1 ГБ, пакетные задачи с GPU и кодеки с ограничениями по лицензиям лучше отдавать серверу или локальной утилите. Разумный вариант — гибрид: лёгкие операции остаются в браузере, для тяжёлых даётся понятная инструкция.

Проверить можно в лаборатории FFmpeg-команд или на обрезке видео. Файл остаётся на устройстве.

FAQ

В: ffmpeg.wasm поддерживает все фильтры и кодеки FFmpeg? О: Нет. В официальном ядре часть кодеков убрана, а функции с системными зависимостями не включены. Проверяйте нужный кодек в сборке.

В: Файл действительно не попадает на сервер? О: Да. Основная обработка идёт в песочнице браузера; сервер отдаёт только статические HTML, JS и wasm.

В: Работает ли Safari? О: В целом да, но у старых версий были проблемы с комбинацией Worker и OffscreenCanvas. Многопоточное ядро лучше проверять на реальных устройствах.

RELATED TOOLS · Примените это на практике