class VideoConverter::Ffmpeg
Attributes
aliases[RW]
bin[RW]
concat_command[RW]
crop_detect_command[RW]
defaults[RW]
ffprobe_bin[RW]
first_pass_command[RW]
key_frames_command[RW]
mux_command[RW]
one_pass_command[RW]
second_pass_command[RW]
split_command[RW]
volume_detect_command[RW]
input[RW]
outputs[RW]
Public Class Methods
concat(inputs, output, method = nil)
click to toggle source
# File lib/video_converter/ffmpeg.rb, line 55 def self.concat(inputs, output, method = nil) method = %w(ts mpg mpeg).include?(File.extname(inputs.first.to_s).delete('.')) ? :protocol : :muxer unless method output.options = { :codec => 'copy' }.merge(output.options) send("concat_#{method}", inputs, output) end
mux(inputs, output)
click to toggle source
# File lib/video_converter/ffmpeg.rb, line 61 def self.mux(inputs, output) output.options = { :codec => 'copy' }.merge(output.options) Command.new(mux_command, prepare_params(nil, output).merge({ :inputs => { '-i' => inputs }, :maps => { '-map' => inputs.each_with_index.map { |_,i| "#{i}:0" }.join(' ') } })).execute end
new(input, outputs)
click to toggle source
# File lib/video_converter/ffmpeg.rb, line 71 def initialize input, outputs self.input = input self.outputs = input.select_outputs(outputs) self.outputs.each do |output| # volume output.options[:audio_filter] = "volume=#{volume(output.volume)}" if output.volume unless output.options[:vn] # autorotate if output.type != 'playlist' && output.rotate == true output.rotate = input.metadata[:rotate] ? 360 - input.metadata[:rotate] : nil end # autocrop output.crop = input.crop_detect if output.type != 'playlist' && output.crop == true # autodeinterlace output.options[:deinterlace] = input.metadata[:interlaced] if output.options[:deinterlace].nil? # filter_complex filter_complex = [] filter_complex << "crop=#{output.crop.shellescape}" if output.crop if output.width || output.height output.width = (output.height * aspect(input, output)).ceil / 2 * 2 if output.height && !output.width output.height = (output.width / aspect(input, output)).ceil / 2 * 2 if output.width && !output.height filter_complex << "scale=#{scale(output.width, :w)}:#{scale(output.height, :h)}" if output.options[:aspect] filter_complex << "setdar=#{output.options.delete(:aspect).to_s.shellescape}" elsif input.video_stream[:dar_width] && input.video_stream[:dar_height] filter_complex << "setdar=#{aspect(input, output)}" end end if output.watermarks && (output.watermarks[:width] || output.watermarks[:height]) filter_complex = ["[0:v] #{filter_complex.join(',')} [main]"] filter_complex << "[1:v] scale=#{scale(output.watermarks[:width], :w, output.width)}:#{scale(output.watermarks[:height], :h, output.height)} [overlay]" filter_complex << "[main] [overlay] overlay=#{overlay(output.watermarks[:x], :w)}:#{overlay(output.watermarks[:y], :h)}" if output.rotate filter_complex[filter_complex.count-1] += ' [overlayed]' filter_complex << '[overlayed] ' + rotate(output.rotate) end output.options[:filter_complex] = "'#{filter_complex.join(';')}'" else filter_complex << "overlay=#{overlay(output.watermarks[:x], :w)}:#{overlay(output.watermarks[:y], :h)}" if output.watermarks filter_complex << rotate(output.rotate) if output.rotate output.options[:filter_complex] = filter_complex.join(',') if filter_complex.any? end else output.options.delete(:deinterlace) output.options.delete(:filter_complex) end output.options[:format] ||= File.extname(output.filename).delete('.') output.options[:format] = 'mpegts' if output.options[:format] == 'ts' output.options[:movflags] = '+faststart' if output.faststart || (output.faststart.nil? && %w(mov mp4).include?(output.options[:format].downcase)) unless output.type == 'playlist' output.options = self.class.defaults.merge(output.options) output.options[:keyint_min] ||= output.options[:frame_rate] output.options[:keyframe_interval] = output.options[:keyint_min] * Output.keyframe_interval_in_seconds end end end
split(input, output)
click to toggle source
# File lib/video_converter/ffmpeg.rb, line 50 def self.split(input, output) output.options = { :format => 'segment', :map => 0, :codec => 'copy' }.merge(output.options) Command.new(split_command, prepare_params(input, output)).execute end
Private Class Methods
concat_muxer(inputs, output)
click to toggle source
# File lib/video_converter/ffmpeg.rb, line 180 def self.concat_muxer(inputs, output) list = File.join(output.work_dir, 'list.txt') # NOTE ffmpeg concat list requires unescaped files File.write(list, inputs.map { |input| "file '#{File.absolute_path(input.to_s)}'" }.join("\n")) success = Command.new(concat_command, prepare_params(list, output)).execute FileUtils.rm list if success success end
concat_protocol(inputs, output)
click to toggle source
# File lib/video_converter/ffmpeg.rb, line 189 def self.concat_protocol(inputs, output) Command.new(one_pass_command, prepare_params('"concat:' + inputs.map { |input| input.to_s.shellescape }.join('|') + '"', output), [:inputs]).execute end
prepare_params(input, output)
click to toggle source
# File lib/video_converter/ffmpeg.rb, line 202 def self.prepare_params input, output { :bin => bin, :inputs => { '-i' => output.watermarks ? [input.to_s, output.watermarks[:url]] : input.to_s }, :options => Hash[*output.options.select { |option, values| !output.respond_to?(option) }.map do |option, values| ['-' + (aliases[option] || option).to_s, values] end.flatten(1)], :output => output.ffmpeg_output, :log => output.log } end
Public Instance Methods
run()
click to toggle source
# File lib/video_converter/ffmpeg.rb, line 129 def run success = true threads = [] input.output_groups(outputs).each_with_index do |group, group_index| qualities = group.select { |output| output.type != 'playlist' } # common first pass if !one_pass?(qualities) && common_first_pass?(qualities) qualities.each do |output| output.options[:passlogfile] = File.join(output.work_dir, "group#{group_index}.log") end best_quality = qualities.sort do |q1, q2| res = q1.options[:video_bitrate].to_i <=> q2.options[:video_bitrate].to_i res = q1.height.to_i <=> q2.height.to_i if res == 0 res = q1.width.to_i <=> q2.width.to_i if res == 0 # TODO compare by size res end.last success &&= Command.new(self.class.first_pass_command, self.class.prepare_params(input, best_quality), ['-filter_complex']).execute end qualities.each_with_index do |output, output_index| command = if one_pass?(qualities) Command.new(self.class.one_pass_command, self.class.prepare_params(input, output), ['-filter_complex']) elsif common_first_pass?(qualities) Command.new(self.class.second_pass_command, self.class.prepare_params(input, output), ['-filter_complex']) else output.options[:passlogfile] = File.join(output.work_dir, "group#{group_index}_#{output_index}.log") output.options[:force_key_frames] = input.metadata[:video_start_time].step(input.metadata[:duration_in_ms] / 1000.0, Output.keyframe_interval_in_seconds).map(&:floor).join(',') output.options[:sc_threshold] = 0 output.options[:keyint_min] = output.options[:keyframe_interval] = nil Command.new(self.class.first_pass_command, self.class.prepare_params(input, output), ['-filter_complex']).append( Command.new(self.class.second_pass_command, self.class.prepare_params(input, output), ['-filter_complex']) ) end # run ffmpeg if VideoConverter.paral threads << Thread.new { success &&= command.execute } else success &&= command.execute end end end threads.each { |t| t.join } if VideoConverter.paral success end
Private Instance Methods
aspect(input, output)
click to toggle source
# File lib/video_converter/ffmpeg.rb, line 244 def aspect(input, output) if aspect = output.options[:aspect] if matches = aspect.to_s.match(/^(\d+):(\d+)$/) matches[1].to_f / matches[2].to_f else Float(aspect) end else width_smaller_in = output.crop ? input.video_stream[:width].to_f / crop_parse(output.crop)[:width].to_f : 1 height_smaller_in = output.crop ? input.video_stream[:height].to_f / crop_parse(output.crop)[:height].to_f : 1 ((input.video_stream[:dar_width] || input.video_stream[:width]).to_f / width_smaller_in) / ((input.video_stream[:dar_height] || input.video_stream[:height]).to_f / height_smaller_in) end end
common_first_pass?(qualities)
click to toggle source
# File lib/video_converter/ffmpeg.rb, line 193 def common_first_pass?(qualities) # if group qualities have different sizes use force_key_frames and separate first passes qualities.uniq { |o| o.height }.count == 1 && qualities.uniq { |o| o.width }.count == 1 && qualities.uniq { |o| o.options[:size] }.count == 1 end
crop_parse(crop)
click to toggle source
# File lib/video_converter/ffmpeg.rb, line 259 def crop_parse(crop) crop.match(/^(?:w=)?(?<width>\d+):(?:h=)?(?<height>\d+)(?::(?:x=)?(?<h>\d+):(?:y=)?(?<y>\d+))?$/) or raise 'Unsupported crop format' end
one_pass?(qualities)
click to toggle source
# File lib/video_converter/ffmpeg.rb, line 198 def one_pass?(qualities) qualities.inject(true) { |r, q| r && q.one_pass } end
overlay(xy, wh)
click to toggle source
# File lib/video_converter/ffmpeg.rb, line 223 def overlay(xy, wh) if xy.to_s.end_with?('%') xy = xy.to_f / 100 xy < 0 ? "main_#{wh}*#{1 + xy}-overlay_#{wh}" : "main_#{wh}*#{xy}" else xy.to_i < 0 ? "main_#{wh}-overlay_#{wh}#{xy}" : xy.to_i end end
rotate(angle)
click to toggle source
# File lib/video_converter/ffmpeg.rb, line 232 def rotate(angle) { 90 => 'transpose=2', 180 => 'transpose=2,transpose=2', 270 => 'transpose=1' }[angle] end
scale(size, wh, percent_of = nil)
click to toggle source
# File lib/video_converter/ffmpeg.rb, line 215 def scale(size, wh, percent_of = nil) if size.to_s.end_with?('%') percent_of ? (percent_of * size.to_f / 100).to_i : "i#{wh}*#{size.to_f/100}" else size || "trunc\\(o#{{:h => 'w/', :w => 'h*'}[wh]}a/2\\)*2" end end
volume(level)
click to toggle source
# File lib/video_converter/ffmpeg.rb, line 236 def volume(level) if level.to_s.end_with?('dB') (level.to_f - input.mean_volume.to_f).round(4).to_s + 'dB' else level end end