ruby-ffprobe 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +7 -0
- data/LICENSE +20 -0
- data/README.rdoc +38 -0
- data/Rakefile +87 -0
- data/VERSION +1 -0
- data/lib/ffprobe.rb +106 -0
- data/lib/ffprobe/entity.rb +33 -0
- data/lib/ffprobe/file_info.rb +14 -0
- data/lib/ffprobe/frame_info.rb +15 -0
- data/lib/ffprobe/packet_info.rb +13 -0
- data/lib/ffprobe/parser.rb +27 -0
- data/lib/ffprobe/result.rb +107 -0
- data/lib/ffprobe/safe_pipe.rb +77 -0
- data/lib/ffprobe/stream_info.rb +17 -0
- data/lib/ffprobe/tags_info.rb +7 -0
- data/lib/ffprobe/unit.rb +13 -0
- data/test/ffprobe/test_ffprobe.rb +74 -0
- data/test/ffprobe/test_file_info.rb +29 -0
- data/test/ffprobe/test_parser.rb +147 -0
- data/test/helper.rb +47 -0
- data/test/test_result.rb +51 -0
- data/test/testcases/files.testcase +19 -0
- data/test/testcases/files_streams_tags.testcase +62 -0
- data/test/testcases/frames.testcase +5667 -0
- data/test/testcases/no_args.testcase +9 -0
- data/test/testcases/packets.testcase +4993 -0
- data/test/testcases/pretty_files.testcase +19 -0
- data/test/testcases/pretty_files_streams_tags.testcase +62 -0
- data/test/testcases/pretty_frames.testcase +5667 -0
- data/test/testcases/pretty_packets.testcase +4993 -0
- data/test/testcases/pretty_streams.testcase +42 -0
- data/test/testcases/pretty_tags.testcase +19 -0
- data/test/testcases/streams.testcase +42 -0
- data/test/testcases/tags.testcase +19 -0
- metadata +144 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'fcntl'
|
2
|
+
|
3
|
+
class FFProbe
|
4
|
+
class SafePipe
|
5
|
+
|
6
|
+
attr_accessor :program, :args, :status
|
7
|
+
|
8
|
+
def initialize(program,*args)
|
9
|
+
self.program = program
|
10
|
+
self.args = args
|
11
|
+
end
|
12
|
+
|
13
|
+
def run(&optional_block)
|
14
|
+
# closing of this pipe will indicate successful exec,
|
15
|
+
# otherwise it will stream the error
|
16
|
+
@rexec, @wexec = IO.pipe.map {|io| set_close_on_exec(io); io }
|
17
|
+
@reader, @writer = IO.pipe
|
18
|
+
if @pid = fork
|
19
|
+
parent(&optional_block)
|
20
|
+
else
|
21
|
+
child
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def success?
|
26
|
+
status ? status.success? : nil
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def parent
|
32
|
+
@writer.close
|
33
|
+
@wexec.close
|
34
|
+
wait_for_exec
|
35
|
+
if block_given?
|
36
|
+
yield(@reader)
|
37
|
+
else
|
38
|
+
@reader.read
|
39
|
+
end
|
40
|
+
ensure
|
41
|
+
# exception might have been caused by yielded code.
|
42
|
+
# send child sigpipe so it might not linger while we ignore it.
|
43
|
+
@reader.close rescue nil
|
44
|
+
# prevent zombie
|
45
|
+
reaped, status = Process.waitpid2(@pid)
|
46
|
+
self.status = status
|
47
|
+
end
|
48
|
+
|
49
|
+
def child
|
50
|
+
@reader.close
|
51
|
+
@rexec.close
|
52
|
+
$stdin.reopen("/dev/null")
|
53
|
+
$stdout.reopen(@writer)
|
54
|
+
$stderr.reopen(@writer)
|
55
|
+
exec(self.program, *self.args)
|
56
|
+
rescue => e
|
57
|
+
@wexec.write Marshal.dump(e)
|
58
|
+
ensure
|
59
|
+
exit!
|
60
|
+
end
|
61
|
+
|
62
|
+
def wait_for_exec
|
63
|
+
exec_result = @rexec.read
|
64
|
+
if exec_result.length != 0
|
65
|
+
# exec failed, this is a marshalled exception
|
66
|
+
raise Marshal.load(exec_result)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def set_close_on_exec(io)
|
71
|
+
flags = io.fcntl(Fcntl::F_GETFD)
|
72
|
+
flags |= Fcntl::FD_CLOEXEC
|
73
|
+
io.fcntl(Fcntl::F_SETFD, flags)
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
class FFProbe
|
2
|
+
class StreamInfo < Entity
|
3
|
+
|
4
|
+
attr_accessor :codec_name, :decoder_time_base, :codec_type,
|
5
|
+
:r_frame_rate, :r_frame_rate_num, :r_frame_rate_den,
|
6
|
+
:width, :height, :gop_size, :has_b_frames, :sample_aspect_ratio,
|
7
|
+
:display_aspect_ratio, :pix_fmt, :index, :time_base,
|
8
|
+
:start_time, :duration, :nb_frames,
|
9
|
+
:sample_rate, :channels, :bits_per_sample,
|
10
|
+
:probed_size, :probed_nb_pkts, :probed_nb_frames
|
11
|
+
|
12
|
+
units :start_time => "s",
|
13
|
+
:sample_rate => "kHz",
|
14
|
+
:duration => "s"
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
data/lib/ffprobe/unit.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require File.join(File.dirname(__FILE__),"..","helper.rb")
|
4
|
+
|
5
|
+
class TestFfprobe
|
6
|
+
class TestFfprobe < Test::Unit::TestCase
|
7
|
+
|
8
|
+
context "A default FFProbe instance" do
|
9
|
+
|
10
|
+
setup do
|
11
|
+
@ffprobe = FFProbe.new
|
12
|
+
end
|
13
|
+
|
14
|
+
should "show streams and tags" do
|
15
|
+
assert @ffprobe.features.length == 2
|
16
|
+
assert @ffprobe.features.include?(:show_streams)
|
17
|
+
assert @ffprobe.features.include?(:show_tags)
|
18
|
+
end
|
19
|
+
|
20
|
+
should "not be :pretty?" do
|
21
|
+
assert !@ffprobe.pretty?
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
context "A misconfigured FFProbe instance" do
|
27
|
+
should "not accept invalid features" do
|
28
|
+
assert_raise FFProbe::InvalidArgument do
|
29
|
+
FFProbe.new(:read_packets, :invalid_feature)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
context "with a bad executable path" do
|
33
|
+
setup do
|
34
|
+
@original_executable = FFProbe.executable
|
35
|
+
FFProbe.executable = "this file doesn't exist"
|
36
|
+
end
|
37
|
+
should "raise an exception" do
|
38
|
+
assert_raise Errno::ENOENT do
|
39
|
+
FFProbe.new.probe(testcase_path("source.ogv"))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
teardown do
|
43
|
+
FFProbe.executable = @original_executable
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context "A configured FFProbe instance" do
|
49
|
+
setup do
|
50
|
+
@features = [:read_packets, :read_frames, :show_files, :show_frames, :show_packets, :show_streams, :show_tags, :pretty]
|
51
|
+
@ffprobe = FFProbe.new(*@features)
|
52
|
+
end
|
53
|
+
should "retain its configuration" do
|
54
|
+
assert @ffprobe.features.length == @features.length
|
55
|
+
@features.each {|f| @ffprobe.features.include?(f) }
|
56
|
+
end
|
57
|
+
should "be :pretty?" do
|
58
|
+
assert @ffprobe.pretty?
|
59
|
+
end
|
60
|
+
context "probing a file" do
|
61
|
+
should "return successfully" do
|
62
|
+
@ffprobe.probe(testcase_path("source.ogv"))
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
|
72
|
+
|
73
|
+
|
74
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require File.join(File.dirname(__FILE__),"..","helper.rb")
|
4
|
+
|
5
|
+
class TestFfprobe
|
6
|
+
class TestFileInfo < Test::Unit::TestCase
|
7
|
+
|
8
|
+
context "A FileInfo instance" do
|
9
|
+
|
10
|
+
context "that is new" do
|
11
|
+
setup do
|
12
|
+
@instance = FFProbe::FileInfo.new
|
13
|
+
end
|
14
|
+
|
15
|
+
should_respond_to :@instance,
|
16
|
+
:filename, :nb_streams, :demuxer_name, :demuxer_long_name,
|
17
|
+
:start_time, :duration, :size, :bit_rate
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
|
27
|
+
|
28
|
+
|
29
|
+
|
@@ -0,0 +1,147 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require File.join(File.dirname(__FILE__),"..","helper.rb")
|
4
|
+
|
5
|
+
class TestFfprobe
|
6
|
+
class TestParser < Test::Unit::TestCase
|
7
|
+
|
8
|
+
context "A Parser" do
|
9
|
+
|
10
|
+
setup do
|
11
|
+
@parser = @instance = FFProbe::Parser.new
|
12
|
+
end
|
13
|
+
|
14
|
+
should_respond_to :parse_stream
|
15
|
+
|
16
|
+
context "given empty ffprobe results" do
|
17
|
+
setup do
|
18
|
+
@parsed = @parser.parse_stream(open_testcase("no_args"))
|
19
|
+
end
|
20
|
+
|
21
|
+
should "return a hash with only EXTRA (or nothing)" do
|
22
|
+
assert @parsed.length <= 1
|
23
|
+
assert [nil,:EXTRA].include?(@parsed.keys[0])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
context "given movie ffprobe results" do
|
28
|
+
setup do
|
29
|
+
@parsed = @parser.parse_stream(open_testcase("files_streams_tags"))
|
30
|
+
end
|
31
|
+
|
32
|
+
should "return a non-empty result" do
|
33
|
+
assert @parsed.length > 1
|
34
|
+
end
|
35
|
+
|
36
|
+
should "have only Symbol keys" do
|
37
|
+
@parsed.keys.each {|key| assert_kind_of Symbol, key }
|
38
|
+
end
|
39
|
+
|
40
|
+
should "have only Array values" do
|
41
|
+
@parsed.values.each {|value| assert_kind_of Array, value }
|
42
|
+
end
|
43
|
+
|
44
|
+
context "in FILE stanza" do
|
45
|
+
setup do
|
46
|
+
@files = @parsed[:FILE]
|
47
|
+
@file = @files.first if @files
|
48
|
+
end
|
49
|
+
|
50
|
+
should "have one" do
|
51
|
+
assert_equal 1, @files.length
|
52
|
+
end
|
53
|
+
|
54
|
+
should "have a filename" do
|
55
|
+
assert_match /./, @file[:filename]
|
56
|
+
end
|
57
|
+
|
58
|
+
should "have a number of streams" do
|
59
|
+
assert_match /\A\d+\Z/, @file[:nb_streams]
|
60
|
+
end
|
61
|
+
|
62
|
+
should "have a demuxer name" do
|
63
|
+
assert_match /./, @file[:demuxer_name]
|
64
|
+
end
|
65
|
+
|
66
|
+
should "have a demuxer long name" do
|
67
|
+
assert_match /./, @file[:demuxer_long_name]
|
68
|
+
end
|
69
|
+
|
70
|
+
should "have a start time" do
|
71
|
+
assert_match /\d/, @file[:start_time]
|
72
|
+
end
|
73
|
+
|
74
|
+
should "have a duration" do
|
75
|
+
assert_match /\d/, @file[:duration]
|
76
|
+
end
|
77
|
+
|
78
|
+
should "have a size" do
|
79
|
+
assert_match /\d/, @file[:size]
|
80
|
+
end
|
81
|
+
|
82
|
+
should "have a bit rate" do
|
83
|
+
assert_match /\d/, @file[:bit_rate]
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
context "in STREAM stanzas" do
|
88
|
+
setup do
|
89
|
+
@streams = @parsed[:STREAM]
|
90
|
+
@video_stream = @streams.first if @streams
|
91
|
+
@audio_stream = @streams.last if @streams
|
92
|
+
end
|
93
|
+
|
94
|
+
should "have a video stream and an audio stream" do
|
95
|
+
assert_equal 2, @streams.length
|
96
|
+
assert_equal "video", @video_stream[:codec_type]
|
97
|
+
assert_equal "audio", @audio_stream[:codec_type]
|
98
|
+
end
|
99
|
+
|
100
|
+
should_have_string_value :@video_stream, %w(
|
101
|
+
codec_name codec_type pix_fmt
|
102
|
+
)
|
103
|
+
should_have_numeric_value :@video_stream, %w(
|
104
|
+
decoder_time_base r_frame_rate r_frame_rate_num r_frame_rate_den
|
105
|
+
width height gop_size has_b_frames sample_aspect_ratio display_aspect_ratio
|
106
|
+
index time_base start_time nb_frames
|
107
|
+
)
|
108
|
+
|
109
|
+
should_have_string_value :@audio_stream, %w(
|
110
|
+
codec_name codec_type
|
111
|
+
)
|
112
|
+
should_have_numeric_value :@audio_stream, %w(
|
113
|
+
decoder_time_base sample_rate channels bits_per_sample index
|
114
|
+
time_base start_time duration nb_frames
|
115
|
+
)
|
116
|
+
end
|
117
|
+
|
118
|
+
context "in TAGS stanza" do
|
119
|
+
setup do
|
120
|
+
@tags_stanzas = @parsed[:TAGS]
|
121
|
+
@tags = @tags_stanzas.first if @tags_stanzas
|
122
|
+
end
|
123
|
+
|
124
|
+
should "have one" do
|
125
|
+
assert_equal 1, @tags_stanzas.length
|
126
|
+
end
|
127
|
+
|
128
|
+
should "have keys from id3v1" do
|
129
|
+
assert_equal %w(track title author copyright comment album year genre).sort,
|
130
|
+
@tags.keys.map(&:to_s).sort
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
end
|
139
|
+
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
|
144
|
+
|
145
|
+
|
146
|
+
|
147
|
+
|
data/test/helper.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
|
2
|
+
$VERBOSE = false
|
3
|
+
require 'test/unit'
|
4
|
+
require 'rubygems'
|
5
|
+
require 'shoulda'
|
6
|
+
|
7
|
+
begin
|
8
|
+
here = File.dirname(__FILE__)
|
9
|
+
%w(lib bin test).each do |dir|
|
10
|
+
path = "#{here}/../#{dir}"
|
11
|
+
$LOAD_PATH.unshift(path) unless $LOAD_PATH.include?(path)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
require 'ffprobe'
|
16
|
+
|
17
|
+
def open_testcase(name)
|
18
|
+
File.open(testcase_path("#{name}.testcase"))
|
19
|
+
end
|
20
|
+
|
21
|
+
def testcase_path(name)
|
22
|
+
File.join(File.dirname(__FILE__), 'testcases', name)
|
23
|
+
end
|
24
|
+
|
25
|
+
def should_respond_to(instance_var, *methods)
|
26
|
+
methods.each do |method|
|
27
|
+
should "respond to ##{method}" do
|
28
|
+
assert_respond_to instance_variable_get(instance_var), method.to_sym
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def should_have_numeric_value(instance_var, *keys)
|
34
|
+
keys.flatten.each do |key|
|
35
|
+
should "have a numeric #{key} in #{instance_var}" do
|
36
|
+
assert_match /\d/, instance_variable_get(instance_var)[key.to_sym]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def should_have_string_value(instance_var, *keys)
|
42
|
+
keys.flatten.each do |key|
|
43
|
+
should "have a string #{key} in #{instance_var}" do
|
44
|
+
assert_kind_of String, instance_variable_get(instance_var)[key.to_sym]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/test/test_result.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require File.join(File.dirname(__FILE__),"helper.rb")
|
4
|
+
|
5
|
+
class FFProbe
|
6
|
+
class ResultTest < Test::Unit::TestCase
|
7
|
+
|
8
|
+
context "An Result instance" do
|
9
|
+
|
10
|
+
context "that is new" do
|
11
|
+
setup do
|
12
|
+
@result = FFProbe::Result.new
|
13
|
+
end
|
14
|
+
|
15
|
+
should "have a file object" do
|
16
|
+
assert_kind_of FileInfo, @result.file
|
17
|
+
end
|
18
|
+
|
19
|
+
should "have an array of frames" do
|
20
|
+
assert_kind_of Array, @result.frames
|
21
|
+
end
|
22
|
+
|
23
|
+
should "have no frames" do
|
24
|
+
assert_equal 0, @result.frames.length
|
25
|
+
end
|
26
|
+
|
27
|
+
should "have an array of packets" do
|
28
|
+
assert_kind_of Array, @result.packets
|
29
|
+
end
|
30
|
+
|
31
|
+
should "have no packets" do
|
32
|
+
assert_equal 0, @result.packets.length
|
33
|
+
end
|
34
|
+
|
35
|
+
should "have an array of streams" do
|
36
|
+
assert_kind_of Array, @result.streams
|
37
|
+
end
|
38
|
+
|
39
|
+
should "have no streams" do
|
40
|
+
assert_equal 0, @result.streams.length
|
41
|
+
end
|
42
|
+
|
43
|
+
should "have a tags object" do
|
44
|
+
assert_kind_of TagsInfo, @result.tags
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|