Screen Recording
Screen Recording generates on-demand WebM video files from your Puppeteer or Playwright automation scripts using CDP commands. It supports both video and audio capture, and requires a paid plan. This feature is not to be confused with Session Replay, which records DOM events for playback in your dashboard.
The Screen Recording feature is not available on the /chrome route due to restrictions from Chrome's runtime environment. Use the /stealth endpoint instead.
Basic Usage
Add record=true to your WebSocket connection string and use CDP commands (Browserless.startRecording / Browserless.stopRecording) to control capture. Screen Recording works with Puppeteer, Playwright, and any library that supports CDP connections.
Each example below shows the complete workflow: connecting to Browserless with recording enabled, starting a recording, performing automation tasks, stopping the recording, and saving the resulting WebM file.
- Puppeteer
- Playwright
import fs from "fs";
import puppeteer from "puppeteer-core";
const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
const token = "YOUR_API_TOKEN_HERE";
const wsEndpoint = `wss://production-sfo.browserless.io?token=${token}&headless=false&stealth&record=true`;
const browser = await puppeteer.connect({ browserWSEndpoint: wsEndpoint });
const page = await browser.newPage();
// Set custom viewport for recording dimensions
await page.setViewport({ width: 1280, height: 720 });
await page.goto("https://browserless.io");
// Start recording session
const cdp = await page.createCDPSession();
await cdp.send("Browserless.startRecording");
// Perform your automation actions here
await sleep(15000);
// Stop recording and save
const response = await cdp.send("Browserless.stopRecording");
const file = Buffer.from(response.value, "binary");
await fs.promises.writeFile("./recording.webm", file);
await browser.close();
- Javascript
- Python
- Java
- C#
On Playwright, you must use the already existing context and page and avoid creating new ones.
import playwright from "playwright-core";
import fs from "fs/promises";
const sleep = (ms) => new Promise((res) => setTimeout(res, ms));
const token = "YOUR_API_TOKEN_HERE";
const wsEndpoint = `wss://production-sfo.browserless.io?token=${token}&headless=false&stealth&record=true`;
(async () => {
// Connect to the remote browser
const browser = await playwright.chromium.connectOverCDP(wsEndpoint);
// Reuse the context and page
const context = browser.contexts()[0];
const page = context.pages()[0];
await page.setViewportSize({ width: 1280, height: 720 });
await page.goto("https://browserless.io");
// Start recording
const cdpSession = await page.context().newCDPSession(page);
await cdpSession.send("Browserless.startRecording");
// Wait for some time while recording
await sleep(15000);
// Stop recording and get the response
const response = await cdpSession.send("Browserless.stopRecording");
const file = Buffer.from(response.value, "binary");
// Save the recording as a webm file
await fs.writeFile("./recording.webm", file);
await browser.close();
})();
On Playwright, you must use the already existing context and page and avoid creating new ones.
from playwright.sync_api import sync_playwright
import time
TOKEN = "YOUR_API_TOKEN_HERE"
WS_ENDPOINT = (
f"wss://production-sfo.browserless.io?token={TOKEN}&headless=false&stealth&record=true"
)
with sync_playwright() as playwright:
# Connect to the remote browser
browser = playwright.chromium.connect_over_cdp(WS_ENDPOINT)
# Reuse the context and page
context = browser.contexts[0]
page = context.pages[0]
page.set_viewport_size({"width": 1280, "height": 720})
page.goto("https://browserless.io")
# Start recording
cdp_session = context.new_cdp_session(page)
cdp_session.send("Browserless.startRecording")
# Wait for some time while recording
time.sleep(15)
# Stop recording and get the response
response = cdp_session.send("Browserless.stopRecording")
recording_data = response["value"]
# Write binary data to a file
with open("recording.webm", "wb") as file:
file.write(recording_data.encode("latin1"))
browser.close()
On Playwright, you must use the already existing context and page and avoid creating new ones.
import com.microsoft.playwright.*;
import java.nio.file.*;
public class App {
public static void main(String[] args) {
String TOKEN = "YOUR_API_TOKEN_HERE";
String WS_ENDPOINT = String.format("wss://production-sfo.browserless.io?token=%s&headless=false&stealth&record=true",
TOKEN);
try (Playwright playwright = Playwright.create()) {
// Connect to the remote browser
Browser browser = playwright.chromium().connectOverCDP(WS_ENDPOINT);
// Reuse the context and page
Page page = browser.contexts().get(0).pages().get(0);
page.setViewportSize(1280, 720);
page.navigate("https://browserless.io");
// Start recording
CDPSession cdpSession = browser.contexts().get(0).newCDPSession(page);
cdpSession.send("Browserless.startRecording", null);
// Wait for 15 seconds
Thread.sleep(15000);
// Stop recording
var response = cdpSession.send("Browserless.stopRecording", null);
String recordingData = response.get("value").getAsString();
// Write binary data to a file
byte[] fileData = recordingData.getBytes("ISO-8859-1");
Files.write(Paths.get("recording.webm"), fileData);
System.out.println("Recording saved as 'recording.webm'");
browser.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
On Playwright, you must use the already existing context and page and avoid creating new ones.
using Microsoft.Playwright;
using System;
using System.IO;
using System.Text;
class BrowserlessRecording
{
public static async Task Main(string[] args)
{
string TOKEN = "YOUR_API_TOKEN_HERE";
string WS_ENDPOINT = $"wss://production-sfo.browserless.io?token={TOKEN}&headless=false&stealth&record=true";
using var playwright = await Playwright.CreateAsync();
var browser = await playwright.Chromium.ConnectOverCDPAsync(WS_ENDPOINT);
// Reuse the context and page
var context = browser.Contexts[0];
var page = context.Pages[0];
await page.SetViewportSizeAsync(1280, 720);
await page.GotoAsync("https://browserless.io");
// Start recording
var cdpSession = await context.NewCDPSessionAsync(page);
await cdpSession.SendAsync("Browserless.startRecording");
// Wait for 15 seconds
await Task.Delay(15000);
// Stop recording
var response = await cdpSession.SendAsync("Browserless.stopRecording");
string recordingData = response.Value<string>("value");
// Write binary data to a file
byte[] fileData = Encoding.GetEncoding("ISO-8859-1").GetBytes(recordingData);
await File.WriteAllBytesAsync("recording.webm", fileData);
Console.WriteLine("Recording saved as 'recording.webm'");
await browser.CloseAsync();
}
}
Key Features
- High-quality WebM output with audio captured automatically
- Video dimensions inherited from the browser viewport at recording start
- Works with Puppeteer, Playwright, and other CDP-compatible libraries
- Can be combined with LiveURL for human-in-the-loop recording workflows
Video Dimensions Configuration
The resulting WebM dimensions match the page viewport size at the moment recording starts. If you run await page.setViewport({ width: 2048, height: 720 }); before Browserless.startRecording, the video will be 2048x720 px.
You can set viewport dimensions in several ways:
- Puppeteer: Use
page.setViewport({ width: 1280, height: 720 }) - URL Parameters: Add
--window-size=1280,720andheadless=falseto your connection string - Launch Arguments: Configure viewport in browser launch options
If your automation library does not expose a page.setViewport or page.setViewportSize method, you can set dimensions directly with the CDP Emulation.setDeviceMetricsOverride method. Most libraries use this CDP command under the hood when updating viewport size. See the DevTools Protocol reference.
Do not change the viewport size while recording is in progress. Mid-recording viewport changes can distort rendered content and produce a visibly stretched or otherwise unusual recording.
Recording with LiveURL
You can combine Screen Recording with LiveURL to create human-in-the-loop recording workflows. This captures a WebM video file while a human interacts with the browser in real time. For full LiveURL documentation, see Hybrid Automation.
import puppeteer from 'puppeteer-core';
import fs from 'fs';
const token = 'YOUR_API_TOKEN_HERE';
const queryParams = new URLSearchParams({
token,
timeout: 180000,
headless: true,
}).toString();
(async () => {
let browser = null;
try {
// Connect with recording enabled
browser = await puppeteer.connect({
browserWSEndpoint: `wss://production-sfo.browserless.io?record=true&${queryParams}`,
});
const page = await browser.newPage();
await page.goto('https://example.com/login', {
waitUntil: 'networkidle2'
});
// Initialize CDP session
const cdp = await page.createCDPSession();
// Start recording
await cdp.send("Browserless.startRecording");
// Generate LiveURL for user interaction
const { liveURL } = await cdp.send('Browserless.liveURL', {
timeout: 30000
});
console.log('Share this URL:', liveURL);
// Wait for user interaction
await new Promise(resolve => setTimeout(resolve, 30000));
// Stop recording and save
const response = await cdp.send("Browserless.stopRecording");
const recordingBuffer = Buffer.from(response.value, "binary");
await fs.promises.writeFile("./session-recording.webm", recordingBuffer);
} catch (error) {
console.error('Recording failed:', error);
} finally {
if (browser) await browser.close();
}
})();
Advanced Session Monitoring
For workflows that need visual feedback, you can implement monitoring logic that notifies users when a task completes:
// Enhanced monitoring with visual feedback
await page.waitForFunction(() => {
const currentUrl = window.location.href;
const isSuccess = currentUrl.includes('logged-in') ||
currentUrl.includes('dashboard') ||
document.querySelector('.post-login') !== null;
if (isSuccess) {
// Create success notification
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed; top: 20px; left: 50%;
transform: translateX(-50%); background-color: #4CAF50;
color: white; padding: 15px 30px; border-radius: 5px;
z-index: 9999; font-family: Arial, sans-serif;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
`;
notification.textContent = 'Login successful - you can close this tab';
document.body.appendChild(notification);
return true;
}
return false;
}, { timeout: 60000 });
Custom Recording Triggers
Start recording only after specific conditions are met:
await page.waitForSelector('.login-form');
await cdp.send("Browserless.startRecording");
Conditional Recording
Record only if a specific element is present:
const hasLoginForm = await page.$('.login-form');
if (hasLoginForm) {
await cdp.send("Browserless.startRecording");
}