Как стать автором
Обновить

Компилируем FFmpeg в WebAssembly (=ffmpeg.js): Часть 3 — Конвертация avi в mp4

Время на прочтение5 мин
Количество просмотров4.7K
Автор оригинала: Jerome Wu



Список переведённых частей серии:


  1. Приготовления
  2. Компиляция с Emscripten
  3. Конвертация avi в mp4 (вы тут)



В этой части ма разберём:



  1. Компиляцию библиотеки FFmpeg с оптимизированными аргументами.
  2. Управление файловой системой Emscripten.
  3. Разработку ffmpeg.js v0.1.0 и конвертацию видео.



Компиляция библиотеки FFmpeg с оптимизированными аргументами


Хотя конечной целью этой части и является создание ffmpeg.js v0.1.0 для конвертации avi в mp4, но в предыдущей части мы создали лишь «голую» версию FFmpeg, которую хорошо бы оптимизировать несколькими параметрами.


  1. -Oz: оптимизируем код и уменьшаем его размер (с 30 до 15 Мб)
  2. -o javascript/ffmpeg-core.js: сохраняем js и wasm файлов в каталог javascript. (откуда мы будем вызывать ffmpeg-core.js из библиотеки-обёртки ffmpeg.js, предоставляющей красивый API)
  3. -s MODULARIZE=1: создаём библиотеку вместо утилиты командной строки (понадобится модификация исходников, детали ниже)
  4. -s EXPORTED_FUNCTIONS="[_ffmpeg]": экспортируем C-функцию «ffmpeg»в мир JavaScript
  5. -s EXTRA_EXPORTED_RUNTIME_METHODS="[cwrap, FS, getValue, setValue]": дополнительные функции для работы с файловой системой и указателями, детали можно почитать в статье Interacting with code.
  6. -s ALLOW_MEMORY_GROWTH=1: убираем ограничение на потребляемую память
  7. -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 подзадачи:


  1. Как создать указатель на массив символов?
  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).


Исходные коды:


Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 14: ↑13 и ↓1+12
Комментарии4

Публикации

Истории

Работа

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань