mhs-rvideo 0.9.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,557 @@
1
+ module RVideo # :nodoc:
2
+ class Inspector
3
+
4
+ attr_reader :filename, :path, :full_filename, :raw_response, :raw_metadata
5
+
6
+ attr_accessor :ffmpeg_binary
7
+
8
+ #
9
+ # To inspect a video or audio file, initialize an Inspector object.
10
+ #
11
+ # file = RVideo::Inspector.new(options_hash)
12
+ #
13
+ # Inspector accepts three options: file, raw_response, and ffmpeg_binary.
14
+ # Either raw_response or file is required; ffmpeg binary is optional.
15
+ #
16
+ # :file is a path to a file to be inspected.
17
+ #
18
+ # :raw_response is the full output of "ffmpeg -i [file]". If the
19
+ # :raw_response option is used, RVideo will not actually inspect a file;
20
+ # it will simply parse the provided response. This is useful if your
21
+ # application has already collected the ffmpeg -i response, and you don't
22
+ # want to call it again.
23
+ #
24
+ # :ffmpeg_binary is an optional argument that specifies the path to the
25
+ # ffmpeg binary to be used. If a path is not explicitly declared, RVideo
26
+ # will assume that ffmpeg exists in the Unix path. Type "which ffmpeg" to
27
+ # check if ffmpeg is installed and exists in your operating system's path.
28
+ #
29
+
30
+ def initialize(options = {})
31
+ if options[:raw_response]
32
+ @raw_response = options[:raw_response]
33
+ elsif options[:file]
34
+ if options[:ffmpeg_binary]
35
+ @ffmpeg_binary = options[:ffmpeg_binary]
36
+ raise RuntimeError, "ffmpeg could not be found (trying #{@ffmpeg_binary})" unless FileTest.exist?(@ffmpeg_binary)
37
+ else
38
+ # assume it is in the unix path
39
+ raise RuntimeError, 'ffmpeg could not be found (expected ffmpeg to be found in the Unix path)' unless FileTest.exist?(`which ffmpeg`.chomp)
40
+ @ffmpeg_binary = "ffmpeg"
41
+ end
42
+
43
+ file = options[:file]
44
+ @filename = File.basename(file)
45
+ @path = File.dirname(file)
46
+ @full_filename = file
47
+ raise TranscoderError::InputFileNotFound, "File not found (#{file})" unless FileTest.exist?(file.gsub("\"",""))
48
+ @raw_response = `#{@ffmpeg_binary} -i #{@full_filename} 2>&1`
49
+ else
50
+ raise ArgumentError, "Must supply either an input file or a pregenerated response" if options[:raw_response].nil? and file.nil?
51
+ end
52
+
53
+ metadata = metadata_match
54
+
55
+ if /Unknown format/i.match(@raw_response) || metadata.nil?
56
+ @unknown_format = true
57
+ elsif /Duration: N\/A/im.match(@raw_response)
58
+ # elsif /Duration: N\/A|bitrate: N\/A/im.match(@raw_response)
59
+ @unreadable_file = true
60
+ @raw_metadata = metadata[1] # in this case, we can at least still get the container type
61
+ else
62
+ @raw_metadata = metadata[1]
63
+ end
64
+ end
65
+
66
+ #
67
+ # Returns true if the file can be read successfully. Returns false otherwise.
68
+ #
69
+
70
+ def valid?
71
+ if @unknown_format or @unreadable_file
72
+ false
73
+ else
74
+ true
75
+ end
76
+ end
77
+
78
+ #
79
+ # Returns false if the file can be read successfully. Returns false otherwise.
80
+ #
81
+
82
+ def invalid?
83
+ !valid?
84
+ end
85
+
86
+ #
87
+ # True if the format is not understood ("Unknown Format")
88
+ #
89
+
90
+ def unknown_format?
91
+ if @unknown_format
92
+ true
93
+ else
94
+ false
95
+ end
96
+ end
97
+
98
+ #
99
+ # True if the file is not readable ("Duration: N/A, bitrate: N/A")
100
+ #
101
+
102
+ def unreadable_file?
103
+ if @unreadable_file
104
+ true
105
+ else
106
+ false
107
+ end
108
+ end
109
+
110
+ #
111
+ # Does the file have an audio stream?
112
+ #
113
+
114
+ def audio?
115
+ if audio_match.nil?
116
+ false
117
+ else
118
+ true
119
+ end
120
+ end
121
+
122
+ #
123
+ # Does the file have a video stream?
124
+ #
125
+
126
+ def video?
127
+ if video_match.nil?
128
+ false
129
+ else
130
+ true
131
+ end
132
+ end
133
+
134
+ #
135
+ # Take a screengrab of a movie. Requires an input file and a time parameter, and optionally takes an output filename. If no output filename is specfied, constructs one.
136
+ #
137
+ # Three types of time parameters are accepted - percentage (e.g. 3%), time in seconds (e.g. 60 seconds), and raw frame (e.g. 37). Will raise an exception if the time in seconds or the frame are out of the bounds of the input file.
138
+ #
139
+ # Types:
140
+ # 37s (37 seconds)
141
+ # 37f (frame 37)
142
+ # 37% (37 percent)
143
+ # 37 (default to seconds)
144
+ #
145
+ # If a time is outside of the duration of the file, it will choose a frame at the 99% mark.
146
+ #
147
+ # Example:
148
+ #
149
+ # t = RVideo::Transcoder.new('path/to/input_file.mp4')
150
+ # t.capture_frame('10%') # => '/path/to/screenshot/input-10p.jpg'
151
+ #
152
+
153
+ def capture_frame(timecode, output_file = nil)
154
+ t = calculate_time(timecode)
155
+ unless output_file
156
+ output_file = "#{TEMP_PATH}/#{File.basename(@full_filename, ".*")}-#{timecode.gsub("%","p")}.jpg"
157
+ end
158
+ # do the work
159
+ # mplayer $input_file$ -ss $start_time$ -frames 1 -vo jpeg -o $output_file$
160
+ # ffmpeg -i $input_file$ -v nopb -ss $start_time$ -b $bitrate$ -an -vframes 1 -y $output_file$
161
+ command = "ffmpeg -i #{@full_filename} -ss #{t} -t 00:00:01 -r 1 -vframes 1 -f image2 #{output_file}"
162
+ Transcoder.logger.info("\nCreating Screenshot: #{command}\n")
163
+ frame_result = `#{command} 2>&1`
164
+ Transcoder.logger.info("\nScreenshot results: #{frame_result}")
165
+ output_file
166
+ end
167
+
168
+ def calculate_time(timecode)
169
+ m = /\A([0-9\.\,]*)(s|f|%)?\Z/.match(timecode)
170
+ if m.nil? or m[1].nil? or m[1].empty?
171
+ raise TranscoderError::ParameterError, "Invalid timecode for frame capture: #{timecode}. Must be a number, optionally followed by s, f, or %."
172
+ end
173
+
174
+ case m[2]
175
+ when "s", nil
176
+ t = m[1].to_f
177
+ when "f"
178
+ t = m[1].to_f / fps.to_f
179
+ when "%"
180
+ # milliseconds / 1000 * percent / 100
181
+ t = (duration.to_i / 1000.0) * (m[1].to_f / 100.0)
182
+ else
183
+ raise TranscoderError::ParameterError, "Invalid timecode for frame capture: #{timecode}. Must be a number, optionally followed by s, f, or p."
184
+ end
185
+
186
+ if (t * 1000) > duration
187
+ calculate_time("99%")
188
+ else
189
+ t
190
+ end
191
+ end
192
+
193
+ #
194
+ # Returns the version of ffmpeg used, In practice, this may or may not be
195
+ # useful.
196
+ #
197
+ # Examples:
198
+ #
199
+ # SVN-r6399
200
+ # CVS
201
+ #
202
+
203
+ def ffmpeg_version
204
+ @ffmpeg_version = @raw_response.split("\n").first.split("version").last.split(",").first.strip
205
+ end
206
+
207
+ #
208
+ # Returns the configuration options used to build ffmpeg.
209
+ #
210
+ # Example:
211
+ #
212
+ # --enable-mp3lame --enable-gpl --disable-ffplay --disable-ffserver
213
+ # --enable-a52 --enable-xvid
214
+ #
215
+
216
+ def ffmpeg_configuration
217
+ /(\s*configuration:)(.*)\n/.match(@raw_response)[2].strip
218
+ end
219
+
220
+ #
221
+ # Returns the versions of libavutil, libavcodec, and libavformat used by
222
+ # ffmpeg.
223
+ #
224
+ # Example:
225
+ #
226
+ # libavutil version: 49.0.0
227
+ # libavcodec version: 51.9.0
228
+ # libavformat version: 50.4.0
229
+ #
230
+
231
+ def ffmpeg_libav
232
+ /^(\s*lib.*\n)+/.match(@raw_response)[0].split("\n").each {|l| l.strip! }
233
+ end
234
+
235
+ #
236
+ # Returns the build description for ffmpeg.
237
+ #
238
+ # Example:
239
+ #
240
+ # built on Apr 15 2006 04:58:19, gcc: 4.0.1 (Apple Computer, Inc. build
241
+ # 5250)
242
+ #
243
+
244
+ def ffmpeg_build
245
+ /(\n\s*)(built on.*)(\n)/.match(@raw_response)[2]
246
+ end
247
+
248
+ #
249
+ # Returns the container format for the file. Instead of returning a single
250
+ # format, this may return a string of related formats.
251
+ #
252
+ # Examples:
253
+ #
254
+ # "avi"
255
+ #
256
+ # "mov,mp4,m4a,3gp,3g2,mj2"
257
+ #
258
+
259
+ def container
260
+ return nil if @unknown_format
261
+
262
+ /Input \#\d+\,\s*(\S+),\s*from/.match(@raw_metadata)[1]
263
+ end
264
+
265
+ #
266
+ # The duration of the movie, as a string.
267
+ #
268
+ # Example:
269
+ #
270
+ # "00:00:24.4" # 24.4 seconds
271
+ #
272
+ def raw_duration
273
+ return nil unless valid?
274
+
275
+ /Duration:\s*([0-9\:\.]+),/.match(@raw_metadata)[1]
276
+ end
277
+
278
+ #
279
+ # The duration of the movie in milliseconds, as an integer.
280
+ #
281
+ # Example:
282
+ #
283
+ # 24400 # 24.4 seconds
284
+ #
285
+ # Note that the precision of the duration is in tenths of a second, not
286
+ # thousandths, but milliseconds are a more standard unit of time than
287
+ # deciseconds.
288
+ #
289
+
290
+ def duration
291
+ return nil unless valid?
292
+
293
+ units = raw_duration.split(":")
294
+ (units[0].to_i * 60 * 60 * 1000) + (units[1].to_i * 60 * 1000) + (units[2].to_f * 1000).to_i
295
+ end
296
+
297
+ #
298
+ # The bitrate of the movie.
299
+ #
300
+ # Example:
301
+ #
302
+ # 3132
303
+ #
304
+
305
+ def bitrate
306
+ return nil unless valid?
307
+
308
+ bitrate_match[1].to_i
309
+ end
310
+
311
+ #
312
+ # The bitrate units used. In practice, this may always be kb/s.
313
+ #
314
+ # Example:
315
+ #
316
+ # "kb/s"
317
+ #
318
+
319
+ def bitrate_units
320
+ return nil unless valid?
321
+
322
+ bitrate_match[2]
323
+ end
324
+
325
+ def audio_bitrate # :nodoc:
326
+ nil
327
+ end
328
+
329
+ def audio_stream
330
+ return nil unless valid?
331
+
332
+ #/\n\s*Stream.*Audio:.*\n/.match(@raw_response)[0].strip
333
+ match = /\n\s*Stream.*Audio:.*\n/.match(@raw_response)
334
+ return match[0].strip if match
335
+ end
336
+
337
+ #
338
+ # The audio codec used.
339
+ #
340
+ # Example:
341
+ #
342
+ # "aac"
343
+ #
344
+
345
+ def audio_codec
346
+ return nil unless audio?
347
+
348
+ audio_match[2]
349
+ end
350
+
351
+ #
352
+ # The sampling rate of the audio stream.
353
+ #
354
+ # Example:
355
+ #
356
+ # 44100
357
+ #
358
+
359
+ def audio_sample_rate
360
+ return nil unless audio?
361
+
362
+ audio_match[3].to_i
363
+ end
364
+
365
+ #
366
+ # The units used for the sampling rate. May always be Hz.
367
+ #
368
+ # Example:
369
+ #
370
+ # "Hz"
371
+ #
372
+
373
+ def audio_sample_units
374
+ return nil unless audio?
375
+
376
+ audio_match[4]
377
+ end
378
+
379
+ #
380
+ # The channels used in the audio stream.
381
+ #
382
+ # Examples:
383
+ # "stereo"
384
+ # "mono"
385
+ # "5:1"
386
+ #
387
+
388
+ def audio_channels_string
389
+ return nil unless audio?
390
+
391
+ audio_match[5]
392
+ end
393
+
394
+ def audio_channels
395
+ return nil unless audio?
396
+
397
+ case audio_match[5]
398
+ when "mono"
399
+ 1
400
+ when "stereo"
401
+ 2
402
+ else
403
+ raise RuntimeError, "Unknown number of channels: #{audio_channels}"
404
+ end
405
+ end
406
+
407
+ #
408
+ # The ID of the audio stream (useful for troubleshooting).
409
+ #
410
+ # Example:
411
+ # #0.1
412
+ #
413
+
414
+ def audio_stream_id
415
+ return nil unless audio?
416
+
417
+ audio_match[1]
418
+ end
419
+
420
+ def video_stream
421
+ return nil unless valid?
422
+
423
+ match = /\n\s*Stream.*Video:.*\n/.match(@raw_response)
424
+ return match[0].strip unless match.nil?
425
+ nil
426
+ end
427
+
428
+ #
429
+ # The ID of the video stream (useful for troubleshooting).
430
+ #
431
+ # Example:
432
+ # #0.0
433
+ #
434
+
435
+ def video_stream_id
436
+ return nil unless video?
437
+
438
+ video_match[1]
439
+ end
440
+
441
+ #
442
+ # The video codec used.
443
+ #
444
+ # Example:
445
+ #
446
+ # "mpeg4"
447
+ #
448
+
449
+ def video_codec
450
+ return nil unless video?
451
+
452
+ video_match[2]
453
+ end
454
+
455
+ #
456
+ # The colorspace of the video stream.
457
+ #
458
+ # Example:
459
+ #
460
+ # "yuv420p"
461
+ #
462
+
463
+ def video_colorspace
464
+ return nil unless video?
465
+
466
+ video_match[3]
467
+ end
468
+
469
+ #
470
+ # The width of the video in pixels.
471
+ #
472
+
473
+ def width
474
+ return nil unless video?
475
+
476
+ video_match[4].to_i
477
+ end
478
+
479
+ #
480
+ # The height of the video in pixels.
481
+ #
482
+
483
+ def height
484
+ return nil unless video?
485
+
486
+ video_match[5].to_i
487
+ end
488
+
489
+ #
490
+ # width x height, as a string.
491
+ #
492
+ # Examples:
493
+ # 320x240
494
+ # 1280x720
495
+ #
496
+
497
+ def resolution
498
+ return nil unless video?
499
+
500
+ "#{width}x#{height}"
501
+ end
502
+
503
+ #
504
+ # The frame rate of the video in frames per second
505
+ #
506
+ # Example:
507
+ #
508
+ # "29.97"
509
+ #
510
+
511
+ def fps
512
+ return nil unless video?
513
+
514
+ /([0-9\.]+) (fps|tb)/.match(video_stream)[1]
515
+ end
516
+
517
+ private
518
+
519
+ def metadata_match
520
+ [
521
+ /(Input \#.*)\nMust/m, # ffmpegX
522
+ /(Input \#.*)\nAt least/m # ffmpeg macports
523
+ ].each do |rgx|
524
+ if md=rgx.match(@raw_response)
525
+ return md
526
+ end
527
+ end
528
+ nil
529
+ end
530
+
531
+ def bitrate_match
532
+ /bitrate: ([0-9\.]+)\s*(.*)\s+/.match(@raw_metadata)
533
+ end
534
+
535
+ def audio_match
536
+ return nil unless valid?
537
+
538
+ /Stream\s*(.*?)[,|:|\(|\[].*?\s*Audio:\s*(.*?),\s*([0-9\.]*) (\w*),\s*([a-zA-Z:]*)/.match(audio_stream)
539
+ end
540
+
541
+ def video_match
542
+ return nil unless valid?
543
+
544
+ match = /Stream\s*(.*?)[,|:|\(|\[].*?\s*Video:\s*(.*?),\s*(.*?),\s*(\d*)x(\d*)/.match(video_stream)
545
+
546
+ # work-around for Apple Intermediate format, which does not have a color space
547
+ # I fake up a match data object (yea, duck typing!) with an empty spot where
548
+ # the color space would be.
549
+ if match.nil?
550
+ match = /Stream\s*(.*?)[,|:|\(|\[].*?\s*Video:\s*(.*?),\s*(\d*)x(\d*)/.match(video_stream)
551
+ match = [nil, match[1], match[2], nil, match[3], match[4]] unless match.nil?
552
+ end
553
+
554
+ match
555
+ end
556
+ end
557
+ end
@@ -0,0 +1,156 @@
1
+ module RVideo # :nodoc:
2
+ module Tools # :nodoc:
3
+ class AbstractTool
4
+
5
+ #
6
+ # AbstractTool is an interface to every transcoder tool class (e.g.
7
+ # ffmpeg, flvtool2). Called by the Transcoder class.
8
+ #
9
+
10
+ def self.assign(cmd, options = {})
11
+ tool_name = cmd.split(" ").first
12
+ begin
13
+ tool = "RVideo::Tools::#{tool_name.classify}".constantize.send(:new, cmd, options)
14
+ rescue NameError, /uninitialized constant/
15
+ raise TranscoderError::UnknownTool, "The recipe tried to use the '#{tool_name}' tool, which does not exist."
16
+ end
17
+ end
18
+
19
+
20
+ module InstanceMethods
21
+ attr_reader :options, :command, :raw_result
22
+ attr_writer :original
23
+
24
+ def initialize(raw_command, options = {})
25
+ @raw_command = raw_command
26
+ @options = HashWithIndifferentAccess.new(options)
27
+ @command = interpolate_variables(raw_command)
28
+ end
29
+
30
+ #
31
+ # Execute the command and parse the result.
32
+ #
33
+
34
+ def execute
35
+ final_command = "#{@command} 2>&1"
36
+ Transcoder.logger.info("\nExecuting Command: #{final_command}\n")
37
+ @raw_result = `#{final_command}`
38
+ Transcoder.logger.info("Result: \n#{@raw_result}")
39
+ parse_result(@raw_result)
40
+ end
41
+
42
+ #
43
+ # Magic parameters
44
+ #
45
+
46
+ def original_fps
47
+ raise ParameterError, "The #{self.class} tool has not implemented the original_fps method."
48
+ end
49
+
50
+ def resolution
51
+ if @options['resolution']
52
+ @options['resolution']
53
+ else
54
+ "#{calculate_width}x#{calculate_height}"
55
+ end
56
+ end
57
+
58
+ def aspect_ratio
59
+ "#{calculate_width}:#{calculate_height}"
60
+ end
61
+
62
+ def fps
63
+ case fps.to_s
64
+ when "?"
65
+ inspect_original if @original.nil?
66
+ @original.fps
67
+ else
68
+ @options['fps']
69
+ end
70
+ end
71
+
72
+ def calculate_width
73
+ width = @options['width']
74
+ height = @options['height']
75
+ if width.to_i > 0
76
+ width
77
+ end
78
+
79
+ case width
80
+ when "x"
81
+ inspect_original if @original.nil?
82
+ x = ((@original.width.to_f / @original.height.to_f) * height.to_f).to_i
83
+ (x.to_f / 16).round * 16
84
+ when "?"
85
+ inspect_original if @original.nil?
86
+ @original.width
87
+ else
88
+ width
89
+ end
90
+ end
91
+
92
+ def calculate_height
93
+ width = @options['width']
94
+ height = @options['height']
95
+ if height.to_i > 0
96
+ height
97
+ end
98
+ #w/(ow/oh) = h
99
+
100
+ case height
101
+ when "x"
102
+ inspect_original if @original.nil?
103
+ x = (width.to_f / (@original.width.to_f / @original.height.to_f)).to_i
104
+ (x.to_f / 16).round * 16
105
+ when "?"
106
+ inspect_original if @original.nil?
107
+ @original.height
108
+ else
109
+ height
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+
116
+ #
117
+ # Look for variables surrounded by $, and interpolate with either
118
+ # variables passed in the options hash, or special methods provided by
119
+ # the tool class (e.g. "$original_fps$" with ffmpeg).
120
+ #
121
+ # $foo$ should match
122
+ # \$foo or $foo\$ or \$foo\$ should not
123
+
124
+ def interpolate_variables(raw_command)
125
+ raw_command.scan(/[^\\]\$[-_a-zA-Z]+\$/).each do |match|
126
+ match.strip!
127
+ raw_command.gsub!(match, matched_variable(match))
128
+ end
129
+ raw_command.gsub("\\$", "$")
130
+ end
131
+
132
+ #
133
+ # Strip the $s. First, look for a supplied option that matches the
134
+ # variable name. If one is not found, look for a method that matches.
135
+ # If not found, raise ParameterError exception.
136
+ #
137
+
138
+ def matched_variable(match)
139
+ variable_name = match.gsub("$","")
140
+ if self.respond_to? variable_name
141
+ self.send(variable_name)
142
+ elsif @options.key?(variable_name)
143
+ @options[variable_name] || ""
144
+ else
145
+ raise TranscoderError::ParameterError, "command is looking for the #{variable_name} parameter, but it was not provided. (Command: #{@raw_command})"
146
+ end
147
+ end
148
+
149
+ def inspect_original
150
+ @original = Inspector.new(:file => options[:input_file])
151
+ end
152
+ end
153
+
154
+ end
155
+ end
156
+ end