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.
@@ -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