ffi-libav 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,113 @@
1
+ require 'ffi/libav'
2
+
3
+ module Libav::Frame; end
4
+
5
+ class Libav::Frame::Video
6
+ include FFI::Libav
7
+
8
+ attr_reader :av_frame, :stream
9
+ attr_accessor :number
10
+
11
+ # Initialize a new frame, and optionally allocate memory for the frame data.
12
+ #
13
+ # If only a +:stream+ is provided, the remaining attributes will be copied
14
+ # from that stream.
15
+ #
16
+ # ==== Options
17
+ # * +:stream+ - Libav::Stream this frame belongs to
18
+ # * +:width+ - width of the frame
19
+ # * +:height+ - height of the frame
20
+ # * +:pixel_format+ - format of the frame
21
+ # * +:alloc+ - Allocate space for the frame data [default: true]
22
+ #
23
+ def initialize(p={})
24
+ # Create our frame and alloc space for the frame data
25
+ @av_frame = AVFrame.new
26
+
27
+ @stream = p[:stream]
28
+ @av_frame[:width] = p[:width] || @stream && @stream.width
29
+ @av_frame[:height] = p[:height] || @stream && @stream.height
30
+ @av_frame[:format] = p[:pixel_format] || @stream && @stream.pixel_format
31
+
32
+ # Allocate the frame's data unless the caller doesn't want us to.
33
+ unless p[:alloc] == false
34
+ av_picture = AVPicture.new @av_frame.pointer
35
+ avpicture_alloc(av_picture, @av_frame[:format], @av_frame[:width],
36
+ @av_frame[:height])
37
+ ObjectSpace.define_finalizer(self, cleanup_proc(av_picture))
38
+ end
39
+ end
40
+
41
+ # Throw together a bunch of helper methods for accessing the AVFrame
42
+ # attributes.
43
+ AVFrame.members.each do |member|
44
+ define_method(member) { @av_frame[member] }
45
+ define_method(member.to_s + "=") { |v| @av_frame[member] = v }
46
+ end
47
+
48
+ def key_frame?
49
+ key_frame == 1
50
+ end
51
+
52
+ def pixel_format
53
+ format.is_a?(Fixnum) ? PixelFormat[format] : format
54
+ end
55
+
56
+ def pixel_format=(v)
57
+ send("format=", (v.is_a?(Fixnum) ? v : PixelFormat[v]))
58
+ end
59
+
60
+ # Scale the frame
61
+ #
62
+ # If any of the +:width+, +:height+, or +:pixel_format+ options are not
63
+ # supplied, they will be copied from the +:output_frame+, if provided,
64
+ # otherwise they will be copied from this frame.
65
+ #
66
+ # If no +:scale_ctx+ is provided, one will be allocated and freed within this
67
+ # method.
68
+ #
69
+ # If no +:output_frame+ is provided, this method will allocate a new one.
70
+ #
71
+ # ==== Options
72
+ # * +:width+ - width of the scaled frame
73
+ # * +:height+ - height of the scaled frame
74
+ # * +:pixel_format+ - pixel format of the scaled frame
75
+ # * +:scale_ctx+ - optional software scaling context
76
+ # + +:output_frame+ - optional output frame
77
+ #
78
+ def scale(p={})
79
+ out = p[:output_frame] ||
80
+ self.class.new(:width => p[:width] || width,
81
+ :height => p[:height] || height,
82
+ :pixel_format => p[:pixel_format] || pixel_format,
83
+ :stream => stream)
84
+ ctx = p[:scale_ctx] ||
85
+ sws_getContext(width, height, pixel_format,
86
+ out.width, out.height, out.pixel_format,
87
+ SWS_BICUBIC, nil, nil, nil)
88
+ raise NoMemoryError, "sws_getContext() failed" if ctx.nil?
89
+
90
+ # Scale the image
91
+ rc = sws_scale(ctx, data, linesize, 0, height, out.data, out.linesize)
92
+
93
+ # Free the scale context if one wasn't provided by the caller
94
+ sws_freeContext(ctx) unless p[:scale_ctx]
95
+
96
+ # Let's copy a handful of attributes to the scaled frame
97
+ %w{ pts number key_frame }.each do |k|
98
+ out.send("#{k}=", send(k))
99
+ end
100
+
101
+ # Return our scaled frame
102
+ out
103
+ end
104
+
105
+ private
106
+
107
+ # Returns Proc responsible for cleaning up the picture memory when it gets
108
+ # garbage collected.
109
+ def cleanup_proc(picture)
110
+ proc { avpicture_free(picture) }
111
+ end
112
+
113
+ end
@@ -0,0 +1,114 @@
1
+ require 'ffi/libav'
2
+
3
+ # Libav file reader
4
+ class Libav::Reader
5
+ include FFI::Libav
6
+
7
+ attr_reader :filename, :streams, :av_format_ctx
8
+
9
+ # Initialize a Reader for a specific file
10
+ #
11
+ # ==== Attributes
12
+ # * +filename+ - file to read
13
+ #
14
+ def initialize(filename, p={})
15
+ @filename = filename or raise ArgumentError, "No filename"
16
+
17
+ Libav.register_all
18
+ @av_format_ctx = FFI::MemoryPointer.new(:pointer)
19
+ rc = avformat_open_input(@av_format_ctx, @filename, nil, nil)
20
+ raise RuntimeError, "avformat_open_input() failed, filename='%s', rc=%d" %
21
+ [filename, rc] if rc != 0
22
+ @av_format_ctx = AVFormatContext.new @av_format_ctx.get_pointer(0)
23
+
24
+ rc = avformat_find_stream_info(@av_format_ctx, nil)
25
+ raise RuntimeError, "av_find_stream_info() failed, rc=#{rc}" if rc < 0
26
+
27
+ # Open all of our streams
28
+ initialize_streams(p)
29
+
30
+ # Set up a finalizer to close all the things we've opened
31
+ ObjectSpace.define_finalizer(self,
32
+ cleanup_proc(@av_format_ctx, streams.map { |s| s.av_codec_ctx }))
33
+
34
+ # Our packet for reading
35
+ @packet = AVPacket.new
36
+ av_init_packet(@packet)
37
+ end
38
+
39
+ # Call +av_dump_format+ to print out the format info for the video
40
+ def dump_format
41
+ FFI::Libav.av_dump_format(@av_format_ctx, 0, @filename, 0)
42
+ end
43
+
44
+ # Video duration in (fractional) seconds
45
+ def duration
46
+ @duration ||= @av_format_ctx[:duration].to_f / AV_TIME_BASE
47
+ end
48
+
49
+ # Loop through each frame
50
+ #
51
+ # ==== Argument
52
+ # * +block+ - block of code to call with the frame
53
+ #
54
+ # ==== Usage
55
+ # # Read each frame
56
+ # reader.each_frame do |frame|
57
+ #
58
+ # # call some method for showing the frame
59
+ # my_show_frame(frame)
60
+ # end
61
+ #
62
+ def each_frame(&block)
63
+ raise ArgumentError, "No block provided" unless block_given?
64
+
65
+ while av_read_frame(@av_format_ctx, @packet) >= 0
66
+ frame = @streams[@packet[:stream_index]].decode_frame(@packet)
67
+ av_free_packet(@packet)
68
+ rc = frame ? yield(frame) : true
69
+ break if rc == false
70
+ end
71
+ end
72
+
73
+ # Get the default stream
74
+ def default_stream
75
+ @streams[av_find_default_stream_index(@av_format_ctx)]
76
+ end
77
+
78
+ # See Libav::Stream.seek
79
+ def seek(p = {})
80
+ default_stream.seek(p)
81
+ end
82
+
83
+ private
84
+
85
+ # Generate the Proc that is responsible for releasing our avcodec, avformat
86
+ # structures.
87
+ def cleanup_proc(codecs, format)
88
+ proc do
89
+ codecs.each { |codec| avcodec_close codec }
90
+ avformat_close_input(format)
91
+ end
92
+ end
93
+
94
+ # Lookup and initialize the streams
95
+ def initialize_streams(p={})
96
+ @streams = @av_format_ctx[:nb_streams].times.map do |i|
97
+ av_stream = AVStream.new \
98
+ @av_format_ctx[:streams].get_pointer(i * FFI::Pointer::SIZE)
99
+ av_codec_ctx = av_stream[:codec]
100
+
101
+ case av_codec_ctx[:codec_type]
102
+ when :video
103
+ Libav::Stream::Video.new(:reader => self,
104
+ :av_stream => av_stream,
105
+ :pixel_format => p[:pixel_format],
106
+ :width => p[:width],
107
+ :height => p[:height])
108
+ else
109
+ Libav::Stream::Unsupported.new(:reader => self,
110
+ :av_stream => av_stream)
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,211 @@
1
+ require 'ffi/libav'
2
+
3
+ # Generic Stream class. Most of the logic resides in Libav::Stream::Video.
4
+ module Libav::Stream
5
+ include FFI::Libav
6
+
7
+ attr_reader :reader, :av_stream, :av_codec_ctx
8
+
9
+ def initialize(p={})
10
+ @reader = p[:reader] or raise ArgumentError, "no :reader"
11
+ @av_stream = p[:av_stream] or raise ArgumentError, "no :av_stream"
12
+ @av_codec_ctx = @av_stream[:codec]
13
+
14
+ # open the codec
15
+ codec = avcodec_find_decoder(@av_codec_ctx[:codec_id]) or
16
+ raise RuntimeError, "No decoder found for #{@av_codec_ctx[:codec_id]}"
17
+ avcodec_open2(@av_codec_ctx, codec, nil) == 0 or
18
+ raise RuntimeError, "avcodec_open() failed"
19
+ end
20
+
21
+ def discard=(value)
22
+ @av_stream[:discard] = value
23
+ end
24
+
25
+ def discard
26
+ @av_stream[:discard]
27
+ end
28
+
29
+ def type
30
+ @av_codec_ctx[:codec_type]
31
+ end
32
+
33
+ def index
34
+ @av_stream[:index]
35
+ end
36
+
37
+ def decode_frame(packet)
38
+ return false
39
+ raise NotImplementedError, "decode_frame() not defined for #{self.class}"
40
+ end
41
+
42
+ # Loop through each frame of this stream
43
+ def each_frame(&block)
44
+ @reader.each_frame { |frame| yield frame if frame.stream == self }
45
+ end
46
+
47
+ # Get the next frame in the stream
48
+ def next_frame
49
+ frame = nil
50
+ each_frame { |f| frame = f; break }
51
+ frame
52
+ end
53
+
54
+ # Skip some +n+ frames in the stream
55
+ def skip_frames(n)
56
+ raise RuntimeError, "Cannot skip frames when discarding all frames" if
57
+ discard == :all
58
+ each_frame { |f| n -= 1 != 0 }
59
+ end
60
+
61
+ # Seek to a specific location within the stream; the location can be either
62
+ # a PTS value or an absolute byte position.
63
+ #
64
+ # Arguments:
65
+ # [:pts] PTS location
66
+ # [:pos] Byte location
67
+ # [:backward] Seek backward
68
+ # [:any] Seek to non-key frames
69
+ #
70
+ def seek(p={})
71
+ p = { :pts => p } unless p.is_a? Hash
72
+
73
+ raise ArgumentError, ":pts and :pos are mutually exclusive" \
74
+ if p[:pts] and p[:pos]
75
+
76
+ pos = p[:pts] || p[:pos]
77
+ flags = 0
78
+ flags |= AVSEEK_FLAG_BYTE if p[:pos]
79
+ flags |= AVSEEK_FLAG_BACKWARD if p[:backward]
80
+ flags |= AVSEEK_FLAG_ANY if p[:any]
81
+
82
+ rc = av_seek_frame(@reader.av_format_ctx, @av_stream[:index], pos, flags)
83
+ raise RuntimeError, "av_seek_frame() failed, %d" % rc if rc < 0
84
+ true
85
+ end
86
+ end
87
+
88
+ class Libav::Stream::Video
89
+ include Libav::Stream
90
+
91
+ attr_reader :raw_frame, :width, :height, :pixel_format, :reader
92
+
93
+ def initialize(p={})
94
+ super(p)
95
+
96
+ # Handle frame width and height and setup any scaling necessary
97
+ @width = p[:widht] || @av_codec_ctx[:width]
98
+ @height = p[:height] || @av_codec_ctx[:height]
99
+ @pixel_format = p[:pixel_format] || @av_codec_ctx[:pix_fmt]
100
+ init_scaling
101
+
102
+ # Our frame structure for decoding the frame
103
+ @raw_frame = Libav::Frame::Video.new :stream => self, :alloc => false,
104
+ :width => @av_codec_ctx[:width],
105
+ :height => @av_codec_ctx[:height],
106
+ :pixel_format =>
107
+ @av_codec_ctx[:pix_fmt]
108
+
109
+ # Pointer used to denote that decode frame was successful
110
+ @frame_finished = FFI::MemoryPointer.new :int
111
+
112
+ # Elaborate schemes to capture an accurate pts value. When the buffer for
113
+ # the frame is allocated, we pull the dts from the last packet processed
114
+ # (set in decode_frame()), and set it in the opaque field of the frame.
115
+ #
116
+ # Since we may be running on a 32-bit platform, we can't just shove the
117
+ # 64-bit dts in the :opaque pointer, so we have to alloc some space for the
118
+ # address. Instead of allocating and freeing repeatedly, we're going to
119
+ # alloc it once now and reuse it for each decoded frame.
120
+ @last_dts = nil
121
+ @opaque = FFI::MemoryPointer.new :uint64
122
+
123
+ @av_codec_ctx[:get_buffer] = \
124
+ FFI::Function.new(:int, [AVCodecContext.ptr, AVFrame.ptr]) do |ctx,frame|
125
+
126
+ # Use the default method to get the buffer
127
+ ret = avcodec_default_get_buffer(ctx, frame)
128
+
129
+ # Update the :opaque field point at a copy of the last pts we've seen.
130
+ @opaque.write_int64 @last_dts
131
+ frame[:opaque] = @opaque
132
+
133
+ ret
134
+ end
135
+ end
136
+
137
+ def fps
138
+ @av_stream[:r_frame_rate]
139
+ end
140
+
141
+ # Set the +width+ of the frames returned by +decode_frame+
142
+ def width=(width)
143
+ @width = width
144
+ init_scaling
145
+ end
146
+
147
+ # Set the +height+ of the frames returned by +decode_frame+
148
+ def height=(height)
149
+ @height = height
150
+ init_scaling
151
+ end
152
+
153
+ # Set the +pixel format+ of the frames returned by +decode_frame+
154
+ def pixel_format=(pixel_format)
155
+ @pixel_format = pixel_format
156
+ init_scaling
157
+ end
158
+
159
+ # Called by Libav::Reader.each_frame to decode each frame
160
+ def decode_frame(packet)
161
+ # initialize_scaling unless @scaling_initialized
162
+
163
+ @last_dts = packet[:dts]
164
+ avcodec_get_frame_defaults(@raw_frame.av_frame)
165
+ rc = avcodec_decode_video2(@av_codec_ctx, @raw_frame.av_frame,
166
+ @frame_finished, packet)
167
+ raise RuntimeError, "avcodec_decode_video2() failed, rc=#{rc}" if rc < 0
168
+ return if @frame_finished.read_int == 0
169
+
170
+ @raw_frame.pts = @raw_frame.av_frame[:opaque].read_int64
171
+ @raw_frame.number = @av_codec_ctx[:frame_number].to_i
172
+
173
+ return @raw_frame unless @swscale_ctx
174
+
175
+ @raw_frame.scale(:scale_ctx => @swscale_ctx,
176
+ :output_frame => @scaled_frame)
177
+ end
178
+
179
+ private
180
+
181
+ def init_scaling
182
+ sws_freeContext(@swscale_ctx) unless @swscale_ctx.nil?
183
+ @swscale_ctx = nil
184
+ @scaled_frame = nil
185
+
186
+ return if @width == @av_codec_ctx[:width] &&
187
+ @height == @av_codec_ctx[:height] &&
188
+ @pixel_format == @av_codec_ctx[:pix_fmt]
189
+
190
+ @swscale_ctx = sws_getContext(@av_codec_ctx[:width],
191
+ @av_codec_ctx[:height],
192
+ @av_codec_ctx[:pix_fmt],
193
+ @width, @height, @pixel_format,
194
+ SWS_BICUBIC, nil, nil, nil) or
195
+ raise NoMemoryError, "sws_getContext() failed"
196
+
197
+ @scaled_frame = Libav::Frame::Video.new(:width => @width,
198
+ :height => @height,
199
+ :pixel_format => @pixel_format,
200
+ :stream => self)
201
+ end
202
+ end
203
+
204
+ class Libav::Stream::Unsupported
205
+ include Libav::Stream
206
+
207
+ def initialize(p={})
208
+ super(p)
209
+ self.discard = :all
210
+ end
211
+ end
@@ -0,0 +1,3 @@
1
+ module Libav
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ffi-libav
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - David M. Lary
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-01-27 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.5'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.5'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: ffi
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ description:
63
+ email:
64
+ - dmlary@gmail.com
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - .gitignore
70
+ - Gemfile
71
+ - LICENSE
72
+ - README.md
73
+ - Rakefile
74
+ - examples/extract_keyframes.rb
75
+ - examples/read_file.rb
76
+ - examples/seek.rb
77
+ - ffi-libav.gemspec
78
+ - lib/ffi-libav.rb
79
+ - lib/ffi/libav.i
80
+ - lib/ffi/libav.rb
81
+ - lib/libav/frame.rb
82
+ - lib/libav/reader.rb
83
+ - lib/libav/stream.rb
84
+ - lib/libav/version.rb
85
+ homepage: https://github.com/dmlary/ffi-libav
86
+ licenses:
87
+ - BSD 3-Clause
88
+ post_install_message:
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ none: false
94
+ requirements:
95
+ - - ! '>='
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ segments:
99
+ - 0
100
+ hash: 2735303191728479060
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ none: false
103
+ requirements:
104
+ - - ! '>='
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ segments:
108
+ - 0
109
+ hash: 2735303191728479060
110
+ requirements: []
111
+ rubyforge_project:
112
+ rubygems_version: 1.8.24
113
+ signing_key:
114
+ specification_version: 3
115
+ summary: Ruby FFI bindings and wrapper for Libav
116
+ test_files: []