Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

libav-ruby

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.

Installation

Install FFmpeg's shared libraries first:

# macOS
brew install ffmpeg@7

# Ubuntu
sudo apt install \
  libavcodec-dev libavformat-dev libavutil-dev \
  libswscale-dev libswresample-dev

Then add the gem:

bundle add libav-ruby

Homebrew'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.

Encode video

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
end

Supported 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 encoding

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.close

An encoder-thread exception is re-raised by push or close.

Decode video

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
end

Pass 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) }
end

Set 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.

Audio

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

Audio-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
end

Audio 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.

Renderer integration

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.mp4

examples/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.mp4

Both 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.

Errors and logging

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 = :info

Levels are :quiet, :panic, :fatal, :error, :warn, :info, :verbose, :debug, and :trace. Set LibAV.logger = nil to discard logs.

Development

bundle install
bundle exec rspec
bundle exec gem build libav.gemspec

The 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/include

Use --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.

License

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.

About

Frame-level Ruby bindings for FFmpeg video and audio encoding/decoding without subprocesses.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages