ruby-ffprobe 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ .config
2
+ InstalledFiles
3
+ *.gem
4
+ doc
5
+ *.swp
6
+ test/testcases/source.ogv
7
+ coverage
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Philip Garrett.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a
4
+ copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included
12
+ in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,38 @@
1
+ = FFProbe - ffprobe wrapper for Ruby
2
+
3
+ FFprobe is a simple multimedia streams analyzer with a command-line
4
+ interface based on the FFmpeg project libraries. This is a ruby
5
+ interface to that command-line program.
6
+
7
+ == You will need
8
+
9
+ * A working Ruby installation
10
+ * A working ffprobe installation (http://sourceforge.net/projects/ffprobe/)
11
+ * A sane build environment
12
+
13
+ == Author
14
+
15
+ * Philip Garrett <philgarr at gmail.com>
16
+
17
+ == Copyright and License
18
+
19
+ Copyright (c) 2010 Philip Garrett.
20
+
21
+ Permission is hereby granted, free of charge, to any person obtaining a
22
+ copy of this software and associated documentation files (the
23
+ "Software"), to deal in the Software without restriction, including
24
+ without limitation the rights to use, copy, modify, merge, publish,
25
+ distribute, sublicense, and/or sell copies of the Software, and to
26
+ permit persons to whom the Software is furnished to do so, subject to
27
+ the following conditions:
28
+
29
+ The above copyright notice and this permission notice shall be included
30
+ in all copies or substantial portions of the Software.
31
+
32
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
33
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
34
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
35
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
36
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
37
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
38
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,87 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+ require 'rcov/rcovtask'
5
+
6
+ begin
7
+ require 'jeweler'
8
+ Jeweler::Tasks.new do |gemspec|
9
+ gemspec.name = "ruby-ffprobe"
10
+ gemspec.summary = "Ruby wrapper for FFprobe multimedia analyzer"
11
+ gemspec.description = File.read(File.join(File.dirname(__FILE__),"README.rdoc"))
12
+ gemspec.email = "philgarr@gmail.com"
13
+ gemspec.homepage = "http://github.com/kingpong/ruby-ffprobe"
14
+ gemspec.authors = ["Philip Garrett"]
15
+ end
16
+ Jeweler::GemcutterTasks.new
17
+ rescue LoadError
18
+ puts "Jeweler not available. Install it with: gem install jeweler"
19
+ end
20
+
21
+ namespace "ffprobe" do
22
+ task "exec" do
23
+ next if ENV["FFPROBE"]
24
+ ENV['PATH'].split(":").each do |dir|
25
+ fqpath = File.join(dir,"ffprobe")
26
+ if File.executable?(fqpath)
27
+ ENV['FFPROBE'] = fqpath
28
+ break
29
+ end
30
+ end
31
+ next if ENV["FFPROBE"]
32
+ raise "Could not find ffprobe in your path. You may set the environment variable FFPROBE to point to your ffprobe executable"
33
+ end
34
+ end
35
+
36
+ task "set_testopts_verbose" do
37
+ ENV["TESTOPTS"] = '-v'
38
+ end
39
+
40
+ Rake::TestTask.new do |t|
41
+ t.libs << "test"
42
+ t.test_files = FileList['test/**/test*.rb']
43
+ t.verbose = true
44
+ end
45
+
46
+ desc "Run tests with verbosity enabled"
47
+ task "vtest" => [:set_testopts_verbose, :test]
48
+
49
+ namespace "test" do
50
+ desc "Generate ffprobe output test cases"
51
+ task "cases" => ["ffprobe:exec","test/testcases/source.ogv"] do |t|
52
+ require "lib/ffprobe/safe_pipe"
53
+ dest_dir = File.expand_path("test/testcases")
54
+ source = File.expand_path(t.prerequisites.last)
55
+ variations = { "no_args" => [] }
56
+ %w{files frames packets streams tags}.each do |opt|
57
+ variations[opt] = ["-show_#{opt}"]
58
+ variations["pretty_#{opt}"] = ["-show_#{opt}", "-pretty"]
59
+ end
60
+ variations["files_streams_tags"] = ["-show_files", "-show_streams", "-show_tags"]
61
+ variations["pretty_files_streams_tags"] = ["-show_files", "-show_streams", "-show_tags", "-pretty"]
62
+ variations.each_pair do |name,args|
63
+ FFProbe::SafePipe.new(ENV["FFPROBE"], *(args + [source])).run do |pipe|
64
+ File.open(File.join(dest_dir,"#{name}.testcase"),"w") do |output|
65
+ output.write pipe.read
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ desc "Cleanup test output and testcases"
72
+ task "clean" do
73
+ Dir["test/testcases/*.testcase"].each { |file| File.unlink(file) }
74
+ end
75
+ end
76
+
77
+ Rcov::RcovTask.new("coverage") do |t|
78
+ t.libs << "test"
79
+ t.test_files = FileList["test/**/test_*.rb"]
80
+ t.output_dir = "coverage"
81
+ t.verbose = true
82
+ t.rcov_opts << '--exclude /gems/'
83
+ end
84
+
85
+ task "rcov" => "coverage"
86
+
87
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
data/lib/ffprobe.rb ADDED
@@ -0,0 +1,106 @@
1
+ #
2
+ # ffprobe.rb
3
+ #
4
+ # Copyright (c) 2010 Philip Garrett.
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a
7
+ # copy of this software and associated documentation files (the
8
+ # "Software"), to deal in the Software without restriction, including
9
+ # without limitation the rights to use, copy, modify, merge, publish,
10
+ # distribute, sublicense, and/or sell copies of the Software, and to
11
+ # permit persons to whom the Software is furnished to do so, subject to
12
+ # the following conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be included
15
+ # in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
18
+ # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
20
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
21
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
22
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
23
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
+ #
25
+
26
+ require 'ffprobe/entity'
27
+ require 'ffprobe/file_info'
28
+ require 'ffprobe/frame_info'
29
+ require 'ffprobe/packet_info'
30
+ require 'ffprobe/parser'
31
+ require 'ffprobe/result'
32
+ require 'ffprobe/safe_pipe'
33
+ require 'ffprobe/stream_info'
34
+ require 'ffprobe/tags_info'
35
+
36
+ class FFProbe
37
+
38
+ class << self
39
+ attr_accessor :executable
40
+ end
41
+ self.executable ||= "ffprobe"
42
+
43
+ class InvalidArgument < StandardError; end
44
+
45
+ FEATURES = [ :read_packets, :read_frames, :show_files, :show_frames, :show_packets, :show_streams, :show_tags, :pretty ]
46
+ FEATURES.each {|feature| attr_accessor(feature) }
47
+
48
+ DEFAULT_FEATURES = [:show_streams, :show_tags]
49
+
50
+ attr_accessor :features
51
+ def features=(desired_features)
52
+ @features = desired_features.each {|feature|
53
+ raise(InvalidArgument, "Unrecognized feature #{feature}") unless FEATURES.include?(feature)
54
+ }
55
+ end
56
+
57
+ attr_accessor :units
58
+ alias :units? :units
59
+ def units=(bool)
60
+ require 'ffprobe/unit' if bool
61
+ @units = bool
62
+ end
63
+
64
+ def initialize(*desired_features)
65
+ desired_features = DEFAULT_FEATURES if desired_features.empty?
66
+ self.features = desired_features.reject {|f| f == :units }
67
+ self.units = desired_features.include?(:units)
68
+ end
69
+
70
+ def probe(filename)
71
+ params = features.map {|f| "-#{f.to_s}" }
72
+ params << filename
73
+ result = nil
74
+ pipe = SafePipe.new(executable,*params)
75
+ pipe.run do |stream|
76
+ result = Result.from_stream(stream, self.units?)
77
+ end
78
+ result.success = pipe.success?
79
+ result.pretty = self.pretty? if result
80
+ result
81
+ end
82
+
83
+ def pretty?
84
+ features.include?(:pretty)
85
+ end
86
+
87
+ private
88
+
89
+ def executable
90
+ self.class.executable
91
+ end
92
+
93
+ end
94
+
95
+
96
+
97
+
98
+
99
+
100
+
101
+
102
+
103
+
104
+
105
+
106
+
@@ -0,0 +1,33 @@
1
+ class FFProbe
2
+ class Entity
3
+
4
+ def self.from_hash(hash,units=false)
5
+ target = new
6
+ target.units = units
7
+ hash.each {|k,v| target.__send__("#{k}=", v) }
8
+ target
9
+ end
10
+
11
+ attr_accessor :units
12
+ alias :units? :units
13
+
14
+ def self.units(mapping)
15
+ mapping.each do |property,unit|
16
+ self.class_eval <<-EOF
17
+ def #{property}=(val)
18
+ if units?
19
+ if val.nil?
20
+ @#{property} = nil
21
+ else
22
+ @#{property} = Unit.new("\#{val} #{unit}")
23
+ end
24
+ else
25
+ @#{property} = val
26
+ end
27
+ end
28
+ EOF
29
+ end
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,14 @@
1
+ class FFProbe
2
+ class FileInfo < Entity
3
+
4
+ attr_accessor :filename, :nb_streams, :demuxer_name, :demuxer_long_name,
5
+ :start_time, :duration, :size, :bit_rate,
6
+ :probed_size, :probed_nb_pkts, :probed_nb_frames
7
+
8
+ units :start_time => "s",
9
+ :duration => "s",
10
+ :size => "byte",
11
+ :bit_rate => "bit/s"
12
+
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ class FFProbe
2
+ class FrameInfo < Entity
3
+
4
+ attr_accessor :codec_type, :pict_type, :quality, :coded_picture_number, :display_picture_number,
5
+ :interlaced_frame, :repeat_pict, :reference, :samples_size, :stream_index,
6
+ :size, :pkt_pts, :pkt_dts, :pkt_duration, :file_pkt_nb, :stream_pkt_nb, :pkt_flag_key
7
+
8
+ units :size => "byte",
9
+ :pkt_pts => "s",
10
+ :pkt_dts => "s",
11
+ :pkt_duration => "s",
12
+ :samples_size => "byte"
13
+
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ class FFProbe
2
+ class PacketInfo < Entity
3
+
4
+ attr_accessor :codec_type, :stream_index, :pts, :pts_time, :dts, :dts_time,
5
+ :size, :file_pkt_nb, :stream_pkt_nb, :duration_ts, :duration_time, :flag_key
6
+
7
+ units :pts_time => "s",
8
+ :dts_time => "s",
9
+ :size => "byte",
10
+ :duration_time => "s"
11
+
12
+ end
13
+ end
@@ -0,0 +1,27 @@
1
+ class FFProbe
2
+ class Parser
3
+
4
+ def parse_stream(io)
5
+ result = {}
6
+ dest = nil
7
+ io.each_line do |line|
8
+ # $stderr.puts line
9
+ line.chomp!
10
+ case line
11
+ when /\A\[(\w+)\]\Z/
12
+ (result[$1.to_sym] ||= []).push( dest = {} )
13
+ when /\A\[\/(\w+)\]\Z/
14
+ # ignored, stanzas can't nest.
15
+ when /\A(\w+)=(.*)/
16
+ if dest
17
+ dest[$1.to_sym] = $2 == "N/A" ? nil : $2.strip
18
+ end
19
+ else
20
+ (result[:EXTRA] ||= []).push(line)
21
+ end
22
+ end
23
+ result
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,107 @@
1
+
2
+ class FFProbe
3
+ class Result
4
+
5
+ def self.from_stream(io,units=false)
6
+ from_hash(Parser.new.parse_stream(io), units)
7
+ end
8
+
9
+ def self.from_hash(hash,units=false)
10
+ target = new
11
+ target.units = units
12
+ hash.each do |key,value|
13
+ case key
14
+ when :FILE
15
+ target.file = FileInfo.from_hash(value[0], units)
16
+ when :FRAME
17
+ target.frames += value.map {|f| FrameInfo.from_hash(f, units) }
18
+ when :PACKET
19
+ target.packets += value.map {|p| PacketInfo.from_hash(p, units) }
20
+ when :STREAM
21
+ target.streams += value.map {|s| StreamInfo.from_hash(s, units) }
22
+ when :TAGS
23
+ target.tags = TagsInfo.from_hash(value[0], units)
24
+ when :EXTRA
25
+ target.extra = Array.new(value)
26
+ end
27
+ end
28
+ target
29
+ end
30
+
31
+ attr_accessor :file
32
+ attr_accessor :frames
33
+ attr_accessor :packets
34
+ attr_accessor :streams
35
+ attr_accessor :tags
36
+ attr_accessor :extra
37
+
38
+ attr_accessor :pretty
39
+ alias :pretty? :pretty
40
+
41
+ attr_accessor :success
42
+ alias :success? :success
43
+
44
+ attr_accessor :units
45
+ alias :units? :units
46
+
47
+ def initialize
48
+ @file = FileInfo.new
49
+ @frames = []
50
+ @packets = []
51
+ @streams = []
52
+ @tags = TagsInfo.new
53
+ @extra = []
54
+ end
55
+
56
+ def has_video?
57
+ @streams.first {|s| s.codec_type == "video" }
58
+ end
59
+
60
+ def video?
61
+ @streams.length == 1 && has_video?
62
+ end
63
+
64
+ def has_audio?
65
+ @streams.first {|s| s.codec_type == "audio" }
66
+ end
67
+
68
+ def audio?
69
+ @streams.length == 1 && has_audio?
70
+ end
71
+
72
+ end
73
+ end
74
+
75
+
76
+
77
+
78
+
79
+
80
+
81
+
82
+
83
+
84
+
85
+
86
+
87
+
88
+
89
+
90
+
91
+
92
+
93
+
94
+
95
+
96
+
97
+
98
+
99
+
100
+
101
+
102
+
103
+
104
+
105
+
106
+
107
+ #