media_processing_tool 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|