newbamboo-rvideo 0.9.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) 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 +68 -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 +46 -0
  11. data/lib/rvideo/errors.rb +24 -0
  12. data/lib/rvideo/float.rb +7 -0
  13. data/lib/rvideo/frame_capturer.rb +127 -0
  14. data/lib/rvideo/inspector.rb +482 -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 +406 -0
  22. data/lib/rvideo/tools/ffmpeg.rb +356 -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/segmenter.rb +21 -0
  31. data/lib/rvideo/tools/yamdi.rb +44 -0
  32. data/lib/rvideo/transcoder.rb +139 -0
  33. data/lib/rvideo/version.rb +9 -0
  34. data/rvideo.gemspec +36 -0
  35. data/scripts/txt2html +67 -0
  36. data/setup.rb +1585 -0
  37. data/spec/files/boat.avi +0 -0
  38. data/spec/files/kites.mp4 +0 -0
  39. data/spec/fixtures/ffmpeg_builds.yml +28 -0
  40. data/spec/fixtures/ffmpeg_results.yml +608 -0
  41. data/spec/fixtures/files.yml +398 -0
  42. data/spec/fixtures/recipes.yml +58 -0
  43. data/spec/integrations/formats_spec.rb +315 -0
  44. data/spec/integrations/frame_capturer_spec.rb +26 -0
  45. data/spec/integrations/inspection_spec.rb +112 -0
  46. data/spec/integrations/recipes_spec.rb +0 -0
  47. data/spec/integrations/rvideo_spec.rb +17 -0
  48. data/spec/integrations/transcoder_integration_spec.rb +29 -0
  49. data/spec/integrations/transcoding_spec.rb +9 -0
  50. data/spec/spec.opts +1 -0
  51. data/spec/spec_helper.rb +16 -0
  52. data/spec/support.rb +36 -0
  53. data/spec/units/abstract_tool_spec.rb +111 -0
  54. data/spec/units/ffmpeg_spec.rb +323 -0
  55. data/spec/units/flvtool2_spec.rb +324 -0
  56. data/spec/units/frame_capturer_spec.rb +72 -0
  57. data/spec/units/inspector_spec.rb +59 -0
  58. data/spec/units/mencoder_spec.rb +4994 -0
  59. data/spec/units/mp4box_spec.rb +34 -0
  60. data/spec/units/mp4creator_spec.rb +34 -0
  61. data/spec/units/mplayer_spec.rb +34 -0
  62. data/spec/units/qtfaststart_spec.rb +35 -0
  63. data/spec/units/string_spec.rb +8 -0
  64. data/spec/units/transcoder_spec.rb +156 -0
  65. data/tasks/deployment.rake +5 -0
  66. data/tasks/testing.rake +27 -0
  67. data/tasks/transcoding.rake +40 -0
  68. data/tasks/website.rake +8 -0
  69. metadata +179 -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,46 @@
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/inspector'
14
+ require 'rvideo/frame_capturer'
15
+ require 'rvideo/errors'
16
+ require 'rvideo/transcoder'
17
+ require 'rvideo/tools/abstract_tool'
18
+ require 'rvideo/tools/ffmpeg'
19
+ require 'rvideo/tools/mencoder'
20
+ require 'rvideo/tools/flvtool2'
21
+ require 'rvideo/tools/mp4box'
22
+ require 'rvideo/tools/mplayer'
23
+ require 'rvideo/tools/mp4creator'
24
+ require 'rvideo/tools/ffmpeg2theora'
25
+ require 'rvideo/tools/yamdi'
26
+ require 'rvideo/tools/qtfaststart'
27
+ require 'rvideo/tools/segmenter'
28
+
29
+ TEMP_PATH = File.expand_path(File.dirname(__FILE__) + '/../tmp')
30
+ REPORT_PATH = File.expand_path(File.dirname(__FILE__) + '/../report')
31
+
32
+ module RVideo
33
+ # Configure logging. Assumes that the logger object has an
34
+ # interface similar to stdlib's Logger class.
35
+ #
36
+ # RVideo.logger = Logger.new(STDOUT)
37
+ #
38
+ def self.logger=(logger)
39
+ @logger = logger
40
+ end
41
+
42
+ def self.logger
43
+ @logger = Logger.new("/dev/null") unless @logger
44
+ @logger
45
+ end
46
+ 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,127 @@
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}"
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 = nil
102
+ # if options[:limit]
103
+ # options[:limit]
104
+ # elsif not options[:interval]
105
+ # 1
106
+ # end
107
+
108
+ output = if options[:output]
109
+ options[:output]
110
+ else
111
+ path = File.dirname File.expand_path(options[:input])
112
+
113
+ name = File.basename(options[:input], ".*")
114
+ if options[:interval]
115
+ name << "-%d"
116
+ else
117
+ name << "-#{offset}"
118
+ end
119
+ name << ".jpg"
120
+
121
+ File.join path, name
122
+ end
123
+
124
+ [offset, rate, limit, output]
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,482 @@
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
+ # The bitrate of the movie.
200
+ #
201
+ # Example:
202
+ #
203
+ # 3132
204
+ #
205
+ def bitrate
206
+ return nil unless valid?
207
+ bitrate_match[1].to_i
208
+ end
209
+
210
+ # The bitrate units used. In practice, this may always be kb/s.
211
+ #
212
+ # Example:
213
+ #
214
+ # "kb/s"
215
+ #
216
+ def bitrate_units
217
+ return nil unless valid?
218
+ bitrate_match[2]
219
+ end
220
+
221
+ def bitrate_with_units
222
+ "#{bitrate} #{bitrate_units}"
223
+ end
224
+
225
+ def audio_bit_rate
226
+ return nil unless audio?
227
+ audio_match[7].to_i
228
+ end
229
+
230
+ def audio_bit_rate_units
231
+ return nil unless audio?
232
+ audio_match[8]
233
+ end
234
+
235
+ def audio_bit_rate_with_units
236
+ "#{audio_bit_rate} #{audio_bit_rate_units}"
237
+ end
238
+
239
+ def audio_stream
240
+ return nil unless valid?
241
+
242
+ match = /\n\s*Stream.*Audio:.*\n/.match(@raw_response)
243
+ match[0].strip if match
244
+ end
245
+
246
+ # The audio codec used.
247
+ #
248
+ # Example:
249
+ #
250
+ # "aac"
251
+ #
252
+ def audio_codec
253
+ return nil unless audio?
254
+ audio_match[2]
255
+ end
256
+
257
+ # The sampling rate of the audio stream.
258
+ #
259
+ # Example:
260
+ #
261
+ # 44100
262
+ #
263
+ def audio_sample_rate
264
+ return nil unless audio?
265
+ audio_match[3].to_i
266
+ end
267
+
268
+ # The units used for the sampling rate. May always be Hz.
269
+ #
270
+ # Example:
271
+ #
272
+ # "Hz"
273
+ #
274
+ def audio_sample_rate_units
275
+ return nil unless audio?
276
+ audio_match[4]
277
+ end
278
+ alias_method :audio_sample_units, :audio_sample_rate_units
279
+
280
+ def audio_sample_rate_with_units
281
+ "#{audio_sample_rate} #{audio_sample_rate_units}"
282
+ end
283
+
284
+ # The channels used in the audio stream.
285
+ #
286
+ # Examples:
287
+ # "stereo"
288
+ # "mono"
289
+ # "5:1"
290
+ #
291
+ def audio_channels_string
292
+ return nil unless audio?
293
+ audio_match[5]
294
+ end
295
+
296
+ def audio_channels
297
+ return nil unless audio?
298
+
299
+ case audio_match[5]
300
+ when "mono" then 1
301
+ when "stereo" then 2
302
+ else
303
+ raise RuntimeError, "Unknown number of channels: #{audio_channels}"
304
+ end
305
+ end
306
+
307
+ # This should almost always return 16,
308
+ # as the vast majority of audio is 16 bit.
309
+ def audio_sample_bit_depth
310
+ return nil unless audio?
311
+ audio_match[6].to_i
312
+ end
313
+
314
+ # The ID of the audio stream (useful for troubleshooting).
315
+ #
316
+ # Example:
317
+ # #0.1
318
+ #
319
+ def audio_stream_id
320
+ return nil unless audio?
321
+ audio_match[1]
322
+ end
323
+
324
+ def video_stream
325
+ return nil unless valid?
326
+
327
+ match = /\n\s*Stream.*Video:.*\n/.match(@raw_response)
328
+ match[0].strip unless match.nil?
329
+ end
330
+
331
+ # The ID of the video stream (useful for troubleshooting).
332
+ #
333
+ # Example:
334
+ # #0.0
335
+ #
336
+ def video_stream_id
337
+ return nil unless video?
338
+ video_match[1]
339
+ end
340
+
341
+ # The video codec used.
342
+ #
343
+ # Example:
344
+ #
345
+ # "mpeg4"
346
+ #
347
+ def video_codec
348
+ return nil unless video?
349
+ video_match[3]
350
+ end
351
+
352
+ # The colorspace of the video stream.
353
+ #
354
+ # Example:
355
+ #
356
+ # "yuv420p"
357
+ #
358
+ def video_colorspace
359
+ return nil unless video?
360
+ video_match[4]
361
+ end
362
+
363
+ # The width of the video in pixels.
364
+ def width
365
+ return nil unless video?
366
+ video_match[5].to_i
367
+ end
368
+
369
+ # The height of the video in pixels.
370
+ def height
371
+ return nil unless video?
372
+ video_match[6].to_i
373
+ end
374
+
375
+ # width x height, as a string.
376
+ #
377
+ # Examples:
378
+ # 320x240
379
+ # 1280x720
380
+ #
381
+ def resolution
382
+ return nil unless video?
383
+ "#{width}x#{height}"
384
+ end
385
+
386
+ def pixel_aspect_ratio
387
+ return nil unless video?
388
+ video_match[7]
389
+ end
390
+
391
+ def display_aspect_ratio
392
+ return nil unless video?
393
+ video_match[8]
394
+ end
395
+
396
+ # The portion of the overall bitrate the video is responsible for.
397
+ def video_bit_rate
398
+ return nil unless video?
399
+ video_match[9]
400
+ end
401
+
402
+ def video_bit_rate_units
403
+ return nil unless video?
404
+ video_match[10]
405
+ end
406
+
407
+ # The frame rate of the video in frames per second
408
+ #
409
+ # Example:
410
+ #
411
+ # "29.97"
412
+ #
413
+ def fps
414
+ return nil unless video?
415
+ video_match[2] or video_match[11]
416
+ end
417
+ alias_method :framerate, :fps
418
+
419
+ def time_base
420
+ return nil unless video?
421
+ video_match[12]
422
+ end
423
+
424
+ def codec_time_base
425
+ return nil unless video?
426
+ video_match[13]
427
+ end
428
+
429
+ private
430
+
431
+ def bitrate_match
432
+ /bitrate: ([0-9\.]+)\s*(.*)\s+/.match(@raw_metadata)
433
+ end
434
+
435
+ ###
436
+ # I am wondering how reliable it would be to simplify a lot
437
+ # of this regexp parsery by using split(/\s*,\s*/) - Seth
438
+
439
+ SEP = '(?:,\s*)'
440
+ VAL = '([^,]+)'
441
+
442
+ RATE = '([\d.]+k?)'
443
+
444
+ AUDIO_MATCH_PATTERN = /
445
+ Stream\s+(.*?)[,:\(\[].*?\s*
446
+ Audio:\s+
447
+ #{VAL}#{SEP} # codec
448
+ #{RATE}\s+(\w*)#{SEP}? # sample rate
449
+ ([a-zA-Z:]*)#{SEP}? # channels
450
+ (?:s(\d+)#{SEP}?)? # audio sample bit depth
451
+ (?:(\d+)\s+(\S+))? # audio bit rate
452
+ /x
453
+
454
+ def audio_match
455
+ return nil unless valid?
456
+ AUDIO_MATCH_PATTERN.match(audio_stream)
457
+ end
458
+
459
+ FPS = 'fps(?:\(r\))?'
460
+
461
+ VIDEO_MATCH_PATTERN = /
462
+ Stream\s*(\#[\d.]+)(?:[\(\[].+?[\)\]])?\s* # stream id
463
+ [,:]\s*
464
+ (?:#{RATE}\s*#{FPS}[,:]\s*)? # frame rate, older builds
465
+ Video:\s*
466
+ #{VAL}#{SEP} # codec
467
+ (?:#{VAL}#{SEP})? # color space
468
+ (\d+)x(\d+) # resolution
469
+ (?:\s*\[?(?:PAR\s*(\d+:\d+))?\s*(?:DAR\s*(\d+:\d+))?\]?)? # pixel and display aspect ratios
470
+ #{SEP}?
471
+ (?:#{RATE}\s*(kb\/s)#{SEP}?)? # video bit rate
472
+ (?:#{RATE}\s*(?:tb\(?r\)?|#{FPS})#{SEP}?)? # frame rate
473
+ (?:#{RATE}\s*tbn#{SEP}?)? # time base
474
+ (?:#{RATE}\s*tbc#{SEP}?)? # codec time base
475
+ /x
476
+
477
+ def video_match
478
+ return nil unless valid?
479
+ VIDEO_MATCH_PATTERN.match(video_stream)
480
+ end
481
+ end
482
+ end