Class: SeleniumScreencast::Encoder

Inherits:
Object
  • Object
show all
Defined in:
lib/selenium_screencast/encoder.rb

Overview

Encodes a frame sequence to webm (VP9) via the ffmpeg concat demuxer. Composed of pure computation (dropping static frames, computing durations, building concat.txt) plus a single ffmpeg call, so the computation part can be unit tested.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(frame_store, output_path:, config: SeleniumScreencast.config) ⇒ Encoder

Returns a new instance of Encoder.



27
28
29
30
31
# File 'lib/selenium_screencast/encoder.rb', line 27

def initialize(frame_store, output_path:, config: SeleniumScreencast.config)
  @frame_store = frame_store
  @output_path = output_path
  @config = config
end

Class Method Details

.file_extension(format) ⇒ Object



11
12
13
# File 'lib/selenium_screencast/encoder.rb', line 11

def self.file_extension(format)
  format_spec(format).fetch(:ext)
end

.format_spec(format) ⇒ Object

Returns the VIDEO_FORMATS entry for a format symbol. An unsupported value is converted into the unified-contract SeleniumScreencast::Error instead of a plain KeyError (matching callers that only rescue SeleniumScreencast::Error).



19
20
21
22
23
24
25
# File 'lib/selenium_screencast/encoder.rb', line 19

def self.format_spec(format)
  SeleniumScreencast::VIDEO_FORMATS.fetch(format) do
    raise SeleniumScreencast::Error,
          "Unsupported video format: #{format.inspect} " \
          "(supported: #{SeleniumScreencast::VIDEO_FORMATS.keys.join(", ")})"
  end
end

Instance Method Details

#build_concat_list(frames) ⇒ Object

Builds the list string for the ffmpeg concat demuxer. The last file is listed twice because the concat demuxer interprets each duration as "the display time until the next file", so giving the last frame a display time requires listing the same file once more.



62
63
64
65
66
67
68
69
70
# File 'lib/selenium_screencast/encoder.rb', line 62

def build_concat_list(frames)
  durations = frame_durations(frames)
  lines =
    frames.each_with_index.flat_map do |frame, i|
      ["file '#{frame[:file]}'", "duration #{durations[i].round(3)}"]
    end
  lines << "file '#{frames.last[:file]}'"
  "#{lines.join("\n")}\n"
end

#drop_leading_static_frames(frames) ⇒ Object

Drops the leading static frames where "nothing has changed yet". Right after a page loads, the same screen tends to repeat, so frames up to the first change are discarded so the video starts from actual interaction. If every frame is identical, only the last one is kept.



50
51
52
53
54
55
56
# File 'lib/selenium_screencast/encoder.rb', line 50

def drop_leading_static_frames(frames)
  first_changed_index =
    frames.each_cons(2).find_index { |prev, curr| !same_frame_content?(prev, curr) }
  return frames.last(1) if first_changed_index.nil?

  frames.drop(first_changed_index + 1)
end

#encodeObject



33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/selenium_screencast/encoder.rb', line 33

def encode
  frames = drop_leading_static_frames(@frame_store.frames)
  return if frames.empty?

  # Resolve @config.format (raises Error on an invalid value) and check
  # that ffmpeg is available before doing wasted work like writing
  # concat.txt.
  format_spec = self.class.format_spec(@config.format)
  ensure_ffmpeg_available!
  concat_path = @frame_store.write_concat_list(build_concat_list(frames))
  run(ffmpeg_argv(concat_path, format_spec))
end