ruby-ffprobe 0.0.1
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 +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
|