Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
213: Video Transcoding Pipelines in Ruby
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.capture3to 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
medversion fails, it should still attempt to create thehighversion, but it must raise a combined error or return a report at the end detailing which specific resolutions failed and why (using thestderrfrom FFmpeg). - Ensure that if a file with the same name exists in the destination, it is overwritten (use the
-yflag).
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.
There are no comments for now.