Modern Ruby wrapper for FFmpeg with clean API and proper error handling.
- Modern Ruby 3+ conventions
- Zero runtime dependencies
- Real-time progress reporting
- Support for video/audio filters and quality presets
- Hardware Acceleration (NVENC, VAAPI, QSV, Vulkan AV1, D3D12)
- Remote input support (HTTP/HTTPS/RTMP/RTSP)
- Rich metadata: chapters, subtitles, EXIF, audio properties
- Video operations: cut, audio extraction, batch screenshots
- Proper error handling with detailed context
- Thread-safe configuration
- Simple, intuitive API
| Feature | Minimum FFmpeg version |
|---|---|
| Core transcoding, probing | Any recent version |
| AV1 hardware encoding (Vulkan) | 8.0+ |
| EXIF metadata parsing | 8.1+ |
| D3D12 hardware acceleration | 8.1+ |
Chapter metadata (chapters) |
Any (via -show_chapters) |
- Ruby 3.2+
- FFmpeg installed (
brew install ffmpegon macOS)
Add to your Gemfile:
gem "ffmpeg_core"Then run:
bundle installrequire "ffmpeg_core"
# Load a video file or remote URL
movie = FFmpegCore::Movie.new("input.mp4")
# movie = FFmpegCore::Movie.new("http://example.com/video.mp4")
# Basic metadata
movie.duration # => 120.5 (seconds)
movie.resolution # => "1920x1080" (automatically swapped if rotated)
movie.width # => 1920
movie.height # => 1080
movie.video_codec # => "h264"
movie.audio_codec # => "aac"
movie.frame_rate # => 29.97
movie.bitrate # => 5000 (kb/s)
movie.valid? # => true
movie.has_video? # => true
movie.has_audio? # => true
# Video stream details
movie.probe.rotation # => 90 (degrees)
movie.probe.aspect_ratio # => "16:9"
movie.probe.video_profile # => "High"
movie.probe.video_level # => 41
movie.probe.pixel_format # => "yuv420p"
# Audio stream details
movie.probe.audio_sample_rate # => 48000
movie.probe.audio_channels # => 2
movie.probe.audio_channel_layout # => "stereo"
movie.probe.audio_streams # => [{ "codec_type" => "audio", ... }, ...]
# Subtitles and chapters
movie.probe.subtitle_streams # => [{ "codec_name" => "subrip", ... }]
movie.probe.chapters # => [{ "tags" => { "title" => "Intro" }, ... }]
# File-level tags and EXIF (FFmpeg 8.1+ for full EXIF support)
movie.probe.tags # => { "title" => "My Video", "artist" => "Author" }
movie.probe.exif # => { "creation_time" => "2024-06-15T14:30:00Z", ... }
# Container format
movie.probe.format_name # => "mov,mp4,m4a,3gp,3g2,mj2"movie = FFmpegCore::Movie.new("input.mp4")
# Basic transcoding with progress
movie.transcode("output.mp4", video_codec: "libx264") do |progress|
puts "Progress: #{(progress * 100).round(2)}%"
end
# Advanced options (Filters & Quality)
movie.transcode("output.mp4", {
video_codec: "libx264",
audio_codec: "aac",
video_bitrate: "1000k",
audio_bitrate: "128k",
resolution: "1280x720",
crop: { width: 500, height: 500, x: 10, y: 10 }, # Crop video
video_filter: "scale=1280:-1,transpose=1", # Resize and rotate
audio_filter: "volume=0.5", # Reduce volume
preset: "slow", # ffmpeg preset (ultrafast, fast, medium, slow, etc.)
crf: 23 # Constant Rate Factor (0-51)
})Use structured APIs for -filter_complex and -map to build complex pipelines without raw string hacks.
movie.transcode("out.mp4", {
filter_graph: [
"[0:v]crop=320:240:0:0[c]",
"[c]scale=640:480[outv]"
],
maps: ["[outv]", "0:a"]
})Lossless trim using stream copy — no re-encoding, nearly instant:
# Trim by start time + duration
movie.cut("clip.mp4", start_time: 30, duration: 60)
# Trim by start and end time
movie.cut("clip.mp4", start_time: 30, end_time: 90)Note:
-c copyseeks to the nearest keyframe. For frame-accurate trimming, usetranscodewithcustom: ["-ss", "30", "-to", "90"].
# Extract audio with automatic codec detection from file extension
movie.extract_audio("audio.aac")
# Specify codec explicitly
movie.extract_audio("audio.mp3", codec: "libmp3lame")
movie.extract_audio("audio.opus", codec: "libopus")# Extract 5 screenshots distributed evenly across the video
paths = movie.screenshots("thumbs/", count: 5)
# => ["thumbs/screenshot_001.jpg", ..., "thumbs/screenshot_005.jpg"]Opt-in to hardware-accelerated encoding with automatic encoder detection and graceful fallback.
# H.264 / HEVC — classic accelerators
movie.transcode("out.mp4", hwaccel: :nvenc) # NVIDIA CUDA
movie.transcode("out.mp4", hwaccel: :vaapi) # Linux VAAPI
movie.transcode("out.mp4", hwaccel: :qsv) # Intel Quick Sync
# AV1 — requires FFmpeg 8.0+
movie.transcode("out.mp4", video_codec: "libaom-av1", hwaccel: :nvenc) # NVIDIA
movie.transcode("out.mp4", video_codec: "libaom-av1", hwaccel: :vaapi) # VAAPI
movie.transcode("out.mp4", video_codec: "libaom-av1", hwaccel: :vulkan) # Vulkan compute
# D3D12 — Windows only, requires FFmpeg 8.1+
movie.transcode("out.mp4", hwaccel: :d3d12)All accelerators gracefully fall back to software encoding if the hardware encoder is not available.
FFmpegCore supports raw FFmpeg filter strings for both video (video_filter or -vf) and audio (audio_filter or -af).
Common Video Filters:
movie.transcode("output.mp4", {
# Scale to width 1280, keep aspect ratio
video_filter: "scale=1280:-1",
# Crop 100x100 starting at position (10,10)
video_filter: "crop=100:100:10:10",
# Rotate 90 degrees clockwise
video_filter: "transpose=1",
# Chain multiple filters (Scale then Rotate)
video_filter: "scale=1280:-1,transpose=1"
})Common Audio Filters:
movie.transcode("output.mp4", {
# Increase volume by 50%
audio_filter: "volume=1.5",
# Fade in first 5 seconds
audio_filter: "afade=t=in:ss=0:d=5"
})movie = FFmpegCore::Movie.new("input.mp4")
# Extract screenshot at specific time
movie.screenshot("thumbnail.jpg", seek_time: 5)
# With resolution and quality
movie.screenshot("thumbnail.jpg", {
seek_time: 10,
resolution: "640x360",
quality: 2 # 2-31, lower is better
})FFmpegCore.configure do |config|
config.ffmpeg_binary = "/usr/local/bin/ffmpeg"
config.ffprobe_binary = "/usr/local/bin/ffprobe"
config.timeout = 60
endFFmpegCore provides specific error classes for different failure scenarios. All execution errors (transcoding, probing, screenshots) inherit from FFmpegCore::ExecutionError, which provides access to the command, exit status, and stderr output.
begin
movie = FFmpegCore::Movie.new("input.mp4")
movie.transcode("output.mp4", video_codec: "libx264")
rescue FFmpegCore::InvalidInputError => e
# File doesn't exist or is not readable
puts "Input error: #{e.message}"
rescue FFmpegCore::ExecutionError => e
# Covers TranscodingError, ProbeError, and ScreenshotError
puts "Execution failed: #{e.message}"
puts "Command: #{e.command}"
puts "Exit status: #{e.exit_status}"
puts "Stderr: #{e.stderr}"
rescue FFmpegCore::BinaryNotFoundError => e
# FFmpeg not installed
puts "FFmpeg not found: #{e.message}"
end| Error | Description | Parent |
|---|---|---|
FFmpegCore::Error |
Base error class | StandardError |
FFmpegCore::BinaryNotFoundError |
FFmpeg/FFprobe not found | Error |
FFmpegCore::InvalidInputError |
Input file doesn't exist or unreadable | Error |
FFmpegCore::OutputError |
Output file cannot be written | Error |
FFmpegCore::ExecutionError |
Base for command execution errors | Error |
FFmpegCore::ProbeError |
Failed to extract metadata | ExecutionError |
FFmpegCore::TranscodingError |
FFmpeg transcoding failed | ExecutionError |
FFmpegCore::ScreenshotError |
Screenshot extraction failed | ExecutionError |
# Install dependencies
bundle install
# Run tests
bundle exec rspec
# Run linter
bundle exec rubocopMIT License. See LICENSE.txt for details.