ffi-libav 0.1.0 → 0.2.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.
- data/.gitignore +3 -0
- data/.rspec +2 -0
- data/Gemfile +1 -0
- data/README.md +114 -3
- data/Rakefile +27 -0
- data/examples/afs_test.rb +92 -0
- data/lib/ffi-libav.rb +1 -0
- data/lib/ffi/libav.i +1 -0
- data/lib/ffi/libav.rb +34 -1
- data/lib/libav/frame.rb +24 -12
- data/lib/libav/reader.rb +138 -9
- data/lib/libav/stream.rb +342 -64
- data/lib/libav/version.rb +1 -1
- data/spec/frame_spec.rb +274 -0
- data/spec/reader_spec.rb +22 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/stream_spec.rb +684 -0
- metadata +14 -10
data/lib/libav/version.rb
CHANGED
data/spec/frame_spec.rb
ADDED
@@ -0,0 +1,274 @@
|
|
1
|
+
require 'ffi-libav'
|
2
|
+
include Libav
|
3
|
+
|
4
|
+
describe Frame::Video, ".new" do
|
5
|
+
context 'when allocating with attributes' do
|
6
|
+
subject do
|
7
|
+
Frame::Video.new :width => 1200, :height => 100,
|
8
|
+
:pixel_format => :gray8
|
9
|
+
end
|
10
|
+
its(:width) { should eq(1200) }
|
11
|
+
its(:height) { should eq(100) }
|
12
|
+
its(:pixel_format) { should eq(:gray8) }
|
13
|
+
its("data.first.address") { should_not eq(0x0) }
|
14
|
+
end
|
15
|
+
|
16
|
+
context 'when not allocating, with attributes' do
|
17
|
+
subject do
|
18
|
+
Frame::Video.new :width => 1200, :height => 100,
|
19
|
+
:pixel_format => :gray8, :alloc => false
|
20
|
+
end
|
21
|
+
its(:width) { should eq(1200) }
|
22
|
+
its(:height) { should eq(100) }
|
23
|
+
its(:pixel_format) { should eq(:gray8) }
|
24
|
+
its("data.first.address") { should eq(0x0) }
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'when allocating, with :stream' do
|
28
|
+
subject do
|
29
|
+
stream = Object.new
|
30
|
+
stream.stub(:width) { 1200 }
|
31
|
+
stream.stub(:height) { 100 }
|
32
|
+
stream.stub(:pixel_format) { :gray8 }
|
33
|
+
Frame::Video.new :stream => stream
|
34
|
+
end
|
35
|
+
its(:width) { should eq(1200) }
|
36
|
+
its(:height) { should eq(100) }
|
37
|
+
its(:pixel_format) { should eq(:gray8) }
|
38
|
+
its("data.first.address") { should_not eq(0x0) }
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe Frame::Video do
|
43
|
+
subject do
|
44
|
+
Frame::Video.new :width => 1200, :height => 100,
|
45
|
+
:pixel_format => :gray8
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "#pts=" do
|
49
|
+
before { subject.pts = 0xFEEDFACE }
|
50
|
+
its(:pts) { should eq(0xFEEDFACE) }
|
51
|
+
it "should set av_frame[:pts]" do
|
52
|
+
expect(subject.av_frame[:pts]).to eq(0xFEEDFACE)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "#key_frame?" do
|
57
|
+
context 'when av_frame[:key_frame] == 1' do
|
58
|
+
before { subject.av_frame[:key_frame] = 1 }
|
59
|
+
its(:key_frame?) { should eq(true) }
|
60
|
+
end
|
61
|
+
|
62
|
+
context 'when av_frame[:key_frame] == 0' do
|
63
|
+
before { subject.av_frame[:key_frame] = 0 }
|
64
|
+
its(:key_frame?) { should eq(false) }
|
65
|
+
end
|
66
|
+
|
67
|
+
context 'when av_frame[:key_frame] == 0xBEEF' do
|
68
|
+
before { subject.key_frame = 0xBEEF }
|
69
|
+
its(:key_frame?) { should eq(true) }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "#key_frame=" do
|
74
|
+
context 'when frame.key_frame=(1)' do
|
75
|
+
before { subject.key_frame = 1 }
|
76
|
+
its(:key_frame?) { should eq(true) }
|
77
|
+
it "should set av_frame[:key_frame]" do
|
78
|
+
expect(subject.av_frame[:key_frame]).to eq(1)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context 'when frame.key_frame=(0)' do
|
83
|
+
before { subject.key_frame = 0 }
|
84
|
+
its(:key_frame?) { should eq(false) }
|
85
|
+
it "should set av_frame[:key_frame]" do
|
86
|
+
expect(subject.av_frame[:key_frame]).to eq(0)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
context 'when frame.key_frame=(0xBEEF)' do
|
91
|
+
before { subject.key_frame = 0xBEEF }
|
92
|
+
its(:key_frame?) { should eq(true) }
|
93
|
+
it "should set av_frame[:key_frame]" do
|
94
|
+
expect(subject.av_frame[:key_frame]).to eq(0xBEEF)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe "#pixel_format" do
|
100
|
+
before { subject.av_frame[:format] = :rgba }
|
101
|
+
its(:pixel_format) { should eq(:rgba) }
|
102
|
+
end
|
103
|
+
|
104
|
+
describe "#[]" do
|
105
|
+
let(:pts) { rand(0xFFFFFFFF) }
|
106
|
+
before { subject.av_frame[:pts] = pts }
|
107
|
+
it "should delegate to frame.av_frame" do
|
108
|
+
expect(subject[:pts]).to eq pts
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe "#av_frame" do
|
113
|
+
its('av_frame.class') { should be FFI::Libav::AVFrame }
|
114
|
+
end
|
115
|
+
|
116
|
+
describe "#timestamp" do
|
117
|
+
subject do
|
118
|
+
# fake a stream
|
119
|
+
stream = Object.new
|
120
|
+
stream.stub(:width) { 1200 }
|
121
|
+
stream.stub(:height) { 100 }
|
122
|
+
stream.stub(:pixel_format) { :gray8 }
|
123
|
+
|
124
|
+
# We stub out the [] method to return the :time_base for 24 FPS
|
125
|
+
stream.stub(:[]) do
|
126
|
+
f = FFI::Libav::AVRational.new
|
127
|
+
f[:num] = 1
|
128
|
+
f[:den] = 24
|
129
|
+
f
|
130
|
+
end
|
131
|
+
|
132
|
+
# Make a frame from this stream
|
133
|
+
frame = Frame::Video.new :stream => stream
|
134
|
+
frame.pts = 25
|
135
|
+
frame
|
136
|
+
end
|
137
|
+
its(:timestamp) { should eq(1/24.0 * 25) }
|
138
|
+
end
|
139
|
+
|
140
|
+
describe "#release" do
|
141
|
+
it "calls stream.release_frame(self)" do
|
142
|
+
stream = Object.new
|
143
|
+
stream.stub(:width) { 1200 }
|
144
|
+
stream.stub(:height) { 100 }
|
145
|
+
stream.stub(:pixel_format) { :gray8 }
|
146
|
+
stream.stub(:release_frame) { @released = true }
|
147
|
+
stream.stub(:released) { @released }
|
148
|
+
frame = Frame::Video.new :stream => stream
|
149
|
+
|
150
|
+
expect(stream.released).to be nil
|
151
|
+
frame.release()
|
152
|
+
expect(stream.released).to be true
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
describe Libav::Frame, "#scale" do
|
158
|
+
subject(:src_frame) do
|
159
|
+
# Craft a frame by hand that has an obvious pattern that we can verify
|
160
|
+
# after we scale.
|
161
|
+
frame = Frame::Video.new :width => 1000, :height => 1000,
|
162
|
+
:pixel_format => :gray8
|
163
|
+
|
164
|
+
# Set some fields in the source frame that we know should be copied to the
|
165
|
+
# scaled frame
|
166
|
+
frame.number = rand(0xEFFFFFFF)
|
167
|
+
frame.pts = rand(0xEFFFFFFF)
|
168
|
+
frame.key_frame = rand(2)
|
169
|
+
|
170
|
+
# We're dividing the entire image into a 4x4 grid
|
171
|
+
rows = 4
|
172
|
+
cols = 4
|
173
|
+
row_height = frame.height / rows
|
174
|
+
col_width = frame.width / cols
|
175
|
+
|
176
|
+
# calculate how many extra bytes we need to pad for the linesize for each
|
177
|
+
# row.
|
178
|
+
pad_length = frame.linesize[0] - frame.width
|
179
|
+
|
180
|
+
pattern = [ "\xff", "\x00", "\xff", "\x00" ].map { |c| c * col_width }
|
181
|
+
|
182
|
+
rows.times do |row|
|
183
|
+
# figure out the contents of a single line in our row
|
184
|
+
line = pattern.join("")
|
185
|
+
line += " " * pad_length
|
186
|
+
|
187
|
+
# Now construct the data for the entire row
|
188
|
+
buf = line * row_height
|
189
|
+
|
190
|
+
# Write our row out
|
191
|
+
frame.data[0].put_bytes(row * row_height * frame.linesize[0], buf)
|
192
|
+
|
193
|
+
# rotate our pattern (to make checker board)
|
194
|
+
pattern.rotate!
|
195
|
+
end
|
196
|
+
|
197
|
+
frame
|
198
|
+
end
|
199
|
+
|
200
|
+
context 'scale(:width => 100, :height => 100)' do
|
201
|
+
subject { src_frame.scale(:width => 100, :height => 100) }
|
202
|
+
its(:class) { should be(Libav::Frame::Video) }
|
203
|
+
its(:__id__) { should_not be src_frame.__id__ }
|
204
|
+
its(:width) { should be(100) }
|
205
|
+
its(:height) { should be(100) }
|
206
|
+
its(:pixel_format) { should be(src_frame.pixel_format) }
|
207
|
+
its(:pts) { should be src_frame.pts }
|
208
|
+
its(:number) { should be src_frame.number }
|
209
|
+
its(:key_frame?) { should be src_frame.key_frame? }
|
210
|
+
end
|
211
|
+
|
212
|
+
context 'scale(:pixel_format => rgba)' do
|
213
|
+
subject { src_frame.scale(:pixel_format => :rgba) }
|
214
|
+
its(:class) { should be(Libav::Frame::Video) }
|
215
|
+
its(:__id__) { should_not be src_frame.__id__ }
|
216
|
+
its(:width) { should be src_frame.width }
|
217
|
+
its(:height) { should be src_frame.height }
|
218
|
+
its(:pixel_format) { should be :rgba }
|
219
|
+
its(:pts) { should be src_frame.pts }
|
220
|
+
its(:number) { should be src_frame.number }
|
221
|
+
its(:key_frame?) { should be src_frame.key_frame? }
|
222
|
+
end
|
223
|
+
|
224
|
+
context 'scale(:output_frame => frame)' do
|
225
|
+
subject(:dst_frame) do
|
226
|
+
Frame::Video.new :pixel_format => :rgba, :width => 120, :height => 120
|
227
|
+
end
|
228
|
+
subject do
|
229
|
+
src_frame.scale(:output_frame => dst_frame)
|
230
|
+
end
|
231
|
+
its(:class) { should be(Libav::Frame::Video) }
|
232
|
+
its(:__id__) { should be dst_frame.__id__ }
|
233
|
+
its(:width) { should be 120 }
|
234
|
+
its(:height) { should be 120 }
|
235
|
+
its(:pixel_format) { should be :rgba }
|
236
|
+
its(:pts) { should be src_frame.pts }
|
237
|
+
its(:number) { should be src_frame.number }
|
238
|
+
its(:key_frame?) { should be src_frame.key_frame? }
|
239
|
+
end
|
240
|
+
|
241
|
+
context 'scale(:scale_ctx => ctx)' do
|
242
|
+
# There's really not much we can do here to verify that the context is
|
243
|
+
# being passed short of examining the data of the scaled frame. So we're
|
244
|
+
# going to create a 150x150 dest frame, but create the scaling context to
|
245
|
+
# be 100x100. We also set all the pixels in the dest frame to 0xBE. To
|
246
|
+
# verify that we're using the scaling context, we make sure that the tail
|
247
|
+
# of frame.data[0][100..149] match /\xBE+$/
|
248
|
+
subject(:dst_frame) do
|
249
|
+
frame = Frame::Video.new :width => 150, :height => 150,
|
250
|
+
:pixel_format => :gray8
|
251
|
+
frame.data[0].write_bytes("\xBE" * 150 * frame.linesize[0])
|
252
|
+
frame
|
253
|
+
end
|
254
|
+
subject do
|
255
|
+
ctx = FFI::Libav.sws_getContext(src_frame.width, src_frame.height,
|
256
|
+
src_frame.pixel_format,
|
257
|
+
100, 100, :gray8,
|
258
|
+
FFI::Libav::SWS_BICUBIC,
|
259
|
+
nil, nil, nil)
|
260
|
+
src_frame.scale(:scale_ctx => ctx, :output_frame => dst_frame)
|
261
|
+
end
|
262
|
+
its(:class) { should be(Libav::Frame::Video) }
|
263
|
+
its(:__id__) { should_not be src_frame.__id__ }
|
264
|
+
its(:pts) { should be src_frame.pts }
|
265
|
+
its(:number) { should be src_frame.number }
|
266
|
+
its(:key_frame?) { should be src_frame.key_frame? }
|
267
|
+
it "contains a 100x100 image" do
|
268
|
+
linesize = subject.linesize[0]
|
269
|
+
expect(subject.data[0].get_bytes(100, 50)).to match(/\xBE+$/)
|
270
|
+
expect(subject.data[0].get_bytes(linesize * 99, 100)).to_not match(/^\xBE+$/)
|
271
|
+
expect(subject.data[0].get_bytes(linesize * 100, 100)).to match(/^\xBE+$/)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
data/spec/reader_spec.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'ffi-libav'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
describe Libav::Stream::Video do
|
5
|
+
# This is the fast frame seek data for the test video. It is used for the
|
6
|
+
# AFS related contexts.
|
7
|
+
subject(:afs_data) do
|
8
|
+
YAML.load_file \
|
9
|
+
File.join(File.dirname(__FILE__), "data/big_buck_bunny-afs.yml")
|
10
|
+
end
|
11
|
+
|
12
|
+
subject(:test_video) do
|
13
|
+
File.join(File.dirname(__FILE__),
|
14
|
+
"data/big_buck_bunny_480p_surround-fix.avi")
|
15
|
+
end
|
16
|
+
subject(:reader) { Libav::Reader.new(test_video) }
|
17
|
+
|
18
|
+
context "with :afs => true" do
|
19
|
+
subject(:reader) { Libav::Reader.new(test_video, :afs => true) }
|
20
|
+
its(:afs) { should eq [[], []] }
|
21
|
+
end
|
22
|
+
end
|
data/spec/spec_helper.rb
ADDED
data/spec/stream_spec.rb
ADDED
@@ -0,0 +1,684 @@
|
|
1
|
+
require 'ffi-libav'
|
2
|
+
require 'yaml'
|
3
|
+
require 'timeout'
|
4
|
+
require 'zlib'
|
5
|
+
|
6
|
+
# For testing
|
7
|
+
class Libav::Frame::Video
|
8
|
+
def crc
|
9
|
+
height.times.inject(0) do |crc,row|
|
10
|
+
Zlib::crc32(data[0].get_bytes(linesize[0] * row, width), crc)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe Libav::Stream::Video do
|
16
|
+
TEST_VIDEO = File.join(File.dirname(__FILE__),
|
17
|
+
"data/big_buck_bunny_480p_surround-fix.avi")
|
18
|
+
|
19
|
+
subject(:test_video) { TEST_VIDEO }
|
20
|
+
|
21
|
+
# Generate a subset of afs data for our test video. We read the file until
|
22
|
+
# we see 50 entries in the afs data.
|
23
|
+
r = Libav::Reader.new(TEST_VIDEO, :afs => true)
|
24
|
+
r.each_frame { break if r.afs.size == 25 }
|
25
|
+
afs_data = r.afs
|
26
|
+
r = nil
|
27
|
+
subject(:afs_data) { afs_data }
|
28
|
+
|
29
|
+
# Set up our reader and stream subjects
|
30
|
+
subject(:reader) { Libav::Reader.new(test_video) }
|
31
|
+
subject(:stream) { reader.streams.find { |stream| stream.type == :video } }
|
32
|
+
|
33
|
+
its(:class) { should be Libav::Stream::Video }
|
34
|
+
its(:width) { should be 854 }
|
35
|
+
its(:height) { should be 480 }
|
36
|
+
its(:pixel_format) { should be :yuv420p }
|
37
|
+
its(:reader) { should be reader }
|
38
|
+
its(:fps) { should eq 24.0 }
|
39
|
+
|
40
|
+
describe "#each_frame" do
|
41
|
+
shared_examples "video frame yielder" do
|
42
|
+
it "yields Frame::Video objects" do
|
43
|
+
frame = nil
|
44
|
+
stream.each_frame { |f| frame = f; break }
|
45
|
+
expect(frame.class).to be Libav::Frame::Video
|
46
|
+
end
|
47
|
+
|
48
|
+
it "only yields frames from this stream" do
|
49
|
+
# This test is invalid right now because we only support video streams
|
50
|
+
# and our test video only contains a single video stream.
|
51
|
+
index = []
|
52
|
+
subject.each_frame(:buffer => buffer_size) do |f|
|
53
|
+
index << f.stream.index
|
54
|
+
stream.release_frame(f)
|
55
|
+
break if index.size == 250
|
56
|
+
end
|
57
|
+
|
58
|
+
expect(index.uniq).to eq [ subject.index ]
|
59
|
+
end
|
60
|
+
|
61
|
+
it "yields sequential frame numbers" do
|
62
|
+
frames = []
|
63
|
+
subject.each_frame(:buffer => buffer_size) do |f|
|
64
|
+
frames << f.number
|
65
|
+
stream.release_frame(f)
|
66
|
+
break if frames.size == 250
|
67
|
+
end
|
68
|
+
|
69
|
+
expect(frames).to eq (1..250).to_a
|
70
|
+
end
|
71
|
+
|
72
|
+
it "adjusts the output frame number for the frame number oafset" do
|
73
|
+
subject.instance_eval { @frame_oafset = 100_000 }
|
74
|
+
subject.each_frame(:buffer => buffer_size) do |frame|
|
75
|
+
expect(frame.number).to be 100_001
|
76
|
+
stream.release_frame(frame)
|
77
|
+
break
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
it "adjusts the output frame pts for the frame pts oafset" do
|
82
|
+
subject.instance_eval { @pts_oafset = 100_000_000 }
|
83
|
+
subject.each_frame(:buffer => buffer_size) do |frame|
|
84
|
+
# PTS of the first frame is 8
|
85
|
+
expect(frame.pts).to be 100_000_008
|
86
|
+
stream.release_frame(frame)
|
87
|
+
break
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
it "will yield frames with correct pts values" do
|
92
|
+
pts = []
|
93
|
+
stream.each_frame(:buffer => buffer_size) do |frame|
|
94
|
+
pts << frame.pts
|
95
|
+
stream.release_frame(frame)
|
96
|
+
break if pts.size == 10
|
97
|
+
end
|
98
|
+
|
99
|
+
# test video starts at pts 8, increments by 1
|
100
|
+
expect(pts).to eq (8..17).to_a
|
101
|
+
end
|
102
|
+
|
103
|
+
it "will yield frames with correct timestamps" do
|
104
|
+
stamps = []
|
105
|
+
stream.each_frame(:buffer => buffer_size) do |frame|
|
106
|
+
stamps << frame.timestamp
|
107
|
+
stream.release_frame(frame)
|
108
|
+
break if stamps.size == 10
|
109
|
+
end
|
110
|
+
|
111
|
+
expect(stamps).to eq (8..17).to_a.map { |pts| pts * (1/stream.fps) }
|
112
|
+
end
|
113
|
+
|
114
|
+
it "will yield a frame with data" do
|
115
|
+
# if one has data, assume they all have it
|
116
|
+
expect(frame.data.first.address).to_not eq 0
|
117
|
+
end
|
118
|
+
|
119
|
+
it "will yield a frame with key_frame set correctly" do
|
120
|
+
keys = []
|
121
|
+
stream.each_frame(:buffer => buffer_size) do |frame|
|
122
|
+
keys << frame.key_frame?
|
123
|
+
stream.release_frame(frame)
|
124
|
+
break if keys.size == 10
|
125
|
+
end
|
126
|
+
|
127
|
+
expect(keys).to eq [true, false, false, false, false, true, false,
|
128
|
+
false, true, true]
|
129
|
+
end
|
130
|
+
|
131
|
+
context "file is at EOF" do
|
132
|
+
before { subject.seek :byte => File.size(reader.filename) }
|
133
|
+
it "yields the block a nil frame" do
|
134
|
+
count = 0
|
135
|
+
subject.each_frame(:buffer => buffer_size) do |frame|
|
136
|
+
stream.release_frame(frame)
|
137
|
+
count += 1
|
138
|
+
expect(count).to be < 2
|
139
|
+
expect(count).to be nil
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
shared_examples "scaling video frame yielder" do
|
146
|
+
context "without scaling" do
|
147
|
+
let(:frame) do
|
148
|
+
stream.each_frame(:buffer => buffer_size) { |f| f.release; break f }
|
149
|
+
end
|
150
|
+
after(:each) { stream.release_frame(frame) }
|
151
|
+
|
152
|
+
it_should_behave_like "video frame yielder"
|
153
|
+
|
154
|
+
it "will preserve frame width" do
|
155
|
+
expect(frame.width).to be stream.av_codec_ctx[:width]
|
156
|
+
end
|
157
|
+
|
158
|
+
it "will preserve frame height" do
|
159
|
+
expect(frame.height).to be stream.av_codec_ctx[:height]
|
160
|
+
end
|
161
|
+
|
162
|
+
it "will preserve pixel format" do
|
163
|
+
expect(frame.pixel_format).to be stream.av_codec_ctx[:pix_fmt]
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
context "with scaling" do
|
168
|
+
before do
|
169
|
+
stream.width /= 2
|
170
|
+
stream.height /= 2
|
171
|
+
stream.pixel_format = :gray8
|
172
|
+
end
|
173
|
+
|
174
|
+
let(:frame) do
|
175
|
+
frame = nil
|
176
|
+
stream.each_frame { |f| frame = f; break }
|
177
|
+
frame
|
178
|
+
end
|
179
|
+
|
180
|
+
after(:each) { stream.release_frame(frame) }
|
181
|
+
|
182
|
+
it_should_behave_like "video frame yielder"
|
183
|
+
|
184
|
+
it "should scale frame width" do
|
185
|
+
expect(frame.width).to be stream.av_codec_ctx[:width] / 2
|
186
|
+
end
|
187
|
+
|
188
|
+
it "should scale frame height" do
|
189
|
+
expect(frame.height).to be stream.av_codec_ctx[:height] / 2
|
190
|
+
end
|
191
|
+
|
192
|
+
it "should change pixel format" do
|
193
|
+
expect(frame.pixel_format).to be :gray8
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
context "without buffer" do
|
199
|
+
let(:buffer_size) { 0 }
|
200
|
+
it_behaves_like "scaling video frame yielder"
|
201
|
+
|
202
|
+
shared_examples "unbuffered frame yielder" do
|
203
|
+
it "yields the same Frame object for each frame" do
|
204
|
+
frames = []
|
205
|
+
subject.each_frame(:buffer => buffer_size) do |frame|
|
206
|
+
frames << frame
|
207
|
+
break if frames.size == 100
|
208
|
+
end
|
209
|
+
expect(frames.uniq).to eq [frames.first]
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
context "without scaling" do
|
214
|
+
it_should_behave_like "unbuffered frame yielder"
|
215
|
+
end
|
216
|
+
|
217
|
+
context "with scaling" do
|
218
|
+
before do
|
219
|
+
stream.width /= 2
|
220
|
+
stream.height /= 2
|
221
|
+
stream.pixel_format = :gray8
|
222
|
+
end
|
223
|
+
|
224
|
+
it_should_behave_like "unbuffered frame yielder"
|
225
|
+
end
|
226
|
+
|
227
|
+
end
|
228
|
+
|
229
|
+
context "with buffer" do
|
230
|
+
let(:buffer_size) { 5 }
|
231
|
+
|
232
|
+
it_behaves_like "scaling video frame yielder"
|
233
|
+
|
234
|
+
shared_examples "buffered frame yielder" do
|
235
|
+
it "yields multiple Frame objects" do
|
236
|
+
frames = []
|
237
|
+
stream.each_frame(:buffer => buffer_size) do |frame|
|
238
|
+
frames << frame
|
239
|
+
stream.release_frame(frame)
|
240
|
+
break if frames.size == 100
|
241
|
+
end
|
242
|
+
expect(frames.uniq.size).to eq stream.buffer
|
243
|
+
end
|
244
|
+
|
245
|
+
it "blocks if the caller doesn't release frames" do
|
246
|
+
expect do
|
247
|
+
Timeout.timeout(2) do
|
248
|
+
count = 0
|
249
|
+
stream.each_frame(:buffer => buffer_size) do
|
250
|
+
count += 1
|
251
|
+
break if count > stream.buffer
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end.to raise_error(Timeout::Error)
|
255
|
+
end
|
256
|
+
|
257
|
+
it "reuses released frames" do
|
258
|
+
frame = nil
|
259
|
+
count = 0
|
260
|
+
|
261
|
+
# Set up a scenario where we drain the frame buffer, and save off one
|
262
|
+
# of the frames we receive.
|
263
|
+
stream.each_frame(:buffer => buffer_size) do |f|
|
264
|
+
frame ||= f
|
265
|
+
count += 1
|
266
|
+
break if count == stream.buffer
|
267
|
+
end
|
268
|
+
|
269
|
+
# Release our saved frame, then pull another 100 frames, saving them to
|
270
|
+
# an array, and releasing them to be reused. The idea is that the next
|
271
|
+
# 100 frames should be the same object as our saved frame.
|
272
|
+
frames = []
|
273
|
+
stream.release_frame(frame)
|
274
|
+
stream.each_frame(:buffer => buffer_size) do |f|
|
275
|
+
frames << f
|
276
|
+
stream.release_frame(f)
|
277
|
+
break if frames.size == 100
|
278
|
+
end
|
279
|
+
|
280
|
+
expect(frames.uniq).to eq [frame]
|
281
|
+
end
|
282
|
+
|
283
|
+
it "preserves the buffer size across calls unless explicitly set" do
|
284
|
+
stream.each_frame(:buffer => 3) { break }
|
285
|
+
expect(stream.buffer).to eq 3
|
286
|
+
stream.each_frame { break }
|
287
|
+
expect(stream.buffer).to eq 3
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
context "without scaling" do
|
292
|
+
it_should_behave_like "buffered frame yielder"
|
293
|
+
end
|
294
|
+
|
295
|
+
context "with scaling" do
|
296
|
+
before do
|
297
|
+
stream.width /= 2
|
298
|
+
stream.height /= 2
|
299
|
+
stream.pixel_format = :gray8
|
300
|
+
end
|
301
|
+
|
302
|
+
it_should_behave_like "buffered frame yielder"
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
describe "#next_frame" do
|
308
|
+
shared_examples "frame reader" do
|
309
|
+
it "returns Frame::Video object" do
|
310
|
+
expect(stream.next_frame.class).to be Libav::Frame::Video
|
311
|
+
end
|
312
|
+
it "only returns frames for this stream" do
|
313
|
+
streams = 100.times.map { stream.next_frame.stream.index }.uniq
|
314
|
+
expect(streams).to eq [ stream.index ]
|
315
|
+
end
|
316
|
+
it "returns the next frame in the stream" do
|
317
|
+
last_frame = nil
|
318
|
+
stream.each_frame do |frame|
|
319
|
+
last_frame = frame.number and break if rand(50) == 0
|
320
|
+
end
|
321
|
+
|
322
|
+
expect(stream.next_frame.number).to eq last_frame + 1
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
context "without scaling" do
|
327
|
+
it_should_behave_like "frame reader"
|
328
|
+
end
|
329
|
+
|
330
|
+
context "with scaling" do
|
331
|
+
before do
|
332
|
+
stream.width /= 2
|
333
|
+
stream.height /= 2
|
334
|
+
stream.pixel_format = :gray8
|
335
|
+
end
|
336
|
+
|
337
|
+
it_should_behave_like "frame reader"
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
describe "#skip_frames" do
|
342
|
+
before { subject.skip_frames(100) }
|
343
|
+
its("next_frame.number") { should eq 101 }
|
344
|
+
end
|
345
|
+
|
346
|
+
describe "#rewind" do
|
347
|
+
it "causes #each_frame to yield previously yielded frames" do
|
348
|
+
first = []
|
349
|
+
stream.each_frame(:buffer => 5) do |frame|
|
350
|
+
first << frame.number
|
351
|
+
frame.release
|
352
|
+
break if first.size == 5
|
353
|
+
end
|
354
|
+
|
355
|
+
stream.rewind(5)
|
356
|
+
second = []
|
357
|
+
stream.each_frame do |frame|
|
358
|
+
second << frame.number
|
359
|
+
frame.release
|
360
|
+
break if second.size == 5
|
361
|
+
end
|
362
|
+
|
363
|
+
expect(first).to eq second
|
364
|
+
end
|
365
|
+
|
366
|
+
it "returns 0 when no frames have been decoded" do
|
367
|
+
# XXX should be in reader_spec
|
368
|
+
expect(stream.rewind).to eq 0
|
369
|
+
end
|
370
|
+
|
371
|
+
it "returns 0 when count is 0" do
|
372
|
+
# XXX should be in reader_spec
|
373
|
+
expect(stream.rewind(0)).to eq 0
|
374
|
+
end
|
375
|
+
|
376
|
+
it "rewinds as many frames as possible when count exceeds buffer" do
|
377
|
+
first = []
|
378
|
+
stream.each_frame(:buffer => 5) do |frame|
|
379
|
+
first << frame.number
|
380
|
+
frame.release
|
381
|
+
break if first.size == 10
|
382
|
+
end
|
383
|
+
|
384
|
+
expect(stream.rewind(10)).to eq 5
|
385
|
+
expect(stream.each_frame { |f| break f }.number).to be 6
|
386
|
+
end
|
387
|
+
|
388
|
+
it "removes rewound frames from decoding queue" do
|
389
|
+
frames = []
|
390
|
+
stream.each_frame(:buffer => 5) do |frame|
|
391
|
+
frames << frame.object_id
|
392
|
+
frame.release
|
393
|
+
break if frames.size == 5
|
394
|
+
end
|
395
|
+
expect(frames.uniq.size).to eq 5
|
396
|
+
|
397
|
+
# rewind the last five frames
|
398
|
+
stream.rewind(5)
|
399
|
+
|
400
|
+
# skip the next five frames
|
401
|
+
stream.skip_frames(5)
|
402
|
+
|
403
|
+
# the sixth frame should be a new object id
|
404
|
+
expect(frames).to_not include(stream.next_frame.object_id)
|
405
|
+
end
|
406
|
+
|
407
|
+
it "can be called multiple times" do
|
408
|
+
frames = []
|
409
|
+
stream.each_frame(:buffer => 5) do |frame|
|
410
|
+
frames << frame.number
|
411
|
+
frame.release
|
412
|
+
break if frames.size == 5
|
413
|
+
end
|
414
|
+
|
415
|
+
stream.rewind(1)
|
416
|
+
expect(stream.next_frame.number).to eq frames[4]
|
417
|
+
|
418
|
+
stream.rewind(2)
|
419
|
+
expect(stream.next_frame.number).to eq frames[3]
|
420
|
+
|
421
|
+
stream.rewind(2)
|
422
|
+
expect(stream.next_frame.number).to eq frames[2]
|
423
|
+
|
424
|
+
stream.rewind(2)
|
425
|
+
expect(stream.next_frame.number).to eq frames[1]
|
426
|
+
|
427
|
+
stream.rewind(2)
|
428
|
+
expect(stream.next_frame.number).to eq frames[0]
|
429
|
+
expect(stream.next_frame.number).to eq frames[1]
|
430
|
+
expect(stream.next_frame.number).to eq frames[2]
|
431
|
+
expect(stream.next_frame.number).to eq frames[3]
|
432
|
+
expect(stream.next_frame.number).to eq frames[4]
|
433
|
+
expect(stream.next_frame.number).to eq frames[4] + 1
|
434
|
+
end
|
435
|
+
|
436
|
+
it "does rewind over re-used frames" do
|
437
|
+
first = stream.next_frame
|
438
|
+
second = stream.next_frame
|
439
|
+
expect(stream.next_frame.number).to eq second.number
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
443
|
+
describe "#index" do
|
444
|
+
its("index") { should be subject.av_stream[:index] }
|
445
|
+
end
|
446
|
+
|
447
|
+
describe "#[]" do
|
448
|
+
let(:num) { rand(0xFFFFFFFF) }
|
449
|
+
before { subject.av_stream[:nb_frames] = num }
|
450
|
+
it "delegates to stream.av_stream" do
|
451
|
+
expect(subject[:nb_frames]).to eq num
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
describe "#discard" do
|
456
|
+
before { subject.av_stream[:discard] = :nonkey }
|
457
|
+
its(:discard) { should be :nonkey }
|
458
|
+
end
|
459
|
+
|
460
|
+
describe "#type" do
|
461
|
+
its(:type) { should be :video }
|
462
|
+
its(:type) { should be subject.av_stream[:codec][:codec_type] }
|
463
|
+
end
|
464
|
+
|
465
|
+
context "with AFS disabled" do
|
466
|
+
describe "#each_frame" do
|
467
|
+
it "does not update AFS data" do
|
468
|
+
count = 0
|
469
|
+
subject.each_frame do |f|
|
470
|
+
count += 1 if f.key_frame
|
471
|
+
break if count == 20
|
472
|
+
end
|
473
|
+
expect(reader.afs).to be nil
|
474
|
+
end
|
475
|
+
end
|
476
|
+
|
477
|
+
describe "#seek" do
|
478
|
+
context "by pts" do
|
479
|
+
before { subject.seek :pts => 30 * subject.fps }
|
480
|
+
its("next_frame.pts") { should be >= 30 * subject.fps }
|
481
|
+
its("next_frame.key_frame?") { should be true }
|
482
|
+
end
|
483
|
+
context "non-key frames" do
|
484
|
+
before { subject.seek :pts => 30 * subject.fps, :any => true }
|
485
|
+
its("next_frame.pts") { should be >= 30 * subject.fps }
|
486
|
+
its("next_frame.key_frame?") { should be false }
|
487
|
+
end
|
488
|
+
context "by byte" do
|
489
|
+
before { subject.seek :byte => 100_000_000 }
|
490
|
+
# The codec used in our test video does not correctly adjust the pts
|
491
|
+
# following a seek.
|
492
|
+
# its("next_frame.pts") { should be ??? }
|
493
|
+
its("next_frame.pos") { should be >= 100_000_000 }
|
494
|
+
|
495
|
+
# Also, seeking by byte does not care about key frames.
|
496
|
+
# its("next_frame.key_frame?") { should be true }
|
497
|
+
end
|
498
|
+
end
|
499
|
+
end
|
500
|
+
|
501
|
+
context "with AFS enabled" do
|
502
|
+
subject(:reader) { Libav::Reader.new(test_video, :afs => true) }
|
503
|
+
subject(:stream) { reader.streams.find { |stream| stream.type == :video } }
|
504
|
+
|
505
|
+
its(:afs) { should be_empty }
|
506
|
+
|
507
|
+
describe "#each_frame" do
|
508
|
+
context "before seeking" do
|
509
|
+
it "updates the afs data for the last frame read" do
|
510
|
+
count = 0
|
511
|
+
stream.each_frame do |frame|
|
512
|
+
count += 1
|
513
|
+
expect(stream.afs.last[0]).to eq frame.number
|
514
|
+
expect(stream.afs.last[1]).to eq frame.pts
|
515
|
+
expect(stream.afs.last[2]).to eq frame.pos
|
516
|
+
expect(stream.afs.last[3]).to eq frame.key_frame?
|
517
|
+
break if count == 200
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
it "only preserves afs data for key frames" do
|
522
|
+
count = 0
|
523
|
+
stream.each_frame do |frame|
|
524
|
+
next unless frame.key_frame?
|
525
|
+
count += 1
|
526
|
+
break if count == 20
|
527
|
+
end
|
528
|
+
expect(stream.afs.size).to eq count
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
532
|
+
context "after seeking" do
|
533
|
+
before { stream.seek(:byte => 100_000) }
|
534
|
+
|
535
|
+
it "does not update afs data for key frames read" do
|
536
|
+
count = 0
|
537
|
+
stream.each_frame { |f| count += 1 if f.key_frame?; count < 20 }
|
538
|
+
expect(stream.afs).to eq []
|
539
|
+
end
|
540
|
+
it "does not update last frame data" do
|
541
|
+
count = 0
|
542
|
+
stream.each_frame do |frame|
|
543
|
+
count += 1
|
544
|
+
break if count == 100
|
545
|
+
end
|
546
|
+
expect(stream.afs).to be_empty
|
547
|
+
end
|
548
|
+
end
|
549
|
+
end
|
550
|
+
end
|
551
|
+
|
552
|
+
context "with AFS data provided" do
|
553
|
+
subject(:reader) { Libav::Reader.new(test_video, :afs => afs_data) }
|
554
|
+
subject(:stream) { reader.streams.find { |stream| stream.type == :video } }
|
555
|
+
|
556
|
+
its(:afs) { should be afs_data[stream.index] }
|
557
|
+
|
558
|
+
describe "#each_frame" do
|
559
|
+
it "does not update afs for known frames" do
|
560
|
+
count = 0
|
561
|
+
stream.each_frame { |f| break if (count += 1) == 100 }
|
562
|
+
expect(stream.afs).to eq afs_data[stream.index]
|
563
|
+
end
|
564
|
+
|
565
|
+
it "updates afs data for frames beyond existing afs data" do
|
566
|
+
stream.seek(:frame => stream.afs[-1][0])
|
567
|
+
|
568
|
+
count = 0
|
569
|
+
stream.each_frame do |frame|
|
570
|
+
count += 1
|
571
|
+
expect(stream.afs.last[0]).to eq frame.number
|
572
|
+
expect(stream.afs.last[1]).to eq frame.pts
|
573
|
+
expect(stream.afs.last[2]).to eq frame.pos
|
574
|
+
expect(stream.afs.last[3]).to eq frame.key_frame?
|
575
|
+
break if count == 200
|
576
|
+
end
|
577
|
+
end
|
578
|
+
end
|
579
|
+
|
580
|
+
describe "#seek" do
|
581
|
+
shared_examples "frame accurate seek" do
|
582
|
+
context "within afs data" do
|
583
|
+
it "can find a key frame by pts" do
|
584
|
+
stream.seek(:pts => 22)
|
585
|
+
frame = stream.next_frame
|
586
|
+
expect(frame.number).to eq 15
|
587
|
+
expect(frame.pts).to eq 22
|
588
|
+
expect(frame.pos).to eq 157836
|
589
|
+
expect(frame.crc.to_s(16)).to eq "6c56942e"
|
590
|
+
end
|
591
|
+
|
592
|
+
it "can find a non-key frame by pts" do
|
593
|
+
stream.seek(:pts => 54)
|
594
|
+
frame = stream.next_frame
|
595
|
+
expect(frame.number).to eq 47
|
596
|
+
expect(frame.pts).to eq 54
|
597
|
+
expect(frame.pos).to eq 658930
|
598
|
+
expect(frame.crc.to_s(16)).to eq "496919b1"
|
599
|
+
end
|
600
|
+
|
601
|
+
it "can find a key frame by byte" do
|
602
|
+
# [270, 277, 4942848, 1, "0xf8ea5eb2"],
|
603
|
+
stream.seek(:byte => 4942848)
|
604
|
+
frame = stream.next_frame
|
605
|
+
expect(frame.number).to eq 270
|
606
|
+
expect(frame.pts).to eq 277
|
607
|
+
expect(frame.pos).to eq 4942848
|
608
|
+
expect(frame.crc.to_s(16)).to eq "f8ea5eb2"
|
609
|
+
end
|
610
|
+
|
611
|
+
it "can find a non-key frame by byte" do
|
612
|
+
# [350, 357, 6901944, 0, "0x3ea2c2ff"]]
|
613
|
+
stream.seek(:byte => 6901944)
|
614
|
+
frame = stream.next_frame
|
615
|
+
expect(frame.number).to eq 350
|
616
|
+
expect(frame.pts).to eq 357
|
617
|
+
expect(frame.pos).to eq 6901944
|
618
|
+
expect(frame.crc.to_s(16)).to eq "3ea2c2ff"
|
619
|
+
end
|
620
|
+
|
621
|
+
it "finds key frame by frame number" do
|
622
|
+
stream.seek(:frame => 20)
|
623
|
+
frame = stream.next_frame
|
624
|
+
expect(frame.number).to eq 20
|
625
|
+
expect(frame.pts).to eq 27
|
626
|
+
expect(frame.pos).to eq 213188
|
627
|
+
expect(frame.crc.to_s(16)).to eq "85190ba9"
|
628
|
+
end
|
629
|
+
|
630
|
+
it "can find a non-key frame by frame number" do
|
631
|
+
stream.seek(:frame => 21)
|
632
|
+
frame = stream.next_frame
|
633
|
+
expect(frame.number).to eq 21
|
634
|
+
expect(frame.pts).to eq 28
|
635
|
+
expect(frame.pos).to eq 224996
|
636
|
+
end
|
637
|
+
|
638
|
+
it "can reach the first key frame" do
|
639
|
+
stream.seek :frame => 1
|
640
|
+
frame = stream.next_frame
|
641
|
+
expect(frame.number).to eq 1
|
642
|
+
expect(frame.pts).to eq 8
|
643
|
+
expect(frame.pos).to eq 46860
|
644
|
+
expect(frame.crc.to_s(16)).to eq "5aaa5831"
|
645
|
+
end
|
646
|
+
end
|
647
|
+
|
648
|
+
context "outside of afs data" do
|
649
|
+
# Seek past the end of our afs data
|
650
|
+
before { stream.seek :frame => stream.afs.last[0] + 1 }
|
651
|
+
|
652
|
+
it "disables afs updates" do
|
653
|
+
expect(stream.instance_eval { @update_afs }).to be false
|
654
|
+
end
|
655
|
+
|
656
|
+
it "re-enables afs updates" do
|
657
|
+
stream.seek :frame => 100
|
658
|
+
expect(stream.instance_eval { @update_afs }).to be true
|
659
|
+
end
|
660
|
+
end
|
661
|
+
end
|
662
|
+
|
663
|
+
context "before reading frames" do
|
664
|
+
it_should_behave_like "frame accurate seek"
|
665
|
+
end
|
666
|
+
context "after seeking to the end" do
|
667
|
+
before { stream.seek :byte => File.size(reader.filename) }
|
668
|
+
it_should_behave_like "frame accurate seek"
|
669
|
+
end
|
670
|
+
context "after reading 100 frames" do
|
671
|
+
before { c = 0; stream.each_frame { break if (c += 1) == 100 } }
|
672
|
+
it_should_behave_like "frame accurate seek"
|
673
|
+
end
|
674
|
+
context "after seeking to the middle, and reading 100 frames" do
|
675
|
+
before do
|
676
|
+
stream.seek :byte => File.size(reader.filename)/2
|
677
|
+
count = 0
|
678
|
+
stream.each_frame { break if (count += 1) == 100 }
|
679
|
+
end
|
680
|
+
it_should_behave_like "frame accurate seek"
|
681
|
+
end
|
682
|
+
end
|
683
|
+
end
|
684
|
+
end
|