ruby-ffprobe 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,7 @@
1
+ class FFProbe
2
+ class TagsInfo < Entity
3
+
4
+ attr_accessor :track, :title, :author, :copyright, :comment, :album, :year, :genre
5
+
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ require 'ruby_units'
2
+
3
+ class FFProbe
4
+ class Unit < ::Unit
5
+ def to_i
6
+ scalar.to_i
7
+ end
8
+
9
+ def to_f
10
+ scalar.to_f
11
+ end
12
+ end
13
+ end
@@ -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
@@ -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