Skip to content

Record a still page that ends before its first frame - #29

Merged
brynary merged 5 commits into
jev-iframe-titlefrom
video-no-frames
Oct 5, 2026
Merged

brynary merged 5 commits into
jev-iframe-titlefrom
video-no-frames

Conversation

@brynary

@brynary brynary commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #28. Fixes the flaky combined_report_keeps_embedded_media_and_missing_scenarios_offline failure on macOS CI (#27's first run): endFlow failed: ... video recording failed: the page produced no frames, exit 3.

Root cause

On Chromium, Whirl's recorder starts the screencast in startFlow, on the blank page before VISIT. Chrome sends a screencast frame only when the page paints, and each frame reaches Node some time after the paint. A short flow on a still page can reach endFlow first. ScreencastRecorder.stop() then had no frame and threw, and a failed endFlow turned a passing file into a runtime error.

Evidence from temporary logging: in all 23 zero-frame cases, the first frame arrived 10–33 ms after stop, and stop came 7–21 ms after the start. Under load the first frame took 5–450 ms. Playwright's own recorder, used for Firefox and WebKit, writes a white frame when it gets none, so only Chromium failed.

Fix

  • When no frame has arrived at stop, the recorder captures the page once (Page.captureScreenshot, 5 s limit) and holds that frame from the start of the recording at the constant rate. Waiting for a first frame at start or at stop would slow every flow, still depend on timing, and still need this fallback.
  • When there is still no frame, the recording holds a white frame, with a warning that says why (see the follow-up below). Only an ffmpeg failure skips the recording: endFlow returns videoSkipped with the reason, Rust reports warning: video recording skipped: …, the HTML report shows "Recording unavailable.", and the JSON report has no runtime.videoFps. Neither warning changes the file's result (SPEC §14.1).
  • The recorder takes a small frame-source interface, so unit tests can use a page that sends nothing.

Follow-up: a page that has not painted

#30's CI then failed once on Ubuntu: right after a VISIT, Chrome answered the capture with "Unable to capture screenshot", because the new document had not painted yet, and the recording was skipped. Two more commits finish the fix:

  • 15f3dae: an ffmpeg failure or a stall over 10 s at endFlow now skips the recording with a warning instead of failing the file. A missing ffmpeg at startFlow is still a runtime error.
  • 7308c4c: when Chrome has nothing painted, the recorder waits 50 ms and captures again, for up to 1 s; a screencast frame that arrives meanwhile serves as well. When there is still no frame, or the capture fails for another reason, the recording holds a white frame of the viewport's size, as Playwright's own recorder does for Firefox and WebKit.
  • 99a4bb4: a recording that holds only a white frame now carries a warning, video recording is blank: …, that says why: the capture failed, as for a crashed page, or the page did not paint within 1 s. The recording stays listed. A failure to move the finished recording into place stays a runtime error.

Under the same load (10 at a time on low-priority cores), the short-flow test failed 7 of 250 runs before and 0 of 250 after. The 7 unpainted pages each got a frame on the second or third try, within 650 ms. In 100 alternating rounds, the old shim failed 4 and the new shim 0.

The flake is gone

The original test failed only under heavy load: 10 copies at a time on low-priority cores (taskpolicy -b) on an 18-core Mac.

Run Before After
Original test, 10 at a time on low-priority cores 3 of 50 fail 0 of 100
VISIT + SCREENSHOT through the CLI, 8 jobs, low priority 2 of 120 files 0 of 120
Short HTTP-only and VISIT-only flows, 1 job 61 of 80 files 0 of 120
New acceptance test 20 of 20 fail 20 of 20 pass

Tests

  • Shim unit tests: a page that sends nothing is captured once and fills the recording; a failed capture skips the recording and leaves no file; a page that sends a frame is not captured.
  • short_video_flows_on_still_pages_pass_with_a_recording: four short flows in one browser exit 0, each with a recording that decodes.
  • a_skipped_recording_is_a_warning_and_the_file_still_passes, through the fake shim, with the JSON and HTML reports.
  • All video tests pass in all three engines with WHIRL_TEST_ALL_BROWSERS=1. mise run check passes. SPEC §13, the shim protocol doc, and the CHANGELOG describe the change.

Notes

  • The shim's recorder unit tests now use Playwright's bundled ffmpeg, as the browser tests already do.
  • An ffmpeg failure at endFlow still fails the file, as the protocol doc says.
  • The captured frame assumes a device scale factor of 1, which Whirl always uses.

🤖 Generated with Claude Code

@brynary brynary mentioned this pull request Sep 27, 2026
brynary added a commit that referenced this pull request Sep 27, 2026
Brings the finished no-frames fix from #29: retries for a page that has
not painted, a white frame when there is still none, and a skip only
when ffmpeg fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brynary added a commit that referenced this pull request Sep 27, 2026
Brings the blank-recording warning from #29.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
brynary added a commit that referenced this pull request Sep 27, 2026
Brings the blank-recording warning from #29 through #30.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brynary
brynary added this pull request to stack #55 October 5, 2026 13:05
brynary and others added 5 commits October 5, 2026 09:05
With --video on Chromium, a short flow on a still page could fail as a
runtime error: "video recording failed: the page produced no frames".
On macOS CI this failed one run of
combined_report_keeps_embedded_media_and_missing_scenarios_offline, and
the rerun passed.

Chrome sends a screencast frame only when the page paints, and the frame
reaches the shim some time after the paint: 5 to 450 ms on a loaded
machine. The recorder starts the screencast on the blank page, and a flow
such as VISIT then SCREENSHOT can reach endFlow first. The recorder then
stopped the screencast, dropped the frame on its way, and failed the
file. With logging added, each such frame arrived 10 to 33 ms after the
stop.

Now, when no frame has arrived, the recorder captures the page once with
Page.captureScreenshot and holds that frame from the start of the
recording at the constant rate. When the capture fails or takes more
than 5 seconds, as for a crashed page, the shim skips the recording and
says why in the new endFlow field videoSkipped. Rust reports it as a
warning, and the file's result does not change, as for SCREENSHOT.

The recorder now takes a frame source, so its unit tests can use a page
that sends no frame.

Before the fix, the CI test failed 3 of 50 runs with 10 copies at once
on low-priority cores, and a new test of four short flows failed 20 of
20 runs. After the fix, they pass 100 of 100 and 20 of 20.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A skipped recording kept runtime.videoFps in the JSON report, but the
machine-output doc says the field is absent without a recording. Rust
now clears it when the shim skips the recording. The fake shim starts a
60 fps recording before it reports the skip, so the test sees the field
go away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With --video on Chromium, an ffmpeg failure at endFlow failed endFlow, so
the file became a runtime error (exit 3). But a recording is evidence,
not a result (SPEC 14.1). Now the shim discards the recording and says
why in videoSkipped, as it does for a page that it cannot capture. Rust
reports a warning, and the file's result does not change. A missing
ffmpeg at startFlow is still a runtime error (SPEC 13).

The recorder now waits for ffmpeg's "close" event, not "exit". Node can
send "exit" before the last stderr output, and the warning then loses
ffmpeg's error text.

A new unit test runs a fake ffmpeg that writes part of its output,
prints an error, and exits with status 1. The recorder skips the
recording with that error and leaves no file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On GitHub Actions ubuntu-24.04, a lone VISIT /stable.html in
short_video_flows_on_still_pages_pass_with_a_recording passed with
"warning: video recording skipped: the page produced no frames, and
capturing it failed: ... Unable to capture screenshot". The test expects
a recording, so it failed. Chrome gives that error when the page has not
painted yet, as right after a navigation or on a new tab: it has nothing
to copy.

With 10 copies of the test at once on low-priority cores, the old
recorder failed 7 of 250 runs this way: 6 times right after a VISIT, and
once on the blank page of an HTTP-only flow. Each failed capture took 2
to 11 ms. With logging added, a new capture 25 ms later, or the one
after it, worked every time, within 360 ms.

Now, when a capture finds nothing painted, the recorder waits 50 ms and
tries again, for up to 1 second. The screencast still feeds the recorder
while it tries, so a frame that arrives is used as a capture would be.
When the recorder still has no frame, or the capture fails for another
reason, as for a crashed page, it holds a white frame of the viewport's
size for the whole recording, as Playwright's recorder does. All the
captures together still stop after 5 seconds. Only an ffmpeg failure
skips a recording now.

The white frame is a JPEG from jpegjs in playwright-core's utilsBundle,
the encoder that Playwright's recorder uses for its own white frame. The
package's export map includes that bundle, and the shim already takes
the image comparator from its core bundle. Node has no JPEG encoder, and
Playwright's ffmpeg decodes only MJPEG and VP8, so a PNG frame cannot go
into the stream.

The frame source now gives the retry time, so the unit tests use 200 ms,
not 1 second. New tests cover a capture that works on the second try, a
page that never paints (its decoded frames are white), a crashed page (a
white frame with no retry), and a screencast frame that arrives between
tries. The fake shim test now reports an ffmpeg failure, the only reason
for a skip.

After the change, under the same load, the short test passed 250 of 250
runs. For 100 of them, rounds of the old and the new recorder
alternated, so both had the same load; the old recorder failed 4 of its
100. In the 1,000 new recordings, 7 captures found nothing painted, and
each of those recordings got a frame on the second or third try, within
650 ms. No recording needed the white frame, and none was skipped. The
combined-report test passed 100 of 100 runs, as it passed 50 of 50
before. Its 18 recordings without a frame at the stop needed no retry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With --video on Chromium, a page that the recorder cannot capture, as a
crashed one, gets a recording that holds a white frame. The report then
showed a white video with no explanation. Now the shim says why in the
new endFlow field videoBlank, and Rust reports "warning: video recording
is blank: <reason>", masked like other warnings. The recording stays in
the report with its frame rate, and the file's result does not change.

A page that has not painted after 1 second of tries also gets the
warning, with the reason "the page did not paint within 1000ms". Its
video is white too, so it needs the same explanation. A capture that
takes more than 5 seconds gives "capturing the page took more than
5000ms". For a crashed page, Chrome answers "cdpSession.send: Protocol
error (Page.captureScreenshot): Internal error", and the warning shows
that text after "capturing the page failed:".

The recorder's unit tests now check the reason for a page that never
paints and for a crashed page. A real frame or a working capture still
gives no reason. The fake shim has a new FAKE_SHIM_VIDEO_BLANK option,
and a new acceptance test checks the masked warning in stdout and in
the JSON report, the passed status, the listed recording, and the frame
rate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@brynary
brynary merged commit 6064828 into pointer-buttons Oct 5, 2026
3 checks passed
@brynary
brynary deleted the video-no-frames branch October 5, 2026 18:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant