Compilando o FFmpeg no WebAssembly (= ffmpeg.js): Parte 3 - Convertendo avi para mp4



Lista de partes traduzidas da série:


  1. Cozinhar
  2. Compilando com o Emscripten
  3. Converter avi para mp4 (você está aqui)



Nesta parte, analisaremos:



  1. Compilação da biblioteca FFmpeg com argumentos otimizados.
  2. Gerenciamento do sistema de arquivos Emscripten.
  3. 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.


  1. -Oz : otimize o código e reduza seu tamanho (de 30 para 15 MB)
  2. -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)
  3. -s MODULARIZE = 1 : cria uma biblioteca em vez de um utilitário de linha de comando (você precisará modificar as fontes, detalhes abaixo)
  4. -s EXPORTED_FUNCTIONS = "[_ ffmpeg]" : exporta a função ffmpeg C para o mundo JavaScript
  5. -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 .
  6. -s ALLOW_MEMORY_GROWTH = 1 : remova o limite da memória consumida
  7. -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:


  1. Como criar um ponteiro para uma matriz de caracteres?
  2. 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:


Source: https://habr.com/ru/post/pt473166/


All Articles