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,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "output_context"
4
+ require_relative "audio_encoder"
5
+
6
+ module LibAV
7
+ class AudioWriter
8
+ DEFAULT_CODECS = {
9
+ ".wav" => :pcm_f32le,
10
+ ".webm" => :opus,
11
+ ".opus" => :opus,
12
+ ".ogg" => :opus
13
+ }.freeze
14
+
15
+ attr_reader :sample_rate, :channels
16
+
17
+ def self.open(path, **options)
18
+ writer = new(path, **options)
19
+ return writer unless block_given?
20
+
21
+ begin
22
+ yield writer
23
+ ensure
24
+ writer.close
25
+ end
26
+ end
27
+
28
+ def initialize(path, codec: nil, **options)
29
+ @output = OutputContext.new(path)
30
+ codec ||= DEFAULT_CODECS.fetch(File.extname(File.path(path)).downcase, :aac)
31
+ @encoder = AudioEncoder.new(@output, codec:, **options)
32
+ @sample_rate = @encoder.sample_rate
33
+ @channels = @encoder.channels
34
+ @output.open!
35
+ rescue StandardError
36
+ close_resources
37
+ raise
38
+ end
39
+
40
+ def <<(pcm_f32_interleaved)
41
+ push(pcm_f32_interleaved)
42
+ end
43
+
44
+ def push(pcm_f32_interleaved)
45
+ raise ClosedError, "audio writer is closed" if closed?
46
+
47
+ @encoder.push(pcm_f32_interleaved)
48
+ self
49
+ end
50
+
51
+ def close
52
+ return if closed?
53
+
54
+ error = nil
55
+ begin
56
+ @encoder.flush
57
+ rescue StandardError => caught
58
+ error = caught
59
+ ensure
60
+ cleanup_error = close_resources
61
+ error ||= cleanup_error
62
+ @closed = true
63
+ end
64
+ raise error if error
65
+
66
+ nil
67
+ end
68
+
69
+ def closed?
70
+ @closed == true
71
+ end
72
+
73
+ private
74
+
75
+ def close_resources
76
+ error = nil
77
+ begin
78
+ @output&.close
79
+ rescue StandardError => caught
80
+ error = caught
81
+ ensure
82
+ @encoder&.close
83
+ end
84
+ error
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LibAV
4
+ class Error < StandardError; end
5
+
6
+ class NativeError < Error
7
+ attr_reader :code, :operation
8
+
9
+ def initialize(code, operation = nil)
10
+ @code = Integer(code)
11
+ @operation = operation
12
+
13
+ prefix = operation ? "#{operation}: " : ""
14
+ super("#{prefix}#{Native.error_string(@code)} (#{@code})")
15
+ end
16
+ end
17
+
18
+ class ClosedError < Error; end
19
+ class EncodingError < Error; end
20
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LibAV
4
+ module Native
5
+ module Layout
6
+ module_function
7
+
8
+ def for_codec_major(major)
9
+ require_relative "v#{major}/layout"
10
+ const_get(:"V#{major}")
11
+ end
12
+
13
+ def read_int(pointer, type, field)
14
+ pointer.get_int32(offset(type, field))
15
+ end
16
+
17
+ def write_int(pointer, type, field, value)
18
+ pointer.put_int32(offset(type, field), Integer(value))
19
+ end
20
+
21
+ def read_int64(pointer, type, field)
22
+ pointer.get_int64(offset(type, field))
23
+ end
24
+
25
+ def write_int64(pointer, type, field, value)
26
+ pointer.put_int64(offset(type, field), Integer(value))
27
+ end
28
+
29
+ def read_pointer(pointer, type, field)
30
+ pointer.get_pointer(offset(type, field))
31
+ end
32
+
33
+ def write_pointer(pointer, type, field, value)
34
+ pointer.put_pointer(offset(type, field), value)
35
+ end
36
+
37
+ def read_rational(pointer, type, field)
38
+ position = offset(type, field)
39
+ numerator = pointer.get_int32(position)
40
+ denominator = pointer.get_int32(position + 4)
41
+ Rational(numerator, denominator.zero? ? 1 : denominator)
42
+ end
43
+
44
+ def write_rational(pointer, type, field, value)
45
+ rational = value.to_r
46
+ position = offset(type, field)
47
+ pointer.put_int32(position, rational.numerator)
48
+ pointer.put_int32(position + 4, rational.denominator)
49
+ end
50
+
51
+ def field_pointer(pointer, type, field)
52
+ pointer + offset(type, field)
53
+ end
54
+
55
+ def offset(type, field)
56
+ Native.layout.fetch(type).fetch(field)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LibAV
4
+ module Native
5
+ module Layout
6
+ V60 = {
7
+ output_format: {flags: 44},
8
+ format_context: {oformat: 16, pb: 32, nb_streams: 44, streams: 48, duration: 72},
9
+ stream: {
10
+ index: 8, codecpar: 16, time_base: 32, start_time: 40, duration: 48,
11
+ nb_frames: 56, avg_frame_rate: 88, r_frame_rate: 216
12
+ },
13
+ codec_context: {
14
+ codec_type: 12, priv_data: 32, bit_rate: 56, flags: 76, time_base: 100,
15
+ width: 116, height: 120, gop_size: 132, pix_fmt: 136, sample_rate: 352,
16
+ sample_fmt: 360, frame_size: 364, framerate: 704, ch_layout: 912
17
+ },
18
+ frame: {
19
+ data: 0, linesize: 64, extended_data: 96, width: 104, height: 108,
20
+ nb_samples: 112, format: 116, pts: 136, sample_rate: 208,
21
+ best_effort_timestamp: 344, ch_layout: 448, duration: 472
22
+ },
23
+ packet: {
24
+ pts: 8, dts: 16, data: 24, size: 32, stream_index: 36,
25
+ duration: 64, time_base: 96
26
+ }
27
+ }.freeze
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LibAV
4
+ module Native
5
+ module Layout
6
+ V61 = {
7
+ output_format: {flags: 44},
8
+ format_context: {oformat: 16, pb: 32, nb_streams: 44, streams: 48, duration: 104},
9
+ stream: {
10
+ index: 8, codecpar: 16, time_base: 32, start_time: 40, duration: 48,
11
+ nb_frames: 56, avg_frame_rate: 88, r_frame_rate: 216
12
+ },
13
+ codec_context: {
14
+ codec_type: 12, priv_data: 32, bit_rate: 56, flags: 64, time_base: 84,
15
+ framerate: 100, width: 116, height: 120, pix_fmt: 140, gop_size: 332,
16
+ sample_rate: 344, sample_fmt: 348, ch_layout: 352, frame_size: 376
17
+ },
18
+ frame: {
19
+ data: 0, linesize: 64, extended_data: 96, width: 104, height: 108,
20
+ nb_samples: 112, format: 116, pts: 136, sample_rate: 192,
21
+ best_effort_timestamp: 320, ch_layout: 408, duration: 432
22
+ },
23
+ packet: {
24
+ pts: 8, dts: 16, data: 24, size: 32, stream_index: 36,
25
+ duration: 64, time_base: 96
26
+ }
27
+ }.freeze
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,311 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+ require_relative "native/layout"
5
+
6
+ module LibAV
7
+ module Native
8
+ class RationalValue < FFI::Struct
9
+ layout :numerator, :int,
10
+ :denominator, :int
11
+ end
12
+
13
+ SUPPORTED = {
14
+ 60 => {avformat: 60, avutil: 58, swscale: 7, swresample: 4},
15
+ 61 => {avformat: 61, avutil: 59, swscale: 8, swresample: 5}
16
+ }.freeze
17
+
18
+ AVERROR_EOF = -541_478_725
19
+ AVERROR_EAGAIN = -Errno::EAGAIN::Errno
20
+ AV_NOPTS_VALUE = -(1 << 63)
21
+ AV_TIME_BASE = 1_000_000
22
+ AVIO_FLAG_WRITE = 2
23
+ AVSEEK_FLAG_BACKWARD = 1
24
+ AVFMT_NOFILE = 0x0001
25
+ AVFMT_GLOBALHEADER = 0x0040
26
+ AV_CODEC_FLAG_GLOBAL_HEADER = 1 << 22
27
+ AVMEDIA_TYPE_VIDEO = 0
28
+ AVMEDIA_TYPE_AUDIO = 1
29
+ AV_CHANNEL_LAYOUT_SIZE = 24
30
+ SWS_BICUBIC = 4
31
+ AV_SAMPLE_FMT_FLT = 3
32
+ LOG_LEVELS = {
33
+ quiet: -8, panic: 0, fatal: 8, error: 16,
34
+ warn: 24, info: 32, verbose: 40, debug: 48, trace: 56
35
+ }.freeze
36
+
37
+ COMPONENT_NAMES = {
38
+ avcodec: "avcodec",
39
+ avformat: "avformat",
40
+ avutil: "avutil",
41
+ swscale: "swscale",
42
+ swresample: "swresample"
43
+ }.freeze
44
+
45
+ class << self
46
+ attr_reader :codec, :format, :util, :scale, :resample, :layout, :versions
47
+
48
+ def load!
49
+ return if @loaded
50
+ raise LoadError, "libav-ruby requires a 64-bit Ruby process" unless FFI.type_size(:pointer) == 8
51
+
52
+ @codec = library(:avcodec, [61, 60], allow_unversioned: true)
53
+ attach(@codec, :avcodec_version, [], :uint)
54
+ codec_major = version_major(@codec.avcodec_version)
55
+ expected = SUPPORTED[codec_major]
56
+ raise unsupported_version(codec_major) unless expected
57
+
58
+ @format = library(:avformat, [expected[:avformat]])
59
+ @util = library(:avutil, [expected[:avutil]])
60
+ @scale = library(:swscale, [expected[:swscale]])
61
+ @resample = library(:swresample, [expected[:swresample]])
62
+ bind_functions
63
+ detect_versions(codec_major)
64
+ validate_versions!(expected)
65
+ @layout = Layout.for_codec_major(codec_major)
66
+ @loaded = true
67
+ rescue FFI::NotFoundError => e
68
+ raise LoadError, "Unable to load FFmpeg shared libraries: #{e.message}. Install FFmpeg 6 or 7."
69
+ end
70
+
71
+ def check(code, operation = nil)
72
+ raise NativeError.new(code, operation) if code.negative?
73
+
74
+ code
75
+ end
76
+
77
+ def control_flow?(code)
78
+ code == AVERROR_EAGAIN || code == AVERROR_EOF
79
+ end
80
+
81
+ def error_string(code)
82
+ buffer = FFI::MemoryPointer.new(:char, 256)
83
+ result = @util.av_strerror(code, buffer, buffer.size)
84
+ return "FFmpeg error" if result.negative?
85
+
86
+ buffer.read_string
87
+ end
88
+
89
+ def version_major(version)
90
+ version >> 16
91
+ end
92
+
93
+ def rational(value)
94
+ value = value.to_r
95
+ RationalValue.new.tap do |rational|
96
+ rational[:numerator] = value.numerator
97
+ rational[:denominator] = value.denominator
98
+ end
99
+ end
100
+
101
+ def log_level=(level)
102
+ numeric = level.is_a?(Integer) ? level : LOG_LEVELS.fetch(level.to_sym) do
103
+ raise ArgumentError, "unknown FFmpeg log level: #{level.inspect}"
104
+ end
105
+ @util.av_log_set_level(numeric)
106
+ end
107
+
108
+ def install_log_callback!
109
+ return if @log_callback
110
+
111
+ @log_callback = proc do |context, level, format, arguments|
112
+ forward_log(context, level, format, arguments)
113
+ rescue StandardError
114
+ nil
115
+ end
116
+ @util.av_log_set_callback(@log_callback)
117
+ end
118
+
119
+ private
120
+
121
+ def bind_functions
122
+ bind_version_functions
123
+ bind_util_functions
124
+ bind_codec_functions
125
+ bind_format_functions
126
+ bind_scale_functions
127
+ bind_resample_functions
128
+ end
129
+
130
+ def bind_version_functions
131
+ attach(@format, :avformat_version, [], :uint)
132
+ attach(@util, :avutil_version, [], :uint)
133
+ attach(@scale, :swscale_version, [], :uint)
134
+ attach(@resample, :swresample_version, [], :uint)
135
+ end
136
+
137
+ def bind_util_functions
138
+ attach(@util, :av_strerror, %i[int pointer size_t], :int)
139
+ attach(@util, :av_get_pix_fmt, [:string], :int)
140
+ attach(@util, :av_get_sample_fmt, [:string], :int)
141
+ attach(@util, :av_opt_set, %i[pointer string string int], :int)
142
+ attach(@util, :av_opt_set_int, %i[pointer string int64 int], :int)
143
+ attach(@util, :av_opt_set_q, %i[pointer string av_rational int], :int)
144
+ attach(@util, :av_frame_alloc, [], :pointer)
145
+ attach(@util, :av_frame_free, [:pointer], :void)
146
+ attach(@util, :av_frame_get_buffer, %i[pointer int], :int)
147
+ attach(@util, :av_frame_make_writable, [:pointer], :int)
148
+ attach(@util, :av_frame_unref, [:pointer], :void)
149
+ attach(@util, :av_audio_fifo_alloc, %i[int int int], :pointer)
150
+ attach(@util, :av_audio_fifo_free, [:pointer], :void)
151
+ attach(@util, :av_audio_fifo_realloc, %i[pointer int], :int)
152
+ attach(@util, :av_audio_fifo_size, [:pointer], :int)
153
+ attach(@util, :av_audio_fifo_write, %i[pointer pointer int], :int)
154
+ attach(@util, :av_audio_fifo_read, %i[pointer pointer int], :int)
155
+ attach(@util, :av_samples_alloc_array_and_samples,
156
+ %i[pointer pointer int int int int], :int)
157
+ attach(@util, :av_samples_set_silence, %i[pointer int int int int], :int)
158
+ attach(@util, :av_freep, [:pointer], :void)
159
+ attach(@util, :av_channel_layout_default, %i[pointer int], :void)
160
+ attach(@util, :av_channel_layout_copy, %i[pointer pointer], :int)
161
+ attach(@util, :av_channel_layout_uninit, [:pointer], :void)
162
+ attach(@util, :av_log_set_level, [:int], :void)
163
+ attach(@util, :av_log_format_line2,
164
+ %i[pointer int string pointer pointer int pointer], :int)
165
+ @util.callback(:libav_log_callback, %i[pointer int string pointer], :void)
166
+ attach(@util, :av_log_set_callback, [:libav_log_callback], :void)
167
+ end
168
+
169
+ def bind_codec_functions
170
+ attach(@codec, :avcodec_find_encoder, [:int], :pointer)
171
+ attach(@codec, :avcodec_find_encoder_by_name, [:string], :pointer)
172
+ attach(@codec, :avcodec_find_decoder, [:int], :pointer)
173
+ attach(@codec, :avcodec_alloc_context3, [:pointer], :pointer)
174
+ attach(@codec, :avcodec_free_context, [:pointer], :void)
175
+ attach(@codec, :avcodec_open2, %i[pointer pointer pointer], :int, blocking: true)
176
+ attach(@codec, :avcodec_parameters_from_context, %i[pointer pointer], :int)
177
+ attach(@codec, :avcodec_parameters_to_context, %i[pointer pointer], :int)
178
+ attach(@codec, :avcodec_send_frame, %i[pointer pointer], :int, blocking: true)
179
+ attach(@codec, :avcodec_receive_packet, %i[pointer pointer], :int, blocking: true)
180
+ attach(@codec, :avcodec_send_packet, %i[pointer pointer], :int, blocking: true)
181
+ attach(@codec, :avcodec_receive_frame, %i[pointer pointer], :int, blocking: true)
182
+ attach(@codec, :avcodec_flush_buffers, [:pointer], :void)
183
+ attach(@codec, :av_packet_alloc, [], :pointer)
184
+ attach(@codec, :av_packet_free, [:pointer], :void)
185
+ attach(@codec, :av_packet_unref, [:pointer], :void)
186
+ attach(@codec, :av_packet_rescale_ts, %i[pointer av_rational av_rational], :void)
187
+ end
188
+
189
+ def bind_format_functions
190
+ attach(@format, :avformat_alloc_output_context2, %i[pointer pointer string string], :int)
191
+ attach(@format, :avformat_new_stream, %i[pointer pointer], :pointer)
192
+ attach(@format, :avformat_write_header, %i[pointer pointer], :int, blocking: true)
193
+ attach(@format, :av_interleaved_write_frame, %i[pointer pointer], :int, blocking: true)
194
+ attach(@format, :av_write_trailer, [:pointer], :int, blocking: true)
195
+ attach(@format, :avformat_free_context, [:pointer], :void)
196
+ attach(@format, :avio_open, %i[pointer string int], :int, blocking: true)
197
+ attach(@format, :avio_closep, [:pointer], :int, blocking: true)
198
+ attach(@format, :avformat_open_input, %i[pointer string pointer pointer], :int, blocking: true)
199
+ attach(@format, :avformat_find_stream_info, %i[pointer pointer], :int, blocking: true)
200
+ attach(@format, :av_find_best_stream, %i[pointer int int int pointer int], :int)
201
+ attach(@format, :av_read_frame, %i[pointer pointer], :int, blocking: true)
202
+ attach(@format, :av_seek_frame, %i[pointer int int64 int], :int, blocking: true)
203
+ attach(@format, :avformat_close_input, [:pointer], :void)
204
+ end
205
+
206
+ def bind_scale_functions
207
+ attach(@scale, :sws_getContext,
208
+ %i[int int int int int int int pointer pointer pointer], :pointer)
209
+ attach(@scale, :sws_scale, %i[pointer pointer pointer int int pointer pointer], :int,
210
+ blocking: true)
211
+ attach(@scale, :sws_freeContext, [:pointer], :void)
212
+ end
213
+
214
+ def bind_resample_functions
215
+ attach(@resample, :swr_alloc_set_opts2,
216
+ %i[pointer pointer int int pointer int int int pointer], :int)
217
+ attach(@resample, :swr_init, [:pointer], :int)
218
+ attach(@resample, :swr_convert, %i[pointer pointer int pointer int], :int, blocking: true)
219
+ attach(@resample, :swr_get_delay, %i[pointer int64], :int64)
220
+ attach(@resample, :swr_get_out_samples, %i[pointer int], :int)
221
+ attach(@resample, :swr_free, [:pointer], :void)
222
+ end
223
+
224
+ def attach(library, name, arguments, returns, **options)
225
+ library.attach_function(name, arguments, returns, options)
226
+ end
227
+
228
+ def library(component, majors, allow_unversioned: false)
229
+ candidates = library_candidates(component, majors, allow_unversioned:)
230
+ Module.new.tap do |library|
231
+ library.extend(FFI::Library)
232
+ library.typedef(RationalValue.by_value, :av_rational)
233
+ library.ffi_lib(candidates)
234
+ end
235
+ end
236
+
237
+ def library_candidates(component, majors, allow_unversioned:)
238
+ base = COMPONENT_NAMES.fetch(component)
239
+ names = majors.flat_map do |major|
240
+ ["lib#{base}.#{major}.dylib", "lib#{base}.so.#{major}", "#{base}-#{major}.dll", "#{base}-#{major}"]
241
+ end
242
+ directories = library_directories
243
+ names.concat(["lib#{base}.dylib", "lib#{base}.so", base]) if
244
+ allow_unversioned || ENV["LIBAV_LIBRARY_PATH"]
245
+ directories.flat_map { |directory| names.map { |name| File.join(directory, name) } } + names
246
+ end
247
+
248
+ def library_directories
249
+ [
250
+ ENV["LIBAV_LIBRARY_PATH"],
251
+ "/opt/homebrew/opt/ffmpeg@7/lib",
252
+ "/opt/homebrew/opt/ffmpeg@6/lib",
253
+ "/usr/local/opt/ffmpeg@7/lib",
254
+ "/usr/local/opt/ffmpeg@6/lib"
255
+ ].compact
256
+ end
257
+
258
+ def detect_versions(codec_major)
259
+ @versions = {
260
+ avcodec: codec_major,
261
+ avformat: version_major(@format.avformat_version),
262
+ avutil: version_major(@util.avutil_version),
263
+ swscale: version_major(@scale.swscale_version),
264
+ swresample: version_major(@resample.swresample_version)
265
+ }.freeze
266
+ end
267
+
268
+ def validate_versions!(expected)
269
+ mismatches = expected.filter_map do |component, major|
270
+ actual = @versions.fetch(component)
271
+ "#{component}=#{actual} (expected #{major})" unless actual == major
272
+ end
273
+ return if mismatches.empty?
274
+
275
+ raise LoadError, "Incompatible FFmpeg library set: #{mismatches.join(", ")}"
276
+ end
277
+
278
+ def unsupported_version(codec_major)
279
+ LoadError.new(
280
+ "Unsupported libavcodec major #{codec_major}; libav-ruby supports FFmpeg 6/7 " \
281
+ "(libavcodec 60/61)"
282
+ )
283
+ end
284
+
285
+ def forward_log(context, level, format, arguments)
286
+ logger = LibAV.logger
287
+ return unless logger
288
+
289
+ buffer = FFI::MemoryPointer.new(:char, 2_048)
290
+ prefix = FFI::MemoryPointer.new(:int)
291
+ prefix.write_int(1)
292
+ @util.av_log_format_line2(
293
+ context, level, format, arguments, buffer, buffer.size, prefix
294
+ )
295
+ message = buffer.read_string.strip
296
+ return if message.empty?
297
+
298
+ logger.public_send(ruby_log_level(level), message)
299
+ end
300
+
301
+ def ruby_log_level(level)
302
+ return :fatal if level <= LOG_LEVELS[:fatal]
303
+ return :error if level <= LOG_LEVELS[:error]
304
+ return :warn if level <= LOG_LEVELS[:warn]
305
+ return :info if level <= LOG_LEVELS[:info]
306
+
307
+ :debug
308
+ end
309
+ end
310
+ end
311
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LibAV
4
+ class OutputContext
5
+ attr_reader :pointer
6
+
7
+ def initialize(path)
8
+ @path = File.path(path)
9
+ @pointer_holder = FFI::MemoryPointer.new(:pointer)
10
+ result = Native.format.avformat_alloc_output_context2(
11
+ @pointer_holder, nil, nil, @path
12
+ )
13
+ Native.check(result, "avformat_alloc_output_context2")
14
+ @pointer = @pointer_holder.read_pointer
15
+ raise EncodingError, "FFmpeg could not determine an output container for #{@path}" if @pointer.null?
16
+
17
+ @io_open = false
18
+ @header_written = false
19
+ @trailer_written = false
20
+ end
21
+
22
+ def new_stream(codec)
23
+ stream = Native.format.avformat_new_stream(pointer, codec)
24
+ raise EncodingError, "avformat_new_stream returned NULL" if stream.null?
25
+
26
+ stream
27
+ end
28
+
29
+ def global_header?
30
+ (output_flags & Native::AVFMT_GLOBALHEADER) != 0
31
+ end
32
+
33
+ def open!
34
+ open_io unless (output_flags & Native::AVFMT_NOFILE) != 0
35
+ Native.check(Native.format.avformat_write_header(pointer, nil), "avformat_write_header")
36
+ @header_written = true
37
+ end
38
+
39
+ def write_packet(packet, codec_time_base, stream)
40
+ stream_time_base = Native::Layout.read_rational(stream, :stream, :time_base)
41
+ Native.codec.av_packet_rescale_ts(
42
+ packet, Native.rational(codec_time_base), Native.rational(stream_time_base)
43
+ )
44
+ stream_index = Native::Layout.read_int(stream, :stream, :index)
45
+ Native::Layout.write_int(packet, :packet, :stream_index, stream_index)
46
+ Native.check(
47
+ Native.format.av_interleaved_write_frame(pointer, packet),
48
+ "av_interleaved_write_frame"
49
+ )
50
+ end
51
+
52
+ def close
53
+ return if @closed
54
+
55
+ error = write_trailer
56
+ io_error = close_io
57
+ error ||= io_error
58
+ Native.format.avformat_free_context(pointer) if pointer && !pointer.null?
59
+ @pointer = nil
60
+ @closed = true
61
+ raise error if error
62
+ end
63
+
64
+ private
65
+
66
+ def output_flags
67
+ output_format = Native::Layout.read_pointer(pointer, :format_context, :oformat)
68
+ Native::Layout.read_int(output_format, :output_format, :flags)
69
+ end
70
+
71
+ def open_io
72
+ pb = Native::Layout.field_pointer(pointer, :format_context, :pb)
73
+ Native.check(Native.format.avio_open(pb, @path, Native::AVIO_FLAG_WRITE), "avio_open")
74
+ @io_open = true
75
+ end
76
+
77
+ def write_trailer
78
+ return unless @header_written
79
+ return if @trailer_written
80
+
81
+ result = Native.format.av_write_trailer(pointer)
82
+ @trailer_written = true
83
+ NativeError.new(result, "av_write_trailer") if result.negative?
84
+ end
85
+
86
+ def close_io
87
+ return unless @io_open
88
+
89
+ pb = Native::Layout.field_pointer(pointer, :format_context, :pb)
90
+ result = Native.format.avio_closep(pb)
91
+ @io_open = false
92
+ NativeError.new(result, "avio_closep") if result.negative?
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LibAV
4
+ module TexelSupport
5
+ module_function
6
+
7
+ def image(width:, height:, data:)
8
+ load!
9
+ Texel::Image.new(
10
+ width:, height:, channels: 4, dtype: :u8,
11
+ color_space: :srgb, data:
12
+ )
13
+ end
14
+
15
+ def load!
16
+ return if defined?(Texel::Image)
17
+
18
+ require "texel"
19
+ rescue LoadError => error
20
+ raise LoadError,
21
+ "Texel output requires the optional 'texel' gem (#{error.message})"
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LibAV
4
+ VERSION = "0.1.0"
5
+ end
6
+
7
+ Libav = LibAV unless defined?(Libav)