Streaming and Recording Headful Browser Sessions
Learn how to show a visible browser session, capture it as video, stream frames, and record hosted sessions with Playwright or Browserless.

Direct answer: use Playwright when the browser workflow and recording run inside your application. Use Browserless when you want a hosted headful browser controlled over CDP. Playwright can save a video or deliver JPEG frames through a callback. Browserless provides on-demand WebM recording through CDP, with recording enabled in the connection URL. In both cases, configure the viewport before capture starts and keep the existing context and page that are wired to recording.
A recording is not automatically a live-streaming product. A saved WebM file is useful for audits, debugging and replay. A frame callback can feed a WebSocket, WebRTC pipeline or image stream that you build. If end users need a live viewer, you still need to design transport, authentication, buffering and lifecycle management around the browser capture.
Choose the capture path
| Requirement | Best starting point | What you receive |
|---|---|---|
| Record a local or self-hosted automation flow | Playwright | Video file or JPEG frames with timestamps |
| Run a hosted visible browser | Browserless over CDP | WebM recording, with audio support documented for paid plans |
| Show a live view to a user | Playwright frame callback plus your transport | Frames that you package into MJPEG, WebSocket messages or another stream |
| Replay DOM activity in a dashboard | Browserless Session Replay | DOM-event playback, which is different from a video file |
Playwright’s page screencast API documents both file capture and an onFrame callback. Browserless documents screen recording over CDP; its current v2 guidance requires headless=false, stealth and record=true, and directs recording users to the /stealth route rather than /chrome.
Record a visible session with Playwright
The example below launches a non-headless Chromium browser, sets the viewport before navigation, starts a page screencast, performs a visible interaction and saves the result. Verify the exact method names against the Playwright version in your project because screencast APIs can vary between releases.

import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext({
viewport: { width: 1440, height: 900 },
// If your version uses context-level video recording, you can also
// configure recordVideo here and read page.video() after the page closes.
});
const page = await context.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle' });
// Method names and options depend on your installed Playwright version.
await page.screencast.start({
path: 'session.webm',
size: { width: 1440, height: 900 }
});
await page.getByRole('link').first().click().catch(() => {});
await page.waitForTimeout(1500);
await page.screencast.stop();
await context.close();
await browser.close();
Keep the capture dimensions stable. If you resize the page during recording, the output may no longer match the intended layout. A fixed viewport also makes visual comparisons and downstream video processing predictable.
Capture frames instead of a file
Use the frame callback when you need to display progress to an operator or forward images to another service. The callback receives JPEG-encoded data and timing information. The following sketch writes each frame to a directory; replace the file write with a WebSocket send or an encoder in a real streaming service.
import { chromium } from 'playwright';
import { mkdir, writeFile } from 'node:fs/promises';
await mkdir('./frames', { recursive: true });
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
const page = await context.newPage();
let frameNumber = 0;
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.screencast.start({
onFrame: async ({ data, timestamp }) => {
const filename = `./frames/frame-${String(frameNumber++).padStart(6, '0')}.jpg`;
await writeFile(filename, data);
console.log({ filename, timestamp });
}
});
await page.waitForTimeout(5000);
await page.screencast.stop();
await context.close();
await browser.close();
Do not treat frame callbacks as a complete viewer. You must choose a delivery protocol, handle slow consumers and decide whether to drop frames or apply backpressure. For a simple internal preview, a bounded queue and “latest frame wins” policy usually keeps the browser responsive. For archival video, write frames to a proper encoder instead of retaining them all in memory.
Context-level video recording
Playwright also documents a video object for pages whose browser context was created with the recordVideo option. This is convenient when you want one file per page and can wait until the page closes. A typical setup is:
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: false });
const context = await browser.newContext({
viewport: { width: 1366, height: 768 },
recordVideo: { dir: './videos', size: { width: 1366, height: 768 } }
});
const page = await context.newPage();
await page.goto('https://example.com');
await page.waitForTimeout(3000);
await page.close();
await context.close(); // finalizes the video file
await browser.close();
Because finalization occurs when the page or context closes, always close resources in a try/finally block in production. If a process is killed, the file may be incomplete.
Record a hosted headful browser with Browserless
Browserless’s hosted workflow connects Playwright or Puppeteer over CDP. Its documentation requires a paid plan for screen recording, enables recording in the WebSocket URL, and returns WebM data from CDP start/stop commands. Recording is not available on the /chrome route in the current v2 documentation; use the documented /stealth route and confirm the endpoint and plan in your account.
import { chromium } from 'playwright';
import { writeFile } from 'node:fs/promises';
const token = process.env.BROWSERLESS_TOKEN;
if (!token) throw new Error('Set BROWSERLESS_TOKEN');
const ws = `wss://production-sfo.browserless.io/stealth?token=${token}&headless=false&stealth&record=true`;
const browser = await chromium.connectOverCDP(ws);
// Reuse the context and page supplied by the connected session.
const contexts = browser.contexts();
const context = contexts[0] ?? await browser.newContext();
const pages = context.pages();
const page = pages[0] ?? await context.newPage();
// Set the viewport before recording starts. Video dimensions inherit it.
await page.setViewportSize({ width: 1280, height: 720 });
await page.goto('https://example.com', { waitUntil: 'networkidle' });
const client = await context.newCDPSession(page);
await client.send('Browserless.startRecording');
await page.waitForTimeout(5000);
const result = await client.send('Browserless.stopRecording');
await writeFile('session.webm', Buffer.from(result.data, 'base64'));
await browser.close();
The important integration detail is object ownership: after connecting, reuse the existing context and page that belong to the recording session. Creating unrelated objects can produce a page that is not wired to the recorder. Set the viewport before startRecording; changing it mid-session cannot retroactively change the video dimensions.
Audio and Session Replay
Browserless documents audio capture for screen recording on paid plans. Treat audio as a separate requirement in your acceptance tests because browser permissions, media devices and page playback can affect what is available. Session Replay is another product capability: it stores DOM events for dashboard playback rather than producing a video file. Choose it when DOM-level replay is sufficient; choose screen recording when you need the rendered pixels.
Designing a real-time viewer
- Start one browser session per viewer or job. Give every session an identifier and an owner.
- Authenticate the viewer separately. Never expose a Browserless token or CDP URL to an untrusted browser.
- Choose a transport. JPEG frames over WebSocket are straightforward; MJPEG works for simple internal pages; WebRTC is better suited to interactive, low-latency video but requires a media pipeline.
- Control frame rate. Capture only as frequently as the consumer needs. Dropping stale frames is safer than allowing an unbounded queue.
- Stop deterministically. On disconnect, stop recording, close the page and release the browser session after a short grace period.
Keep recordings out of process memory. Stream them to durable storage or write them incrementally. Include session metadata such as URL, viewport, start time, browser version and outcome, but avoid storing secrets that appear in the page.
Reliability, performance and cost considerations
- Viewport is part of configuration. Set width, height and device scale before capture. A larger retina scale increases pixel work and output size.
- Wait for a meaningful state. Use a selector, a known delay or network-idle logic appropriate to the page. “Network idle” can be unsuitable for apps with long polling.
- Bound recording duration. Add a maximum session time and stop on navigation failures, authentication expiry or disconnected clients.
- Expect incomplete output on crashes. Use process supervision and finalize files in cleanup handlers.
- Measure your own workload. The cited documentation does not establish comparative speed, quality, price or reliability between Playwright and Browserless.
- Budget hosted usage. Browserless documents recording as a paid-plan feature. Check current service terms before deploying.

Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Video is blank or missing | Capture started before navigation or the page was closed before finalization | Navigate first, keep the page open for the capture interval, then stop and close resources cleanly. |
| Wrong video dimensions | Viewport was changed after recording began | Set the viewport before starting the screencast or Browserless recording. |
| Browserless recording command fails | Wrong route or missing flags | Use the documented /stealth route with headless=false, stealth and record=true; verify paid-plan access. |
| Recorded page is not the controlled page | A new context or page was created after CDP connection | Reuse the existing connected context and page. |
| Viewer lags and memory grows | Producer is faster than the consumer | Use a bounded queue, drop stale frames and monitor queue length. |
| Audio is silent | Media permissions or page playback policy blocked audio | Grant the required permissions, trigger playback from an allowed interaction and verify the hosted plan supports audio. |
| File cannot be opened | Base64 response was written as text or recording was not stopped | Decode Browserless data with Buffer.from(value, 'base64') and call the stop command before writing. |
Or skip the browser setup
If you need a clean image or PDF of a page rather than a live browser session, ScreenshotNeo provides a single HTTP request. It removes cookie and consent banners, newsletter popups and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and billing result. Its MCP server lets Claude, Cursor and other MCP clients use take_screenshot, get_page_info and capture_pdf.
See the ScreenshotNeo API documentation for all options. cURL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo includes full-page capture with lazy images loaded, element capture by CSS selector, dark mode, device presets, custom viewports, retina scale, PDF controls, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, caching, signed links, async jobs, bulk capture and a usage API. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
FAQ
Can Playwright stream a browser as HLS?
Playwright supplies a video file or JPEG frames. HLS packaging, distribution and playback are application responsibilities.
Is Browserless Session Replay a video?
No. Session Replay records DOM events for playback. Screen recording produces rendered video.
Can I change the viewport while recording?
You can change a page viewport in some browser APIs, but Browserless states that recording dimensions are inherited when capture starts. Set the final viewport first.
Do I need headful mode for every recording?
For the Browserless workflow described here, yes: the documented setup requires headless=false. Playwright’s capture API can be used in the browser mode appropriate to your workflow.
Which option is cheaper or faster?
The supplied documentation does not provide a fair comparative benchmark or pricing study. Measure your own pages and account for hosted-plan requirements.


