ffi-ffmpeg 0.5.8135c9c5dc36
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.
- data/.hgignore +10 -0
- data/Gemfile +4 -0
- data/README.md +70 -0
- data/Rakefile +1 -0
- data/examples/extract_keyframes.rb +47 -0
- data/examples/read_file.rb +46 -0
- data/examples/seek.rb +49 -0
- data/ffi-ffmpeg.gemspec +26 -0
- data/lib/ffi-ffmpeg.rb +30 -0
- data/lib/ffi/ffmpeg.rb +629 -0
- data/lib/ffmpeg/frame.rb +58 -0
- data/lib/ffmpeg/reader.rb +74 -0
- data/lib/ffmpeg/stream.rb +244 -0
- data/lib/ffmpeg/version.rb +3 -0
- metadata +72 -0
data/lib/ffmpeg/frame.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'ffi/ffmpeg'
|
2
|
+
|
3
|
+
module FFmpeg::Frame; end
|
4
|
+
|
5
|
+
class FFmpeg::Frame::Video
|
6
|
+
include FFI::FFmpeg
|
7
|
+
|
8
|
+
attr_reader :av_frame, :width, :height, :pixel_format, :stream
|
9
|
+
attr_accessor :pts, :number
|
10
|
+
|
11
|
+
def initialize(p={})
|
12
|
+
@stream = p[:stream] or raise ArgumentError, "no :stream"
|
13
|
+
@width = p[:width] or raise ArgumentError, "no :width"
|
14
|
+
@height = p[:height] or raise ArgumentError, "no :height"
|
15
|
+
@pixel_format = p[:pixel_format] or
|
16
|
+
raise ArgumentError "no :pixel_format"
|
17
|
+
@av_frame = avcodec_alloc_frame or
|
18
|
+
raise NoMemoryError "avcodec_alloc_frame() failed"
|
19
|
+
@av_frame = AVFrame.new @av_frame
|
20
|
+
|
21
|
+
# Set up our finalizer which calls av_free() on the av_frame.
|
22
|
+
ObjectSpace.define_finalizer(self, self.class.method(:finalize).to_proc)
|
23
|
+
|
24
|
+
bytes = avpicture_get_size(@pixel_format, @width, @height)
|
25
|
+
@buffer = FFI::MemoryPointer.new(:uchar, bytes)
|
26
|
+
avpicture_fill(@av_frame, @buffer, @pixel_format, @width, @height)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.finalize(id)
|
30
|
+
av_free(@av_frame)
|
31
|
+
end
|
32
|
+
|
33
|
+
def key_frame?
|
34
|
+
@av_frame[:key_frame] == 1
|
35
|
+
end
|
36
|
+
|
37
|
+
def scale(p={})
|
38
|
+
width = p[:width] || @width
|
39
|
+
height = p[:height] || @height
|
40
|
+
pixel_format = p[:pixel_format] || @pixel_format
|
41
|
+
out = FFmpeg::Frame::Video.new(:width => width, :height => height,
|
42
|
+
:pixel_format => pixel_format,
|
43
|
+
:stream => stream)
|
44
|
+
|
45
|
+
scale_ctx = sws_getContext(@width, @height, @pixel_format,
|
46
|
+
width, height, pixel_format,
|
47
|
+
:bicubic, nil, nil, nil) or
|
48
|
+
raise NoMemoryError, "sws_getContext() failed"
|
49
|
+
|
50
|
+
rc = sws_scale(scale_ctx, @av_frame[:data], @av_frame[:linesize], 0,
|
51
|
+
@height, out.av_frame[:data], out.av_frame[:linesize])
|
52
|
+
sws_freeContext(scale_ctx)
|
53
|
+
|
54
|
+
out.pts = @pts
|
55
|
+
out.number = @number
|
56
|
+
out
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'ffi/ffmpeg'
|
2
|
+
|
3
|
+
class FFmpeg::Reader
|
4
|
+
include FFI::FFmpeg
|
5
|
+
|
6
|
+
attr_reader :filename, :streams, :av_format_ctx
|
7
|
+
|
8
|
+
def initialize(filename, p={})
|
9
|
+
@filename = filename or raise ArgumentError, "No filename"
|
10
|
+
|
11
|
+
FFmpeg.register_all
|
12
|
+
@av_format_ctx = FFI::MemoryPointer.new(:pointer)
|
13
|
+
rc = av_open_input_file(@av_format_ctx, @filename, nil, 0, nil)
|
14
|
+
raise RuntimeError, "av_open_input_file() failed, filename='%s', rc=%d" %
|
15
|
+
[filename, rc] if rc != 0
|
16
|
+
@av_format_ctx = AVFormatContext.new @av_format_ctx.get_pointer(0)
|
17
|
+
|
18
|
+
rc = av_find_stream_info(@av_format_ctx)
|
19
|
+
raise RuntimeError, "av_find_stream_info() failed, rc=#{rc}" if rc < 0
|
20
|
+
|
21
|
+
initialize_streams(p)
|
22
|
+
end
|
23
|
+
|
24
|
+
def dump_format
|
25
|
+
FFI::FFmpeg.dump_format(@av_format_ctx, 0, @filename, 0)
|
26
|
+
end
|
27
|
+
|
28
|
+
def each_frame(&block)
|
29
|
+
raise ArgumentError, "No block provided" unless block_given?
|
30
|
+
|
31
|
+
packet = avcodec_alloc_frame or
|
32
|
+
raise NoMemoryError, "avcodec_alloc_frame() failed"
|
33
|
+
packet = AVPacket.new packet
|
34
|
+
|
35
|
+
while av_read_frame(@av_format_ctx, packet) >= 0
|
36
|
+
frame = @streams[packet[:stream_index]].decode_frame(packet)
|
37
|
+
rc = frame ? yield(frame) : true
|
38
|
+
av_free_packet(packet)
|
39
|
+
|
40
|
+
break if rc == false
|
41
|
+
end
|
42
|
+
|
43
|
+
av_free(packet)
|
44
|
+
end
|
45
|
+
|
46
|
+
def default_stream
|
47
|
+
@streams[av_find_default_stream_index(@av_format_ctx)]
|
48
|
+
end
|
49
|
+
|
50
|
+
def seek(p = {})
|
51
|
+
default_stream.seek(p)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def initialize_streams(p={})
|
57
|
+
@streams = @av_format_ctx[:nb_streams].times.map do |i|
|
58
|
+
av_stream = AVStream.new @av_format_ctx[:streams][i]
|
59
|
+
av_codec_ctx = AVCodecContext.new av_stream[:codec]
|
60
|
+
|
61
|
+
case av_codec_ctx[:codec_type]
|
62
|
+
when :video
|
63
|
+
FFmpeg::Stream::Video.new(:reader => self,
|
64
|
+
:av_stream => av_stream,
|
65
|
+
:pixel_format => p[:pixel_format],
|
66
|
+
:width => p[:width],
|
67
|
+
:height => p[:height])
|
68
|
+
else
|
69
|
+
FFmpeg::Stream::Unsupported.new(:reader => self,
|
70
|
+
:av_stream => av_stream)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,244 @@
|
|
1
|
+
require 'ffi/ffmpeg'
|
2
|
+
require 'pp'
|
3
|
+
|
4
|
+
class String
|
5
|
+
def hexdump
|
6
|
+
buf = ""
|
7
|
+
offset = 0
|
8
|
+
words = self.unpack("N%d" % (self.length/4.0).ceil)
|
9
|
+
until words.empty?
|
10
|
+
line = words.shift(4).compact
|
11
|
+
buf += sprintf("[%04x] " + ("%08x " * line.size) + "|%s|\n",
|
12
|
+
offset * 16, *line,
|
13
|
+
line.pack("N%d" % line.size).tr("^\040-\176","."))
|
14
|
+
offset += 1
|
15
|
+
end
|
16
|
+
buf
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module FFmpeg::Stream
|
21
|
+
include FFI::FFmpeg
|
22
|
+
|
23
|
+
attr_reader :reader, :av_stream, :av_codec_ctx
|
24
|
+
|
25
|
+
def initialize(p={})
|
26
|
+
@reader = p[:reader] or raise ArgumentError, "no :reader"
|
27
|
+
@av_stream = p[:av_stream] or raise ArgumentError, "no :av_stream"
|
28
|
+
@av_codec_ctx = AVCodecContext.new @av_stream[:codec]
|
29
|
+
|
30
|
+
# open the codec
|
31
|
+
codec = avcodec_find_decoder(@av_codec_ctx[:codec_id]) or
|
32
|
+
raise RuntimeError, "No decoder found for #{@av_codec_ctx[:codec_id]}"
|
33
|
+
avcodec_open(@av_codec_ctx, codec) == 0 or
|
34
|
+
raise RuntimeError, "avcodec_open() failed"
|
35
|
+
end
|
36
|
+
|
37
|
+
def discard=(value)
|
38
|
+
@av_stream[:discard] = value
|
39
|
+
end
|
40
|
+
|
41
|
+
def discard
|
42
|
+
@av_stream[:discard]
|
43
|
+
end
|
44
|
+
|
45
|
+
def type
|
46
|
+
@av_codec_ctx[:codec_type]
|
47
|
+
end
|
48
|
+
|
49
|
+
def index
|
50
|
+
@av_stream[:index]
|
51
|
+
end
|
52
|
+
|
53
|
+
def decode_frame(packet)
|
54
|
+
return false
|
55
|
+
raise NotImplementedError, "decode_frame() not defined for #{self.class}"
|
56
|
+
end
|
57
|
+
|
58
|
+
def each_frame
|
59
|
+
@reader.each_frame { |frame| yield frame if frame.stream == self }
|
60
|
+
end
|
61
|
+
|
62
|
+
def next_frame
|
63
|
+
frame = nil
|
64
|
+
each_frame { |f| frame = f; break }
|
65
|
+
frame
|
66
|
+
end
|
67
|
+
|
68
|
+
def skip_frames(n)
|
69
|
+
raise RuntimeError, "Cannot skip frames when discarding all frames" if
|
70
|
+
discard == :all
|
71
|
+
each_frame { |f| n -= 1 != 0 }
|
72
|
+
end
|
73
|
+
|
74
|
+
# Seek to a specific location within the stream; the location can be either
|
75
|
+
# a PTS value or an absolute byte position.
|
76
|
+
#
|
77
|
+
# Arguments:
|
78
|
+
# [:pts] PTS location
|
79
|
+
# [:pos] Byte location
|
80
|
+
# [:backward] Seek backward
|
81
|
+
# [:any] Seek to non-key frames
|
82
|
+
#
|
83
|
+
def seek(p={})
|
84
|
+
p = { :pts => p } unless p.is_a? Hash
|
85
|
+
|
86
|
+
raise ArgumentError, ":pts and :pos are mutually exclusive" \
|
87
|
+
if p[:pts] and p[:pos]
|
88
|
+
|
89
|
+
pos = p[:pts] || p[:pos]
|
90
|
+
flags = 0
|
91
|
+
flags |= AVSEEK_FLAG_BYTE if p[:pos]
|
92
|
+
flags |= AVSEEK_FLAG_BACKWARD if p[:backward]
|
93
|
+
flags |= AVSEEK_FLAG_ANY if p[:any]
|
94
|
+
|
95
|
+
rc = av_seek_frame(@reader.av_format_ctx, @av_stream[:index], pos, flags)
|
96
|
+
raise RuntimeError, "av_seek_frame() failed, %d" % rc if rc < 0
|
97
|
+
true
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class FFmpeg::Stream::Video
|
102
|
+
include FFmpeg::Stream
|
103
|
+
|
104
|
+
attr_reader :raw_frame, :width, :height, :pixel_format, :buffer_size, :reader
|
105
|
+
|
106
|
+
def initialize(p={})
|
107
|
+
super(p)
|
108
|
+
@width = p[:widht] || @av_codec_ctx[:width]
|
109
|
+
@height = p[:height] || @av_codec_ctx[:height]
|
110
|
+
@pixel_format = p[:pixel_format] || @av_codec_ctx[:pix_fmt]
|
111
|
+
@buffer_size = p[:buffer_size] || 1
|
112
|
+
|
113
|
+
@raw_frame = FFmpeg::Frame::Video.new :stream => self,
|
114
|
+
:width => @av_codec_ctx[:width],
|
115
|
+
:height => @av_codec_ctx[:height],
|
116
|
+
:pixel_format =>
|
117
|
+
@av_codec_ctx[:pix_fmt]
|
118
|
+
|
119
|
+
@frame_finished = FFI::MemoryPointer.new :int
|
120
|
+
|
121
|
+
@scaling_initialized = false
|
122
|
+
@swscale_ctx = nil
|
123
|
+
@buffered_frames = nil
|
124
|
+
@last_pts = nil
|
125
|
+
|
126
|
+
# Callback function for storing the dts at the time of buffer
|
127
|
+
# allocation to later be used as pts.
|
128
|
+
@av_codec_ctx[:get_buffer] = @get_buffer_callback = \
|
129
|
+
FFI::Function.new(:void, [:pointer, :pointer]) do |ctx, frame|
|
130
|
+
ret = avcodec_default_get_buffer(ctx, frame)
|
131
|
+
AVFrame.new(frame)[:opaque] = FFI::Pointer.new(@last_pts)
|
132
|
+
# pp :alloc => @last_pts
|
133
|
+
ret
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def fps
|
138
|
+
@av_stream[:r_frame_rate]
|
139
|
+
end
|
140
|
+
|
141
|
+
def width=(width)
|
142
|
+
@scaling_initialized = false
|
143
|
+
@width = width
|
144
|
+
end
|
145
|
+
|
146
|
+
def height=(height)
|
147
|
+
@scaling_initialized = false
|
148
|
+
@height = height
|
149
|
+
end
|
150
|
+
|
151
|
+
def pixel_format=(pixel_format)
|
152
|
+
@scaling_initialized = false
|
153
|
+
@pixel_format = pixel_format
|
154
|
+
end
|
155
|
+
|
156
|
+
def buffer_size=(frames)
|
157
|
+
@scaling_initialized = false
|
158
|
+
@buffer_size = frames
|
159
|
+
end
|
160
|
+
|
161
|
+
def decode_frame(packet)
|
162
|
+
initialize_scaling unless @scaling_initialized
|
163
|
+
|
164
|
+
# pp :read => packet[:dts]
|
165
|
+
@last_pts = packet[:dts]
|
166
|
+
rc = avcodec_decode_video(@av_codec_ctx, @raw_frame.av_frame,
|
167
|
+
@frame_finished, packet[:data], packet[:size])
|
168
|
+
raise RuntimeError, "avcodec_decode_video() failed, rc=#{rc}" if rc < 0
|
169
|
+
|
170
|
+
return if @frame_finished.read_int == 0
|
171
|
+
# pp :finished => @raw_frame.av_frame[:opaque].address,
|
172
|
+
# :pts => @raw_frame.av_frame[:pts],
|
173
|
+
# :dts => packet[:dts],
|
174
|
+
# :type => @raw_frame.av_frame[:pict_type],
|
175
|
+
# :key_frame => @raw_frame.av_frame[:key_frame]
|
176
|
+
|
177
|
+
# avcodec_decode_video() returns frames in the correct pts order, and
|
178
|
+
# according to the dranger tutorial, the packet's dts is the frame's
|
179
|
+
# pts. When the dts has not been set (AV_NOPTS_VALUE) use the dts from
|
180
|
+
# the first packet of the frame which is stored in the :opaque field of
|
181
|
+
# the AVFrame.
|
182
|
+
@raw_frame.pts = nil
|
183
|
+
@raw_frame.pts = packet[:dts] unless packet[:dts] == AV_NOPTS_VALUE
|
184
|
+
@raw_frame.pts ||= @raw_frame.av_frame[:opaque].address
|
185
|
+
|
186
|
+
@raw_frame.number = @av_codec_ctx[:frame_number].to_i
|
187
|
+
|
188
|
+
return @raw_frame unless @swscale_ctx
|
189
|
+
|
190
|
+
# XXX Need to provide a better mechanism for making sure buffer is ready
|
191
|
+
# for use.
|
192
|
+
scaled_frame = @buffered_frames.shift
|
193
|
+
@buffered_frames << scaled_frame
|
194
|
+
|
195
|
+
out_frame = scaled_frame.av_frame
|
196
|
+
in_frame = @raw_frame.av_frame
|
197
|
+
|
198
|
+
# Make sure we copy the key_frame value across.
|
199
|
+
# XXX Need to also do this for some other fields
|
200
|
+
out_frame[:key_frame] = in_frame[:key_frame]
|
201
|
+
|
202
|
+
rc = sws_scale(@swscale_ctx, in_frame[:data], in_frame[:linesize], 0,
|
203
|
+
@raw_frame.height, out_frame[:data], out_frame[:linesize])
|
204
|
+
scaled_frame.pts = @raw_frame.pts
|
205
|
+
scaled_frame.number = @av_codec_ctx[:frame_number]
|
206
|
+
scaled_frame
|
207
|
+
end
|
208
|
+
|
209
|
+
private
|
210
|
+
|
211
|
+
def initialize_scaling
|
212
|
+
@scaling_initialized = true
|
213
|
+
@swscale_ctx = nil
|
214
|
+
@buffered_frames = nil
|
215
|
+
|
216
|
+
return if @width == @av_codec_ctx[:width] &&
|
217
|
+
@height == @av_codec_ctx[:height] &&
|
218
|
+
@pixel_format == @av_codec_ctx[:pix_fmt] &&
|
219
|
+
@buffer_size < 2
|
220
|
+
|
221
|
+
@buffered_frames = @buffer_size.times.map do
|
222
|
+
FFmpeg::Frame::Video.new :stream => self,
|
223
|
+
:width => @width,
|
224
|
+
:height => @height,
|
225
|
+
:pixel_format => @pixel_format
|
226
|
+
end
|
227
|
+
|
228
|
+
@swscale_ctx = sws_getContext(@av_codec_ctx[:width],
|
229
|
+
@av_codec_ctx[:height],
|
230
|
+
@av_codec_ctx[:pix_fmt],
|
231
|
+
@width, @height, @pixel_format,
|
232
|
+
:bicubic, nil, nil, nil) or
|
233
|
+
raise NoMemoryError, "sws_getContext() failed"
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
class FFmpeg::Stream::Unsupported
|
238
|
+
include FFmpeg::Stream
|
239
|
+
|
240
|
+
def initialize(p={})
|
241
|
+
super(p)
|
242
|
+
self.discard = :all
|
243
|
+
end
|
244
|
+
end
|
metadata
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ffi-ffmpeg
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.8135c9c5dc36
|
5
|
+
prerelease: 8
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- David M. Lary
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-11-01 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: ffi
|
16
|
+
requirement: &12966700 !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: *12966700
|
25
|
+
description: ! "Ruby FFI bindings for FFmpeg libraries:\n * libavformat\n
|
26
|
+
\ * libavcodec\n * libavutil\n *
|
27
|
+
libswscale"
|
28
|
+
email:
|
29
|
+
- dmlary@gmail.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- .hgignore
|
35
|
+
- Gemfile
|
36
|
+
- README.md
|
37
|
+
- Rakefile
|
38
|
+
- examples/extract_keyframes.rb
|
39
|
+
- examples/read_file.rb
|
40
|
+
- examples/seek.rb
|
41
|
+
- ffi-ffmpeg.gemspec
|
42
|
+
- lib/ffi-ffmpeg.rb
|
43
|
+
- lib/ffi/ffmpeg.rb
|
44
|
+
- lib/ffmpeg/frame.rb
|
45
|
+
- lib/ffmpeg/reader.rb
|
46
|
+
- lib/ffmpeg/stream.rb
|
47
|
+
- lib/ffmpeg/version.rb
|
48
|
+
homepage: http://bitbucket.com/dmlary/ffi-ffmpeg
|
49
|
+
licenses: []
|
50
|
+
post_install_message:
|
51
|
+
rdoc_options: []
|
52
|
+
require_paths:
|
53
|
+
- lib
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
55
|
+
none: false
|
56
|
+
requirements:
|
57
|
+
- - ! '>='
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ! '>'
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: 1.3.1
|
66
|
+
requirements: []
|
67
|
+
rubyforge_project: ffi-ffmpeg
|
68
|
+
rubygems_version: 1.8.10
|
69
|
+
signing_key:
|
70
|
+
specification_version: 3
|
71
|
+
summary: Ruby FFI bindings for FFmpeg libraries
|
72
|
+
test_files: []
|