Class: Aspera::Preview::Utils

Inherits:
Object
  • Object
show all
Defined in:
lib/aspera/preview/utils.rb

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.office_tool ⇒ Object

Parameters:

  • tool (Symbol) —

    either unoconv or soffice



29
30
31
# File 'lib/aspera/preview/utils.rb', line 29

def office_tool
  @office_tool
end

Class Method Details

.available_h264_encoder ⇒ String

Return the first H.264 encoder available in the local ffmpeg installation. Result is memoized after the first call.

Returns:

  • (String) —

    encoder name (e.g. 'libx264', 'libopenh264')

Raises:

  • (RuntimeError) —

    if no supported H.264 encoder is found



35
36
37
38
39
40
41
42
# File 'lib/aspera/preview/utils.rb', line 35

def available_h264_encoder
  return @available_h264_encoder if defined?(@available_h264_encoder)
  stdout, = execute(:ffmpeg, '-encoders', mode: :capture, exception: false)
  available = stdout.lines.grep(/h264/i).map { |l| l.split[1] }
  @available_h264_encoder = H264_ENCODER_PREFERENCE.find { |enc| available.include?(enc) }
  Aspera.assert(@available_h264_encoder) { "No supported H.264 encoder found in ffmpeg. Available: #{available.join(', ')}" }
  @available_h264_encoder
end

.check_tools(skip_types = []) ⇒ nil

Check that external tools can be executed.

Parameters:

  • skip_types (Array<Symbol>) (defaults to: []) —

    list of tools to skip

Returns:

  • (nil)

Raises:

  • (RuntimeError) —

    if a required tool binary is missing



48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/aspera/preview/utils.rb', line 48

def check_tools(skip_types = [])
  tools_to_check = EXTERNAL_TOOLS.dup
  tools_to_check.delete(:unoconv) if skip_types.include?(:office) || office_tool.eql?(:soffice)
  tools_to_check.delete(:soffice) if skip_types.include?(:office) || office_tool.eql?(:unoconv)
  # Check for binaries
  tools_to_check.each do |command_sym|
    silent_execute(command_sym, '-h')
  rescue Errno::ENOENT => e
    raise "missing #{command_sym} binary: #{e}"
  rescue
    nil
  end
end

.execute(*args, **kwargs) ⇒ Array<String>

Execute external command, verify it is in the supported list.

Parameters:

Returns:

  • (Array<String>) —

    captured stdout and stderr lines depending on mode

Raises:



67
68
69
70
# File 'lib/aspera/preview/utils.rb', line 67

def execute(*args, **kwargs)
  Aspera.assert_values(args.first, EXTERNAL_TOOLS) { 'command' }
  Environment.secure_execute(*args, **kwargs)
end

.ffmpeg(in:, out:, global: FFMPEG_DEFAULT_PARAMS) ⇒ nil

Execute ffmpeg, capturing output. On failure, the ffmpeg stderr is logged at debug level and re-raised.

Parameters:

  • in (Array) —

    input file path followed by input options

  • out (Array) —

    output file path followed by output options

  • global (Array<String>) (defaults to: FFMPEG_DEFAULT_PARAMS) —

    global options for ffmpeg

Returns:

  • (nil)


87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/aspera/preview/utils.rb', line 87

def ffmpeg(in:, out:, global: FFMPEG_DEFAULT_PARAMS)
  Aspera.assert_type(global, Array)
  # NOTE: cannot use just "in", as it is a reserved word in ruby
  in_args = binding.local_variable_get(:in).dup
  out_args = out.dup
  Aspera.assert_type(in_args, Array)
  Aspera.assert_type(out_args, Array)
  in_file = in_args.shift
  out_file = out_args.shift
  execute(:ffmpeg, *global, *in_args, '-i', in_file, *out_args, out_file, mode: :capture)
  nil
end

.ffmpeg_fmt(temp_folder) ⇒ String

File output pattern for ffmpeg, including temp folder.

Parameters:

  • temp_folder (String) —

    path to temp folder

Returns:

  • (String) —

    file path pattern



117
118
119
# File 'lib/aspera/preview/utils.rb', line 117

def ffmpeg_fmt(temp_folder)
  return File.join(temp_folder, TEMP_FORMAT)
end

.get_tmp_num_filepath(temp_folder, file_number) ⇒ String

Get numbered temporary file path.

Parameters:

  • temp_folder (String) —

    path to temp folder

  • file_number (Integer) —

    frame index

Returns:



125
126
127
# File 'lib/aspera/preview/utils.rb', line 125

def get_tmp_num_filepath(temp_folder, file_number)
  return File.join(temp_folder, format(TEMP_FORMAT, file_number))
end

.parse_magick_fonts(output) ⇒ Hash

Parse the output of magick identify -list font command

Parameters:

  • output (String) —

    the output from magick -list font

Returns:

  • (Hash) —

    with keys :path and :fonts :path [String] the path to the type.xml file :fonts [Array] array of font hashes with keys:

    :name, :family, :style, :stretch, :weight, :metrics, :glyphs, :index


178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/aspera/preview/utils.rb', line 178

def parse_magick_fonts(output)
  result = {path: nil, fonts: []}
  current_font = nil
  output.each_line do |line|
    line = line.strip
    # Parse the Path line
    if line.start_with?('Path:')
      result[:path] = line.sub(/^Path:\s*/, '')
    # Parse Font name
    elsif line.start_with?('Font:')
      # Save previous font if exists
      result[:fonts] << current_font if current_font
      # Start new font
      current_font = {name: line.sub(/^Font:\s*/, '')}
    # Parse font properties
    elsif current_font && line.include?(':')
      key, value = line.split(':', 2)
      key = key.strip.gsub(/\s+/, '_').to_sym
      value = value.strip
      # Convert numeric values
      value = value.to_i if key == :weight || key == :index
      current_font[key] = value
    end
  end
  # Don't forget the last font
  result[:fonts] << current_font if current_font
  result
end

.silent_execute(*args) ⇒ nil

Execute external command, capturing and discarding output unless it fails. On failure, the captured stderr is included in the raised exception message.

Parameters:

  • args (Array) —

    command name followed by CLI arguments

Returns:

  • (nil)


76
77
78
79
# File 'lib/aspera/preview/utils.rb', line 76

def silent_execute(*args)
  execute(*args, mode: :capture)
  nil
end

.video_blend_frames(temp_folder, index_begin, index_end) ⇒ nil

Blend transition frames between two keyframes using ImageMagick.

Parameters:

  • temp_folder (String) —

    path to temp folder

  • index_begin (Integer) —

    starting frame index

  • index_end (Integer) —

    ending frame index

Returns:

  • (nil)


147
148
149
150
151
152
153
154
155
156
157
# File 'lib/aspera/preview/utils.rb', line 147

def video_blend_frames(temp_folder, index_begin, index_end)
  img1 = get_tmp_num_filepath(temp_folder, index_begin)
  img2 = get_tmp_num_filepath(temp_folder, index_end)
  count = index_end - index_begin - 1
  1.upto(count) do |i|
    percent = i * 100 / (count + 1)
    filename = get_tmp_num_filepath(temp_folder, index_begin + i)
    silent_execute(:magick, 'composite', '-blend', percent, img2, img1, filename)
  end
  nil
end

.video_dump_frame(input_file, offset_seconds, scale, output_file) ⇒ nil

Dump a frame from a video file

Parameters:

  • input_file (String) —

    the input file path

  • offset_seconds (Integer) —

    the offset in seconds

  • scale (String) —

    the scale of the output frame

  • output_file (String) —

    the output file path

Returns:

  • (nil)


165
166
167
168
169
170
# File 'lib/aspera/preview/utils.rb', line 165

def video_dump_frame(input_file, offset_seconds, scale, output_file)
  ffmpeg(
    in:  [input_file, '-ss', offset_seconds],
    out: [output_file, '-frames:v', 1, '-filter:v', "scale='#{scale}'"]
  )
end

.video_dupe_frame(temp_folder, index, count) ⇒ nil

Duplicate a video frame by creating symlinks.

Parameters:

  • temp_folder (String) —

    path to temp folder

  • index (Integer) —

    frame index to duplicate

  • count (Integer) —

    number of duplicate frames to create

Returns:

  • (nil)


134
135
136
137
138
139
140
# File 'lib/aspera/preview/utils.rb', line 134

def video_dupe_frame(temp_folder, index, count)
  input_file = get_tmp_num_filepath(temp_folder, index)
  1.upto(count) do |i|
    FileUtils.ln_s(input_file, get_tmp_num_filepath(temp_folder, index + i))
  end
  nil
end

.video_get_duration(input_file) ⇒ Float

Get duration of a video file using ffprobe.

Parameters:

  • input_file (String) —

    path to video file

Returns:

  • (Float) —

    duration in seconds



103
104
105
106
107
108
109
110
111
112
# File 'lib/aspera/preview/utils.rb', line 103

def video_get_duration(input_file)
  return execute(
    :ffprobe,
    '-loglevel', 'error',
    '-show_entries', 'format=duration',
    '-print_format', 'default=noprint_wrappers=1:nokey=1', # cspell:disable-line
    input_file,
    mode: :capture
  ).first.to_f
end