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