Lista de partes traduzidas da série:
- Cozinhar
- Compilando com o Emscripten
- Converter avi para mp4 (você está aqui)
Nesta parte, analisaremos:
- Compilação da biblioteca FFmpeg com argumentos otimizados.
- Gerenciamento do sistema de arquivos Emscripten.
- Desenvolvimento de ffmpeg.js v0.1.0 e conversão de vídeo.
Compilando a biblioteca FFmpeg com argumentos otimizados
Embora o objetivo final desta parte seja criar o ffmpeg.js v0.1.0 para converter avi em mp4, na parte anterior, criamos apenas uma versão “bare” do FFmpeg, o que seria bom otimizar com vários parâmetros.
- -Oz : otimize o código e reduza seu tamanho (de 30 para 15 MB)
- -o javascript / ffmpeg-core.js : salve os arquivos js e wasm no diretório javascript. (de onde chamaremos ffmpeg-core.js da biblioteca de wrapper ffmpeg.js, que fornece uma boa API)
- -s MODULARIZE = 1 : cria uma biblioteca em vez de um utilitário de linha de comando (você precisará modificar as fontes, detalhes abaixo)
- -s EXPORTED_FUNCTIONS = "[_ ffmpeg]" : exporta a função ffmpeg C para o mundo JavaScript
- -s EXTRA_EXPORTED_RUNTIME_METHODS = "[cwrap, FS, getValue, setValue]" : funções adicionais para trabalhar com o sistema de arquivos e ponteiros, detalhes podem ser encontrados no artigo Interagindo com o código .
- -s ALLOW_MEMORY_GROWTH = 1 : remova o limite da memória consumida
- -lpthread : removido, pois planejamos criar nosso próprio trabalhador. (este é um atraso para a quarta parte das publicações)
Mais detalhes sobre cada um dos argumentos podem ser encontrados em src / settings.js no repositório do github emscripten.
Ao adicionar -s MODULARIZE = 1, precisaremos modificar o código fonte para atender aos requisitos de modularidade (de fato, livre-se da função main ()). Você precisa alterar apenas três linhas.
1. fftools / ffmpeg.c : renomeie main para ffmpeg
- int main(int argc, char **argv) + int ffmpeg(int argc, char **argv)
2. fftools / ffmpeg.h : adicione ffmpeg ao final do arquivo para exportar a função
+ int ffmpeg(int argc, char** argv); #endif /* FFTOOLS_FFMPEG_H */
3. fftools / cmdutils.c : comente exit (ret) para que nossa biblioteca não saia do tempo de execução para nós (iremos melhorar esse ponto posteriormente).
void exit_program(int ret){ if (program_exit) program_exit(ret); - exit(ret); + // exit(ret); }
Nossa nova versão do script de compilação:
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
O ffmpeg-core.js está pronto!
Se você tem experiência com o ffmpeg, já sabe como é um comando típico:
$ ffmpeg -i input.avi output.mp4
E como usamos a função ffmpeg em vez de main, a chamada de comando será assim:
const args = ['./ffmpeg', '-i', 'input.avi', 'output.mp4']; ffmpeg(args.length, args);
Obviamente, nem tudo é tão simples, precisaremos construir uma ponte entre os mundos do JavaScript e C, então vamos começar com o sistema de arquivos emscripten.
Gerenciamento do sistema de arquivos Emscripten
O Emscripten possui um sistema de arquivos virtual para suportar a leitura / gravação de C, que o ffmpeg-core.js usa para trabalhar com arquivos de vídeo.
Leia mais sobre isso na API do sistema de arquivos .
Para que tudo funcione, exportamos a API FS do emscripten, devido ao parâmetro acima:
-s EXTRA_EXPORTED_RUNTIME_METHODS="[cwrap, FS, getValue, setValue]"
Para salvar o arquivo, você precisa preparar a matriz no formato Uint8Array no ambiente do Node.js, o que pode ser feito da seguinte maneira:
const fs = require('fs'); const data = new Uint8Array(fs.readFileSync('./input.avi'));
E salve-o no sistema de arquivos emscripten usando FS.writeFile ():
require('./ffmpeg-core.js)() .then(Module => { Module.FS.writeFile('input.avi', data); });
E para baixar o arquivo do emscripten:
require('./ffmpeg-core.js)() .then(Module => { const data = Module.FS.readFile('output.mp4'); });
Vamos começar a desenvolver o ffmpeg.js para ocultar essas complexidades por trás de uma bela API.
Desenvolvimento ffmpeg.js v0.1.0 e conversão de vídeo
O desenvolvimento do ffmpeg.js não é trivial, pois você constantemente precisa alternar entre os mundos do JavaScript e C, mas se você estiver familiarizado com os ponteiros , será muito mais fácil entender o que está acontecendo aqui.
Nossa tarefa é desenvolver o ffmpeg.js assim:
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); })();
Primeiro, baixe o ffmpeg-core.js, que é tradicionalmente feito de forma assíncrona para não bloquear o encadeamento principal.
Aqui está o que parece:
const { setModule } = require('./util/module'); const FFmpegCore = require('./ffmpeg-core'); module.exports = () => ( new Promise((resolve, reject) => { FFmpegCore() .then((Module) => { setModule(Module); resolve(); }); }) );
Pode parecer estranho cumprirmos uma promessa em outra, porque FFmpegCore () não é uma promessa real, mas apenas uma função que simula a API da promessa.
O próximo passo é usar o Module para obter a função ffmpeg usando a função cwrap :
// int ffmpeg(int argc, char **argv) const ffmpeg = Module.cwrap('ffmpeg', 'number', ['number', 'number']);
O primeiro argumento para cwrap é o nome da função (que deve estar em EXPORTED_FUNCTIONS com o sublinhado anterior), o segundo é o tipo do valor de retorno, o terceiro é o tipo dos argumentos da função (int argc e char ** argv).
Está claro por que argc é um número, mas por que argv também é um número? argv é um ponteiro e o ponteiro armazena o endereço na memória (do tipo 0xfffffff); portanto, o tipo de ponteiro é de 32 bits sem assinatura no WebAssembly. É por isso que especificamos o número como o tipo argv .
Para chamar ffmpeg (), o primeiro argumento será um número regular em JavaScript, mas o segundo argumento deve ser um ponteiro para uma matriz de caracteres (Uint8 em JavaScript).
Dividimos esta tarefa em 2 subtarefas:
- Como criar um ponteiro para uma matriz de caracteres?
- Como criar um ponteiro para uma matriz de ponteiros?
Resolveremos o primeiro problema criando o utilitário 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 () é semelhante ao malloc () em C, ele aloca um pedaço de memória no heap. Module.setValue () define o valor específico por ponteiro.
Lembre-se de adicionar 0 ao final da matriz de caracteres para evitar situações imprevistas.
Depois de lidar com a primeira subtarefa, crie strList2ptr para resolver a segunda:
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; };
A principal coisa a entender aqui é que o ponteiro é um valor Uint32 dentro do JavaScript, portanto listPtr é um ponteiro para uma matriz Uint32 que armazena ponteiros para uma matriz Uint8.
Juntando tudo, obtemos a seguinte implementação de 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}`)); };
Feito! Agora temos o ffmpeg.js v0.1.0 para converter avi para mp4.
Você pode testar o resultado instalando a biblioteca:
$ npm install @ffmpeg/ffmpeg@0.1.0
E convertendo o arquivo assim:
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); })();
Lembre-se de que, até o momento, a biblioteca funciona apenas para o Node.js., mas na próxima parte, adicionaremos suporte para web-worker (e child_process no Node.js.).
Códigos de origem: