Grave legendas na imagem dos vídeos com Lua e FFmpeg
Use Lua para executar o FFmpeg e gravar um arquivo SRT existente na imagem de um vídeo. O script abaixo produz um MP4 com legendas gravadas na imagem e áudio copiado. Um vídeo de teste gerado permite conferir o texto e a sincronização antes de usar seus próprios arquivos.
Legendas gravadas na imagem se tornam pixels do vídeo. Quem assiste não pode desativá-las nem selecionar outro idioma, e leitores de tela não conseguem lê-las como uma faixa de texto. Mantenha o SRT separado se o seu player precisar de legendas selecionáveis. Este exemplo renderiza o texto fornecido; ele não transcreve nem traduz áudio.
Confira os pré-requisitos no Linux
Use um shell POSIX, /dev/fd, Lua 5.4 ou 5.5 e FFmpeg com o filtro
subtitles da libass e o codificador libx264.
Este guia foi feito para Linux. Os comandos abaixo instalam os pré-requisitos no Ubuntu 24.04;
outras compilações dos pacotes podem oferecer recursos diferentes do FFmpeg. A verificação de
decodificação também usa mktemp, cat e
rm do pacote coreutils comum no Linux.
O exemplo foi testado com Lua 5.4.6 e FFmpeg 6.1.1 do Ubuntu, e com Lua 5.5.1 e FFmpeg 9.0.1. Use pacotes que ainda recebem manutenção em vez de instalar uma versão antiga só para corresponder às versões testadas.
Instale Lua, FFmpeg e uma fonte com os caracteres usados neste exemplo:
sudo apt-get update && sudo apt-get install lua5.4 ffmpeg fonts-dejavu-core
Confira o executável chamado lua antes de criar arquivos. Se a sua
distribuição fornecer apenas lua5.4, use esse nome em cada chamada ao Lua
abaixo. A documentação do filtro subtitles do FFmpeg explica o
requisito da libass.
lua -v &&
ffmpeg -hide_banner -h filter=subtitles &&
ffmpeg -hide_banner -h encoder=libx264 &&
ffprobe -version &&
command -v mktemp && command -v cat && command -v rm
Pare se o filtro ou o codificador for desconhecido. Lua executa o comando do shell; o FFmpeg faz a decodificação, a renderização e a codificação. Nenhuma biblioteca de mídia para Lua é necessária.
Crie um vídeo e legendas sincronizadas
Trabalhe em um novo diretório privado, sem processos escrevendo nele simultaneamente. Gere um vídeo preto de 8,25 segundos com um tom de teste de 660 Hz e áudio AAC. O tamanho reduzido dos quadros e o número limitado de threads do codificador mantêm baixo o custo desta verificação:
(
mkdir lua-subtitles-demo && cd lua-subtitles-demo &&
ffmpeg -nostdin -n -f lavfi -i 'color=c=black:s=640x360:r=24:d=8.25' \
-f lavfi -i 'sine=frequency=660:sample_rate=48000:duration=8.25' \
-c:v libx264 -threads 2 -pix_fmt yuv420p -c:a aac -shortest input.mp4
)
Quando esse comando for concluído com sucesso, entre no novo diretório para as etapas restantes:
cd lua-subtitles-demo
Salve o conteúdo a seguir como subtitles.srt em UTF-8. Os timestamps do SRT usam
uma vírgula antes dos milissegundos; linhas em branco separam as entradas de legenda. A palavra
acentuada serve para verificar se a fonte e a codificação são compatíveis.
1
00:00:01,000 --> 00:00:02,000
Hello from Lua: café!
2
00:00:04,000 --> 00:00:05,000
The second subtitle.
Se a preparação parar no meio, mantenha os arquivos que ela criou. A opção
-n do FFmpeg recusa um input.mp4 existente.
Inspecione-o antes de tentar novamente ou recomece em outro diretório novo; não repita o comando
mkdir dentro do primeiro diretório.
Salve e execute o programa Lua
Salve este conteúdo como add_subtitles.lua no mesmo diretório do vídeo e do SRT.
Ele coloca os argumentos do shell entre aspas e passa o arquivo de legendas pelo descritor 3,
de modo que o nome do arquivo nunca faça parte da sintaxe de filtros do FFmpeg. Isso permite lidar
com espaços, apóstrofos, dois-pontos, colchetes e hífens iniciais em nomes de arquivos locais.
#!/usr/bin/env lua
if #arg ~= 0 and #arg ~= 3 then
io.stderr:write("Usage: lua add_subtitles.lua <video.mp4> <subtitles.srt> <new-output.mp4>\n")
os.exit(1)
end
local video_file = arg[1] or "input.mp4"
local subtitles_file = arg[2] or "subtitles.srt"
local output_file = arg[3] or "output.mp4"
local existing = io.open(output_file, "rb")
if existing then
existing:close()
io.stderr:write("Output already exists; choose a new filename.\n")
os.exit(1)
end
local function shell_quote(value)
return "'" .. value:gsub("'", "'\\''") .. "'"
end
local function local_path(value)
if value:sub(1, 1) == "/" then return value end
return "./" .. value
end
local filter = "subtitles=/dev/fd/3"
local command = string.format(
"ffmpeg -nostdin -n -filter_threads 1 -i %s -vf %s -c:v libx264 -threads 2 -crf 23 -c:a copy %s 3<%s",
shell_quote(local_path(video_file)),
shell_quote(filter),
shell_quote(local_path(output_file)),
shell_quote(local_path(subtitles_file))
)
local success = os.execute(command)
if not success then
io.stderr:write("Subtitle conversion failed. Inspect any partial output before retrying.\n")
os.exit(1)
end
print("Subtitles added successfully.")
Execute-o com o vídeo, o arquivo de legendas e um novo nome de arquivo de saída, nessa ordem:
lua add_subtitles.lua input.mp4 subtitles.srt output.mp4
Depois que o FFmpeg termina, o programa exibe:
Subtitles added successfully.
O programa informa como erro a existência de um arquivo de saída, e -n
instrui o FFmpeg a não substituí-lo. Ele usa o
status do comando do shell retornado pelo Lua para não exibir a
mensagem de sucesso quando o FFmpeg falha. Isso não é uma publicação transacional: uma conversão que
falha pode deixar um novo arquivo parcial, e a verificação de existência não coordena processos
que escrevem simultaneamente. Inspecione qualquer saída parcial e escolha um novo caminho de saída
antes de tentar novamente.
Confira o resultado visual e o áudio
Abra output.mp4 no seu player de vídeo. Você deve ouvir o tom durante todo o
vídeo, ver “Hello from Lua: café!” de 1 a 2 segundos e ver “The second subtitle.” de 4 a 5 segundos.
O fundo deve ficar preto, sem texto fora desses intervalos. Confira o acento e a sincronização;
a codificação bem-sucedida, por si só, não comprova que as legendas estão legíveis.
Inspecione os fluxos e decodifique toda a saída como uma verificação adicional:
ffprobe -v error -show_entries stream=codec_type,codec_name -of compact output.mp4 &&
(
decode_log=$(mktemp) || exit 1
trap 'rm -f "$decode_log"' EXIT
if ffmpeg -nostdin -v error -xerror -i output.mp4 -map 0:v:0 -map 0:a:0 -f null - \
2>"$decode_log" && [ ! -s "$decode_log" ]; then
printf 'Full decode finished without errors.\n'
else
cat "$decode_log" >&2
false
fi
)
Para a amostra de teste gerada, o resultado esperado é vídeo H.264, áudio AAC e a mensagem
“Full decode finished without errors.” O subshell verifica a saída de erros do FFmpeg e também seu
status: algumas compilações relatam um erro de decodificação mesmo retornando status zero.
Ele remove o log temporário e mantém o shell que o chamou em execução, seja qual for o resultado.
Essa verificação não avalia o texto nem a sincronização das legendas; mantenha a verificação visual.
Com sua própria mídia, use um vídeo local válido e um codec de áudio compatível com MP4, pois
-c:a copy copia o fluxo de áudio selecionado sem convertê-lo.
A cópia também significa que o comando Lua pode terminar com um pacote de áudio danificado ainda presente na saída. A decodificação completa acima pode revelar esse dano mesmo quando a listagem dos fluxos parece correta. A mensagem de sucesso, por si só, não é uma verificação de integridade.
Atrase as legendas ou escolha uma fonte
Para atrasar cada entrada de legenda em 2,5 segundos, crie um SRT separado com os tempos deslocados
e passe esse arquivo para o mesmo programa. A
opção -itsoffset do FFmpeg adiciona o deslocamento
aos timestamps de entrada:
ffmpeg -nostdin -n -itsoffset 2.5 -i subtitles.srt -c:s srt shifted.srt &&
lua add_subtitles.lua input.mp4 shifted.srt delayed.mp4
Em delayed.mp4, a primeira entrada deve aparecer de 3,5 a 4,5 segundos, e a
segunda, de 6,5 a 7,5 segundos. Não deve haver texto em 1,5 segundos. Abra
shifted.srt para conferir os timestamps alterados se o resultado ainda seguir a
sincronização original.
Para fixar a fonte e o tamanho, substitua apenas a variável filter no
programa salvo por esta linha. A fonte DejaVu Sans precisa estar instalada:
local filter = "subtitles=/dev/fd/3:force_style='FontName=DejaVu Sans,FontSize=24'"
Execute o programa alterado com um novo nome de arquivo de saída:
lua add_subtitles.lua input.mp4 subtitles.srt styled.mp4
Mantenha essa string de estilo fixa no script. Expressões de filtro arbitrárias fornecidas por
usuários precisam de validação própria; colocar argumentos do shell entre aspas não valida a
sintaxe de filtros do FFmpeg. O FFmpeg documenta as configurações de
force_style na mesma referência do filtro subtitles vinculada acima.
Use seus próprios arquivos e diagnostique falhas
Passe seus próprios caminhos como os três argumentos. Os originais continuam sendo entradas;
escolha uma saída separada com a extensão .mp4. Arquivos ausentes, um SRT
inválido e áudio que não pode ser copiado para MP4 devem produzir um status de saída diferente de
zero. Leia o diagnóstico do FFmpeg antes de tentar novamente. Este pequeno wrapper não valida cada
pacote nem cada entrada de legenda; decodificadores podem recuperar mídia danificada, então compare
o resultado com o original até o fim da gravação.
Se os caracteres acentuados forem renderizados incorretamente, confirme que o SRT está em UTF-8 e que a fonte escolhida contém esses caracteres. Converta uma codificação legada conhecida para um arquivo separado em vez de sobrescrever a origem:
iconv -f ISO-8859-1 -t UTF-8 legacy.srt > converted.srt
Use um destino ainda não utilizado para esse redirecionamento, que, caso contrário, pode sobrescrever um arquivo mesmo se a conversão falhar. Não passe uploads não confiáveis diretamente para este comando local: execute o FFmpeg em um processo de trabalho isolado, com limites de acesso ao sistema de arquivos e à rede, de memória e de tempo de execução. Colocar argumentos do shell entre aspas não cria um ambiente isolado para processar mídia.
