Live view
Three ways to see a remote browser: stream its frames into your own code, open a live view that a person can watch — or take over — from the dashboard or any page with noVNC, or launch a browser straight into live view with no code at all.
Playwright 1.60's page.screencast streams a JPEG frame to your code every time the page changes, over the session's own connection — no extra port, no extra service, nothing to enable at launch. Forward the frames to your own UI, log them, or save them.
Stream frames
Start the screencast after opening the page and stop it before closing. Tick screencast in the code builder to get this for any browser.
// npm install [email protected]
// Browsers run on cdpfleet, so no `npx playwright install` is needed.
import { chromium } from 'playwright';
const KEY = process.env.CDPFLEET_API_KEY;
// 1. Launch the browser
const res = await fetch('https://starter.cdpfleet.com/chromium/session', {
method: 'POST',
headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
body: JSON.stringify({
proxy: 'http://user:[email protected]:8080',
headless: 'new',
}),
});
if (!res.ok) throw new Error(`launch failed: ${res.status} ${await res.text()}`);
const { wsUrl } = await res.json();
// 2. Connect and drive it
const browser = await chromium.connect(wsUrl, { headers: { 'x-api-key': KEY } });
const page = await browser.newPage();
// Live view: JPEG frames whenever the page changes (Playwright 1.60+)
await page.screencast.start({ quality: 60, onFrame: ({ data }) => {
console.log(`frame: ${data.length} bytes`); // send it to your UI, a websocket, a file…
} });
await page.goto('https://example.com');
console.log(await page.title());
await page.screencast.stop();
await browser.close(); // ends the session and stops billing
# pip install playwright==1.60.0 requests
# Browsers run on cdpfleet, so no `playwright install` is needed.
import os
import requests
from playwright.sync_api import sync_playwright
KEY = os.environ["CDPFLEET_API_KEY"]
# 1. Launch the browser
res = requests.post(
"https://starter.cdpfleet.com/chromium/session",
headers={"x-api-key": KEY},
json={
"proxy": "http://user:[email protected]:8080",
"headless": "new",
},
timeout=60,
)
res.raise_for_status()
ws_url = res.json()["wsUrl"]
# 2. Connect and drive it
with sync_playwright() as p:
browser = p.chromium.connect(ws_url, headers={"x-api-key": KEY})
page = browser.new_page()
# Live view: JPEG frames whenever the page changes (Playwright 1.60+)
page.screencast.start(quality=60, on_frame=lambda frame: print(f"frame: {len(frame['data'])} bytes"))
page.goto("https://example.com")
print(page.title())
page.screencast.stop()
browser.close() # ends the session and stops billing
// Maven: com.microsoft.playwright:playwright:1.60.0 and com.google.code.gson:gson:2.11.0
// Run with PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1: browsers run on cdpfleet.
import com.google.gson.JsonParser;
import com.microsoft.playwright.*;
import java.net.URI;
import java.net.http.*;
import java.util.Map;
public class Example {
public static void main(String[] args) throws Exception {
String key = System.getenv("CDPFLEET_API_KEY");
// 1. Launch the browser
String body = """
{
"proxy": "http://user:[email protected]:8080",
"headless": "new"
}
""";
HttpResponse<String> res = HttpClient.newHttpClient().send(
HttpRequest.newBuilder(URI.create("https://starter.cdpfleet.com/chromium/session"))
.header("x-api-key", key)
.header("content-type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build(),
HttpResponse.BodyHandlers.ofString());
if (res.statusCode() != 200) {
throw new RuntimeException("launch failed: " + res.statusCode() + " " + res.body());
}
String wsUrl = JsonParser.parseString(res.body()).getAsJsonObject().get("wsUrl").getAsString();
// 2. Connect and drive it
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().connect(wsUrl,
new BrowserType.ConnectOptions().setHeaders(Map.of("x-api-key", key)));
Page page = browser.newPage();
// Live view: JPEG frames whenever the page changes (Playwright 1.60+)
page.screencast().start(new Screencast.StartOptions().setQuality(60)
.setOnFrame(frame -> System.out.println("frame: " + frame.data().length + " bytes")));
page.navigate("https://example.com");
System.out.println(page.title());
page.screencast().stop();
browser.close(); // ends the session and stops billing
}
}
}
// dotnet add package Microsoft.Playwright --version 1.60.0
// Browsers run on cdpfleet, so no `playwright install` is needed.
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using Microsoft.Playwright;
var key = Environment.GetEnvironmentVariable("CDPFLEET_API_KEY")!;
// 1. Launch the browser
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-api-key", key);
var body = """
{
"proxy": "http://user:[email protected]:8080",
"headless": "new"
}
""";
var res = await http.PostAsync("https://starter.cdpfleet.com/chromium/session",
new StringContent(body, Encoding.UTF8, "application/json"));
if (!res.IsSuccessStatusCode)
throw new Exception($"launch failed: {(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}");
var wsUrl = (await res.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("wsUrl").GetString()!;
// 2. Connect and drive it
using var playwright = await Playwright.CreateAsync();
var browser = await playwright.Chromium.ConnectAsync(wsUrl, new BrowserTypeConnectOptions
{
Headers = new Dictionary<string, string> { ["x-api-key"] = key },
});
var page = await browser.NewPageAsync();
// Live view: JPEG frames whenever the page changes (Playwright 1.60+)
await page.Screencast.StartAsync(new ScreencastStartOptions
{
Quality = 60,
OnFrame = frame =>
{
Console.WriteLine($"frame: {frame.Data.Length} bytes"); // send it to your UI, a websocket, a file…
return Task.CompletedTask;
},
});
await page.GotoAsync("https://example.com");
Console.WriteLine(await page.TitleAsync());
await page.Screencast.StopAsync();
await browser.CloseAsync(); // ends the session and stops billing
// go get github.com/playwright-community/[email protected]
//
// One-time driver setup (needs Node.js 18+): playwright-go can no longer download the
// 1.60.0 driver itself, so build it from npm and point PLAYWRIGHT_DRIVER_PATH at it:
// mkdir -p ~/.cache/pw-driver-1.60.0 && cd ~/.cache/pw-driver-1.60.0 &&
// curl -sL https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz | tar xz &&
// ln -sf "$(command -v node)" node
// export PLAYWRIGHT_DRIVER_PATH=~/.cache/pw-driver-1.60.0
// Browsers run on cdpfleet, so no browsers are downloaded (SkipInstallBrowsers).
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"github.com/playwright-community/playwright-go"
)
func main() {
key := os.Getenv("CDPFLEET_API_KEY")
// 1. Launch the browser
body := `{
"proxy": "http://user:[email protected]:8080",
"headless": "new"
}`
req, _ := http.NewRequest("POST", "https://starter.cdpfleet.com/chromium/session", strings.NewReader(body))
req.Header.Set("x-api-key", key)
req.Header.Set("content-type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
msg, _ := io.ReadAll(res.Body)
log.Fatalf("launch failed: %s %s", res.Status, msg)
}
var session struct {
WsURL string `json:"wsUrl"`
}
if err := json.NewDecoder(res.Body).Decode(&session); err != nil {
log.Fatal(err)
}
// 2. Connect and drive it (Install fetches only the Playwright driver, once)
opts := &playwright.RunOptions{SkipInstallBrowsers: true}
if err := playwright.Install(opts); err != nil {
log.Fatal(err)
}
pw, err := playwright.Run(opts)
if err != nil {
log.Fatal(err)
}
defer pw.Stop()
browser, err := pw.Chromium.Connect(session.WsURL, playwright.BrowserTypeConnectOptions{
Headers: map[string]string{"x-api-key": key},
})
if err != nil {
log.Fatal(err)
}
page, err := browser.NewPage()
if err != nil {
log.Fatal(err)
}
// Live view: JPEG frames whenever the page changes (Playwright 1.60+)
screencast, err := page.Screencast()
if err != nil {
log.Fatal(err)
}
if err := screencast.Start(playwright.ScreencastStartOptions{
Quality: playwright.Int(60),
OnFrame: func(frame playwright.OnFrame) { fmt.Printf("frame: %d bytes\n", len(frame.Data)) },
}); err != nil {
log.Fatal(err)
}
if _, err := page.Goto("https://example.com"); err != nil {
log.Fatal(err)
}
title, _ := page.Title()
fmt.Println(title)
screencast.Stop()
browser.Close() // ends the session and stops billing
}
quality(0–100) trades sharpness for bytes; 60 is a good default. In Node.js you can also passsize: { width, height }to scale frames down.- Each frame carries
data(JPEG bytes) and the page'sviewportWidth/viewportHeightat capture time. (In the .NET 1.60 client both sizes come through as 0 — usepage.ViewportSizeinstead.) - Add
path: 'session.webm'to record a video file at the same time.
Show it in a browser
A minimal relay in Node.js: every frame goes to the browsers watching ws://localhost:8080, which draw it into an <img>.
// npm install ws
import { WebSocketServer } from 'ws';
const viewers = new WebSocketServer({ port: 8080 });
await page.screencast.start({ quality: 60, onFrame: ({ data }) => {
for (const v of viewers.clients) if (v.readyState === 1) v.send(data);
} });
// viewer page:
// const img = document.querySelector('img'); const ws = new WebSocket('ws://localhost:8080');
// ws.binaryType = 'blob'; ws.onmessage = (e) => { img.src = URL.createObjectURL(e.data); };
What to expect
Measured on the fleet through connect() at 640×360, quality 60, on a page that was changing. Chromium-based browsers and WebKit send frames only when pixels change — nothing at all while a page sits still. Firefox and Camoufox keep streaming (about 20 frames a second even on a static page), so throttle in your callback if you only need a preview. Details in the live view case.
| Browser | Frames per second | Per frame |
|---|---|---|
| Camoufox | ~19 | 12 KB |
| Firefox | ~12 | 13 KB |
| Chromium (headless) | ~9 | 10–11 KB |
| Chromium (headful) | ~5 | 10–11 KB |
| Chrome | ~4 | 11 KB |
| WebKit | ~3 | 24 KB |
Watch or take control from outside
To show a session to someone else — a teammate, a support agent, a person who has to solve a captcha or type a one-time code — ask for a live link. It works without your code's help, and in control mode the person's mouse and keyboard reach the browser while your code keeps running.
- Dashboard: every running session in your dashboard has Watch and Take control.
- API: ask the router that carries the session (the host of its
wsUrl):
POST https://router01.cdpfleet.com/admin/session/live
x-api-key: <your key>
{ "session_id": "…", "mode": "view" } // or "control"
→ { "live_url": "wss://router01.cdpfleet.com/session/…/live?token=…",
"protocol": "vnc", "mode": "view", "expires_in_s": 300, "viewers": 0 }
"protocol": "vnc" — headful sessions (the default): VNC over WebSocket, the whole browser window. Open it with noVNC:
import RFB from 'https://cdn.jsdelivr.net/npm/@novnc/[email protected]/core/rfb.js';
const rfb = new RFB(document.getElementById('screen'), live_url);
rfb.viewOnly = mode === 'view';
rfb.scaleViewport = true;
"protocol": "screencast" — headless Chromium-based sessions launched with "live_view": true: a WebSocket with one JPEG per binary message (~12 fps while the page changes, nothing when it doesn't), JSON {"type":"pages"} and {"type":"meta"} messages, and in control mode you send {"type":"mouse"}, {"type":"key"} and {"type":"text"} messages in the page's CSS pixels, plus navigate, back, forward and reload (see Launch interactive). Headless sessions without live_view answer 409.
- Single-use links: a
live_urlworks once and expires after 5 minutes; ask again to reconnect. At most 4 viewers per session. - Watching never ends the session and isn't billed; the session ending closes its viewers.
- cdpfleet staff can watch a session too (view only, never control) when you ask for support, to troubleshoot the service or to investigate abuse; each access is logged. See the Privacy Policy.
- Right after launch: the router API above gives a live link as soon as the session is launched, before your code connects. The dashboard's Watch becomes available once your code has connected.
- Control is real input: pages receive trusted mouse and keyboard events, and they count as activity for the idle timeout. WebKit shows and takes keyboard input but ignores clicks.
- Bandwidth: VNC of a busy 1920×1080 page is about 2.5–4.5 Mbit/s; a still page sends almost nothing. Set the display size with
"screen_size": "1366x768"at launch. - Fill the view: Playwright sizes a headful context's window to its viewport (1280×720 by default); create contexts with
viewport: nulland the page fills the whole display.
Launch interactive
Try a browser before you write code. In your dashboard, the code generator has Try it live below the form: pick the browser, proxy and options, enter a start page and click Launch interactive. The browser opens on that page in a live view with full mouse and keyboard control — headless sessions get an address bar with back, forward and reload — and the code next to it launches exactly the same browser.
Over the API it's one launch option, start_url: the server opens the page itself, so no client has to connect, and you drive the session through a live link in control mode:
POST https://starter.cdpfleet.com/chrome/session
{ "proxy": "…", "headless": "new", "live_view": true, "start_url": "https://example.com" }
POST https://router01.cdpfleet.com/admin/session/live // the host of wsUrl
{ "session_id": "…", "mode": "control" }
- It stays open while you use it: mouse and keyboard input in control mode count as activity, and so does an open viewer — watching keeps it alive. With no input and no one watching, the session ends after
inactivity_timeout— 120 s by default, up to 10 minutes on shared plans (5 on Community) and 24 h on dedicated. The dashboard ends it when you close the viewer, unless you launched withkeep_alive. - Which browsers: any browser headful, or headless Chromium-based browsers with
"live_view": true(otherwise400 start_url_not_viewable). On Rebrowser the server opens the page with a client of its own, so code that connects later gets fresh contexts and doesn't see it — live view does. - Your code can join later: connect to
wsUrlorcdpUrlas usual andbrowser.contexts()[0].pages()holds the start page. Withoutkeep_alive, disconnecting that client ends the session. - Screencast navigation: in control mode the screencast socket also takes
{"type":"navigate","url":"https://…"},{"type":"back"},{"type":"forward"}and{"type":"reload"}(http and https only). - Billing: a normal session — threads and minutes while it runs, on every plan including Community.
Good to know
- Playwright 1.60 or newer: the method doesn't exist in older clients, so it isn't available on Rebrowser (1.52).
page.screencastis for your own code: frames go to the client that drives the page and it's view-only. To show the session to someone else or let them click and type, use a live link (above).- Billing: nothing extra — frames travel over the session you already pay for.
- CDP: with
"cdp": true, use CDPPage.startScreencastovercdpUrlinstead (Puppeteer:page.screencast()or a CDP session). See CDP & Puppeteer. - After the fact: for a step-by-step replay with DOM snapshots and the network log, record a Playwright trace and video.