Streaming made easy: convert videos to HLS and MPEG-DASH with Ruby
Delivering video content efficiently requires adaptive streaming formats like HLS (HTTP Live Streaming) and MPEG-DASH. In this guide, we demonstrate how to convert videos to these formats using Ruby, FFmpeg, and the open-source Streamio FFMPEG gem. By automating video conversion tasks, you can enhance streaming quality across diverse devices and network conditions.
Introduction to HLS and MPEG-DASH
HLS is a streaming protocol developed by Apple that segments video content into small, HTTP-based files, enabling adaptive bitrate streaming. MPEG-DASH (Dynamic Adaptive Streaming over HTTP) is a similar standard that adjusts video quality dynamically based on the user's network conditions.
Implementing these formats improves the viewer's experience by reducing buffering and optimizing video quality. Ensuring smooth streaming across various devices and networks is essential for modern applications.
Setting up your Ruby environment for video conversion
Use Ruby 3.2 or later, FFmpeg with the libx264 and aac encoders, and streamio-ffmpeg 3.0.2.
The examples below use a 16:9 input with both video and audio.
Ruby 4 moved logger from a default gem to a bundled gem.
List it explicitly in your Gemfile so streamio-ffmpeg can require it when running under Bundler.
Verify your Ruby installation:
ruby -v
If you need to install or upgrade Ruby, use a version manager like RVM or rbenv.
Next, install FFmpeg, which is essential for processing video files.
For macOS users:
brew install ffmpeg
For Ubuntu/Debian users:
sudo apt-get update
sudo apt-get install ffmpeg
Create a new project directory and initialize it:
mkdir video_conversion
cd video_conversion
bundle init
Add the required gem to your Gemfile:
source 'https://rubygems.org'
gem 'streamio-ffmpeg', '~> 3.0.2'
gem 'logger'
Alternatively, install the gem directly via the command line:
gem install streamio-ffmpeg -v 3.0.2
gem install logger
Install the dependencies:
bundle install
Introduction to FFmpeg and the Streamio FFmpeg gem
FFmpeg is a robust open-source tool for multimedia manipulation, allowing you to record, convert, and stream audio and video files. The Streamio FFMPEG gem offers a Ruby interface to FFmpeg, simplifying video processing integration in your applications.
Converting videos to HLS format in Ruby
Below is a Ruby class to handle video conversion to HLS and DASH. It uses the gem for input inspection and encoder options, then runs FFmpeg with an argument array and checks its exit status. Each conversion creates a new output directory; an existing directory is rejected to protect earlier playlists and segments.
Create a new file called 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
Converting videos to MPEG-DASH in Ruby
The to_dash method above maps the input video twice, creating 720p and 360p representations, plus
one shared audio representation. Both video encodes use the same frame rate and force keyframes
every ten seconds, so the segment boundaries align. HLS uses the same keyframe schedule for its
three renditions.
Using the converter
Create a script to utilize the VideoConverter class. This example demonstrates converting a video
file to both HLS and MPEG-DASH formats:
# 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
Testing the streams
To test the generated streams, save the following page as player.html beside the output
directory and serve that directory over HTTP. Video.js 8 includes HTTP Streaming support for HLS and
DASH. The first supported source is selected; remove the HLS source temporarily to test 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>
Advanced error handling
For production environments, propagate conversion failures to the calling job. For example:
begin
VideoConverter.new('input.mp4').to_hls('output/new-hls')
rescue StandardError => e
warn "Conversion failed: #{e.message}"
exit 1
end
In addition to error logging, monitor system resources and implement retry mechanisms for failed conversions. A failed run can leave partial files in its newly created directory. Use a fresh directory when retrying, and publish playlists only after the entire conversion succeeds.
Common challenges and solutions
Container format support
Ensure your input videos use widely supported container formats such as MP4 (H.264) for optimal compatibility. While FFmpeg supports diverse formats, H.264 in an MP4 container offers the best streaming performance.
Performance optimization
For large-scale video processing:
- Process videos asynchronously using background jobs.
- Utilize lower resolutions for preview generation.
- Implement caching for frequently accessed segments.
- Monitor system resources and adjust batch sizes accordingly.
Robust error handling
Include comprehensive error handling in your production code. Validate input files, monitor output directories for disk space, and log detailed error messages to ease debugging.
Conclusion
Automating video conversion with Ruby, FFmpeg, and the Streamio FFMPEG gem simplifies adaptive streaming with HLS and MPEG-DASH. By following these updated best practices and incorporating robust error handling, you can improve video delivery across devices and network conditions.
For a scalable and robust solution to handle video encoding and streaming, consider using Transloadit's video encoding service, which streamlines these conversions and enhances your workflow.
