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:

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}"
end

Key Considerations

  1. 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.
  2. Closing Stdin: You must call stdin.close once 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.
  3. 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_moof to FFmpeg so it writes a fragmented stream that does not require seeking.