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
data/.gitignore
ADDED
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
|
+
#
|