Streaming fácil: convierte videos a HLS y MPEG-DASH con Ruby
Entregar contenido de video de forma eficiente requiere formatos de streaming adaptativo como HLS (HTTP Live Streaming) y MPEG-DASH. En esta guía mostramos cómo convertir videos a estos formatos con Ruby, FFmpeg y la gema Streamio FFMPEG de código abierto. Al automatizar las tareas de conversión de video, puedes mejorar la calidad del streaming en diversos dispositivos y condiciones de red.
Introducción a HLS y MPEG-DASH
HLS es un protocolo de streaming desarrollado por Apple que segmenta el contenido de video en archivos pequeños basados en HTTP y habilita el streaming adaptativo de bitrate. MPEG-DASH (Dynamic Adaptive Streaming over HTTP) es un estándar similar que ajusta la calidad del video de forma dinámica según las condiciones de red del usuario.
Implementar estos formatos mejora la experiencia del espectador al reducir el buffering y optimizar la calidad del video. Garantizar un streaming fluido en distintos dispositivos y redes es esencial para las aplicaciones modernas.
Configurar tu entorno de Ruby para la conversión de video
Usa Ruby 3.2 o posterior, FFmpeg con los encoders libx264 y aac, y streamio-ffmpeg 3.0.2.
Los ejemplos siguientes usan una entrada 16:9 con video y audio.
Ruby 4 pasó logger de gema predeterminada a gema empaquetada.
Decláralo de forma explícita en tu Gemfile para que streamio-ffmpeg pueda requerirlo al ejecutarse con Bundler.
Verifica tu instalación de Ruby:
ruby -v
Si necesitas instalar o actualizar Ruby, usa un gestor de versiones como RVM o rbenv.
A continuación, instala FFmpeg, que es esencial para procesar archivos de video.
Para usuarios de macOS:
brew install ffmpeg
Para usuarios de Ubuntu/Debian:
sudo apt-get update
sudo apt-get install ffmpeg
Crea un nuevo directorio de proyecto e inicialízalo:
mkdir video_conversion
cd video_conversion
bundle init
Agrega la gema requerida a tu Gemfile:
source 'https://rubygems.org'
gem 'streamio-ffmpeg', '~> 3.0.2'
gem 'logger'
Como alternativa, instala la gema directamente desde la línea de comandos:
gem install streamio-ffmpeg -v 3.0.2
gem install logger
Instala las dependencias:
bundle install
Introducción a FFmpeg y a la gema Streamio FFmpeg
FFmpeg es una herramienta robusta de código abierto para manipular contenido multimedia que te permite grabar, convertir y transmitir archivos de audio y video. La gema Streamio FFMPEG ofrece una interfaz de Ruby para FFmpeg y simplifica la integración del procesamiento de video en tus aplicaciones.
Convertir videos al formato HLS en Ruby
A continuación se muestra una clase de Ruby para gestionar la conversión de video a HLS y DASH. Usa la gema para inspeccionar la entrada y definir las opciones del encoder, luego ejecuta FFmpeg con un array de argumentos y comprueba su estado de salida. Cada conversión crea un nuevo directorio de salida; si el directorio ya existe, se rechaza para proteger las listas de reproducción y los segmentos anteriores.
Crea un nuevo archivo llamado video_converter.rb:
require 'streamio-ffmpeg'
require 'fileutils'
class VideoConverter
def initialize(input_file)
begin
@movie = FFMPEG::Movie.new(input_file)
raise "Invalid file" unless @movie.valid?
@input_file = input_file
rescue FFMPEG::Error => e
raise "FFmpeg error: #{e.message}"
rescue StandardError => e
raise "Error initializing converter: #{e.message}"
end
end
def to_hls(output_dir)
FileUtils.mkdir_p(File.dirname(output_dir))
Dir.mkdir(output_dir)
variants = [
{ resolution: '1280x720', video_bitrate: '2500k', audio_bitrate: '128k' },
{ resolution: '854x480', video_bitrate: '1500k', audio_bitrate: '96k' },
{ resolution: '640x360', video_bitrate: '800k', audio_bitrate: '64k' }
]
begin
variants.each do |variant|
options = {
video_codec: 'libx264',
audio_codec: 'aac',
frame_rate: 30,
resolution: variant[:resolution],
video_bitrate: variant[:video_bitrate],
audio_bitrate: variant[:audio_bitrate],
custom: [
'-map', '0:v:0', '-map', '0:a:0',
'-g', '300', '-keyint_min', '300', '-sc_threshold', '0',
'-force_key_frames', 'expr:gte(t,n_forced*10)',
'-hls_time', '10',
'-hls_playlist_type', 'vod',
'-hls_segment_filename', "#{output_dir}/#{variant[:resolution]}_%03d.ts"
]
}
transcode("#{output_dir}/#{variant[:resolution]}.m3u8", options)
end
generate_master_playlist(output_dir, variants)
rescue FFMPEG::Error => e
raise "Transcoding error: #{e.message}"
rescue StandardError => e
raise "General error: #{e.message}"
end
end
def to_dash(output_dir)
FileUtils.mkdir_p(File.dirname(output_dir))
Dir.mkdir(output_dir)
begin
options = {
video_codec: 'libx264',
audio_codec: 'aac',
frame_rate: 30,
custom: [
'-map', '0:v:0', '-map', '0:v:0', '-map', '0:a:0',
'-s:v:0', '1280x720', '-b:v:0', '2500k',
'-s:v:1', '640x360', '-b:v:1', '800k',
'-g', '300', '-keyint_min', '300', '-sc_threshold', '0',
'-force_key_frames', 'expr:gte(t,n_forced*10)',
'-use_template', '1',
'-use_timeline', '1',
'-seg_duration', '10',
'-adaptation_sets', 'id=0,streams=v id=1,streams=a',
'-f', 'dash'
]
}
transcode("#{output_dir}/manifest.mpd", options)
rescue FFMPEG::Error => e
raise "DASH conversion error: #{e.message}"
rescue StandardError => e
raise "General error: #{e.message}"
end
end
private
def transcode(output_file, options)
arguments = FFMPEG::EncodingOptions.new(options).to_a
system(FFMPEG.ffmpeg_binary, '-nostdin', '-n', '-i', @input_file,
*arguments, output_file, exception: true)
end
def generate_master_playlist(output_dir, variants)
master_playlist = "#EXTM3U\n#EXT-X-VERSION:3\n"
variants.each do |variant|
# Measure the multiplexed segments, including audio and container overhead.
duration = nil
rates = []
File.foreach("#{output_dir}/#{variant[:resolution]}.m3u8") do |line|
if line.start_with?('#EXTINF:')
duration = Float(line.delete_prefix('#EXTINF:').split(',').first)
elsif !line.start_with?('#') && !line.strip.empty?
raise 'Missing segment duration' unless duration && duration.positive?
rates << File.size(File.join(output_dir, line.strip)) * 8 / duration
duration = nil
end
end
raise 'No HLS segments generated' if rates.empty?
bandwidth = rates.max.ceil
master_playlist += <<~PLAYLIST
#EXT-X-STREAM-INF:BANDWIDTH=#{bandwidth},RESOLUTION=#{variant[:resolution]}
#{variant[:resolution]}.m3u8
PLAYLIST
end
File.write("#{output_dir}/master.m3u8", master_playlist)
end
end
Convertir videos a MPEG-DASH en Ruby
El método to_dash anterior mapea el video de entrada dos veces y genera variantes de video de 720p
y 360p, además de una variante de audio compartida. Ambas codificaciones de video usan la misma tasa
de fotogramas y fuerzan keyframes cada diez segundos, de modo que los límites de los segmentos
quedan alineados. HLS usa el mismo esquema de keyframes para sus tres variantes.
Usar el conversor
Crea un script para usar la clase VideoConverter. Este ejemplo muestra cómo convertir un archivo de
video a los formatos HLS y MPEG-DASH:
# Convert.rb
require_relative 'video_converter'
begin
converter = VideoConverter.new('input.mp4')
puts 'Converting to HLS...'
converter.to_hls('output/hls')
puts 'Converting to MPEG-DASH...'
converter.to_dash('output/dash')
puts 'Conversion completed successfully'
rescue StandardError => e
puts "Error: #{e.message}"
exit 1
end
Probar los streams
Para probar los streams generados, guarda la siguiente página como player.html junto al directorio
output y sirve ese directorio por HTTP. Video.js 8 incluye compatibilidad con HTTP Streaming para
HLS y DASH. Se selecciona la primera fuente compatible; elimina temporalmente la fuente HLS para
probar DASH.
<!DOCTYPE html>
<html>
<head>
<title>Video Player</title>
<link href="https://vjs.zencdn.net/8.10.0/video-js.css" rel="stylesheet" />
<script src="https://vjs.zencdn.net/8.10.0/video.min.js"></script>
</head>
<body>
<video
id="player"
class="video-js vjs-default-skin"
controls
preload="auto"
width="640"
height="360"
>
<source src="output/hls/master.m3u8" type="application/x-mpegURL" />
<source src="output/dash/manifest.mpd" type="application/dash+xml" />
</video>
<script>
var player = videojs('player')
</script>
</body>
</html>
Manejo avanzado de errores
En entornos de producción, propaga los errores de conversión al trabajo que hace la llamada. Por ejemplo:
begin
VideoConverter.new('input.mp4').to_hls('output/new-hls')
rescue StandardError => e
warn "Conversion failed: #{e.message}"
exit 1
end
Además de registrar los errores, monitorea los recursos del sistema e implementa mecanismos de reintento para las conversiones fallidas. Una ejecución fallida puede dejar archivos parciales en el directorio que acaba de crear. Usa un directorio nuevo al reintentar y publica las listas de reproducción solo después de que toda la conversión se complete correctamente.
Desafíos comunes y soluciones
Compatibilidad con formatos de contenedor
Asegúrate de que tus videos de entrada usen formatos de contenedor ampliamente compatibles, como MP4 (H.264), para lograr una compatibilidad óptima. Aunque FFmpeg admite formatos diversos, H.264 en un contenedor MP4 ofrece el mejor rendimiento de streaming.
Optimización del rendimiento
Para el procesamiento de video a gran escala:
- Procesa los videos de forma asíncrona con trabajos en segundo plano.
- Usa resoluciones más bajas para generar vistas previas.
- Implementa caché para los segmentos a los que se accede con frecuencia.
- Monitorea los recursos del sistema y ajusta los tamaños de los lotes en consecuencia.
Manejo robusto de errores
Incluye un manejo integral de errores en tu código de producción. Valida los archivos de entrada, vigila el espacio en disco de los directorios de salida y registra mensajes de error detallados para facilitar la depuración.
Conclusión
Automatizar la conversión de video con Ruby, FFmpeg y la gema Streamio FFMPEG simplifica el streaming adaptativo con HLS y MPEG-DASH. Si sigues estas mejores prácticas actualizadas e incorporas un manejo robusto de errores, puedes mejorar la entrega de video en distintos dispositivos y condiciones de red.
Para una solución escalable y robusta que gestione el encoding y el streaming de video, considera usar el servicio de encoding de video de Transloadit, que agiliza estas conversiones y mejora tu flujo de trabajo.
