Есть ли такая возможность обрабатывать аудио и видео на фронтенде? Например, если пользователь выбирает видео, а затем поддерживает его, чтобы установить любой кадр видео в качестве обложки, нет необходимости загружать все видео на сервер для обработки. После некоторого изучения автором эта функция в основном реализована, полная демонстрация:Функция захвата видеокадра ffmpeg wasm:
Поддержка mp4/mov/mkv/avi и других файлов. Основная идея такова:
Используйте ввод файла, чтобы позволить пользователю выбрать видеофайл, затем прочитать его как ArrayBuffer и передать в ffmpeg.wasm для обработки.После обработки выходные данные rgb рисуются на холсте или преобразуются в base64 в качестве атрибута src тега img для формирования изображения. (Canvas может напрямую использовать видеодом в качестве объекта drawImage для получения видеокадра, но формат видео, которое можно воспроизвести, относительно невелик, в этой статье основное внимание уделяется реализации схемы ffmpeg, поскольку ffmpeg может делать и другие вещи, это это просто пример)
Вот вопрос, а зачем использовать ffmpeg вместо того, чтобы писать его прямо на JS? Поскольку библиотека C для обработки мультимедиа относительно зрелая, ffmpeg является одной из них, и она все еще с открытым исходным кодом, и wasm может просто преобразовать ее в формат и использовать на веб-страницах.Есть относительно немного библиотек JS, связанных с обработкой мультимедиа. .demux) и сложность декодирования видео можно себе представить, прямое кодирование и декодирование JS также будет трудоемким. Поэтому сначала используйте то, что уже доступно.
Первым шагом является компиляция (если вас не интересует процесс компиляции, вы можете сразу перейти к шагу 2)
1. Скомпилируйте ffmpeg в версию wasm
Сначала я думал, что это будет очень сложно, но потом я узнал, что это не так уж и сложно, потому чтоvideoconverter.jsОн был перенесен (использует ffmpeg для реализации транскодирования аудио и видео на веб-страницах), главное отключить некоторые бесполезные функции во время настройки, иначе при компиляции будет сообщено о синтаксической ошибке. используется здесьemsdkВключите wasm, метод установки emsdk находится в егоРуководство по установкеЭто было сделано очень ясно, в основном с использованием системы оценки сценариев для загрузки различных скомпилированных файлов. После загрузки будет несколько исполняемых файлов, в том числе emcc, emc++, emar и другие команды, emcc — компилятор C, emc++ — компилятор C++, а emar используется для упаковки разных файлов библиотеки .o в один файл .a.
первый вffmpegОфициальный сайт для загрузки исходного кода.
(1) настроить
Разархивируйте в каталог и выполните следующие команды:
emconfigure ./configure --cc="emcc" --enable-cross-compile --target-os=none --arch=x86_32 --cpu=generic \
--disable-ffplay --disable-ffprobe --disable-asm --disable-doc --disable-devices --disable-pthreads --disable-w32threads --disable-network \
--disable-hwaccels --disable-parsers --disable-bsfs --disable-debug --disable-protocols --disable-indevs --disable-outdevs --enable-protocol=file
Обычно роль configure заключается в создании Makefile — фаза configure подтверждает некоторые параметры и среду компиляции, а затем генерирует команды компиляции и помещает их в Makefile.
Основная функция предыдущего emconfigure — указать компилятор как emcc, но этого недостаточно, т.к. в ffmpeg есть некоторые подмодули, и полностью указать все компиляторы как emcc нельзя, благо в конфиге ffmpeg можно указать пользовательский компилятор с помощью параметра --cc На Mac компилятор C обычно использует /usr/bin/clang, который здесь указан как emcc.
Последнее отключение предназначено для отключения некоторых функций, которые не поддерживают wasm. Например, --disable-asm отключает часть, использующую ассемблерный код, потому что этот синтаксис сборки emcc несовместим. Если он не отключен, компиляция сообщит об ошибке синтаксическая ошибка. Другая --disable-hwaccels отключает жесткое декодирование.Некоторые видеокарты поддерживают прямое декодирование без декодирования приложения (мягкое декодирование).Производительность жесткого декодирования явно выше, чем у мягкого декодирования.После отключения это приведет к последующему использованию , При сообщении о предупреждении:
[swscaler @ 0x105c480] No accelerated colorspace conversion found from yuv420p to rgb24.
Но это не влияет на использование.
(Процесс выполнения configure сообщит об ошибке сегмента, но в последующем процессе она не будет иметь никакого эффекта.)
После выполнения команды configure будут сгенерированы Makefile и некоторые связанные с ним файлы конфигурации.
(2) сделать
make — это этап для начала компиляции, выполните следующую команду для компиляции:
emmake make
Выполните на Mac, вы обнаружите, что будет сообщено об ошибке, когда вы, наконец, соберете несколько файлов .o в файлы .a:
AR libavdevice/libavdevice.a
fatal error: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ar: fatal error in /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ranlib
Чтобы решить эту проблему, вам нужно изменить команду упаковки с ar на emar, а затем удалить процесс ranlib.Измените файл ffbuild/config.mak:
# 修改ar为emar
- AR=ar
+ AR=emar
# 去掉ranlib
- RANLIB=ranlib
+ #RANLIB=ranlib
А потом заново сделать на нем.
После завершения компиляции в каталоге ffmpeg будет сгенерирован общий файл ffmpeg, а в каталоге libavcodec ffmpeg будут созданы такие файлы, как libavcodec.a. Эти файлы являются файлами битового кода, которые мы будем использовать позже. Биткод является промежуточным код скомпилированной программы.
(Наконец, зависнет при выполнении команды strip -o ffmpeg ffmpeg_g, но это не беда, просто меняем полосу на cp ffmpeg_g ffmpeg)
2. Использование ffmpeg
ffmpeg в основном состоит из нескольких каталогов lib:
- libavcodec: предоставляет функции кодека
- libavformat: демультиплексирование (demux) и мультиплексирование (mux)
- libswscale: масштабирование изображения и преобразование формата пикселей
Возьмем в качестве примера файл mp4. mp4 – это контейнерный формат. Во-первых, используйте API-интерфейс libavformat для демультиплексирования mp4, чтобы получить такую информацию, как место хранения аудио и видео в этом файле. Видео обычно кодируется с использованием h264 и т. д. Поэтому необходимо использовать libavcodec для декодирования формата yuv изображения и, наконец, преобразовать его в формат rgb с помощью libswscale.
Существует два способа использования ffmpeg: первый — напрямую скомпилировать файл ffmpeg, полученный на первом шаге, в wasm:
# 需要拷贝一个.bc后缀,因为emcc是根据后缀区分文件格式的
cp ffmpeg_g ffmpeg.bc
emcc ffmpeg.bc -o ffmpeg.html
Затем будут сгенерированы ffmpeg.js и ffpmeg.wasm.ffmpeg.js используется для загрузки и компиляции файла wasm и предоставления глобального объекта модуля для управления функциями API ffmpeg в wasm. При этом API ffmpeg вызывается через модуль в JS.
Но я чувствую, что этот метод более хлопотный, есть много различий между типами данных JS и C. В JS часто корректируется API C, и более проблематично передавать данные туда и обратно, потому что это занимает много настроек для реализации функции перехвата.API для ffmpeg.
Поэтому я использую второй метод: сначала пишу код на C, реализую функцию на C и, наконец, предоставляю интерфейс для использования JS, чтобы JS и WASM могли взаимодействовать только через API интерфейса.Первый способ вызывается так же часто. .
Таким образом, проблема превращается в два шага:
Первым шагом является использование языка C для написания функции для ffmpeg для сохранения изображений видеокадров.
Второй шаг — скомпилировать wasm и js для взаимодействия с данными.
Реализация первого шага в основном относится к учебнику по ffmpeg:ffmpeg tutorial. Код внутри уже готов и просто скопируйте его напрямую.Есть некоторые небольшие проблемы, что версия ffmpeg, которую он использует, немного устарела, и некоторые параметры API необходимо изменить. Код загружен на гитхаб, видно:cfile/simple.c.
Метод использования был представлен в файле readme и скомпилирован в исполняемый файл с помощью следующей команды:
gcc simple.c -lavutil -lavformat -lavcodec `pkg-config --libs --cflags libavutil` `pkg-config --libs --cflags libavformat` `pkg-config --libs --cflags libavcodec` `pkg-config --libs --cflags libswscale` -o simple
Затем, когда вы используете его, вы можете загрузить местоположение видеофайла:
./simple mountain.mp4
Картинка в формате pcm будет сгенерирована в текущем каталоге.
Этот simple.c — это API, который ffmpeg вызывает для автоматического чтения файлов жесткого диска.Его нужно изменить, чтобы прочитать содержимое файла из памяти, то есть мы читаем буфер в памяти, а затем передаем его в ffmpeg, и тогда мы можем меняем передачу данных в буфер из JS.Get, реализация этого видна:simple-from-memory.c.Конкретный код C здесь анализироваться не будет.Он предназначен для настройки API.Это относительно просто.Необходимо знать, как его использовать.Документов по разработке по ffmpeg в сети относительно мало.
Таким образом, первый шаг завершен, а затем вторым шагом является изменение ввода данных, которые должны быть получены из JS, и вывода, которые должны быть возвращены в JS.
3. Взаимодействие между js и wasm
Конкретная реализация версии wasm находится вweb.c(Также есть proccess.c, который разбирает некоторые функции simple.c), в web.c есть функция, которая открыта для вызовов JS, назовем ее setFile, этот setFile вызывается для JS:
EMSCRIPTEN_KEEPALIVE // 这个宏表示这个函数要作为导出的函数
ImageData *setFile(uint8_t *buff, const int buffLength, int timestamp) {
// process ...
return result;
}
Необходимо передать три параметра:
- buff: оригинальные видеоданные (передаются через JS ArrayBuffer)
- buffLength: общий размер видеобаффа (в байтах).
- временная метка: это видеокадр первых нескольких секунд, которые вы хотите захватить
Наконец, обрабатывается структура данных, возвращающая ImageData:
typedef struct {
uint32_t width;
uint32_t height;
uint8_t *data;
} ImageData;
В нем три поля: ширина и высота картинки и данные rgb.
Скомпилируйте после написания этих файлов C:
emcc web.c process.c ../lib/libavformat.bc ../lib/libavcodec.bc ../lib/libswscale.bc ../lib/libswresample.bc ../lib/libavutil.bc \
-Os -s WASM=1 -o index.html -s EXTRA_EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]' -s ALLOW_MEMORY_GROWTH=1 -s TOTAL_MEMORY=16777216
Используйте libavcode.bc и другие файлы, сгенерированные при компиляции на шаге 1. Эти файлы имеют порядок зависимостей и не могут быть изменены, зависимые должны быть размещены позже. Вот некоторые параметры для объяснения:
-o index.html
Указывает, что файл hmtl экспортируется, и он будет экспортирован одновременно.index.jsа такжеindex.wasm, в основном используя эти два, сгенерированный index.html бесполезен;
-s EXTRA_EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]
Указывает, что необходимо экспортировать две функции, ccall и cwrap.Функции этих двух функций должны вызывать функцию setFile, написанную на C выше;
-s TOTAL_MEMORY=16777216
Указывает, что общий размер памяти wasm составляет около 16 МБ, что также является значением по умолчанию, которое должно быть кратно 64;
-s ALLOW_MEMORY_GROWTH=1
Автоматическое расширение, когда память превышает общий размер.
После компиляции напишите main.html, добавьте элементы управления, такие как input[type=file], и импортируйте сгенерированный выше index.js. Он загрузит index.wasm и предоставит глобальный объект модуля для управления wasm API, включая приведенные выше. Укажите экспортируемую функцию во время компиляции, как показано в следующем коде:
<!DOCType html>
<html>
<head>
<meta charset="utf-8">
<title>ffmpeg wasm截取视频帧功能</title>
</head>
<body>
<form>
<p>请选择一个视频(本地操作不会上传)</p>
<input type="file" required name="file">
<label>时间(秒)</label><input type="number" step="1" value="0" required name="time">
<input type="submit" value="获取图像" style="font-size:16px;">
</form>
<!--这个canvas用来画导出的图像-->
<canvas width="600" height="400" id="canvas"></canvas>
<!--引入index.js-->
<script src="index.js"></script>
<script>
<script>
!function() {
let setFile = null;
// WASM下载并解析完毕
Module.onRuntimeInitialized = function () {
console.log('WASM initialized done!');
// 导出的核心处理函数
setFile = Module.cwrap('setFile', 'number',
['number', 'number', 'number']);
};
}();
</script>
Операция должна быть запущена после того, как wasm загружен и проанализирован, он обеспечивает обратный вызов onRuntimeInitialized.
Чтобы иметь возможность использовать функцию, экспортированную в файл C, вы можете использовать Module.cwrap, первый параметр — это имя функции, а второй параметр — тип возвращаемого значения.Поскольку возвращаемым значением является адрес указателя, вот 32-битное число, поэтому используйте числовой тип js, третий параметр — тип параметра.
Затем прочитайте содержимое входного файла и поместите его в буфер:
let form = document.querySelector('form');
// 监听onchange事件
form.file.onchange = function () {
if (!setFile) {
console.warn('WASM未加载解析完毕,请稍候');
return;
}
let fileReader = new FileReader();
fileReader.onload = function () {
// 得到文件的原始二进制数据ArrayBuffer
// 并放在buffer的Unit8Array里面
let buffer = new Uint8Array(this.result);
// ...
};
// 读取文件
fileReader.readAsArrayBuffer(form.file.files[0]);
};
Буфер, полученный при чтении, помещается в массив Uint8Array, который представляет собой массив, каждый элемент которого имеет тип unit8, то есть беззнаковое 8-битное целое, которое представляет собой число размером 0101 в одном байте.
Следующий ключевой вопрос: как передать этот буфер в функцию wasm setFile? Это требует понимания модели кучи памяти wasm.
4. модель кучи памяти wasm
Общий объем памяти, используемый wasm, указан во время компиляции выше, содержимое памяти можно просмотреть через Module.buffer и Module.HEAP8:
Эта штука является ключом к взаимодействию данных между JS и WASM.Поместите данные в этот массив HEAP8 в JS, а затем сообщите WASM, где адрес указателя данных и занимаемый объем памяти, то есть индекс и занимаемая длина этого массива HEAP8. В свою очередь, когда WASM хочет вернуть данные в JS, он также помещается в этот HEA8, а затем возвращает адрес указателя и длину.
Но мы не можем просто указать местоположение, нам нужно использовать предоставляемый им API для выделения и расширения. В JS подайте заявку на память через Module._molloc или Module.dynamicMalloc, как показано в следующем коде:
// 得到文件的原始二进制数据,放在buffer里面
let buffer = new Uint8Array(this.result);
// 在HEAP里面申请一块指定大小的内存空间
// 返回起始指针地址
let offset = Module._malloc(buffer.length);
// 填充数据
Module.HEAP8.set(buffer, offset);
// 最后调WASM的函数
let ptr = setFile(offset, buffer.length, +form.time.value * 1000);
Вызовите malloc, передайте требуемый размер области памяти, а затем верните смещение начального адреса выделенной памяти, которое на самом деле является индексом в массиве HEAP8, а затем вызовите метод set Uint8Array для заполнения данных. Затем передайте адрес указателя этого смещения в setFile и сообщите размер памяти. Таким образом JS передает данные в WASM.
После вызова setFile возвращаемое значение представляет собой адрес указателя, указывающий на структуру данных struct:
typedef struct {
uint32_t width;
uint32_t height;
uint8_t *data;
} ImageData;
Его первые 4 байта используются для указания ширины, следующие 4 байта - высота, последний - указатель rgb данных картинки, размер указателя тоже 4 байта, это опускает длину данных, т.к. можно получить по ширине * высоте * 3.
Таким образом, содержимое, хранящееся в [ptr, ptr + 4), — это ширина, содержимое, хранящееся в [ptr + 4, ptr + 8), — это длина, а содержимое, хранящееся в [ptr + 8, ptr + 12), — это указатель. к данным изображения показан следующий код:
let ptr = setFile(offset, buffer.length, +form.time.value * 1000);
let width = Module.HEAPU32[ptr / 4]
height = Module.HEAPU32[ptr / 4 + 1],
imgBufferPtr = Module.HEAPU32[ptr / 4 + 2],
imageBuffer = Module.HEAPU8.subarray(imgBufferPtr,
imgBufferPtr + width * height * 3);
HEAPU32 похож на HEAP8 выше, за исключением того, что он считывает число для каждого 32-битного числа. Поскольку мы все выше 32-битные числа, это правильно использовать это. Это единица из 4 байтов, а ptr - это байт. является единицей, поэтому ptr / 4 получает index. Не беспокойтесь о том, что число не делится на 4, потому что оно 64-битное.
Таким образом, мы получаем содержимое данных rgb изображения, а затем рисуем его с помощью холста.
5. Рисование изображений на холсте
Используйте класс ImageData Canvas, как показано в следующем коде:
function drawImage(width, height, buffer) {
let imageData = ctx.createImageData(width, height);
let k = 0;
// 把buffer内存放到ImageData
for (let i = 0; i < buffer.length; i++) {
// 注意buffer数据是rgb的,而ImageData是rgba的
if (i && i % 3 === 0) {
imageData.data[k++] = 255;
}
imageData.data[k++] = buffer[i];
}
imageData.data[k] = 255;
memCanvas.width = width;
memCanvas.height = height;
canvas.height = canvas.width * height / width;
memContext.putImageData(imageData, 0, 0, 0, 0, width, height);
ctx.drawImage(memCanvas, 0, 0, width, height, 0, 0, canvas.width, canvas.height);
}
drawImage(width, height, imageBuffer);
Это в принципе сделано, но осталось сделать еще очень важную вещь, а именно освободить прикладную память.Иначе после повторных операций несколько раз память веб-страницы взлетит до одного-двух G, а потом выкинет исключение недостаточной памяти. , поэтому после drawImage освобождается выделенная память:
drawImage(width, height, imageBuffer);
// 释放内存
Module._free(offset);
Module._free(ptr);
Module._free(imgBufferPtr);
Код, написанный на C, также должен освобождать память, запрошенную промежуточным процессом, иначе эта утечка памяти будет довольно мощной. Если free указан правильно, адрес malloc каждый раз равен 16358200. Если свободного места нет, емкость будет каждый раз повторно расширяться, и будет возвращен увеличенный адрес смещения.
Но общее потребление памяти этой штукой по-прежнему относительно велико.
6. Проблемы
После инициализации ffmpeg память, используемая веб-страницей, увеличится до 500 МБ, а если выбран файл размером 300 МБ, то объем памяти увеличится до 1,3 ГБ, потому что при вызове setFile вам нужно выделить 300 МБ памяти, а затем установить setFile в Код C. В процессе выполнения контекстная переменная размером 300 МБ будет распределена, потому что, если вы хотите обработать формат mov/m4v, чтобы получить информацию о moov, она должна быть очень большой и не оптимизирована для В сумме они составляют более 1 Гб, а WebAssembly.Memory может только расти, сжиматься нельзя, то есть можно только расширять, но не сжимать, а расширенная память всегда будет рядом. Для обычных файлов mp4 контекстной переменной требуется всего 1 МБ, что позволяет управлять памятью в пределах 1 ГБ.
Вторая проблема заключается в том, что созданный wasm-файл имеет относительно большой размер: 12,6 МБ изначально и 5 МБ после gzip, как показано на следующем рисунке:
Поскольку ffmpeg сам по себе относительно большой, если вы можете подробно изучить исходный код, а затем отключить некоторые бесполезные функции или не включать их, вы сможете уменьшить его или извлечь только полезный код, что может быть немного сложнее. .
Третьей проблемой является надежность кода.Помимо попыток уменьшить объем памяти, вам также необходимо учитывать некоторые проблемы с доступом к памяти за ее пределами, потому что иногда во время работы возникает это исключение:
Uncaught RuntimeError: memory access out of bounds
Хотя есть некоторые проблемы, по крайней мере, он работает, и в настоящее время он может не иметь значения для развертывания производственной среды, и его можно постепенно оптимизировать позже.
В дополнение к примеру в этой статье вы также можете использовать ffmpeg для достижения некоторых других функций, чтобы веб-страницы также могли напрямую обрабатывать мультимедиа. По сути, пока ffmpeg может это делать, он может работать на веб-страницах, а производительность wasm выше, чем у прямого запуска JS.