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…

O problema da solução ingênua da obtenção de valores aleatórios.

Considere que você quer fazer um programinha que “role” um dado de 6 faces. Geralmente, alguém faz algo assim:

int r;

srand( time( NULL ) );
r = rand() % 6 + 1;

A ideia é que obter o resto da divisão por 6 acarretará em valores menores que 6 (de 0 até 5) e somando 1, obtemos um dos 6 valores possíveis. Mas há um problema!

Considere que rand() retorna valores entre 0 e RAND_MAX que, em arquiteturas de 32 bits ou mais pode equivaler a 2^{31}-1, ou seja, o valor máximo é 2147483647 (31 bits setados). Isso representa um em 2147483648 possíveis valores e essa quantidade não é divisível por 6! Outra forma de pensar nisso é que temos 357913941 blocos de 6 valores possíveis e um bloco incompleto com os 2 primeiros valores, ou seja, no último bloco faltam 4 possibilidades para ter uma distribuição uniforme de valores possíveis.

Outra forma de pensar no assunto é essa: Considere que você tenha um dado de 6 faces e queira escolher, aleatóriamente, 1 entre 4 itens (a, b, c, d). As possibilidades são: (1=a, 2=b, 3=c, 4=d, 5=a e 6=b). Os itens a e b têm mais chance de serem escolhidos (2 chances em 6, cada) porque não temos uma distribuição de valores aleatórios uniforme! A solução é nos livrarmos dos 2 últimos valores, seja “jogando” o dado novamente, colhendo apenas um dos 4 valores possíveis, ou aplicando alguma regra maluca para nos livrarmos desses dois valores. Infelizmente, a segunda hipótese não é tão simples…

Considere agora o retorno de rand() como mostrado no fragmento de código lá em cima. Obteremos valores entre 1 e 6. Acontece que temos um espaço amostral de 2^{31}=2147483648 valores (RAND_MAX) dividido em \frac{2^{31}}{6}+1=357913942 blocos de 6 elementos, onde o último bloco só tem 2 (2^{31}\mod\,6=2). Assim, a chance de obtermos 1 ou 2 são de \frac{357913942}{2147483648} e de obter os outros 4 valores são de \frac{357913941}{2147483648}. Ora \frac{357913941}{2147483648} < \frac{1}{6} < \frac{357913942}{2147483648}. Não precisa ser um gênio para entender que 1 e 2 podem ocorrer mais vezes que os outros 4 valores.

A solução para esse problema não é trivial e, acredito, que nem mesmo seja possível num ambiente computacional binário. Em primeiro lugar, qualquer algorítmo que nos dê um valor aleatório conterá um valor entre 2^n outros. Ao obter o resto da divisão para limitar o escopo, criaremos, automaticamente, um último grupo incompleto, se o divisor não for, também, um valor 2^m menor ou igual a quantidade de amostras possíveis… Tudo o que podemos fazer é diminuir o erro relativo das distribuições e nisso rand() pode ser confiável, já que os erros relativos das distribuições, no nosso exemplo, são de, respectivamente, 1.86\cdot10^{-7}\,\% para 1 e 2; e 9,31\cdot10^{-8}\,\% para o resto:

\displaystyle \begin{matrix}  E(\%)=100\cdot\left|\frac{X - X_{ideal}}{X_{ideal}}\right|\\  \\  E_{1,2}(\%) = 100\cdot\left|\frac{\frac{357913942}{2147483648} - \frac{1}{6}}{\frac{1}{6}}\right|\approx1.86\cdot10^{-7}\,\%\\  \\  E_{3..6}(\%) = 100\cdot\left|\frac{\frac{357913941}{2147483648} - \frac{1}{6}}{\frac{1}{6}}\right|\approx9.31\cdot10^{-8}\,\%\\  \end{matrix}

Ou seja, a cada 500 milhões (100\cdot\frac{1}{E_{1,2}(\%)}) de escolhas, aproximadamente, teremos mais 1’s e 2’s que o resto. Poderíamos melhorar esse erro de distribuição aumentando a quantidade de bits do valor aleatório. Se tivessemos 2^{64}=18446744073709551616 amostras (64 bits) teríamos 4 valores no último grupo e 3074457345618258603 grupos. As chances dos valores de 1 a 4 seriam de \frac{3074457345618258603}{18446744073709551616} e de 5 e 6 seria de \frac{3074457345618258602}{18446744073709551616}, Os erros de distribuição relativos da primeira faixa seriam de, aproximadamente, 1,8\cdot10^{-17}\,\% e da segunda, 1,2\cdot10^{-18}\,\%, ou seja, em 50 quatrilhões de “jogos” observaríamos o problema, que só se repetiria nos próximos 50 quatrilhões de jogadas, possivelmente.

Ahhh, mas eu sou um chato né? O erro é tão pequeno para aplicações práticas, não é? Acontece que RAND_MAX só tem 31 bits de tamanho em certos compiladores e certas arquiteturas. Outras podem usar valores muito menores, como 32767 (15 bits). De fato, a especificação ISO 9989 sugere que esse seja o valor mínimo para RAND_MAX, mas ele pode ser ainda menor!

Assim, neste caso, teríamos 5462 blocos e o último teria apenas 2 valores (como com 31 bits) e as distribuições seriam de \frac{5461}{32768} (para 3 até 6) e \frac{5462}{32768} (para 1 e 2), o que daria, para o maior erro relativo, um valor muito grande (0.012% para 1 e 2). A cada 10000 jogadas você perceberia o problema. Você já pode considerar esse dado “viciado”…

Ahhhh… note que não estou considerando que rand() geralmente é implementado usando LCG que oferece menos aleatoriedade nos bits inferiores. Assim, obter o resto de uma divisão por um valor pequeno tende a criar valores ainda menos aleatórios.

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!).

Indo a extremos para evitar usar ponto flutuante. Você deveria fazer isso também!

Eis um problema que preciso resolver para o meu mais novo projeto de brinquedo: Preciso determinar se uma imagem (stream de vídeo) tem aspecto mais próximo do aspect ratio \frac{4}{3} ou do \frac{16}{9}. A solução mais “simples” é simplesmente dividir o comprimento (width) pela altura (height) e comparar o resultado com uma fração de ‘meio termo”. O “meio termo”, é claro, é a média aritmética simples dos dois aspectos:

\displaystyle \frac{\frac{16}{9} + \frac{4}{3}}{2}=\frac{16+12}{9}\cdot\frac{1}{2}=\frac{28}{18}=\frac{14}{9}

Por que a fração de “meio termo”? É que o vídeo pode ter resolução não padronizada e, neste caso, colocarei “barras pretas” horizontais ou verticais, dependendo do aspecto. Assim, temos:

int is_letterbox(unsigned int width, unsigned int height)
{
  double ratio = ( double )width / height;

  return ratio <= 14.0 / 9.0;
}

Nada mais simples, né? Se o aspecto for menor ou igual a \frac{14}{9}, então encaro o vídeo como candidato para ser convertido para letterbox, ao invés de widescreen. Ahhh… sim… note que o operador de comparação (<=) tem menor precedência que o de divisão (/)… Por isso a expressão intera faz o que queremos!

O problema é que nem a divisão de \frac{width}{height}, nem a divisão \frac{14}{9} são exatas. Bem… a primeira até pode resultar num valor exato, mas o quociente constante de \frac{14}{9} não! Isso poderia causar um “falso positivo” no caso de teste por “menor que” e um “falso negativo” no caso do teste de igualdade… Mas, aqui temos um outro problema: Imagine que o vídeo tenha resolução de 1400×901. Isso dará um aspecto de \frac{1400}{901}, necessariamente, que é muito próximo de \frac{14}{9}.

Falei “necessariamente” porque para obter a manor fração possível podemos usar o “máximo divisor comum” para fazer a divisão do dividendo e divisor (inteiras), obtendo os menores valores. Por exemplo:

#include <stdio.h>
#include <stdlib.h>

static unsigned int gcd( unsigned int, unsigned int );

int main( int argc, char *argv[] )
{
  unsigned int w, h, d;

  if ( argc != 3 )
  {
    fputs( "ERROR\n", stderr );
    return EXIT_FAILURE;
  }

  w = atoi( argv[1] );
  h = atoi( argv[2] );

  d = gcd( w, h );

  printf( "%u/%u -> %u/%u\n",
          w, h, w / d, h / d );

  return EXIT_SUCCESS;
}

unsigned int gcd( unsigned int a, unsigned int b )
{
  unsigned int t;

  if ( a < b )
  {
    t = a;
    a = b;
    b = t;
  }

  if ( ! b )
    return 1;

  while ( b )
  {
    t = b;
    b = a % b;
    a = t;
  }

  return a;
}

Compilando e testando:

$ cc -o test test.c
$ ./test 1280 720
1280/720 -> 16/9
$ ./test 1400 901
1400/901 -> 1400/901
$ ./test 1400 920
1400/920 -> 35/23

Se formos comparar o numerador e o denominador isoladamente a coisa vai complicar um bocado. Note que mesmo numa resolução maluca diferente (1400×920) o aspecto ainda tem o dividendo e divisor bem diferentes (\frac{35}{23}).

De volta ao ensino fundamental:

Como sabemos que, por exemplo, \frac{2}{3} é maior que \frac{3}{5}? A solução que você aprendeu lá no ensino fundamental é converter ambas as frações ao mesmo denominador. Daí podemos comparar diretamente os numeradores, certo? Basta multiplicar ambos o numerador e o denominador de ambas as frações pelo denominador da outra fração:

\displaystyle\frac{2}{3}\,\texttt{e}\,\frac{3}{5}\rightarrow\frac{2\cdot5}{3\cdot5}\,\texttt{e}\,\frac{3\cdot3}{5\cdot3}\rightarrow\frac{10}{15}\,\texttt{e}\,\frac{9}{15}

Agora, com ambas as frações “normalizadas”, podemos comparar diretamente os numeradores. No caso, 10 é maior que 9 e, por isso, \frac{2}{3} é maior que \frac{3}{5}

Vocẽ reparou que não precisamos mais nos preocupar com os denominadores se simplesmente multiplicarmos um numerador pelo denominador da outra fração, né? Isso facilita um bocado a nossa rotina is_letterbox(), evitando o uso de ponto flutuante:

int is_letterbox( unsigned int width, unsigned int height )
{
  unsigned int n1, n2;

  /* Não precisamos calcular os denominadores! */
  n1 = width * 9;   // 9 de 14/9
  n2 = 14 * height; // height de width/height

  return n1 <= n2;
}

Comparando as duas rotinas:

Compilando com otimizações e gerando o código em assembly:

$ cc -O2 -mtune=native -S -masm=intel test.c

Olhem só isso, com ambos os códigos lado a lado:

is_letterbox_fp:                  is_letterbox_int:      
  mov edi, edi                      imul  esi, esi, 14
  mov esi, esi                      lea eax, [rdi+rdi*8]
  pxor  xmm0, xmm0                  cmp eax, esi
  xor eax, eax                      setbe al
  pxor  xmm1, xmm1                  movzx eax, al
  cvtsi2sdq xmm0, rdi               ret
  cvtsi2sdq xmm1, rsi
  divsd xmm0, xmm1
  movsd xmm1, [rip+.LC0]
  ucomisd xmm1, xmm0
  setnb al
  ret

.LC0:
  dq 14.0 / 9.0  ; funciona no NASM.

Pra começar, a segunda rotina não tem divisões (que são LERDAS!). Ainda, a segunda rotina, além de ser menor, é melhor otimizada pelo processador (fusões, reordenações, etc). Outra vantagem é que, na primeira rotina, se height for zero, ela nos causará problemas porque a divisão resultará em NaN, que sempre retornará falso na comparação em ponto flutuante! Mais uma: A comparação, na segunda rotina, é sempre exata!

O único problema da segunda rotina poderia ser o caso de ocorrência de overflows, mas, como as resoluções gráficas que usaremos não resultam em frações com componentes muito grandes (pelo menos, não o suficiente para, quando multiplicados por 9 e 14 causem overflows), então a rotina é bem segura. Se, mesmo assim, você quer evitar a possibilidade de overflows, mude os tipos de n1 e n2 para unsigned long int ou unsigned long long. É prudente colocar um ‘UL‘ depois das constantes ou fazer um casting das variáveis nas multiplicações:

;  int is_letterbox( unsigned int width, 
;                    unsigned int height )
;  {
;    unsigned long n1, n2;
;  
;    n1 = width * 9UL;
;    n2 = 14UL * height;
;  
;    return n1 <= n2;
;  }
is_letterbox:
  mov esi, esi         ; Apenas para zeras os 32 bits
  mov edi, edi         ; superiores dos argumentos.

  lea rax, [rsi*8]     ; RAX = height*8
  lea rdx, [rdi+rdi*8] ; RDX = width*9
  sub rax, rsi         ; RAX = height*7
  add rax, rax         ; RAX = height*14

  cmp rdx, rax
  setbe al
  movzx eax, al
  ret

Mesmo assim o código ainda é melhor que o equivalente em ponto flutuante.

Siga o conselho do Tio Fred: Fuja de ponto flutuante sempre que puder.

Meu script de conversão em C (libav) – Introdução conceitual

Well… como não tenho porra nenhuma pra fazer, vou converter meu script de conversão de videos, com todas as regras que apresentei até agora, para um código em C usando as bibliotecas do ffmpeg (libavformat, libavcodec, libavfilter, libavresample, libswscale e libavutil – aquelas mesmas informadas pelo ffmpeg):

$ ffmpeg -version
ffmpeg version 3.4.6-0ubuntu0.18.04.1 Copyright (c) 2000-2019 the FFmpeg developers
built with gcc 7 (Ubuntu 7.3.0-16ubuntu3)
configuration: --prefix=/usr --extra-version=0ubuntu0.18.04.1 --toolchain=hardened --libdir=/usr/lib/x86_64-linux-gnu --incdir=/usr/include/x86_64-linux-gnu --enable-gpl --disable-stripping --enable-avresample --enable-avisynth --enable-gnutls --enable-ladspa --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librubberband --enable-librsvg --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-omx --enable-openal --enable-opengl --enable-sdl2 --enable-libdc1394 --enable-libdrm --enable-libiec61883 --enable-chromaprint --enable-frei0r --enable-libopencv --enable-libx264 --enable-shared
libavutil      55. 78.100 / 55. 78.100
libavcodec     57.107.100 / 57.107.100
libavformat    57. 83.100 / 57. 83.100
libavdevice    57. 10.100 / 57. 10.100
libavfilter     6.107.100 /  6.107.100
libavresample   3.  7.  0 /  3.  7.  0
libswscale      4.  8.100 /  4.  8.100
libswresample   2.  9.100 /  2.  9.100
libpostproc    54.  7.100 / 54.  7.100

Já falei em como usar essas bibliotecas por aqui, mas numa versão antiga do ffmpeg (que, infelizmente, não compila mais). Agora, as libs estão padronizadas e tudo deve funcionar redondinho.

Conceitos

Do ponto de vista do ffmpeg (e da libav) um arquivo de vídeo é composto do container (chamado de “formato” pela libav), ou seja, AVI, MPEG-4, MKV, …; Dentro do container temos os streams (áudio, vídeo, legendas…); dentro de cada stream temos “pacotes” e cada pacote deve ser decodificado (ou codificado, dependendo se estamos lendo ou gravando um arquivo) por um codec. Esse passo final, na leitura, nos dará os frames (no caso de vídeo) decodificados e “descomprimidos” — um bitmap, por assim dizer.

Na nomenclatura da libav temos as estruturas AVFormat (container), AVStream, AVPacket e AVCodec… Existem outras estruturas para outros usos. Por exemplo, a biblioteca libavdevice permite o uso de outros dispositivos que não apenas arquivos — você pode abrir vídeos de um capturador de vídeos via USB, por exemplo… Já a libavfilter, que usaremos depois, permite executar filtergraphs, como nas opções -vf, -filter_complex ou -af, do ffmpeg.

Instalando os pacotes necessários

No Debian/Ubuntu:

$ sudo apt install libavformat-dev \
                   libavcodec-dev \
                   libavfilter-dev \
                   libavresample-dev \
                   libavutil-dev \
                   libswresample-dev \
                   libswscale-dev

Agora temos todos os headers e shared objects instalados no sistema e disponíveis para o GCC (ou CLANG).
Como pode ser visto abaixo, os headers estão em subdiretórios nos diretórios padrão dos headers do GCC… Os shared objects podem ser informados diretamente na linkagem como, por exemplo, -lavformat. Veremos como linkar mais tarde, mas é bom saber, também, que podemos obter esses dados via pkg-config:

$ pkg-config --list-all | grep ^libav
libavformat                    libavformat - FFmpeg container format library
libavcodec                     libavcodec - FFmpeg codec library
libavfilter                    libavfilter - FFmpeg audio/video filtering library
libavdevice                    libavdevice - FFmpeg device handling library
libavresample                  libavresample - Libav audio resampling library
libavutil                      libavutil - FFmpeg utility library

$ pkg-config --libs libavformat libavcodec libavutil
-lavformat -lavcodec -lavutil

Registrando todos os formatos e codecs e abrindo o container

Eis um início de código simples:

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
...
int main( int argc, char *argv[] )
{
  AVFormatContext *fmtctx;
  ...

  // Necessário se não quisermos selecionar, "manualmente",
  // os formatos e codecs que usaremos.
  av_register_all();  // registra todos os formatos.
  avcodec_register_all(); // registra todos os codecs.

  ...
  if ( ! ( fmtctx = avformat_alloc_context() ) )
  {
    //... processa erro de alocação de contexto aqui (aborta?).
  }

  // abre arquivo de entrada (nome em argv[1]).
  if ( avformat_open_input( &fmtctx, argv[1], NULL, NULL ) )
  {
    //... processa erro de abertura do arquivo... (aborta?).
  }

  // Obtém info dos streams do container
  if ( avformat_find_stream_info( &fmtctx, NULL ) < 0 )
  {
    //... processa erro de obtenção de info de streams... (aborta?).
  }

  ...
}

Até esse ponto estamos pronto para começar a processar os streams… Note que só lidamos com o container (formato) até agora. É útil conhecer a estrutura AVFormat porque lidaremos com alguns membros em outro artigo:

typedef struct AVFormatContext {
    const AVClass *av_class;
    struct AVInputFormat *iformat;
    struct AVOutputFormat *oformat;
    void *priv_data;
    AVIOContext *pb;
    int ctx_flags;

    unsigned int nb_streams;
    AVStream **streams;

    char filename[1024];
    int64_t start_time;
    int64_t duration;
    int64_t bit_rate;
    unsigned int packet_size;
    int max_delay;
    int flags;
    int64_t probesize;
    int64_t max_analyze_duration;
    const uint8_t *key;
    int keylen;

    unsigned int nb_programs;
    AVProgram **programs;

    enum AVCodecID video_codec_id;
    enum AVCodecID audio_codec_id;
    enum AVCodecID subtitle_codec_id;
    unsigned int max_index_size;
    unsigned int max_picture_buffer;

    unsigned int nb_chapters;
    AVChapter **chapters;

    AVDictionary *metadata;
    int64_t start_time_realtime;
    int fps_probe_size;
    int error_recognition;
    AVIOInterruptCB interrupt_callback;
    int debug;
    int64_t max_interleave_delta;
    int strict_std_compliance;
    int event_flags;
    int max_ts_probe;
    int avoid_negative_ts;
    int ts_id;
    int audio_preload;
    int max_chunk_duration;
    int max_chunk_size;
    int use_wallclock_as_timestamps;
    int avio_flags;
    enum AVDurationEstimationMethod duration_estimation_method;
    int64_t skip_initial_bytes;
    unsigned int correct_ts_overflow;
    int seek2any;
    int flush_packets;
    int probe_score;
    int format_probesize;
    char *codec_whitelist;
    char *format_whitelist;
    AVFormatInternal *internal;
    int io_repositioned;
    AVCodec *video_codec;
    AVCodec *audio_codec;
    AVCodec *subtitle_codec;
    AVCodec *data_codec;
    int metadata_header_padding;
    void *opaque;
    av_format_control_message control_message_cb;
    int64_t output_ts_offset;
    uint8_t *dump_separator;
    enum AVCodecID data_codec_id;
#if FF_API_OLD_OPEN_CALLBACKS
    attribute_deprecated
    int (*open_cb)(struct AVFormatContext *s, \
                   AVIOContext **p, \
                   const char *url, \
                   int flags, \
                   const AVIOInterruptCB *int_cb, \
                   AVDictionary **options);
#endif
    char *protocol_whitelist;
    int (*io_open)(struct AVFormatContext *s, \
                   AVIOContext **pb, \
                   const char *url,
                   int flags, \
                   AVDictionary **options);
    void (*io_close)(struct AVFormatContext *s, \
                     AVIOContext *pb);
    char *protocol_blacklist;
    int max_streams;
} AVFormatContext;

Tem muita informação ai e todas podem ser obtidas do header avformat.h ou da documentação oficial da libav (aqui), mas repare nos membros streams e nb_streams. Uma vez obtidos os streams via chamada a avformat_find_stream_info() poderemos começar a lidar com os decodecs/encodecs (codecs) e os pacotes… Deixo isso pro próximo texto.