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,257 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LibAV
|
|
4
|
+
class VideoEncoder
|
|
5
|
+
CODECS = {
|
|
6
|
+
h264: %w[libx264 h264],
|
|
7
|
+
hevc: %w[libx265 hevc],
|
|
8
|
+
vp9: %w[libvpx-vp9 vp9],
|
|
9
|
+
prores: %w[prores_ks prores_aw prores],
|
|
10
|
+
png: %w[png]
|
|
11
|
+
}.freeze
|
|
12
|
+
CODEC_IDS = {
|
|
13
|
+
h264: 27,
|
|
14
|
+
png: 61,
|
|
15
|
+
prores: 147,
|
|
16
|
+
vp9: 167,
|
|
17
|
+
hevc: 173
|
|
18
|
+
}.freeze
|
|
19
|
+
CRF_CODECS = %i[h264 hevc vp9].freeze
|
|
20
|
+
PRESET_CODECS = %i[h264 hevc].freeze
|
|
21
|
+
DEFAULT_PIXEL_FORMATS = {
|
|
22
|
+
png: :rgba,
|
|
23
|
+
prores: :yuv422p10le
|
|
24
|
+
}.freeze
|
|
25
|
+
PIXEL_FORMAT_ALIASES = {
|
|
26
|
+
prores: {
|
|
27
|
+
"rgba" => "yuva444p10le",
|
|
28
|
+
"yuv444p" => "yuv444p10le"
|
|
29
|
+
}
|
|
30
|
+
}.freeze
|
|
31
|
+
MAX_DIMENSION = 32_768
|
|
32
|
+
MAX_PIXELS = 134_217_728
|
|
33
|
+
|
|
34
|
+
attr_reader :stream, :time_base
|
|
35
|
+
|
|
36
|
+
def initialize(output, width:, height:, fps:, codec: :h264, bitrate: nil, crf: 18,
|
|
37
|
+
preset: "medium", pixel_format: nil, gop: nil, **codec_options)
|
|
38
|
+
@output = output
|
|
39
|
+
@width = dimension(width, :width)
|
|
40
|
+
@height = dimension(height, :height)
|
|
41
|
+
validate_pixel_count!
|
|
42
|
+
@fps = positive_rational(fps, :fps)
|
|
43
|
+
@codec_name = codec.to_sym
|
|
44
|
+
@time_base = Rational(@fps.denominator, @fps.numerator)
|
|
45
|
+
@pixel_format = normalize_pixel_format(pixel_format)
|
|
46
|
+
validate_subsampled_dimensions!
|
|
47
|
+
|
|
48
|
+
@codec, @named_encoder = find_encoder
|
|
49
|
+
@stream = output.new_stream(@codec)
|
|
50
|
+
@context = Native.codec.avcodec_alloc_context3(@codec)
|
|
51
|
+
raise EncodingError, "avcodec_alloc_context3 returned NULL" if @context.null?
|
|
52
|
+
|
|
53
|
+
configure_context(bitrate:, gop:)
|
|
54
|
+
configure_private_options(crf:, preset:, codec_options:)
|
|
55
|
+
open_context
|
|
56
|
+
allocate_conversion
|
|
57
|
+
@frame_index = 0
|
|
58
|
+
rescue StandardError
|
|
59
|
+
close
|
|
60
|
+
raise
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def encode(rgba)
|
|
64
|
+
raise ClosedError, "video encoder is closed" if @closed
|
|
65
|
+
|
|
66
|
+
Native.check(Native.util.av_frame_make_writable(@frame), "av_frame_make_writable")
|
|
67
|
+
source = FFI::MemoryPointer.from_string(rgba)
|
|
68
|
+
@source_data.put_pointer(0, source)
|
|
69
|
+
scaled = Native.scale.sws_scale(
|
|
70
|
+
@scale, @source_data, @source_stride, 0, @height,
|
|
71
|
+
Native::Layout.field_pointer(@frame, :frame, :data),
|
|
72
|
+
Native::Layout.field_pointer(@frame, :frame, :linesize)
|
|
73
|
+
)
|
|
74
|
+
Native.check(scaled, "sws_scale(video encode)") if scaled.negative?
|
|
75
|
+
raise EncodingError, "sws_scale converted #{scaled} rows, expected #{@height}" unless scaled == @height
|
|
76
|
+
|
|
77
|
+
Native::Layout.write_int64(@frame, :frame, :pts, @frame_index)
|
|
78
|
+
Native::Layout.write_int64(@frame, :frame, :duration, 1)
|
|
79
|
+
@frame_index += 1
|
|
80
|
+
send_frame(@frame)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def flush
|
|
84
|
+
return if @flushed || @closed
|
|
85
|
+
|
|
86
|
+
send_frame(nil)
|
|
87
|
+
@flushed = true
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def close
|
|
91
|
+
return if @closed
|
|
92
|
+
|
|
93
|
+
Native.scale.sws_freeContext(@scale) if @scale && !@scale.null?
|
|
94
|
+
free_pointer(@frame, :av_frame_free)
|
|
95
|
+
free_pointer(@packet, :av_packet_free, Native.codec)
|
|
96
|
+
free_pointer(@context, :avcodec_free_context, Native.codec)
|
|
97
|
+
@scale = @frame = @packet = @context = nil
|
|
98
|
+
@closed = true
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def configure_context(bitrate:, gop:)
|
|
104
|
+
pixel_format = Native.util.av_get_pix_fmt(@pixel_format)
|
|
105
|
+
raise ArgumentError, "unknown pixel format: #{@pixel_format}" if pixel_format.negative?
|
|
106
|
+
|
|
107
|
+
Native::Layout.write_int(@context, :codec_context, :width, @width)
|
|
108
|
+
Native::Layout.write_int(@context, :codec_context, :height, @height)
|
|
109
|
+
Native::Layout.write_int(@context, :codec_context, :pix_fmt, pixel_format)
|
|
110
|
+
Native::Layout.write_int(@context, :codec_context, :gop_size, gop || default_gop)
|
|
111
|
+
Native::Layout.write_rational(@context, :codec_context, :time_base, time_base)
|
|
112
|
+
Native::Layout.write_rational(@context, :codec_context, :framerate, @fps)
|
|
113
|
+
Native::Layout.write_int64(@context, :codec_context, :bit_rate, bitrate) if bitrate
|
|
114
|
+
Native::Layout.write_rational(stream, :stream, :time_base, time_base)
|
|
115
|
+
|
|
116
|
+
return unless @output.global_header?
|
|
117
|
+
|
|
118
|
+
flags = Native::Layout.read_int(@context, :codec_context, :flags)
|
|
119
|
+
Native::Layout.write_int(
|
|
120
|
+
@context, :codec_context, :flags, flags | Native::AV_CODEC_FLAG_GLOBAL_HEADER
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def configure_private_options(crf:, preset:, codec_options:)
|
|
125
|
+
private_data = Native::Layout.read_pointer(@context, :codec_context, :priv_data)
|
|
126
|
+
set_option(private_data, "crf", crf) if
|
|
127
|
+
@named_encoder && CRF_CODECS.include?(@codec_name) && !crf.nil?
|
|
128
|
+
set_option(private_data, "preset", preset) if
|
|
129
|
+
@named_encoder && PRESET_CODECS.include?(@codec_name) && !preset.nil?
|
|
130
|
+
codec_options.each { |name, value| set_option(private_data, name, value) }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def open_context
|
|
134
|
+
Native.check(Native.codec.avcodec_open2(@context, @codec, nil), "avcodec_open2")
|
|
135
|
+
codec_parameters = Native::Layout.read_pointer(stream, :stream, :codecpar)
|
|
136
|
+
Native.check(
|
|
137
|
+
Native.codec.avcodec_parameters_from_context(codec_parameters, @context),
|
|
138
|
+
"avcodec_parameters_from_context"
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def allocate_conversion
|
|
143
|
+
@frame = Native.util.av_frame_alloc
|
|
144
|
+
@packet = Native.codec.av_packet_alloc
|
|
145
|
+
raise EncodingError, "failed to allocate an AVFrame or AVPacket" if @frame.null? || @packet.null?
|
|
146
|
+
|
|
147
|
+
Native::Layout.write_int(@frame, :frame, :format,
|
|
148
|
+
Native::Layout.read_int(@context, :codec_context, :pix_fmt))
|
|
149
|
+
Native::Layout.write_int(@frame, :frame, :width, @width)
|
|
150
|
+
Native::Layout.write_int(@frame, :frame, :height, @height)
|
|
151
|
+
Native.check(Native.util.av_frame_get_buffer(@frame, 0), "av_frame_get_buffer")
|
|
152
|
+
|
|
153
|
+
@scale = Native.scale.sws_getContext(
|
|
154
|
+
@width, @height, Native.util.av_get_pix_fmt("rgba"),
|
|
155
|
+
@width, @height, Native::Layout.read_int(@context, :codec_context, :pix_fmt),
|
|
156
|
+
Native::SWS_BICUBIC, nil, nil, nil
|
|
157
|
+
)
|
|
158
|
+
raise EncodingError, "sws_getContext returned NULL" if @scale.null?
|
|
159
|
+
|
|
160
|
+
@source_data = FFI::MemoryPointer.new(:pointer)
|
|
161
|
+
@source_stride = FFI::MemoryPointer.new(:int)
|
|
162
|
+
@source_stride.write_int(@width * 4)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def send_frame(frame)
|
|
166
|
+
result = Native.codec.avcodec_send_frame(@context, frame)
|
|
167
|
+
drain_packets if result == Native::AVERROR_EAGAIN
|
|
168
|
+
result = Native.codec.avcodec_send_frame(@context, frame) if result == Native::AVERROR_EAGAIN
|
|
169
|
+
Native.check(result, "avcodec_send_frame")
|
|
170
|
+
drain_packets
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def drain_packets
|
|
174
|
+
loop do
|
|
175
|
+
result = Native.codec.avcodec_receive_packet(@context, @packet)
|
|
176
|
+
break if Native.control_flow?(result)
|
|
177
|
+
|
|
178
|
+
Native.check(result, "avcodec_receive_packet")
|
|
179
|
+
begin
|
|
180
|
+
packet_duration = Native::Layout.read_int64(@packet, :packet, :duration)
|
|
181
|
+
Native::Layout.write_int64(@packet, :packet, :duration, 1) unless packet_duration.positive?
|
|
182
|
+
@output.write_packet(@packet, time_base, stream)
|
|
183
|
+
ensure
|
|
184
|
+
Native.codec.av_packet_unref(@packet)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def find_encoder
|
|
190
|
+
names = CODECS.fetch(@codec_name) do
|
|
191
|
+
raise ArgumentError, "unsupported video codec: #{@codec_name.inspect}"
|
|
192
|
+
end
|
|
193
|
+
names.each do |name|
|
|
194
|
+
encoder = Native.codec.avcodec_find_encoder_by_name(name)
|
|
195
|
+
return [encoder, true] unless encoder.null?
|
|
196
|
+
end
|
|
197
|
+
encoder = Native.codec.avcodec_find_encoder(CODEC_IDS.fetch(@codec_name))
|
|
198
|
+
return [encoder, false] unless encoder.null?
|
|
199
|
+
|
|
200
|
+
raise EncodingError, "FFmpeg does not provide a #{names.join(" or ")} encoder"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def set_option(target, name, value)
|
|
204
|
+
raise ArgumentError, "#{@codec_name} encoder does not accept private options" if target.null?
|
|
205
|
+
|
|
206
|
+
option_name = name.to_s.tr("_", "-")
|
|
207
|
+
Native.check(
|
|
208
|
+
Native.util.av_opt_set(target, option_name, value.to_s, 0),
|
|
209
|
+
"av_opt_set(#{option_name})"
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def dimension(value, name)
|
|
214
|
+
value = Integer(value)
|
|
215
|
+
return value if value.positive? && value <= MAX_DIMENSION
|
|
216
|
+
|
|
217
|
+
raise ArgumentError, "#{name} must be between 1 and #{MAX_DIMENSION}"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def positive_rational(value, name)
|
|
221
|
+
value = value.to_r
|
|
222
|
+
return value if value.positive?
|
|
223
|
+
|
|
224
|
+
raise ArgumentError, "#{name} must be positive"
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def validate_subsampled_dimensions!
|
|
228
|
+
return unless @pixel_format == "yuv420p"
|
|
229
|
+
return if @width.even? && @height.even?
|
|
230
|
+
|
|
231
|
+
raise ArgumentError, "yuv420p requires even width and height"
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def normalize_pixel_format(pixel_format)
|
|
235
|
+
requested = (pixel_format || DEFAULT_PIXEL_FORMATS.fetch(@codec_name, :yuv420p)).to_s
|
|
236
|
+
PIXEL_FORMAT_ALIASES.fetch(@codec_name, {}).fetch(requested, requested)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def validate_pixel_count!
|
|
240
|
+
return if @width * @height <= MAX_PIXELS
|
|
241
|
+
|
|
242
|
+
raise ArgumentError, "frame dimensions exceed the #{MAX_PIXELS}-pixel limit"
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def default_gop
|
|
246
|
+
[(@fps * 2).round, 1].max
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def free_pointer(pointer, function, library = Native.util)
|
|
250
|
+
return unless pointer && !pointer.null?
|
|
251
|
+
|
|
252
|
+
holder = FFI::MemoryPointer.new(:pointer)
|
|
253
|
+
holder.write_pointer(pointer)
|
|
254
|
+
library.public_send(function, holder)
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LibAV
|
|
4
|
+
class VideoFrame
|
|
5
|
+
attr_reader :width, :height, :data, :pts
|
|
6
|
+
|
|
7
|
+
def initialize(width:, height:, data:, pts: nil)
|
|
8
|
+
@width = Integer(width)
|
|
9
|
+
@height = Integer(height)
|
|
10
|
+
@data = data.to_str
|
|
11
|
+
@data = @data.dup unless @data.encoding == Encoding::BINARY
|
|
12
|
+
@data.force_encoding(Encoding::BINARY)
|
|
13
|
+
@pts = pts
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def to_str
|
|
17
|
+
data
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def bytesize
|
|
21
|
+
data.bytesize
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LibAV
|
|
4
|
+
class VideoReader
|
|
5
|
+
FRAME_TYPES = %i[video_frame texel].freeze
|
|
6
|
+
|
|
7
|
+
attr_reader :width, :height, :fps, :duration, :frame_count, :frame_type
|
|
8
|
+
|
|
9
|
+
def self.open(path, **options)
|
|
10
|
+
reader = new(path, **options)
|
|
11
|
+
return reader unless block_given?
|
|
12
|
+
|
|
13
|
+
begin
|
|
14
|
+
yield reader
|
|
15
|
+
ensure
|
|
16
|
+
reader.close
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(path, frame_type: :video_frame)
|
|
21
|
+
@path = File.path(path)
|
|
22
|
+
@frame_type = normalize_frame_type(frame_type)
|
|
23
|
+
TexelSupport.load! if @frame_type == :texel
|
|
24
|
+
@format_holder = FFI::MemoryPointer.new(:pointer)
|
|
25
|
+
Native.check(
|
|
26
|
+
Native.format.avformat_open_input(@format_holder, @path, nil, nil),
|
|
27
|
+
"avformat_open_input"
|
|
28
|
+
)
|
|
29
|
+
@format_context = @format_holder.read_pointer
|
|
30
|
+
Native.check(
|
|
31
|
+
Native.format.avformat_find_stream_info(@format_context, nil),
|
|
32
|
+
"avformat_find_stream_info"
|
|
33
|
+
)
|
|
34
|
+
open_video_stream
|
|
35
|
+
allocate_decode_state
|
|
36
|
+
load_metadata
|
|
37
|
+
@reuse_buffer = false
|
|
38
|
+
reset_decode_state
|
|
39
|
+
rescue StandardError
|
|
40
|
+
close
|
|
41
|
+
raise
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def read_frame
|
|
45
|
+
raise ClosedError, "video reader is closed" if closed?
|
|
46
|
+
|
|
47
|
+
loop do
|
|
48
|
+
return nil unless receive_next_frame
|
|
49
|
+
|
|
50
|
+
frame = convert_frame
|
|
51
|
+
next if before_seek_target?(@decoded_pts)
|
|
52
|
+
|
|
53
|
+
@seek_target = nil
|
|
54
|
+
@last_read_pts = @decoded_pts
|
|
55
|
+
return frame
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def each_frame
|
|
60
|
+
return enum_for(__method__) unless block_given?
|
|
61
|
+
|
|
62
|
+
while (frame = read_frame)
|
|
63
|
+
yield frame, @last_read_pts
|
|
64
|
+
end
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def seek(seconds)
|
|
69
|
+
raise ClosedError, "video reader is closed" if closed?
|
|
70
|
+
|
|
71
|
+
seconds = Float(seconds)
|
|
72
|
+
raise ArgumentError, "seek time must be nonnegative" if seconds.negative? || !seconds.finite?
|
|
73
|
+
|
|
74
|
+
timestamp = (seconds / @time_base).round
|
|
75
|
+
Native.check(
|
|
76
|
+
Native.format.av_seek_frame(
|
|
77
|
+
@format_context, @stream_index, timestamp, Native::AVSEEK_FLAG_BACKWARD
|
|
78
|
+
),
|
|
79
|
+
"av_seek_frame"
|
|
80
|
+
)
|
|
81
|
+
Native.codec.avcodec_flush_buffers(@codec_context)
|
|
82
|
+
Native.util.av_frame_unref(@frame)
|
|
83
|
+
Native.codec.av_packet_unref(@packet)
|
|
84
|
+
reset_decode_state
|
|
85
|
+
@seek_target = seconds
|
|
86
|
+
self
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def reuse_buffer
|
|
90
|
+
@reuse_buffer == true
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def reuse_buffer=(enabled)
|
|
94
|
+
enabled = !!enabled
|
|
95
|
+
raise ArgumentError, "reuse_buffer is incompatible with immutable Texel::Image output" if
|
|
96
|
+
enabled && frame_type == :texel
|
|
97
|
+
|
|
98
|
+
@reuse_buffer = enabled
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def close
|
|
102
|
+
return if closed?
|
|
103
|
+
|
|
104
|
+
Native.scale.sws_freeContext(@scale) if @scale && !@scale.null?
|
|
105
|
+
free_pointer(@frame, :av_frame_free, Native.util)
|
|
106
|
+
free_pointer(@packet, :av_packet_free, Native.codec)
|
|
107
|
+
free_pointer(@codec_context, :avcodec_free_context, Native.codec)
|
|
108
|
+
if @format_context && !@format_context.null?
|
|
109
|
+
@format_holder.write_pointer(@format_context)
|
|
110
|
+
Native.format.avformat_close_input(@format_holder)
|
|
111
|
+
end
|
|
112
|
+
@format_context = @codec_context = @frame = @packet = @scale = nil
|
|
113
|
+
@closed = true
|
|
114
|
+
nil
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def closed?
|
|
118
|
+
@closed == true
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def open_video_stream
|
|
124
|
+
decoder_holder = FFI::MemoryPointer.new(:pointer)
|
|
125
|
+
@stream_index = Native.format.av_find_best_stream(
|
|
126
|
+
@format_context, Native::AVMEDIA_TYPE_VIDEO, -1, -1, decoder_holder, 0
|
|
127
|
+
)
|
|
128
|
+
Native.check(@stream_index, "av_find_best_stream(video)")
|
|
129
|
+
decoder = decoder_holder.read_pointer
|
|
130
|
+
raise Error, "FFmpeg did not return a video decoder" if decoder.null?
|
|
131
|
+
|
|
132
|
+
@stream = stream_at(@stream_index)
|
|
133
|
+
@codec_context = Native.codec.avcodec_alloc_context3(decoder)
|
|
134
|
+
raise Error, "avcodec_alloc_context3 returned NULL" if @codec_context.null?
|
|
135
|
+
|
|
136
|
+
parameters = Native::Layout.read_pointer(@stream, :stream, :codecpar)
|
|
137
|
+
Native.check(
|
|
138
|
+
Native.codec.avcodec_parameters_to_context(@codec_context, parameters),
|
|
139
|
+
"avcodec_parameters_to_context"
|
|
140
|
+
)
|
|
141
|
+
Native.check(
|
|
142
|
+
Native.codec.avcodec_open2(@codec_context, decoder, nil),
|
|
143
|
+
"avcodec_open2"
|
|
144
|
+
)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def allocate_decode_state
|
|
148
|
+
@frame = Native.util.av_frame_alloc
|
|
149
|
+
@packet = Native.codec.av_packet_alloc
|
|
150
|
+
raise Error, "failed to allocate an AVFrame or AVPacket" if @frame.null? || @packet.null?
|
|
151
|
+
|
|
152
|
+
@destination_data = FFI::MemoryPointer.new(:pointer)
|
|
153
|
+
@destination_stride = FFI::MemoryPointer.new(:int)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def load_metadata
|
|
157
|
+
@width = Native::Layout.read_int(@codec_context, :codec_context, :width)
|
|
158
|
+
@height = Native::Layout.read_int(@codec_context, :codec_context, :height)
|
|
159
|
+
raise Error, "invalid decoded dimensions #{@width}x#{@height}" unless
|
|
160
|
+
@width.positive? && @height.positive?
|
|
161
|
+
|
|
162
|
+
@time_base = Native::Layout.read_rational(@stream, :stream, :time_base)
|
|
163
|
+
real_rate = Native::Layout.read_rational(@stream, :stream, :r_frame_rate)
|
|
164
|
+
rate = real_rate.positive? ? real_rate : Native::Layout.read_rational(
|
|
165
|
+
@stream, :stream, :avg_frame_rate
|
|
166
|
+
)
|
|
167
|
+
@fps = rate.positive? ? rate : nil
|
|
168
|
+
|
|
169
|
+
stream_duration = Native::Layout.read_int64(@stream, :stream, :duration)
|
|
170
|
+
@duration = if valid_timestamp?(stream_duration)
|
|
171
|
+
stream_duration * @time_base
|
|
172
|
+
else
|
|
173
|
+
format_duration
|
|
174
|
+
end
|
|
175
|
+
frames = Native::Layout.read_int64(@stream, :stream, :nb_frames)
|
|
176
|
+
@frame_count = if frames.positive?
|
|
177
|
+
frames
|
|
178
|
+
elsif @duration && @fps
|
|
179
|
+
(@duration * @fps).round
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def receive_next_frame
|
|
184
|
+
loop do
|
|
185
|
+
result = Native.codec.avcodec_receive_frame(@codec_context, @frame)
|
|
186
|
+
return true if result.zero?
|
|
187
|
+
return false if result == Native::AVERROR_EOF
|
|
188
|
+
|
|
189
|
+
Native.check(result, "avcodec_receive_frame") unless result == Native::AVERROR_EAGAIN
|
|
190
|
+
return false unless feed_decoder
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def feed_decoder
|
|
195
|
+
if @input_eof
|
|
196
|
+
return true if send_flush_packet
|
|
197
|
+
|
|
198
|
+
return false
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
loop do
|
|
202
|
+
result = Native.format.av_read_frame(@format_context, @packet)
|
|
203
|
+
if result == Native::AVERROR_EOF
|
|
204
|
+
@input_eof = true
|
|
205
|
+
return send_flush_packet
|
|
206
|
+
end
|
|
207
|
+
Native.check(result, "av_read_frame")
|
|
208
|
+
|
|
209
|
+
packet_stream = Native::Layout.read_int(@packet, :packet, :stream_index)
|
|
210
|
+
if packet_stream == @stream_index
|
|
211
|
+
begin
|
|
212
|
+
Native.check(
|
|
213
|
+
Native.codec.avcodec_send_packet(@codec_context, @packet),
|
|
214
|
+
"avcodec_send_packet"
|
|
215
|
+
)
|
|
216
|
+
ensure
|
|
217
|
+
Native.codec.av_packet_unref(@packet)
|
|
218
|
+
end
|
|
219
|
+
return true
|
|
220
|
+
end
|
|
221
|
+
Native.codec.av_packet_unref(@packet)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def send_flush_packet
|
|
226
|
+
return false if @flush_sent
|
|
227
|
+
|
|
228
|
+
result = Native.codec.avcodec_send_packet(@codec_context, nil)
|
|
229
|
+
Native.check(result, "avcodec_send_packet(flush)") unless result == Native::AVERROR_EOF
|
|
230
|
+
@flush_sent = true
|
|
231
|
+
true
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def convert_frame
|
|
235
|
+
frame_width = Native::Layout.read_int(@frame, :frame, :width)
|
|
236
|
+
frame_height = Native::Layout.read_int(@frame, :frame, :height)
|
|
237
|
+
frame_format = Native::Layout.read_int(@frame, :frame, :format)
|
|
238
|
+
prepare_conversion(frame_width, frame_height, frame_format)
|
|
239
|
+
|
|
240
|
+
rows = Native.scale.sws_scale(
|
|
241
|
+
@scale,
|
|
242
|
+
Native::Layout.field_pointer(@frame, :frame, :data),
|
|
243
|
+
Native::Layout.field_pointer(@frame, :frame, :linesize),
|
|
244
|
+
0, frame_height, @destination_data, @destination_stride
|
|
245
|
+
)
|
|
246
|
+
Native.check(rows, "sws_scale(video decode)") if rows.negative?
|
|
247
|
+
raise Error, "sws_scale converted #{rows} rows, expected #{frame_height}" unless rows == frame_height
|
|
248
|
+
|
|
249
|
+
data = frame_data(@rgba_size)
|
|
250
|
+
timestamp = Native::Layout.read_int64(@frame, :frame, :best_effort_timestamp)
|
|
251
|
+
timestamp = Native::Layout.read_int64(@frame, :frame, :pts) unless valid_timestamp?(timestamp)
|
|
252
|
+
@decoded_pts = valid_timestamp?(timestamp) ? timestamp * @time_base : nil
|
|
253
|
+
build_frame(width: frame_width, height: frame_height, data:, pts: @decoded_pts)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def prepare_conversion(frame_width, frame_height, frame_format)
|
|
257
|
+
signature = [frame_width, frame_height, frame_format]
|
|
258
|
+
return if signature == @conversion_signature
|
|
259
|
+
|
|
260
|
+
Native.scale.sws_freeContext(@scale) if @scale && !@scale.null?
|
|
261
|
+
@scale = Native.scale.sws_getContext(
|
|
262
|
+
frame_width, frame_height, frame_format,
|
|
263
|
+
frame_width, frame_height, Native.util.av_get_pix_fmt("rgba"),
|
|
264
|
+
Native::SWS_BICUBIC, nil, nil, nil
|
|
265
|
+
)
|
|
266
|
+
raise Error, "sws_getContext returned NULL" if @scale.null?
|
|
267
|
+
|
|
268
|
+
@rgba_size = frame_width * frame_height * 4
|
|
269
|
+
@rgba_native = FFI::MemoryPointer.new(:uint8, @rgba_size)
|
|
270
|
+
@destination_data.put_pointer(0, @rgba_native)
|
|
271
|
+
@destination_stride.write_int(frame_width * 4)
|
|
272
|
+
@conversion_signature = signature
|
|
273
|
+
@width = frame_width
|
|
274
|
+
@height = frame_height
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def frame_data(size)
|
|
278
|
+
decoded = @rgba_native.read_string_length(size)
|
|
279
|
+
return decoded unless reuse_buffer
|
|
280
|
+
|
|
281
|
+
@reused_data ||= String.new(encoding: Encoding::BINARY)
|
|
282
|
+
@reused_data.replace(decoded)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def build_frame(width:, height:, data:, pts:)
|
|
286
|
+
return TexelSupport.image(width:, height:, data:) if frame_type == :texel
|
|
287
|
+
|
|
288
|
+
VideoFrame.new(width:, height:, data:, pts:)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def normalize_frame_type(value)
|
|
292
|
+
value = value.to_sym
|
|
293
|
+
return value if FRAME_TYPES.include?(value)
|
|
294
|
+
|
|
295
|
+
raise ArgumentError, "frame_type must be :video_frame or :texel"
|
|
296
|
+
rescue NoMethodError
|
|
297
|
+
raise ArgumentError, "frame_type must be :video_frame or :texel"
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def before_seek_target?(pts)
|
|
301
|
+
@seek_target && pts && pts < @seek_target
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def reset_decode_state
|
|
305
|
+
@input_eof = false
|
|
306
|
+
@flush_sent = false
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def stream_at(index)
|
|
310
|
+
streams = Native::Layout.read_pointer(@format_context, :format_context, :streams)
|
|
311
|
+
streams.get_pointer(index * FFI.type_size(:pointer))
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def format_duration
|
|
315
|
+
value = Native::Layout.read_int64(@format_context, :format_context, :duration)
|
|
316
|
+
return unless valid_timestamp?(value)
|
|
317
|
+
|
|
318
|
+
Rational(value, Native::AV_TIME_BASE)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def valid_timestamp?(value)
|
|
322
|
+
value && value != Native::AV_NOPTS_VALUE && value >= 0
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def free_pointer(pointer, function, library)
|
|
326
|
+
return unless pointer && !pointer.null?
|
|
327
|
+
|
|
328
|
+
holder = FFI::MemoryPointer.new(:pointer)
|
|
329
|
+
holder.write_pointer(pointer)
|
|
330
|
+
library.public_send(function, holder)
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
end
|