Incrusta subtítulos en videos con Lua y FFmpeg
Usa Lua para ejecutar FFmpeg e incrustar un archivo SRT existente en un video. El siguiente script produce un MP4 con subtítulos incrustados y audio copiado. Un video de prueba generado te permite revisar el texto y los tiempos antes de usar tus propios archivos.
Los subtítulos incrustados se convierten en píxeles del video. Los espectadores no pueden desactivarlos ni seleccionar otro idioma, y los lectores de pantalla no pueden leerlos como una pista de texto. Conserva el SRT por separado si tu reproductor necesita subtítulos seleccionables. Este ejemplo renderiza el texto proporcionado; no transcribe ni traduce el audio.
Verifica los requisitos previos de Linux
Usa un shell POSIX, /dev/fd, Lua 5.4 o 5.5, y FFmpeg con el filtro
subtitles de libass y el codificador libx264.
Esta guía está orientada a Linux. Los siguientes comandos instalan los requisitos en Ubuntu 24.04;
otras compilaciones de los paquetes pueden ofrecer funciones de FFmpeg diferentes. La prueba de
decodificación también usa mktemp, cat y
rm del paquete coreutils habitual de Linux.
El ejemplo se probó con Lua 5.4.6 y FFmpeg 6.1.1 de Ubuntu, y con Lua 5.5.1 y FFmpeg 9.0.1. Usa paquetes que reciban mantenimiento en lugar de instalar una versión antigua solo para coincidir con las versiones probadas.
Instala Lua, FFmpeg y una fuente que contenga los caracteres usados en este ejemplo:
sudo apt-get update && sudo apt-get install lua5.4 ffmpeg fonts-dejavu-core
Verifica el ejecutable llamado lua antes de crear archivos. Si tu
distribución solo proporciona lua5.4, usa ese nombre en cada invocación
de Lua que aparece a continuación. La
documentación del filtro subtitles de FFmpeg explica el requisito
de 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
Detente si no se reconoce el filtro o el codificador. Lua ejecuta el comando del shell; FFmpeg se encarga de la decodificación, el renderizado y la codificación. No se necesita una biblioteca multimedia de Lua.
Crea un video y subtítulos sincronizados
Trabaja en un directorio privado nuevo sin procesos que escriban en él simultáneamente. Genera un video negro de 8,25 segundos con un tono de prueba de 660 Hz y audio AAC. El tamaño reducido de los fotogramas y el número limitado de hilos del codificador mantienen bajo el costo computacional de esta prueba:
(
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
)
Cuando ese comando termine correctamente, entra en el directorio nuevo para los pasos restantes:
cd lua-subtitles-demo
Guarda lo siguiente como subtitles.srt en UTF-8. Las marcas de tiempo SRT usan
una coma antes de los milisegundos; las líneas en blanco separan las entradas. La palabra con
acento permite comprobar que la fuente y la codificación de caracteres sean compatibles.
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.
Si la preparación se detiene a mitad del proceso, conserva los archivos creados. La opción
-n de FFmpeg rechaza un input.mp4 existente.
Inspecciónalo antes de volver a intentarlo, o comienza de nuevo en otro directorio nuevo; no
repitas el comando mkdir dentro del primero.
Guarda y ejecuta el programa de Lua
Guarda esto como add_subtitles.lua junto al video y al SRT. El programa entrecomilla
los argumentos del shell y pasa el archivo de subtítulos a través del descriptor 3, de modo que
su nombre nunca forma parte de la sintaxis de filtros de FFmpeg. Así se admiten espacios,
apóstrofos, dos puntos, corchetes y guiones iniciales en los nombres de archivos locales.
#!/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.")
Ejecútalo con el video, el archivo de subtítulos y un nombre de archivo de salida nuevo, en ese orden:
lua add_subtitles.lua input.mp4 subtitles.srt output.mp4
Cuando FFmpeg termina, el programa imprime:
Subtitles added successfully.
El programa informa de un error si la salida ya existe, y -n indica a
FFmpeg que no la reemplace. Usa el estado del comando del shell
de Lua para no mostrar el mensaje de éxito cuando FFmpeg falla. No se trata de una publicación
transaccional: una conversión fallida puede dejar un archivo nuevo incompleto, y la comprobación
de existencia no coordina los procesos que escriben simultáneamente. Inspecciona cualquier salida
incompleta y elige una ruta de salida nueva antes de volver a intentarlo.
Revisa el resultado visible y el audio
Abre output.mp4 en tu reproductor de video. Deberías escuchar el tono durante
toda la reproducción, ver «Hello from Lua: café!» de 1 a 2 segundos y, de 4 a 5 segundos, ver
«The second subtitle.». Fuera de esos intervalos, el fondo debería ser negro y no mostrar texto.
Revisa tanto el acento como los tiempos; que la codificación termine correctamente no demuestra
por sí solo que los subtítulos sean legibles.
Inspecciona los flujos y decodifica toda la salida como prueba 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 el archivo de prueba generado, deberías obtener video H.264, audio AAC y el mensaje
«Full decode finished without errors.». El subshell comprueba tanto la salida de errores de
FFmpeg como su estado: algunas compilaciones informan de un error del decodificador aunque
devuelvan un estado cero. Elimina su registro temporal y mantiene en ejecución el shell desde el
que lo invocaste, independientemente del resultado. Esta prueba no evalúa el texto ni los tiempos
de los subtítulos; conserva la revisión visual. Con tus propios archivos multimedia, usa un video
local válido y un códec de audio compatible con MP4, porque -c:a copy copia
el flujo de audio seleccionado sin convertirlo.
La copia también implica que el comando de Lua puede terminar aunque quede un paquete de audio dañado en la salida. La decodificación completa anterior puede revelar ese daño incluso cuando la lista de flujos parece correcta. El mensaje de éxito por sí solo no es una prueba de integridad.
Retrasa los subtítulos o elige una fuente
Para retrasar cada entrada 2,5 segundos, crea un SRT separado con los tiempos desplazados y pasa
ese archivo al mismo programa. La opción -itsoffset
de FFmpeg suma el desplazamiento a las marcas de tiempo 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
En delayed.mp4, la primera entrada debería aparecer de 3,5 a 4,5 segundos y la
segunda de 6,5 a 7,5 segundos. No debería haber texto a los 1,5 segundos. Abre
shifted.srt para revisar las marcas de tiempo modificadas si el resultado
sigue usando los tiempos originales.
Para fijar la fuente y el tamaño, reemplaza solo la variable filter del
programa guardado por esta línea. DejaVu Sans debe estar instalada:
local filter = "subtitles=/dev/fd/3:force_style='FontName=DejaVu Sans,FontSize=24'"
Ejecuta el programa modificado con un nombre de archivo de salida nuevo:
lua add_subtitles.lua input.mp4 subtitles.srt styled.mp4
Mantén fija esta cadena de estilo en el script. Las expresiones de filtro arbitrarias
proporcionadas por usuarios requieren su propia validación; entrecomillar los argumentos del
shell no valida la sintaxis de filtros de FFmpeg. FFmpeg documenta los ajustes de
force_style en la misma referencia del filtro subtitles enlazada anteriormente.
Usa tus propios archivos y diagnostica los fallos
Pasa tus propias rutas como los tres argumentos. Los originales siguen siendo entradas; elige una
salida separada con la extensión .mp4. Los archivos faltantes, un SRT
no válido y el audio que no se pueda copiar en MP4 deberían producir un estado de salida distinto
de cero. Lee el diagnóstico de FFmpeg antes de volver a intentarlo. Este pequeño programa
auxiliar no valida cada paquete ni cada entrada de subtítulos; los decodificadores pueden
recuperar contenido multimedia dañado, así que compara el resultado con el original hasta el
final de la grabación.
Si los caracteres acentuados se renderizan incorrectamente, confirma que el SRT esté en UTF-8 y que la fuente elegida los contenga. Convierte una codificación de caracteres antigua conocida a un archivo separado en lugar de sobrescribir el original:
iconv -f ISO-8859-1 -t UTF-8 legacy.srt > converted.srt
Usa un destino que no exista para esa redirección, ya que, de lo contrario, puede sobrescribir un archivo incluso si la conversión falla. No pases archivos subidos no confiables directamente a este comando local: ejecuta FFmpeg en un proceso de trabajo aislado con límites de sistema de archivos, red, memoria y tiempo de ejecución. Entrecomillar los argumentos del shell no crea un entorno aislado para el procesamiento multimedia.
