media_processing_tool 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/README.md +2 -0
- data/bin/catalog +181 -0
- data/bin/catalog_assets +187 -0
- data/bin/fcp_xml_parser +41 -0
- data/bin/mig +44 -0
- data/bin/mig_http +52 -0
- data/bin/xml_processor +51 -0
- data/config/default/xml_processor_config +49 -0
- data/lib/axml.rb +59 -0
- data/lib/cli.rb +88 -0
- data/lib/final_cut_pro.rb +31 -0
- data/lib/final_cut_pro/sequence_processor.rb +135 -0
- data/lib/final_cut_pro/xml_parser.rb +15 -0
- data/lib/final_cut_pro/xml_parser/common.rb +121 -0
- data/lib/final_cut_pro/xml_parser/document.rb +18 -0
- data/lib/final_cut_pro/xml_parser/fcpxml/version_1.rb +28 -0
- data/lib/final_cut_pro/xml_parser/xmeml/version_5.rb +234 -0
- data/lib/itunes/xml_parser.rb +51 -0
- data/lib/media_processing_tool/publisher.rb +52 -0
- data/lib/media_processing_tool/xml_parser.rb +30 -0
- data/lib/media_processing_tool/xml_parser/document.rb +38 -0
- data/lib/media_processing_tool/xml_parser/identifier.rb +43 -0
- data/lib/media_processing_tool/xml_processor.rb +132 -0
- data/lib/mig.rb +158 -0
- data/lib/mig/http.rb +54 -0
- data/lib/mig/modules/common.rb +333 -0
- data/lib/mig/modules/exiftool.rb +26 -0
- data/lib/mig/modules/ffmpeg.rb +225 -0
- data/lib/mig/modules/media_type.rb +23 -0
- data/lib/mig/modules/mediainfo.rb +91 -0
- data/lib/timecode_methods.rb +108 -0
- data/lib/udam_utils/publish_map_processor.rb +710 -0
- metadata +111 -0
data/lib/mig/http.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'sinatra/base'
|
3
|
+
|
4
|
+
class MediaInformationGatherer
|
5
|
+
|
6
|
+
class HTTP < Sinatra::Base
|
7
|
+
enable :logging
|
8
|
+
disable :protection
|
9
|
+
|
10
|
+
# Will try to convert a body to parameters and merge them into the params hash
|
11
|
+
# Params will override the body parameters
|
12
|
+
#
|
13
|
+
# @params [Hash] _params (params) The parameters parsed from the query and form fields
|
14
|
+
def merge_params_from_body(_params = params)
|
15
|
+
_params = _params.dup
|
16
|
+
if request.media_type == 'application/json'
|
17
|
+
request.body.rewind
|
18
|
+
body_contents = request.body.read
|
19
|
+
logger.debug { "Parsing: '#{body_contents}'" }
|
20
|
+
if body_contents
|
21
|
+
json_params = JSON.parse(body_contents)
|
22
|
+
if json_params.is_a?(Hash)
|
23
|
+
_params = json_params.merge(_params)
|
24
|
+
else
|
25
|
+
_params['body'] = json_params
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
_params
|
30
|
+
end # merge_params_from_body
|
31
|
+
|
32
|
+
|
33
|
+
post '/' do
|
34
|
+
logger.level = Logger::DEBUG
|
35
|
+
_params = merge_params_from_body
|
36
|
+
logger.debug { "Params: #{_params}" }
|
37
|
+
#return params
|
38
|
+
|
39
|
+
response = { }
|
40
|
+
file_paths = _params['file_paths']
|
41
|
+
[*file_paths].each do |file_path|
|
42
|
+
begin
|
43
|
+
response[file_path] = settings.mig.run(file_path)
|
44
|
+
rescue => e
|
45
|
+
response[file_path] = {:exception => {:message => e.message, :backtrace => e.backtrace}}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
content_type :json
|
49
|
+
JSON.generate(response)
|
50
|
+
end # post '/'
|
51
|
+
|
52
|
+
end # HTTP
|
53
|
+
|
54
|
+
end # MediaInformationGatherer
|
@@ -0,0 +1,333 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
class MediaInformationGatherer
|
4
|
+
|
5
|
+
class Common
|
6
|
+
|
7
|
+
STANDARD_VIDEO_FRAME_RATES = [ 23.97, 23.976, 24.0, 24.97, 24.975, 25.0, 29.97, 30.0, 50.0, 59.94, 60.0 ]
|
8
|
+
|
9
|
+
def self.common_variables(metadata_sources)
|
10
|
+
new.common_variables(metadata_sources)
|
11
|
+
end
|
12
|
+
|
13
|
+
def metadata_sources; @metadata_sources || { } end
|
14
|
+
def ffmpeg; @ffmpeg ||= metadata_sources[:ffmpeg] || { } end
|
15
|
+
def mediainfo; @mediainfo ||= metadata_sources[:mediainfo] || { 'section_type_count' => { 'audio' => 0 } } end
|
16
|
+
def stat; @stat ||= metadata_sources[:stat] || { } end
|
17
|
+
def cv; @cv ||= { } end
|
18
|
+
|
19
|
+
def common_variables(_metadata_sources)
|
20
|
+
@metadata_sources = _metadata_sources.dup
|
21
|
+
@cv = { }
|
22
|
+
|
23
|
+
file_path = ffmpeg['path']
|
24
|
+
source_directory = file_path ? File.dirname(File.expand_path(file_path)) : ''
|
25
|
+
creation_date_time = Time.parse(ffmpeg['creation_time']).strftime('%B %d, %Y %r') rescue ffmpeg['creation_time']
|
26
|
+
|
27
|
+
cv[:file_path] = file_path
|
28
|
+
cv[:source_directory] = source_directory
|
29
|
+
cv[:creation_date_time] = creation_date_time
|
30
|
+
cv[:ctime] = stat[:ctime]
|
31
|
+
cv[:mtime] = stat[:mtime]
|
32
|
+
cv[:bytes] = stat[:size]
|
33
|
+
cv[:size] = (mediainfo['General'] || { })['File size']
|
34
|
+
cv[:uid] = stat[:uid]
|
35
|
+
cv[:gid] = stat[:gid]
|
36
|
+
cv[:ftype] = stat[:ftype]
|
37
|
+
|
38
|
+
type = :video # Need to figure out where to determine the type from
|
39
|
+
case type #.to_s.downcase.to_sym
|
40
|
+
when :video
|
41
|
+
common_audio_variables
|
42
|
+
common_video_variables
|
43
|
+
when :audio
|
44
|
+
common_audio_variables
|
45
|
+
when :image
|
46
|
+
common_image_variables
|
47
|
+
else
|
48
|
+
# What else is there?
|
49
|
+
end
|
50
|
+
if RUBY_VERSION.start_with?('1.8.')
|
51
|
+
Hash[cv.map { |a| [ a[0].to_s, a[1] ] }.sort.map { |a| [ a[0].to_sym, a[1] ] }]
|
52
|
+
else
|
53
|
+
Hash[cv.sort]
|
54
|
+
end
|
55
|
+
end # common_variables
|
56
|
+
|
57
|
+
def common_audio_variables
|
58
|
+
mi_audio = mediainfo['Audio'] || { }
|
59
|
+
mi_audio = { } unless mi_audio.is_a?(Hash)
|
60
|
+
|
61
|
+
duration = ffmpeg['duration']
|
62
|
+
if duration
|
63
|
+
dl = duration
|
64
|
+
dlh = dl / 3600
|
65
|
+
dl %= 3600
|
66
|
+
dlm = dl / 60
|
67
|
+
dl %= 60
|
68
|
+
duration_long = sprintf('%02d:%02d:%02d', dlh, dlm, dl)
|
69
|
+
else
|
70
|
+
duration_long = '00:00:00'
|
71
|
+
end
|
72
|
+
section_type_counts = mediainfo['section_type_counts'] || { }
|
73
|
+
audio_track_count = section_type_counts['audio']
|
74
|
+
|
75
|
+
cv[:audio_codec_id] = mi_audio['Codec ID']
|
76
|
+
cv[:audio_sample_rate] = ffmpeg['audio_sample_rate']
|
77
|
+
cv[:duration] = duration
|
78
|
+
cv[:duration_long] = duration_long
|
79
|
+
cv[:number_of_audio_tracks] = audio_track_count # Determine the number of audio channels
|
80
|
+
cv[:number_of_audio_channels] = ffmpeg['audio_channel_count']
|
81
|
+
|
82
|
+
end # common_audio_variables
|
83
|
+
|
84
|
+
# @return [Fixed]
|
85
|
+
def aspect_from_dimensions(height, width)
|
86
|
+
aspect = width.to_f / height.to_f
|
87
|
+
aspect.nan? ? nil : aspect
|
88
|
+
end
|
89
|
+
|
90
|
+
# Determines if the aspect from dimensions is widescreen (>= 1.5 (3/2)
|
91
|
+
# 1.55 is derived from the following tables
|
92
|
+
# {http://en.wikipedia.org/wiki/Storage_Aspect_Ratio#Previous_and_currently_used_aspect_ratios Aspect Ratios}
|
93
|
+
# {http://en.wikipedia.org/wiki/List_of_common_resolutions#Television}
|
94
|
+
#
|
95
|
+
# 1.55:1 (14:9): Widescreen aspect ratio sometimes used in shooting commercials etc. as a compromise format
|
96
|
+
# between 4:3 (12:9) and 16:9. When converted to a 16:9 frame, there is slight pillarboxing, while conversion to
|
97
|
+
# 4:3 creates slight letterboxing. All widescreen content on ABC Family's SD feed is presented in this ratio.
|
98
|
+
#
|
99
|
+
# @return [Boolean]
|
100
|
+
def is_widescreen?(height, width)
|
101
|
+
_aspect_from_dimensions = aspect_from_dimensions(height, width)
|
102
|
+
(_aspect_from_dimensions ? (_aspect_from_dimensions >= 1.55) : false)
|
103
|
+
end
|
104
|
+
|
105
|
+
# (@link http://en.wikipedia.osrg/wiki/List_of_common_resolution)
|
106
|
+
#
|
107
|
+
# Lowest Width High Resolution Format Found:
|
108
|
+
# Panasonic DVCPRO100 for 50/60Hz over 720p - SMPTE Resolution = 960x720
|
109
|
+
#
|
110
|
+
# @return [Boolean]
|
111
|
+
def is_high_definition?(height, width)
|
112
|
+
(width.respond_to?(:to_i) and height.respond_to?(:to_i)) ? (width.to_i >= 950 and height.to_i >= 700) : false
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
def common_image_variables
|
117
|
+
|
118
|
+
end # common_image_variables
|
119
|
+
|
120
|
+
def common_video_variables
|
121
|
+
mi_video = mediainfo['Video'] || { }
|
122
|
+
#return unless ffmpeg['video_stream'] or !mi_video.empty?
|
123
|
+
|
124
|
+
frame_rate = ffmpeg['frame_rate']
|
125
|
+
frame_rate ||= mi_video['Frame rate'].respond_to?(:to_f) ? mi_video['Frame rate'].to_f : mi_video['Frame rate']
|
126
|
+
|
127
|
+
height = ffmpeg['height'] || mi_video['Height']
|
128
|
+
width = ffmpeg['width'] || mi_video['Width']
|
129
|
+
|
130
|
+
is_widescreen = ffmpeg['is_widescreen']
|
131
|
+
is_widescreen ||= is_widescreen?(height, width) if is_widescreen.nil?
|
132
|
+
|
133
|
+
is_high_definition = is_high_definition?(height, width)
|
134
|
+
|
135
|
+
calculated_aspect_ratio = ffmpeg['calculated_aspect_ratio'] || (height.respond_to?(:to_f) and width.respond_to?(:to_f) ? (width.to_f / height.to_f) : nil)
|
136
|
+
|
137
|
+
video_codec_id = mi_video['Codec ID']
|
138
|
+
video_codec_description = video_codec_descriptions.fetch(video_codec_id, 'Unknown')
|
139
|
+
|
140
|
+
video_system = determine_video_system(height, width, frame_rate)
|
141
|
+
|
142
|
+
#aspect_ratio = ffmpeg['video_stream'] ? (ffmpeg['is_widescreen'] ? '16:9' : '4:3') : nil
|
143
|
+
if ffmpeg['video_stream']
|
144
|
+
aspect_ratio = ffmpeg['is_widescreen'] ? '16:9' : '4:3'
|
145
|
+
else
|
146
|
+
aspect_ratio = nil
|
147
|
+
end
|
148
|
+
|
149
|
+
cv[:aspect_ratio] = aspect_ratio
|
150
|
+
cv[:bit_depth] = mi_video['Bit depth']
|
151
|
+
cv[:calculated_aspect_ratio] = calculated_aspect_ratio
|
152
|
+
cv[:chroma_subsampling] = mi_video['Chroma subsampling']
|
153
|
+
cv[:display_aspect_ratio] = ffmpeg['display_aspect_ratio']
|
154
|
+
cv[:frames_per_second] = frame_rate # Video frames per second
|
155
|
+
cv[:height] = height
|
156
|
+
cv[:is_high_definition] = is_high_definition # Determine if video is Standard Def or High Definition
|
157
|
+
cv[:is_widescreen] = is_widescreen
|
158
|
+
cv[:pixel_aspect_ratio] = ffmpeg['pixel_aspect_ratio']
|
159
|
+
cv[:resolution] = ffmpeg['resolution']
|
160
|
+
cv[:scan_order] = mi_video['Scan order']
|
161
|
+
cv[:scan_type] = mi_video['Scan type']
|
162
|
+
cv[:storage_aspect_ratio] = ffmpeg['storage_aspect_ratio']
|
163
|
+
cv[:timecode] = ffmpeg['timecode']
|
164
|
+
cv[:video_codec_id] = video_codec_id
|
165
|
+
cv[:video_codec_commercial_name] = mi_video['Commercial name']
|
166
|
+
cv[:video_codec_description] = video_codec_description
|
167
|
+
cv[:video_system] = video_system
|
168
|
+
cv[:width] = width
|
169
|
+
cv
|
170
|
+
end # common_video_variables
|
171
|
+
|
172
|
+
# A hash of fourcc codes
|
173
|
+
# http://www.videolan.org/developers/vlc/src/misc/fourcc.c
|
174
|
+
def fourcc_codes
|
175
|
+
@fourcc_codes ||= {
|
176
|
+
'2vuy' => 'Apple FCP Uncompressed 8-bit 4:2:2',
|
177
|
+
'v210' => 'Apple FCP Uncompressed 10-bit 4:2:2',
|
178
|
+
'apcn' => 'Apple ProRes Standard',
|
179
|
+
'apch' => 'Apple ProRes High Quality (HQ)',
|
180
|
+
'apcs' => 'Apple ProRes LT',
|
181
|
+
'apco' => 'Apple ProRes Proxy',
|
182
|
+
'ap4c' => 'Apple ProRes 4444',
|
183
|
+
'ap4h' => 'Apple ProRes 4444',
|
184
|
+
'xdv1' => 'XDCAM HD 720p30 35Mb/s',
|
185
|
+
'xdv2' => 'XDCAM HD 1080i60 35Mb/s',
|
186
|
+
'xdv3' => 'XDCAM HD 1080i50 35Mb/s',
|
187
|
+
'xdv4' => 'XDCAM HD 720p24 35Mb/s',
|
188
|
+
'xdv5' => 'XDCAM HD 720p25 35Mb/s',
|
189
|
+
'xdv6' => 'XDCAM HD 1080p24 35Mb/s',
|
190
|
+
'xdv7' => 'XDCAM HD 1080p25 35Mb/s',
|
191
|
+
'xdv8' => 'XDCAM HD 1080p30 35Mb/s',
|
192
|
+
'xdv9' => 'XDCAM HD 720p60 35Mb/s',
|
193
|
+
'xdva' => 'XDCAM HD 720p50 35Mb/s',
|
194
|
+
'xdhd' => 'XDCAM HD 540p',
|
195
|
+
'xdh2' => 'XDCAM HD422 540p',
|
196
|
+
'xdvb' => 'XDCAM EX 1080i60 50Mb/s CBR',
|
197
|
+
'xdvc' => 'XDCAM EX 1080i50 50Mb/s CBR',
|
198
|
+
'xdvd' => 'XDCAM EX 1080p24 50Mb/s CBR',
|
199
|
+
'xdve' => 'XDCAM EX 1080p25 50Mb/s CBR',
|
200
|
+
'xdvf' => 'XDCAM EX 1080p30 50Mb/s CBR',
|
201
|
+
'xd54' => 'XDCAM HD422 720p24 50Mb/s CBR',
|
202
|
+
'xd55' => 'XDCAM HD422 720p25 50Mb/s CBR',
|
203
|
+
'xd59' => 'XDCAM HD422 720p60 50Mb/s CBR',
|
204
|
+
'xd5a' => 'XDCAM HD422 720p50 50Mb/s CBR',
|
205
|
+
'xd5b' => 'XDCAM HD422 1080i60 50Mb/s CBR',
|
206
|
+
'xd5c' => 'XDCAM HD422 1080i50 50Mb/s CBR',
|
207
|
+
'xd5d' => 'XDCAM HD422 1080p24 50Mb/s CBR',
|
208
|
+
'xd5e' => 'XDCAM HD422 1080p25 50Mb/s CBR',
|
209
|
+
'xd5f' => 'XDCAM HD422 1080p30 50Mb/s CBR',
|
210
|
+
'dvh2' => 'DV Video 720p24',
|
211
|
+
'dvh3' => 'DV Video 720p25',
|
212
|
+
'dvh4' => 'DV Video 720p30',
|
213
|
+
'dvcp' => 'DV Video PAL',
|
214
|
+
'dvc' => 'DV Video NTSC',
|
215
|
+
'dvp' => 'DV Video Pro',
|
216
|
+
'dvpp' => 'DV Video Pro PAL',
|
217
|
+
'dv50' => 'DV Video C Pro 50',
|
218
|
+
'dv5p' => 'DV Video C Pro 50 PAL',
|
219
|
+
'dv5n' => 'DV Video C Pro 50 NTSC',
|
220
|
+
'dv1p' => 'DV Video C Pro 100 PAL',
|
221
|
+
'dv1n' => 'DV Video C Pro 100 NTSC',
|
222
|
+
'dvhp' => 'DV Video C Pro HD 720p',
|
223
|
+
'dvh5' => 'DV Video C Pro HD 1080i50',
|
224
|
+
'dvh6' => 'DV Video C Pro HD 1080i60',
|
225
|
+
'AVdv' => 'AVID DV',
|
226
|
+
'AVd1' => 'MPEG2 I',
|
227
|
+
'mx5n' => 'MPEG2 IMX NTSC 625/60 50Mb/s (FCP)',
|
228
|
+
'mx5p' => 'MPEG2 IMX PAL 525/50 50Mb/s (FCP',
|
229
|
+
'mx4n' => 'MPEG2 IMX NTSC 625/60 40Mb/s (FCP)',
|
230
|
+
'mx4p' => 'MPEG2 IMX PAL 525/50 40Mb/s (FCP',
|
231
|
+
'mx3n' => 'MPEG2 IMX NTSC 625/60 30Mb/s (FCP)',
|
232
|
+
'mx3p' => 'MPEG2 IMX PAL 525/50 30Mb/s (FCP)',
|
233
|
+
'hdv1' => 'HDV 720p30',
|
234
|
+
'hdv2' => 'HDV 1080i60',
|
235
|
+
'hdv3' => 'HDV 1080i50',
|
236
|
+
'hdv4' => 'HDV 720p24',
|
237
|
+
'hdv5' => 'HDV 720p25',
|
238
|
+
'hdv6' => 'HDV 1080p24',
|
239
|
+
'hdv7' => 'HDV 1080p25',
|
240
|
+
'hdv8' => 'HDV 1080p30',
|
241
|
+
'hdv9' => 'HDV 720p60',
|
242
|
+
'hdva' => 'HDV 720p50',
|
243
|
+
'avc1' => 'AVC-Intra',
|
244
|
+
'ai5p' => 'AVC-Intra 50M 720p25/50',
|
245
|
+
'ai5q' => 'AVC-Intra 50M 1080p25/50',
|
246
|
+
'ai52' => 'AVC-Intra 50M 1080p24/30',
|
247
|
+
'ai53' => 'AVC-Intra 50M 1080i50',
|
248
|
+
'ai55' => 'AVC-Intra 50M 1080i60',
|
249
|
+
'ai56' => 'AVC-Intra 100M 720p24/30',
|
250
|
+
'ai1p' => 'AVC-Intra 100M 720p25/50',
|
251
|
+
'ai1q' => 'AVC-Intra 100M 1080p25/50',
|
252
|
+
'ai12' => 'AVC-Intra 100M 1080p24/30',
|
253
|
+
'ai13' => 'AVC-Intra 100M 1080i50',
|
254
|
+
'ai15' => 'AVC-Intra 100M 1080i60',
|
255
|
+
'ai16' => 'AVC-Intra 100M 1080i60',
|
256
|
+
'mpgv' => 'MPEG-2',
|
257
|
+
'mp1v' => 'MPEG-2',
|
258
|
+
'mpeg' => 'MPEG-2',
|
259
|
+
'mpg1' => 'MPEG-2',
|
260
|
+
'mp2v' => 'MPEG-2',
|
261
|
+
'MPEG' => 'MPEG-2',
|
262
|
+
'mpg2' => 'MPEG-2',
|
263
|
+
'MPG2' => 'MPEG-2',
|
264
|
+
'H262' => 'MPEG-2',
|
265
|
+
'mjpg' => 'Motion JPEG',
|
266
|
+
'mJPG' => 'Motion JPEG',
|
267
|
+
'mjpa' => 'Motion JPEG',
|
268
|
+
'JPEG' => 'Motion JPEG',
|
269
|
+
'AVRn' => 'Avid Motion JPEG',
|
270
|
+
'AVRn' => 'Avid Motion JPEG',
|
271
|
+
'AVDJ' => 'Avid Motion JPEG',
|
272
|
+
'ADJV' => 'Avid Motion JPEG',
|
273
|
+
'wvc1' => 'Microsoft VC-1',
|
274
|
+
'vc-1' => 'Microsoft VC-1',
|
275
|
+
'VC-1' => 'Microsoft VC-1',
|
276
|
+
'jpeg' => 'Photo JPEG',
|
277
|
+
}
|
278
|
+
end # fourcc_codes
|
279
|
+
|
280
|
+
def video_codec_descriptions
|
281
|
+
@video_codec_descriptions ||= fourcc_codes.merge({
|
282
|
+
27 => 'MPEG-TS',
|
283
|
+
})
|
284
|
+
end
|
285
|
+
|
286
|
+
def determine_video_system(height, width, frame_rate)
|
287
|
+
# http://en.wikipedia.org/wiki/Broadcast_television_system
|
288
|
+
# http://en.wikipedia.org/wiki/Standard-definition_television#Resolution
|
289
|
+
# http://en.wikipedia.org/wiki/Pixel_aspect_ratio
|
290
|
+
# http://www.bambooav.com/ntsc-and-pal-video-standards.html
|
291
|
+
# Programmer's Guide to Video Systems - http://lurkertech.com/lg/video-systems/#fields
|
292
|
+
|
293
|
+
# PAL = 25fps Standard: 768x576 Widescreen: 1024x576
|
294
|
+
# NTSC = 29.97fps Standard: 720x540 Widescreen: 854x480
|
295
|
+
video_system = 'unknown'
|
296
|
+
|
297
|
+
return video_system unless height and width and frame_rate
|
298
|
+
|
299
|
+
frame_rate = frame_rate.to_f
|
300
|
+
return video_system unless STANDARD_VIDEO_FRAME_RATES.include?(frame_rate)
|
301
|
+
|
302
|
+
height = height.to_i
|
303
|
+
width = width.to_i
|
304
|
+
|
305
|
+
# The following case statement is based off of - http://images.apple.com/finalcutpro/docs/Apple_ProRes_White_Paper_October_2012.pdf
|
306
|
+
case height
|
307
|
+
when 480, 486, 512
|
308
|
+
case width
|
309
|
+
when 720, 848, 854
|
310
|
+
video_system = 'NTSC'
|
311
|
+
end
|
312
|
+
when 576, 608
|
313
|
+
case width
|
314
|
+
when 720
|
315
|
+
video_system = 'PAL'
|
316
|
+
end
|
317
|
+
when 720
|
318
|
+
case width
|
319
|
+
when 960, 1280
|
320
|
+
video_system = 'HD'
|
321
|
+
end
|
322
|
+
when 1080
|
323
|
+
case width
|
324
|
+
when 1280, 1440, 1920
|
325
|
+
video_system = 'HD'
|
326
|
+
end
|
327
|
+
end # case height
|
328
|
+
video_system
|
329
|
+
end # determine_video_system
|
330
|
+
|
331
|
+
end # Common
|
332
|
+
|
333
|
+
end # MediaInformationGatherer
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'shellwords'
|
3
|
+
class MediaInformationGatherer
|
4
|
+
|
5
|
+
class ExifTool
|
6
|
+
|
7
|
+
DEFAULT_EXECUTABLE_PATH = 'exiftool'
|
8
|
+
|
9
|
+
def initialize(options = { })
|
10
|
+
#@logger = options[:logger] || Logger.new(STDOUT)
|
11
|
+
@exiftool_cmd_path = options.fetch(:exiftool_cmd_path, DEFAULT_EXECUTABLE_PATH)
|
12
|
+
end # initialize
|
13
|
+
|
14
|
+
# @param [String] file_path
|
15
|
+
# @param [Hash] options
|
16
|
+
def run(file_path, options = {})
|
17
|
+
cmd_line = [@exiftool_cmd_path, '-json', file_path].shelljoin
|
18
|
+
#@logger.debug { "[ExifTool] Executing command: #{cmd_line}" }
|
19
|
+
metadata_json = %x(#{cmd_line})
|
20
|
+
#@logger.debug { "[ExifTool] Result: #{metadata_json}" }
|
21
|
+
JSON.parse(metadata_json)[0]
|
22
|
+
end # self.run
|
23
|
+
|
24
|
+
end #ExifTool
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,225 @@
|
|
1
|
+
require 'open3'
|
2
|
+
require 'shellwords'
|
3
|
+
require 'time' # unless defined? Time
|
4
|
+
|
5
|
+
class MediaInformationGatherer
|
6
|
+
class FFMPEG
|
7
|
+
|
8
|
+
DEFAULT_EXECUTABLE_PATH = 'ffmpeg'
|
9
|
+
|
10
|
+
class Movie
|
11
|
+
attr_reader :command, :output,
|
12
|
+
:path, :duration, :time, :bitrate, :rotation, :creation_time,
|
13
|
+
:video_stream, :video_codec, :video_bitrate, :colorspace, :resolution,
|
14
|
+
:dar, :display_aspect_ratio,
|
15
|
+
:sar, :storage_aspect_ratio,
|
16
|
+
:par, :pixel_aspect_ratio,
|
17
|
+
:width, :height, :is_widescreen, :is_high_definition, :calculated_aspect_ratio,
|
18
|
+
:audio_stream, :audio_codec, :audio_bitrate, :audio_sample_rate
|
19
|
+
|
20
|
+
def initialize(path, options = { })
|
21
|
+
raise Errno::ENOENT, "No such file or directory - '#{path}'" unless File.exists?(path)
|
22
|
+
@path = path
|
23
|
+
|
24
|
+
#@logger = options.fetch(:logger, Logger.new(STDOUT))
|
25
|
+
@ffmpeg_cmd_path = options.fetch(:ffmpeg_cmd_path, FFMPEG::DEFAULT_EXECUTABLE_PATH)
|
26
|
+
|
27
|
+
|
28
|
+
# ffmpeg will output to stderr
|
29
|
+
@command = [@ffmpeg_cmd_path, '-i', path].shelljoin
|
30
|
+
#@logger.debug { "[FFMPEG] Executing command '#{command}'" }
|
31
|
+
@output = Open3.popen3(command) { |stdin, stdout, stderr| stderr.read }
|
32
|
+
#@logger.debug { "[FFMPEG] Command response: #{@output}" }
|
33
|
+
|
34
|
+
fix_encoding(@output)
|
35
|
+
|
36
|
+
@output[/Duration: (\d{2}):(\d{2}):(\d{2}\.\d{2})/]
|
37
|
+
@duration = ($1.to_i*60*60) + ($2.to_i*60) + $3.to_f
|
38
|
+
|
39
|
+
@output[/start: (\d*\.\d*)/]
|
40
|
+
@time = $1 ? $1.to_f : 0.0
|
41
|
+
|
42
|
+
@output[/creation_time {1,}: {1,}(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/]
|
43
|
+
@creation_time = $1 ? Time.parse("#{$1}") : nil
|
44
|
+
|
45
|
+
@output[/bitrate: (\d*)/]
|
46
|
+
@bitrate = $1 ? $1.to_i : nil
|
47
|
+
|
48
|
+
@output[/rotate\ {1,}:\ {1,}(\d*)/]
|
49
|
+
@rotation = $1 ? $1.to_i : nil
|
50
|
+
|
51
|
+
@output[/Video: (.*)/]
|
52
|
+
@video_stream = $1
|
53
|
+
|
54
|
+
@output[/Audio: (.*)/]
|
55
|
+
@audio_stream = $1
|
56
|
+
|
57
|
+
@output[/timecode .* : (.*)/]
|
58
|
+
@timecode = $1
|
59
|
+
|
60
|
+
if video_stream
|
61
|
+
# Example Strings
|
62
|
+
#
|
63
|
+
# "dvvideo (dvc / 0x20637664), 720x480, 28771 kb/s): unspecified pixel format"
|
64
|
+
# "mpeg2video (4:2:2) (mx5n / 0x6E35786D), yuv422p, 720x512 [SAR 512:405 DAR 16:9], 50084 kb/s, SAR 40:33 DAR 75:44, 29.97 fps, 29.97 tbr, 2997 tbn, 59.94 tbc"
|
65
|
+
# "h264 (Main) (avc1 / 0x31637661), yuv420p, 854x480 [SAR 1:1 DAR 427:240], 2196 kb/s, 29.97 fps, 29.97 tbr, 2997 tbn, 59.94 tbc"
|
66
|
+
# "prores (apcn / 0x6E637061), yuv422p10le, 720x486, 23587 kb/s, SAR 10:11 DAR 400:297, 29.97 fps, 29.97 tbr, 2997 tbn, 2997 tbc"
|
67
|
+
# "Apple ProRes 422 (HQ), 1920x1080, 187905 kb/s, 29.97 fps, 29.97 tbr, 29970 tbn, 29970 tbc"
|
68
|
+
if video_stream.end_with?('unspecified pixel format')
|
69
|
+
@video_codec, @resolution, video_bitrate = video_stream.split(':').first.split(/\s?,\s?/)
|
70
|
+
video_bitrate = video_bitrate[0..-2] if video_bitrate and video_bitrate.end_with?(')')
|
71
|
+
else
|
72
|
+
@video_codec, @colorspace, @resolution, video_bitrate, aspect_ratios = video_stream.split(/\s?,\s?/)
|
73
|
+
if @colorspace.match(/\d*x\d*/)
|
74
|
+
@colorspace = nil
|
75
|
+
@video_codec, @resolution, video_bitrate, fps, tbr, tbn, tbc = video_stream.split(/\s?,\s?/)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
@video_bitrate = video_bitrate =~ %r(\A(\d+) kb/s\Z) ? $1.to_i : nil
|
81
|
+
|
82
|
+
|
83
|
+
process_aspect_ratios(aspect_ratios) if aspect_ratios and aspect_ratios.include?(':')
|
84
|
+
|
85
|
+
@resolution, aspect_ratios = @resolution.strip.split(' ', 2) rescue @resolution = aspect_ratios = nil
|
86
|
+
|
87
|
+
process_aspect_ratios(aspect_ratios) if aspect_ratios and aspect_ratios.include?(':')
|
88
|
+
|
89
|
+
@width, @height = @resolution.split('x') rescue @width = @height = nil
|
90
|
+
@frame_rate = $1 if video_stream[/(\d*\.?\d*)\s?fps/]
|
91
|
+
|
92
|
+
is_widescreen?
|
93
|
+
|
94
|
+
is_high_definition?
|
95
|
+
end
|
96
|
+
|
97
|
+
if audio_stream
|
98
|
+
@audio_codec, audio_sample_rate, @audio_channels, unused, audio_bitrate = audio_stream.split(/\s?,\s?/)
|
99
|
+
@audio_bitrate = audio_bitrate =~ %r(\A(\d+) kb/s\Z) ? $1.to_i : nil
|
100
|
+
@audio_sample_rate = audio_sample_rate[/\d*/].to_i
|
101
|
+
end
|
102
|
+
|
103
|
+
@invalid = true if @video_stream.to_s.empty? && @audio_stream.to_s.empty?
|
104
|
+
@invalid = true if @output.include?('is not supported')
|
105
|
+
@invalid = true if @output.include?('could not find codec parameters')
|
106
|
+
end # initialize
|
107
|
+
|
108
|
+
def process_aspect_ratios(aspect_ratios)
|
109
|
+
@dar = @display_aspect_ratio = $1 if aspect_ratios[/DAR (\d+:\d+)/] rescue nil # Display Aspect Ratio = SAR * PAR
|
110
|
+
@sar = @storage_aspect_ratio = $1 if aspect_ratios[/SAR (\d+:\d+)/] rescue nil # Storage Aspect Ratio = DAR/PAR
|
111
|
+
@par = @pixel_aspect_ratio = $1 if aspect_ratios[/PAR (\d+:\d+)/] rescue nil # Pixel aspect ratio = DAR/SAR
|
112
|
+
end # process_aspect_ratios
|
113
|
+
|
114
|
+
# @return [Boolean]
|
115
|
+
def valid?
|
116
|
+
not @invalid
|
117
|
+
end
|
118
|
+
|
119
|
+
# Determines if the aspect from dimensions is widescreen (> 1.5 (3/2)
|
120
|
+
# 1.55 is derived from the following tables
|
121
|
+
# {http://en.wikipedia.org/wiki/Storage_Aspect_Ratio#Previous_and_currently_used_aspect_ratios Aspect Ratios}
|
122
|
+
# {http://en.wikipedia.org/wiki/List_of_common_resolutions#Television}
|
123
|
+
#
|
124
|
+
# 1.55:1 (14:9): Widescreen aspect ratio sometimes used in shooting commercials etc. as a compromise format
|
125
|
+
# between 4:3 (12:9) and 16:9. When converted to a 16:9 frame, there is slight pillarboxing, while conversion to
|
126
|
+
# 4:3 creates slight letterboxing. All widescreen content on ABC Family's SD feed is presented in this ratio.
|
127
|
+
#
|
128
|
+
# @return [Boolean]
|
129
|
+
def is_widescreen?
|
130
|
+
@is_widescreen ||= (calculated_aspect_ratio ? (calculated_aspect_ratio >= 1.55) : false)
|
131
|
+
end
|
132
|
+
alias :is_wide_screen :is_widescreen
|
133
|
+
|
134
|
+
# (@link http://en.wikipedia.osrg/wiki/List_of_common_resolution)
|
135
|
+
#
|
136
|
+
# Lowest Width High Resolution Format Found:
|
137
|
+
# Panasonic DVCPRO100 for 50/60Hz over 720p - SMPTE Resolution = 960x720
|
138
|
+
#
|
139
|
+
# @return [Boolean]
|
140
|
+
def is_high_definition?
|
141
|
+
@is_high_definition ||= ( (width.respond_to?(:to_i) and height.respond_to?(:to_i)) ? (@width.to_i >= 950 and @height.to_i >= 700) : false )
|
142
|
+
end
|
143
|
+
alias :is_high_def? :is_high_definition?
|
144
|
+
|
145
|
+
# Will attempt to
|
146
|
+
def calculated_aspect_ratio
|
147
|
+
@calculated_aspect_ratio ||= aspect_from_dar || aspect_from_dimensions
|
148
|
+
end
|
149
|
+
|
150
|
+
# @return [Integer] File Size
|
151
|
+
def size
|
152
|
+
@size ||= File.size(@path)
|
153
|
+
end
|
154
|
+
|
155
|
+
# @return [Integer]
|
156
|
+
def audio_channel_count(audio_channels = @audio_channels)
|
157
|
+
return 0 unless audio_channels
|
158
|
+
return 1 if audio_channels['mono']
|
159
|
+
return 2 if audio_channels['stereo']
|
160
|
+
return 6 if audio_channels['5.1']
|
161
|
+
return 9 if audio_channels['7.2']
|
162
|
+
|
163
|
+
# If we didn't hit a match above then find any number in #.# format and add them together to get a channel count
|
164
|
+
audio_channels[/(\d+.?\d?).*/]
|
165
|
+
audio_channels = $1.to_s.split('.').map(&:to_i).inject(:+) if $1 rescue audio_channels
|
166
|
+
return audio_channels if audio_channels.is_a? Integer
|
167
|
+
end
|
168
|
+
|
169
|
+
# Outputs relavant instance variables names and values as a hash
|
170
|
+
# @return [Hash]
|
171
|
+
def to_hash
|
172
|
+
hash = Hash.new
|
173
|
+
variables = instance_variables
|
174
|
+
[ :@ffmpeg_cmd_path, :@logger ].each { |cmd| variables.delete(cmd) }
|
175
|
+
variables.each { |instance_variable_name|
|
176
|
+
hash[instance_variable_name.to_s[1..-1]] = instance_variable_get(instance_variable_name)
|
177
|
+
}
|
178
|
+
hash['audio_channel_count'] = audio_channel_count
|
179
|
+
hash['calculated_aspect_ratio'] = calculated_aspect_ratio
|
180
|
+
hash
|
181
|
+
end
|
182
|
+
|
183
|
+
protected
|
184
|
+
# @return [Integer|nil]
|
185
|
+
def aspect_from_dar
|
186
|
+
return nil unless dar
|
187
|
+
return @aspect_from_dar if @aspect_from_dar
|
188
|
+
w, h = dar.split(':')
|
189
|
+
aspect = w.to_f / h.to_f
|
190
|
+
@aspect_from_dar = aspect.zero? ? nil : aspect
|
191
|
+
end
|
192
|
+
|
193
|
+
# @return [Fixed]
|
194
|
+
def aspect_from_dimensions
|
195
|
+
return @aspect_from_dimensions if @aspect_from_dimensions
|
196
|
+
|
197
|
+
aspect = width.to_f / height.to_f
|
198
|
+
@aspect_from_dimensions = aspect.nan? ? nil : aspect
|
199
|
+
end
|
200
|
+
|
201
|
+
# @param [String] output
|
202
|
+
def fix_encoding(output)
|
203
|
+
output[/test/] # Running a regexp on the string throws error if it's not UTF-8
|
204
|
+
rescue ArgumentError
|
205
|
+
output.force_encoding('ISO-8859-1')
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# @param [Hash] options
|
210
|
+
# @option options [String] :ffmpeg_cmd_path
|
211
|
+
def initialize(options = { })
|
212
|
+
@ffmpeg_cmd_path = options.fetch(:ffmpeg_cmd_path, 'ffmpeg')
|
213
|
+
end # initialize
|
214
|
+
|
215
|
+
# @param [String] file_path
|
216
|
+
# @param [Hash] options
|
217
|
+
# @option options [String] :ffmpeg_cmd_path
|
218
|
+
def run(file_path, options = { })
|
219
|
+
options = { :ffmpeg_cmd_path => @ffmpeg_cmd_path }.merge(options)
|
220
|
+
Movie.new(file_path, options).to_hash
|
221
|
+
end # run
|
222
|
+
|
223
|
+
end # FFMPEG
|
224
|
+
|
225
|
+
end
|