Por que dimensionar vídeos com número par de linhas e colunas?

Já reparou nisso? Se você pede para dimensionar seus vídeos com número ímpar de linhas ou colunas o ffmpeg (e outros encoders) reclamam. Ainda, às vezes, quando você edita seu vídeo dimensionado dessa forma, ou observa colunas ou linhas pretas adicionadas no lado direito ou abaixo, ou linhas com “lixo” (um padrão de pixels maluco)… Isso acontece porque vídeos codificados com o padrão YUV 4:2:0 (ou NV12, por exemplo) ou qualquer um dos padrões YUV exigem que 4 pixels sejam agrupados juntos (2 pixels horizontais versus 2 pixels verticais) e, portanto, sua resolução tem que ter componentes pares.

Outro detalhe é relativo aos codecs. Se você usa um dos dois mais populares: h264 ou h265, cada quadro é dividido em blocos de pixels de tamanho (horizontal e vertical) múltiplos de 2^n (4×4, 8×8, 16×16 ou, no caso do h265, outros tamanhos múltiplos de 2^n). Isso significa, de novo, que a imagem precisa ter dimensionamento par nos dois sentidos.

Fica a dica…

O pesadelo dos “padrões” de tamanhos de vídeos

De novo esse troço? Well… Info para os aficionados por vídeos: Existem vários padrões de tamanho!

Os padrões mais simples de lembrar são os formatos widescreen, de aspecto \frac{16}{9}. Aqui temos o formato HD (1280×720), Full HD (1920×1080) e 4K (3840×2160). Vale notar que o formato 4K tem o dobro do tamanho (vertical e horizontal) do formato Full HD. Outra nota interessante é que ele não é, necessariamente “4K” (4 mil), mas tem 3840 pixels horizontais! Se bem que esses formatos geralmente são medidos pela quantidade de linhas, não de colunas. Seria esperado que o formato 4K fosse referenciado como 2.16K! :)

Existem padrões mais antigos também. O formato desses não seguem o aspecto \frac{16}{9}, mas são mais “quadrados”. Por exemplo, existe o padrão NTSC-M (720×486), resultando num formato de aspecto \frac{40}{27}, bem próximo do padrão letterbox de aspecto \frac{4}{3}. Por quê 486 linhas? No formato NTSC temos dois quadros entrelaçados de 262.5 linhas, ou 525 linhas por quadro completo, mas apenas 486 são visíveis, as 39 linhas “invisíveis” são usadas no pulso de retraço vertical… No entanto, existem formatos europeus (um deles usado no Brasil também): PAL-M e SECAM. O formato PAL-M usa as mesmas 525 linhas, mas tem 480 visíveis, resultando no formato 720×480 com um aspecto de \frac{3}{2}. É um aspecto um pouco mais “largo” (wide) que o letterbox. O padrão SECAM tem mais linhas que o NTSC-M e PAL-M (625 linhas), mas, até onde sei, não é mais usado.

Até aqui temos os formatos disponibilizados para TVs. No entanto, existem diversos formatos para vídeos em outras mídias, como cinema, por exemplo. Na telona o aspecto mais usado é de \frac{11}{8} e IMAX usa os aspectos de \frac{143}{10} ou \frac{19}{10}, dependendo do filme. Isso não significa que não existam outros aspectos em uso. Filmes chamados de “35 mm” costumam ter aspecto de \frac{3}{2}, ou seja, são duas vezes mais largos que a altura; “Super 16 mm”, por outro lado, usam \frac{5}{3}; Enquanto “70 mm” usam \frac{11}{5}; TVs antigas e alguns monitores tinham aspecto de \frac{5}{4}; Alguns distribuidores de filmes usam o aspecto de \frac{14}{9} para facilitar o escalonamento para os aspectos widescreen e letterbox; Alguns monitores atuais têm aspecto de \frac{8}{5}; O padrão norte-americano para a telona de cinema costuma seguir o aspecto de \frac{37}{2}… É um pesadelo, não? Não tenho como citar todas os “padrões” existentes porque simplesmente não tenho a acesso a todos eles. Não citei, por exemplo, o usado em “Super 8 mm” (popular nos primórdios, especialmente pela distribuição de filmes pornográficos nesse formato!).

Assim, recomendo que você se atenha aos padrões “de fato” mais comuns, como 540p (aspecto \frac{4}{3}); 720p, 1080p e 2160p (aspecto \frac{16}{9}) e use aquele esquema de colocar barras horizontais (ou verticais, se assim for necessário), centralizando o vídeo, se o mesmo não tiver o aspecto desejado.

Ahhh… ainda existe o pesadelo da velocidade de reprodução (quadros por segundo)… mas, deixarei isso pra depois…

Convertendo vídeos para 1280×720 (juntando tudo)

Eis o script final para converter vídeos “widescreen”, mesmo com resoluções malucas. Aqui, considero qualquer vídeo com aspecto maior ou igual a \frac{16}{9}-10\%, ou \frac{159}{90}, como “widescreen”. Note que a comparação (em is_wide()) é feita usando cálculo inteiro, não ponto flutuante:

#!/bin/bash

# v2wide <files>
if [ -z "$1" ]; then
  echo -e "\e[1;33mUsage\e[m: $0 <files>"
  exit 1
fi

# Qualquer vídeo com variação de aspect ratio de 16/9 - 10%
# ou superior, considero como widescreen.
is_wide() {
  if [[ $(($1 * 90)) -ge $(($2 * 159)) ]]; then
    return 0
  fi
  return 1
}

showvideoinfo() {
  FR="`mediainfo --inform='Video;%FrameRate%' "$1"`"
  DURATION="`mediainfo --inform='Video;%Duration/String3%' "$1"`"
  SIZE="`du -h "$1" | cut -f1`"

  echo -e "\e[1m'$1'\e[m (${2}x${3}, $FR fps, $DURATION, $SIZE)"
}

# Converte TODOS os vídeos passados na linha de comando.
# Aceita, inclusive "globs", como '*.mkv', por exemplo.
for i in "$@"; do
  W="`mediainfo --inform='Video;%Width%' "$i"`"
  H="`mediainfo --inform='Video;%Height%' "$i"`"

  if ! is_wide $W $H; then
    echo -e "'$i': \e[1;31mNot a widescreen video.\e[m"
    continue
  fi

  NW=1280
  NH="`bc <<< "scale=3; 1280 / ( $W / $H )" | sed -E 's/\..+$//'`"

  # Se o aspecto é inferior a 16/9, considera 16/9
  if [ $NH -gt 720 ]; then
    NH=720
  fi
  BARH=$(( ( 720 - $NH ) / 2 ))

  FR='ntsc-film'  # Video framerate
  VBR="`bc <<< 'scale=3;(1280*720*(24000/1001))/13.824' | sed -E 's/\..+$//'`" # Video bitrate
  ABR='128k'      # Audio bitrate
  ASR='44.1k'     # Audio sampling rate

  # Cria um arquivo temporário em /tmp/
  TMPFILE="`mktemp /tmp/XXXXXXXXX.mp4`"

  # Mostra infos do arquivo original (aproveira width
  # e height já obtidos).
  showvideoinfo "$i" $W $H

  # Usando NVIDIA NENC para h264.
  #
  # Mude o codec para h265_nvenc para h265 (duh!).
  #
  # Note que sua placa pode codificar, no máximo, dois
  # vídeos simultâneos usando NVENC (a maioria das
  # low-end NVIDIA têm esse limite a não ser a Quadro e
  # Titan - que podem codificar 30, acho).
  #
  # Aqui, uso a aceleração CUDA (cuvid) e o encoder da NVIDIA
  # (h264_nvenc);
  #
  # Uma sincronização adaptativa de frames baseada no "novo"
  # framerate (vsync 1);
  #
  # Formato de subsampling de chroma YUV 4:2:0 progressivo;
  #
  # Codec de áudio AC3 (mude para fdk AAC se quiser);
  #
  # Downsampling para stereo (ac 2);
  #
  # Sampling rate de 44.1 kHz;
  #
  # Resincronização de áudio com vídeo (porque o framerate
  # pode ter mudado e a sincronização (vsync) pode "comer"
  # frames, mudando as marcas de temporização.
  #
  # Eliminação de metadados e informação de "capítulos"
  # (se houverem).
  #
  # -y (assume yes), -v error (só mostra erros se houverem),
  # mostra apenas a estatística da codificação (-stats).
  #
  # Repare que o arquivo original é apagado e substituído por um
  # de mesmo nome, mas com extensão .mp4. Em caso de falha,
  # o arquivo temporário é apagado.
  ffmpeg -y \
         -v error \
         -stats \
         -hwaccel cuvid \
         -i "$i" \
         -c:v h264_nvenc \
         -r $FR \
         -bufsize $VBR \
         -minrate $VBR \
         -maxrate $VBR \
         -b:v $VBR \
         -vsync 1 \
         -pix_fmt yuv420p \
         -vf scale=$NW:$NH:lanczos,pad=1280:720:0:$BARH:black,setdar=16/9 \
         -c:a ac3 \
         -b:a $ABR \
         -ac 2 \
         -ar $ASR \
         -async 1 \
         -map_metadata -1 \
         -map_chapters -1 \
         "$TMPFILE" && \
  ( rm "$i"; mv "$TMPFILE" "${i%.*}.mp4" ) || \
  rm "$TMPFILE"
done

Concatenando vídeos com ffmpeg

Existem alguns meios de “juntar” dois ou mais vídeos num só. O ffmpeg possui um filtro para isso, um muxer e até um protocolo. São 3 métodos diferentes de fazer a mágica, mas a minha preferida é converter os vídeos em um formato headless, juntar os arquivos manualmente e depois recodificá-lo.

O jeito que consome menos espaço em disco, e é mais rápido, é converter os vídeos para o formato mpeg2 (container e codecs – de vídeo e áudio). Esse formato é headless, ao contrário do que temos no container mpeg4, como pode ser visto abaixo:

MPEG-4 vs MPEG-2

Repare que o container MPEG-4 possui um header cheio de metadados (que podem ser retirados), mas o MPEG-2, não! Embora o formato MPEG-2 não tenha um header, as infos dos streams estão disponíveis. Como o arquivo MPEG-2 é comprimido também, ele não consumirá muito disco, mas você terá uma pequeníssima degradação da imagem ao converter os vídeos originais para MPEG-2, juntá-los e depois reconverter tudo de volta para MPEG-4 ou usando outro par de container/codecs quaisquer.

De qualquer maneira, para juntar dois vídeos basta ter certeza que estejam com a mesma resolução, mesmo frame rate (e o mesmo sampling rate do áudio também)… Como exemplo, suponha que tenhamos um video1.mkv e um video2.avi e eu queira juntá-los. Suponha que ambos tenham resolução wide screen, mas o primeiro seja 1920×1080 e o segundo 1280×720. Suponha, ainda, que eu queira um vídeo final em 1280×720:

$ ffmpeg -i video1.mkv \
         -s 1280x720 -r ntsc-film \
         -maxrate 2M -bufsize 2M -b:v 2M \
         -b:a 128k -ar 44.1k -ac 2 \
         video1.mpg
$ ffmpeg -i video2.avi \
         -s 1280x720 -r ntsc-film \
         -maxrate 2M -bufsize 2M -b:v 2M \
         -b:a 128k -ar 44.1k -ac 2 \
         video2.mpg
$ cat video{1,2}.mpg > video.mpg
$ ffmpeg -i video.mpg \
         -c:v libx264 \
         -maxrate 1.6M -bufsize 1.6M -b:v 1.6M \
         -c:a ac3 \
         -b:a 128k
         video.mp4
$ rm *.mpg

Os primeiros dois comandos ffmpeg convertem os vídeos para MPEG-2, garantindo a resolução de 1280×720 e o frame rate de 23.976 fps. Informei o bitrate do vídeo ligeiramente maior que o necessário para não perder qualidade (o ffmpeg usa bitrate de 200 kb/s de vídeo e 192 kb/s de áudio por default!). Repare, ainda, que não forneci os codecs de vídeo e áudio para os arquivos MPEG-2. O ffmpeg usa os defaults (mpeg2video e mp2).

Obtidos os vídeos video1.mpg e video2.mpg, é uma questão de concatená-los via comando cat e recodificar o resultado para o formato desejado. Neste caso, usando os codecs libx264 (h264) e AC-3, para um container MPEG-4.

Podemos evitar o problema de dupla degradação da imagem, por recompressões sucessivas, obtendo as características dos vídeos e convertendo-os para um formato não comprimido (RAW), mas, ainda assim, headless. Primeiro, converto os vídeos para um formato RAW com frame rate e formato de chroma subsampling conhecidos, respectivamente ntsc-film (23.976 fps) e YUV 4:2:0p, e também na mesma resolução (1280×720). Mais ou menos como fizemos antes:

$ ffmpeg -i video1.mkv 
         -r ntsc-film \
         -pix_fmt yuv420p \
         -s 1280x720 \
         -an \
         video1.yuv
$ ffmpeg -i video2.avi \
         -r ntsc-film \
         -pix_fmt yuv420p \
         -s 1280x720 \
         -an \
         video2.yuv

Note que o bitrate é supérfluo, já que estamos convertendo para um formato RAW. Um aviso: Prepare-se! Os vídeos *.yuv, por não serem comprimidos, ocuparão um espaço enorme em disco. Para dar uma ideia, um vídeo de uns 30 minutos, por exemplo, pode chegar a ocupar uns 100 GiB! (Yep! GibiBytes!) Esse é o preço que se paga para tentar manter a qualidade…

Os “containers” *.yuv não permitem múltiplos streams. Trata-se de um arquivo com um monte de bytes, sem nenhum metadado sobre o seu conteúdo. Assim, temos que extrair os áudios, com o mesmo sample rate e quantidade de canais, mas no formato PCM (aqui usei 32 bits, little endian, sinalizado). Os áudios também não são comprimidos e, portanto, temos menos perdas, mas bastante espaço em disco usado:

$ ffmpeg -i video1.mkv -vn \
         -f s32le -ar 44.1k -ac 2 \
         audio1.raw
$ ffmpeg -i video2.avi -vn \
         -f s32le -ar 44.1k -ac 2 \
         audio2.raw

De novo, o bitrate do áudio é supérfluo aqui…

Agora, junto tudo (vídeos e áudios, isoladamente) com o cat e depois converto pro formato desejado:

$ cat video{1,2}.yuv > video.yuv
$ cat audio{1,2}.raw > audio.raw
$ ffmpeg -f rawvideo \
         -pix_fmt yuv420p -s 1280x720 -r ntsc-film \
         -i video.yuv \
         -f s32le -ar 44.1k -ac 2 \
         -i audio.raw \
         -c:v libx264 -r ntsc-film \
         -maxrate 1.6M -bufsize 1.6M -b:v 1.6M \
         -c:a ac3 -b:a 128k \
         -map 0 -map 1 \
         -f mp4 videoout.mp4
$ rm *.yuv *.raw

O ffmpeg obedece a ordem das opções fornecidas. Antes da inclusão do arquivo de vídeo temos que dizer que ele é rawvideo e o seu formato (as opções -f, -pix_fmt, -s e -r, antes da opção -i). Depois, temos que fazer o mesmo para o áudio. Depois da inclusão dos arquivos as opções se aplicam à saída. Note que usei as opções -map para dizer que ambas as entradas têm que ser usadas na saída.

Por fim, matamos os arquivos temporários (que, provavelmente, estarão consumindo o seu disco todo, se houver espaço).

Recomendo usar alguma variação disso apenas se você quer manter a qualidade… Uma dica para a saída é não usar as opções -maxrate, -bufsize e -b:v, mas sim a opção -qp 1, que te dará a máxima qualidade possível (mas um arquivo grande), se qualidade é realmente importante no arquivo final. No que se refere ao áudio, aumente o bitrate, se necessário (192 kb/s é mais que suficiente!).

Usando um frame-rate e um bit-rate menores…

Ok… “comi mosca”, como dizia um professor meu.

Até o momento eu estava recodificando vídeos com um frame rate de \frac{30000}{1001} fps (ou 29.97 fps), ou seja, padrão NTSC. Com isso, segundo minha “formuletinha”, que mostrei aqui, para resolução HD, ou 720p, tería um bit rate de 1.998 Mb/s.

Mas… Que tal usar um frame rate e um bit rate menores? O menor frame rate padronizado é o NTSC-FILM (\frac{24000}{1001} fps, ou 23.976 fps. Isso nos dá um bit rate, para 1280×720, de 1.599 Mb/s… Esses cerca de 400 kb/s a menos nos dá uma redução de cerca de duas vezes o tamanho do stream de vídeo. Se, usando minha “formuletinha” eu conseguia uma redução de 4 a 6 vezes do tamanho do stream, agora tenho uma redução de 6 a 8 vezes!

E não… com a redução do frame rate você não tem uma degradação do vídeo. Pelo menos, não uma que você possa perceber (a não ser que você venha de Krypton, seja o Flash ou tenha os poderes de percepção de um pombo ou uma mosca!).

Considere que tenhamos um vídeo “videoin.mp4” com resolução de 1920×1080 @ 30 fps, 4.5 Mb/s… Querendo reduzir isso significativamente, eu faço:

$ ffmpeg -hwaccel cuvid \
         -i videoin.mp4 \
         -c:v h264_nvenc \
         -r ntsc-film \
         -maxrate 1.599M -bufsize 1.599M -b:v 1.599M \
         -vf scale=1280:720:flags=lanczos,setdar=16/9 \
         -c:a ac3 \
         -b:a 128k \
         -ac 2 \
         -ar 44.1k \
         -async 1 \
         -map_metadata -1 \
         videoout.mp4

Espere obter um arquivo umas 6 vezes menor (pelo menos!).

Usando aceleração da Intel para codificação de vídeos h264

Assim como a nVidia, processadores Intel também tem suporte a codificação/decodificação de streams MPEG, especificamente ao codec AVC1 (ou h264). Mas o uso não é tão direto assim.

Em primeiro lugar, ao invés de usar o decoder cuvid, devemos usar a vaapi e, para isso, devemos instalar a biblioteca libva1 (e possivelmente a libva2). É provável que você também tenha que instalar o pacote i965-va-driver:

$ sudo apt install libva1 libva2 i965-va-driver

Agora o ffmpeg pode usar a VAAPI e, para isso devemos usar o decoder vaapi como em -hwaccel vaapi, mas também temos que informar qual é o device que queremos. Geralmente esse device é o /dev/dri/renderD128:

$ ls -l /dev/dri
total 0
drwxr-xr-x  2 root root        80 jan  9 08:21 by-path
crw-rw----+ 1 root video 226,   0 jan  9 08:21 card0
crw-rw----+ 1 root video 226, 128 jan  9 08:21 renderD128

Agora, o encoder h264_vaapi pode ser usado, mas, precisa acrescentar um filtro final (se usarmos algum filtro de vídeo): hwupload. Isso fará com que cada quadro seja repassado para o codec para ser processado em hardware.

Eis uma linha de comando geral para codificação:

$ ffmpeg -hwaccel vaapi -vaapi_device /dev/dri/renderD128 \
         -i videoin.mp4 -c:v h264_vaapi \
         -maxrate 1.998M -bufsize 1.998M -b:v 1.998M \
         -vf format=nv12,scale=1280:720:flags=lanczos,hwupload \
         -c:a ac3 -b:a 96k -ac 2 -ar 44.1k -async 1 \
         videoout.mp4

Note que hwupload vai enviar o quadro depois que ele foi escalonado (e depois que qualquer outro filtro antes da codificação).

Sem a opção -vaapi_device (ou -haccel_device, que acredito que funcionará do mesmo jeito) e sem o filtro final hwupload, a codificação usando h264_vaapi não funcionará.

Outro detalhe importante (pelo menos onte pude testar) é que o chroma subsampling YUV 4:2:0 não funciona com esse encoder (h264_vaapi) – pelo menos não num i5-3570. É possível que em gerações mais novas funcione bem… Mas o subsampling NV12 funciona. Por isso o filtro format=nv12.

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: