Список переведённых частей серии:
В этой части ма разберём:
- Компиляцию библиотеки FFmpeg с оптимизированными аргументами.
- Управление файловой системой Emscripten.
- Разработку ffmpeg.js v0.1.0 и конвертацию видео.
Компиляция библиотеки FFmpeg с оптимизированными аргументами
Хотя конечной целью этой части и является создание ffmpeg.js v0.1.0 для конвертации avi в mp4, но в предыдущей части мы создали лишь «голую» версию FFmpeg, которую хорошо бы оптимизировать несколькими параметрами.
- -Oz: оптимизируем код и уменьшаем его размер (с 30 до 15 Мб)
- -o javascript/ffmpeg-core.js: сохраняем js и wasm файлов в каталог javascript. (откуда мы будем вызывать ffmpeg-core.js из библиотеки-обёртки ffmpeg.js, предоставляющей красивый API)
- -s MODULARIZE=1: создаём библиотеку вместо утилиты командной строки (понадобится модификация исходников, детали ниже)
- -s EXPORTED_FUNCTIONS="[_ffmpeg]": экспортируем C-функцию «ffmpeg»в мир JavaScript
- -s EXTRA_EXPORTED_RUNTIME_METHODS="[cwrap, FS, getValue, setValue]": дополнительные функции для работы с файловой системой и указателями, детали можно почитать в статье Interacting with code.
- -s ALLOW_MEMORY_GROWTH=1: убираем ограничение на потребляемую память
- -lpthread: убран, так как мы планируем создать наш собственный воркер. (это задел для четвёртой части публикаций)
Больше деталей о каждом из аргументов можно почитать в src/settings.js в github репозитории emscripten.
При добавлении -s MODULARIZE=1 нам понадобится модифицировать исходный код для соответствия требованиям модульности (фактически, избавиться от функции main()). Придётся поменять всего три строчки.
1. fftools/ffmpeg.c: переименуйте main в ffmpeg
- int main(int argc, char **argv)
+ int ffmpeg(int argc, char **argv)
2. fftools/ffmpeg.h: добавьте ffmpeg в конец файла для экспорта функции
+ int ffmpeg(int argc, char** argv);
#endif /* FFTOOLS_FFMPEG_H */
3. fftools/cmdutils.c: закомментируйте exit(ret), чтобы наша библиотека не выходила из рантайма за нас (мы улучшим этот момент позже).
void exit_program(int ret){
if (program_exit)
program_exit(ret);
- exit(ret);
+ // exit(ret);
}
Наша новая версия скрипта для компиляции:
emcc \
-Llibavcodec -Llibavdevice -Llibavfilter -Llibavformat -Llibavresample -Llibavutil -Llibpostproc -Llibswscale -Llibswresample \
-Qunused-arguments -Oz \
-o javascript/ffmpeg-core.js fftools/ffmpeg_opt.o fftools/ffmpeg_filter.o fftools/ffmpeg_hw.o fftools/cmdutils.o fftools/ffmpeg.o \
-lavdevice -lavfilter -lavformat -lavcodec -lswresample -lswscale -lavutil -lm \
-s MODULARIZE=1 \
-s EXPORTED_FUNCTIONS="[_ffmpeg]" \
-s EXTRA_EXPORTED_RUNTIME_METHODS="[cwrap, FS, getValue, setValue]" \
-s TOTAL_MEMORY=33554432 \
-s ALLOW_MEMORY_GROWTH=1
ffmpeg-core.js готов!
Если у вас есть опыт работы с ffmpeg, вы уже знаете как выглядит типичная команда:
$ ffmpeg -i input.avi output.mp4
А так как мы используем функцию ffmpeg вместо main, то вызов команды будет выглядеть так:
const args = ['./ffmpeg', '-i', 'input.avi', 'output.mp4'];
ffmpeg(args.length, args);
Конечно не всё так просто, нам понадобится построить мост между мирами JavaScript и C, так что начнём с файловой системы emscripten.
Управление файловой системой Emscripten
В emscripten есть виртуальная файловая система для поддержки чтения/записи из C, которую ffmpeg-core.js использует для работы с видео файлами.
Подробнее об этом читайте в File System API.
Для того чтобы всё работало, мы экспортируем FS API из emscripten, что происходит благодаря параметру выше:
-s EXTRA_EXPORTED_RUNTIME_METHODS="[cwrap, FS, getValue, setValue]"
Чтобы сохранить файл, необходимо подготовить массив в формате Uint8Array в окружении Node.js, что можно сделать примерно так:
const fs = require('fs');
const data = new Uint8Array(fs.readFileSync('./input.avi'));
И сохранить его в файловую систему emscripten с помощью FS.writeFile():
require('./ffmpeg-core.js)()
.then(Module => {
Module.FS.writeFile('input.avi', data);
});
А для загрузки файла из emscripten:
require('./ffmpeg-core.js)()
.then(Module => {
const data = Module.FS.readFile('output.mp4');
});
Приступим к разработке ffmpeg.js, чтобы спрятать эти сложности за красивым API.
Разработка ffmpeg.js v0.1.0 и конвертация видео
Разработка ffmpeg.js нетривиальна, так как постоянно нужно переключаться между мирами JavaScript и C, но если вы знакомы с указателями, вам будет гораздо легче понять, что здесь происходит.
Наша задача разработать ffmpeg.js таким:
const fs = require('fs');
const ffmpeg = require('@ffmpeg/ffmpeg');
(async () => {
await ffmpeg.load();
const data = ffmpeg.transcode('./input.avi', 'mp4');
fs.writeFileSync('./output.mp4', data);
})();
Для начала загрузим ffmpeg-core.js, что делается по традиции асинхронно, чтобы не блокировать главный тред.
Вот как это выглядит:
const { setModule } = require('./util/module');
const FFmpegCore = require('./ffmpeg-core');
module.exports = () => (
new Promise((resolve, reject) => {
FFmpegCore()
.then((Module) => {
setModule(Module);
resolve();
});
})
);
Может показаться странным, что мы оборачиваем один промис в другой, это происходит потому что FFmpegCore() не настоящий промис, а всего лишь функция симулирующая API промиса.
Следующим шагом используем Module для получения функции ffmpeg с помощью функции cwrap :
// int ffmpeg(int argc, char **argv)
const ffmpeg = Module.cwrap('ffmpeg', 'number', ['number', 'number']);
Первый аргумент cwrap — имя функции (которая должна быть в EXPORTED_FUNCTIONS с предшествующим подчеркиванием), второй — тип возвращаемого значения, третий — тип аргументов функции (int argc и char **argv).
Понятно почему argc число, но почему argv тоже число? argv — указатель, а указатель хранит адрес в памяти (типа 0xfffffff), так что тип указателя 32 битное беззнаковое в WebAssembly. Вот почему мы указываем число как тип argv.
Для вызова ffmpeg() первый аргумент будет обычным числом в JavaScript, но вот второй аргумент должен быть указателем на массив символов (Uint8 в JavaScript).
Разобьём эту задачу на 2 подзадачи:
- Как создать указатель на массив символов?
- Как создать указатель на массив указателей?
Первую проблему мы решим, создав утилиту str2ptr:
const { getModule } = require('./module');
module.exports = (s) => {
const Module = getModule();
const ptr = Module._malloc((s.length+1)*Uint8Array.BYTES_PER_ELEMENT);
for (let i = 0; i < s.length; i++) {
Module.setValue(ptr+i, s.charCodeAt(i), 'i8');
}
Module.setValue(ptr+s.length, 0, 'i8');
return ptr;
};
Module._malloc() похож на malloc() в C, он выделяет порцию памяти в куче. Module.setValue() устанавливает конкретное значение по указателю.
Не забывайте добавлять 0 в конец массива символов, чтобы избежать непредвиденных ситуаций.
Разобравшись с первой подзадачей, создадим strList2ptr для решения второй:
const { getModule } = require('./module');
const str2ptr = require('./str2ptr');
module.exports = (strList) => {
const Module = getModule();
const listPtr = Module._malloc(strList.length*Uint32Array.BYTES_PER_ELEMENT);
strList.forEach((s, idx) => {
const strPtr = str2ptr(s);
Module.setValue(listPtr + (4*idx), strPtr, 'i32');
});
return listPtr;
};
Главное что тут надо понять, это то что указатель — Uint32 значение внутри JavaScript, поэтому listPtr это указатель на массив Uint32, хранящий указатели на массив Uint8.
Собрав всё это вместе получим такую реализацию ffmepg.transcode():
const fs = require('fs');
const { getModule } = require('./util/module');
const strList2ptr = require('./util/strList2ptr');
module.exports = (inputPath, outputExt) => {
const Module = getModule();
const data = new Uint8Array(fs.readFileSync(inputPath));
const ffmpeg = Module.cwrap('ffmpeg', 'number', ['number', 'number']);
const args = ['./ffmpeg', '-i', 'input.avi', `output.${outputExt}`];
Module.FS.writeFile('input.avi', data);
ffmpeg(args.length, strList2ptr(args));
return Buffer.from(Module.FS.readFile(`output.${outputExt}`));
};
Готово! Теперь у нас есть ffmpeg.js v0.1.0 для конвертации avi в mp4.
Вы можете испытать результат самостоятельно, установив библиотеку:
$ npm install @ffmpeg/ffmpeg@0.1.0
И конвертировав файл так:
const fs = require('fs');
const ffmpeg = require('@ffmpeg/ffmpeg');
(async () => {
await ffmpeg.load();
const data = ffmpeg.transcode('./input.avi', 'mp4');
fs.writeFileSync('./output.mp4', data);
})();
Только учтите что пока что библиотека работает только для Node.js, но в следующей части мы добавим поддержку веб-воркера (и child_process в Node.js).
Исходные коды: