twinge-rvideo 0.9.6

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