stlondemand-rvideo 0.9.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. data/CHANGELOG +70 -0
  2. data/ENV +100 -0
  3. data/ENV2 +129 -0
  4. data/LICENSE +20 -0
  5. data/Manifest +72 -0
  6. data/README +106 -0
  7. data/RULES +11 -0
  8. data/Rakefile +63 -0
  9. data/config/boot.rb +25 -0
  10. data/lib/rvideo.rb +49 -0
  11. data/lib/rvideo/command_executor.rb +91 -0
  12. data/lib/rvideo/errors.rb +24 -0
  13. data/lib/rvideo/float.rb +7 -0
  14. data/lib/rvideo/frame_capturer.rb +139 -0
  15. data/lib/rvideo/inspector.rb +519 -0
  16. data/lib/rvideo/reporter.rb +176 -0
  17. data/lib/rvideo/reporter/views/index.html.erb +27 -0
  18. data/lib/rvideo/reporter/views/report.css +27 -0
  19. data/lib/rvideo/reporter/views/report.html.erb +81 -0
  20. data/lib/rvideo/reporter/views/report.js +9 -0
  21. data/lib/rvideo/string.rb +5 -0
  22. data/lib/rvideo/tools/abstract_tool.rb +459 -0
  23. data/lib/rvideo/tools/ffmpeg.rb +314 -0
  24. data/lib/rvideo/tools/ffmpeg2theora.rb +72 -0
  25. data/lib/rvideo/tools/flvtool2.rb +50 -0
  26. data/lib/rvideo/tools/handbrakecli.rb +61 -0
  27. data/lib/rvideo/tools/lame.rb +58 -0
  28. data/lib/rvideo/tools/mencoder.rb +126 -0
  29. data/lib/rvideo/tools/mp4box.rb +21 -0
  30. data/lib/rvideo/tools/mp4creator.rb +35 -0
  31. data/lib/rvideo/tools/mplayer.rb +31 -0
  32. data/lib/rvideo/tools/qtfaststart.rb +37 -0
  33. data/lib/rvideo/tools/segmenter.rb +29 -0
  34. data/lib/rvideo/tools/yamdi.rb +44 -0
  35. data/lib/rvideo/transcoder.rb +170 -0
  36. data/lib/rvideo/version.rb +9 -0
  37. data/scripts/txt2html +67 -0
  38. data/spec/files/boat.avi +0 -0
  39. data/spec/files/kites.mp4 +0 -0
  40. data/spec/fixtures/ffmpeg_builds.yml +28 -0
  41. data/spec/fixtures/ffmpeg_results.yml +608 -0
  42. data/spec/fixtures/files.yml +398 -0
  43. data/spec/fixtures/recipes.yml +58 -0
  44. data/spec/integrations/formats_spec.rb +315 -0
  45. data/spec/integrations/frame_capturer_spec.rb +26 -0
  46. data/spec/integrations/inspection_spec.rb +125 -0
  47. data/spec/integrations/recipes_spec.rb +0 -0
  48. data/spec/integrations/rvideo_spec.rb +17 -0
  49. data/spec/integrations/transcoder_integration_spec.rb +29 -0
  50. data/spec/integrations/transcoding_spec.rb +9 -0
  51. data/spec/spec.opts +1 -0
  52. data/spec/spec_helper.rb +16 -0
  53. data/spec/support.rb +36 -0
  54. data/spec/units/abstract_tool_spec.rb +111 -0
  55. data/spec/units/command_executor_spec.rb +106 -0
  56. data/spec/units/ffmpeg_spec.rb +385 -0
  57. data/spec/units/flvtool2_spec.rb +323 -0
  58. data/spec/units/frame_capturer_spec.rb +71 -0
  59. data/spec/units/inspector_spec.rb +59 -0
  60. data/spec/units/mencoder_spec.rb +4994 -0
  61. data/spec/units/mp4box_spec.rb +34 -0
  62. data/spec/units/mp4creator_spec.rb +34 -0
  63. data/spec/units/mplayer_spec.rb +34 -0
  64. data/spec/units/qtfaststart_spec.rb +35 -0
  65. data/spec/units/string_spec.rb +8 -0
  66. data/spec/units/transcoder_spec.rb +154 -0
  67. data/stlondemand-rvideo.gemspec +36 -0
  68. data/tasks/deployment.rake +5 -0
  69. data/tasks/testing.rake +27 -0
  70. data/tasks/transcoding.rake +40 -0
  71. data/tasks/website.rake +8 -0
  72. data/test_progress_reporting.rb +14 -0
  73. metadata +187 -0
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,63 @@
1
+ require "rubygems"
2
+ require "fileutils"
3
+ require "echoe"
4
+
5
+ __HERE__ = File.dirname(__FILE__)
6
+ require File.join(__HERE__, 'lib', 'rvideo', 'version')
7
+ require File.join(__HERE__, 'lib', 'rvideo')
8
+
9
+ ###
10
+
11
+ AUTHOR = [
12
+ "Peter Boling",
13
+ "Jonathan Dahl (Slantwise Design)",
14
+ "Seth Thomas Rasmussen"
15
+ ]
16
+ EMAIL = "sethrasmussen@gmail.com"
17
+ DESCRIPTION = "Inspect and transcode video and audio files."
18
+
19
+ NAME = "rvideo"
20
+
21
+ REV = `git log -n1 --pretty=oneline | cut -d' ' -f1`.strip
22
+ BRANCH = `git branch | grep '*' | cut -d' ' -f2`.strip
23
+
24
+ # This is not the version used for the gem.
25
+ # That is parsed from the CHANGELOG by Echoe.
26
+ VERS = "#{RVideo::VERSION::STRING} (#{BRANCH} @ #{REV})"
27
+
28
+ Echoe.new NAME do |p|
29
+ p.author = AUTHOR
30
+ p.description = DESCRIPTION
31
+ p.email = EMAIL
32
+ p.summary = DESCRIPTION
33
+ p.url = "http://github.com/greatseth/rvideo"
34
+
35
+ p.runtime_dependencies = ["activesupport"]
36
+ p.development_dependencies = ["rspec"]
37
+
38
+ p.ignore_pattern = [
39
+ "spec/files/boat.mpg",
40
+ "spec/files/dinner.3g2",
41
+ "spec/files/foo $ bar & baz",
42
+ "spec/files/hats.3gp",
43
+ "spec/files/quads.wmv",
44
+ "spec/files/sword.3gp",
45
+ "website/**/*",
46
+ "tmp/**/*",
47
+ "scripts/test_progress.rb"
48
+ ]
49
+
50
+ p.rdoc_options = [
51
+ "--quiet",
52
+ "--title", "rvideo documentation",
53
+ "--opname", "index.html",
54
+ "--line-numbers",
55
+ "--main", "README",
56
+ "--inline-source"
57
+ ]
58
+ end
59
+
60
+ # Load supporting Rake files
61
+ Dir[File.join(__HERE__, "tasks", "*.rake")].each { |t| load t }
62
+
63
+ puts "#{NAME} #{VERS}"
@@ -0,0 +1,25 @@
1
+ # I don't know what this file was intended for originally, but I've been
2
+ # using it to setup RVideo to play with in an IRB session. - Seth
3
+
4
+ $LOAD_PATH.unshift File.expand_path(File.join(File.dirname(__FILE__), '..'))
5
+ require "lib/rvideo"
6
+ require "spec/support"
7
+
8
+ include RVideo
9
+
10
+ ###
11
+
12
+ class Inspector
13
+ public :video_match
14
+ public :audio_match
15
+ end
16
+
17
+ def inspector(filename)
18
+ options = if filename.is_a? Symbol
19
+ { :raw_response => files(filename) }
20
+ else
21
+ { :file => spec_file(filename) }
22
+ end
23
+
24
+ Inspector.new options
25
+ end
@@ -0,0 +1,49 @@
1
+ $LOAD_PATH.unshift File.dirname(__FILE__)
2
+
3
+ # core extensions
4
+ require 'rvideo/float'
5
+ require 'rvideo/string'
6
+
7
+ # gems
8
+ require 'rubygems'
9
+ require 'active_support'
10
+ require 'open4'
11
+
12
+ # rvideo
13
+ require 'rvideo/command_executor'
14
+ require 'rvideo/inspector'
15
+ require 'rvideo/frame_capturer'
16
+ require 'rvideo/errors'
17
+ require 'rvideo/transcoder'
18
+ require 'rvideo/tools/abstract_tool'
19
+ require 'rvideo/tools/ffmpeg'
20
+ require 'rvideo/tools/mencoder'
21
+ require 'rvideo/tools/flvtool2'
22
+ require 'rvideo/tools/mp4box'
23
+ require 'rvideo/tools/mplayer'
24
+ require 'rvideo/tools/mp4creator'
25
+ require 'rvideo/tools/ffmpeg2theora'
26
+ require 'rvideo/tools/yamdi'
27
+ require 'rvideo/tools/qtfaststart'
28
+ require 'rvideo/tools/segmenter'
29
+ require 'rvideo/tools/handbrakecli'
30
+ require 'rvideo/tools/lame'
31
+
32
+ TEMP_PATH = File.expand_path(File.dirname(__FILE__) + '/../tmp')
33
+ REPORT_PATH = File.expand_path(File.dirname(__FILE__) + '/../report')
34
+
35
+ module RVideo
36
+ # Configure logging. Assumes that the logger object has an
37
+ # interface similar to stdlib's Logger class.
38
+ #
39
+ # RVideo.logger = Logger.new(STDOUT)
40
+ #
41
+ def self.logger=(logger)
42
+ @logger = logger
43
+ end
44
+
45
+ def self.logger
46
+ @logger = Logger.new("/dev/null") unless @logger
47
+ @logger
48
+ end
49
+ end
@@ -0,0 +1,91 @@
1
+ require 'open4'
2
+
3
+ class IO
4
+
5
+ def each_with_timeout(timeout, sep_string=$/)
6
+ q = Queue.new
7
+ th = nil
8
+
9
+ timer_set = lambda do |timeout|
10
+ th = new_thread{ to(timeout){ q.pop } }
11
+ end
12
+
13
+ timer_cancel = lambda do |timeout|
14
+ th.kill if th rescue nil
15
+ end
16
+
17
+ timer_set[timeout]
18
+ begin
19
+ self.each(sep_string) do |buf|
20
+ timer_cancel[timeout]
21
+ yield buf
22
+ timer_set[timeout]
23
+ end
24
+ ensure
25
+ timer_cancel[timeout]
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def new_thread *a, &b
32
+ cur = Thread.current
33
+ Thread.new(*a) do |*a|
34
+ begin
35
+ b[*a]
36
+ rescue Exception => e
37
+ cur.raise e
38
+ end
39
+ end
40
+ end
41
+
42
+ def to timeout = nil
43
+ Timeout.timeout(timeout){ yield }
44
+ end
45
+
46
+ end
47
+
48
+
49
+ module RVideo
50
+ module CommandExecutor
51
+ class ProcessHungError < StandardError; end
52
+
53
+ STDOUT_TIMEOUT = 200
54
+
55
+ def self.execute_with_block(command, line_separator=$/, use_stderr = true)
56
+ begin
57
+ pid, stdin, stdout, stderr = Open4::open4(command)
58
+ c_pipe = use_stderr ? stderr : stdout
59
+ pipe_result = ''
60
+
61
+ c_pipe.each_with_timeout(STDOUT_TIMEOUT, line_separator) do |line|
62
+ yield line
63
+ pipe_result += line
64
+ end
65
+ Process.kill("SIGKILL", pid)
66
+ Process.waitpid2 pid
67
+
68
+ stdout_result = use_stderr ? stdout.read : pipe_result
69
+ stderr_result = use_stderr ? pipe_result : stderr.read
70
+
71
+ [stdin, stdout, stderr].each{|io| io.close}
72
+
73
+ return [stderr_result, stdout_result]
74
+ rescue Timeout::Error => e
75
+ Process.kill("SIGKILL", pid)
76
+ Process.waitpid2 pid
77
+ [stdin, stdout, stderr].each{|io| io.close}
78
+ raise ProcessHungError
79
+ end
80
+ end
81
+
82
+ def self.execute_tailing_stderr(command, number_of_lines = 500)
83
+ result = String.new
84
+ open4(command) do |pid, i, o, e|
85
+ open4.spawn "tail -n #{number_of_lines}", :stdin=>e, :stdout=>result, :stdin_timeout => 24*60*60
86
+ end
87
+ result
88
+ end
89
+
90
+ end
91
+ end
@@ -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,139 @@
1
+ module RVideo
2
+ # FrameCapturer uses ffmpeg to capture frames from a movie in JPEG format.
3
+ #
4
+ # You can capture one or many frames in a variety of ways:
5
+ #
6
+ # - one frame at a given offset
7
+ # - multiple frames every n seconds from a given offset
8
+ #
9
+ # TODO
10
+ #
11
+ # - n frames total, evenly distributed across the duration of the movie
12
+ #
13
+ # For the offset options, three types of values are accepted:
14
+ # - percentage e.g. '37%'
15
+ # - seconds e.g. '37s' or simply '37'
16
+ # - frame e.g. '37f'
17
+ #
18
+ # If a time is outside of the duration of the file, it will choose a frame at the
19
+ # 99% mark.
20
+ #
21
+ # Example:
22
+ #
23
+ # RVideo::FrameCapturer.capture! :input => 'path/to/input.mp4', :offset => '10%'
24
+ # # => ['/path/to/screenshot/input-10p.jpg']
25
+ #
26
+ # In the case where you specify an :interval, e.g. :interval => 5 for a frame every
27
+ # 5 seconds, you will generally get a few more images that you might expect.
28
+ # Typically there will be at least two extra, one for the very start and one for
29
+ # the very end of the video. Then, depending on how close to a simple integer of
30
+ # seconds the duration of the video is, you may get one or two more.
31
+ #
32
+ # # Assuming input.mp4 is 19.6 seconds long..
33
+ # RVideo::FrameCapturer.capture! :input => 'path/to/input.mp4', :interval => 5
34
+ # # => ['/path/to/input-1.jpg','/path/to/input-2.jpg','/path/to/input-3.jpg',
35
+ # '/path/to/input-4.jpg','/path/to/input-5.jpg','/path/to/input-6.jpg']
36
+ #
37
+ # For more precision, you can try multiple capture commands, each getting
38
+ # a single frame but with increasing offsets.
39
+ class FrameCapturer
40
+ attr_reader :input, :output, :offset, :rate, :limit, :inspector, :command
41
+
42
+ def self.capture!(options)
43
+ new(options).capture!
44
+ end
45
+
46
+ def initialize(options)
47
+ @ffmpeg_binary = options[:ffmpeg_binary] || "ffmpeg"
48
+
49
+ @input = options[:input] || raise(ArgumentError, "need :input => /path/to/movie")
50
+
51
+ @inspector = Inspector.new :file => @input
52
+
53
+ @offset, @rate, @limit, @output = parse_options options
54
+ @command = create_command(@input, @output, @offset)
55
+ end
56
+
57
+ def capture!
58
+ RVideo.logger.info("\nCreating Screenshot: #{@command}\n")
59
+ frame_result = do_execute("#{@command} 2>&1")
60
+ RVideo.logger.info("\nScreenshot results: #{frame_result}")
61
+
62
+ Dir[File.expand_path(@output).sub("%d", "*")].entries
63
+ end
64
+
65
+ def do_execute(command)
66
+ `#{command}`
67
+ end
68
+
69
+ VALID_TIMECODE_FORMAT = /\A([0-9.,]*)(s|f|%)?\Z/
70
+
71
+ # TODO This method should not be public, but I'm too lazy to update the specs right now..
72
+ def calculate_time(timecode)
73
+ m = VALID_TIMECODE_FORMAT.match(timecode.to_s)
74
+ if m.nil? or m[1].nil? or m[1].empty?
75
+ raise TranscoderError::ParameterError,
76
+ "Invalid timecode for frame capture: #{timecode}. " <<
77
+ "Must be a number, optionally followed by s, f, or %."
78
+ end
79
+
80
+ case m[2]
81
+ when "s", nil
82
+ t = m[1].to_f
83
+ when "f"
84
+ t = m[1].to_f / @inspector.fps.to_f
85
+ when "%"
86
+ # milliseconds / 1000 * percent / 100
87
+ t = (@inspector.duration.to_i / 1000.0) * (m[1].to_f / 100.0)
88
+ else
89
+ raise TranscoderError::ParameterError,
90
+ "Invalid timecode for frame capture: #{timecode}. " <<
91
+ "Must be a number, optionally followed by s, f, or p."
92
+ end
93
+
94
+ if (t * 1000) > @inspector.duration
95
+ calculate_time("99%")
96
+ else
97
+ t
98
+ end
99
+ end
100
+
101
+ private
102
+
103
+ def create_command(input, output, offset)
104
+ options = {:input_file => input, :output_file => output}
105
+ ffmpeg = RVideo::Tools::Ffmpeg.new("#{@ffmpeg_binary} -i $input_file$ -ss #{offset} -vframes 1 -vcodec mjpeg -y -f image2 $resolution$ $output_file$", options)
106
+ ffmpeg.command
107
+ end
108
+
109
+ def parse_options(options)
110
+ offset = options[:offset] ? calculate_time(options[:offset]) : 0
111
+ rate = options[:interval] ? (1 / options[:interval].to_f) : 1
112
+
113
+ limit = nil
114
+ # if options[:limit]
115
+ # options[:limit]
116
+ # elsif not options[:interval]
117
+ # 1
118
+ # end
119
+
120
+ output = if options[:output]
121
+ options[:output]
122
+ else
123
+ path = File.dirname File.expand_path(options[:input])
124
+
125
+ name = File.basename(options[:input], ".*")
126
+ if options[:interval]
127
+ name << "-%d"
128
+ else
129
+ name << "-#{offset}"
130
+ end
131
+ name << ".jpg"
132
+
133
+ File.join path, name
134
+ end
135
+
136
+ [offset, rate, limit, output]
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,519 @@
1
+ module RVideo # :nodoc:
2
+ # To inspect a video or audio file, initialize an Inspector object.
3
+ #
4
+ # file = RVideo::Inspector.new(options_hash)
5
+ #
6
+ # Inspector accepts three options: file, raw_response, and ffmpeg_binary.
7
+ # Either raw_response or file is required; ffmpeg binary is optional.
8
+ #
9
+ # :file is a path to a file to be inspected.
10
+ #
11
+ # :raw_response is the full output of "ffmpeg -i [file]". If the
12
+ # :raw_response option is used, RVideo will not actually inspect a file;
13
+ # it will simply parse the provided response. This is useful if your
14
+ # application has already collected the ffmpeg -i response, and you don't
15
+ # want to call it again.
16
+ #
17
+ # :ffmpeg_binary is an optional argument that specifies the path to the
18
+ # ffmpeg binary to be used. If a path is not explicitly declared, RVideo
19
+ # will assume that ffmpeg exists in the Unix path. Type "which ffmpeg" to
20
+ # check if ffmpeg is installed and exists in your operating system's path.
21
+ class Inspector
22
+ attr_reader :filename, :path, :full_filename, :raw_response, :raw_metadata
23
+
24
+ attr_accessor :ffmpeg_binary
25
+
26
+ def initialize(options = {})
27
+ if not (options[:raw_response] or options[:file])
28
+ raise ArgumentError, "Must supply either an input file or a pregenerated response"
29
+ end
30
+
31
+ if options[:raw_response]
32
+ initialize_with_raw_response(options[:raw_response])
33
+ elsif options[:file]
34
+ initialize_with_file(options[:file], options[:ffmpeg_binary])
35
+ end
36
+
37
+ metadata = /(Input \#.*)\n(Must|At\sleast)/m.match(@raw_response)
38
+ # metadata = /(Input \#.*)\n.+\n\Z/m.match(@raw_response)
39
+
40
+ if /Unknown format/i.match(@raw_response) || metadata.nil?
41
+ @unknown_format = true
42
+ elsif /Duration: N\/A/im.match(@raw_response)
43
+ # in this case, we can at least still get the container type
44
+ @unreadable_file = true
45
+ @raw_metadata = metadata[1]
46
+ else
47
+ @raw_metadata = metadata[1]
48
+ end
49
+ end
50
+
51
+ def initialize_with_raw_response(raw_response)
52
+ @raw_response = raw_response
53
+ end
54
+
55
+ def initialize_with_file(file, ffmpeg_binary = nil)
56
+ if ffmpeg_binary
57
+ @ffmpeg_binary = ffmpeg_binary
58
+ if not FileTest.exist?(@ffmpeg_binary)
59
+ raise "ffmpeg could not be found (trying #{@ffmpeg_binary})"
60
+ end
61
+ else
62
+ # assume it is in the unix path
63
+ if not FileTest.exist?(`which ffmpeg`.chomp)
64
+ raise "ffmpeg could not be found (expected ffmpeg to be found in the Unix path)"
65
+ end
66
+ @ffmpeg_binary = "ffmpeg"
67
+ end
68
+
69
+ if not FileTest.exist?(file.gsub('"',''))
70
+ raise TranscoderError::InputFileNotFound, "File not found (#{file})"
71
+ end
72
+
73
+ @full_filename = file
74
+ @filename = File.basename(@full_filename)
75
+ @path = File.dirname(@full_filename)
76
+
77
+ @raw_response = `#{@ffmpeg_binary} -i #{@full_filename.shell_quoted} 2>&1`
78
+ end
79
+
80
+ # Returns true if the file can be read successfully. Returns false otherwise.
81
+ def valid?
82
+ not (@unknown_format or @unreadable_file)
83
+ end
84
+
85
+ # Returns false if the file can be read successfully. Returns false otherwise.
86
+ def invalid?
87
+ not valid?
88
+ end
89
+
90
+ # True if the format is not understood ("Unknown Format")
91
+ def unknown_format?
92
+ @unknown_format ? true : false
93
+ end
94
+
95
+ # True if the file is not readable ("Duration: N/A, bitrate: N/A")
96
+ def unreadable_file?
97
+ @unreadable_file ? true : false
98
+ end
99
+
100
+ # Does the file have an audio stream?
101
+ def audio?
102
+ not audio_match.nil?
103
+ end
104
+
105
+ # Does the file have a video stream?
106
+ def video?
107
+ not video_match.nil?
108
+ end
109
+
110
+ # Returns the version of ffmpeg used, In practice, this may or may not be
111
+ # useful.
112
+ #
113
+ # Examples:
114
+ #
115
+ # SVN-r6399
116
+ # CVS
117
+ #
118
+ def ffmpeg_version
119
+ @ffmpeg_version = @raw_response.split("\n").first.split("version").last.split(",").first.strip
120
+ end
121
+
122
+ # Returns the configuration options used to build ffmpeg.
123
+ #
124
+ # Example:
125
+ #
126
+ # --enable-mp3lame --enable-gpl --disable-ffplay --disable-ffserver
127
+ # --enable-a52 --enable-xvid
128
+ #
129
+ def ffmpeg_configuration
130
+ /(\s*configuration:)(.*)\n/.match(@raw_response)[2].strip
131
+ end
132
+
133
+ # Returns the versions of libavutil, libavcodec, and libavformat used by
134
+ # ffmpeg.
135
+ #
136
+ # Example:
137
+ #
138
+ # libavutil version: 49.0.0
139
+ # libavcodec version: 51.9.0
140
+ # libavformat version: 50.4.0
141
+ #
142
+ def ffmpeg_libav
143
+ /^(\s*lib.*\n)+/.match(@raw_response)[0].split("\n").each {|l| l.strip! }
144
+ end
145
+
146
+ # Returns the build description for ffmpeg.
147
+ #
148
+ # Example:
149
+ #
150
+ # built on Apr 15 2006 04:58:19, gcc: 4.0.1 (Apple Computer, Inc. build
151
+ # 5250)
152
+ #
153
+ def ffmpeg_build
154
+ /(\n\s*)(built on.*)(\n)/.match(@raw_response)[2]
155
+ end
156
+
157
+ # Returns the container format for the file. Instead of returning a single
158
+ # format, this may return a string of related formats.
159
+ #
160
+ # Examples:
161
+ #
162
+ # "avi"
163
+ #
164
+ # "mov,mp4,m4a,3gp,3g2,mj2"
165
+ #
166
+ def container
167
+ return nil if @unknown_format
168
+ /Input \#\d+\,\s*(\S+),\s*from/.match(@raw_metadata)[1]
169
+ end
170
+
171
+ # The duration of the movie, as a string.
172
+ #
173
+ # Example:
174
+ #
175
+ # "00:00:24.4" # 24.4 seconds
176
+ #
177
+ def raw_duration
178
+ return nil unless valid?
179
+ /Duration:\s*([0-9\:\.]+),/.match(@raw_metadata)[1]
180
+ end
181
+
182
+ # The duration of the movie in milliseconds, as an integer.
183
+ #
184
+ # Example:
185
+ #
186
+ # 24400 # 24.4 seconds
187
+ #
188
+ # Note that the precision of the duration is in tenths of a second, not
189
+ # thousandths, but milliseconds are a more standard unit of time than
190
+ # deciseconds.
191
+ #
192
+ def duration
193
+ return nil unless valid?
194
+
195
+ units = raw_duration.split(":")
196
+ (units[0].to_i * 60 * 60 * 1000) + (units[1].to_i * 60 * 1000) + (units[2].to_f * 1000).to_i
197
+ end
198
+
199
+ def ratio
200
+ return nil unless valid?
201
+ width.to_f / height.to_f
202
+ end
203
+
204
+ # The bitrate of the movie.
205
+ #
206
+ # Example:
207
+ #
208
+ # 3132
209
+ #
210
+ def bitrate
211
+ return nil unless valid?
212
+ bitrate_match[1].to_i
213
+ end
214
+
215
+ # The bitrate units used. In practice, this may always be kb/s.
216
+ #
217
+ # Example:
218
+ #
219
+ # "kb/s"
220
+ #
221
+ def bitrate_units
222
+ return nil unless valid?
223
+ bitrate_match[2]
224
+ end
225
+
226
+ def bitrate_with_units
227
+ "#{bitrate} #{bitrate_units}"
228
+ end
229
+
230
+ def audio_bit_rate
231
+ return nil unless audio?
232
+ audio_match[7].to_i
233
+ end
234
+
235
+ def audio_bit_rate_units
236
+ return nil unless audio?
237
+ audio_match[8]
238
+ end
239
+
240
+ def audio_bit_rate_with_units
241
+ "#{audio_bit_rate} #{audio_bit_rate_units}"
242
+ end
243
+
244
+ def audio_stream
245
+ return nil unless valid?
246
+
247
+ match = /\n\s*Stream.*Audio:.*\n/.match(@raw_response)
248
+ match[0].strip if match
249
+ end
250
+
251
+ # The audio codec used.
252
+ #
253
+ # Example:
254
+ #
255
+ # "aac"
256
+ #
257
+ def audio_codec
258
+ return nil unless audio?
259
+ audio_match[2]
260
+ end
261
+
262
+ # The sampling rate of the audio stream.
263
+ #
264
+ # Example:
265
+ #
266
+ # 44100
267
+ #
268
+ def audio_sample_rate
269
+ return nil unless audio?
270
+ audio_match[3].to_i
271
+ end
272
+
273
+ # The units used for the sampling rate. May always be Hz.
274
+ #
275
+ # Example:
276
+ #
277
+ # "Hz"
278
+ #
279
+ def audio_sample_rate_units
280
+ return nil unless audio?
281
+ audio_match[4]
282
+ end
283
+ alias_method :audio_sample_units, :audio_sample_rate_units
284
+
285
+ def audio_sample_rate_with_units
286
+ "#{audio_sample_rate} #{audio_sample_rate_units}"
287
+ end
288
+
289
+ # The channels used in the audio stream.
290
+ #
291
+ # Examples:
292
+ # "stereo"
293
+ # "mono"
294
+ # "5:1"
295
+ #
296
+ def audio_channels_string
297
+ return nil unless audio?
298
+ audio_match[5]
299
+ end
300
+
301
+ def audio_channels
302
+ return nil unless audio?
303
+
304
+ case audio_match[5]
305
+ when "mono" then 1
306
+ when "stereo" then 2
307
+ when /(\d+) channels/ then $1.to_i
308
+ when /^(\d).(\d)$/ then $1.to_i+$2.to_i
309
+ else
310
+ raise RuntimeError, "Unknown number of channels"
311
+ end
312
+ end
313
+
314
+ # This should almost always return 16,
315
+ # as the vast majority of audio is 16 bit.
316
+ def audio_sample_bit_depth
317
+ return nil unless audio?
318
+ audio_match[6].to_i
319
+ end
320
+
321
+ # The ID of the audio stream (useful for troubleshooting).
322
+ #
323
+ # Example:
324
+ # #0.1
325
+ #
326
+ def audio_stream_id
327
+ return nil unless audio?
328
+ audio_match[1]
329
+ end
330
+
331
+ def video_stream
332
+ return nil unless valid?
333
+
334
+ match = /\n\s*Stream.*Video:.*\n/.match(@raw_response)
335
+ match[0].strip unless match.nil?
336
+ end
337
+
338
+ # The ID of the video stream (useful for troubleshooting).
339
+ #
340
+ # Example:
341
+ # #0.0
342
+ #
343
+ def video_stream_id
344
+ return nil unless video?
345
+ video_match[1]
346
+ end
347
+
348
+ # The video codec used.
349
+ #
350
+ # Example:
351
+ #
352
+ # "mpeg4"
353
+ #
354
+ def video_codec
355
+ return nil unless video?
356
+ video_match[3]
357
+ end
358
+
359
+ # The colorspace of the video stream.
360
+ #
361
+ # Example:
362
+ #
363
+ # "yuv420p"
364
+ #
365
+ def video_colorspace
366
+ return nil unless video?
367
+ video_match[4]
368
+ end
369
+
370
+ # The width of the video in pixels.
371
+ def width
372
+ return nil unless video?
373
+ if aspect_rotated?
374
+ video_match[6].to_i
375
+ else
376
+ video_match[5].to_i
377
+ end
378
+ end
379
+
380
+ # The height of the video in pixels.
381
+ def height
382
+ return nil unless video?
383
+ if aspect_rotated?
384
+ video_match[5].to_i
385
+ else
386
+ video_match[6].to_i
387
+ end
388
+ end
389
+
390
+ # width x height, as a string.
391
+ #
392
+ # Examples:
393
+ # 320x240
394
+ # 1280x720
395
+ #
396
+ def resolution
397
+ return nil unless video?
398
+ "#{width}x#{height}"
399
+ end
400
+
401
+ def video_orientation
402
+ stdout=''
403
+ stderr=''
404
+ open4.spawn "qtrotate #{full_filename}", :stdout=> stdout, :timeout => 10, :stderr => stderr
405
+ @orientation ||= stdout.chomp.to_i
406
+ rescue Timeout::Error
407
+ 0
408
+ rescue
409
+ 0
410
+ end
411
+
412
+ def rotated?
413
+ video_orientation != 0
414
+ end
415
+
416
+ def aspect_rotated?
417
+ video_orientation % 180 == 90
418
+ end
419
+
420
+ def pixel_aspect_ratio
421
+ return nil unless video?
422
+ video_match[7]
423
+ end
424
+
425
+ def display_aspect_ratio
426
+ return nil unless video?
427
+ video_match[8]
428
+ end
429
+
430
+ # The portion of the overall bitrate the video is responsible for.
431
+ def video_bit_rate
432
+ return nil unless video?
433
+ video_match[9]
434
+ end
435
+
436
+ def video_bit_rate_units
437
+ return nil unless video?
438
+ video_match[10]
439
+ end
440
+
441
+ # The frame rate of the video in frames per second
442
+ #
443
+ # Example:
444
+ #
445
+ # "29.97"
446
+ #
447
+ def fps
448
+ return nil unless video?
449
+ video_match[2] || video_match[13] || video_match[14]
450
+ end
451
+ alias_method :framerate, :fps
452
+
453
+ def time_base
454
+ return nil unless video?
455
+ video_match[14]
456
+ end
457
+
458
+ def codec_time_base
459
+ return nil unless video?
460
+ video_match[15]
461
+ end
462
+
463
+ private
464
+
465
+ def bitrate_match
466
+ /bitrate: ([0-9\.]+)\s*(.*)\s+/.match(@raw_metadata)
467
+ end
468
+
469
+ ###
470
+ # I am wondering how reliable it would be to simplify a lot
471
+ # of this regexp parsery by using split(/\s*,\s*/) - Seth
472
+
473
+ SEP = '(?:,\s*)'
474
+ VAL = '([^,]+)'
475
+
476
+ RATE = '([\d.]+k?)'
477
+ PAR_DAR = '(?:\s*\[?(?:PAR\s*(\d+:\d+))?\s*(?:DAR\s*(\d+:\d+))?\]?)?'
478
+
479
+ AUDIO_MATCH_PATTERN = /
480
+ Stream\s+(.*?)[,:\(\[].*?\s*
481
+ Audio:\s+
482
+ #{VAL}#{SEP} # codec
483
+ #{RATE}\s+(\w*)#{SEP}? # sample rate
484
+ #{VAL}#{SEP}? # channels
485
+ (?:s(\d+)#{SEP}?)? # audio sample bit depth
486
+ (?:(\d+)\s+(\S+))? # audio bit rate
487
+ /x
488
+
489
+ def audio_match
490
+ return nil unless valid?
491
+ AUDIO_MATCH_PATTERN.match(audio_stream)
492
+ end
493
+
494
+ FPS = 'fps(?:\(r\))?'
495
+
496
+ VIDEO_MATCH_PATTERN = /
497
+ Stream\s*(\#[\d.]+)(?:[\(\[].+?[\)\]])?\s* # stream id
498
+ [,:]\s*
499
+ (?:#{RATE}\s*#{FPS}[,:]\s*)? # frame rate, older builds
500
+ Video:\s*
501
+ #{VAL}#{SEP} # codec
502
+ (?:#{VAL}#{SEP})? # color space
503
+ (\d+)x(\d+) # resolution
504
+ #{PAR_DAR} # pixel and display aspect ratios
505
+ #{SEP}?
506
+ (?:#{RATE}\s*(kb\/s)#{SEP}?)? # video bit rate
507
+ #{PAR_DAR}#{SEP}? # pixel and display aspect ratios
508
+ (?:#{RATE}\s*#{FPS}#{SEP}?)? # frame rate
509
+ (?:#{RATE}\s*tbr#{SEP}?)? # time base
510
+ (?:#{RATE}\s*tbn#{SEP}?)? # time base
511
+ (?:#{RATE}\s*tbc#{SEP}?)? # codec time base
512
+ /x
513
+
514
+ def video_match
515
+ return nil unless valid?
516
+ VIDEO_MATCH_PATTERN.match(video_stream)
517
+ end
518
+ end
519
+ end