Skip to content

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:

  1. Redundant Chromium + OS Dependency Installation:
    • Executing playwright install --with-deps chromium downloads the full Chromium browser archive (~160 MB compressed) and runs apt-get install for dozens of shared X11/GTK/NSS system libraries.
    • Across recent ci.yml runs, this step consumes 38–62 seconds of wall-clock time per runner and introduces transient upstream apt mirror flakiness.
  2. Local Preview Emulation vs. True Edge Execution:
    • Each Playwright invocation starts and stops a local astro preview --port <PORT> server (webServer in apps/*/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.

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/playwright accessibility 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_URL is provided (pointing to a newly deployed Preview, Staging, or Production edge URL), webServer is set to undefined so Playwright never boots a local astro preview process.
    • When PLAYWRIGHT_CDP_URL is set, or when PLAYWRIGHT_USE_CLOUDFLARE_CDP=true alongside CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN, use.connectOptions attaches directly to Cloudflare Browser Run with a 30,000ms connection timeout.

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”
  1. Playwright’s contextOptions.recordVideo Limitation:
    • Playwright’s built-in recordVideo option 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 over connectOverCDP, Playwright does not transfer remote video artifacts back over the WebSocket channel.
  2. Cloudflare Browser Run ?recording=true (rrweb DOM Events vs. Pixel Video):
    • Passing ?recording=true on the Browser Run WebSocket URL instructs Cloudflare to record rrweb DOM mutation and interaction JSON events (retrievable via /accounts/<ACCOUNT_ID>/browser-run/recording/<SESSION_ID>).
    • While rrweb JSON 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> .mp4 or animated .webp preview in client outreach portals or PR summaries.

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 .webp

4. 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”
  1. 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 the 10–15 fps smooth scroll band)
    • Route traversal budget: 4–6 seconds per route (~15–20 seconds per viewport across 3–4 core routes), keeping dual-viewport wall-clock time at 24–34 seconds per site (well below the < 45s evaluation ceiling).
  2. Primary Walkthrough Video (.mp4):
    • ffmpeg flags:
      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) .mp4 containers (1.2 MB – 4.5 MB typical size, strictly < 10 MB) with moov atom relocated to the front (+faststart) for instant progressive playback in Desktop and iOS/Android <video> elements.
  3. Animated Preview Loop (.webp) & Route Keyframes:
    • ffmpeg flags 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’s page.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”
  1. 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: 1 in 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 in try { ... } finally { await browser.close(); } to immediately release remote browser capacity rather than waiting for the 300-second keep_alive expiration.
    • Desktop and Mobile viewports in #402 should reuse a single connectOverCDP browser instance by creating two sequential BrowserContext instances (browser.newContext(VIEWPORT_PROFILES.desktop) followed by browser.newContext(VIEWPORT_PROFILES.mobile)), halving WebSocket connection overhead and session quota usage.
  2. Hermetic Offline Local Developer Fallback:
    • resolveBrowserConnectionConfig automatically detects when CLOUDFLARE_ACCOUNT_ID / CLOUDFLARE_API_TOKEN (and PLAYWRIGHT_CDP_URL) are unset—or when PLAYWRIGHT_FORCE_LOCAL=true is specified—and returns { mode: 'local' }.
    • Local developers running pnpm run test:e2e or pnpm run test:a11y offline continue to use their local Chromium installation and local astro preview server with zero Cloudflare credentials required.

Per INTENT_LINEAGE_AND_GOVERNANCE.md §5.2, the following architectural decisions are intentionally deferred to dedicated downstream tickets:

  1. External Prospect & Client Outreach Demo Link Hosting Topology:
    • How generated walkthrough .mp4/.webp artifacts 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).
  2. 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.
  3. Interactive rrweb JSON Timeline Player in Command Center:
    • While ?recording=true is enabled on the Browser Run endpoint to preserve rrweb session telemetry, embedding an interactive rrweb-player DOM inspector inside apps/command-center is deferred until core <video> playback in the Demo Studio (#403) is shipped.