Class: TestRecorder::CdpRecorder

Inherits:
Object
  • Object
show all
Defined in:
lib/test_recorder/cdp_recorder.rb

Defined Under Namespace

Classes: Record

Constant Summary collapse

FFMPEG_ENCODE_OPTIONS =
%w[-y -an -r 25 -c:v vp8 -qmin 0 -qmax 50 -crf 8 -deadline realtime -speed 8 -b:v 1M -threads 1].freeze
FFMPEG_DURATION_SLACK_S =

ffmpeg's -t treats the given duration as an exclusive cutoff, dropping the very frame that marks the video's real length if it lands exactly on it. This nudges -t just past that frame so it is kept, without adding a duration long enough to be noticeable.

0.01

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(enabled:) ⇒ CdpRecorder

Returns a new instance of CdpRecorder.



73
74
75
76
# File 'lib/test_recorder/cdp_recorder.rb', line 73

def initialize(enabled:)
  @enabled = enabled
  @started = nil
end

Class Method Details

.record(devtools) ⇒ Object



19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/test_recorder/cdp_recorder.rb', line 19

def record(devtools)
  records[devtools] ||= begin
    page = devtools.page
    page.enable

    record = Record.new(page)
    page.on(:screencast_frame) do |event|
      write_frame(record, event)
      record.page.screencast_frame_ack(session_id: event["sessionId"])
    end
    record
  end
end

Instance Method Details

#start(page:, enabled: nil) ⇒ Object



78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/test_recorder/cdp_recorder.rb', line 78

def start(page:, enabled: nil)
  enabled = @enabled if enabled.nil?
  @started = enabled
  return unless @started

  @tmp_video = Tempfile.new(["testrecorder", ".mkv"])
  @tmp_video.binmode

  @record = self.class.record(page.driver.browser.devtools)
  @frame_writer = FrameWriter.new(@tmp_video)
  @record.io = @frame_writer

  @record.page.start_screencast(format: "jpeg", quality: TestRecorder.jpeg_quality, max_width: TestRecorder.max_dimension, max_height: TestRecorder.max_dimension, every_nth_frame: TestRecorder.every_nth_frame)
end

#stop_and_discardObject



93
94
95
96
97
98
99
# File 'lib/test_recorder/cdp_recorder.rb', line 93

def stop_and_discard
  return unless @started

  @record.io = nil
  @record.page.stop_screencast
  @tmp_video.close!
end

#stop_and_save(filename) ⇒ Object



101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/test_recorder/cdp_recorder.rb', line 101

def stop_and_save(filename)
  return "" unless @started

  @record.io = nil
  @record.page.stop_screencast
  @frame_writer.finish
  @tmp_video.flush

  # Chrome sends a frame as soon as the screencast starts, so a test that fails
  # before it draws anything still leaves behind the one frame of the blank page
  # it started on.
  if @frame_writer.frame_count < 2
    warn "[TestRecorder] The screencast captured no page updates, so no video was saved."
    @tmp_video.close!
    return ""
  end

  video_dir = ::Rails.root.join("tmp", "videos")
  FileUtils.mkdir_p(video_dir)
  video_path = video_dir.join(filename).to_s

  # Each frame carries its own timestamp in the Matroska stream, so ffmpeg knows how long to
  # hold it and duplicates frames to reach the constant output frame rate on its own. Without a
  # known end, though, ffmpeg pads however long it likes past the last real timestamp, so -t
  # caps the output at the video's real duration explicitly.
  duration = @frame_writer.duration + FFMPEG_DURATION_SLACK_S
  result = system("ffmpeg", "-loglevel", "error", "-f", "matroska", "-i", @tmp_video.path, "-t", duration.to_s, *FFMPEG_ENCODE_OPTIONS, video_path)

  @tmp_video.close!

  if result.nil?
    warn "[TestRecorder] Failed to execute ffmpeg. Please make sure that FFmpeg is installed."
    return ""
  elsif !result
    warn "[TestRecorder] ffmpeg failed to encode #{video_path}."
    return ""
  end

  video_path
end