Control FFmpeg with Ruby via Standard I/O
This article explains how to interface a Ruby application with the
FFmpeg binary using redirected standard input (stdin), standard output
(stdout), and standard error (stderr). By leveraging Ruby’s built-in
Open3 library, you can stream media data directly into
FFmpeg, process it in real-time, and capture the output stream and logs
directly in your Ruby code without relying on intermediate disk
files.
Why Use Standard I/O with FFmpeg?
Interfacing with FFmpeg via standard I/O (pipes) is highly efficient for web applications and data pipelines. Instead of saving an uploaded video to disk, calling FFmpeg on that file, and reading the output file back, you can stream the upload data directly to the FFmpeg process and stream the result to its final destination (such as cloud storage). This reduces disk I/O bottlenecks and minimizes latency.
Setting Up FFmpeg for Piping
FFmpeg represents standard input and standard output using the
pipe: protocol:
pipe:0(or-): Represents standard input (stdin).pipe:1(or-): Represents standard output (stdout).pipe:2: Represents standard error (stderr).
When streaming container formats like MP4 over stdout, you must
configure FFmpeg to write fragmented data (using
-movflags frag_keyframe+empty_moov), because standard MP4
requires seeking to write the metadata index (moov atom) at the end,
which is impossible in a non-seekable stdout stream. Alternatively, you
can use naturally streamable formats like MPEG-TS
(-f mpegts).
The Ruby Implementation
Using Open3
To manage stdin, stdout, and stderr simultaneously without causing
system deadlocks, you should use Ruby’s Open3.popen3
method. This method spawns the FFmpeg binary as a subprocess and yields
independent IO objects for writing to stdin, reading from stdout, and
reading from stderr.
Using separate Ruby threads to handle the input writing, output reading, and error logging ensures that none of the pipe buffers overflow, which would otherwise freeze the process.
Here is a complete, production-ready Ruby script demonstrating this workflow:
require 'open3'
# Define the input and output file paths for this example
input_file_path = "input.mp4"
output_file_path = "output.ts"
# FFmpeg command:
# -i pipe:0 -> Read input from stdin
# -f mpegts -> Use MPEG-TS format (ideal for streaming/piping)
# pipe:1 -> Write output to stdout
ffmpeg_cmd = "ffmpeg -i pipe:0 -c:v copy -c:a copy -f mpegts pipe:1"
begin
# Spawn the FFmpeg subprocess
Open3.popen3(ffmpeg_cmd) do |stdin, stdout, stderr, wait_thr|
# Thread 1: Stream input data into FFmpeg's stdin
writer = Thread.new do
File.open(input_file_path, "rb") do |file|
while (chunk = file.read(16384)) # Read in 16KB chunks
stdin.write(chunk)
end
end
stdin.close # Close stdin to signal EOF (End of File) to FFmpeg
end
# Thread 2: Retrieve processed data from FFmpeg's stdout
reader = Thread.new do
File.open(output_file_path, "wb") do |out_file|
while (chunk = stdout.read(16384)) # Read in 16KB chunks
out_file.write(chunk)
end
end
end
# Thread 3: Capture FFmpeg logs from stderr (essential for debugging)
logger = Thread.new do
while (line = stderr.gets)
# Process or log FFmpeg output lines here
puts "[FFmpeg Log] #{line.strip}"
end
end
# Wait for all streaming threads to complete
writer.join
reader.join
logger.join
# Check the exit status of the FFmpeg process
exit_status = wait_thr.value
if exit_status.success?
puts "FFmpeg processing completed successfully."
else
warn "FFmpeg failed with exit code: #{exit_status.exitstatus}"
end
end
rescue Errno::ENOENT
warn "Error: The 'ffmpeg' binary was not found in your system PATH."
rescue => e
warn "An error occurred: #{e.message}"
endKey Considerations
- Chunk Size: Reading and writing in chunks (e.g., 16KB to 64KB) balances memory usage and I/O performance. Avoid reading entire large media files into memory at once.
- Closing Stdin: You must call
stdin.closeonce all input data has been written. If you do not close the stdin stream, FFmpeg will wait indefinitely for more data, causing your application to hang. - Format Compatibility: Not all media codecs and
containers support piping. If you must output MP4, ensure you pass the
flags
-movflags frag_keyframe+empty_moov+default_base_moofto FFmpeg so it writes a fragmented stream that does not require seeking.