ez_video 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 = File.split(cmd.split(" ").first).last
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