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'),
})
}
}
Три детали:
- Перед запасной загрузкой вызывайте
terminate(). После неудачногоload()worker может остаться в полуинициализированном состоянии. - Используйте
toBlobURL. Worker загружает скрипт через blob URL, и это обходит кросс-доменные ограничения. - Не смешивайте два вида прогресса.
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() после ошибки CDN | Worker в промежуточном состоянии | Сначала 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. Многопоточное ядро лучше проверять на реальных устройствах.