ruby-ffmpeg

A modern Ruby wrapper for FFmpeg with zero dependencies. Provides clean APIs for metadata extraction, transcoding, scene detection, and keyframe extraction.

Gem Version Ruby

Features

  • Zero Dependencies - Pure Ruby, no gem dependencies
  • Modern Ruby - Requires Ruby 3.1+, uses Data classes and modern patterns
  • FFmpeg 4-7 Support - Tested against FFmpeg versions 4, 5, 6, and 7
  • Scene Detection - Detect scene changes for video analysis
  • Keyframe Extraction - Extract frames at intervals, timestamps, or I-frames
  • Progress Reporting - Real-time transcoding progress callbacks
  • Rails Integration - Active Storage analyzer and previewer included

Installation

Add to your Gemfile:

gem 'ruby-ffmpeg'

Or install directly:

gem install ruby-ffmpeg

Note: FFmpeg must be installed on your system. Install with:

  • macOS: brew install ffmpeg
  • Ubuntu: sudo apt install ffmpeg
  • Windows: choco install ffmpeg

Quick Start

require 'ffmpeg'

# Load a video
media = FFMPEG::Media.new("/path/to/video.mp4")

# Get metadata
media.duration      # => 120.5
media.resolution    # => "1920x1080"
media.video_codec   # => "h264"
media.audio_codec   # => "aac"
media.frame_rate    # => 29.97
media.bit_rate      # => 5000000

# Check properties
media.valid?        # => true
media.video?        # => true
media.audio?        # => true
media.hd?           # => true
media.portrait?     # => false

Transcoding

# Basic transcoding
media.transcode("/output.mp4")

# With options
media.transcode("/output.webm",
  video_codec: "libvpx-vp9",
  audio_codec: "libopus",
  resolution: "1280x720",
  video_bitrate: "2M"
)

# With progress callback
media.transcode("/output.mp4") do |progress|
  puts "Progress: #{progress.round(1)}%"
end

# Copy streams (no re-encoding)
media.transcode("/output.mp4",
  copy_video: true,
  copy_audio: true
)

# Trim video
media.transcode("/clip.mp4",
  seek: 30,      # Start at 30 seconds
  duration: 10   # 10 second clip
)

Scene Detection

Detect scene changes for video analysis, chapter generation, or highlight extraction:

# Detect scenes with default threshold
scenes = media.detect_scenes
# => [
#   { timestamp: 0.0, score: 1.0 },
#   { timestamp: 5.23, score: 0.45 },
#   { timestamp: 12.8, score: 0.38 }
# ]

# Custom threshold (lower = more sensitive)
scenes = media.detect_scenes(threshold: 0.2)

# Using SceneDetector directly for more options
detector = FFMPEG::SceneDetector.new(media)
scenes = detector.detect(
  threshold: 0.3,
  min_scene_length: 2.0,  # Minimum 2 seconds between scenes
  max_scenes: 20          # Maximum 20 scenes
)

# Detect scenes and extract keyframes
scenes = detector.detect_with_keyframes(
  threshold: 0.3,
  output_dir: "/tmp/scenes"
)
# Each scene now has :keyframe_path

Keyframe Extraction

Extract frames for thumbnails, video analysis, or sprite sheets:

# Extract at regular intervals
frames = media.extract_keyframes(
  output_dir: "/tmp/frames",
  interval: 5.0  # Every 5 seconds
)
# => ["/tmp/frames/frame_0000.jpg", "/tmp/frames/frame_0005.jpg", ...]

# Extract specific number of frames
frames = media.extract_keyframes(
  output_dir: "/tmp/frames",
  count: 10  # 10 evenly-distributed frames
)

# Extract at specific timestamps
frames = media.extract_keyframes(
  output_dir: "/tmp/frames",
  timestamps: [0, 30, 60, 90, 120]
)

# Using KeyframeExtractor directly for more options
extractor = FFMPEG::KeyframeExtractor.new(media)

# Extract actual I-frames (keyframes) from video
iframes = extractor.extract_iframes(
  output_dir: "/tmp/iframes",
  max_frames: 50
)

# Create a thumbnail sprite sheet (for video scrubbing)
sprite = extractor.create_sprite(
  columns: 10,
  rows: 10,
  width: 160,
  output_path: "/tmp/sprite.jpg"
)
# => { path: "/tmp/sprite.jpg", columns: 10, rows: 10, ... }

Stream Information

Access detailed stream information:

# Video stream
video = media.video
video.codec         # => "h264"
video.width         # => 1920
video.height        # => 1080
video.frame_rate    # => 29.97
video.bit_rate      # => 4500000
video.pixel_format  # => "yuv420p"
video.rotation      # => 0

# Audio stream
audio = media.audio
audio.codec         # => "aac"
audio.sample_rate   # => 48000
audio.channels      # => 2
audio.channel_layout # => "stereo"

# All streams
media.streams.each do |stream|
  puts stream.to_s
end

Configuration

FFMPEG.configure do |config|
  # Custom binary paths
  config.ffmpeg_binary = "/usr/local/bin/ffmpeg"
  config.ffprobe_binary = "/usr/local/bin/ffprobe"

  # Default timeout (seconds)
  config.timeout = 600

  # Enable logging
  config.logger = Logger.new(STDOUT)

  # Default codecs
  config.default_video_codec = "libx264"
  config.default_audio_codec = "aac"

  # Number of threads (0 = auto)
  config.threads = 0

  # Temporary directory
  config.temp_dir = "/tmp/ffmpeg"

  # Overwrite existing files
  config.overwrite_output = true
end

Rails Integration

Active Storage Analyzer

Automatically extract video metadata for Active Storage:

# config/initializers/active_storage.rb
Rails.application.config.active_storage.analyzers.prepend(
  FFMPEG::ActiveStorage::Analyzer
)

Now video blobs will have metadata like:

video.
# => { width: 1920, height: 1080, duration: 120.5, video_codec: "h264", ... }

Active Storage Previewer

Generate video previews:

# config/initializers/active_storage.rb
Rails.application.config.active_storage.previewers.prepend(
  FFMPEG::ActiveStorage::Previewer
)

Rake Tasks

# Check FFmpeg installation
rake ffmpeg:check

# Analyze a video
rake ffmpeg:analyze[/path/to/video.mp4]

# Extract keyframes
rake ffmpeg:keyframes[/path/to/video.mp4,output_dir,5]

# Detect scenes
rake ffmpeg:scenes[/path/to/video.mp4,0.3]

Error Handling

begin
  media = FFMPEG::Media.new("/path/to/video.mp4")
  media.transcode("/output.mp4")
rescue FFMPEG::MediaNotFound => e
  puts "File not found: #{e.message}"
rescue FFMPEG::InvalidMedia => e
  puts "Invalid media: #{e.message}"
rescue FFMPEG::TranscodingError => e
  puts "Transcoding failed: #{e.message}"
  puts "Command: #{e.command}"
  puts "Output: #{e.output}"
rescue FFMPEG::CommandTimeout => e
  puts "Command timed out: #{e.message}"
end

Comparison with streamio-ffmpeg

Feature ruby-ffmpeg streamio-ffmpeg
Ruby Version 3.1+ 2.0+
FFmpeg Version 4-7 2.8.4
Dependencies None None
Scene Detection Built-in No
Keyframe Extraction Built-in No
Active Storage Built-in No
Progress Callbacks Yes Yes
Last Updated 2026 2016

Development

# Clone the repo
git clone https://github.com/activeagents/ruby-ffmpeg.git
cd ruby-ffmpeg

# Install dependencies
bin/setup

# Run tests
rake test

# Run linter
rake rubocop

Contributing

  1. Fork it
  2. Create your feature branch (git checkout -b feature/my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin feature/my-new-feature)
  5. Create a new Pull Request

License

The gem is available as open source under the terms of the MIT License.

Credits

Developed and maintained by Active Agents.

Inspired by streamio-ffmpeg and instructure/ruby-ffmpeg.