mediainfo 0.7.2 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +16 -0
- data/.rspec +3 -0
- data/Changelog +19 -3
- data/GemFile +5 -0
- data/LICENSE +5 -14
- data/README.md +140 -0
- data/Rakefile +4 -61
- data/lib/attribute_standardization_rules.yml +18 -0
- data/lib/mediainfo.rb +85 -497
- data/lib/mediainfo/errors.rb +14 -0
- data/lib/mediainfo/string.rb +0 -10
- data/lib/mediainfo/tracks.rb +172 -0
- data/lib/mediainfo/version.rb +3 -0
- data/mediainfo.gemspec +22 -19
- metadata +96 -69
- data/Manifest +0 -10
- data/README.markdown +0 -84
- data/index.html.template +0 -9
- data/lib/mediainfo/attr_readers.rb +0 -75
- data/test/mediainfo_awaywego_test.rb +0 -284
- data/test/mediainfo_broken_embraces_test.rb +0 -264
- data/test/mediainfo_dinner_test.rb +0 -262
- data/test/mediainfo_hats_test.rb +0 -254
- data/test/mediainfo_multiple_streams_test.rb +0 -526
- data/test/mediainfo_omen_image_test.rb +0 -232
- data/test/mediainfo_string_test.rb +0 -20
- data/test/mediainfo_subtilte_test.rb +0 -44
- data/test/mediainfo_test.rb +0 -152
- data/test/mediainfo_vimeo_test.rb +0 -262
- data/test/test_helper.rb +0 -19
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a35ae85e3e7fa526c49c97050a6121e744f51e4db9079457d2eb5e6da5f5ca01
|
4
|
+
data.tar.gz: 4789c91354bcd189b99951c2cc424e5f4bdf05f18e01563487306e54cdc90bd2
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 170f8c1656897087b1fceb1dc82cf63e390d466b8d721e19bd2065f62016eb5972bcff5c0018e820e68bb6b64705bd888c2c04773c13eb161abe72f318b4bb7b
|
7
|
+
data.tar.gz: fa98fbcadf950ac6bba432228370763fa2b069aec50ac39ff6a9c574fa6637eafdb217dded2a41045c0812c6943e0d207c91f2c2b7ff18e05aa58bbb504c0580
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Changelog
CHANGED
@@ -1,6 +1,22 @@
|
|
1
|
-
|
2
|
-
-
|
3
|
-
-
|
1
|
+
v1.0.0
|
2
|
+
- Rewrite of foundation
|
3
|
+
- Module name change to MediaInfo to match convention (everything else does MediaInfo with an uppercase I)
|
4
|
+
- Readme rewrite + examples
|
5
|
+
- Rspec for testing, all features tested + new xml to test with
|
6
|
+
- Removal of all double quotes for single quotes, unless interpolation is needed
|
7
|
+
- Removal of underscore method that isn't used
|
8
|
+
- Variable name changes for readability and DRY
|
9
|
+
- Hpricot is EOL, removing code to support it
|
10
|
+
- Removal of all old testing, Rakefile contents
|
11
|
+
- Every method missing exception will return nil (if the type exists)
|
12
|
+
- New .track_types for visibility in client application
|
13
|
+
- pull/18 (Eliminated attr_reader needing to be updated)
|
14
|
+
- pull/12 (Eliminated need to add new Track/Streams)
|
15
|
+
- pull/15 (URL parsing added)
|
16
|
+
- Supports the use of <ID> element value (if integer/existent) when generating "track#"
|
17
|
+
- Supports <extra> (Apple/iPhone video)
|
18
|
+
- Parameter/element sanitization + conversion of string with integer/float in it to integer/float
|
19
|
+
- Standardized attribute names under tracks & added yml file to keep track
|
4
20
|
|
5
21
|
v0.7.1 Tweakage
|
6
22
|
- Removed Mediainfo#parsed_response accessor which always returned nil after
|
data/GemFile
ADDED
data/LICENSE
CHANGED
@@ -1,18 +1,9 @@
|
|
1
|
-
|
2
|
-
the extent permitted by applicable law. You can redistribute it
|
3
|
-
and/or modify it under the terms of the Do What The Fuck You Want
|
4
|
-
To Public License, Version 2, as published by Sam Hocevar. See
|
5
|
-
http://sam.zoy.org/wtfpl/COPYING for more details.
|
1
|
+
MIT License
|
6
2
|
|
7
|
-
|
8
|
-
Version 2, December 2004
|
3
|
+
Copyright (c) <year> <copyright holders>
|
9
4
|
|
10
|
-
|
11
|
-
Everyone is permitted to copy and distribute verbatim or modified
|
12
|
-
copies of this license document, and changing it is allowed as long
|
13
|
-
as the name is changed.
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
14
6
|
|
15
|
-
|
16
|
-
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
17
8
|
|
18
|
-
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
# MediaInfo
|
2
|
+
|
3
|
+
MediaInfo is a class wrapping [the mediainfo CLI](http://mediainfo.sourceforge.net).
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
$ gem install mediainfo
|
8
|
+
|
9
|
+
## Usage
|
10
|
+
|
11
|
+
#### Parsing raw XML
|
12
|
+
media_info = MediaInfo.from(File.open('iphone6+_video.mov.xml').read)
|
13
|
+
#### Handling a local file
|
14
|
+
media_info = MediaInfo.from('~/Desktop/test.mov')
|
15
|
+
#### Handling a URL
|
16
|
+
media_info = MediaInfo.from('http://techslides.com/demos/sample-videos/small.mp4')
|
17
|
+
|
18
|
+
You can specify an alternate path for the MediaInfo Binary:
|
19
|
+
|
20
|
+
ENV['MEDIAINFO_PATH'] = "/opt/local/bin/mediainfo"
|
21
|
+
|
22
|
+
Once you have an MediaInfo object, you can start inspecting tracks:
|
23
|
+
|
24
|
+
media_info.track_types => ['general','video','audio']
|
25
|
+
media_info.track_types.count => 3
|
26
|
+
media_info.video? => true
|
27
|
+
media_info.image? => nil
|
28
|
+
media_info.image.filesize => MethodNotFound exception
|
29
|
+
|
30
|
+
When inspecting specific types of tracks, you have a couple general API options. The
|
31
|
+
first approach assumes one track of a given type, a common scenario in many video files,
|
32
|
+
for example:
|
33
|
+
|
34
|
+
media_info.video.count => 1
|
35
|
+
media_info.video.duration => 120 (seconds)
|
36
|
+
|
37
|
+
Sometimes you'll have more than one track of a given type:
|
38
|
+
- The first track type name, or any track type with <ID>1</ID> will not contain '1'
|
39
|
+
|
40
|
+
|
41
|
+
media_info.track_types => ['general','video','video2','audio','other','other2']
|
42
|
+
media_info.track_types.count => 5
|
43
|
+
media_info.video? => true
|
44
|
+
media_info.image? => nil
|
45
|
+
media_info.video.count => 1
|
46
|
+
media_info.video.duration => 29855000
|
47
|
+
media_info.video.display_aspect_ratio => 1.222
|
48
|
+
media_info.other.count => 2
|
49
|
+
media_info.video2.duration => 29855000
|
50
|
+
|
51
|
+
- Note that the above automatically converts MediaInfo Strings into Time, Integer, and Float objects:
|
52
|
+
|
53
|
+
|
54
|
+
media_info.video.encoded_date.class => Time
|
55
|
+
media_info.video2.duration.class => Integer
|
56
|
+
media_info.video.display_aspect_ratio.class => Float
|
57
|
+
|
58
|
+
- Any track attribute name with "date" and matching /\d-/ will be converted using Time.parse:
|
59
|
+
|
60
|
+
|
61
|
+
media_info.video.encoded_date => 2018-03-30 12:12:08 -0400
|
62
|
+
media_info.video.customdate => 2016-02-10 01:00:00 -0600
|
63
|
+
|
64
|
+
- .duration and .overall_duration will be returned as milliseconds AS LONG AS the Duration and Overall_Duration match one of the expected units:
|
65
|
+
- h (\<Duration>15h\</Duration>) (hour)
|
66
|
+
- hour (\<Duration>15hour\</Duration>)
|
67
|
+
- mn (\<Duration>15hour 6mn\</Duration>) (minute)
|
68
|
+
- m (\<Duration>15hour 6m\</Duration>) (minute)
|
69
|
+
- min (\<Duration>15hour 6min\</Duration>) (minute)
|
70
|
+
- s (\<Duration>15hour 6min 59s\</Duration>) (second)
|
71
|
+
- sec (\<Duration>15hour 6min 59sec\</Duration>) (second)
|
72
|
+
- ms (\<Duration>15hour 6min 59sec 301ms\</Duration>) (milliseconds)
|
73
|
+
- [Submit an issue to add more!](https://github.com/greatseth/mediainfo/issues)
|
74
|
+
|
75
|
+
|
76
|
+
media_info.video.duration => 9855000 (\<Duration>15s 164ms\</Duration>)
|
77
|
+
media_info.video.duration => 17196000 (\<Duration>36s 286ms\</Duration>)
|
78
|
+
|
79
|
+
- We standardize the naming of several Attributes:
|
80
|
+
- You can review lib/attribute_standardization_rules.yml to see them all
|
81
|
+
|
82
|
+
|
83
|
+
media_info.video.bitrate => "41.2 Mbps" (\<Bit_rate>41.2 Mbps\</Bit_rate>)
|
84
|
+
media_info.video.bit_rate => nil (\<Bit_rate>41.2 Mbps\</Bit_rate>)
|
85
|
+
media_info.general.filesize => "11.5 MiB" (\<File_size>11.5 MiB\</File_size>
|
86
|
+
|
87
|
+
|
88
|
+
In order to support all possible MediaInfo variations, you may see the following situation:
|
89
|
+
|
90
|
+
media_info.track_types => ['general','video','video5','audio','other','other2']
|
91
|
+
|
92
|
+
The track type media_info.video5 is available, but no video2, 3, and 4. This is because the MediaInfo from the video has:
|
93
|
+
|
94
|
+
<track type="Video">
|
95
|
+
<ID>1</ID>
|
96
|
+
...
|
97
|
+
<track type="Video">
|
98
|
+
<ID>5</ID>
|
99
|
+
...
|
100
|
+
|
101
|
+
*The ID will take priority for labeling.* Else if no ID exists, you'll see consecutive numbering for duplicate tracks in the Media.
|
102
|
+
|
103
|
+
Any second level attributes are also available:
|
104
|
+
|
105
|
+
MediaInfo.from('~/Desktop/test.mov').general.extra
|
106
|
+
=> #<MediaInfo::Tracks::Attributes::Extra:0x00007fa89f13aa98
|
107
|
+
@com_apple_quicktime_creationdate=2018-03-30 08:12:08 -0400,
|
108
|
+
@com_apple_quicktime_location_iso6709="+39.0286-077.3958+095.957/",
|
109
|
+
@com_apple_quicktime_make="Apple",
|
110
|
+
@com_apple_quicktime_model=0,
|
111
|
+
@com_apple_quicktime_software=11.2>
|
112
|
+
|
113
|
+
REXML is used as the XML parser by default. If you'd like, you can
|
114
|
+
configure Mediainfo to use Nokogiri instead:
|
115
|
+
|
116
|
+
* define the `MEDIAINFO_XML_PARSER` environment variable to be the
|
117
|
+
name of the parser as you'd pass to a :gem or :require call.
|
118
|
+
|
119
|
+
e.g. `export MEDIAINFO_XML_PARSER=nokogiri`
|
120
|
+
|
121
|
+
Once you've got an instance setup, you can call numerous methods to get
|
122
|
+
a variety of information about a file. Some attributes may be present
|
123
|
+
for some files where others are not, but any supported attribute
|
124
|
+
should at least return `nil`.
|
125
|
+
|
126
|
+
## Requirements
|
127
|
+
|
128
|
+
Gem version 1.0.0 has been tested on v18.03.1
|
129
|
+
Gem versions < 1.0.0 require at least: MediaInfoLib v0.7.25
|
130
|
+
Gem versions <= 0.5.1 worked against MediaInfoLib v0.7.11, which did not
|
131
|
+
generate XML output, and is no longer supported.
|
132
|
+
|
133
|
+
## Contributors
|
134
|
+
|
135
|
+
* Seth Thomas Rasmussen - [http://greatseth.com](http://greatseth.com)
|
136
|
+
* Peter Vandenberk - [http://github.com/pvdb](http://github.com/pvdb)
|
137
|
+
* Ned Campion - [http://github.com/nedcampion](http://github.com/nedcampion)
|
138
|
+
* Daniel Jagszent - [http://github.com/d--j](http://github.com/d--j)
|
139
|
+
* Robert Mrasek - [http://github.com/derobo](http://github.com/derobo)
|
140
|
+
* Nathan Pierce - [http://github.com/NorseGaud](http://github.com/NorseGaud)
|
data/Rakefile
CHANGED
@@ -1,63 +1,6 @@
|
|
1
|
-
require "
|
2
|
-
require "
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rspec/core/rake_task"
|
3
3
|
|
4
|
-
|
5
|
-
def honor_gitignore!
|
6
|
-
self.ignore_pattern += \
|
7
|
-
Dir["**/.gitignore"].inject([]) do |pattern,gitignore|
|
8
|
-
pattern.concat \
|
9
|
-
File.readlines(gitignore).
|
10
|
-
map { |line| line.strip }.
|
11
|
-
reject { |line| "" == line }.
|
12
|
-
map { |glob|
|
13
|
-
d = File.dirname(gitignore)
|
14
|
-
d == "." ? glob : File.join(d, glob)
|
15
|
-
}
|
16
|
-
end.flatten.uniq
|
17
|
-
end
|
18
|
-
end
|
4
|
+
RSpec::Core::RakeTask.new(:spec)
|
19
5
|
|
20
|
-
|
21
|
-
p.description = p.summary = "Mediainfo is a class wrapping the mediainfo CLI (http://mediainfo.sourceforge.net)"
|
22
|
-
p.author = "Seth Thomas Rasmussen"
|
23
|
-
p.email = "sethrasmussen@gmail.com"
|
24
|
-
p.url = "http://greatseth.github.com/mediainfo"
|
25
|
-
p.ignore_pattern = %w( test/**/* )
|
26
|
-
p.retain_gemspec = true
|
27
|
-
p.honor_gitignore!
|
28
|
-
end
|
29
|
-
|
30
|
-
task :rexml do
|
31
|
-
ENV.delete "MEDIAINFO_XML_PARSER"
|
32
|
-
Rake::Task[:test].invoke
|
33
|
-
end
|
34
|
-
|
35
|
-
task :hpricot do
|
36
|
-
ENV["MEDIAINFO_XML_PARSER"] = "hpricot"
|
37
|
-
Rake::Task[:test].invoke
|
38
|
-
end
|
39
|
-
|
40
|
-
task :nokogiri do
|
41
|
-
ENV["MEDIAINFO_XML_PARSER"] = "nokogiri"
|
42
|
-
Rake::Task[:test].invoke
|
43
|
-
end
|
44
|
-
|
45
|
-
# TODO This doesn't work.
|
46
|
-
task :all => [:rexml, :nokogiri, :hpricot]
|
47
|
-
|
48
|
-
|
49
|
-
task :fixture do
|
50
|
-
unless file = ENV["FILE"]
|
51
|
-
puts "Usage: rake mediainfo:fixture file=/path/to/file"
|
52
|
-
exit
|
53
|
-
end
|
54
|
-
fixture = File.expand_path "./test/fixtures/#{File.basename file}.xml"
|
55
|
-
system "mediainfo #{file} --Output=XML > #{fixture}"
|
56
|
-
if File.exist? fixture
|
57
|
-
puts "Generated fixture #{fixture}."
|
58
|
-
else
|
59
|
-
puts "Error generating fixture. #{fixture} not created."
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
# require 'github/pages/tasks'
|
6
|
+
task :default => :spec
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Format:
|
2
|
+
# 'ELEMENT_NAME_TO_CHANGE': 'WHAT IT NEEDS TO BE'
|
3
|
+
# BitRate will become
|
4
|
+
## Bit_rate -> Bitrate
|
5
|
+
Bit_rate: Bitrate
|
6
|
+
Bitrate_mode: Bitrate_mode
|
7
|
+
Bit_rate_mode: Bitrate_mode
|
8
|
+
Overallbitrate: Overall_bitrate
|
9
|
+
Overall_bit_rate: Overall_bitrate
|
10
|
+
## *_size -> *size
|
11
|
+
File_size: Filesize
|
12
|
+
Stream_size: Streamsize
|
13
|
+
## Frame_rate -> Framerate
|
14
|
+
Frame_rate: Framerate
|
15
|
+
Frame_rate_mode: Framerate_mode
|
16
|
+
Maximum_frame_rate: Maximum_framerate
|
17
|
+
Displayaspectratio: Display_aspect_ratio
|
18
|
+
Pixelaspectratio: Pixel_aspect_ratio
|
data/lib/mediainfo.rb
CHANGED
@@ -1,516 +1,104 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
1
|
+
require 'forwardable'
|
2
|
+
require 'net/http'
|
3
|
+
require 'mediainfo/errors'
|
4
|
+
require 'mediainfo/tracks'
|
5
|
+
require 'mediainfo/string'
|
4
6
|
|
5
|
-
|
6
|
-
# Mediainfo
|
7
|
+
module MediaInfo
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
## Usage
|
15
|
-
|
16
|
-
info = Mediainfo.new "/path/to/file"
|
17
|
-
|
18
|
-
That will issue the system call to `mediainfo` and parse the output.
|
19
|
-
You can specify an alternate path if necessary:
|
20
|
-
|
21
|
-
Mediainfo.path = "/opt/local/bin/mediainfo"
|
22
|
-
|
23
|
-
Once you have an info object, you can start inspecting streams and general metadata.
|
24
|
-
|
25
|
-
info.streams.count # 2
|
26
|
-
info.audio? # true
|
27
|
-
info.video? # true
|
28
|
-
info.image? # false
|
29
|
-
|
30
|
-
When inspecting specific types of streams, you have a couple general API options. The
|
31
|
-
first approach assumes one stream of a given type, a common scenario in many video files,
|
32
|
-
for example.
|
33
|
-
|
34
|
-
info.video.count # 1
|
35
|
-
info.audio.count # 1
|
36
|
-
info.video.duration # 120 (seconds)
|
37
|
-
|
38
|
-
Sometimes you'll have more than one stream of a given type. Quicktime files can often
|
39
|
-
contain artifacts like this from somebody editing a more 'normal' file.
|
40
|
-
|
41
|
-
info = Mediainfo.new "funky.mov"
|
42
|
-
|
43
|
-
info.video? # true
|
44
|
-
info.video.count # 2
|
45
|
-
info.video.duration # raises SingleStreamAPIError !
|
46
|
-
info.video[0].duration # 120
|
47
|
-
info.video[1].duration # 10
|
48
|
-
|
49
|
-
For some more usage examples, please see the very reasonable test suite accompanying the source code
|
50
|
-
for this library. It contains a bunch of relevant usage examples. More docs in the future.. contributions
|
51
|
-
*very* welcome!
|
52
|
-
|
53
|
-
Moving on, REXML is used as the XML parser by default. If you'd like, you can
|
54
|
-
configure Mediainfo to use Hpricot or Nokogiri instead using one of
|
55
|
-
the following approaches:
|
56
|
-
|
57
|
-
* define the `MEDIAINFO_XML_PARSER` environment variable to be the
|
58
|
-
name of the parser as you'd pass to a :gem or :require call.
|
59
|
-
|
60
|
-
e.g. `export MEDIAINFO_XML_PARSER=nokogiri`
|
61
|
-
|
62
|
-
* assign to Mediainfo.xml_parser after you've loaded the gem,
|
63
|
-
following the same naming conventions mentioned previously.
|
64
|
-
|
65
|
-
e.g. `Mediainfo.xml_parser = "hpricot"`
|
66
|
-
|
67
|
-
|
68
|
-
Once you've got an instance setup, you can call numerous methods to get
|
69
|
-
a variety of information about a file. Some attributes may be present
|
70
|
-
for some files where others are not, but any supported attribute
|
71
|
-
should at least return `nil`.
|
72
|
-
|
73
|
-
For a list of all possible attributes supported:
|
74
|
-
|
75
|
-
Mediainfo.supported_attributes
|
76
|
-
|
77
|
-
## Requirements
|
9
|
+
# Allow user to set custom mediainfo_path with ENV['MEDIAINFO_PATH']
|
10
|
+
def self.location
|
11
|
+
ENV['MEDIAINFO_PATH'].nil? ? mediainfo_location = '/usr/local/bin/mediainfo' : mediainfo_location = ENV['MEDIAINFO_PATH']
|
12
|
+
raise EnvironmentError, "#{mediainfo_location} cannot be found. Are you sure mediainfo is installed?" unless ::File.exist? mediainfo_location
|
13
|
+
return mediainfo_location
|
14
|
+
end
|
78
15
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
extend AttrReaders
|
90
|
-
|
91
|
-
class Error < StandardError; end
|
92
|
-
class ExecutionError < Error; end
|
93
|
-
class IncompatibleVersionError < Error; end
|
94
|
-
class UnknownVersionError < Error; end
|
95
|
-
|
96
|
-
def self.delegate(method_name, stream_type = nil)
|
97
|
-
if stream_type == :general
|
98
|
-
def_delegator :"@#{stream_type}_stream", method_name
|
16
|
+
# Allow collection of MediaInfo version details
|
17
|
+
def self.version
|
18
|
+
version ||= `#{location} --Version`[/v([\d.]+)/, 1]
|
19
|
+
# Ensure MediaInfo isn't buggy and returns something
|
20
|
+
raise UnknownVersionError, 'Unable to determine mediainfo version. ' + "We tried: #{location} --Version." +
|
21
|
+
'Set MediaInfo.path = \'/full/path/of/mediainfo\' if it is not in your PATH.' unless version
|
22
|
+
# Ensure you're not using an old version of MediaInfo
|
23
|
+
if version < '0.7.25'
|
24
|
+
raise IncompatibleVersionError, "Your version of mediainfo, #{version}, " +
|
25
|
+
'is not compatible with this gem. >= 0.7.25 required.'
|
99
26
|
else
|
100
|
-
|
27
|
+
@version = version
|
101
28
|
end
|
29
|
+
|
102
30
|
end
|
103
|
-
|
104
|
-
def self.
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
end
|
111
|
-
|
112
|
-
# AttrReaders depends on this.
|
113
|
-
def self.supported_attributes; @supported_attributes ||= []; end
|
114
|
-
|
115
|
-
SECTIONS = [:general, :video, :audio, :image, :menu, :text]
|
116
|
-
NON_GENERAL_SECTIONS = SECTIONS - [:general]
|
117
|
-
|
118
|
-
attr_reader :streams
|
119
|
-
|
120
|
-
# Size of source file as reported by File.size.
|
121
|
-
# Returns nil if you haven't yet fired off the system command.
|
122
|
-
def size; File.size(@full_filename) if @full_filename; end
|
123
|
-
|
124
|
-
class StreamProxy
|
125
|
-
def initialize(mediainfo, stream_type)
|
126
|
-
unless Mediainfo::SECTIONS.include? stream_type
|
127
|
-
raise ArgumentError, "invalid stream_type: #{stream_type.inspect}"
|
128
|
-
end
|
129
|
-
|
130
|
-
@stream_type = stream_type
|
131
|
-
@mediainfo = mediainfo
|
132
|
-
@streams = @mediainfo.streams.select { |x| x.send("#{stream_type}?") }
|
133
|
-
end
|
134
|
-
|
135
|
-
def [](id); @streams[id]; end
|
136
|
-
def count; @streams.size; end
|
137
|
-
attr_reader :streams
|
138
|
-
attr_reader :stream_type
|
139
|
-
|
140
|
-
class SingleStreamAPIError < RuntimeError; end
|
141
|
-
class NoStreamsForProxyError < NoMethodError; end
|
142
|
-
|
143
|
-
def method_missing(m, *a, &b)
|
144
|
-
if streams.size > 1
|
145
|
-
raise SingleStreamAPIError, "You cannot use the single stream, convenience API on a multi-stream file."
|
146
|
-
else
|
147
|
-
if relevant_stream = streams.detect { |s| s.respond_to?(m) }
|
148
|
-
relevant_stream.send(m, *a, &b)
|
149
|
-
else
|
150
|
-
raise NoStreamsForProxyError, "there are no :#{stream_type} streams to send :#{m} to"
|
151
|
-
end
|
152
|
-
end
|
31
|
+
|
32
|
+
def self.xml_parser
|
33
|
+
ENV['MEDIAINFO_XML_PARSER'].nil? || ENV['MEDIAINFO_XML_PARSER'].to_s.strip.empty? ? xml_parser = 'rexml/document' : xml_parser = ENV['MEDIAINFO_XML_PARSER']
|
34
|
+
begin
|
35
|
+
require xml_parser
|
36
|
+
rescue Gem::LoadError => ex
|
37
|
+
raise Gem::LoadError, "Your specified XML parser, #{xml_parser.inspect}, could not be loaded: #{ex.message}"
|
153
38
|
end
|
39
|
+
return xml_parser
|
154
40
|
end
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
41
|
+
|
42
|
+
|
43
|
+
def self.run(input = nil)
|
44
|
+
raise ArgumentError, 'Your input cannot be blank.' if input.nil?
|
45
|
+
command = "#{location} #{input} --Output=XML 2>&1"
|
46
|
+
raw_response = `#{command}`
|
47
|
+
unless $? == 0
|
48
|
+
raise ExecutionError, "Execution of '#{command}' failed. #{raw_response.inspect}"
|
49
|
+
end
|
50
|
+
return raw_response
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.from(input)
|
54
|
+
input_guideline_message = 'Bad Input' + "\n" + "Input must be: \n" +
|
55
|
+
"A video or xml file location. Example: '~/videos/test_video.mov' or '~/videos/test_video.xml' \n" +
|
56
|
+
"A valid URL. Example: 'http://www.site.com/videofile.mov' \n" +
|
57
|
+
"Or MediaInfo XML \n"
|
58
|
+
if input # User must specify file
|
59
|
+
if input.include?('<?xml') # Must be first, else we could parse input (raw xml) with a URL in it and think it's a URL
|
60
|
+
return MediaInfo::Tracks.new(input)
|
61
|
+
elsif input.downcase.include?('http') || input.downcase.include?('www') # Handle Url Parsing
|
62
|
+
@uri = URI(input)
|
63
|
+
# Check if URL is valid
|
64
|
+
http = ::Net::HTTP.new(@uri.host,@uri.port)
|
65
|
+
request = Net::HTTP::Head.new(@uri.request_uri) # Only grab the Headers to be sure we don't try and download the whole file
|
66
|
+
response = http.request(request)
|
67
|
+
case response
|
68
|
+
when Net::HTTPOK
|
69
|
+
@escaped_input = URI.escape(@uri.to_s)
|
166
70
|
else
|
167
|
-
raise "
|
71
|
+
raise RemoteUrlError, "HTTP call to #{input} is not working!"
|
168
72
|
end
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
if Mediainfo.const_defined?(stream_class_name)
|
180
|
-
Mediainfo.const_get(stream_class_name).new(stream_type)
|
73
|
+
elsif input.include?('.xml')
|
74
|
+
return MediaInfo::Tracks.new(::File.open(input).read)
|
75
|
+
elsif input.include?('/')
|
76
|
+
@file = ::File.expand_path input # turns ~/path/to/file into /home/user/path/to/file
|
77
|
+
raise ArgumentError, 'You must include a file location.' if @file.nil?
|
78
|
+
raise ArgumentError, "need a path to a video file, #{@file} does not exist" unless ::File.exist? @file
|
79
|
+
@file_path = ::File.dirname @file
|
80
|
+
@filename = ::File.basename @file
|
81
|
+
@escaped_input = @file.shell_escape_double_quotes
|
82
|
+
# Set variable for returned XML
|
181
83
|
else
|
182
|
-
raise
|
84
|
+
raise ArgumentError, input_guideline_message
|
183
85
|
end
|
86
|
+
return MediaInfo::Tracks.new(MediaInfo.run(@escaped_input))
|
87
|
+
else
|
88
|
+
raise ArgumentError, input_guideline_message
|
184
89
|
end
|
185
|
-
|
186
|
-
def initialize(stream_type)
|
187
|
-
raise ArgumentError, "need a stream_type, received #{stream_type.inspect}" if stream_type.nil?
|
188
|
-
|
189
|
-
@stream_type = stream_type.downcase.to_sym
|
190
|
-
|
191
|
-
# TODO @parsed_response is not the best name anymore, but I'm leaving it
|
192
|
-
# alone to focus on refactoring the interface to the streams
|
193
|
-
# before I refactor the attribute reader implementations.
|
194
|
-
@parsed_response = { @stream_type => {} }
|
195
|
-
end
|
196
|
-
|
197
|
-
attr_reader :parsed_response
|
198
|
-
|
199
|
-
def [](k); @parsed_response[@stream_type][k]; end
|
200
|
-
def []=(k,v); @parsed_response[@stream_type][k] = v; end
|
201
|
-
|
202
|
-
Mediainfo::SECTIONS.each { |t| define_method("#{t}?") { t == @stream_type } }
|
203
90
|
end
|
204
|
-
|
205
|
-
class GeneralStream < Stream
|
206
|
-
mediainfo_attr_reader :codec_id, "Codec ID"
|
207
|
-
|
208
|
-
mediainfo_duration_reader :duration
|
209
|
-
|
210
|
-
mediainfo_attr_reader :format
|
211
|
-
mediainfo_attr_reader :format_profile
|
212
|
-
mediainfo_attr_reader :format_info
|
213
|
-
mediainfo_attr_reader :overall_bit_rate
|
214
|
-
mediainfo_attr_reader :writing_application
|
215
|
-
mediainfo_attr_reader :writing_library
|
216
|
-
|
217
|
-
mediainfo_date_reader :mastered_date
|
218
|
-
mediainfo_date_reader :tagged_date
|
219
|
-
mediainfo_date_reader :encoded_date
|
220
|
-
end
|
221
|
-
|
222
|
-
class VideoStream < Stream
|
223
|
-
mediainfo_attr_reader :stream_id, "ID"
|
224
|
-
|
225
|
-
mediainfo_duration_reader :duration
|
226
|
-
|
227
|
-
mediainfo_attr_reader :stream_size
|
228
|
-
mediainfo_attr_reader :bit_rate
|
229
|
-
mediainfo_attr_reader :nominal_bit_rate
|
230
|
-
|
231
|
-
mediainfo_attr_reader :bit_rate_mode
|
232
|
-
def cbr?; video? and "Constant" == bit_rate_mode; end
|
233
|
-
def vbr?; video? and not cbr?; end
|
234
|
-
|
235
|
-
mediainfo_attr_reader :scan_order
|
236
|
-
mediainfo_attr_reader :scan_type
|
237
|
-
def interlaced?; video? and "Interlaced" == scan_type; end
|
238
|
-
def progressive?; video? and not interlaced? end
|
239
|
-
|
240
|
-
mediainfo_int_reader :resolution
|
241
|
-
|
242
|
-
mediainfo_attr_reader :colorimetry
|
243
|
-
alias_method :colorspace, :colorimetry
|
244
|
-
|
245
|
-
mediainfo_attr_reader :format
|
246
|
-
mediainfo_attr_reader :format_info
|
247
|
-
mediainfo_attr_reader :format_profile
|
248
|
-
mediainfo_attr_reader :format_version
|
249
|
-
mediainfo_attr_reader :format_settings_cabac, "Format settings, CABAC"
|
250
|
-
mediainfo_attr_reader :format_settings_reframes, "Format settings, ReFrames"
|
251
|
-
mediainfo_attr_reader :format_settings_matrix, "Format settings, Matrix"
|
252
|
-
# Format settings, BVOP : Yes
|
253
|
-
# Format settings, QPel : No
|
254
|
-
# Format settings, GMC : No warppoints
|
255
|
-
# mediainfo_attr_reader :format_settings_qpel, "Format settings, QPel"
|
256
|
-
mediainfo_attr_reader :color_primaries
|
257
|
-
mediainfo_attr_reader :transfer_characteristics
|
258
|
-
mediainfo_attr_reader :matrix_coefficients
|
259
91
|
|
260
|
-
mediainfo_attr_reader :codec_id, "Codec ID"
|
261
|
-
mediainfo_attr_reader :codec_info, "Codec ID/Info"
|
262
|
-
alias_method :codec_id_info, :codec_info
|
263
92
|
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
mediainfo_attr_reader :maximum_frame_rate
|
273
|
-
def max_fps; maximum_frame_rate[/[\d.]+/].to_f if video?; end
|
274
|
-
alias_method :max_framerate, :max_fps
|
275
|
-
|
276
|
-
mediainfo_attr_reader :frame_rate_mode
|
277
|
-
|
278
|
-
mediainfo_attr_reader :display_aspect_ratio
|
279
|
-
# alias_method :display_aspect_ratio, :display_aspect_ratio
|
280
|
-
|
281
|
-
mediainfo_attr_reader :bits_pixel_frame, "Bits/(Pixel*Frame)"
|
282
|
-
|
283
|
-
mediainfo_int_reader :width
|
284
|
-
mediainfo_int_reader :height
|
285
|
-
|
286
|
-
def frame_size; "#{width}x#{height}" if width or height; end
|
287
|
-
|
288
|
-
mediainfo_date_reader :encoded_date
|
289
|
-
mediainfo_date_reader :tagged_date
|
290
|
-
|
291
|
-
mediainfo_attr_reader :standard
|
292
|
-
end
|
293
|
-
|
294
|
-
class AudioStream < Stream
|
295
|
-
mediainfo_attr_reader :stream_id, "ID"
|
296
|
-
|
297
|
-
mediainfo_duration_reader :duration
|
298
|
-
|
299
|
-
mediainfo_attr_reader :sampling_rate
|
300
|
-
def sample_rate
|
301
|
-
return unless rate = sampling_rate_before_type_cast
|
302
|
-
number = rate.gsub(/[^\d.]+/, "").to_f
|
303
|
-
number = case rate
|
304
|
-
when /KHz/ then number * 1000
|
305
|
-
when /Hz/ then number
|
306
|
-
else
|
307
|
-
raise "unhandled sample rate! please report bug!"
|
308
|
-
end
|
309
|
-
number.to_i
|
93
|
+
def self.set_singleton_method(object,name,parameters)
|
94
|
+
# Handle parameters with invalid characters (instance_variable_set throws error)
|
95
|
+
name.gsub!('.','_') if name.include?('.') ## period in name
|
96
|
+
name.downcase!
|
97
|
+
# Create singleton_method
|
98
|
+
object.instance_variable_set("@#{name}",parameters)
|
99
|
+
object.define_singleton_method name do
|
100
|
+
object.instance_variable_get "@#{name}"
|
310
101
|
end
|
311
|
-
alias_method :sampling_rate, :sample_rate
|
312
|
-
|
313
|
-
mediainfo_attr_reader :stream_size
|
314
|
-
mediainfo_attr_reader :bit_rate
|
315
|
-
mediainfo_attr_reader :bit_rate_mode
|
316
|
-
mediainfo_attr_reader :interleave_duration, "Interleave, duration"
|
317
|
-
|
318
|
-
mediainfo_int_reader :resolution
|
319
|
-
alias_method :sample_bit_depth, :resolution
|
320
|
-
|
321
|
-
mediainfo_attr_reader :format
|
322
|
-
mediainfo_attr_reader :format_profile
|
323
|
-
mediainfo_attr_reader :format_version
|
324
|
-
mediainfo_attr_reader :format_info, "Format/Info"
|
325
|
-
mediainfo_attr_reader :format_settings_sbr, "Format settings, SBR"
|
326
|
-
mediainfo_attr_reader :format_settings_endianness, "Format settings, Endianness"
|
327
|
-
mediainfo_attr_reader :format_settings_sign, "Format settings, Sign"
|
328
|
-
mediainfo_attr_reader :codec_id, "Codec ID"
|
329
|
-
mediainfo_attr_reader :codec_info, "Codec ID/Info"
|
330
|
-
mediainfo_attr_reader :codec_id_hint
|
331
|
-
mediainfo_attr_reader :channel_positions
|
332
|
-
|
333
|
-
mediainfo_int_reader :channels, "Channel(s)"
|
334
|
-
def stereo?; 2 == channels; end
|
335
|
-
def mono?; 1 == channels; end
|
336
|
-
|
337
|
-
mediainfo_date_reader :encoded_date
|
338
|
-
mediainfo_date_reader :tagged_date
|
339
|
-
end
|
340
|
-
|
341
|
-
class ImageStream < Stream
|
342
|
-
mediainfo_attr_reader :resolution
|
343
|
-
mediainfo_attr_reader :format
|
344
|
-
|
345
|
-
mediainfo_int_reader :width
|
346
|
-
mediainfo_int_reader :height
|
347
|
-
|
348
|
-
def frame_size; "#{width}x#{height}" if width or height; end
|
349
102
|
end
|
350
103
|
|
351
|
-
|
352
|
-
mediainfo_attr_reader :stream_id, "ID"
|
353
|
-
mediainfo_attr_reader :format
|
354
|
-
mediainfo_attr_reader :codec_id, "Codec ID"
|
355
|
-
mediainfo_attr_reader :codec_info, "Codec ID/Info"
|
356
|
-
end
|
357
|
-
|
358
|
-
class MenuStream < Stream
|
359
|
-
mediainfo_attr_reader :stream_id, "ID"
|
360
|
-
mediainfo_date_reader :encoded_date
|
361
|
-
mediainfo_date_reader :tagged_date
|
362
|
-
mediainfo_int_reader :delay
|
363
|
-
end
|
364
|
-
|
365
|
-
Mediainfo::SECTIONS.each do |stream_type|
|
366
|
-
class_eval %{
|
367
|
-
def #{stream_type}; @#{stream_type}_proxy ||= StreamProxy.new(self, :#{stream_type}); end
|
368
|
-
def #{stream_type}?; streams.any? { |x| x.#{stream_type}? }; end
|
369
|
-
}, __FILE__, __LINE__
|
370
|
-
end
|
371
|
-
|
372
|
-
###
|
373
|
-
|
374
|
-
attr_reader :raw_response, :full_filename, :filename, :path, :escaped_full_filename
|
375
|
-
|
376
|
-
###
|
377
|
-
|
378
|
-
def initialize(full_filename = nil)
|
379
|
-
unless mediainfo_version
|
380
|
-
raise UnknownVersionError,
|
381
|
-
"Unable to determine mediainfo version. " +
|
382
|
-
"We tried: #{self.class.version_command} " +
|
383
|
-
"Are you sure mediainfo is installed at #{self.class.path.inspect}? " +
|
384
|
-
"Set Mediainfo.path = /where/is/mediainfo if it is not in your PATH."
|
385
|
-
end
|
386
|
-
|
387
|
-
if mediainfo_version < "0.7.25"
|
388
|
-
raise IncompatibleVersionError,
|
389
|
-
"Your version of mediainfo, #{mediainfo_version}, " +
|
390
|
-
"is not compatible with this gem. >= 0.7.25 required."
|
391
|
-
end
|
392
|
-
|
393
|
-
@streams = []
|
394
|
-
|
395
|
-
if full_filename
|
396
|
-
@full_filename = File.expand_path full_filename
|
397
|
-
@path = File.dirname @full_filename
|
398
|
-
@filename = File.basename @full_filename
|
399
|
-
|
400
|
-
raise ArgumentError, "need a path to a video file, got nil" unless @full_filename
|
401
|
-
raise ArgumentError, "need a path to a video file, #{@full_filename} does not exist" unless File.exist? @full_filename
|
402
|
-
|
403
|
-
@escaped_full_filename = @full_filename.shell_escape_double_quotes
|
404
|
-
|
405
|
-
self.raw_response = mediainfo!
|
406
|
-
end
|
407
|
-
end
|
408
|
-
|
409
|
-
def raw_response=(response)
|
410
|
-
raise ArgumentError, "raw response is nil" if response.nil?
|
411
|
-
@raw_response = response
|
412
|
-
parse!
|
413
|
-
@raw_response
|
414
|
-
end
|
415
|
-
|
416
|
-
class << self
|
417
|
-
attr_accessor :path
|
418
|
-
|
419
|
-
def load_xml_parser!(parser = xml_parser)
|
420
|
-
begin
|
421
|
-
gem parser
|
422
|
-
require parser
|
423
|
-
rescue Gem::LoadError => e
|
424
|
-
raise Gem::LoadError,
|
425
|
-
"your specified XML parser, #{parser.inspect}, could not be loaded: #{e}"
|
426
|
-
end
|
427
|
-
end
|
428
|
-
|
429
|
-
attr_reader :xml_parser
|
430
|
-
def xml_parser=(parser)
|
431
|
-
load_xml_parser! parser
|
432
|
-
@xml_parser = parser
|
433
|
-
end
|
434
|
-
end
|
435
|
-
|
436
|
-
unless ENV["MEDIAINFO_XML_PARSER"].to_s.strip.empty?
|
437
|
-
self.xml_parser = ENV["MEDIAINFO_XML_PARSER"]
|
438
|
-
end
|
439
|
-
|
440
|
-
def path; self.class.path; end
|
441
|
-
def xml_parser; self.class.xml_parser; end
|
442
|
-
|
443
|
-
def self.default_mediainfo_path!; self.path = "mediainfo"; end
|
444
|
-
default_mediainfo_path! unless path
|
445
|
-
|
446
|
-
def mediainfo_version; self.class.version; end
|
447
|
-
|
448
|
-
attr_reader :last_command
|
449
|
-
|
450
|
-
def inspect
|
451
|
-
super.sub(/@raw_response=".+?", @/, %{@raw_response="...", @})
|
452
|
-
end
|
453
|
-
|
454
|
-
private
|
455
|
-
def mediainfo!
|
456
|
-
@last_command = "#{path} #{@escaped_full_filename} --Output=XML"
|
457
|
-
run_command!
|
458
|
-
end
|
459
|
-
|
460
|
-
def run_command!
|
461
|
-
raw_response = `#{@last_command} 2>&1`
|
462
|
-
unless $? == 0
|
463
|
-
raise ExecutionError,
|
464
|
-
"Execution of `#{@last_command}` failed: #{raw_response.inspect}"
|
465
|
-
end
|
466
|
-
raw_response
|
467
|
-
end
|
468
|
-
|
469
|
-
def parse!
|
470
|
-
if xml_parser
|
471
|
-
self.class.load_xml_parser!
|
472
|
-
else
|
473
|
-
require "rexml/document"
|
474
|
-
end
|
475
|
-
|
476
|
-
case xml_parser
|
477
|
-
when "nokogiri"
|
478
|
-
Nokogiri::XML(@raw_response).xpath("//track").each { |t|
|
479
|
-
s = Stream.create(t['type'])
|
480
|
-
t.xpath("*").each do |c|
|
481
|
-
s[key_for(c)] = c.content.strip
|
482
|
-
end
|
483
|
-
@streams << s
|
484
|
-
}
|
485
|
-
when "hpricot"
|
486
|
-
Hpricot::XML(@raw_response).search("track").each { |t|
|
487
|
-
s = Stream.create(t['type'])
|
488
|
-
t.children.select { |n| n.is_a? Hpricot::Elem }.each do |c|
|
489
|
-
s[key_for(c)] = c.inner_html.strip
|
490
|
-
end
|
491
|
-
@streams << s
|
492
|
-
}
|
493
|
-
else
|
494
|
-
REXML::Document.new(@raw_response).elements.each("/Mediainfo/File/track") { |t|
|
495
|
-
s = Stream.create(t.attributes['type'])
|
496
|
-
t.children.select { |n| n.is_a? REXML::Element }.each do |c|
|
497
|
-
s[key_for(c)] = c.text.strip
|
498
|
-
end
|
499
|
-
@streams << s
|
500
|
-
}
|
501
|
-
end
|
502
|
-
|
503
|
-
SECTIONS.each do |section|
|
504
|
-
default_target_stream = if send("#{section}?")
|
505
|
-
send(section).streams.first
|
506
|
-
else
|
507
|
-
Mediainfo.const_get("#{section.to_s.capitalize}Stream").new(section.to_s.capitalize)
|
508
|
-
end
|
509
|
-
instance_variable_set "@#{section}_stream", default_target_stream
|
510
|
-
end
|
511
|
-
end
|
512
|
-
|
513
|
-
def key_for(attribute_node)
|
514
|
-
attribute_node.name.downcase.gsub(/_+/, "_").gsub(/_s(\W|$)/, "s").strip
|
515
|
-
end
|
516
|
-
end
|
104
|
+
end # end Module
|