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

Start by confirming your Ruby version. Although Ruby 2.0 works, we recommend using Ruby 3.2.3 or later for improved performance and security.

Below is the compatibility matrix for the required components:

Component Minimum Version Recommended Version
Ruby 2.0 3.2.3+
FFmpeg 2.8.4 6.1.1+
streamio-ffmpeg 3.0.2 3.0.2

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'

Alternatively, install the gem directly via the command line:

gem install streamio-ffmpeg

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. Note that the code includes enhanced error handling and updated transcoding options for better reliability.

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(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: [
            '-hls_time', '10',
            '-hls_playlist_type', 'vod',
            '-hls_segment_filename', "#{output_dir}/#{variant[:resolution]}_%03d.ts"
          ]
        }

        @movie.transcode("#{output_dir}/#{variant[:resolution]}.m3u8", options) do |progress|
          puts "Progress for #{variant[:resolution]}: #{(progress * 100).round}%"
        end
      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(output_dir)

    begin
      options = {
        video_codec: 'libx264',
        audio_codec: 'aac',
        frame_rate: 30,
        custom: [
          '-use_template', '1',
          '-use_timeline', '1',
          '-seg_duration', '10',
          '-adaptation_sets', 'id=0,streams=v id=1,streams=a',
          '-f', 'dash'
        ]
      }

      @movie.transcode("#{output_dir}/manifest.mpd", options) do |progress|
        puts "DASH conversion progress: #{(progress * 100).round}%"
      end
    rescue FFMPEG::Error => e
      raise "DASH conversion error: #{e.message}"
    rescue StandardError => e
      raise "General error: #{e.message}"
    end
  end

  private

  def generate_master_playlist(output_dir, variants)
    master_playlist = "#EXTM3U\n#EXT-X-VERSION:3\n"

    variants.each do |variant|
      bandwidth = variant[:video_bitrate].to_i * 1000
      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

Enhance the VideoConverter class to support MPEG-DASH conversion. The method below demonstrates the process:

def to_dash(output_dir)
  FileUtils.mkdir_p(output_dir)

  begin
    options = {
      video_codec: 'libx264',
      audio_codec: 'aac',
      frame_rate: 30,
      custom: [
        '-use_template', '1',
        '-use_timeline', '1',
        '-seg_duration', '10',
        '-adaptation_sets', 'id=0,streams=v id=1,streams=a',
        '-f', 'dash'
      ]
    }

    @movie.transcode("#{output_dir}/manifest.mpd", options) do |progress|
      puts "DASH conversion progress: #{(progress * 100).round}%"
    end
  rescue FFMPEG::Error => e
    raise "DASH conversion error: #{e.message}"
  rescue StandardError => e
    raise "General error: #{e.message}"
  end
end

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, use Video.js, a popular HTML5 video player:

<!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, ensure robust error handling. Wrap transcoding operations in detailed error checks to capture and log issues. For example:

begin
  movie = FFMPEG::Movie.new(input_file)
  raise "Invalid file" unless movie.valid?
  movie.transcode(output_file, options) do |progress|
    puts "Progress: #{(progress * 100).round}%"
  end
rescue FFMPEG::Error => e
  puts "Transcoding error: #{e.message}"
rescue StandardError => e
  puts "General error: #{e.message}"
end

In addition to error logging, monitor system resources and implement retry mechanisms for failed conversions.

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:

  1. Process videos asynchronously using background jobs.
  2. Utilize lower resolutions for preview generation.
  3. Implement caching for frequently accessed segments.
  4. 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.