ADR-0005: Cloudflare Browser Run CDP for Zero-Chromium CI & Dual-Viewport Screencast Capture
ADR-0005: Cloudflare Browser Run CDP for Zero-Chromium CI & Dual-Viewport Screencast Capture
Section titled “ADR-0005: Cloudflare Browser Run CDP for Zero-Chromium CI & Dual-Viewport Screencast Capture”Status: 🟢 Approved & Adopted
Date: 2026-10-14
Deciders: SiteSwarm Architecture Council, Platform Engineering
Related Epics & Issues: Epic #399 (Automated Client Demo & Walkthrough Video Generation), Issue #409 (Cloudflare Browser Run CDP Spike & Benchmark), Issue #401 (Reusable Edge Acceptance Workflow), Issue #402 (Programmatic Demo Capture Engine), Issue #403 (Command Center Demo Studio Module), Issue #404 (Shareable Client Outreach Hosting Spike)
Related Implementation Artifacts:scripts/browser-run-cdp.ts,scripts/browser-run-cdp.test.ts,apps/*/playwright.config.ts
1. Context & Baseline CI Browser Overhead Audit
Section titled “1. Context & Baseline CI Browser Overhead Audit”SiteSwarm executes comprehensive browser verification across six deployable edge applications (bakery, software-agency, auto-repair, me-portfolio, marketing, and rcc-stpatrick). Under the baseline .github/workflows/ci.yml pipeline, both the platform-verification job and each matrix shard of selective-verification perform two costly operations on ephemeral GitHub Actions ubuntu-latest runners:
- Redundant Chromium + OS Dependency Installation:
- Executing
playwright install --with-deps chromiumdownloads the full Chromium browser archive (~160 MB compressed) and runsapt-get installfor dozens of shared X11/GTK/NSS system libraries. - Across recent
ci.ymlruns, this step consumes 38–62 seconds of wall-clock time per runner and introduces transient upstreamaptmirror flakiness.
- Executing
- Local Preview Emulation vs. True Edge Execution:
- Each Playwright invocation starts and stops a local
astro preview --port <PORT>server (webServerinapps/*/playwright.config.ts), adding 6–11 seconds of cold boot overhead per app while testing against a local Node/workerd shim rather than the newly deployed Cloudflare Workers edge isolate.
- Each Playwright invocation starts and stops a local
1.1 Baseline vs. Cloudflare Browser Run Benchmark Summary
Section titled “1.1 Baseline vs. Cloudflare Browser Run Benchmark Summary”| Pipeline Phase | Baseline Local Chromium in CI (ci.yml) |
Cloudflare Browser Run Remote CDP (scripts/browser-run-cdp.ts) |
Delta / Impact |
|---|---|---|---|
playwright install --with-deps chromium |
38s – 62s per job (~160 MB download + apt-get) |
0s (0 MB downloaded; uses remote edge browser) |
-100% browser install overhead |
Local astro preview WebServer Boot |
6s – 11s per app (4321..4328) |
0s when PLAYWRIGHT_BASE_URL targets live Edge URL |
Eliminates local preview boot in CD |
CDP WebSocket Handshake (connectOverCDP) |
N/A (local pipe) | 420ms – 780ms (wss://api.cloudflare.com/.../browser-run/devtools/browser) |
Sub-second session acquisition |
E2E + @axe-core/playwright Assertion Parity |
100% (bakery, software-agency, marketing, etc.) |
100% assertion & DOM parity over remote CDP | Full functional equivalence |
Dual-Viewport Walkthrough Capture (Desktop + Mobile) |
Local recordVideo (webm only, no remote CDP support) |
24s – 34s per site (Page.startScreencast -> ffmpeg .mp4 + .webp) |
< 45s budget met; native HTML5 .mp4 (< 10MB) |
2. Architecture Decision 1: 100% Cloudflare Browser Run over Remote CDP
Section titled “2. Architecture Decision 1: 100% Cloudflare Browser Run over Remote CDP”We standardize all live edge acceptance testing (#401) and programmatic walkthrough capture (#402) on Cloudflare Browser Run accessed via remote Chrome DevTools Protocol (CDP) WebSockets.
2.1 Canonical CDP Endpoint & Authentication
Section titled “2.1 Canonical CDP Endpoint & Authentication”Connections are established via chromium.connectOverCDP (or Playwright Test’s use.connectOptions) using the account-scoped WebSocket endpoint implemented in buildBrowserRunEndpoint:
wss://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/browser-run/devtools/browser?recording=true&keep_alive=300000- Authentication Header:
Authorization: Bearer ${CLOUDFLARE_API_TOKEN}
- Keep-Alive Window (
keep_alive=300000): Sets a 5-minute idle ceiling so multi-route E2E,@axe-core/playwrightaccessibility scans, and smooth-scroll screencast traversals never drop mid-suite due to transient step pauses, while guaranteeing automatic server-side reclamation if a CI runner aborts unexpectedly. - Playwright Config Integration (
apps/*/playwright.config.ts):- When
PLAYWRIGHT_BASE_URLis provided (pointing to a newly deployed Preview, Staging, or Production edge URL),webServeris set toundefinedso Playwright never boots a localastro previewprocess. - When
PLAYWRIGHT_CDP_URLis set, or whenPLAYWRIGHT_USE_CLOUDFLARE_CDP=truealongsideCLOUDFLARE_ACCOUNT_IDandCLOUDFLARE_API_TOKEN,use.connectOptionsattaches directly to Cloudflare Browser Run with a30,000msconnection timeout.
- When
3. Architecture Decision 2: CDP Page.startScreencast -> ffmpeg Pixel Video Pipeline
Section titled “3. Architecture Decision 2: CDP Page.startScreencast -> ffmpeg Pixel Video Pipeline”3.1 Why Playwright recordVideo and Native ?recording=true Are Insufficient for Pixel Video
Section titled “3.1 Why Playwright recordVideo and Native ?recording=true Are Insufficient for Pixel Video”- Playwright’s
contextOptions.recordVideoLimitation:- Playwright’s built-in
recordVideooption relies on launching a local browser subprocess and writing frame buffers directly to the local filesystem from the browser host process. When connected to a remote browser overconnectOverCDP, Playwright does not transfer remote video artifacts back over the WebSocket channel.
- Playwright’s built-in
- Cloudflare Browser Run
?recording=true(rrwebDOM Events vs. Pixel Video):- Passing
?recording=trueon the Browser Run WebSocket URL instructs Cloudflare to recordrrwebDOM mutation and interaction JSON events (retrievable via/accounts/<ACCOUNT_ID>/browser-run/recording/<SESSION_ID>). - While
rrwebJSON is valuable for lightweight DOM timeline inspection and session telemetry, it is not a pixel video stream and cannot be embedded natively as an HTML5<video>.mp4or animated.webppreview in client outreach portals or PR summaries.
- Passing
3.2 Ratified Page.startScreencast -> ffmpeg Pipeline
Section titled “3.2 Ratified Page.startScreencast -> ffmpeg Pipeline”To produce deterministic, high-frame-rate pixel videos over remote CDP without local Chromium, ScreencastRecorder and encodeScreencastFramesToVideo implement a streaming CDP screencast pipeline:
sequenceDiagram participant Script as Capture Runner (scripts/browser-run-cdp.ts) participant CF as Cloudflare Browser Run (Remote Chromium CDP) participant FF as Runner ffmpeg (stdin image2pipe)
Script->>CF: connectOverCDP(wss://.../browser-run/devtools/browser?recording=true&keep_alive=300000) Script->>CF: browser.newContext(VIEWPORT_PROFILES.desktop | mobile) Script->>CF: context.newCDPSession(page) Script->>CF: Page.startScreencast({ format: "jpeg", quality: 80, maxWidth, maxHeight, everyNthFrame: 1 }) loop Route Traversal & Smooth Scroll CF-->>Script: Page.screencastFrame({ data (base64 JPEG), metadata, sessionId }) Script->>CF: Page.screencastFrameAck({ sessionId }) Script->>FF: Write decoded JPEG buffer to stdin end Script->>CF: Page.stopScreencast() Script->>CF: browser.close() FF-->>Script: Emit H.264 .mp4 (yuv420p +faststart) & Animated .webp4. Ratified Viewport & Encoding Parameters (Unblocking #401 and #402)
Section titled “4. Ratified Viewport & Encoding Parameters (Unblocking #401 and #402)”4.1 Canonical Viewport Profiles (VIEWPORT_PROFILES)
Section titled “4.1 Canonical Viewport Profiles (VIEWPORT_PROFILES)”| Profile ID | Dimensions (width x height) |
deviceScaleFactor |
isMobile |
hasTouch |
Target Use Case |
|---|---|---|---|---|---|
desktop |
1920 x 1080 |
1 |
false |
false |
Full-HD desktop layout walkthrough & keyframe capture |
mobile |
390 x 844 |
2 |
true |
true |
Modern mobile viewport (iPhone 14/15 logical geometry) with touch & mobile nav drawer verification |
4.2 Screencast & ffmpeg Encoding Specification
Section titled “4.2 Screencast & ffmpeg Encoding Specification”- CDP Screencast Parameters:
format:'jpeg'quality:80(optimal visual fidelity vs. WebSocket frame payload size ~35–75 KB/frame)everyNthFrame:1- Target capture framerate:
12 fps(within the10–15 fpssmooth scroll band) - Route traversal budget:
4–6 secondsper route (~15–20 secondsper viewport across 3–4 core routes), keeping dual-viewport wall-clock time at24–34 secondsper site (well below the< 45sevaluation ceiling).
- Primary Walkthrough Video (
.mp4):ffmpegflags:Terminal window ffmpeg -y -f image2pipe -c:v mjpeg -framerate 12 -i - \-c:v libx264 -pix_fmt yuv420p -movflags +faststart -preset veryfast -crf 23 \walkthrough-<viewport>.mp4- Produces universal H.264 (
avc1).mp4containers (1.2 MB – 4.5 MBtypical size, strictly< 10 MB) withmoovatom relocated to the front (+faststart) for instant progressive playback in Desktop and iOS/Android<video>elements.
- Animated Preview Loop (
.webp) & Route Keyframes:ffmpegflags for animated preview:Terminal window ffmpeg -y -f image2pipe -c:v mjpeg -framerate 12 -i - \-vcodec libwebp -lossless 0 -compression_level 3 -q:v 70 -loop 0 -an \walkthrough-<viewport>.webp- Static route keyframes (
keyframe-<route>-<viewport>.webp) are captured at each route stop via Playwright’spage.screenshot()and converted/stored alongside the video artifacts.
5. Quota, Concurrency, Session Teardown Hygiene & Local Fallback
Section titled “5. Quota, Concurrency, Session Teardown Hygiene & Local Fallback”- Concurrency & Session Teardown Hygiene:
- Cloudflare Browser Run enforces per-account concurrent browser session limits and new-session rate limits.
- All Playwright acceptance configurations set
workers: 1in CI (process.env.CI ? 1 : undefined) so each workflow job consumes exactly one Browser Run session at a time. - Custom capture scripts (
#402) must wrap session lifecycles intry { ... } finally { await browser.close(); }to immediately release remote browser capacity rather than waiting for the 300-secondkeep_aliveexpiration. - Desktop and Mobile viewports in
#402should reuse a singleconnectOverCDPbrowser instance by creating two sequentialBrowserContextinstances (browser.newContext(VIEWPORT_PROFILES.desktop)followed bybrowser.newContext(VIEWPORT_PROFILES.mobile)), halving WebSocket connection overhead and session quota usage.
- Hermetic Offline Local Developer Fallback:
resolveBrowserConnectionConfigautomatically detects whenCLOUDFLARE_ACCOUNT_ID/CLOUDFLARE_API_TOKEN(andPLAYWRIGHT_CDP_URL) are unset—or whenPLAYWRIGHT_FORCE_LOCAL=trueis specified—and returns{ mode: 'local' }.- Local developers running
pnpm run test:e2eorpnpm run test:a11yoffline continue to use their local Chromium installation and localastro previewserver with zero Cloudflare credentials required.
6. Decisions Intentionally Deferred
Section titled “6. Decisions Intentionally Deferred”Per INTENT_LINEAGE_AND_GOVERNANCE.md §5.2, the following architectural decisions are intentionally deferred to dedicated downstream tickets:
- External Prospect & Client Outreach Demo Link Hosting Topology:
- How generated walkthrough
.mp4/.webpartifacts are exposed outside the internal Zero Trust Command Center (e.g., signed time-boxed R2 presigned URLs vs. a public unguessable capability share worker route) is intentionally deferred to Issue #404 (spike(outreach): evaluate shareable prospect demo link hosting).
- How generated walkthrough
- R2 Bucket Object Key Prefix & Retention Lifecycle Policy:
- Exact R2 bucket storage prefixes, commit-SHA indexing metadata, and automated object lifecycle expiration rules for historical demo captures are deferred to Issue #402 and Issue #403.
- Interactive
rrwebJSON Timeline Player in Command Center:- While
?recording=trueis enabled on the Browser Run endpoint to preserverrwebsession telemetry, embedding an interactiverrweb-playerDOM inspector insideapps/command-centeris deferred until core<video>playback in the Demo Studio (#403) is shipped.
- While