Class: SeleniumScreencast::Encoder
- Inherits:
-
Object
- Object
- SeleniumScreencast::Encoder
- 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
- .file_extension(format) ⇒ Object
-
.format_spec(format) ⇒ Object
Returns the VIDEO_FORMATS entry for a format symbol.
Instance Method Summary collapse
-
#build_concat_list(frames) ⇒ Object
Builds the list string for the ffmpeg concat demuxer.
-
#drop_leading_static_frames(frames) ⇒ Object
Drops the leading static frames where "nothing has changed yet".
- #encode ⇒ Object
-
#initialize(frame_store, output_path:, config: SeleniumScreencast.config) ⇒ Encoder
constructor
A new instance of Encoder.
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 |
#encode ⇒ Object
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 |