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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +256 -0
- data/Rakefile +15 -0
- data/examples/frames_to_mp4.rb +23 -0
- data/examples/stagecraft_offscreen_to_mp4.rb +47 -0
- data/examples/vizcore_scene_to_mp4.rb +40 -0
- data/lib/libav/audio_encoder.rb +326 -0
- data/lib/libav/audio_frame.rb +25 -0
- data/lib/libav/audio_reader.rb +336 -0
- data/lib/libav/audio_writer.rb +87 -0
- data/lib/libav/errors.rb +20 -0
- data/lib/libav/native/layout.rb +60 -0
- data/lib/libav/native/v60/layout.rb +30 -0
- data/lib/libav/native/v61/layout.rb +30 -0
- data/lib/libav/native.rb +311 -0
- data/lib/libav/output_context.rb +95 -0
- data/lib/libav/texel_support.rb +24 -0
- data/lib/libav/version.rb +7 -0
- data/lib/libav/video_encoder.rb +257 -0
- data/lib/libav/video_frame.rb +24 -0
- data/lib/libav/video_reader.rb +333 -0
- data/lib/libav/video_writer.rb +213 -0
- data/lib/libav.rb +35 -0
- data/native/abi_probe.c +72 -0
- data/native/generate_layout.rb +103 -0
- metadata +82 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thread"
|
|
4
|
+
require_relative "output_context"
|
|
5
|
+
require_relative "video_encoder"
|
|
6
|
+
require_relative "audio_encoder"
|
|
7
|
+
|
|
8
|
+
module LibAV
|
|
9
|
+
class VideoWriter
|
|
10
|
+
ASYNC_STOP = Object.new.freeze
|
|
11
|
+
QUEUE_DEPTH = 4
|
|
12
|
+
|
|
13
|
+
attr_reader :width, :height
|
|
14
|
+
|
|
15
|
+
def self.open(path, **options)
|
|
16
|
+
writer = new(path, **options)
|
|
17
|
+
return writer unless block_given?
|
|
18
|
+
|
|
19
|
+
begin
|
|
20
|
+
yield writer
|
|
21
|
+
ensure
|
|
22
|
+
writer.close
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(path, width:, height:, fps:, async: false, audio: nil, **options)
|
|
27
|
+
@path = File.path(path)
|
|
28
|
+
@width = Integer(width)
|
|
29
|
+
@height = Integer(height)
|
|
30
|
+
@output = OutputContext.new(@path)
|
|
31
|
+
@encoder = VideoEncoder.new(
|
|
32
|
+
@output, width: @width, height: @height, fps:, **options
|
|
33
|
+
)
|
|
34
|
+
@audio_encoder = AudioEncoder.new(@output, **audio_options(audio)) if audio
|
|
35
|
+
@output.open!
|
|
36
|
+
@mutex = Mutex.new
|
|
37
|
+
self.async = async
|
|
38
|
+
rescue StandardError
|
|
39
|
+
close_resources
|
|
40
|
+
raise
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def <<(frame)
|
|
44
|
+
push(frame)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def push(frame)
|
|
48
|
+
raise_if_unavailable!
|
|
49
|
+
rgba = normalize_frame(frame)
|
|
50
|
+
@async ? enqueue(rgba) : encode(rgba)
|
|
51
|
+
self
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def async
|
|
55
|
+
@async == true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def async=(enabled)
|
|
59
|
+
enabled = !!enabled
|
|
60
|
+
return enabled if enabled == async
|
|
61
|
+
raise ClosedError, "video writer is closed" if @closed
|
|
62
|
+
raise ArgumentError, "async mode cannot be disabled after it has started" unless enabled
|
|
63
|
+
|
|
64
|
+
@queue = SizedQueue.new(QUEUE_DEPTH)
|
|
65
|
+
@async = true
|
|
66
|
+
@worker = Thread.new { worker_loop }
|
|
67
|
+
enabled
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def push_audio(pcm_f32_interleaved)
|
|
71
|
+
raise_if_unavailable!
|
|
72
|
+
raise Error, "this writer has no audio track" unless @audio_encoder
|
|
73
|
+
|
|
74
|
+
@mutex.synchronize { @audio_encoder.push(pcm_f32_interleaved) }
|
|
75
|
+
self
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def close
|
|
79
|
+
return if @closed
|
|
80
|
+
|
|
81
|
+
error = nil
|
|
82
|
+
begin
|
|
83
|
+
stop_worker if async
|
|
84
|
+
raise_worker_error!
|
|
85
|
+
@mutex.synchronize do
|
|
86
|
+
@encoder.flush
|
|
87
|
+
@audio_encoder&.flush
|
|
88
|
+
end
|
|
89
|
+
rescue StandardError => caught
|
|
90
|
+
error = caught
|
|
91
|
+
ensure
|
|
92
|
+
cleanup_error = close_resources
|
|
93
|
+
error ||= cleanup_error
|
|
94
|
+
@closed = true
|
|
95
|
+
end
|
|
96
|
+
raise error if error
|
|
97
|
+
|
|
98
|
+
nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def closed?
|
|
102
|
+
@closed == true
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def normalize_frame(frame)
|
|
108
|
+
frame_width, frame_height, data = frame_parts(frame)
|
|
109
|
+
frame_width ||= width
|
|
110
|
+
frame_height ||= height
|
|
111
|
+
raise ArgumentError, "frame dimensions must be #{width}x#{height}" unless
|
|
112
|
+
frame_width == width && frame_height == height
|
|
113
|
+
validate_pixel_metadata(frame)
|
|
114
|
+
|
|
115
|
+
data = data.to_str.b
|
|
116
|
+
expected = width * height * 4
|
|
117
|
+
raise ArgumentError, "RGBA frame must contain #{expected} bytes, got #{data.bytesize}" unless
|
|
118
|
+
data.bytesize == expected
|
|
119
|
+
|
|
120
|
+
data.dup
|
|
121
|
+
rescue NoMethodError, TypeError
|
|
122
|
+
raise ArgumentError, "frame must provide packed RGBA bytes"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def frame_parts(frame)
|
|
126
|
+
return [nil, nil, frame] if frame.respond_to?(:to_str)
|
|
127
|
+
if frame.respond_to?(:to_hash)
|
|
128
|
+
hash = frame.to_hash
|
|
129
|
+
return [
|
|
130
|
+
hash[:width] || hash["width"],
|
|
131
|
+
hash[:height] || hash["height"],
|
|
132
|
+
hash[:data] || hash["data"]
|
|
133
|
+
]
|
|
134
|
+
end
|
|
135
|
+
return [frame.width, frame.height, frame.data] if
|
|
136
|
+
frame.respond_to?(:width) && frame.respond_to?(:height) && frame.respond_to?(:data)
|
|
137
|
+
|
|
138
|
+
[nil, nil, nil]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def validate_pixel_metadata(frame)
|
|
142
|
+
return unless frame.respond_to?(:channels) || frame.respond_to?(:dtype)
|
|
143
|
+
|
|
144
|
+
channels = frame.channels if frame.respond_to?(:channels)
|
|
145
|
+
dtype = frame.dtype if frame.respond_to?(:dtype)
|
|
146
|
+
raise ArgumentError, "image frames must have 4 RGBA channels" unless channels.nil? || channels == 4
|
|
147
|
+
raise ArgumentError, "image frames must use :u8 components" unless dtype.nil? || dtype.to_sym == :u8
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def encode(rgba)
|
|
151
|
+
@mutex.synchronize { @encoder.encode(rgba) }
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def enqueue(rgba)
|
|
155
|
+
loop do
|
|
156
|
+
raise_worker_error!
|
|
157
|
+
@queue.push(rgba, true)
|
|
158
|
+
break
|
|
159
|
+
rescue ThreadError
|
|
160
|
+
Thread.pass
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def stop_worker
|
|
165
|
+
enqueue(ASYNC_STOP)
|
|
166
|
+
@worker.join
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def worker_loop
|
|
170
|
+
loop do
|
|
171
|
+
frame = @queue.pop
|
|
172
|
+
break if frame.equal?(ASYNC_STOP)
|
|
173
|
+
|
|
174
|
+
encode(frame)
|
|
175
|
+
end
|
|
176
|
+
rescue StandardError => error
|
|
177
|
+
@worker_error = error
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def raise_if_unavailable!
|
|
181
|
+
raise ClosedError, "video writer is closed" if @closed
|
|
182
|
+
|
|
183
|
+
raise_worker_error!
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def raise_worker_error!
|
|
187
|
+
raise @worker_error if @worker_error
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def close_resources
|
|
191
|
+
error = nil
|
|
192
|
+
begin
|
|
193
|
+
@output&.close
|
|
194
|
+
rescue StandardError => caught
|
|
195
|
+
error = caught
|
|
196
|
+
ensure
|
|
197
|
+
@encoder&.close
|
|
198
|
+
@audio_encoder&.close
|
|
199
|
+
end
|
|
200
|
+
error
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def audio_options(audio)
|
|
204
|
+
if audio.respond_to?(:transform_keys)
|
|
205
|
+
options = audio.transform_keys(&:to_sym)
|
|
206
|
+
options[:codec] ||= :opus if File.extname(@path).downcase == ".webm"
|
|
207
|
+
return options
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
raise ArgumentError, "audio must be a Hash of encoder options"
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
data/lib/libav.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
require_relative "libav/version"
|
|
5
|
+
require_relative "libav/errors"
|
|
6
|
+
require_relative "libav/native"
|
|
7
|
+
require_relative "libav/video_frame"
|
|
8
|
+
require_relative "libav/texel_support"
|
|
9
|
+
require_relative "libav/video_writer"
|
|
10
|
+
require_relative "libav/video_reader"
|
|
11
|
+
require_relative "libav/audio_frame"
|
|
12
|
+
require_relative "libav/audio_writer"
|
|
13
|
+
require_relative "libav/audio_reader"
|
|
14
|
+
|
|
15
|
+
module LibAV
|
|
16
|
+
class << self
|
|
17
|
+
attr_accessor :logger
|
|
18
|
+
attr_reader :log_level
|
|
19
|
+
|
|
20
|
+
def native_versions
|
|
21
|
+
Native.versions.dup
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def log_level=(level)
|
|
25
|
+
Native.log_level = level
|
|
26
|
+
@log_level = level.respond_to?(:to_sym) ? level.to_sym : level
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
LibAV::Native.load!
|
|
32
|
+
LibAV.logger = Logger.new($stderr)
|
|
33
|
+
LibAV.logger.level = Logger::WARN
|
|
34
|
+
LibAV.log_level = :warn
|
|
35
|
+
LibAV::Native.install_log_callback!
|
data/native/abi_probe.c
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#include <stddef.h>
|
|
2
|
+
#include <stdio.h>
|
|
3
|
+
|
|
4
|
+
#include <libavcodec/avcodec.h>
|
|
5
|
+
#include <libavcodec/packet.h>
|
|
6
|
+
#include <libavformat/avformat.h>
|
|
7
|
+
#include <libavutil/channel_layout.h>
|
|
8
|
+
#include <libavutil/frame.h>
|
|
9
|
+
|
|
10
|
+
#define FIELD(type, field) \
|
|
11
|
+
printf("%s.%s=%zu\n", #type, #field, offsetof(type, field))
|
|
12
|
+
|
|
13
|
+
int main(void)
|
|
14
|
+
{
|
|
15
|
+
printf("pointer_size=%zu\n", sizeof(void *));
|
|
16
|
+
printf("channel_layout_size=%zu\n", sizeof(AVChannelLayout));
|
|
17
|
+
|
|
18
|
+
FIELD(AVOutputFormat, flags);
|
|
19
|
+
|
|
20
|
+
FIELD(AVFormatContext, oformat);
|
|
21
|
+
FIELD(AVFormatContext, pb);
|
|
22
|
+
FIELD(AVFormatContext, nb_streams);
|
|
23
|
+
FIELD(AVFormatContext, streams);
|
|
24
|
+
FIELD(AVFormatContext, duration);
|
|
25
|
+
|
|
26
|
+
FIELD(AVStream, index);
|
|
27
|
+
FIELD(AVStream, time_base);
|
|
28
|
+
FIELD(AVStream, start_time);
|
|
29
|
+
FIELD(AVStream, duration);
|
|
30
|
+
FIELD(AVStream, nb_frames);
|
|
31
|
+
FIELD(AVStream, r_frame_rate);
|
|
32
|
+
FIELD(AVStream, avg_frame_rate);
|
|
33
|
+
FIELD(AVStream, codecpar);
|
|
34
|
+
|
|
35
|
+
FIELD(AVCodecContext, codec_type);
|
|
36
|
+
FIELD(AVCodecContext, priv_data);
|
|
37
|
+
FIELD(AVCodecContext, bit_rate);
|
|
38
|
+
FIELD(AVCodecContext, flags);
|
|
39
|
+
FIELD(AVCodecContext, time_base);
|
|
40
|
+
FIELD(AVCodecContext, width);
|
|
41
|
+
FIELD(AVCodecContext, height);
|
|
42
|
+
FIELD(AVCodecContext, gop_size);
|
|
43
|
+
FIELD(AVCodecContext, pix_fmt);
|
|
44
|
+
FIELD(AVCodecContext, sample_rate);
|
|
45
|
+
FIELD(AVCodecContext, sample_fmt);
|
|
46
|
+
FIELD(AVCodecContext, frame_size);
|
|
47
|
+
FIELD(AVCodecContext, framerate);
|
|
48
|
+
FIELD(AVCodecContext, ch_layout);
|
|
49
|
+
|
|
50
|
+
FIELD(AVFrame, data);
|
|
51
|
+
FIELD(AVFrame, linesize);
|
|
52
|
+
FIELD(AVFrame, extended_data);
|
|
53
|
+
FIELD(AVFrame, width);
|
|
54
|
+
FIELD(AVFrame, height);
|
|
55
|
+
FIELD(AVFrame, nb_samples);
|
|
56
|
+
FIELD(AVFrame, format);
|
|
57
|
+
FIELD(AVFrame, pts);
|
|
58
|
+
FIELD(AVFrame, best_effort_timestamp);
|
|
59
|
+
FIELD(AVFrame, duration);
|
|
60
|
+
FIELD(AVFrame, sample_rate);
|
|
61
|
+
FIELD(AVFrame, ch_layout);
|
|
62
|
+
|
|
63
|
+
FIELD(AVPacket, pts);
|
|
64
|
+
FIELD(AVPacket, dts);
|
|
65
|
+
FIELD(AVPacket, data);
|
|
66
|
+
FIELD(AVPacket, size);
|
|
67
|
+
FIELD(AVPacket, stream_index);
|
|
68
|
+
FIELD(AVPacket, duration);
|
|
69
|
+
FIELD(AVPacket, time_base);
|
|
70
|
+
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "open3"
|
|
5
|
+
require "optparse"
|
|
6
|
+
require "rbconfig"
|
|
7
|
+
require "tmpdir"
|
|
8
|
+
|
|
9
|
+
TYPE_NAMES = {
|
|
10
|
+
"AVOutputFormat" => :output_format,
|
|
11
|
+
"AVFormatContext" => :format_context,
|
|
12
|
+
"AVStream" => :stream,
|
|
13
|
+
"AVCodecContext" => :codec_context,
|
|
14
|
+
"AVFrame" => :frame,
|
|
15
|
+
"AVPacket" => :packet
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
options = {}
|
|
19
|
+
OptionParser.new do |parser|
|
|
20
|
+
parser.banner = "Usage: ruby native/generate_layout.rb [options]"
|
|
21
|
+
parser.on("--codec-major MAJOR", Integer, "libavcodec major (60 or 61)") do |value|
|
|
22
|
+
options[:major] = value
|
|
23
|
+
end
|
|
24
|
+
parser.on("--include DIRECTORY", "FFmpeg include directory") do |value|
|
|
25
|
+
options[:include] = File.expand_path(value)
|
|
26
|
+
end
|
|
27
|
+
parser.on("--output FILE", "Ruby layout file to write") do |value|
|
|
28
|
+
options[:output] = File.expand_path(value)
|
|
29
|
+
end
|
|
30
|
+
parser.on("--verify", "verify that the existing layout matches the native headers") do
|
|
31
|
+
options[:verify] = true
|
|
32
|
+
end
|
|
33
|
+
end.parse!
|
|
34
|
+
|
|
35
|
+
major = options.fetch(:major) { abort "missing --codec-major" }
|
|
36
|
+
abort "codec major must be 60 or 61" unless [60, 61].include?(major)
|
|
37
|
+
include_directory = options.fetch(:include) { abort "missing --include" }
|
|
38
|
+
output = options.fetch(
|
|
39
|
+
:output,
|
|
40
|
+
File.expand_path("../lib/libav/native/v#{major}/layout.rb", __dir__)
|
|
41
|
+
)
|
|
42
|
+
probe = File.expand_path("abi_probe.c", __dir__)
|
|
43
|
+
compiler = ENV.fetch("CC", RbConfig::CONFIG.fetch("CC", "cc"))
|
|
44
|
+
|
|
45
|
+
raw = Dir.mktmpdir("libav-abi") do |directory|
|
|
46
|
+
binary = File.join(directory, "abi_probe")
|
|
47
|
+
command = [compiler, "-I#{include_directory}", probe, "-o", binary]
|
|
48
|
+
_stdout, stderr, status = Open3.capture3(*command)
|
|
49
|
+
abort "ABI probe compilation failed:\n#{stderr}" unless status.success?
|
|
50
|
+
|
|
51
|
+
stdout, run_stderr, run_status = Open3.capture3(binary)
|
|
52
|
+
abort "ABI probe failed:\n#{run_stderr}" unless run_status.success?
|
|
53
|
+
|
|
54
|
+
stdout
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
metadata = {}
|
|
58
|
+
values = Hash.new { |hash, key| hash[key] = {} }
|
|
59
|
+
raw.each_line do |line|
|
|
60
|
+
name, offset = line.strip.split("=", 2)
|
|
61
|
+
unless name.include?(".")
|
|
62
|
+
metadata[name] = Integer(offset)
|
|
63
|
+
next
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
native_type, field = name.split(".", 2)
|
|
67
|
+
type = TYPE_NAMES.fetch(native_type)
|
|
68
|
+
values[type][field.to_sym] = Integer(offset)
|
|
69
|
+
end
|
|
70
|
+
abort "ABI probe requires 64-bit pointers" unless metadata.fetch("pointer_size") == 8
|
|
71
|
+
abort "unexpected AVChannelLayout size" unless metadata.fetch("channel_layout_size") == 24
|
|
72
|
+
|
|
73
|
+
body = values.map do |type, fields|
|
|
74
|
+
entries = fields.map { |field, offset| "#{field}: #{offset}" }.join(", ")
|
|
75
|
+
" #{type}: {#{entries}}"
|
|
76
|
+
end.join(",\n")
|
|
77
|
+
|
|
78
|
+
source = <<~RUBY
|
|
79
|
+
# frozen_string_literal: true
|
|
80
|
+
|
|
81
|
+
module LibAV
|
|
82
|
+
module Native
|
|
83
|
+
module Layout
|
|
84
|
+
V#{major} = {
|
|
85
|
+
#{body}
|
|
86
|
+
}.freeze
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
RUBY
|
|
91
|
+
|
|
92
|
+
if options[:verify]
|
|
93
|
+
load output
|
|
94
|
+
generated = LibAV::Native::Layout.const_get(:"V#{major}")
|
|
95
|
+
expected = values.transform_values(&:to_h).to_h
|
|
96
|
+
abort "ABI layout mismatch: regenerate #{output}" unless generated == expected
|
|
97
|
+
|
|
98
|
+
puts "ABI layout verified: #{output}"
|
|
99
|
+
else
|
|
100
|
+
FileUtils.mkdir_p(File.dirname(output))
|
|
101
|
+
File.write(output, source)
|
|
102
|
+
puts output
|
|
103
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: libav-ruby
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yudai Takada
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: ffi
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.17'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.17'
|
|
26
|
+
description: Encode and decode video and audio frames through FFmpeg shared libraries
|
|
27
|
+
without spawning subprocesses.
|
|
28
|
+
email:
|
|
29
|
+
- t.yudai92@gmail.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- LICENSE.txt
|
|
35
|
+
- README.md
|
|
36
|
+
- Rakefile
|
|
37
|
+
- examples/frames_to_mp4.rb
|
|
38
|
+
- examples/stagecraft_offscreen_to_mp4.rb
|
|
39
|
+
- examples/vizcore_scene_to_mp4.rb
|
|
40
|
+
- lib/libav.rb
|
|
41
|
+
- lib/libav/audio_encoder.rb
|
|
42
|
+
- lib/libav/audio_frame.rb
|
|
43
|
+
- lib/libav/audio_reader.rb
|
|
44
|
+
- lib/libav/audio_writer.rb
|
|
45
|
+
- lib/libav/errors.rb
|
|
46
|
+
- lib/libav/native.rb
|
|
47
|
+
- lib/libav/native/layout.rb
|
|
48
|
+
- lib/libav/native/v60/layout.rb
|
|
49
|
+
- lib/libav/native/v61/layout.rb
|
|
50
|
+
- lib/libav/output_context.rb
|
|
51
|
+
- lib/libav/texel_support.rb
|
|
52
|
+
- lib/libav/version.rb
|
|
53
|
+
- lib/libav/video_encoder.rb
|
|
54
|
+
- lib/libav/video_frame.rb
|
|
55
|
+
- lib/libav/video_reader.rb
|
|
56
|
+
- lib/libav/video_writer.rb
|
|
57
|
+
- native/abi_probe.c
|
|
58
|
+
- native/generate_layout.rb
|
|
59
|
+
homepage: https://github.com/ydah/libav-ruby
|
|
60
|
+
licenses:
|
|
61
|
+
- MIT
|
|
62
|
+
metadata:
|
|
63
|
+
homepage_uri: https://github.com/ydah/libav-ruby
|
|
64
|
+
rubygems_mfa_required: 'true'
|
|
65
|
+
rdoc_options: []
|
|
66
|
+
require_paths:
|
|
67
|
+
- lib
|
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: 3.2.0
|
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
74
|
+
requirements:
|
|
75
|
+
- - ">="
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: '0'
|
|
78
|
+
requirements: []
|
|
79
|
+
rubygems_version: 4.0.6
|
|
80
|
+
specification_version: 4
|
|
81
|
+
summary: Frame-level Ruby bindings for FFmpeg
|
|
82
|
+
test_files: []
|