From 77cb3577c2972ca6414b7a10b66b7c53aaa6bf48 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Mon, 2 Mar 2026 17:31:53 -0800 Subject: [PATCH 1/7] feat(video): add screencast mode to Video.start for JPEG frame events --- docs/src/api/class-video.md | 24 +++++++ packages/playwright-client/types/types.d.ts | 70 +++++++++++++++++++ packages/playwright-core/src/client/video.ts | 22 +++++- .../playwright-core/src/protocol/validator.ts | 6 +- .../src/server/dispatchers/pageDispatcher.ts | 21 +++++- .../playwright-core/src/server/screencast.ts | 1 + packages/playwright-core/types/types.d.ts | 70 +++++++++++++++++++ packages/protocol/src/channels.d.ts | 9 ++- packages/protocol/src/protocol.yml | 11 ++- tests/library/video.spec.ts | 24 +++++++ 10 files changed, 251 insertions(+), 7 deletions(-) diff --git a/docs/src/api/class-video.md b/docs/src/api/class-video.md index 294478f29373b..df8950961b7b1 100644 --- a/docs/src/api/class-video.md +++ b/docs/src/api/class-video.md @@ -92,6 +92,23 @@ Saves the video to a user-specified path. If using the sync API, this must be ca Path where the video should be saved. +## event: Video.frame +* since: v1.59 +* langs: js +- argument: <[Buffer]> + +Emitted for each screencast frame when video was started with `mode: 'screencast'`. + +**Usage** + +```js +const video = page.video(); +video.on('frame', data => console.log('received frame, jpeg size:', data.length)); +await video.start({ mode: 'screencast' }); +// ... perform actions ... +await video.stop(); +``` + ## async method: Video.start * since: v1.59 @@ -129,6 +146,13 @@ await page.Video.StartAsync(); await page.Video.StopAsync(new() { Path = "video.webm" }); ``` +### option: Video.start.mode +* since: v1.59 +* langs: js +- `mode` ?<[VideoMode]<"video"|"screencast">> + +Recording mode. When set to `'screencast'`, JPEG frames are emitted as [`event: Video.frame`] events on the [Video] object instead of writing a video file. Defaults to `'video'`. + ### option: Video.start.size * since: v1.59 - `size` ?<[Object]> diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 979a929dd6823..177f24e1db8e7 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -21920,6 +21920,69 @@ export interface Tracing { * */ export interface Video { + /** + * Emitted for each screencast frame when video was started with `mode: 'screencast'`. + * + * **Usage** + * + * ```js + * const video = page.video(); + * video.on('frame', data => console.log('received frame, jpeg size:', data.length)); + * await video.start({ mode: 'screencast' }); + * // ... perform actions ... + * await video.stop(); + * ``` + * + */ + on(event: 'frame', listener: (buffer: Buffer) => any): this; + + /** + * Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event. + */ + once(event: 'frame', listener: (buffer: Buffer) => any): this; + + /** + * Emitted for each screencast frame when video was started with `mode: 'screencast'`. + * + * **Usage** + * + * ```js + * const video = page.video(); + * video.on('frame', data => console.log('received frame, jpeg size:', data.length)); + * await video.start({ mode: 'screencast' }); + * // ... perform actions ... + * await video.stop(); + * ``` + * + */ + addListener(event: 'frame', listener: (buffer: Buffer) => any): this; + + /** + * Removes an event listener added by `on` or `addListener`. + */ + removeListener(event: 'frame', listener: (buffer: Buffer) => any): this; + + /** + * Removes an event listener added by `on` or `addListener`. + */ + off(event: 'frame', listener: (buffer: Buffer) => any): this; + + /** + * Emitted for each screencast frame when video was started with `mode: 'screencast'`. + * + * **Usage** + * + * ```js + * const video = page.video(); + * video.on('frame', data => console.log('received frame, jpeg size:', data.length)); + * await video.start({ mode: 'screencast' }); + * // ... perform actions ... + * await video.stop(); + * ``` + * + */ + prependListener(event: 'frame', listener: (buffer: Buffer) => any): this; + /** * Deletes the video file. Will wait for the video to finish if necessary. */ @@ -21952,6 +22015,13 @@ export interface Video { * @param options */ start(options?: { + /** + * Recording mode. When set to `'screencast'`, JPEG frames are emitted as + * [video.on('frame')](https://playwright.dev/docs/api/class-video#video-event-frame) events on the + * [Video](https://playwright.dev/docs/api/class-video) object instead of writing a video file. Defaults to `'video'`. + */ + mode?: "video"|"screencast"; + /** * Optional dimensions of the recorded video. If not specified the size will be equal to page viewport scaled down to * fit into 800x800. Actual picture of the page will be scaled down if necessary to fit the specified size. diff --git a/packages/playwright-core/src/client/video.ts b/packages/playwright-core/src/client/video.ts index 2c0d700508b7e..9963852fab7ec 100644 --- a/packages/playwright-core/src/client/video.ts +++ b/packages/playwright-core/src/client/video.ts @@ -15,30 +15,46 @@ */ import { Artifact } from './artifact'; +import { EventEmitter } from './eventEmitter'; import type { Connection } from './connection'; import type { Page } from './page'; import type * as api from '../../types/types'; +import type * as channels from '@protocol/channels'; -export class Video implements api.Video { +export class Video extends EventEmitter implements api.Video { private _artifact: Artifact | undefined; private _isRemote = false; private _page: Page; + private _stopFrameEvents: (() => void) | null = null; constructor(page: Page, connection: Connection, artifact: Artifact | undefined) { + super(page._platform); this._page = page; this._isRemote = connection.isRemote(); this._artifact = artifact; } - async start(options: { size?: { width: number, height: number } } = {}): Promise { + async start(options: { size?: { width: number, height: number }, mode?: 'video' | 'screencast' } = {}): Promise { const result = await this._page._channel.videoStart(options); - this._artifact = Artifact.from(result.artifact); + if (result.artifact) + this._artifact = Artifact.from(result.artifact); + if (options.mode === 'screencast') { + const listener = ({ data }: channels.PageVideoFrameEvent) => { + this.emit('frame', data); + }; + this._page._channel.on('videoFrame', listener); + this._stopFrameEvents = () => (this._page._channel as unknown as EventEmitter).removeListener('videoFrame', listener); + } } async stop(options: { path?: string } = {}): Promise { await this._page._wrapApiCall(async () => { await this._page._channel.videoStop(); + if (this._stopFrameEvents) { + this._stopFrameEvents(); + this._stopFrameEvents = null; + } if (options.path) await this.saveAs(options.path); }); diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index d554ad8983b86..ac00a8782ed63 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -1231,6 +1231,9 @@ scheme.PageWebSocketRouteEvent = tObject({ scheme.PageWebSocketEvent = tObject({ webSocket: tChannel(['WebSocket']), }); +scheme.PageVideoFrameEvent = tObject({ + data: tBinary, +}); scheme.PageWorkerEvent = tObject({ worker: tChannel(['Worker']), }); @@ -1541,9 +1544,10 @@ scheme.PageVideoStartParams = tObject({ width: tInt, height: tInt, })), + mode: tOptional(tEnum(['video', 'screencast'])), }); scheme.PageVideoStartResult = tObject({ - artifact: tChannel(['Artifact']), + artifact: tOptional(tChannel(['Artifact'])), }); scheme.PageVideoStopParams = tOptional(tObject({})); scheme.PageVideoStopResult = tOptional(tObject({})); diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index 96875090ee40d..b256011ad4755 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -17,6 +17,7 @@ import { Page, Worker } from '../page'; import { Dispatcher } from './dispatcher'; import { parseError, serializeError } from '../errors'; +import { validateVideoSize } from '../browserContext'; import { ArtifactDispatcher } from './artifactDispatcher'; import { ElementHandleDispatcher } from './elementHandlerDispatcher'; import { FrameDispatcher } from './frameDispatcher'; @@ -27,6 +28,7 @@ import { RouteDispatcher, WebSocketDispatcher } from './networkDispatchers'; import { WebSocketRouteDispatcher } from './webSocketRouteDispatcher'; import { SdkObject } from '../instrumentation'; import { deserializeURLMatch, urlMatches } from '../../utils/isomorphic/urlMatch'; +import { eventsHelper } from '../../utils'; import { PageAgentDispatcher } from './pageAgentDispatcher'; import { Recorder } from '../recorder'; @@ -38,11 +40,13 @@ import type { FileChooser } from '../fileChooser'; import type { JSHandle } from '../javascript'; import type { BrowserContextDispatcher } from './browserContextDispatcher'; import type { Frame } from '../frames'; +import type { RegisteredListener } from '../../utils'; import type { RouteHandler } from '../network'; import type { InitScript, PageBinding } from '../page'; import type * as channels from '@protocol/channels'; import type { Progress } from '@protocol/progress'; import type { URLMatch } from '../../utils/isomorphic/urlMatch'; +import type * as types from '../types'; export class PageDispatcher extends Dispatcher implements channels.PageChannel { _type_EventTarget = true; @@ -58,6 +62,7 @@ export class PageDispatcher extends Dispatcher(); private _jsCoverageActive = false; private _cssCoverageActive = false; + private _videoFrameListener: RegisteredListener | null = null; static from(parentScope: BrowserContextDispatcher, page: Page): PageDispatcher { return PageDispatcher.fromNullable(parentScope, page)!; @@ -357,11 +362,25 @@ export class PageDispatcher extends Dispatcher { + if (params.mode === 'screencast') { + const size = validateVideoSize(params.size, this._page.emulatedSize()?.viewport); + this._videoFrameListener = eventsHelper.addEventListener(this._page, Page.Events.ScreencastFrame, (frame: types.ScreencastFrame) => { + this._dispatchEvent('videoFrame', { data: frame.buffer }); + }); + await this._page.screencast.startScreencast(this, { quality: 90, width: size.width, height: size.height }); + return {}; + } const artifact = await this._page.screencast.startExplicitVideoRecording(params); - return { artifact: createVideoDispatcher(this.parentScope(), artifact) }; + return { artifact: createVideoDispatcher(this.parentScope(), artifact!) }; } async videoStop(params: channels.PageVideoStopParams, progress: Progress): Promise { + if (this._videoFrameListener) { + await this._page.screencast.stopScreencast(this); + eventsHelper.removeEventListeners([this._videoFrameListener]); + this._videoFrameListener = null; + return; + } await this._page.screencast.stopExplicitVideoRecording(); } diff --git a/packages/playwright-core/src/server/screencast.ts b/packages/playwright-core/src/server/screencast.ts index 38de70b9e9a75..d84eea5d41406 100644 --- a/packages/playwright-core/src/server/screencast.ts +++ b/packages/playwright-core/src/server/screencast.ts @@ -28,6 +28,7 @@ export class Screencast { private _page: Page; private _videoRecorder: VideoRecorder | null = null; private _videoId: string | null = null; + private _framesMode = false; private _screencastClients = new Set(); // Aiming at 25 fps by default - each frame is 40ms, but we give some slack with 35ms. diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 979a929dd6823..177f24e1db8e7 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -21920,6 +21920,69 @@ export interface Tracing { * */ export interface Video { + /** + * Emitted for each screencast frame when video was started with `mode: 'screencast'`. + * + * **Usage** + * + * ```js + * const video = page.video(); + * video.on('frame', data => console.log('received frame, jpeg size:', data.length)); + * await video.start({ mode: 'screencast' }); + * // ... perform actions ... + * await video.stop(); + * ``` + * + */ + on(event: 'frame', listener: (buffer: Buffer) => any): this; + + /** + * Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event. + */ + once(event: 'frame', listener: (buffer: Buffer) => any): this; + + /** + * Emitted for each screencast frame when video was started with `mode: 'screencast'`. + * + * **Usage** + * + * ```js + * const video = page.video(); + * video.on('frame', data => console.log('received frame, jpeg size:', data.length)); + * await video.start({ mode: 'screencast' }); + * // ... perform actions ... + * await video.stop(); + * ``` + * + */ + addListener(event: 'frame', listener: (buffer: Buffer) => any): this; + + /** + * Removes an event listener added by `on` or `addListener`. + */ + removeListener(event: 'frame', listener: (buffer: Buffer) => any): this; + + /** + * Removes an event listener added by `on` or `addListener`. + */ + off(event: 'frame', listener: (buffer: Buffer) => any): this; + + /** + * Emitted for each screencast frame when video was started with `mode: 'screencast'`. + * + * **Usage** + * + * ```js + * const video = page.video(); + * video.on('frame', data => console.log('received frame, jpeg size:', data.length)); + * await video.start({ mode: 'screencast' }); + * // ... perform actions ... + * await video.stop(); + * ``` + * + */ + prependListener(event: 'frame', listener: (buffer: Buffer) => any): this; + /** * Deletes the video file. Will wait for the video to finish if necessary. */ @@ -21952,6 +22015,13 @@ export interface Video { * @param options */ start(options?: { + /** + * Recording mode. When set to `'screencast'`, JPEG frames are emitted as + * [video.on('frame')](https://playwright.dev/docs/api/class-video#video-event-frame) events on the + * [Video](https://playwright.dev/docs/api/class-video) object instead of writing a video file. Defaults to `'video'`. + */ + mode?: "video"|"screencast"; + /** * Optional dimensions of the recorded video. If not specified the size will be equal to page viewport scaled down to * fit into 800x800. Actual picture of the page will be scaled down if necessary to fit the specified size. diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index f7206cdc0052b..b582c13c77eac 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -2102,6 +2102,7 @@ export interface PageEventTarget { on(event: 'route', callback: (params: PageRouteEvent) => void): this; on(event: 'webSocketRoute', callback: (params: PageWebSocketRouteEvent) => void): this; on(event: 'webSocket', callback: (params: PageWebSocketEvent) => void): this; + on(event: 'videoFrame', callback: (params: PageVideoFrameEvent) => void): this; on(event: 'worker', callback: (params: PageWorkerEvent) => void): this; } export interface PageChannel extends PageEventTarget, EventTargetChannel { @@ -2192,6 +2193,9 @@ export type PageWebSocketRouteEvent = { export type PageWebSocketEvent = { webSocket: WebSocketChannel, }; +export type PageVideoFrameEvent = { + data: Binary, +}; export type PageWorkerEvent = { worker: WorkerChannel, }; @@ -2667,15 +2671,17 @@ export type PageVideoStartParams = { width: number, height: number, }, + mode?: 'video' | 'screencast', }; export type PageVideoStartOptions = { size?: { width: number, height: number, }, + mode?: 'video' | 'screencast', }; export type PageVideoStartResult = { - artifact: ArtifactChannel, + artifact?: ArtifactChannel, }; export type PageVideoStopParams = {}; export type PageVideoStopOptions = {}; @@ -2744,6 +2750,7 @@ export interface PageEvents { 'route': PageRouteEvent; 'webSocketRoute': PageWebSocketRouteEvent; 'webSocket': PageWebSocketEvent; + 'videoFrame': PageVideoFrameEvent; 'worker': PageWorkerEvent; } diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 1e27b0edbd380..74eec4c6cd1b4 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -2083,8 +2083,13 @@ Page: properties: width: int height: int + mode: + type: enum? + literals: + - video + - screencast returns: - artifact: Artifact + artifact: Artifact? videoStop: title: Stop video recording @@ -2185,6 +2190,10 @@ Page: parameters: webSocket: WebSocket + videoFrame: + parameters: + data: binary + worker: parameters: worker: Worker diff --git a/tests/library/video.spec.ts b/tests/library/video.spec.ts index 89154044bf50f..bf3bc788afbf0 100644 --- a/tests/library/video.spec.ts +++ b/tests/library/video.spec.ts @@ -897,6 +897,30 @@ it.describe('screencast', () => { await context.close(); expectFrames(videoPath, size, isAlmostWhite); }); + + it('video.start with frames option emits frame events', async ({ browser, browserName, server }) => { + const size = browserName === 'firefox' ? { width: 500, height: 400 } : { width: 320, height: 240 }; + const context = await browser.newContext({ viewport: size }); + const page = await context.newPage(); + + const frames: Buffer[] = []; + page.video().on('frame', (data: Buffer) => frames.push(data)); + + await page.video().start({ size, mode: 'screencast' }); + await page.goto(server.EMPTY_PAGE); + await page.evaluate(() => document.body.style.backgroundColor = 'red'); + await rafraf(page, 100); + await page.video().stop(); + + expect(frames.length).toBeGreaterThan(0); + // Each frame must be a valid JPEG (starts with FF D8) + for (const frame of frames) { + expect(frame[0]).toBe(0xff); + expect(frame[1]).toBe(0xd8); + } + + await context.close(); + }); }); it('should saveAs video', async ({ browser }, testInfo) => { From 1cd284749bb400c2a4a71a3b49d5bb45c9e5a818 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 3 Mar 2026 11:59:30 -0800 Subject: [PATCH 2/7] move listeners --- packages/playwright-core/src/client/page.ts | 1 + packages/playwright-core/src/client/video.ts | 13 ------------- .../src/server/dispatchers/pageDispatcher.ts | 15 +++------------ packages/playwright-core/src/server/screencast.ts | 1 - 4 files changed, 4 insertions(+), 26 deletions(-) diff --git a/packages/playwright-core/src/client/page.ts b/packages/playwright-core/src/client/page.ts index 42f14dec3bb7d..76cf483c33cf0 100644 --- a/packages/playwright-core/src/client/page.ts +++ b/packages/playwright-core/src/client/page.ts @@ -142,6 +142,7 @@ export class Page extends ChannelOwner implements api.Page const artifactObject = Artifact.from(artifact); this.emit(Events.Page.Download, new Download(this, url, suggestedFilename, artifactObject)); }); + this._channel.on('videoFrame', ({ data }) => this._video.emit('frame', data)); this._channel.on('fileChooser', ({ element, isMultiple }) => this.emit(Events.Page.FileChooser, new FileChooser(this, ElementHandle.from(element), isMultiple))); this._channel.on('frameAttached', ({ frame }) => this._onFrameAttached(Frame.from(frame))); this._channel.on('frameDetached', ({ frame }) => this._onFrameDetached(Frame.from(frame))); diff --git a/packages/playwright-core/src/client/video.ts b/packages/playwright-core/src/client/video.ts index 9963852fab7ec..3a56812ed29e1 100644 --- a/packages/playwright-core/src/client/video.ts +++ b/packages/playwright-core/src/client/video.ts @@ -20,13 +20,11 @@ import { EventEmitter } from './eventEmitter'; import type { Connection } from './connection'; import type { Page } from './page'; import type * as api from '../../types/types'; -import type * as channels from '@protocol/channels'; export class Video extends EventEmitter implements api.Video { private _artifact: Artifact | undefined; private _isRemote = false; private _page: Page; - private _stopFrameEvents: (() => void) | null = null; constructor(page: Page, connection: Connection, artifact: Artifact | undefined) { super(page._platform); @@ -39,22 +37,11 @@ export class Video extends EventEmitter implements api.Video { const result = await this._page._channel.videoStart(options); if (result.artifact) this._artifact = Artifact.from(result.artifact); - if (options.mode === 'screencast') { - const listener = ({ data }: channels.PageVideoFrameEvent) => { - this.emit('frame', data); - }; - this._page._channel.on('videoFrame', listener); - this._stopFrameEvents = () => (this._page._channel as unknown as EventEmitter).removeListener('videoFrame', listener); - } } async stop(options: { path?: string } = {}): Promise { await this._page._wrapApiCall(async () => { await this._page._channel.videoStop(); - if (this._stopFrameEvents) { - this._stopFrameEvents(); - this._stopFrameEvents = null; - } if (options.path) await this.saveAs(options.path); }); diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index b256011ad4755..918efda653c73 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -28,7 +28,6 @@ import { RouteDispatcher, WebSocketDispatcher } from './networkDispatchers'; import { WebSocketRouteDispatcher } from './webSocketRouteDispatcher'; import { SdkObject } from '../instrumentation'; import { deserializeURLMatch, urlMatches } from '../../utils/isomorphic/urlMatch'; -import { eventsHelper } from '../../utils'; import { PageAgentDispatcher } from './pageAgentDispatcher'; import { Recorder } from '../recorder'; @@ -62,7 +61,6 @@ export class PageDispatcher extends Dispatcher(); private _jsCoverageActive = false; private _cssCoverageActive = false; - private _videoFrameListener: RegisteredListener | null = null; static from(parentScope: BrowserContextDispatcher, page: Page): PageDispatcher { return PageDispatcher.fromNullable(parentScope, page)!; @@ -111,6 +109,7 @@ export class PageDispatcher extends Dispatcher this._dispatchEvent('videoFrame', { data: frame.buffer })); this.addObjectListener(Page.Events.EmulatedSizeChanged, () => this._dispatchEvent('viewportSizeChanged', { viewportSize: page.emulatedSize()?.viewport })); this.addObjectListener(Page.Events.FileChooser, (fileChooser: FileChooser) => this._dispatchEvent('fileChooser', { element: ElementHandleDispatcher.from(mainFrame, fileChooser.element()), @@ -364,23 +363,15 @@ export class PageDispatcher extends Dispatcher { if (params.mode === 'screencast') { const size = validateVideoSize(params.size, this._page.emulatedSize()?.viewport); - this._videoFrameListener = eventsHelper.addEventListener(this._page, Page.Events.ScreencastFrame, (frame: types.ScreencastFrame) => { - this._dispatchEvent('videoFrame', { data: frame.buffer }); - }); await this._page.screencast.startScreencast(this, { quality: 90, width: size.width, height: size.height }); return {}; } const artifact = await this._page.screencast.startExplicitVideoRecording(params); - return { artifact: createVideoDispatcher(this.parentScope(), artifact!) }; + return { artifact: createVideoDispatcher(this.parentScope(), artifact) }; } async videoStop(params: channels.PageVideoStopParams, progress: Progress): Promise { - if (this._videoFrameListener) { - await this._page.screencast.stopScreencast(this); - eventsHelper.removeEventListeners([this._videoFrameListener]); - this._videoFrameListener = null; - return; - } + await this._page.screencast.stopScreencast(this); await this._page.screencast.stopExplicitVideoRecording(); } diff --git a/packages/playwright-core/src/server/screencast.ts b/packages/playwright-core/src/server/screencast.ts index d84eea5d41406..38de70b9e9a75 100644 --- a/packages/playwright-core/src/server/screencast.ts +++ b/packages/playwright-core/src/server/screencast.ts @@ -28,7 +28,6 @@ export class Screencast { private _page: Page; private _videoRecorder: VideoRecorder | null = null; private _videoId: string | null = null; - private _framesMode = false; private _screencastClients = new Set(); // Aiming at 25 fps by default - each frame is 40ms, but we give some slack with 35ms. From b81a3502985951fe433fc22b8e623b65b969379d Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Tue, 3 Mar 2026 14:59:20 -0800 Subject: [PATCH 3/7] inspector namespace --- docs/src/api/class-inspector.md | 71 ++++++ docs/src/api/class-page.md | 19 ++ docs/src/api/class-video.md | 24 -- packages/playwright-client/types/types.d.ts | 225 ++++++++++++------ .../playwright-core/src/client/inspector.ts | 38 +++ packages/playwright-core/src/client/page.ts | 8 +- packages/playwright-core/src/client/video.ts | 5 +- .../playwright-core/src/protocol/validator.ts | 18 +- .../src/server/dispatchers/pageDispatcher.ts | 22 +- .../src/utils/isomorphic/protocolMetainfo.ts | 2 + packages/playwright-core/types/types.d.ts | 225 ++++++++++++------ packages/protocol/src/channels.d.ts | 32 ++- packages/protocol/src/protocol.yml | 29 ++- tests/library/video.spec.ts | 12 +- 14 files changed, 523 insertions(+), 207 deletions(-) create mode 100644 docs/src/api/class-inspector.md create mode 100644 packages/playwright-core/src/client/inspector.ts diff --git a/docs/src/api/class-inspector.md b/docs/src/api/class-inspector.md new file mode 100644 index 0000000000000..9ec866a7447d3 --- /dev/null +++ b/docs/src/api/class-inspector.md @@ -0,0 +1,71 @@ +# class: Inspector +* since: v1.59 +* langs: js + +The `Inspector` object provides access to the Playwright inspector's screencast capabilities, allowing you to capture live JPEG frames from the page as it renders. + +**Usage** + +```js +const inspector = page.inspector(); +inspector.on('screencastFrame', data => { + console.log('received frame, jpeg size:', data.length); +}); +await inspector.startScreencast(); +// ... perform actions ... +await inspector.stopScreencast(); +``` + +## event: Inspector.screencastFrame +* since: v1.59 +- argument: <[Buffer]> + +Emitted for each captured JPEG screencast frame while the screencast is running. + +**Usage** + +```js +const inspector = page.inspector(); +inspector.on('screencastFrame', data => { + require('fs').writeFileSync('frame.jpg', data); +}); +await inspector.startScreencast({ size: { width: 1280, height: 720 } }); +// ... perform actions ... +await inspector.stopScreencast(); +``` + +## async method: Inspector.startScreencast +* since: v1.59 + +Starts capturing screencast frames. Frames are emitted as [`event: Inspector.screencastFrame`] events. + +**Usage** + +```js +const inspector = page.inspector(); +inspector.on('screencastFrame', data => console.log('frame size:', data.length)); +await inspector.startScreencast({ size: { width: 800, height: 600 } }); +// ... perform actions ... +await inspector.stopScreencast(); +``` + +### option: Inspector.startScreencast.size +* since: v1.59 +- `size` ?<[Object]> + - `width` <[int]> Frame width in pixels. + - `height` <[int]> Frame height in pixels. + +Optional dimensions for the screencast frames. If not specified, the page viewport size is used. + +## async method: Inspector.stopScreencast +* since: v1.59 + +Stops the screencast started with [`method: Inspector.startScreencast`]. + +**Usage** + +```js +await inspector.startScreencast(); +// ... perform actions ... +await inspector.stopScreencast(); +``` diff --git a/docs/src/api/class-page.md b/docs/src/api/class-page.md index fa226172acb9f..4618253e18d4d 100644 --- a/docs/src/api/class-page.md +++ b/docs/src/api/class-page.md @@ -2597,6 +2597,25 @@ Throws for non-input elements. However, if the element is inside the `