SeleniumScreencast
Record your Capybara/Selenium system specs as webm or mp4 video, using the
Chrome DevTools Protocol (CDP) screencast API. SeleniumScreencast captures
the JPEG frame stream from Page.startScreencast, then encodes it into a
webm or mp4 video with ffmpeg — no browser extensions or external
recording tools required.
Requirements
ffmpegavailable onPATH(or pointFFMPEG_BIN/ffmpeg_binat it). Formp4output,ffmpegmust include an H.264 (libx264) encoder.- A Chrome/Chromium-based driver used through
selenium-webdriver, since recording relies on the CDPdevtoolsconnection exposed by Selenium's Chrome bridge (e.g. system specs driven by:selenium_chrome_headlessor similar)
Installation
Add the gem to your application's test group:
bundle add selenium_screencast --group test
Usage
RSpec / Capybara
Require the RSpec adapter once, e.g. in rails_helper.rb or a spec support
file:
require "selenium_screencast/rspec"
This automatically records every type: :system example when recording is
enabled, and by default saves a video after each example finishes (regardless
of pass/fail). See Recording only failed specs
to keep videos only for failures.
Enable recording by setting RECORD_VIDEO when running specs:
RECORD_VIDEO=1 bundle exec rspec spec/system/checkout_spec.rb
Videos are written as webm (or mp4, depending on config.format) files
under the configured output_dir (Rails.root.join("tmp/videos") in a Rails
app, or tmp/videos relative to the working directory otherwise). Each file
is named after the example's full description, sanitized to a safe filename
with a random suffix to avoid collisions, e.g.
Checkout_completes_purchase_a1b2c3d4.webm (or ....mp4 when format is
:mp4).
Configuration
Configure defaults in an initializer or spec support file:
SeleniumScreencast.configure do |config|
config.slow_factor = 4 # default: 8.0
config.output_fps = 30 # default: 30
config.ffmpeg_bin = "ffmpeg" # default: "ffmpeg"
config.quality = 80 # default: 80
config.every_nth_frame = 1 # default: 1
config.last_frame_duration = 0.5 # default: 0.5 (seconds, before slow_factor)
config.inflight_delay = 0.2 # default: 0.2 (seconds, wait before stopping)
config.output_dir = "tmp/videos" # default: Rails.root/tmp/videos, or tmp/videos
config.debug = false # default: false
config.format = :webm # default: :webm (:webm or :mp4)
config.failed_only = false # default: false
end
| Option | Description | Default |
|---|---|---|
slow_factor |
Playback slow-motion multiplier applied to inter-frame durations | 8.0 |
output_fps |
Fixed output frame rate (CFR) that ffmpeg re-encodes to | 30 |
ffmpeg_bin |
Path to the ffmpeg executable |
"ffmpeg" |
quality |
JPEG quality requested from the CDP screencast; must be 0-100 | 80 |
every_nth_frame |
Only capture every Nth screencast frame from Chrome; must be >= 1 | 1 |
last_frame_duration |
Seconds the final frame is held for, before slow_factor is applied |
0.5 |
inflight_delay |
Seconds to wait after stopping the screencast, to let in-flight frames land. Skipped when the frames are discarded without encoding | 0.2 |
output_dir |
Directory videos are written to | Rails.root/tmp/videos (Rails) or tmp/videos |
debug |
Keep the intermediate JPEG frame files (and concat list) instead of deleting them after encoding | false |
format |
Output video format (:webm or :mp4); determines container and codec |
:webm |
failed_only |
Only keep the video for examples that failed; passing examples are recorded but never encoded | false |
Precedence: environment variable > configure value > default. When one
of the environment variables below is set, it always wins over a value set
via SeleniumScreencast.configure.
Clearing previous videos
When recording is enabled (RECORD_VIDEO is set), output_dir is emptied
once at the start of each rspec run, so this run's videos aren't mixed with
stale files from previous runs. This happens automatically via the
selenium_screencast/rspec adapter's before(:suite) hook — no
configuration needed.
When RECORD_VIDEO is unset, the output dir is left untouched (the
"disabled -> nothing happens" contract).
The cleanup runs at most once per process. With parallel_tests,
before(:suite) fires once per RSpec worker process, so each worker empties
the shared output dir at startup (not exactly once across all processes).
Because the workers share one output_dir and start at different times, a
slower-starting worker can wipe videos a faster worker already recorded in
the same run. If you rely on parallel_tests, give each worker its own
output_dir (e.g. keyed by TEST_ENV_NUMBER) so a worker only clears its
own directory.
Only the directory contents are removed; the directory itself is kept.
Environment variables
| Variable | Effect | Default |
|---|---|---|
RECORD_VIDEO |
Enables recording when present (any non-empty value) | disabled |
RECORD_VIDEO_TESTS |
Comma-separated spec file paths to record. When set (alongside RECORD_VIDEO), only matching specs are recorded. See Recording only specific specs. |
all specs |
RECORD_VIDEO_TESTS_FILE |
Path to a file listing spec paths (one per line) to record. Combined (union) with RECORD_VIDEO_TESTS. A missing file raises an error. |
unset |
RECORD_VIDEO_FAILED_ONLY |
Only keep the video for examples that failed, when present (any non-empty value). See Recording only failed specs. | disabled |
RECORD_SLOW_FACTOR |
Overrides the playback slow-motion multiplier | 8 |
RECORD_QUALITY |
Overrides the JPEG quality (0-100) requested from the screencast | 80 |
RECORD_EVERY_NTH_FRAME |
Overrides how many screencast frames to skip (capture every Nth frame; must be >= 1) | 1 |
FFMPEG_BIN |
Overrides the path to the ffmpeg executable |
ffmpeg |
RECORD_DEBUG |
Keeps intermediate frame files on disk when present | disabled |
RECORD_VIDEO_FORMAT |
Overrides the output format (webm / mp4) |
webm |
Recording only specific specs
By default, when RECORD_VIDEO is enabled, every type: :system example is
recorded. When you run the full suite but only want video for a few specs — for
example, to review a newly added spec — narrow recording to specific spec files
with RECORD_VIDEO_TESTS (comma-separated) and/or RECORD_VIDEO_TESTS_FILE
(one path per line). All the specs still run; only the listed ones are
recorded.
# Inline list — the whole suite runs; only these two specs are recorded
RECORD_VIDEO=1 RECORD_VIDEO_TESTS="spec/system/checkout_spec.rb,spec/system/login_spec.rb" \
bundle exec rspec
# List from a file (e.g. generated by CI from the changed specs)
RECORD_VIDEO=1 RECORD_VIDEO_TESTS_FILE=tmp/record_targets.txt bundle exec rspec
When both RECORD_VIDEO_TESTS and RECORD_VIDEO_TESTS_FILE are set, the union
of the two lists is used.
Behavior summary:
RECORD_VIDEO |
Target list (RECORD_VIDEO_TESTS / RECORD_VIDEO_TESTS_FILE) |
Result |
|---|---|---|
| unset | (ignored) | Recording disabled entirely; the target list is never read. |
| set | both unset | Every system spec is recorded (backward-compatible default). |
| set | set, with matching paths | Only the matching spec files are recorded. |
| set | set, but effectively empty (blank values only) | Nothing is recorded — an empty selection does not fall back to recording everything. |
Paths are compared after normalizing against Rails.root (or the working
directory when not running under Rails), so relative paths (spec/system/...,
./spec/system/...) and absolute paths all match the same spec.
Recording only failed specs
When running the full suite in CI, you typically don't want a video for every
passing example — only for the ones that failed, so they can be attached as
build artifacts. Set RECORD_VIDEO_FAILED_ONLY (or config.failed_only) to
do this:
RECORD_VIDEO=1 RECORD_VIDEO_FAILED_ONLY=1 bundle exec rspec
Honest overhead note: whether an example passed or failed is only known
after it finishes, so CDP frame capture still runs for every example —
this can't be avoided. What's actually skipped is ffmpeg encoding, which is
the dominant cost of recording. For examples that pass, ffmpeg is never
invoked, so neither the video file nor its intermediate frame directory
(*_frames) is ever created — encoding isn't skipped by deleting output
afterward, it's skipped from the start. The inflight_delay wait is skipped
too, since it only exists to let a trailing frame make it into the video.
Whether a video is kept depends on the example's outcome:
| Example outcome | Video kept? |
|---|---|
| passed | No |
| failed | Yes |
failed via aggregate_failures |
Yes |
failed in a user's own after hook |
Yes |
| pending, failed as expected | No (a known failure doesn't need a video) |
| pending but unexpectedly passed | Yes (this is a real failure) |
| skipped | No |
RECORD_VIDEO_FAILED_ONLY combines with RECORD_VIDEO_TESTS /
RECORD_VIDEO_TESTS_FILE as an AND: an example is only encoded if it's
both in the target list (or no list is set) and it failed.
Interaction with RECORD_DEBUG: for a passing example, nothing is encoded,
so there are no frames left to keep — RECORD_DEBUG has no effect on
passing examples. RECORD_DEBUG remains useful for inspecting the source
frames behind a video that was produced (i.e. a failing example).
Reducing recording overhead
Since CDP frame capture can't be skipped for any example (see above), the remaining lever is making that capture itself cheaper. Two knobs help:
- Lowering
RECORD_QUALITY(orconfig.quality) shrinks each JPEG frame, reducing CDP transfer and disk I/O. - Raising
RECORD_EVERY_NTH_FRAME(orconfig.every_nth_frame) reduces the number of frames captured at all.
Note: raising every_nth_frame does not shorten the video or make it
play faster. Frame display durations are computed from the deltas between
consecutive CDP frame timestamps, so skipping frames just widens those gaps
and each surviving frame is held on screen longer — total playback time is
preserved. What you lose is temporal resolution (motion looks choppier
between frames), not timing fidelity.
RECORD_VIDEO=1 RECORD_QUALITY=40 RECORD_EVERY_NTH_FRAME=2 bundle exec rspec
Core API (without Capybara)
SeleniumScreencast::Recorder only needs an object that responds to
#devtools (as provided by selenium-webdriver's Chrome bridge), so it can
be used directly without Capybara or RSpec:
require "selenium_screencast"
require "selenium-webdriver"
driver = Selenium::WebDriver.for(:chrome)
recorder = SeleniumScreencast::Recorder.new(driver, output_path: "out.webm")
recorder.start
# ... drive the browser ...
recorder.stop
How it works
- Starts a CDP screencast session (
Page.startScreencast) and writes each incoming JPEG frame to a sequential file, acknowledging every frame so Chrome keeps streaming. - On stop, builds an ffmpeg concat list where each frame's display duration
is derived from the delta between its Chrome timestamp and the next
frame's (variable frame rate), scaled by
slow_factor. - Runs
ffmpegonce against that concat list, re-encoding the variable frame rate stream to a fixedoutput_fps(CFR) file, using VP9/webm or H.264/mp4 depending onconfig.format. If the configuredffmpegexecutable cannot be found, a clearSeleniumScreencast::Erroris raised before encoding instead of failing silently.
Development
After checking out the repo, run bin/setup to install dependencies. Then,
run rake spec to run the tests. You can also run bin/console for an
interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install.
To release a new version, update the version number in version.rb, and
then run bundle exec rake release, which will create a git tag for the
version, push git commits and the created tag, and push the .gem file to
rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/aki77/selenium_screencast. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the SeleniumScreencast project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.