kakra-rvideo 0.9.6

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