Nodejs - Extraindo Frames de Vídeos

Automatize a extração de frames de vídeos com Node.js e FFmpeg

Nodejs - Extraindo Frames de Vídeos

Olá meus Unicórnios! 🦄✨

No Tutorial anterior aprendemos como extrair o áudio de um vídeo.

Nodejs - Extraindo Áudio de Vídeos
Aprenda a configurar uma API que baixa vídeos e extrai áudio, gerando arquivos MP3 prontos para uso

Agora vamos além, iremos extrair frames de um vídeo, utilizando o FFmpeg.

Executando a Extração Diretamente na Linha de Comando

Inicialmente, instale o FFmpeg em seu Servidor:

Download FFmpeg

💡
Caso você esteja utilizando Windows, não esqueça de adicionar a pasta "bin" do FFmpeg ao PATH de seu computador, para que esteja disponível, no CDM, o "ffmpeg.exe"

O comando que iremos utilizar é o comando abaixo:

ffmpeg -i [VIDEO] -vf fps=1/[TEMPO_ENTRE_FRAMES] -t [TEMPO_DE_PROCESSAMENTO] [PASTA]/%d.jpg

Exemplo:

ffmpeg -i teste1.mp4 -vf fps=1/5 -t 50 C:\Testes\%d.jpg

Quando o comando é executado, o FFmpeg faz a extração e nos mostra os detalhes:

Porem, temos de analisar o comando que iremos realizar:

-fps

Neste campo, iremos definir o tempo entre cada frame.

No caso de "1/5" estamos dizendo:

Extraia uma imagem a cada 5 segundos

-t

Neste campo iremos indicar quando tempo do vídeo será processado.

Ou seja, se configuramos o "-fps" para "1/5" e o "-t" para "20", estamos dizendo:

Extraia uma imagem a cada 5 segundos limitando a 20 segundos do vídeo

Nesta configuração, serão gerado até 4 imagens

Criando nossa API com NodeJs

A função que iremos criar ira se chamar "executar", ira rodar na porta "8019", e ira esperar um POST de um Json:

const fs = require("fs");
const { v4: uuidv4 } = require("uuid");
const path = require('path');
const { promisify } = require('util');
const exec = promisify(require('child_process').exec);

const express = require("express");
const bodyParser = require("body-parser");
const axios = require('axios');
const { pipeline } = require('stream/promises');

var app = express();
app.use(bodyParser.json());

app.post("/executar", async function(req, res){

  //

});

app.listen(8019, function(){ 
    
    console.log("API Iniciada");

});

Observe que já inclui as dependências que iremos utilizar:

  • fs: Para manipular arquivos
  • uuid: Para gerar uma string aleatória que iremos utilizar no nome dos arquivos
  • path: Para criar os caminhos independente do sistema operacional
  • promisify: Para converter funções que retornam um callback em funções que retornam promessas (Sendo controladas pelo async/await)
  • exec: Para executar o "ffmpeg" que é uma aplicação instalada no servidor
  • express: Para criar a API em si
  • bodyParser: Para permitir configurar o Body das chamadas (Iremos utilizar para definir que esperamos receber um Json)
  • axios: Para as requisições (Iremos utilizar para carregar o arquivo de vídeo)
  • pipeline: Para escrevermos o Vídeo retornado em um arquivo físico

O Json que será enviado possui apenas um elemento "url":

{
  "url": "https://download.samplelib.com/mp4/sample-30s.mp4"
}

Antes de começarmos a aplicar qualquer ação, precisamos garantir que a Url foi informada e que o arquivo é um arquivo de extensão "mp4":

if(!req.body.url)
{

    return res.status(400).json({ Message: "Preencha o Campo 'URL'" });

}

if(!req.body.url.endsWith('.mp4')) 
{

    return res.status(400).json({ Message: "Preeencha a 'URL' com um arquivo MP4" });

}

Neste momento ainda não podemos validar que o arquivo é realmente um MP4, apenas podemos validar a extensão (Afinal, ainda não temos o arquivo em si apenas a Url dele).

Mas iremos fazer esta validação posteriormente, para garantir que o "ffmpeg" não falhe.

Antes de seguirmos para as partes mais interessantes, iremos criar uma constante com um nome aleatório, utilizando o "uuidv4" e já popular uma constante com o caminho da pasta onde ira ficar o Vídeo e as Imagens:

const InfFilename = uuidv4();
const InfTmpDir = path.join(__dirname, "tmp", InfFilename);
const InfArquivoVideo = path.join(InfTmpDir, "Video.mp4");

Isto ira garantir que todos os arquivos relacionados a um processamento fiquem na mesma pasta.

Observe que iremos utilizar a pasta "tmp", então iremos configurar o "express" para tornar público o conteúdo desta pasta:

app.use('/tmp', express.static(path.join(__dirname, 'tmp')));

Já temos o que precisamos, vamos fazer um Axios para carregar o Vídeo:

const response = await axios({
url: req.body.url,
method: 'GET',
responseType: 'stream'
});

Não podemos deixar de validar que tivemos um Sucesso

if(response.status !== 200) 
{

    return res.status(400).json({ Message: "Falha em Obter Arquivo da URL Informada: " + response.status });

}

E que o tipo de arquivo realmente é um MP4:

const contentType = response.headers['content-type'];

if(!contentType || !contentType.includes('video/mp4')) 
{

    return res.status(400).json({ Message: "A URL informada não contém um arquivo MP4" });

}

Pronto! Temos um arquivo retornado com sucesso.

Vamos salvar o conteúdo do arquivo, retornado pelo Axios, no arquivo MP4 que preparamos na constante anterior:

await pipeline(response.data, fs.createWriteStream(InfArquivoVideo));

Agora vamos para a parte legal, executar o "FFmpeg" para extrair os frames e salvar na pasta que criamos anteriormente:

await exec("ffmpeg -i " + InfArquivoVideo + " -vf fps=1/5 -t 50 " + InfTmpDir + "/%d.jpg");

Aqui temos várias abordagens para identificar a quantidade de imagens geradas, mas considero a mais simples utilizar o TEMPO.

Se estamos gerando uma imagem a cada 5 segundos (fps=1/5) e irmos processar até 50 segundos se vídeo (-t 50) então iremos ter até 10 imagens:

= 50 / 5 = 10

Então, vamos fazer um For, e verificar as imagens que foram geradas:

var InfImagens = [];

for( let i = 1; i <= 10; i++ )
{

    var InfImagem  = i + ".jpg";

    if( fs.existsSync(path.join(InfTmpDir, InfImagem)) )
    {

        InfImagens.push(InfImagem);

    }

}

E para identificar se a extração teve sucesso, vamos checar se ao menos uma imagem foi extraída:

if( InfImagem.length > 0 )
{

    // Sucesso

}else{

    // Falha

}

PRONTO! Temos uma API que ira nos retornar as Imagens:

GitHub - cmacetko/ExtrairFramesDeVideo
Contribute to cmacetko/ExtrairFramesDeVideo development by creating an account on GitHub.

Vamos Testar?

Iremos fazer um POST para a API "http://127.0.0.1:8019/executar" passando a Url do vídeo no Json:

{
  "url": "https://download.samplelib.com/mp4/sample-30s.mp4"
}

O retorno será um array "arquivos" com todas as imagens geradas:

{
  "arquivos": [
    "/tmp/d855ebaf-4957-42bc-bb45-05f98b80f97d/1.jpg",
    "/tmp/d855ebaf-4957-42bc-bb45-05f98b80f97d/2.jpg",
    "/tmp/d855ebaf-4957-42bc-bb45-05f98b80f97d/3.jpg",
    "/tmp/d855ebaf-4957-42bc-bb45-05f98b80f97d/4.jpg",
    "/tmp/d855ebaf-4957-42bc-bb45-05f98b80f97d/5.jpg",
    "/tmp/d855ebaf-4957-42bc-bb45-05f98b80f97d/6.jpg"
  ]
}

a

Depois disto, podemos carregar as imagens diretamente:

http://127.0.0.1:8019/tmp/d855ebaf-4957-42bc-bb45-05f98b80f97d/1.jpg

O que fazer com isto?

Utilizando a OpenAI podemos descrever a imagem:

ChatGPT — Descrevendo Imagens
Descubra como utilizar a API do ChatGPT para descrever imagens de forma precisa e detalhada, proporcionando uma experiência imersiva e informativa

Com estas descrições, podemos fazer perguntas ao ChatGPT:

ChatGPT - Questionando o contexto de uma conversa
Aprenda a usar o modelo GPT-4o da OpenAI para fazer perguntas sobre transcrições de reuniões e gerar respostas precisas com base em conversas.

E não apenas isto, podemos obter um Json, com elementos das descrições:

ChatGPT - Retornando um Json com elementos relacionados a conversa
Descubra como solicitar respostas no formato JSON com a OpenAI para extrair informações específicas de conversas

Por hoje é só, meus unicórnios! 🦄✨

Que a magia do arco-íris continue brilhando em suas vidas! Até mais! 🌈🌟