kakra-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.
- data/RULES +11 -0
- data/lib/rvideo/errors.rb +24 -0
- data/lib/rvideo/float.rb +7 -0
- data/lib/rvideo/inspector.rb +542 -0
- data/lib/rvideo/tools/abstract_tool.rb +393 -0
- data/lib/rvideo/tools/ffmpeg.rb +269 -0
- data/lib/rvideo/tools/ffmpeg2theora.rb +42 -0
- data/lib/rvideo/tools/flvtool2.rb +50 -0
- data/lib/rvideo/tools/mencoder.rb +103 -0
- data/lib/rvideo/tools/mp4box.rb +21 -0
- data/lib/rvideo/tools/mp4creator.rb +37 -0
- data/lib/rvideo/tools/mplayer.rb +31 -0
- data/lib/rvideo/tools/yamdi.rb +44 -0
- data/lib/rvideo/transcoder.rb +120 -0
- data/lib/rvideo/version.rb +9 -0
- data/lib/rvideo.rb +42 -0
- data/rvideo.gemspec +10 -0
- metadata +73 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
module RVideo # :nodoc:
|
|
2
|
+
module Tools # :nodoc:
|
|
3
|
+
|
|
4
|
+
# AbstractTool is an interface to every transcoder tool class
|
|
5
|
+
# (e.g. ffmpeg, flvtool2). Called by the Transcoder class.
|
|
6
|
+
class AbstractTool
|
|
7
|
+
|
|
8
|
+
def self.assign(cmd, options = {})
|
|
9
|
+
tool_name = cmd.split(" ").first
|
|
10
|
+
begin
|
|
11
|
+
tool = "RVideo::Tools::#{tool_name.classify}".constantize.send(:new, cmd, options)
|
|
12
|
+
# rescue NameError, /uninitialized constant/
|
|
13
|
+
# raise TranscoderError::UnknownTool, "The recipe tried to use the '#{tool_name}' tool, which does not exist."
|
|
14
|
+
rescue => e
|
|
15
|
+
RVideo.logger.info e.message
|
|
16
|
+
RVideo.logger.info e.backtrace.join("\n")
|
|
17
|
+
raise e
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
module InstanceMethods
|
|
23
|
+
# Defines abstract methods in the convention of "format_#{attribute}"
|
|
24
|
+
# which are meant to be redefined by classes including this behavior.
|
|
25
|
+
def self.abstract_attribute_formatter(*names)
|
|
26
|
+
names.map { |n| "format_#{n}" }.each do |name|
|
|
27
|
+
class_eval %{
|
|
28
|
+
def #{name}(params = {})
|
|
29
|
+
raise ParameterError,
|
|
30
|
+
"The #{self.class} tool has not implemented the :#{name} method."
|
|
31
|
+
end
|
|
32
|
+
}, __FILE__, __LINE__
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
abstract_attribute_formatter :resolution, :fps,
|
|
37
|
+
:video_bit_rate, :video_bit_rate_tolerance,
|
|
38
|
+
:video_bit_rate_min, :video_bit_rate_max,
|
|
39
|
+
:audio_channels, :audio_bit_rate, :audio_sample_rate
|
|
40
|
+
|
|
41
|
+
###
|
|
42
|
+
|
|
43
|
+
attr_reader :options, :command, :raw_result
|
|
44
|
+
attr_writer :original
|
|
45
|
+
|
|
46
|
+
def initialize(raw_command, options = {})
|
|
47
|
+
@raw_command = raw_command
|
|
48
|
+
@options = HashWithIndifferentAccess.new(options)
|
|
49
|
+
@command = interpolate_variables(raw_command)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def execute
|
|
53
|
+
@output_params = {}
|
|
54
|
+
|
|
55
|
+
# Dump the log output into a temp file
|
|
56
|
+
log_temp_file_name = "/tmp/transcode_output_#{Time.now.to_i}.txt"
|
|
57
|
+
|
|
58
|
+
final_command = "#{@command} 2>#{log_temp_file_name}"
|
|
59
|
+
RVideo.logger.info("\nExecuting Command: #{final_command}\n")
|
|
60
|
+
do_execute final_command
|
|
61
|
+
|
|
62
|
+
populate_raw_result(log_temp_file_name)
|
|
63
|
+
|
|
64
|
+
RVideo.logger.info("Result: \n#{@raw_result}")
|
|
65
|
+
parse_result(@raw_result)
|
|
66
|
+
|
|
67
|
+
# Cleanup log file
|
|
68
|
+
begin
|
|
69
|
+
File.delete(log_temp_file_name)
|
|
70
|
+
rescue Exception => e
|
|
71
|
+
RVideo.logger.error("Failed to delete output log file: #{log_temp_file_name}, e=#{e}")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Wrapper around the system call, for whenever we need to
|
|
76
|
+
# hook on or redefine this without messing with Kernel
|
|
77
|
+
def do_execute(command)
|
|
78
|
+
system command
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
#
|
|
82
|
+
# Magic parameters
|
|
83
|
+
#
|
|
84
|
+
def temp_dir
|
|
85
|
+
if @options['output_file']
|
|
86
|
+
"#{File.dirname(@options['output_file'])}/"
|
|
87
|
+
else
|
|
88
|
+
""
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
###
|
|
93
|
+
# FPS aka framerate
|
|
94
|
+
|
|
95
|
+
def fps
|
|
96
|
+
format_fps(get_fps)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def get_fps
|
|
100
|
+
inspect_original if @original.nil?
|
|
101
|
+
fps = @options['fps'] || ""
|
|
102
|
+
case fps
|
|
103
|
+
when "copy"
|
|
104
|
+
get_original_fps
|
|
105
|
+
else
|
|
106
|
+
get_specific_fps
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def get_original_fps
|
|
111
|
+
return {} if @original.fps.nil?
|
|
112
|
+
{ :fps => @original.fps }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def get_specific_fps
|
|
116
|
+
{ :fps => @options['fps'] }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
###
|
|
120
|
+
# Resolution
|
|
121
|
+
|
|
122
|
+
def resolution
|
|
123
|
+
format_resolution(get_resolution)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def get_resolution
|
|
127
|
+
inspect_original if @original.nil?
|
|
128
|
+
|
|
129
|
+
case @options['resolution']
|
|
130
|
+
when "copy" then get_original_resolution
|
|
131
|
+
when "width" then get_fit_to_width_resolution
|
|
132
|
+
when "height" then get_fit_to_height_resolution
|
|
133
|
+
when "letterbox" then get_letterbox_resolution
|
|
134
|
+
else
|
|
135
|
+
if @options["width"] and not @options["height"]
|
|
136
|
+
get_fit_to_width_resolution
|
|
137
|
+
elsif @options["height"] and not @options["width"]
|
|
138
|
+
get_fit_to_height_resolution
|
|
139
|
+
elsif @options["width"] and @options["height"]
|
|
140
|
+
get_specific_resolution
|
|
141
|
+
else
|
|
142
|
+
get_original_resolution
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def get_fit_to_width_resolution
|
|
148
|
+
w = @options['width']
|
|
149
|
+
|
|
150
|
+
raise TranscoderError::ParameterError,
|
|
151
|
+
"invalid width of '#{w}' for fit to width" unless valid_dimension?(w)
|
|
152
|
+
|
|
153
|
+
h = calculate_height(@original.width, @original.height, w)
|
|
154
|
+
|
|
155
|
+
{ :scale => { :width => w, :height => h } }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def get_fit_to_height_resolution
|
|
159
|
+
h = @options['height']
|
|
160
|
+
|
|
161
|
+
raise TranscoderError::ParameterError,
|
|
162
|
+
"invalid height of '#{h}' for fit to height" unless valid_dimension?(h)
|
|
163
|
+
|
|
164
|
+
w = calculate_width(@original.width, @original.height, h)
|
|
165
|
+
|
|
166
|
+
{ :scale => { :width => w, :height => h } }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def get_letterbox_resolution
|
|
170
|
+
lw = @options['width'].to_i
|
|
171
|
+
lh = @options['height'].to_i
|
|
172
|
+
|
|
173
|
+
raise TranscoderError::ParameterError,
|
|
174
|
+
"invalid width of '#{lw}' for letterbox" unless valid_dimension?(lw)
|
|
175
|
+
raise TranscoderError::ParameterError,
|
|
176
|
+
"invalid height of '#{lh}' for letterbox" unless valid_dimension?(lh)
|
|
177
|
+
|
|
178
|
+
w = calculate_width(@original.width, @original.height, lh)
|
|
179
|
+
h = calculate_height(@original.width, @original.height, lw)
|
|
180
|
+
|
|
181
|
+
if w > lw
|
|
182
|
+
w = lw
|
|
183
|
+
h = calculate_height(@original.width, @original.height, lw)
|
|
184
|
+
else
|
|
185
|
+
h = lh
|
|
186
|
+
w = calculate_width(@original.width, @original.height, lh)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
{ :scale => { :width => w, :height => h },
|
|
190
|
+
:letterbox => { :width => lw, :height => lh } }
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def get_original_resolution
|
|
194
|
+
{ :scale => { :width => @original.width, :height => @original.height } }
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def get_specific_resolution
|
|
198
|
+
w = @options['width']
|
|
199
|
+
h = @options['height']
|
|
200
|
+
|
|
201
|
+
raise TranscoderError::ParameterError,
|
|
202
|
+
"invalid width of '#{w}' for specific resolution" unless valid_dimension?(w)
|
|
203
|
+
raise TranscoderError::ParameterError,
|
|
204
|
+
"invalid height of '#{h}' for specific resolution" unless valid_dimension?(h)
|
|
205
|
+
|
|
206
|
+
{ :scale => { :width => w, :height => h } }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def calculate_width(ow, oh, h)
|
|
210
|
+
w = ((ow.to_f / oh.to_f) * h.to_f).to_i
|
|
211
|
+
(w.to_f / 16).round * 16
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def calculate_height(ow, oh, w)
|
|
215
|
+
h = (w.to_f / (ow.to_f / oh.to_f)).to_i
|
|
216
|
+
(h.to_f / 16).round * 16
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def valid_dimension?(dim)
|
|
220
|
+
dim.to_i > 0
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
###
|
|
224
|
+
# Audio channels
|
|
225
|
+
|
|
226
|
+
def audio_channels
|
|
227
|
+
format_audio_channels(get_audio_channels)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def get_audio_channels
|
|
231
|
+
channels = @options['audio_channels'] || ""
|
|
232
|
+
case channels
|
|
233
|
+
when "stereo"
|
|
234
|
+
get_stereo_audio
|
|
235
|
+
when "mono"
|
|
236
|
+
get_mono_audio
|
|
237
|
+
else
|
|
238
|
+
{}
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def get_stereo_audio
|
|
243
|
+
{ :channels => "2" }
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def get_mono_audio
|
|
247
|
+
{ :channels => "1" }
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def get_specific_audio_bit_rate
|
|
251
|
+
{ :bit_rate => @options['audio_bit_rate'] }
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def get_specific_audio_sample_rate
|
|
255
|
+
{ :sample_rate => @options['audio_sample_rate'] }
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
###
|
|
259
|
+
# Audio bit rate
|
|
260
|
+
|
|
261
|
+
def audio_bit_rate
|
|
262
|
+
format_audio_bit_rate(get_audio_bit_rate)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def get_audio_bit_rate
|
|
266
|
+
bit_rate = @options['audio_bit_rate'] || ""
|
|
267
|
+
case bit_rate
|
|
268
|
+
when ""
|
|
269
|
+
{}
|
|
270
|
+
else
|
|
271
|
+
get_specific_audio_bit_rate
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
###
|
|
276
|
+
# Audio sample rate
|
|
277
|
+
|
|
278
|
+
def audio_sample_rate
|
|
279
|
+
format_audio_sample_rate(get_audio_sample_rate)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def get_audio_sample_rate
|
|
283
|
+
sample_rate = @options['audio_sample_rate'] || ""
|
|
284
|
+
case sample_rate
|
|
285
|
+
when ""
|
|
286
|
+
{}
|
|
287
|
+
else
|
|
288
|
+
get_specific_audio_sample_rate
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
###
|
|
293
|
+
# Video quality
|
|
294
|
+
|
|
295
|
+
def video_quality
|
|
296
|
+
format_video_quality(get_video_quality)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def get_video_quality
|
|
300
|
+
quality = @options['video_quality'] || 'medium'
|
|
301
|
+
|
|
302
|
+
{ :video_quality => quality }.
|
|
303
|
+
merge!(get_fps).
|
|
304
|
+
merge!(get_resolution).
|
|
305
|
+
merge!(get_video_bit_rate)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def video_bit_rate
|
|
309
|
+
format_video_bit_rate(get_video_bit_rate)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def get_video_bit_rate
|
|
313
|
+
{ :video_bit_rate => @options["video_bit_rate"] }
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def video_bit_rate_tolerance
|
|
317
|
+
format_video_bit_rate_tolerance(get_video_bit_rate_tolerance)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def get_video_bit_rate_tolerance
|
|
321
|
+
{ :video_bit_rate_tolerance => @options["video_bit_rate_tolerance"] }
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def video_bit_rate_min
|
|
325
|
+
format_video_bit_rate_min(get_video_bit_rate_min)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def get_video_bit_rate_min
|
|
329
|
+
{ :video_bit_rate_min => @options["video_bit_rate_min"] }
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def video_bit_rate_max
|
|
333
|
+
format_video_bit_rate_max(get_video_bit_rate_max)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def get_video_bit_rate_max
|
|
337
|
+
{ :video_bit_rate_max => @options["video_bit_rate_max"] }
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
private
|
|
341
|
+
|
|
342
|
+
VARIABLE_INTERPOLATION_SCAN_PATTERN = /[^\\]\$[-_a-zA-Z]+\$/
|
|
343
|
+
|
|
344
|
+
def interpolate_variables(raw_command)
|
|
345
|
+
raw_command.scan(VARIABLE_INTERPOLATION_SCAN_PATTERN).each do |match|
|
|
346
|
+
match = match[0..0] == "$" ? match : match[1..(match.size - 1)]
|
|
347
|
+
match.strip!
|
|
348
|
+
|
|
349
|
+
value = if ["$input_file$", "$output_file$"].include?(match)
|
|
350
|
+
matched_variable(match).to_s.shell_quoted
|
|
351
|
+
else
|
|
352
|
+
matched_variable(match).to_s
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
raw_command.gsub!(match, value)
|
|
356
|
+
end
|
|
357
|
+
raw_command.gsub("\\$", "$")
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
#
|
|
361
|
+
# Strip the $s. First, look for a supplied option that matches the
|
|
362
|
+
# variable name. If one is not found, look for a method that matches.
|
|
363
|
+
# If not found, raise ParameterError exception.
|
|
364
|
+
#
|
|
365
|
+
|
|
366
|
+
def matched_variable(match)
|
|
367
|
+
variable_name = match.gsub("$","")
|
|
368
|
+
if self.respond_to? variable_name
|
|
369
|
+
self.send(variable_name)
|
|
370
|
+
elsif @options.key?(variable_name)
|
|
371
|
+
@options[variable_name]
|
|
372
|
+
else
|
|
373
|
+
raise TranscoderError::ParameterError,
|
|
374
|
+
"command is looking for the #{variable_name} parameter, but it was not provided. (Command: #{@raw_command})"
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def inspect_original
|
|
380
|
+
@original = Inspector.new(:file => options[:input_file])
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# Pulls the interesting bits of the temp log file into memory. This is fairly tool-specific, so
|
|
384
|
+
# it's doubtful that this default version is going to work without being overridded.
|
|
385
|
+
def populate_raw_result(temp_file_name)
|
|
386
|
+
@raw_result = `tail -n 500 #{temp_file_name}`
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
end # InstanceMethods
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
end
|
|
393
|
+
end
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
module RVideo
|
|
2
|
+
module Tools
|
|
3
|
+
class Ffmpeg
|
|
4
|
+
RESOLUTION_ABBREVIATIONS = {
|
|
5
|
+
"sqcif" => "128x96",
|
|
6
|
+
"qcif" => "176x144",
|
|
7
|
+
"cif" => "352x288",
|
|
8
|
+
"4cif" => "704x576",
|
|
9
|
+
"qqvga" => "160x120",
|
|
10
|
+
"qvga" => "320x240",
|
|
11
|
+
"vga" => "640x480",
|
|
12
|
+
"svga" => "800x600",
|
|
13
|
+
"xga" => "1024x768",
|
|
14
|
+
"uxga" => "1600x1200",
|
|
15
|
+
"qxga" => "2048x1536",
|
|
16
|
+
"sxga" => "1280x1024",
|
|
17
|
+
"qsxga" => "2560x2048",
|
|
18
|
+
"hsxga" => "5120x4096",
|
|
19
|
+
"wvga" => "852x480",
|
|
20
|
+
"wxga" => "1366x768",
|
|
21
|
+
"wsxga" => "1600x1024",
|
|
22
|
+
"wuxga" => "1920x1200",
|
|
23
|
+
"woxga" => "2560x1600",
|
|
24
|
+
"wqsxga" => "3200x2048",
|
|
25
|
+
"wquxga" => "3840x2400",
|
|
26
|
+
"whsxga" => "6400x4096",
|
|
27
|
+
"whuxga" => "7680x4800",
|
|
28
|
+
"cga" => "320x200",
|
|
29
|
+
"ega" => "640x350",
|
|
30
|
+
"hd480" => "852x480",
|
|
31
|
+
"hd720" => "1280x720",
|
|
32
|
+
"hd1080" => "1920x1080"
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
VALID_ASPECT_STRINGS = ["4:3", "16:9"]
|
|
36
|
+
VALID_ASPECT_FLOATS = [1.3333, 1.7777]
|
|
37
|
+
|
|
38
|
+
# The flag used to set video bitrate has apparently changed between
|
|
39
|
+
# different ffmpeg versions. In the latest builds -b is used.
|
|
40
|
+
# In older builds it was -v which is now used to set verbosity of logging.
|
|
41
|
+
DEFAULT_VIDEO_BIT_RATE_PARAMETER = "b"
|
|
42
|
+
cattr_accessor :video_bit_rate_parameter
|
|
43
|
+
self.video_bit_rate_parameter = DEFAULT_VIDEO_BIT_RATE_PARAMETER
|
|
44
|
+
|
|
45
|
+
include AbstractTool::InstanceMethods
|
|
46
|
+
|
|
47
|
+
attr_reader :frame, :q, :size, :time, :output_bitrate, :video_size, :audio_size, :header_size, :overhead, :psnr, :output_fps
|
|
48
|
+
|
|
49
|
+
# Not sure if this is needed anymore...
|
|
50
|
+
def tool_command
|
|
51
|
+
'ffmpeg'
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def format_fps(params={})
|
|
55
|
+
"-r #{params[:fps]}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def format_video_bit_rate(params = {})
|
|
59
|
+
"-#{video_bit_rate_parameter} #{params[:video_bit_rate]}k"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def format_video_bit_rate_tolerance(params = {})
|
|
63
|
+
"-bt #{params[:video_bit_rate_tolerance]}k"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def format_video_bit_rate_min(params = {})
|
|
67
|
+
"-minrate #{params[:video_bit_rate_min]}k"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def format_video_bit_rate_max(params = {})
|
|
71
|
+
"-maxrate #{params[:video_bit_rate_max]}k"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def format_video_quality(params={})
|
|
75
|
+
bitrate = params[:video_bit_rate].blank? ? nil : params[:video_bit_rate]
|
|
76
|
+
|
|
77
|
+
params.merge! get_original_fps unless params[:fps]
|
|
78
|
+
|
|
79
|
+
factor = params[:scale][:width].to_f * params[:scale][:height].to_f * params[:fps].to_f
|
|
80
|
+
|
|
81
|
+
case params[:video_quality]
|
|
82
|
+
when 'low'
|
|
83
|
+
bitrate ||= (factor / 12000).to_i
|
|
84
|
+
params[:video_bit_rate] = bitrate
|
|
85
|
+
"#{format_video_bit_rate params} -crf 30 -me zero -subq 1 -refs 1 -threads auto"
|
|
86
|
+
when 'medium'
|
|
87
|
+
bitrate ||= (factor / 9000).to_i
|
|
88
|
+
params[:video_bit_rate] = bitrate
|
|
89
|
+
"#{format_video_bit_rate params} -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"
|
|
90
|
+
when 'high'
|
|
91
|
+
bitrate ||= (factor / 3600).to_i
|
|
92
|
+
params[:video_bit_rate] = bitrate
|
|
93
|
+
"#{format_video_bit_rate params} -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"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def format_resolution(params={})
|
|
98
|
+
p = "-s #{params[:scale][:width]}x#{params[:scale][:height]}"
|
|
99
|
+
if params[:letterbox]
|
|
100
|
+
plr = ((params[:letterbox][:width] - params[:scale][:width]) / 2).to_i
|
|
101
|
+
ptb = ((params[:letterbox][:height] - params[:scale][:height]) / 2).to_i
|
|
102
|
+
p += " -padtop #{ptb} -padbottom #{ptb} -padleft #{plr} -padright #{plr} "
|
|
103
|
+
end
|
|
104
|
+
p
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def format_audio_channels(params={})
|
|
108
|
+
"-ac #{params[:channels]}"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def format_audio_bit_rate(params={})
|
|
112
|
+
"-ab #{params[:bit_rate]}k"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def format_audio_sample_rate(params={})
|
|
116
|
+
"-ar #{params[:sample_rate]}"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
# Turns the temp log file into a useful string, from which we can parse the
|
|
123
|
+
# transcoding results.
|
|
124
|
+
# These log files can be enormous, so pulling the whole thing into memory is not an
|
|
125
|
+
# option.
|
|
126
|
+
def populate_raw_result(temp_file_name)
|
|
127
|
+
@raw_result = ""
|
|
128
|
+
|
|
129
|
+
# Is the log file exceptionally long? It's really not a big deal to pull in a thousand lines or so
|
|
130
|
+
# into memory. It's the gigantic files that cause problems. If the files isn't too large,
|
|
131
|
+
# just pull it in.
|
|
132
|
+
line_count = 0
|
|
133
|
+
if m = /^\s*(\d+)/.match(`wc -l #{temp_file_name}`)
|
|
134
|
+
line_count = m[1].to_i
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
if line_count > 500
|
|
138
|
+
# Find the message indicating that the command is actually running.
|
|
139
|
+
running_string = "Press .* to stop encoding"
|
|
140
|
+
@raw_result << `grep "#{running_string}" #{temp_file_name}`
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Append the bottom of the log file, where the interesting bits live.
|
|
144
|
+
@raw_result << `tail -n 500 #{temp_file_name}`
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def parse_result(result)
|
|
148
|
+
if m = /Expected .+ for (.+) but found: (.+)/.match(result)
|
|
149
|
+
raise TranscoderError::InvalidCommand, m.to_s
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
if m = /Unable for find a suitable output format for.*$/.match(result)
|
|
153
|
+
raise TranscoderError::InvalidCommand, m[0]
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
if m = /Unknown codec \'(.*)\'/.match(result)
|
|
157
|
+
raise TranscoderError::InvalidFile, "Codec #{m[1]} not supported by this build of ffmpeg"
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
if m = /could not find codec parameters/.match(result)
|
|
161
|
+
raise TranscoderError::InvalidFile, "Codec not supported by this build of ffmpeg"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
if m = /I\/O error occured\n(.*)$/.match(result)
|
|
165
|
+
raise TranscoderError::InvalidFile, "I/O error: #{m[1].strip}"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
if m = /\n(.*)Unknown Format$/.match(result)
|
|
169
|
+
raise TranscoderError::InvalidFile, "unknown format (#{m[1]})"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
if m = /\nERROR.*/m.match(result)
|
|
173
|
+
raise TranscoderError::InvalidFile, m[0]
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
if result =~ /usage: ffmpeg/
|
|
177
|
+
raise TranscoderError::InvalidCommand, "must pass a command to ffmpeg"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
if result =~ /Output file does not contain.*stream/
|
|
181
|
+
raise TranscoderError, "Output file does not contain any video or audio streams."
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
if m = /Unsupported codec.*id=(.*)\).*for input stream\s*(.*)\s*/.match(result)
|
|
185
|
+
inspect_original if @original.nil?
|
|
186
|
+
case m[2]
|
|
187
|
+
when @original.audio_stream_id
|
|
188
|
+
codec_type = "audio"
|
|
189
|
+
codec = @original.audio_codec
|
|
190
|
+
when @original.video_stream_id
|
|
191
|
+
codec_type = "video"
|
|
192
|
+
codec = @original.video_codec
|
|
193
|
+
else
|
|
194
|
+
codec_type = "video or audio"
|
|
195
|
+
codec = "unknown"
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
raise TranscoderError::InvalidFile, "Unsupported #{codec_type} codec: #{codec} (id=#{m[1]}, stream=#{m[2]})"
|
|
199
|
+
#raise TranscoderError, "Codec #{m[1]} not supported (in stream #{m[2]})"
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Could not open './spec/../config/../tmp/processed/1/kites-1.avi'
|
|
203
|
+
if result =~ /Could not open .#{@output_file}.\Z/
|
|
204
|
+
raise TranscoderError, "Could not write output file to #{@output_file}"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
full_details = /Press .* to stop encoding\n(.*)/m.match(result)
|
|
208
|
+
raise TranscoderError, "Unexpected result details (#{result})" if full_details.nil?
|
|
209
|
+
details = full_details[1].strip.gsub(/\s*\n\s*/," - ")
|
|
210
|
+
|
|
211
|
+
if details =~ /Could not write header/
|
|
212
|
+
raise TranscoderError, details
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
#frame= 584 q=6.0 Lsize= 708kB time=19.5 bitrate= 297.8kbits/s
|
|
216
|
+
#video:49kB audio:153kB global headers:0kB muxing overhead 250.444444%
|
|
217
|
+
|
|
218
|
+
#frame= 4126 q=31.0 Lsize= 5917kB time=69.1 bitrate= 702.0kbits/s
|
|
219
|
+
#video:2417kB audio:540kB global headers:0kB muxing overhead 100.140277%
|
|
220
|
+
|
|
221
|
+
#frame= 273 fps= 31 q=10.0 Lsize= 398kB time=5.9 bitrate= 551.8kbits/s
|
|
222
|
+
#video:284kB audio:92kB global headers:0kB muxing overhead 5.723981%
|
|
223
|
+
|
|
224
|
+
#mdb:94, lastbuf:0 skipping granule 0
|
|
225
|
+
#size= 1080kB time=69.1 bitrate= 128.0kbits /s
|
|
226
|
+
#video:0kB audio:1080kB global headers:0kB muxing overhead 0.002893%
|
|
227
|
+
|
|
228
|
+
#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%
|
|
229
|
+
|
|
230
|
+
#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
|
|
231
|
+
|
|
232
|
+
#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
|
|
233
|
+
|
|
234
|
+
#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
|
|
235
|
+
|
|
236
|
+
#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=
|
|
237
|
+
|
|
238
|
+
#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
|
|
239
|
+
|
|
240
|
+
# NOTE: had to remove "\s" from "\s.*L.*size=" from this regexp below.
|
|
241
|
+
# Not sure why. Unit tests were succeeding, but hand tests weren't.
|
|
242
|
+
if details =~ /video:/
|
|
243
|
+
#success = /^frame=\s*(\S*)\s*q=(\S*).*L.*size=\s*(\S*)\s*time=\s*(\S*)\s*bitrate=\s*(\S*)\s*/m.match(details)
|
|
244
|
+
@frame = details[/frame=\s*(\S*)/, 1]
|
|
245
|
+
@output_fps = details[/fps=\s*(\S*)/, 1]
|
|
246
|
+
@q = details[/\s+q=\s*(\S*)/, 1]
|
|
247
|
+
@size = details[/size=\s*(\S*)/, 1]
|
|
248
|
+
@time = details[/time=\s*(\S*)/, 1]
|
|
249
|
+
@output_bitrate = details[/bitrate=\s*(\S*)/, 1]
|
|
250
|
+
|
|
251
|
+
@video_size = details[/video:\s*(\S*)/, 1]
|
|
252
|
+
@audio_size = details[/audio:\s*(\S*)/, 1]
|
|
253
|
+
@header_size = details[/headers:\s*(\S*)/, 1]
|
|
254
|
+
@overhead = details[/overhead[:]*\s*(\S*)/, 1]
|
|
255
|
+
|
|
256
|
+
psnr_match = /PSNR=(.*)\s*size=/.match(details)
|
|
257
|
+
@psnr = psnr_match[1].strip if psnr_match
|
|
258
|
+
return true
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
#[mp3 @ 0x54340c]flv doesnt support that sample rate, choose from (44100, 22050, 11025)
|
|
262
|
+
#Could not write header for output file #0 (incorrect codec parameters ?)
|
|
263
|
+
|
|
264
|
+
raise TranscoderError::UnexpectedResult, details
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module RVideo
|
|
2
|
+
module Tools
|
|
3
|
+
class Ffmpeg2theora
|
|
4
|
+
include AbstractTool::InstanceMethods
|
|
5
|
+
|
|
6
|
+
attr_reader :raw_metadata
|
|
7
|
+
|
|
8
|
+
def tool_command
|
|
9
|
+
'ffmpeg2theora'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def format_video_quality(params={})
|
|
13
|
+
bitrate = params[:video_bit_rate].blank? ? nil : params[:video_bit_rate]
|
|
14
|
+
factor = (params[:scale][:width].to_f * params[:scale][:height].to_f * params[:fps].to_f)
|
|
15
|
+
case params[:video_quality]
|
|
16
|
+
when 'low'
|
|
17
|
+
" -v 1 "
|
|
18
|
+
when 'medium'
|
|
19
|
+
"-v 5 "
|
|
20
|
+
when 'high'
|
|
21
|
+
"-v 10 "
|
|
22
|
+
else
|
|
23
|
+
""
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def parse_result(result)
|
|
28
|
+
if m = /does not exist or has an unknown data format/.match(result)
|
|
29
|
+
raise TranscoderError::InvalidFile, "I/O error"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if m = /General output options/.match(result)
|
|
33
|
+
raise TranscoderError::InvalidCommand, "no command passed to ffmpeg2theora, or no output file specified"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
@raw_metadata = result.empty? ? "No Results" : result
|
|
37
|
+
return true
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|