libav-ruby provides frame-level Ruby bindings for the FFmpeg libraries. It
encodes packed RGBA frames without starting an ffmpeg subprocess, decodes
video to RGBA, and reads or writes interleaved f32 audio.
The current release supports FFmpeg 6 and 7 on 64-bit Ruby
(libavcodec majors 60 and 61). The gem checks the complete FFmpeg library set
at load time and raises LoadError for unsupported or mixed versions.
Install FFmpeg's shared libraries first:
# macOS
brew install ffmpeg@7
# Ubuntu
sudo apt install \
libavcodec-dev libavformat-dev libavutil-dev \
libswscale-dev libswresample-devThen add the gem:
bundle add libav-rubyHomebrew's standard Intel and Apple Silicon ffmpeg@6/ffmpeg@7 keg paths
are detected automatically. For any other non-standard installation, set
LIBAV_LIBRARY_PATH to the directory containing the shared libraries before
requiring the gem.
Input is packed RGBA bytes, a {width:, height:, data:} Hash, or any object
with width, height, and data methods. A Texel::Image is accepted when
it has four channels and dtype: :u8, which is the format returned by
Stagecraft's offscreen renderer.
require "libav"
width = 1_280
height = 720
LibAV::VideoWriter.open(
"output.mp4",
width: width,
height: height,
fps: 60,
codec: :h264,
crf: 18,
preset: "medium"
) do |writer|
frames.each do |rgba|
writer << rgba
end
endSupported video codec names are :h264, :hevc, :vp9, :prores, and
:png. The matching encoder must be present in the system FFmpeg build.
H.264/HEVC use yuv420p by default, ProRes uses yuv422p10le, and PNG uses
rgba. For ProRes, pixel_format: :rgba selects alpha-preserving
yuva444p10le, while :yuv444p selects yuv444p10le. Extra keyword options
are forwarded to the codec's FFmpeg option set; underscores in Ruby keyword
names become hyphens.
For a PNG sequence, use a numbered output pattern:
LibAV::VideoWriter.open(
"frames/frame-%06d.png",
width: 640,
height: 480,
fps: 30,
codec: :png
) { |writer| frames.each { |frame| writer << frame } }Async mode overlaps rendering and encoding with a bounded queue of four frames. Input strings are copied before being queued.
writer = LibAV::VideoWriter.open(
"output.mp4", width: 1_280, height: 720, fps: 60
)
writer.async = true
frames.each { |frame| writer << frame }
writer.closeAn encoder-thread exception is re-raised by push or close.
read_frame returns a LibAV::VideoFrame with width, height, data, and
pts. data is a packed RGBA String.
LibAV::VideoReader.open("input.mp4") do |reader|
puts "#{reader.width}x#{reader.height} at #{reader.fps} fps"
reader.each_frame do |frame, pts_seconds|
texture.update(frame.data)
end
reader.seek(12.5)
frame = reader.read_frame
endPass frame_type: :texel to return Texel::Image objects instead. Texel is an
optional dependency and must be installed separately by applications using
this mode. Timestamps remain the second value yielded by each_frame.
LibAV::VideoReader.open("input.mp4", frame_type: :texel) do |reader|
reader.each_frame { |image, pts| texture.update(image) }
endSet reader.reuse_buffer = true to keep replacing the same decoded String.
Previously returned frames then observe later decoded data, so this mode is
intended for immediate uploads or other zero-retention processing.
Buffer reuse is unavailable with immutable Texel output.
fps, duration, and frame_count are container metadata or estimates and
can be nil when the input does not provide enough timing information.
Add an AAC or Opus track by passing audio options when the video writer is
opened. push_audio accepts packed, interleaved native-endian f32 PCM in
arbitrary sample-aligned chunks, or an Array of floats.
LibAV::VideoWriter.open(
"music-video.mp4",
width: 1_920,
height: 1_080,
fps: 60,
audio: {
codec: :aac,
sample_rate: 48_000,
channels: 2,
bitrate: 192_000
}
) do |writer|
writer.push_audio(pcm_f32_stereo)
frames.each { |frame| writer << frame }
endAudio-only files use AudioWriter. WAV defaults to f32 PCM, WebM/Ogg/Opus
containers default to Opus, and other containers default to AAC. A WebM
VideoWriter audio track also defaults to Opus when audio[:codec] is
omitted.
LibAV::AudioWriter.open(
"tone.wav", sample_rate: 48_000, channels: 2
) { |writer| writer << pcm }
LibAV::AudioReader.open("tone.wav") do |reader|
reader.seek(1.5)
reader.each_frame do |frame, pts_seconds|
consume(frame.data, frame.samples)
end
endAudio input is converted with libswresample and accumulated in FFmpeg's
AVAudioFifo, then submitted in the encoder's native frame size. Calls may
therefore use any chunk size that contains complete interleaved samples.
AudioReader#seek performs a backward native seek, discards decoded audio
before the requested time, and trims the first overlapping frame to a sample
boundary.
examples/stagecraft_offscreen_to_mp4.rb
renders a Stagecraft scene offscreen and passes each returned Texel::Image
directly to an asynchronous video writer:
ruby examples/stagecraft_offscreen_to_mp4.rb output.mp4examples/vizcore_scene_to_mp4.rb runs a
Vizcore scene with deterministic dummy audio analysis and encodes its software
snapshots without temporary frame files:
ruby examples/vizcore_scene_to_mp4.rb path/to/scene.rb output.mp4Both examples accept WIDTH, HEIGHT, FPS, and FRAMES environment
variables. Their renderer gems are optional application dependencies and are
not installed with libav-ruby.
AudioReader#read_samples returns only the packed String.
AudioReader#each_chunk yields the String and PTS instead of an AudioFrame.
Negative FFmpeg return codes become LibAV::NativeError instances with the
native error string, numeric code, and operation name. EAGAIN and EOF remain
internal control flow. Public objects have idempotent close methods, and all
block-form open methods close in an ensure.
FFmpeg logs are forwarded to LibAV.logger. The default logger writes warnings
and errors to stderr.
LibAV.logger = my_logger
LibAV.log_level = :infoLevels are :quiet, :panic, :fatal, :error, :warn, :info,
:verbose, :debug, and :trace. Set LibAV.logger = nil to discard logs.
bundle install
bundle exec rspec
bundle exec gem build libav.gemspecThe integration specs create short media files in temporary directories and
use ffprobe for container assertions. native/abi_probe.c prints the native
offsets used by the version-specific layouts. Regenerate a layout against
matching FFmpeg development headers with:
ruby native/generate_layout.rb \
--codec-major 61 \
--include /path/to/ffmpeg/includeUse --verify instead of rewriting the file to compare the checked-in layout
against the selected headers. CI performs this check on FFmpeg 6 and FFmpeg 7
across Linux, macOS, and Windows.
The gem is available under the MIT License. FFmpeg and optional encoders have their own licenses; the effective FFmpeg license depends on how the system libraries were built.