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