module MovieMasher::ShellHelper

executes commands and optionally checks created files for duration

Public Class Methods

audio_command(path) click to toggle source
# File lib/util/shell_helper.rb, line 9
def audio_command(path)
  if path.to_s.empty? || !File.exist?(path)
    raise(Error::Parameter, "__audio_from_file with invalid path #{path}")
  end

  file_name = File.basename(path, File.extname(path))
  file_name = "#{file_name}-intermediate.#{Intermediate::AUDIO_EXTENSION}"
  out_file = Path.concat(File.dirname(path), file_name)
  switches = []
  switches << switch(path, 'i')
  switches << switch(2, 'ac')
  switches << switch(44_100, 'ar')
  exec_opts = {}
  exec_opts[:command] = switches.join
  exec_opts[:file] = out_file
  exec_opts
end
capture(cmd) click to toggle source
# File lib/util/shell_helper.rb, line 27
def capture(cmd)
  result = Open3.capture3(cmd)
  result = result.reject { |s| s.to_s.empty? }
  result = result.join("\n")
  # puts result
  # make sure result is utf-8 encoded
  enc_options = {}
  enc_options[:invalid] = :replace
  enc_options[:undef] = :replace
  enc_options[:replace] = '?'
  # enc_options[:universal_newline] = true
  result.encode(Encoding::UTF_8, **enc_options)
end
command(options) click to toggle source
# File lib/util/shell_helper.rb, line 41
def command(options)
  out_file = options[:file].to_s
  app = options[:app] || 'ffmpeg'
  app_path = MovieMasher.configuration["#{app}_path".to_sym]
  app_path = app if app_path.to_s.empty?
  cmd = "#{app_path} #{options[:command]}"
  cmd += " #{out_file}" unless out_file.empty?
  cmd
end
escape(string) click to toggle source
# File lib/util/shell_helper.rb, line 51
def escape(string)
  Shellwords.escape(string)
end
execute(options) click to toggle source
# File lib/util/shell_helper.rb, line 55
def execute(options)
  capture(command(options))
end
output_command(output, av_type, duration = nil) click to toggle source
# File lib/util/shell_helper.rb, line 59
def output_command(output, av_type, duration = nil)
  switches = []
  switches << switch(FloatUtil.string(duration), 't') if duration
  if AV::VIDEO_ONLY == av_type
    switches << switch('', 'an')
  else # we have audio output
    switches << switch(output[:audio_bitrate], 'b:a', 'k')
    switches << switch(output[:audio_rate], 'r:a')
    switches << switch(output[:audio_codec], 'c:a')
  end
  if AV::AUDIO_ONLY == av_type
    switches << switch('', 'vn')
  else # we have visuals
    case output[:type]
    when Type::VIDEO
      switches << switch(output[:dimensions], 's')
      switches << switch(output[:video_format], 'f:v')
      switches << switch(output[:video_codec], 'c:v')
      switches << switch(output[:video_bitrate], 'b:v', 'k')
      switches << switch(output[:video_rate], 'r:v')
    when Type::IMAGE
      switches << switch(output[:quality], 'q:v')
      switches << switch('1', 'vframes')
      unless output[:offset].to_s.empty?
        output_time = TimeRange.input_time(output, :offset)
        switches << switch(output_time, 'ss')
      end
    when Type::SEQUENCE
      switches << switch(output[:quality], 'q:v')
      switches << switch(output[:video_rate], 'r:v')
    end
  end
  switches << switch(output[:metadata], 'metadata')
  switches.join
end
raise_unless_rendered(result, cmd, options) click to toggle source
# File lib/util/shell_helper.rb, line 95
def raise_unless_rendered(result, cmd, options)
  logs = []
  out_file = options[:file].to_s
  outputs = !['', '/dev/null'].include?(out_file)
  dur = options[:duration]
  precision = options[:precision] || 1
  logs += __raise_if_no_file(out_file, cmd, result) if outputs
  if dur
    logs += __raise_unless_duration(result, dur, precision, out_file, cmd)
  end
  logs
end
set_output_commands(job, output) click to toggle source
# File lib/util/shell_helper.rb, line 108
def set_output_commands(job, output)
  output[:commands] = []
  switches = end_switches = ''
  audio_dur = video_dur = FloatUtil::ZERO
  rend_path, out_path = __job_paths(job, output)
  avb, v_graphs, a_graphs = __output_graphs(output, job)
  output_type = output[:type]
  a_or_v = Type::RAW_AVS.include?(output_type)
  unless AV::AUDIO_ONLY == avb
    if v_graphs.length == 1
      graph = v_graphs.first
      video_dur = graph.duration
      switches += __graph_command(graph, output)
    else
      ffconcat = 'ffconcat version 1.0'
      v_graphs.length.times do |index|
        graph = v_graphs[index]
        duration = graph.duration
        video_dur += duration
        out_file_name = "concat-#{index}.#{output[:extension]}"
        out_file = "#{out_path}#{out_file_name}"
        ffconcat += "\nfile '#{out_file_name}'\nduration #{duration}"
        out_command = output_command(output, AV::VIDEO_ONLY, duration)
        grph_command = __graph_command(graph, output)
        output[:commands] << {
          duration: duration,
          file: out_file, precision: output[:precision],
          command: "-y#{grph_command}#{out_command}#{switch('0', 'qp')}"
        }
      end
      file_path = "#{out_path}concat.txt"
      output[:commands] << { content: ffconcat, file: file_path }
      switches += switch("'#{file_path}'", 'i')
      end_switches += switch('copy', 'c:v')
    end
  end
  unless AV::VIDEO_ONLY == avb
    if __audio_raw(a_graphs)
      # just one non-looping graph, starting at zero with no gain change
      graph = a_graphs.first
      audio_dur = graph[:length]
      cmd_hash = audio_command(graph[:cached_file])
      output[:commands] << cmd_hash
      graph[:waved_file] = cmd_hash[:file]
    else
      # merge audio and feed resulting file to ffmpeg
      audio_cmd = ''
      a_len = a_graphs.length
      a_len.times do |index|
        graph = a_graphs[index]
        __raise_if_negative(graph[:start], "negative start time #{graph}")
        __raise_if_zero(graph[:length], "zero length #{graph}")
        audio_dur = FloatUtil.max(
          audio_dur, graph[:start] + graph[:length]
        )
        cmd_hash = audio_command(graph[:cached_file])
        output[:commands] << cmd_hash
        graph[:waved_file] = cmd_hash[:file]
        audio_cmd += __audio_graph(graph, index)
      end
      max_dur = FloatUtil.max(audio_dur, video_dur)
      audio_cmd += __audio_silence(a_len, max_dur)
      path = __audio_path(out_path, audio_cmd)
      output[:commands] << {
        app: 'ecasound', command: audio_cmd,
        precision: output[:precision],
        file: path, duration: max_dur
      }
      graph = {
        type: Type::AUDIO, offset: FloatUtil::ZERO, length: audio_dur,
        waved_file: path
      }
    end
    # audio graph now represents just one file
    if Type::WAVEFORM == output_type
      output[:commands] << {
        app: 'audiowaveform', file: rend_path,
        command: __waveform_switches(graph, output)
      }
    else
      switches += switch(graph[:waved_file], 'i')
      end_switches += __audio_switches(graph, audio_dur)
    end
  end
  return if switches.empty?

  # we've got audio and/or video
  max_dur = FloatUtil.max(audio_dur, video_dur)
  duration = __output_duration(a_or_v, max_dur)
  out_command = output_command(output, avb, duration)
  output[:commands] << {
    file: rend_path, precision: output[:precision],
    pass: __is_two_pass(a_or_v, v_graphs),
    duration: __type_duration(output_type, max_dur),
    command: "-y #{switches + end_switches}#{out_command}"
  }
end
switch(value, prefix = '', suffix = '', dont_escape: false) click to toggle source
# File lib/util/shell_helper.rb, line 206
def switch(value, prefix = '', suffix = '', dont_escape: false)
  cmd = ''
  if value
    value = value.to_s.strip
    unless dont_escape
      splits = Shellwords.split(value)
      splits = splits.map { |word| escape(word) }
      # puts "SPLITS: #{splits}" if 1 < splits.length
      value = splits.join(' ')
    end
    cmd += ' ' # always add a leading space
    if value.start_with?('-') # it's a switch, include and ignore rest
      cmd += value
    else # prepend value with prefix and space
      cmd += '-' unless prefix.start_with?('-')
      cmd += prefix
      cmd += " #{value}" unless value.empty?
      cmd += suffix unless cmd.end_with?(suffix) # NOTE: lack of space!
    end
  end
  cmd
end