# 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](https://cdpfleet.com/docs/builder) to get this for any browser.

 

```node
// npm install playwright@1.60.0
// 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:pass@proxy.example.com: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
```

```python
# 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:pass@proxy.example.com: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
```

```java
// 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:pass@proxy.example.com: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
    }
  }
}
```

```csharp
// 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:pass@proxy.example.com: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
// go get github.com/playwright-community/playwright-go@v0.6000.0
//
// 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:pass@proxy.example.com: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 pass `size: { width, height }` to scale frames down.
-   Each frame carries `data` (JPEG bytes) and the page's `viewportWidth`/`viewportHeight` at capture time. (In the .NET 1.60 client both sizes come through as 0 — use `page.ViewportSize` instead.)
-   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>`.

```js
// 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](https://cdpfleet.com/docs/cases/live-view).

| 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](https://cdpfleet.com/app) has **Watch** and **Take control**.
-   **API:** ask the router that carries the session (the host of its `wsUrl`):

```http
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](https://github.com/novnc/noVNC):

```js
import RFB from 'https://cdn.jsdelivr.net/npm/@novnc/novnc@1.7.0/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](#launch-interactive)). Headless sessions without `live_view` answer `409`.

-   **Single-use links:** a `live_url` works 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](https://cdpfleet.com/privacy).
-   **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: null` and the page fills the whole display.

## Launch interactive

Try a browser before you write code. In [your dashboard](https://cdpfleet.com/app), 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:

```http
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 with `keep_alive`.
-   **Which browsers:** any browser headful, or headless Chromium-based browsers with `"live_view": true` (otherwise `400 start_url_not_viewable`). Not on Rebrowser: its 1.52 server opens the page with a client of its own, so your code would never see it.
-   **Your code can join later:** connect to `wsUrl` or `cdpUrl` as usual and `browser.contexts()[0].pages()` holds the start page. Without `keep_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](https://cdpfleet.com/docs/browsers/rebrowser) (1.52).
-   **`page.screencast` is 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 CDP `Page.startScreencast` over `cdpUrl` instead (Puppeteer: `page.screencast()` or a CDP session). See [CDP & Puppeteer](https://cdpfleet.com/docs/cdp).
-   **After the fact:** for a step-by-step replay with DOM snapshots and the network log, record a [Playwright trace and video](https://cdpfleet.com/docs/cases/trace-and-video).

---
Source: https://cdpfleet.com/docs/live-view · Markdown: https://cdpfleet.com/docs/live-view.md · All docs for AI tools: https://cdpfleet.com/llms.txt
