debprado-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.
@@ -0,0 +1,339 @@
1
+ module RVideo # :nodoc:
2
+ module Tools # :nodoc:
3
+ class AbstractTool
4
+
5
+ #
6
+ # AbstractTool is an interface to every transcoder tool class (e.g.
7
+ # ffmpeg, flvtool2). Called by the Transcoder class.
8
+ #
9
+
10
+ def self.assign(cmd, options = {})
11
+ tool_name = cmd.split(" ").first
12
+ begin
13
+ tool = "RVideo::Tools::#{tool_name.classify}".constantize.send(:new, cmd, options)
14
+ # rescue NameError, /uninitialized constant/
15
+ # raise TranscoderError::UnknownTool, "The recipe tried to use the '#{tool_name}' tool, which does not exist."
16
+ rescue => e
17
+ LOGGER.info $!
18
+ LOGGER.info e.backtrace.join("\n")
19
+ end
20
+ end
21
+
22
+
23
+ module InstanceMethods
24
+ attr_reader :options, :command, :raw_result
25
+ attr_writer :original
26
+
27
+ def initialize(raw_command, options = {})
28
+ @raw_command = raw_command
29
+ @options = HashWithIndifferentAccess.new(options)
30
+ @command = interpolate_variables(raw_command)
31
+ end
32
+
33
+ #
34
+ # Execute the command and parse the result.
35
+ #
36
+ # def execute
37
+ # @output_params = {}
38
+ # final_command = "#{@command} 2>&1"
39
+ # Transcoder.logger.info("\nExecuting Command: #{final_command}\n")
40
+ # @raw_result = `#{final_command}`
41
+ # Transcoder.logger.info("Result: \n#{@raw_result}")
42
+ # parse_result(@raw_result)
43
+ # end
44
+
45
+ def execute
46
+ @output_params = {}
47
+
48
+ # Dump the log output into a temp file
49
+ log_temp_file_name = "/tmp/transcode_output_#{Time.now.to_i}.txt"
50
+
51
+ final_command = "#{@command} 2>#{log_temp_file_name}"
52
+ Transcoder.logger.info("\nExecuting Command: #{final_command}\n")
53
+ `#{final_command}`
54
+
55
+ populate_raw_result(log_temp_file_name)
56
+
57
+ Transcoder.logger.info("Result: \n#{@raw_result}")
58
+ parse_result(@raw_result)
59
+
60
+ # Cleanup log file
61
+ begin
62
+ File.delete(log_temp_file_name)
63
+ rescue Exception => e
64
+ Transcoder.logger.error("Failed to delete output log file: #{log_temp_file_name}, e=#{e}")
65
+ end
66
+ end
67
+
68
+ #
69
+ # Magic parameters
70
+ #
71
+ def temp_dir
72
+ if @options['output_file']
73
+ "#{File.dirname(@options['output_file'])}/"
74
+ else
75
+ ""
76
+ end
77
+ end
78
+
79
+
80
+ def fps
81
+ format_fps(get_fps)
82
+ end
83
+
84
+ def get_fps
85
+ inspect_original if @original.nil?
86
+ fps = @options['fps'] || ""
87
+ case fps
88
+ when "copy"
89
+ get_original_fps
90
+ else
91
+ get_specific_fps
92
+ end
93
+ end
94
+
95
+
96
+ def resolution
97
+ format_resolution(get_resolution)
98
+ end
99
+
100
+ def get_resolution
101
+ inspect_original if @original.nil?
102
+ resolution_setting = @options['resolution'] || ""
103
+ case resolution_setting
104
+ when "copy"
105
+ get_original_resolution
106
+ when "width"
107
+ get_fit_to_width_resolution
108
+ when "height"
109
+ get_fit_to_height_resolution
110
+ when "letterbox"
111
+ get_letterbox_resolution
112
+ else
113
+ get_specific_resolution
114
+ end
115
+ end
116
+
117
+
118
+ def audio_channels
119
+ format_audio_channels(get_audio_channels)
120
+ end
121
+
122
+ def get_audio_channels
123
+ channels = @options['audio_channels'] || ""
124
+ case channels
125
+ when "stereo"
126
+ get_stereo_audio
127
+ when "mono"
128
+ get_mono_audio
129
+ else
130
+ {}
131
+ end
132
+ end
133
+
134
+ def audio_bit_rate
135
+ format_audio_bit_rate(get_audio_bit_rate)
136
+ end
137
+
138
+ def get_audio_bit_rate
139
+ bit_rate = @options['audio_bit_rate'] || ""
140
+ case bit_rate
141
+ when ""
142
+ {}
143
+ else
144
+ get_specific_audio_bit_rate
145
+ end
146
+ end
147
+
148
+ def audio_sample_rate
149
+ format_audio_sample_rate(get_audio_sample_rate)
150
+ end
151
+
152
+ def get_audio_sample_rate
153
+ sample_rate = @options['audio_sample_rate'] || ""
154
+ case sample_rate
155
+ when ""
156
+ {}
157
+ else
158
+ get_specific_audio_sample_rate
159
+ end
160
+ end
161
+
162
+ def video_quality
163
+ format_video_quality(get_video_quality)
164
+ end
165
+
166
+ def get_video_quality
167
+ inspect_original if @original.nil?
168
+ quality = @options['video_quality'] || 'medium'
169
+ video_bit_rate = @options['video_bit_rate'] || nil
170
+ h = {:video_quality => quality, :video_bit_rate => video_bit_rate}
171
+ h.merge!(get_fps).merge!(get_resolution)
172
+ end
173
+
174
+
175
+
176
+ def get_fit_to_width_resolution
177
+ w = @options['width']
178
+ raise TranscoderError::ParameterError, "invalid width of '#{w}' for fit to width" unless valid_dimension?(w)
179
+ h = calculate_height(@original.width, @original.height, w)
180
+ {:scale => {:width => w, :height => h}}
181
+ end
182
+
183
+ def get_fit_to_height_resolution
184
+ h = @options['height']
185
+ raise TranscoderError::ParameterError, "invalid height of '#{h}' for fit to height" unless valid_dimension?(h)
186
+ w = calculate_width(@original.width, @original.height, h)
187
+ {:scale => {:width => w, :height => h}}
188
+ end
189
+
190
+ def get_letterbox_resolution
191
+ lw = @options['width'].to_i
192
+ lh = @options['height'].to_i
193
+ raise TranscoderError::ParameterError, "invalid width of '#{lw}' for letterbox" unless valid_dimension?(lw)
194
+ raise TranscoderError::ParameterError, "invalid height of '#{lh}' for letterbox" unless valid_dimension?(lh)
195
+ w = calculate_width(@original.width, @original.height, lh)
196
+ h = calculate_height(@original.width, @original.height, lw)
197
+ if w > lw
198
+ w = lw
199
+ h = calculate_height(@original.width, @original.height, lw)
200
+ else
201
+ h = lh
202
+ w = calculate_width(@original.width, @original.height, lh)
203
+ end
204
+ {:scale => {:width => w, :height => h}, :letterbox => {:width => lw, :height => lh}}
205
+ end
206
+
207
+ def get_original_resolution
208
+ {:scale => {:width => @original.width, :height => @original.height}}
209
+ end
210
+
211
+ def get_specific_resolution
212
+ w = @options['width']
213
+ h = @options['height']
214
+ raise TranscoderError::ParameterError, "invalid width of '#{w}' for specific resolution" unless valid_dimension?(w)
215
+ raise TranscoderError::ParameterError, "invalid height of '#{h}' for specific resolution" unless valid_dimension?(h)
216
+ {:scale => {:width => w, :height => h}}
217
+ end
218
+
219
+ def get_original_fps
220
+ return {} if @original.fps.nil?
221
+ {:fps => @original.fps}
222
+ end
223
+
224
+ def get_specific_fps
225
+ {:fps => @options['fps']}
226
+ end
227
+
228
+ # def get_video_quality
229
+ # fps = @options['fps'] || @original.fps
230
+ # raise TranscoderError::ParameterError, "could not find fps in order to determine video quality" if fps.nil?
231
+ # width = @original.width
232
+ # height = @
233
+ # format_video_quality({:quality => @options['video_quality'], :bit_rate => @options['video_bit_rate']})
234
+ # end
235
+
236
+ def get_stereo_audio
237
+ {:channels => "2"}
238
+ end
239
+
240
+ def get_mono_audio
241
+ {:channels => "1"}
242
+ end
243
+
244
+ def get_specific_audio_bit_rate
245
+ {:bit_rate => @options['audio_bit_rate']}
246
+ end
247
+
248
+ def get_specific_audio_sample_rate
249
+ {:sample_rate => @options['audio_sample_rate']}
250
+ end
251
+
252
+ def calculate_width(ow, oh, h)
253
+ w = ((ow.to_f / oh.to_f) * h.to_f).to_i
254
+ (w.to_f / 16).round * 16
255
+ end
256
+
257
+ def calculate_height(ow, oh, w)
258
+ h = (w.to_f / (ow.to_f / oh.to_f)).to_i
259
+ (h.to_f / 16).round * 16
260
+ end
261
+
262
+
263
+ def valid_dimension?(dim)
264
+ return false if dim.to_i <= 0
265
+ return true
266
+ end
267
+
268
+ def format_resolution(params={})
269
+ raise ParameterError, "The #{self.class} tool has not implemented the format_resolution method."
270
+ end
271
+
272
+ def format_fps(params={})
273
+ raise ParameterError, "The #{self.class} tool has not implemented the format_fps method."
274
+ end
275
+
276
+ def format_audio_channels(params={})
277
+ raise ParameterError, "The #{self.class} tool has not implemented the format_audio_channels method."
278
+ end
279
+
280
+ def format_audio_bit_rate(params={})
281
+ raise ParameterError, "The #{self.class} tool has not implemented the format_audio_bit_rate method."
282
+ end
283
+
284
+ def format_audio_sample_rate(params={})
285
+ raise ParameterError, "The #{self.class} tool has not implemented the format_audio_sample_rate method."
286
+ end
287
+
288
+ private
289
+
290
+
291
+ #
292
+ # Look for variables surrounded by $, and interpolate with either
293
+ # variables passed in the options hash, or special methods provided by
294
+ # the tool class (e.g. "$original_fps$" with ffmpeg).
295
+ #
296
+ # $foo$ should match
297
+ # \$foo or $foo\$ or \$foo\$ should not
298
+
299
+ def interpolate_variables(raw_command)
300
+ raw_command.scan(/[^\\]\$[-_a-zA-Z]+\$/).each do |match|
301
+ match = match[0..0] == "$" ? match : match[1..(match.size - 1)]
302
+ match.strip!
303
+ raw_command.gsub!(match, matched_variable(match))
304
+ end
305
+ raw_command.gsub("\\$", "$")
306
+ end
307
+
308
+ #
309
+ # Strip the $s. First, look for a supplied option that matches the
310
+ # variable name. If one is not found, look for a method that matches.
311
+ # If not found, raise ParameterError exception.
312
+ #
313
+
314
+ def matched_variable(match)
315
+ variable_name = match.gsub("$","")
316
+ if self.respond_to? variable_name
317
+ self.send(variable_name)
318
+ elsif @options.key?(variable_name)
319
+ @options[variable_name] || ""
320
+ else
321
+ raise TranscoderError::ParameterError, "command is looking for the #{variable_name} parameter, but it was not provided. (Command: #{@raw_command})"
322
+ end
323
+ end
324
+
325
+ def inspect_original
326
+ @original = Inspector.new(:file => options[:input_file])
327
+ end
328
+
329
+ # Pulls the interesting bits of the temp log file into memory. This is fairly tool-specific, so
330
+ # it's doubtful that this default version is going to work without being overridded.
331
+ def populate_raw_result(temp_file_name)
332
+ @raw_result = `tail -n 500 #{temp_file_name}`
333
+ end
334
+
335
+ end
336
+
337
+ end
338
+ end
339
+ end
@@ -0,0 +1,208 @@
1
+ module RVideo
2
+ module Tools
3
+ class Ffmpeg
4
+ include AbstractTool::InstanceMethods
5
+
6
+ attr_reader :frame, :q, :size, :time, :output_bitrate, :video_size, :audio_size, :header_size, :overhead, :psnr, :output_fps
7
+
8
+ # Not sure if this is needed anymore...
9
+ def tool_command
10
+ 'ffmpeg'
11
+ end
12
+
13
+ def format_fps(params={})
14
+ " -r #{params[:fps]}"
15
+ end
16
+ def format_video_quality(params={})
17
+ bitrate = params[:video_bit_rate].blank? ? nil : params[:video_bit_rate]
18
+ factor = (params[:scale][:width].to_f * params[:scale][:height].to_f * params[:fps].to_f)
19
+ case params[:video_quality]
20
+ when 'low'
21
+ bitrate ||= (factor / 12000).to_i
22
+ " -v #{bitrate}k -crf 30 -me zero -subq 1 -refs 1 -threads auto "
23
+ when 'medium'
24
+ bitrate ||= (factor / 9000).to_i
25
+ " -v #{bitrate}k -crf 22 -flags +loop -cmp +sad -partitions +parti4x4+partp8x8+partb8x8 -flags2 +mixed_refs -me hex -subq 3 -trellis 1 -refs 2 -bf 3 -b_strategy 1 -coder 1 -me_range 16 -g 250"
26
+ when 'high'
27
+ bitrate ||= (factor / 3600).to_i
28
+ " -v #{bitrate}k -crf 18 -flags +loop -cmp +sad -partitions +parti4x4+partp8x8+partb8x8 -flags2 +mixed_refs -me full -subq 6 -trellis 1 -refs 3 -bf 3 -b_strategy 1 -coder 1 -me_range 16 -g 250 -keyint_min 25 -sc_threshold 40 -i_qfactor 0.71"
29
+ else
30
+ ""
31
+ end
32
+ end
33
+
34
+
35
+ def format_resolution(params={})
36
+ p = " -s #{params[:scale][:width]}x#{params[:scale][:height]} "
37
+ if params[:letterbox]
38
+ plr = ((params[:letterbox][:width] - params[:scale][:width]) / 2).to_i
39
+ ptb = ((params[:letterbox][:height] - params[:scale][:height]) / 2).to_i
40
+ p += " -padtop #{ptb} -padbottom #{ptb} -padleft #{plr} -padright #{plr} "
41
+ end
42
+ p
43
+ end
44
+
45
+ def format_audio_channels(params={})
46
+ " -ac #{params[:channels]}"
47
+ end
48
+
49
+ def format_audio_bit_rate(params={})
50
+ " -ab #{params[:bit_rate]}k"
51
+ end
52
+
53
+ def format_audio_sample_rate(params={})
54
+ " -ar #{params[:sample_rate]}"
55
+ end
56
+
57
+
58
+ private
59
+
60
+ # Turns the temp log file into a useful string, from which we can parse the
61
+ # transcoding results.
62
+ # These log files can be enormous, so pulling the whole thing into memory is not an
63
+ # option.
64
+ def populate_raw_result(temp_file_name)
65
+ @raw_result = ""
66
+
67
+ # Is the log file exceptionally long? It's really not a big deal to pull in a thousand lines or so
68
+ # into memory. It's the gigantic files that cause problems. If the files isn't too large,
69
+ # just pull it in.
70
+ line_count = 0
71
+ if m = /^\s*(\d+)/.match(`wc -l #{temp_file_name}`)
72
+ line_count = m[1].to_i
73
+ end
74
+
75
+ if line_count > 500
76
+ # Find the message indicating that the command is actually running.
77
+ running_string = "Press .* to stop encoding"
78
+ @raw_result << `grep "#{running_string}" #{temp_file_name}`
79
+ end
80
+
81
+ # Append the bottom of the log file, where the interesting bits live.
82
+ @raw_result << `tail -n 500 #{temp_file_name}`
83
+ end
84
+
85
+ def parse_result(result)
86
+
87
+ if m = /Unable for find a suitable output format for.*$/.match(result)
88
+ raise TranscoderError::InvalidCommand, m[0]
89
+ end
90
+
91
+ if m = /Unknown codec \'(.*)\'/.match(result)
92
+ raise TranscoderError::InvalidFile, "Codec #{m[1]} not supported by this build of ffmpeg"
93
+ end
94
+
95
+ if m = /could not find codec parameters/.match(result)
96
+ raise TranscoderError::InvalidFile, "Codec not supported by this build of ffmpeg"
97
+ end
98
+
99
+ if m = /I\/O error occured\n(.*)$/.match(result)
100
+ raise TranscoderError::InvalidFile, "I/O error: #{m[1].strip}"
101
+ end
102
+
103
+ if m = /\n(.*)Unknown Format$/.match(result)
104
+ raise TranscoderError::InvalidFile, "unknown format (#{m[1]})"
105
+ end
106
+
107
+ if m = /\nERROR.*/m.match(result)
108
+ raise TranscoderError::InvalidFile, m[0]
109
+ end
110
+
111
+ if result =~ /usage: ffmpeg/
112
+ raise TranscoderError::InvalidCommand, "must pass a command to ffmpeg"
113
+ end
114
+
115
+ if result =~ /Output file does not contain.*stream/
116
+ raise TranscoderError, "Output file does not contain any video or audio streams."
117
+ end
118
+
119
+ if m = /Unsupported codec.*id=(.*)\).*for input stream\s*(.*)\s*/.match(result)
120
+ inspect_original if @original.nil?
121
+ case m[2]
122
+ when @original.audio_stream_id
123
+ codec_type = "audio"
124
+ codec = @original.audio_codec
125
+ when @original.video_stream_id
126
+ codec_type = "video"
127
+ codec = @original.video_codec
128
+ else
129
+ codec_type = "video or audio"
130
+ codec = "unknown"
131
+ end
132
+
133
+ raise TranscoderError::InvalidFile, "Unsupported #{codec_type} codec: #{codec} (id=#{m[1]}, stream=#{m[2]})"
134
+ #raise TranscoderError, "Codec #{m[1]} not supported (in stream #{m[2]})"
135
+ end
136
+
137
+ # Could not open './spec/../config/../tmp/processed/1/kites-1.avi'
138
+ if result =~ /Could not open .#{@output_file}.\Z/
139
+ raise TranscoderError, "Could not write output file to #{@output_file}"
140
+ end
141
+
142
+ full_details = /Press .* to stop encoding\n(.*)/m.match(result)
143
+ raise TranscoderError, "Unexpected result details (#{result})" if full_details.nil?
144
+ details = full_details[1].strip.gsub(/\s*\n\s*/," - ")
145
+
146
+ if details =~ /Could not write header/
147
+ raise TranscoderError, details
148
+ end
149
+
150
+ #frame= 584 q=6.0 Lsize= 708kB time=19.5 bitrate= 297.8kbits/s
151
+ #video:49kB audio:153kB global headers:0kB muxing overhead 250.444444%
152
+
153
+ #frame= 4126 q=31.0 Lsize= 5917kB time=69.1 bitrate= 702.0kbits/s
154
+ #video:2417kB audio:540kB global headers:0kB muxing overhead 100.140277%
155
+
156
+ #frame= 273 fps= 31 q=10.0 Lsize= 398kB time=5.9 bitrate= 551.8kbits/s
157
+ #video:284kB audio:92kB global headers:0kB muxing overhead 5.723981%
158
+
159
+ #mdb:94, lastbuf:0 skipping granule 0
160
+ #size= 1080kB time=69.1 bitrate= 128.0kbits /s
161
+ #video:0kB audio:1080kB global headers:0kB muxing overhead 0.002893%
162
+
163
+ #size= 80kB time=5.1 bitrate= 128.0kbits/s ^Msize= 162kB time=10.3 bitrate= 128.0kbits/s ^Msize= 241kB time=15.4 bitrate= 128.0kbits/s ^Msize= 329kB time=21.1 bitrate= 128.0kbits/s ^Msize= 413kB time=26.4 bitrate= 128.0kbits/s ^Msize= 506kB time=32.4 bitrate= 128.0kbits/s ^Msize= 591kB time=37.8 bitrate= 128.0kbits/s ^Msize= 674kB time=43.2 bitrate= 128.0kbits/s ^Msize= 771kB time=49.4 bitrate= 128.0kbits/s ^Msize= 851kB time=54.5 bitrate= 128.0kbits/s ^Msize= 932kB time=59.6 bitrate= 128.0kbits/s ^Msize= 1015kB time=64.9 bitrate= 128.0kbits/s ^Msize= 1094kB time=70.0 bitrate= 128.0kbits/s ^Msize= 1175kB time=75.2 bitrate= 128.0kbits/s ^Msize= 1244kB time=79.6 bitrate= 128.0kbits/s ^Msize= 1335kB time=85.4 bitrate= 128.0kbits/s ^Msize= 1417kB time=90.7 bitrate= 128.0kbits/s ^Msize= 1508kB time=96.5 bitrate= 128.0kbits/s ^Msize= 1589kB time=101.7 bitrate= 128.0kbits/s ^Msize= 1671kB time=106.9 bitrate= 128.0kbits/s ^Msize= 1711kB time=109.5 bitrate= 128.0kbits/s - video:0kB audio:1711kB global headers:0kB muxing overhead 0.001826%
164
+
165
+ #mdb:14, lastbuf:0 skipping granule 0 - overread, skip -5 enddists: -2 -2 - overread, skip -5 enddists: -2 -2 - size= 90kB time=5.7 bitrate= 128.0kbits/s \nsize= 189kB time=12.1 bitrate= 128.0kbits/s
166
+
167
+ #size= 59kB time=20.2 bitrate= 24.0kbits/s \nsize= 139kB time=47.4 bitrate= 24.0kbits/s \nsize= 224kB time=76.5 bitrate= 24.0kbits/s \nsize= 304kB time=103.7 bitrate= 24.0kbits/s \nsi
168
+
169
+ #mdb:14, lastbuf:0 skipping granule 0 - overread, skip -5 enddists: -2 -2 - overread, skip -5 enddists: -2 -2 - size= 81kB time=10.3 bitrate= 64.0kbits/s \nsize= 153kB time=19.6 bitrate= 64.0kbits/s
170
+
171
+ #size= 65kB time=4.1 bitrate= 128.1kbits/s \nsize= 119kB time=7.6 bitrate= 128.0kbits/s \nsize= 188kB time=12.0 bitrate= 128.0kbits/s \nsize= 268kB time=17.1 bitrate= 128.0kbits/s \nsize=
172
+
173
+ #Error while decoding stream #0.1 [mpeg4aac @ 0xb7d089f0]faac: frame decoding failed: Gain control not yet implementedError while decoding stream #0.1frame= 2143 fps= 83 q=4.0 size= 4476kB time=71.3 bitrate= 514.5kbits/s ^M[mpeg4aac @ 0xb7d089f0]faac: frame decoding failed: Gain control not yet implementedError while decoding stream #0.1
174
+
175
+ # NOTE: had to remove "\s" from "\s.*L.*size=" from this regexp below.
176
+ # Not sure why. Unit tests were succeeding, but hand tests weren't.
177
+ if details =~ /video:/
178
+ #success = /^frame=\s*(\S*)\s*q=(\S*).*L.*size=\s*(\S*)\s*time=\s*(\S*)\s*bitrate=\s*(\S*)\s*/m.match(details)
179
+ @frame = sanitary_match(/frame=\s*(\S*)/, details)
180
+ @output_fps = sanitary_match(/fps=\s*(\S*)/, details)
181
+ @q = sanitary_match(/\s+q=\s*(\S*)/, details)
182
+ @size = sanitary_match(/size=\s*(\S*)/, details)
183
+ @time = sanitary_match(/time=\s*(\S*)/, details)
184
+ @output_bitrate = sanitary_match(/bitrate=\s*(\S*)/, details)
185
+
186
+ @video_size = /video:\s*(\S*)/.match(details)[1]
187
+ @audio_size = /audio:\s*(\S*)/.match(details)[1]
188
+ @header_size = /headers:\s*(\S*)/.match(details)[1]
189
+ @overhead = /overhead[:]*\s*(\S*)/.match(details)[1]
190
+ psnr_match = /PSNR=(.*)\s*size=/.match(details)
191
+ @psnr = psnr_match[1].strip if psnr_match
192
+ return true
193
+ end
194
+
195
+ #[mp3 @ 0x54340c]flv doesnt support that sample rate, choose from (44100, 22050, 11025)
196
+ #Could not write header for output file #0 (incorrect codec parameters ?)
197
+
198
+ raise TranscoderError::UnexpectedResult, details
199
+ end
200
+
201
+ def sanitary_match(regexp, string)
202
+ match = regexp.match(string)
203
+ return match[1] if match
204
+ end
205
+
206
+ end
207
+ end
208
+ end