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,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
|
data/lib/libav/errors.rb
ADDED
|
@@ -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
|
data/lib/libav/native.rb
ADDED
|
@@ -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
|