libav-ruby 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: baa53a631a37482e35cdb6c5c93ccdd698852ef87b51958aecdbb7278078d320
4
+ data.tar.gz: 0ec4ac62f3fc523251cb3c0da083efa6538e0721031764cce71e6286ebfc4e70
5
+ SHA512:
6
+ metadata.gz: 852262785ea88ea96eb09593559a669d9a752d6f5b11aace8af77f303b6eab3e34adf7ef5248c30e8f6dfc42bd83ef000b4b1a98ebdef3cb8117878365f92490
7
+ data.tar.gz: cb33b4511134f81247ea518ae5faad65278e126208ef5fcb644d1828e60a74db928a6feea78194e37679c283c993ea2d3c3e6701fcb44170c2c4c5db3ac8d94f
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Yudai Takada
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,256 @@
1
+ # libav-ruby
2
+
3
+ `libav-ruby` provides frame-level Ruby bindings for the FFmpeg libraries. It
4
+ encodes packed RGBA frames without starting an `ffmpeg` subprocess, decodes
5
+ video to RGBA, and reads or writes interleaved f32 audio.
6
+
7
+ The current release supports FFmpeg 6 and 7 on 64-bit Ruby
8
+ (`libavcodec` majors 60 and 61). The gem checks the complete FFmpeg library set
9
+ at load time and raises `LoadError` for unsupported or mixed versions.
10
+
11
+ ## Installation
12
+
13
+ Install FFmpeg's shared libraries first:
14
+
15
+ ```sh
16
+ # macOS
17
+ brew install ffmpeg@7
18
+
19
+ # Ubuntu
20
+ sudo apt install \
21
+ libavcodec-dev libavformat-dev libavutil-dev \
22
+ libswscale-dev libswresample-dev
23
+ ```
24
+
25
+ Then add the gem:
26
+
27
+ ```sh
28
+ bundle add libav-ruby
29
+ ```
30
+
31
+ Homebrew's standard Intel and Apple Silicon `ffmpeg@6`/`ffmpeg@7` keg paths
32
+ are detected automatically. For any other non-standard installation, set
33
+ `LIBAV_LIBRARY_PATH` to the directory containing the shared libraries before
34
+ requiring the gem.
35
+
36
+ ## Encode video
37
+
38
+ Input is packed RGBA bytes, a `{width:, height:, data:}` Hash, or any object
39
+ with `width`, `height`, and `data` methods. A `Texel::Image` is accepted when
40
+ it has four channels and `dtype: :u8`, which is the format returned by
41
+ Stagecraft's offscreen renderer.
42
+
43
+ ```ruby
44
+ require "libav"
45
+
46
+ width = 1_280
47
+ height = 720
48
+
49
+ LibAV::VideoWriter.open(
50
+ "output.mp4",
51
+ width: width,
52
+ height: height,
53
+ fps: 60,
54
+ codec: :h264,
55
+ crf: 18,
56
+ preset: "medium"
57
+ ) do |writer|
58
+ frames.each do |rgba|
59
+ writer << rgba
60
+ end
61
+ end
62
+ ```
63
+
64
+ Supported video codec names are `:h264`, `:hevc`, `:vp9`, `:prores`, and
65
+ `:png`. The matching encoder must be present in the system FFmpeg build.
66
+ H.264/HEVC use `yuv420p` by default, ProRes uses `yuv422p10le`, and PNG uses
67
+ `rgba`. For ProRes, `pixel_format: :rgba` selects alpha-preserving
68
+ `yuva444p10le`, while `:yuv444p` selects `yuv444p10le`. Extra keyword options
69
+ are forwarded to the codec's FFmpeg option set; underscores in Ruby keyword
70
+ names become hyphens.
71
+
72
+ For a PNG sequence, use a numbered output pattern:
73
+
74
+ ```ruby
75
+ LibAV::VideoWriter.open(
76
+ "frames/frame-%06d.png",
77
+ width: 640,
78
+ height: 480,
79
+ fps: 30,
80
+ codec: :png
81
+ ) { |writer| frames.each { |frame| writer << frame } }
82
+ ```
83
+
84
+ ### Async encoding
85
+
86
+ Async mode overlaps rendering and encoding with a bounded queue of four
87
+ frames. Input strings are copied before being queued.
88
+
89
+ ```ruby
90
+ writer = LibAV::VideoWriter.open(
91
+ "output.mp4", width: 1_280, height: 720, fps: 60
92
+ )
93
+ writer.async = true
94
+ frames.each { |frame| writer << frame }
95
+ writer.close
96
+ ```
97
+
98
+ An encoder-thread exception is re-raised by `push` or `close`.
99
+
100
+ ## Decode video
101
+
102
+ `read_frame` returns a `LibAV::VideoFrame` with `width`, `height`, `data`, and
103
+ `pts`. `data` is a packed RGBA String.
104
+
105
+ ```ruby
106
+ LibAV::VideoReader.open("input.mp4") do |reader|
107
+ puts "#{reader.width}x#{reader.height} at #{reader.fps} fps"
108
+
109
+ reader.each_frame do |frame, pts_seconds|
110
+ texture.update(frame.data)
111
+ end
112
+
113
+ reader.seek(12.5)
114
+ frame = reader.read_frame
115
+ end
116
+ ```
117
+
118
+ Pass `frame_type: :texel` to return `Texel::Image` objects instead. Texel is an
119
+ optional dependency and must be installed separately by applications using
120
+ this mode. Timestamps remain the second value yielded by `each_frame`.
121
+
122
+ ```ruby
123
+ LibAV::VideoReader.open("input.mp4", frame_type: :texel) do |reader|
124
+ reader.each_frame { |image, pts| texture.update(image) }
125
+ end
126
+ ```
127
+
128
+ Set `reader.reuse_buffer = true` to keep replacing the same decoded String.
129
+ Previously returned frames then observe later decoded data, so this mode is
130
+ intended for immediate uploads or other zero-retention processing.
131
+ Buffer reuse is unavailable with immutable Texel output.
132
+
133
+ `fps`, `duration`, and `frame_count` are container metadata or estimates and
134
+ can be `nil` when the input does not provide enough timing information.
135
+
136
+ ## Audio
137
+
138
+ Add an AAC or Opus track by passing audio options when the video writer is
139
+ opened. `push_audio` accepts packed, interleaved native-endian f32 PCM in
140
+ arbitrary sample-aligned chunks, or an Array of floats.
141
+
142
+ ```ruby
143
+ LibAV::VideoWriter.open(
144
+ "music-video.mp4",
145
+ width: 1_920,
146
+ height: 1_080,
147
+ fps: 60,
148
+ audio: {
149
+ codec: :aac,
150
+ sample_rate: 48_000,
151
+ channels: 2,
152
+ bitrate: 192_000
153
+ }
154
+ ) do |writer|
155
+ writer.push_audio(pcm_f32_stereo)
156
+ frames.each { |frame| writer << frame }
157
+ end
158
+ ```
159
+
160
+ Audio-only files use `AudioWriter`. WAV defaults to f32 PCM, WebM/Ogg/Opus
161
+ containers default to Opus, and other containers default to AAC. A WebM
162
+ `VideoWriter` audio track also defaults to Opus when `audio[:codec]` is
163
+ omitted.
164
+
165
+ ```ruby
166
+ LibAV::AudioWriter.open(
167
+ "tone.wav", sample_rate: 48_000, channels: 2
168
+ ) { |writer| writer << pcm }
169
+
170
+ LibAV::AudioReader.open("tone.wav") do |reader|
171
+ reader.seek(1.5)
172
+ reader.each_frame do |frame, pts_seconds|
173
+ consume(frame.data, frame.samples)
174
+ end
175
+ end
176
+ ```
177
+
178
+ Audio input is converted with libswresample and accumulated in FFmpeg's
179
+ `AVAudioFifo`, then submitted in the encoder's native frame size. Calls may
180
+ therefore use any chunk size that contains complete interleaved samples.
181
+
182
+ `AudioReader#seek` performs a backward native seek, discards decoded audio
183
+ before the requested time, and trims the first overlapping frame to a sample
184
+ boundary.
185
+
186
+ ## Renderer integration
187
+
188
+ [`examples/stagecraft_offscreen_to_mp4.rb`](examples/stagecraft_offscreen_to_mp4.rb)
189
+ renders a Stagecraft scene offscreen and passes each returned `Texel::Image`
190
+ directly to an asynchronous video writer:
191
+
192
+ ```sh
193
+ ruby examples/stagecraft_offscreen_to_mp4.rb output.mp4
194
+ ```
195
+
196
+ [`examples/vizcore_scene_to_mp4.rb`](examples/vizcore_scene_to_mp4.rb) runs a
197
+ Vizcore scene with deterministic dummy audio analysis and encodes its software
198
+ snapshots without temporary frame files:
199
+
200
+ ```sh
201
+ ruby examples/vizcore_scene_to_mp4.rb path/to/scene.rb output.mp4
202
+ ```
203
+
204
+ Both examples accept `WIDTH`, `HEIGHT`, `FPS`, and `FRAMES` environment
205
+ variables. Their renderer gems are optional application dependencies and are
206
+ not installed with libav-ruby.
207
+
208
+ `AudioReader#read_samples` returns only the packed String.
209
+ `AudioReader#each_chunk` yields the String and PTS instead of an `AudioFrame`.
210
+
211
+ ## Errors and logging
212
+
213
+ Negative FFmpeg return codes become `LibAV::NativeError` instances with the
214
+ native error string, numeric code, and operation name. EAGAIN and EOF remain
215
+ internal control flow. Public objects have idempotent `close` methods, and all
216
+ block-form `open` methods close in an `ensure`.
217
+
218
+ FFmpeg logs are forwarded to `LibAV.logger`. The default logger writes warnings
219
+ and errors to stderr.
220
+
221
+ ```ruby
222
+ LibAV.logger = my_logger
223
+ LibAV.log_level = :info
224
+ ```
225
+
226
+ Levels are `:quiet`, `:panic`, `:fatal`, `:error`, `:warn`, `:info`,
227
+ `:verbose`, `:debug`, and `:trace`. Set `LibAV.logger = nil` to discard logs.
228
+
229
+ ## Development
230
+
231
+ ```sh
232
+ bundle install
233
+ bundle exec rspec
234
+ bundle exec gem build libav.gemspec
235
+ ```
236
+
237
+ The integration specs create short media files in temporary directories and
238
+ use `ffprobe` for container assertions. `native/abi_probe.c` prints the native
239
+ offsets used by the version-specific layouts. Regenerate a layout against
240
+ matching FFmpeg development headers with:
241
+
242
+ ```sh
243
+ ruby native/generate_layout.rb \
244
+ --codec-major 61 \
245
+ --include /path/to/ffmpeg/include
246
+ ```
247
+
248
+ Use `--verify` instead of rewriting the file to compare the checked-in layout
249
+ against the selected headers. CI performs this check on FFmpeg 6 and FFmpeg 7
250
+ across Linux, macOS, and Windows.
251
+
252
+ ## License
253
+
254
+ The gem is available under the MIT License. FFmpeg and optional encoders have
255
+ their own licenses; the effective FFmpeg license depends on how the system
256
+ libraries were built.
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ desc "Check bundled examples for Ruby syntax errors"
9
+ task :examples do
10
+ Dir[File.join(__dir__, "examples", "*.rb")].sort.each do |path|
11
+ ruby "-c", path
12
+ end
13
+ end
14
+
15
+ task default: %i[spec examples]
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "libav"
4
+
5
+ width = 640
6
+ height = 360
7
+ frame_count = 120
8
+
9
+ LibAV::VideoWriter.open(
10
+ ARGV.fetch(0, "gradient.mp4"),
11
+ width:,
12
+ height:,
13
+ fps: 60,
14
+ codec: :h264,
15
+ crf: 18,
16
+ preset: "medium"
17
+ ) do |writer|
18
+ frame_count.times do |index|
19
+ red = index * 255 / (frame_count - 1)
20
+ pixel = [red, 80, 255 - red, 255].pack("C4")
21
+ writer << pixel * (width * height)
22
+ end
23
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "libav"
4
+ require "stagecraft"
5
+
6
+ width = Integer(ENV.fetch("WIDTH", 1_280))
7
+ height = Integer(ENV.fetch("HEIGHT", 720))
8
+ fps = Integer(ENV.fetch("FPS", 60))
9
+ frame_count = Integer(ENV.fetch("FRAMES", fps * 5))
10
+ output = ARGV.first || "stagecraft.mp4"
11
+
12
+ scene = Stagecraft::Scene.new
13
+ scene.background = "#10141d"
14
+ scene.add(Stagecraft::Lights::Ambient.new(intensity: 0.3))
15
+ sun = Stagecraft::Lights::Directional.new(intensity: 3.0)
16
+ sun.position.set(4, 7, 5)
17
+ scene.add(sun)
18
+
19
+ cube = Stagecraft::Mesh.new(
20
+ Stagecraft::Geometries.box,
21
+ Stagecraft::Materials::PBR.new(
22
+ base_color: "#e91e63", roughness: 0.35, metallic: 0.15
23
+ )
24
+ )
25
+ scene.add(cube)
26
+
27
+ camera = Stagecraft::Cameras::Perspective.new(
28
+ fov: 55, aspect: width.to_f / height, near: 0.1, far: 200
29
+ )
30
+ camera.position.set(0, 2, 6)
31
+ renderer = Stagecraft::Renderer.offscreen(width:, height:, msaa: 4)
32
+
33
+ begin
34
+ LibAV::VideoWriter.open(
35
+ output, width:, height:, fps:, codec: :h264,
36
+ preset: "medium", crf: 18, async: true
37
+ ) do |writer|
38
+ frame_count.times do
39
+ cube.rotation.rotate_y!(1.0 / fps)
40
+ writer << renderer.render(scene, camera).read_pixels
41
+ end
42
+ end
43
+ ensure
44
+ renderer.dispose
45
+ end
46
+
47
+ puts output
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "libav"
4
+ require "texel"
5
+ require "vizcore"
6
+ require "vizcore/config"
7
+ require "vizcore/renderer"
8
+
9
+ scene_path = ARGV.fetch(0) do
10
+ abort "Usage: ruby examples/vizcore_scene_to_mp4.rb SCENE.rb [OUTPUT.mp4]"
11
+ end
12
+ output = ARGV[1] || "vizcore.mp4"
13
+ width = Integer(ENV.fetch("WIDTH", 1_280))
14
+ height = Integer(ENV.fetch("HEIGHT", 720))
15
+ fps = Integer(ENV.fetch("FPS", 60))
16
+ frame_count = Integer(ENV.fetch("FRAMES", fps * 5))
17
+
18
+ config = Vizcore::Config.new(scene_file: scene_path, audio_source: :dummy)
19
+ source = Vizcore::Renderer::SceneFrameSource.new(config:, frame_rate: fps)
20
+ renderer = Vizcore::Renderer::SnapshotRenderer.new(width:, height:)
21
+
22
+ begin
23
+ source.start
24
+ LibAV::VideoWriter.open(
25
+ output, width:, height:, fps:, codec: :h264,
26
+ preset: "medium", crf: 18, async: true
27
+ ) do |writer|
28
+ frame_count.times do
29
+ frame = source.capture
30
+ png = renderer.render(
31
+ scene: frame.fetch(:scene), audio: frame.fetch(:audio)
32
+ )
33
+ writer << Texel.load(png, channels: 4, dtype: :u8)
34
+ end
35
+ end
36
+ ensure
37
+ source.stop
38
+ end
39
+
40
+ puts output