Browser Live View Example
Pricing
Pay per usage
Go to Store
Browser Live View Example
0.0 (0)
Pricing
Pay per usage
0
Total users
2
Monthly users
2
Runs succeeded
>99%
Last modified
4 days ago
.dockerignore
# configurations.idea.vscode
# crawlee and apify storage foldersapify_storagecrawlee_storagestorage
# installed filesnode_modules
# git folder.git
.editorconfig
root = true
[*]indent_style = spaceindent_size = 4charset = utf-8trim_trailing_whitespace = trueinsert_final_newline = trueend_of_line = lf
.gitignore
# This file tells Git which files shouldn't be added to source control
.DS_Store.idea.vscode.zeddistnode_modulesapify_storagestorage
# Added by Apify CLI.venv
eslint.config.mjs
1import apifyTypescriptConfig from '@apify/eslint-config/ts.js';2
3// eslint-disable-next-line import/no-default-export4export default [5 { ignores: ['**/dist'] }, // Ignores need to happen first6 ...apifyTypescriptConfig,7 {8 languageOptions: {9 sourceType: 'module',10
11 parserOptions: {12 project: 'tsconfig.json',13 },14 rules: {15 'no-use-before-define': 'off',16 },17 },18 },19];
package.json
{ "name": "chrome-dev-tools", "version": "0.0.1", "type": "module", "description": "This is an example of an Apify actor.", "engines": { "node": ">=18.0.0" }, "dependencies": { "@fastify/cors": "^11.0.1", "@fastify/static": "^8.1.1", "@fastify/websocket": "^11.0.2", "apify": "^3.2.6", "cors": "^2.8.5", "crawlee": "^3.11.5", "express": "^5.1.0", "express-ws": "^5.0.2", "fastify": "^5.3.3", "http-proxy": "^1.18.1", "http-proxy-middleware": "^3.0.5", "http-terminator": "^3.2.0", "httpxy": "^0.1.7", "playwright": "*", "puppeteer": "*", "simple-get": "^4.0.1" }, "devDependencies": { "@apify/eslint-config": "^1.0.0", "@apify/tsconfig": "^0.1.0", "@types/cors": "^2.8.18", "@types/express": "^5.0.1", "@types/express-ws": "^3.0.5", "@types/http-proxy": "^1.17.16", "@types/simple-get": "^4.0.3", "@types/ws": "^8.18.1", "eslint": "^9.26.0", "tsx": "^4.6.2", "typescript": "^5.3.3", "typescript-eslint": "^8.32.1" }, "scripts": { "start": "npm run start:dev", "start:prod": "node dist/main.js", "start:dev": "tsx src/main.ts", "build": "tsc", "lint": "eslint .", "lint:fix": "eslint . --fix", "test": "echo \"Error: oops, the actor has no tests yet, sad!\" && exit 1", "postinstall": "npx crawlee install-playwright-browsers" }, "author": "It's not you it's me", "license": "ISC"}
tsconfig.json
{ "extends": "@apify/tsconfig", "compilerOptions": { "module": "NodeNext", "moduleResolution": "NodeNext", "target": "ES2022", "outDir": "dist", "noUnusedLocals": false, "skipLibCheck": true, "lib": ["DOM"], }, "include": [ "./src/**/*" ]}
.actor/Dockerfile
# Specify the base Docker image. You can read more about# the available images at https://crawlee.dev/docs/guides/docker-images# You can also use any other image from Docker Hub.FROM apify/actor-node-playwright-chrome:20 AS builder
# Check preinstalled packagesRUN npm ls crawlee apify puppeteer playwright
# Copy just package.json and package-lock.json# to speed up the build using Docker layer cache.COPY package*.json ./
# Install all dependencies. Don't audit to speed up the installation.RUN npm install --include=dev --audit=false --force
# Next, copy the source files using the user set# in the base image.COPY . ./
# Copy the public directory for build-time needsCOPY public ./public
# Install all dependencies and build the project.# Don't audit to speed up the installation.RUN npm run build
# Create final imageFROM apify/actor-node-playwright-chrome:20
# Check preinstalled packagesRUN npm ls crawlee apify puppeteer playwright
# Copy just package.json and package-lock.json# to speed up the build using Docker layer cache.COPY package*.json ./
# Install NPM packages, skip optional and development dependencies to# keep the image small. Avoid logging too much and print the dependency# tree for debuggingRUN npm --quiet set progress=false \ && npm install --omit=dev --omit=optional --force \ && echo "Installed NPM packages:" \ && (npm list --omit=dev --all || true) \ && echo "Node.js version:" \ && node --version \ && echo "NPM version:" \ && npm --version \ && rm -r ~/.npm
# Copy built JS files from builder imageCOPY /home/myuser/dist ./dist
# Copy the public directory from the builder imageCOPY /home/myuser/public ./public
# Next, copy the remaining files and directories with the source code.# Since we do this after NPM install, quick build will be really fast# for most source file changes.COPY . ./
# Run the image. If you know you won't need headful browsers,# you can remove the XVFB start script for a micro perf gain.CMD ./start_xvfb_and_run_cmd.sh && npm run start:prod --silent
.actor/actor.json
{ "actorSpecification": 1, "name": "browser-live-view-example", "title": "Browser Live View Example", "description": "This is an example implementation of browser live view.", "version": "0.0", "meta": { "templateId": "ts-crawlee-playwright-chrome" }, "input": "./input_schema.json", "dockerfile": "./Dockerfile"}
.actor/input_schema.json
{ "title": "PlaywrightCrawler Template", "type": "object", "schemaVersion": 1, "properties": { "url": { "title": "URL", "type": "string", "description": "URL to navigate to", "editor": "textfield", "prefill": "https://apify.com" }, "proxyConfiguration": { "title": "Proxy configuration", "type": "object", "description": "Proxy settings for the crawler (see Apify proxy configuration object)", "editor": "proxy", "prefill": { "useApifyProxy": true } } }}
src/cdp-proxy-server.ts
1import express from 'express';2import cors from 'cors';3import expressWs from 'express-ws';4import { CDPSession } from 'playwright';5import path from 'node:path';6
7export const createCdpProxyServer = async (cdpSession: CDPSession) => {8 const app = express();9 expressWs(app);10
11 app.use(cors({ origin: '*', credentials: true }));12 app.use(express.static(path.join(path.dirname(new URL(import.meta.url).pathname), '../public')));13
14 app.get('/health', (_req, res) => {15 res.status(200).send('OK');16 });17
18 (app as unknown as expressWs.Application).ws('/cdp', (ws, _req) => {19 console.info('Client connected');20
21 const eventHandler = (eventName: string, eventData: any) => {22 ws.send(JSON.stringify({ method: eventName, params: eventData }));23 };24
25 const originalEmitFn = (cdpSession as any).emit;26 (cdpSession as any).emit = (...args: any[]) => {27 const [eventName, eventData] = args;28 eventHandler(eventName, eventData);29 originalEmitFn.apply(cdpSession, args);30 };31
32 interface CDPCommand {33 id: number;34 method: string;35 params?: any;36 }37
38 ws.on('message', async (message: Buffer) => {39 const { id, method, params } = JSON.parse(message.toString('utf8')) as CDPCommand;40 try {41 const response = await cdpSession.send(method as any, params);42 ws.send(JSON.stringify({ id, result: response }));43 } catch (error) {44 ws.send(JSON.stringify({ id, error: { message: (error as Error).message ?? '' } }));45 return;46 }47 });48
49 ws.on('error', (err: Error) => {50 console.error(err.message);51 });52
53 ws.on('close', () => {54 console.info('Connection closed');55 });56 });57
58 // Return the app and the underlying server59 return {60 app,61 listen: (opts: { port: number }, cb: (err?: Error, address?: string) => void) => {62 const server = app.listen(opts.port, () => {63 cb(undefined, `http://localhost:${opts.port}`);64 });65 server.on('error', (err) => cb(err));66 return server;67 },68 close: (server: any) => new Promise<void>((resolve, reject) => {69 server.close((err: any) => {70 if (err) reject(err);71 else resolve();72 });73 })74 };75};
src/main.ts
1import { Actor } from 'apify';2import { PlaywrightCrawler, ProxyConfigurationOptions } from 'crawlee';3
4import { createCdpProxyServer } from './cdp-proxy-server.js';5import { router } from './routes.js';6
7interface Input {8 url: string;9 proxyConfiguration: ProxyConfigurationOptions;10}11
12await Actor.init();13
14const { containerUrl, containerPort } = Actor.getEnv();15
16// const CONTAINER_HOST = Actor.isAtHome() ? containerUrl : 'localhost';17// const CONTAINER_PORT = containerPort;18// const CHROME_DEBUGGING_PORT = 9222;19
20const {21 url = 'https://www.apify.com',22 proxyConfiguration: proxyConfigurationOptions = { useApifyProxy: true },23} = await Actor.getInput<Input>() ?? {} as Input;24
25const proxyConfiguration = await Actor.createProxyConfiguration(proxyConfigurationOptions);26
27let server: any | undefined;28let httpServer: any | undefined;29
30const crawler = new PlaywrightCrawler({31 proxyConfiguration,32 requestHandler: router,33 maxConcurrency: 1,34 requestHandlerTimeoutSecs: 3600, // 1 hour35 navigationTimeoutSecs: 3600,36 headless: true,37 launchContext: {38 launchOptions: {39 args: [40 // `--remote-debugging-port=${CHROME_DEBUGGING_PORT}`, // Enable remote debugging41 // '--remote-debugging-address=0.0.0.0',42 ],43 },44 },45 preNavigationHooks: [46 async ({ page }) => {47 const context = page.context();48 const client = await context.newCDPSession(page);49
50 // Create the proxy server51 server = await createCdpProxyServer(client);52 httpServer = server.listen({ port: containerPort ?? 4321 }, (err: any, address?: string) => {53 if (err) {54 console.error('Error starting server:', err);55 return;56 }57
58 console.log(`Listening on ${address}`);59 });60 },61 ],62});63
64await crawler.run([{ url }]);65
66if (server && httpServer) {67 console.log('Closing server');68 await server.close(httpServer);69}70
71await Actor.exit();
src/routes.ts
1import { createPlaywrightRouter, createPuppeteerRouter, sleep } from 'crawlee';2
3export const router = createPlaywrightRouter();4
5router.addDefaultHandler(async ({ log, page }) => {6 log.info('Waiting for network idle');7
8 log.info('Sleeping for 20 minutes');9 await page.waitForTimeout(20 * 60 * 1000);10 // await sleep(20 * 60 * 1000);11
12 log.info('Done -> closing page');13});
public/index.html
<!DOCTYPE html><html lang="en"><head> <title>Remote browser</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta property="og:title" content="Remote browser"> <meta property="og:description" content="Control your remote browser"> <style> html, body { height: 100vh; width: 100vw; margin: 0; padding: 0; overflow: hidden; } body { font-family: "Courier", monospace; height: 100vh; width: 100vw; } #imageContainer { position: relative; width: 100vw; height: 100vh; display: flex; align-items: center; justify-content: center; overflow: hidden; } #imageContainer img { width: 100%; height: 100%; object-fit: contain; display: block; } </style></head><body><div id="imageContainer"> <p>Waiting for image...</p></div><script> const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'; const socket = new WebSocket(`${protocol}://${window.location.host}/cdp`); const imageContainer = document.getElementById('imageContainer'); let imgElement = null; let requestIdCounter = 1; const pendingRequests = new Map(); const eventHandlers = new Map();
socket.addEventListener('open', async () => { console.log('WebSocket connection opened');
// Function to resize remote browser const resizeRemote = async () => { const rect = imageContainer.getBoundingClientRect(); const width = Math.floor(rect.width); const height = Math.floor(rect.height); await sendCdpCommand('Emulation.setDeviceMetricsOverride', { width, height, deviceScaleFactor: window.devicePixelRatio || 1, mobile: false, screenWidth: width, screenHeight: height, }); };
// Initial resize await resizeRemote();
const version = await sendCdpCommand('Browser.getVersion'); console.log('Browser version:', version);
// Start the screencast await sendCdpCommand('Page.startScreencast', { format: 'jpeg', quality: 100 });
// Listen for window resize and update remote browser window.addEventListener('resize', async () => { await resizeRemote(); }); });
const sendCdpCommand = (method, params) => { return new Promise((resolve, reject) => { const requestId = requestIdCounter++;
try { socket.send(JSON.stringify({ id: requestId, method, params })); pendingRequests.set(requestId, { resolve, reject }); } catch (err) { reject(err); } }); };
socket.addEventListener('message', (event) => { try { const message = JSON.parse(event.data);
if ('id' in message) { console.dir(message); if ('result' in message) { const callback = pendingRequests.get(message.id); if (callback) { callback.resolve(message.result); pendingRequests.delete(message.id); } } else { const callback = pendingRequests.get(message.id); if (callback) { callback.reject(new Error('Error: ', message.error.message)); pendingRequests.delete(message.id); } } } else { const { method, params } = message; const callback = eventHandlers.get(method);
if (callback) { callback(params); } } } catch (err) { console.error('Error parsing message:', err); } });
eventHandlers.set('Page.screencastFrame', async (params) => { const { sessionId, data } = params; const imageUrl = 'data:image/jpeg;base64,' + data; const imageContainer = document.getElementById('imageContainer');
if (!imgElement) { imgElement = document.createElement('img'); imgElement.id = 'image'; imageContainer.innerHTML = ''; imageContainer.appendChild(imgElement);
addMouseEventListeners(imgElement); }
imgElement.src = imageUrl; imgElement.alt = 'Image from screencast';
await sendCdpCommand('Page.screencastFrameAck', { sessionId }); });
const addMouseEventListeners = (element) => { element.addEventListener('mousedown', async (event) => { const { button, clientX, clientY } = event; event.preventDefault();
const rect = imgElement.getBoundingClientRect();
await sendCdpCommand('Input.dispatchMouseEvent', { type: 'mousePressed', x: Math.floor(clientX - rect.left), y: Math.floor(clientY - rect.top), button: button === 0 ? 'left' : button === 1 ? 'middle' : 'right', clickCount: 1, }); });
element.addEventListener('mouseup', async (event) => { const { button, clientX, clientY } = event; event.preventDefault();
const rect = imgElement.getBoundingClientRect();
await sendCdpCommand('Input.dispatchMouseEvent', { type: 'mouseReleased', x: Math.floor(clientX - rect.left), y: Math.floor(clientY - rect.top), button: button === 0 ? 'left' : button === 1 ? 'middle' : 'right', clickCount: 1, }); });
element.addEventListener('wheel', async (event) => { const { clientX, clientY, deltaX, deltaY } = event; event.preventDefault();
const rect = imgElement.getBoundingClientRect(); const x = Math.floor(clientX - rect.left); const y = Math.floor(clientY - rect.top);
await sendCdpCommand('Input.dispatchMouseEvent', { type: 'mouseWheel', x, y, deltaX, deltaY, modifiers: 0, button: 'none', }, { passive: false }); });
element.addEventListener('contextmenu', async (event) => { event.preventDefault();
const rect = imgElement.getBoundingClientRect();
await sendCdpCommand('Input.dispatchMouseEvent', { type: 'mousePressed', x: Math.floor(event.clientX - rect.left), y: Math.floor(event.clientY - rect.top), button: 'right', clickCount: 1, }); await sendCdpCommand('Input.dispatchMouseEvent', { type: 'mouseReleased', x: Math.floor(event.clientX - rect.left), y: Math.floor(event.clientY - rect.top), button: 'right', clickCount: 1, }); });
element.addEventListener('mousemove', async (event) => { const { clientX, clientY } = event;
const rect = imgElement.getBoundingClientRect();
await sendCdpCommand('Input.dispatchMouseEvent', { type: 'mouseMoved', x: Math.floor(clientX - rect.left), y: Math.floor(clientY - rect.top), button: 'none', clickCount: 0, }); }); };
// Keyboard event handling window.addEventListener('keydown', async (event) => { // Ignore modifier keys alone if (event.key.length === 1 || event.key === 'Enter' || event.key === 'Backspace' || event.key === 'Tab') { await sendCdpCommand('Input.dispatchKeyEvent', { type: 'keyDown', key: event.key, code: event.code, keyCode: event.keyCode, windowsVirtualKeyCode: event.keyCode, text: event.key.length === 1 ? event.key : undefined, unmodifiedText: event.key.length === 1 ? event.key : undefined, modifiers: (event.shiftKey ? 8 : 0) | (event.ctrlKey ? 4 : 0) | (event.altKey ? 2 : 0) | (event.metaKey ? 1 : 0), }); event.preventDefault(); } });
window.addEventListener('keyup', async (event) => { if (event.key.length === 1 || event.key === 'Enter' || event.key === 'Backspace' || event.key === 'Tab') { await sendCdpCommand('Input.dispatchKeyEvent', { type: 'keyUp', key: event.key, code: event.code, keyCode: event.keyCode, windowsVirtualKeyCode: event.keyCode, modifiers: (event.shiftKey ? 8 : 0) | (event.ctrlKey ? 4 : 0) | (event.altKey ? 2 : 0) | (event.metaKey ? 1 : 0), }); event.preventDefault(); } });
window.addEventListener('paste', async (event) => { const text = event.clipboardData.getData('text/plain'); if (text) { await sendCdpCommand('Input.insertText', { text }); event.preventDefault(); } });
// Close the connection when the page is closed window.addEventListener('beforeunload', function() { if (socket.readyState === WebSocket.OPEN) { socket.close(); } });</script></body></html>