Skip to Content
Course content

213: Video Transcoding Pipelines in Ruby

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When you start building video pipelines in Ruby, it's easy to fall into the trap of thinking that Ruby is the one doing the heavy lifting. It isn't. Ruby is the orchestrator; you're essentially writing a sophisticated wrapper around a command-line tool—usually FFmpeg. The danger lies in how you hand off control to the operating system.

I see this specific pattern all the time when developers first try to automate their media processing. Take a look at this snippet:

def transcode_to_mp4(input_path, output_path)
  puts "Transcoding #{input_path}..."
  `ffmpeg -i #{input_path} -vcodec libx264 -acodec aac #{output_path}`
  puts "Done! File saved to #{output_path}"
end

# This works for "video.mov", but fails for "my vacation.mov"
transcode_to_mp4("my vacation.mov", "output.mp4")

The Shell Injection and Silent Crash

If you run the code above with a filename containing a space, it will crash. Why? Because backticks (and system() or exec()) pass that string directly to the shell. The shell sees ffmpeg -i my vacation.mov... and thinks you're trying to pass an input file called my and then some weird argument called vacation.mov.

But the bigger problem is the silent failure. If FFmpeg encounters a corrupted frame or an unsupported codec, it will print an error to stderr and exit with a non-zero status. However, the Ruby code above doesn't care. It just keeps going and prints "Done!", leaving you with a non-existent or corrupted output file. You'll only find out something went wrong much later when your app tries to play a 0-byte video file.

Safe Process Execution with Open3

To fix this, we need to stop using shell strings and start using argument arrays. When you pass an array to a process spawn, Ruby bypasses the shell entirely and executes the binary directly. This eliminates shell injection and handles spaces in filenames automatically.

I always reach for the Open3 module for this. It gives us access to stdin, stdout, and stderr, plus the process exit status. Here is the professional way to wrap a transcoding call:

require 'open3'

def transcode_to_mp4(input_path, output_path)
  command = [
    'ffmpeg', '-i', input_path, 
    '-vcodec', 'libx264', 
    '-acodec', 'aac', 
    '-y', # Overwrite output file if it exists
    output_path
  ]

  stdout, stderr, status = Open3.capture3(*command)

  unless status.success?
    raise "FFmpeg failed with status #{status.exitstatus}: #{stderr}"
  end

  true
end

By using Open3.capture3, we capture the error stream. If FFmpeg fails, we don't just ignore it; we raise an exception containing the actual error message from the tool. This turns a "silent bug" into a "loud failure," which is infinitely easier to debug.

Orchestrating the Pipeline

A real video pipeline isn't just one command; it's a sequence. You might need to extract a thumbnail, generate a low-res proxy, and then upload the final version to S3. If you run these sequentially in a single Ruby thread, your application will hang until the video is finished. That's a non-starter for any production app.

The key is to treat the pipeline as a series of idempotent stages. I recommend creating a Pipeline class that tracks the state of the job. If the "Thumbnail" stage succeeds but the "Transcode" stage fails, you don't want to re-generate the thumbnail when you retry the job.

class VideoPipeline
  def initialize(video_id, source_path)
    @video_id = video_id
    @source_path = source_path
  end

  def process!
    # We wrap each stage in a begin/rescue or a state check
    generate_thumbnail
    transcode_versions
    cleanup_temp_files
  rescue StandardError => e
    log_error(e)
    # Notify user or mark record as 'failed' in DB
  end

  private

  def generate_thumbnail
    # Use the Open3 pattern from above...
  end

  def transcode_versions
    # Perhaps iterate through [ '360p', '720p', '1080p' ]
  end
end

Handling Partial Failures and Cleanup

One thing people often forget: Temporary files. Transcoding creates massive files. If your pipeline crashes halfway through, you'll end up with gigabytes of orphaned .ts or .mp4 fragments cluttering your disk.

I always wrap my pipeline logic in an ensure block or use a dedicated temporary directory (via Dir.mktmpdir). This ensures that whether the transcode succeeds or explodes, the temporary workspace is wiped clean. In a high-volume environment, failing to do this is the fastest way to trigger a "Disk Full" alert at 3 AM.




📋 Practical Task

Build a Multi-Resolution Transcoding Orchestrator

Your task is to build a Ruby class called VideoProcessor that takes a source video file and produces three different versions of it (low, medium, and high resolution).

Requirements:

  • The class should accept a source path and a destination directory.
  • It must use Open3.capture3 to execute FFmpeg commands.
  • It must support three presets:
    • low: 360p (scale=640:360)
    • med: 720p (scale=1280:720)
    • high: 1080p (scale=1920:1080)
  • The processor must track which versions were successfully created. If the med version fails, it should still attempt to create the high version, but it must raise a combined error or return a report at the end detailing which specific resolutions failed and why (using the stderr from FFmpeg).
  • Ensure that if a file with the same name exists in the destination, it is overwritten (use the -y flag).

Note: You don't need FFmpeg installed to write the logic, but your code should be syntactically correct and follow the argument-array pattern to avoid shell injection.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.