Convertendo vídeos para texto com ffmpeg e caca-utils

Yep… você leu certo: caca-utils. Não me pergunte o que caca significa. Só sei que os caras chamam de Colour ASCII Art Library. O detalhe interessante é que alguns players conseguem renderizar vídeos usando essa biblioteca. É o caso do VLC e também do ffmpeg.

Então, dá para criar um vídeo usando ASCII art? Well… dá, mas não é direto. O problema desses codecs é que eles renderizam o vídeo em formato texto e, assim, não tem como incorporar cada “quadro” num container MP4, Matroska, AVI ou o raio que o parta… Infelizmente, temos que renderizar os quadros em formato gráfico para poder convertê-los e incorporá-los nos citados containers. Eis uma maneira de fazer:

  • O primeiro passo é converter o vídeo em quadros individuais e de tamanho bem definido. Isso pode ser feito com o ffmpeg da seguinte maneira:
$ ffmpeg -i video.mp4 -s hd720 -r 30 -f image2 %07d.png

Isso ai vai criar um porrilhão de arquivos PNG nomeados como 0000001.png, 0000002.png … até o último quadro do vídeo. Note que pedi, explicitamente, que o framerate seja de 30 quadros por segundo e que a resolução gráfica seja de 1280×720 (HD).

Aliás… esse é o método favorito de “misturar” vídeos, onde um deles (o sobreposto) tenha um canal Alpha, ou seja, transparência… A maioria dos editores de vídeo suporta essa feature. O OpenShot, por exemplo, usa esse recurso quando criamos um “efeito especial” usando Blender, por exemplo.

  • De posse de todos os quadros, temos que convertê-los para texto. É aqui que entra o caca-utils. Nele, temos um programinha chamado img2txt, que converte uma imagem qualquer (JPG, PNG etc) em texto, seja ASCII puro (sem cores), HTML, SVG ou um formato gráfico chamado TARGA. É esse que usarei, já que isso facilita a reconstrução do vídeo:
$ for i in *.png; do \
    img2txt -f tga -b ordered8 -W 160 "$i" > "${i%.*}.tga"; \
  done

A opção -b ordered8 usa subsampling em blocos de 8×8 para realizar uma emulação de dithering, em modo texto. A opção -W 160 informa ao img2txt que usaremos linhas com 160 caracteres… Você pode usar menos, como 80, que é o padrão para o modo terminal em muitas instalações (especialmente Windows), mas, veremos que usar linhas maiores (ou retângulos maiores) nos dará resoluções “textuais” melhores.

O loop acima converterá todos os PNG para TGA.

O padrão TARGA é muito usado por profissionais porque é um padrão lossless, se nenhuma compressão for usada.

  • Tudo o que temos que fazer agora é juntar todos esses arquivos TGA na mesma velocidade (framerate):
$ ffmpeg -r 30 -f image2 -i %07d.tga -s hd720 \
  -b:v 1200k -minrate 1200k -maxrate 1200k -bufsize 1200k \
  -c:v libx264 -an "out.mp4"

Voilà! Temos um arquivo out.mp4 com o vídeo em “ascii”. Para efeitos de comparação, eis um vídeozinho de 13 segundos (já usei ele por aqui antes) e o vídeo convertido logo abaixo:

E, abaixo, está a comparação da conversão dos PNGs com 80, 160 e 240 caracteres por linha:

Monitoramento com câmeras de vigilância do homem probre

Um amigo me perguntou se “alguém conhece algum software…” que crie uma grade com várias fontes de vídeo ao estilo de sistemas de monitoramento de segurança com câmeras… well… EU conheço: ffmpeg!

O ffmpeg é bem versátil: Ele permite a leitura e escrita em diversos meios, não apenas arquivos. Podemos usar protocolos como async, bluray, cache, concat, crypto, data, file, ftp, gopher, hls, http, httpproxy, https, mmsh, mmst, pipe, rtp, sctp, srtp, subfile, tcp, tls, udp, udplite, unix, rtmp, rtmpe, rtmps, rtmpt, rtmpte, sftp.

Suponha que você tenha 4 câmeras Go Pro, disponibilizando streams através de UDP. Em seu /etc/hosts elas serão conhecidas sob os nomes camera1.local, camera2.local, camera3.local e camera4.local e você quer criar um stream de vídeo com os 4 streams das câmeras numa grade 2×2… Os streams das câmeras serão obtidos via URL udp://camera1.local/stream, por exemplo e o stream de saída será enviado para o ffserver via URL http://localhost:8090/stream.ffm.

Esses são os nomes dos “arquivos” de entrada e saída que usaremos no ffmpeg. Mas, para facilitar a minha vida neste texto, para que eu não tenha que explicar como configurar o ffserver (leia a man page!), usarei “arquivos” fictícios de entrada nomeados 1.mp4, 2.mp4, 3.mp4 e 4.mp4 e o arquivo de saída será output.mp4, codificado com o codec h264 e sem áudio.

Para criarmos a grade podemos usar um filtro complexo (múltiplas entradas e uma saída), usando os filtros scale, hstack e vstack. O grafo do filtro fica assim:

Grafo para o filtro complexo

Os filtros hstack “empilham” dois ou mais entradas horizontalmente, enquanto vstack faz o mesmo verticalmente. Do jeito que o filtro está montado as fontes 1 e 2 ficarão na primeira linha e as 3 e 4 na segunda. Os filtros scale estão ai para garantir que todos os vídeos aplicados aos filtros de empilhamento terão o mesmo tamanho (por exemplo, 320×200). Assim, a linha de comando com o filtro complexo fica assim:

$ ffmpeg -i 1.mp4 -i 2.mp4 -i 3.mp4 -i 4.mp4 -an -c:v libx264 \
-filter_complex '[v:0]scale=320:200[t1];\
[v:1]scale=320:200[t2];\
[v:2]scale=320:200[t3];\
[v:3]scale=320:200[t4];\
[t1][t2]hstack[v1];\
[t3][t4]hstack[v2];\
[v1][v2]vstack[vout]' -map '[vout]' \
output.mp4

Se os vídeos tiverem duração diferentes, use a opção -shortest para usar a duração do menor deles, por exemplo… Isso não é problema se estivermos lidanco com streams.

E se eu tiver um número ímpar de fontes de vídeo?

Existe uma fonte “especial” chamada nullsrc, onde você pode especificar, num grafo, "nullsrc=s=320x200[vnull]" e a fonte vnull será um vídeo de 320×200 vazio.

Mas… ficou muito chapado!

Os filtros vstack e hstack colocam os vídeos lado a lado, sem espaçamento… Isso não significa que, ao invés de usar esses filtros, você não possa usar o filtro overlay para os vídeos, como mostrei neste post aqui, só que para imagens.

Você pode usar uma imagem PNG como vídeo de fundo, por exemplo, com “painéis”, criados com o GIMP onde os streams serão colocados. Basta posicionar os streams, depois de “escalonados” nos locais corretos.

Mas… Eu quero fazer um programinha!

Você terá que lidar com as bibliotecas da família libav, como mostrei neste post. Mas, atenção, o código do artigo não funciona mais… existem muitas diferenças entre as versões atuais das bibliotecas da família libav do que as da época em que escrevi aquele artigo! Consulte a documentação da versão de suas libs.

Além do mais… o código final ficará muito grande e complicado (teremos que obter frames de fontes diferentes, via rede e “servir” o stream final, trabalhado… Os escalonamentos e posicionamentos podem ainda ser feitos por filtros e, suspeito, que as bibliotecas sejam agnósticas quanto às fontes dos streams… mas, isso será mesmo uma complicação. Deixo para o leitor os detalhes de implementação sabendo que será uma pequena dor-de-cabeça.

O ffserver:

O usuário vai querer ver o stream final, ao invés dos individuais. O meio mais “simples” de fazer isso é usar o ffserver, que receberá o stream gerado acima e permitirá que um usuário conecte ao servidor usando seu player favorito (VLC, por exemplo).

O ffserver precisa de um arquivo de configuração informando os dados da conexão que ele esperará receber, bem como o formato do stream de saída. Ao invés de enviar a saída do ffmpeg para um arquivo output.mp4, enviamos para, por exemplo, http://localhost:8090/stream.ffm. Onde a porta 8090 é a usada (por default) pelo ffserver e stream.ffm o path definido no arquivo de configuração. Do lado do cliente, podemos ver o stream via URL (por exemplo): rtsp://<meuhost>/stream. Claro, depende de como você configura o ffserver.

Restringindo o tamanho de um arquivo de vídeo (ffmpeg)

Imagine que você tem um arquivo de vídeo de uns 50 MiB de tamanho, vamos chamá-lo de video.mp4. Agora, suponha que você não esteja muito interessado em manter a qualidade máxima do vídeo, mas queira que o arquivo caiba exatamente num tamanho definido por você. Como fazer isso?

Para começar, temos que entender o que é um bit rate. Essa grandeza é a velocidade com que os bits são transmitidos do stream para um dispositivo qualquer… Por exemplo, no caso de vídeo, se o bit rate for de 1 Mib/s, isso significa que, a cada segundo, 1048576 bits são transmitidos para o decoder do vídeo… Note que temos bits por unidade de tempo, o que caracteriza velocidade, assim como na física. Se temos a duração da viagem e a distância a ser viajada, podemos calcular facilmente a velocidade média. No nosso caso a duração do vídeo continuará sendo a mesma, mas, queremos viajar um espaço menor, ou, transmitir menos bits (o comprimento do stream). Portanto temos que diminuir a velocidade.

Se você souber o tamanho total do vídeo, em segundos, pode calcular um bit rate que faça com que o vídeo caiba num tamanho especifico. Para obter a duração do seu vídeo você pode fazer:

$ ffprobe -v error -select_streams v:0 \
  -show_entries stream=duration \
  -of default=nk=1:nw=1 video.mp4

Se quiséssemos que o arquivo de 50 MiB caiba em 10 MiB e esse vídeo tem, por exemplo, 3 minutos (3\cdot60=180\,s) de duração, o bit rate deveria ser de \frac{8\cdot tamanho}{tempo}=\frac{8\cdot10\cdot1048576}{180}=466033.77\,b/s, ou, aproximadamente, 455 Kib/s. Há, obviamente, a necessidade de multiplicarmos o tamanho do arquivo final por 8, já que um byte tem 8 bits!

Se o vídeo contiver stream de áudio, temos que subtrair o bit rate deste áudio do obtido acima, porque o áudio também ocupa espaço no container… Suponha que tenhamos um stream de áudio com bit rate de 64 Kib/s. Para obter o bit rate do áudio usamos uma linha de comando similar à mostrada acima:

$ ffprobe -v error -select_streams a:0 \
  -show_entries stream=bit_rate \
  -of default=nk=1:nw=1 video.mp4

A diferença está no parâmetro passado para a opção -show_entries e na seleção do stream.

No nosso exemplo o vídeo terá que ser “recodificado” com bit rate de 455-64=391\,Kib/s. E isso deve ser feito em dois passos para garantir maior precisão no bit rate final dos streams:

$ ffmpeg -y -i video.mp4 -c:v libx264 -preset veryslow \
  -b:v 391k -pass 1 -c:a libvo_aacenc -b:a 64k \
  -f mp4 /dev/null

$ ffmpeg -i video.mp4 -c:v libx264 -preset veryslow \
  -b:v 391k -pass 2 -c:a libvo_aacenc -b:a 64k videoout.mp4

$ rm ffmpeg*.log*

O primeiro comando não gera um arquivo de saída… Ele gera dois arquivos de log, usados pelo ffmpeg, para refinar os cálculos do segundo passo (o segundo comando). No passo 2 os mesmos parâmetros são usados, execeto pelo <tt>-pass</tt> e pela informação do vídeo de saída. Não é necessário a opção <tt>-f mp4</tt>, porque o próprio arquivo nos diz o formato desejado.

Dependendo dos codecs o arquivo final pode ficar um pouco menor que o tamanho máximo desejado… Mas, é claro, depende também da complexidade do vídeo…

O preset veryslow, usado acima, é apenas um ajuste para o codec h264, na intenção de manter, ao máximo, a qualidade do vídeo.

Eis um simples script para fazer essa mágica toda:

#!/bin/bash

if [ ! -f "$1" ]; then
  cat << EOF
Usage: calc_vbr <videofile>

Calculates the appropriate video bitrate for an specific
desired file dize.
EOF
  exit 1
fi

duration="`ffprobe -v error -select_streams v:0 -show_entries stream=duration -of default=nk=1:nw=1 "$1"`"
abr="`ffprobe -v error -select_streams a:0 -show_entries stream=bit_rate -of default=nk=1:nw=1 "$1"`"
if [ -z "$abr" ]; then abr=0; fi

# Por estranho que pareça o ffmpeg volta bit_rates
# múltiplos de 1000, não 1024.
abr=$((abr/1000))

# Pergunta o tamanho do arquivo desejado!
read -p 'Qual é o tamanho do arquivo desejado (em KiB): ' \
  desired_filesize

# Aqui é bom ter um pouco mais de precisão...
vbr="`echo "(($desired_filesize * 8) / $duration) - $abr" | bc`"

# Só teremos opções de audio, se o bit rate do audio for != 0.
AUDIOOPTS=""
if [ $abr -ne 0 ]; then 
  AUDIOOPTS="-c:a libvo_aacenc -b:a ${abr}k"; 
fi

ffmpeg -y -i "$1" -c:v libx264 -preset veryslow \
  -b:v ${vbr}k -pass 1 $AUDIOOPTS -f mp4 \
  /dev/null && \
ffmpeg -i "$1" -c:v libx264 -preset veryslow \
  -b:v ${vbr}k -pass 2 $AUDIOOPTS "_${1%.*}.mp4" && \
rm ffmpeg*.log*;

# Mesmo nome do arquivo original, exceto pela extensão...
# Se o arquivo original for .mp4, sobrescreve.
mv --force "_${1%.*}.mp4" "${1%.*}.mp4"

Como passo final, basta nos livrarmos dos arquivos temporários criados pelo ffmpeg…