twinge-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.
Files changed (68) 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 +67 -0
  6. data/README +91 -0
  7. data/RULES +11 -0
  8. data/Rakefile +63 -0
  9. data/config/boot.rb +25 -0
  10. data/lib/rvideo.rb +44 -0
  11. data/lib/rvideo/errors.rb +24 -0
  12. data/lib/rvideo/float.rb +7 -0
  13. data/lib/rvideo/frame_capturer.rb +126 -0
  14. data/lib/rvideo/inspector.rb +481 -0
  15. data/lib/rvideo/reporter.rb +176 -0
  16. data/lib/rvideo/reporter/views/index.html.erb +27 -0
  17. data/lib/rvideo/reporter/views/report.css +27 -0
  18. data/lib/rvideo/reporter/views/report.html.erb +81 -0
  19. data/lib/rvideo/reporter/views/report.js +9 -0
  20. data/lib/rvideo/string.rb +5 -0
  21. data/lib/rvideo/tools/abstract_tool.rb +401 -0
  22. data/lib/rvideo/tools/ffmpeg.rb +277 -0
  23. data/lib/rvideo/tools/ffmpeg2theora.rb +42 -0
  24. data/lib/rvideo/tools/flvtool2.rb +50 -0
  25. data/lib/rvideo/tools/mencoder.rb +103 -0
  26. data/lib/rvideo/tools/mp4box.rb +21 -0
  27. data/lib/rvideo/tools/mp4creator.rb +35 -0
  28. data/lib/rvideo/tools/mplayer.rb +31 -0
  29. data/lib/rvideo/tools/qtfaststart.rb +37 -0
  30. data/lib/rvideo/tools/yamdi.rb +44 -0
  31. data/lib/rvideo/transcoder.rb +120 -0
  32. data/lib/rvideo/version.rb +9 -0
  33. data/rvideo.gemspec +37 -0
  34. data/scripts/txt2html +67 -0
  35. data/setup.rb +1585 -0
  36. data/spec/files/boat.avi +0 -0
  37. data/spec/files/kites.mp4 +0 -0
  38. data/spec/fixtures/ffmpeg_builds.yml +28 -0
  39. data/spec/fixtures/ffmpeg_results.yml +608 -0
  40. data/spec/fixtures/files.yml +398 -0
  41. data/spec/fixtures/recipes.yml +58 -0
  42. data/spec/integrations/formats_spec.rb +315 -0
  43. data/spec/integrations/frame_capturer_spec.rb +26 -0
  44. data/spec/integrations/inspection_spec.rb +112 -0
  45. data/spec/integrations/recipes_spec.rb +0 -0
  46. data/spec/integrations/rvideo_spec.rb +17 -0
  47. data/spec/integrations/transcoder_integration_spec.rb +29 -0
  48. data/spec/integrations/transcoding_spec.rb +9 -0
  49. data/spec/spec.opts +1 -0
  50. data/spec/spec_helper.rb +16 -0
  51. data/spec/support.rb +36 -0
  52. data/spec/units/abstract_tool_spec.rb +111 -0
  53. data/spec/units/ffmpeg_spec.rb +323 -0
  54. data/spec/units/flvtool2_spec.rb +324 -0
  55. data/spec/units/frame_capturer_spec.rb +72 -0
  56. data/spec/units/inspector_spec.rb +59 -0
  57. data/spec/units/mencoder_spec.rb +4994 -0
  58. data/spec/units/mp4box_spec.rb +34 -0
  59. data/spec/units/mp4creator_spec.rb +34 -0
  60. data/spec/units/mplayer_spec.rb +34 -0
  61. data/spec/units/qtfaststart_spec.rb +35 -0
  62. data/spec/units/string_spec.rb +8 -0
  63. data/spec/units/transcoder_spec.rb +156 -0
  64. data/tasks/deployment.rake +5 -0
  65. data/tasks/testing.rake +27 -0
  66. data/tasks/transcoding.rake +40 -0
  67. data/tasks/website.rake +8 -0
  68. metadata +175 -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
+
data/Rakefile ADDED
@@ -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}"
data/config/boot.rb ADDED
@@ -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
data/lib/rvideo.rb ADDED
@@ -0,0 +1,44 @@
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
+
11
+ # rvideo
12
+ require 'rvideo/inspector'
13
+ require 'rvideo/frame_capturer'
14
+ require 'rvideo/errors'
15
+ require 'rvideo/transcoder'
16
+ require 'rvideo/tools/abstract_tool'
17
+ require 'rvideo/tools/ffmpeg'
18
+ require 'rvideo/tools/mencoder'
19
+ require 'rvideo/tools/flvtool2'
20
+ require 'rvideo/tools/mp4box'
21
+ require 'rvideo/tools/mplayer'
22
+ require 'rvideo/tools/mp4creator'
23
+ require 'rvideo/tools/ffmpeg2theora'
24
+ require 'rvideo/tools/yamdi'
25
+ require 'rvideo/tools/qtfaststart'
26
+
27
+ TEMP_PATH = File.expand_path(File.dirname(__FILE__) + '/../tmp')
28
+ REPORT_PATH = File.expand_path(File.dirname(__FILE__) + '/../report')
29
+
30
+ module RVideo
31
+ # Configure logging. Assumes that the logger object has an
32
+ # interface similar to stdlib's Logger class.
33
+ #
34
+ # RVideo.logger = Logger.new(STDOUT)
35
+ #
36
+ def self.logger=(logger)
37
+ @logger = logger
38
+ end
39
+
40
+ def self.logger
41
+ @logger = Logger.new("/dev/null") unless @logger
42
+ @logger
43
+ end
44
+ 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,126 @@
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
+ @input = options[:input] || raise(ArgumentError, "need :input => /path/to/movie")
48
+
49
+ @inspector = Inspector.new :file => @input
50
+
51
+ @offset, @rate, @limit, @output = parse_options options
52
+
53
+ @command = "ffmpeg -i #{@input.shell_quoted} -ss #{@offset} -r #{@rate} #{@output.shell_quoted} -vframes #{@limit}"
54
+ end
55
+
56
+ def capture!
57
+ RVideo.logger.info("\nCreating Screenshot: #{@command}\n")
58
+ frame_result = `#{@command} 2>&1`
59
+ RVideo.logger.info("\nScreenshot results: #{frame_result}")
60
+
61
+ Dir[File.expand_path(@output).sub("%d", "*")].entries
62
+ end
63
+
64
+ VALID_TIMECODE_FORMAT = /\A([0-9.,]*)(s|f|%)?\Z/
65
+
66
+ # TODO This method should not be public, but I'm too lazy to update the specs right now..
67
+ def calculate_time(timecode)
68
+ m = VALID_TIMECODE_FORMAT.match(timecode.to_s)
69
+ if m.nil? or m[1].nil? or m[1].empty?
70
+ raise TranscoderError::ParameterError,
71
+ "Invalid timecode for frame capture: #{timecode}. " <<
72
+ "Must be a number, optionally followed by s, f, or %."
73
+ end
74
+
75
+ case m[2]
76
+ when "s", nil
77
+ t = m[1].to_f
78
+ when "f"
79
+ t = m[1].to_f / @inspector.fps.to_f
80
+ when "%"
81
+ # milliseconds / 1000 * percent / 100
82
+ t = (@inspector.duration.to_i / 1000.0) * (m[1].to_f / 100.0)
83
+ else
84
+ raise TranscoderError::ParameterError,
85
+ "Invalid timecode for frame capture: #{timecode}. " <<
86
+ "Must be a number, optionally followed by s, f, or p."
87
+ end
88
+
89
+ if (t * 1000) > @inspector.duration
90
+ calculate_time("99%")
91
+ else
92
+ t
93
+ end
94
+ end
95
+
96
+ private
97
+ def parse_options(options)
98
+ offset = options[:offset] ? calculate_time(options[:offset]) : 0
99
+ rate = options[:interval] ? (1 / options[:interval].to_f) : 1
100
+
101
+ limit = if options[:limit]
102
+ options[:limit]
103
+ elsif not options[:interval]
104
+ 1
105
+ end
106
+
107
+ output = if options[:output]
108
+ options[:output]
109
+ else
110
+ path = File.dirname File.expand_path(options[:input])
111
+
112
+ name = File.basename(options[:input], ".*")
113
+ if options[:interval]
114
+ name << "-%d"
115
+ else
116
+ name << "-#{offset}"
117
+ end
118
+ name << ".jpg"
119
+
120
+ File.join path, name
121
+ end
122
+
123
+ [offset, rate, limit, output]
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,481 @@
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.+\n\Z/m.match(@raw_response)
38
+
39
+ if /Unknown format/i.match(@raw_response) || metadata.nil?
40
+ @unknown_format = true
41
+ elsif /Duration: N\/A/im.match(@raw_response)
42
+ # in this case, we can at least still get the container type
43
+ @unreadable_file = true
44
+ @raw_metadata = metadata[1]
45
+ else
46
+ @raw_metadata = metadata[1]
47
+ end
48
+ end
49
+
50
+ def initialize_with_raw_response(raw_response)
51
+ @raw_response = raw_response
52
+ end
53
+
54
+ def initialize_with_file(file, ffmpeg_binary = nil)
55
+ if ffmpeg_binary
56
+ @ffmpeg_binary = ffmpeg_binary
57
+ if not FileTest.exist?(@ffmpeg_binary)
58
+ raise "ffmpeg could not be found (trying #{@ffmpeg_binary})"
59
+ end
60
+ else
61
+ # assume it is in the unix path
62
+ if not FileTest.exist?(`which ffmpeg`.chomp)
63
+ raise "ffmpeg could not be found (expected ffmpeg to be found in the Unix path)"
64
+ end
65
+ @ffmpeg_binary = "ffmpeg"
66
+ end
67
+
68
+ if not FileTest.exist?(file.gsub('"',''))
69
+ raise TranscoderError::InputFileNotFound, "File not found (#{file})"
70
+ end
71
+
72
+ @full_filename = file
73
+ @filename = File.basename(@full_filename)
74
+ @path = File.dirname(@full_filename)
75
+
76
+ @raw_response = `#{@ffmpeg_binary} -i #{@full_filename.shell_quoted} 2>&1`
77
+ end
78
+
79
+ # Returns true if the file can be read successfully. Returns false otherwise.
80
+ def valid?
81
+ not (@unknown_format or @unreadable_file)
82
+ end
83
+
84
+ # Returns false if the file can be read successfully. Returns false otherwise.
85
+ def invalid?
86
+ not valid?
87
+ end
88
+
89
+ # True if the format is not understood ("Unknown Format")
90
+ def unknown_format?
91
+ @unknown_format ? true : false
92
+ end
93
+
94
+ # True if the file is not readable ("Duration: N/A, bitrate: N/A")
95
+ def unreadable_file?
96
+ @unreadable_file ? true : false
97
+ end
98
+
99
+ # Does the file have an audio stream?
100
+ def audio?
101
+ not audio_match.nil?
102
+ end
103
+
104
+ # Does the file have a video stream?
105
+ def video?
106
+ not video_match.nil?
107
+ end
108
+
109
+ # Returns the version of ffmpeg used, In practice, this may or may not be
110
+ # useful.
111
+ #
112
+ # Examples:
113
+ #
114
+ # SVN-r6399
115
+ # CVS
116
+ #
117
+ def ffmpeg_version
118
+ @ffmpeg_version = @raw_response.split("\n").first.split("version").last.split(",").first.strip
119
+ end
120
+
121
+ # Returns the configuration options used to build ffmpeg.
122
+ #
123
+ # Example:
124
+ #
125
+ # --enable-mp3lame --enable-gpl --disable-ffplay --disable-ffserver
126
+ # --enable-a52 --enable-xvid
127
+ #
128
+ def ffmpeg_configuration
129
+ /(\s*configuration:)(.*)\n/.match(@raw_response)[2].strip
130
+ end
131
+
132
+ # Returns the versions of libavutil, libavcodec, and libavformat used by
133
+ # ffmpeg.
134
+ #
135
+ # Example:
136
+ #
137
+ # libavutil version: 49.0.0
138
+ # libavcodec version: 51.9.0
139
+ # libavformat version: 50.4.0
140
+ #
141
+ def ffmpeg_libav
142
+ /^(\s*lib.*\n)+/.match(@raw_response)[0].split("\n").each {|l| l.strip! }
143
+ end
144
+
145
+ # Returns the build description for ffmpeg.
146
+ #
147
+ # Example:
148
+ #
149
+ # built on Apr 15 2006 04:58:19, gcc: 4.0.1 (Apple Computer, Inc. build
150
+ # 5250)
151
+ #
152
+ def ffmpeg_build
153
+ /(\n\s*)(built on.*)(\n)/.match(@raw_response)[2]
154
+ end
155
+
156
+ # Returns the container format for the file. Instead of returning a single
157
+ # format, this may return a string of related formats.
158
+ #
159
+ # Examples:
160
+ #
161
+ # "avi"
162
+ #
163
+ # "mov,mp4,m4a,3gp,3g2,mj2"
164
+ #
165
+ def container
166
+ return nil if @unknown_format
167
+ /Input \#\d+\,\s*(\S+),\s*from/.match(@raw_metadata)[1]
168
+ end
169
+
170
+ # The duration of the movie, as a string.
171
+ #
172
+ # Example:
173
+ #
174
+ # "00:00:24.4" # 24.4 seconds
175
+ #
176
+ def raw_duration
177
+ return nil unless valid?
178
+ /Duration:\s*([0-9\:\.]+),/.match(@raw_metadata)[1]
179
+ end
180
+
181
+ # The duration of the movie in milliseconds, as an integer.
182
+ #
183
+ # Example:
184
+ #
185
+ # 24400 # 24.4 seconds
186
+ #
187
+ # Note that the precision of the duration is in tenths of a second, not
188
+ # thousandths, but milliseconds are a more standard unit of time than
189
+ # deciseconds.
190
+ #
191
+ def duration
192
+ return nil unless valid?
193
+
194
+ units = raw_duration.split(":")
195
+ (units[0].to_i * 60 * 60 * 1000) + (units[1].to_i * 60 * 1000) + (units[2].to_f * 1000).to_i
196
+ end
197
+
198
+ # The bitrate of the movie.
199
+ #
200
+ # Example:
201
+ #
202
+ # 3132
203
+ #
204
+ def bitrate
205
+ return nil unless valid?
206
+ bitrate_match[1].to_i
207
+ end
208
+
209
+ # The bitrate units used. In practice, this may always be kb/s.
210
+ #
211
+ # Example:
212
+ #
213
+ # "kb/s"
214
+ #
215
+ def bitrate_units
216
+ return nil unless valid?
217
+ bitrate_match[2]
218
+ end
219
+
220
+ def bitrate_with_units
221
+ "#{bitrate} #{bitrate_units}"
222
+ end
223
+
224
+ def audio_bit_rate
225
+ return nil unless audio?
226
+ audio_match[7].to_i
227
+ end
228
+
229
+ def audio_bit_rate_units
230
+ return nil unless audio?
231
+ audio_match[8]
232
+ end
233
+
234
+ def audio_bit_rate_with_units
235
+ "#{audio_bit_rate} #{audio_bit_rate_units}"
236
+ end
237
+
238
+ def audio_stream
239
+ return nil unless valid?
240
+
241
+ match = /\n\s*Stream.*Audio:.*\n/.match(@raw_response)
242
+ match[0].strip if match
243
+ end
244
+
245
+ # The audio codec used.
246
+ #
247
+ # Example:
248
+ #
249
+ # "aac"
250
+ #
251
+ def audio_codec
252
+ return nil unless audio?
253
+ audio_match[2]
254
+ end
255
+
256
+ # The sampling rate of the audio stream.
257
+ #
258
+ # Example:
259
+ #
260
+ # 44100
261
+ #
262
+ def audio_sample_rate
263
+ return nil unless audio?
264
+ audio_match[3].to_i
265
+ end
266
+
267
+ # The units used for the sampling rate. May always be Hz.
268
+ #
269
+ # Example:
270
+ #
271
+ # "Hz"
272
+ #
273
+ def audio_sample_rate_units
274
+ return nil unless audio?
275
+ audio_match[4]
276
+ end
277
+ alias_method :audio_sample_units, :audio_sample_rate_units
278
+
279
+ def audio_sample_rate_with_units
280
+ "#{audio_sample_rate} #{audio_sample_rate_units}"
281
+ end
282
+
283
+ # The channels used in the audio stream.
284
+ #
285
+ # Examples:
286
+ # "stereo"
287
+ # "mono"
288
+ # "5:1"
289
+ #
290
+ def audio_channels_string
291
+ return nil unless audio?
292
+ audio_match[5]
293
+ end
294
+
295
+ def audio_channels
296
+ return nil unless audio?
297
+
298
+ case audio_match[5]
299
+ when "mono" then 1
300
+ when "stereo" then 2
301
+ else
302
+ raise RuntimeError, "Unknown number of channels: #{audio_channels}"
303
+ end
304
+ end
305
+
306
+ # This should almost always return 16,
307
+ # as the vast majority of audio is 16 bit.
308
+ def audio_sample_bit_depth
309
+ return nil unless audio?
310
+ audio_match[6].to_i
311
+ end
312
+
313
+ # The ID of the audio stream (useful for troubleshooting).
314
+ #
315
+ # Example:
316
+ # #0.1
317
+ #
318
+ def audio_stream_id
319
+ return nil unless audio?
320
+ audio_match[1]
321
+ end
322
+
323
+ def video_stream
324
+ return nil unless valid?
325
+
326
+ match = /\n\s*Stream.*Video:.*\n/.match(@raw_response)
327
+ match[0].strip unless match.nil?
328
+ end
329
+
330
+ # The ID of the video stream (useful for troubleshooting).
331
+ #
332
+ # Example:
333
+ # #0.0
334
+ #
335
+ def video_stream_id
336
+ return nil unless video?
337
+ video_match[1]
338
+ end
339
+
340
+ # The video codec used.
341
+ #
342
+ # Example:
343
+ #
344
+ # "mpeg4"
345
+ #
346
+ def video_codec
347
+ return nil unless video?
348
+ video_match[3]
349
+ end
350
+
351
+ # The colorspace of the video stream.
352
+ #
353
+ # Example:
354
+ #
355
+ # "yuv420p"
356
+ #
357
+ def video_colorspace
358
+ return nil unless video?
359
+ video_match[4]
360
+ end
361
+
362
+ # The width of the video in pixels.
363
+ def width
364
+ return nil unless video?
365
+ video_match[5].to_i
366
+ end
367
+
368
+ # The height of the video in pixels.
369
+ def height
370
+ return nil unless video?
371
+ video_match[6].to_i
372
+ end
373
+
374
+ # width x height, as a string.
375
+ #
376
+ # Examples:
377
+ # 320x240
378
+ # 1280x720
379
+ #
380
+ def resolution
381
+ return nil unless video?
382
+ "#{width}x#{height}"
383
+ end
384
+
385
+ def pixel_aspect_ratio
386
+ return nil unless video?
387
+ video_match[7]
388
+ end
389
+
390
+ def display_aspect_ratio
391
+ return nil unless video?
392
+ video_match[8]
393
+ end
394
+
395
+ # The portion of the overall bitrate the video is responsible for.
396
+ def video_bit_rate
397
+ return nil unless video?
398
+ video_match[9]
399
+ end
400
+
401
+ def video_bit_rate_units
402
+ return nil unless video?
403
+ video_match[10]
404
+ end
405
+
406
+ # The frame rate of the video in frames per second
407
+ #
408
+ # Example:
409
+ #
410
+ # "29.97"
411
+ #
412
+ def fps
413
+ return nil unless video?
414
+ video_match[2] or video_match[11]
415
+ end
416
+ alias_method :framerate, :fps
417
+
418
+ def time_base
419
+ return nil unless video?
420
+ video_match[12]
421
+ end
422
+
423
+ def codec_time_base
424
+ return nil unless video?
425
+ video_match[13]
426
+ end
427
+
428
+ private
429
+
430
+ def bitrate_match
431
+ /bitrate: ([0-9\.]+)\s*(.*)\s+/.match(@raw_metadata)
432
+ end
433
+
434
+ ###
435
+ # I am wondering how reliable it would be to simplify a lot
436
+ # of this regexp parsery by using split(/\s*,\s*/) - Seth
437
+
438
+ SEP = '(?:,\s*)'
439
+ VAL = '([^,]+)'
440
+
441
+ RATE = '([\d.]+k?)'
442
+
443
+ AUDIO_MATCH_PATTERN = /
444
+ Stream\s+(.*?)[,:\(\[].*?\s*
445
+ Audio:\s+
446
+ #{VAL}#{SEP} # codec
447
+ #{RATE}\s+(\w*)#{SEP}? # sample rate
448
+ ([a-zA-Z:]*)#{SEP}? # channels
449
+ (?:s(\d+)#{SEP}?)? # audio sample bit depth
450
+ (?:(\d+)\s+(\S+))? # audio bit rate
451
+ /x
452
+
453
+ def audio_match
454
+ return nil unless valid?
455
+ AUDIO_MATCH_PATTERN.match(audio_stream)
456
+ end
457
+
458
+ FPS = 'fps(?:\(r\))?'
459
+
460
+ VIDEO_MATCH_PATTERN = /
461
+ Stream\s*(\#[\d.]+)(?:[\(\[].+?[\)\]])?\s* # stream id
462
+ [,:]\s*
463
+ (?:#{RATE}\s*#{FPS}[,:]\s*)? # frame rate, older builds
464
+ Video:\s*
465
+ #{VAL}#{SEP} # codec
466
+ (?:#{VAL}#{SEP})? # color space
467
+ (\d+)x(\d+) # resolution
468
+ (?:\s*\[?(?:PAR\s*(\d+:\d+))?\s*(?:DAR\s*(\d+:\d+))?\]?)? # pixel and display aspect ratios
469
+ #{SEP}?
470
+ (?:#{RATE}\s*(kb\/s)#{SEP}?)? # video bit rate
471
+ (?:#{RATE}\s*(?:tb\(?r\)?|#{FPS})#{SEP}?)? # frame rate
472
+ (?:#{RATE}\s*tbn#{SEP}?)? # time base
473
+ (?:#{RATE}\s*tbc#{SEP}?)? # codec time base
474
+ /x
475
+
476
+ def video_match
477
+ return nil unless valid?
478
+ VIDEO_MATCH_PATTERN.match(video_stream)
479
+ end
480
+ end
481
+ end