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…