media_processing_tool 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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