diff --git a/docs/src/api/class-screencast.md b/docs/src/api/class-screencast.md index c6df3fcd3fb3a..8ed62770bf254 100644 --- a/docs/src/api/class-screencast.md +++ b/docs/src/api/class-screencast.md @@ -4,43 +4,28 @@ Interface for capturing screencast frames from a page. -## event: Screencast.screencastFrame +## async method: Screencast.start * since: v1.59 -- argument: <[Object]> - - `data` <[Buffer]> JPEG-encoded frame data. +- returns: <[Disposable]> -Emitted for each captured JPEG screencast frame while the screencast is running. +Starts capturing screencast frames. **Usage** ```js -const screencast = page.screencast; -screencast.on('screencastframe', ({ data, width, height }) => { - console.log(`frame ${width}x${height}, jpeg size: ${data.length}`); - require('fs').writeFileSync('frame.jpg', data); -}); -await screencast.start({ maxSize: { width: 1200, height: 800 } }); +await page.screencast.start(buffer => { + console.log(`frame size: ${buffer.length}`); +}, { maxSize: { width: 800, height: 600 } }); // ... perform actions ... -await screencast.stop(); +await page.screencast.stop(); ``` -## async method: Screencast.start +### param: Screencast.start.onFrame * since: v1.59 -- returns: <[Disposable]> - -Starts capturing screencast frames. Frames are emitted as [`event: Screencast.screencastFrame`] events. - -**Usage** +* langs: js +- `onFrame` <[function]\([Buffer]\): [Promise|any]> -```js -const screencast = page.screencast; -screencast.on('screencastframe', ({ data, width, height }) => { - console.log(`frame ${width}x${height}, size: ${data.length}`); -}); -await screencast.start({ maxSize: { width: 800, height: 600 } }); -// ... perform actions ... -await screencast.stop(); -``` +Callback that receives JPEG-encoded frame data. ### option: Screencast.start.maxSize * since: v1.59 @@ -50,6 +35,7 @@ await screencast.stop(); Maximum screencast frame dimensions. The output frame may be smaller to preserve the page aspect ratio. Defaults to 800×800. + ## async method: Screencast.stop * since: v1.59 @@ -58,7 +44,7 @@ Stops the screencast started with [`method: Screencast.start`]. **Usage** ```js -await screencast.start(); +await screencast.start(buffer => { /* handle frame */ }); // ... perform actions ... await screencast.stop(); ``` diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 98132f06fd933..55e9b0e7ab036 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -21664,127 +21664,22 @@ export interface Route { */ export interface Screencast { /** - * Emitted for each captured JPEG screencast frame while the screencast is running. + * Starts capturing screencast frames. * * **Usage** * * ```js - * const screencast = page.screencast; - * screencast.on('screencastframe', ({ data, width, height }) => { - * console.log(`frame ${width}x${height}, jpeg size: ${data.length}`); - * require('fs').writeFileSync('frame.jpg', data); - * }); - * await screencast.start({ maxSize: { width: 1200, height: 800 } }); - * // ... perform actions ... - * await screencast.stop(); - * ``` - * - */ - on(event: 'screencastframe', listener: (data: { - /** - * JPEG-encoded frame data. - */ - data: 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: 'screencastframe', listener: (data: { - /** - * JPEG-encoded frame data. - */ - data: Buffer; - }) => any): this; - - /** - * Emitted for each captured JPEG screencast frame while the screencast is running. - * - * **Usage** - * - * ```js - * const screencast = page.screencast; - * screencast.on('screencastframe', ({ data, width, height }) => { - * console.log(`frame ${width}x${height}, jpeg size: ${data.length}`); - * require('fs').writeFileSync('frame.jpg', data); - * }); - * await screencast.start({ maxSize: { width: 1200, height: 800 } }); + * await page.screencast.start(buffer => { + * console.log(`frame size: ${buffer.length}`); + * }, { maxSize: { width: 800, height: 600 } }); * // ... perform actions ... - * await screencast.stop(); - * ``` - * - */ - addListener(event: 'screencastframe', listener: (data: { - /** - * JPEG-encoded frame data. - */ - data: Buffer; - }) => any): this; - - /** - * Removes an event listener added by `on` or `addListener`. - */ - removeListener(event: 'screencastframe', listener: (data: { - /** - * JPEG-encoded frame data. - */ - data: Buffer; - }) => any): this; - - /** - * Removes an event listener added by `on` or `addListener`. - */ - off(event: 'screencastframe', listener: (data: { - /** - * JPEG-encoded frame data. - */ - data: Buffer; - }) => any): this; - - /** - * Emitted for each captured JPEG screencast frame while the screencast is running. - * - * **Usage** - * - * ```js - * const screencast = page.screencast; - * screencast.on('screencastframe', ({ data, width, height }) => { - * console.log(`frame ${width}x${height}, jpeg size: ${data.length}`); - * require('fs').writeFileSync('frame.jpg', data); - * }); - * await screencast.start({ maxSize: { width: 1200, height: 800 } }); - * // ... perform actions ... - * await screencast.stop(); - * ``` - * - */ - prependListener(event: 'screencastframe', listener: (data: { - /** - * JPEG-encoded frame data. - */ - data: Buffer; - }) => any): this; - - /** - * Starts capturing screencast frames. Frames are emitted as - * [screencast.on('screencastframe')](https://playwright.dev/docs/api/class-screencast#screencast-event-screencast-frame) - * events. - * - * **Usage** - * - * ```js - * const screencast = page.screencast; - * screencast.on('screencastframe', ({ data, width, height }) => { - * console.log(`frame ${width}x${height}, size: ${data.length}`); - * }); - * await screencast.start({ maxSize: { width: 800, height: 600 } }); - * // ... perform actions ... - * await screencast.stop(); + * await page.screencast.stop(); * ``` * + * @param onFrame Callback that receives JPEG-encoded frame data. * @param options */ - start(options?: { + start(onFrame: ((buffer: Buffer) => Promise|any), options?: { /** * Maximum screencast frame dimensions. The output frame may be smaller to preserve the page aspect ratio. Defaults to * 800×800. @@ -21804,12 +21699,12 @@ export interface Screencast { /** * Stops the screencast started with - * [screencast.start([options])](https://playwright.dev/docs/api/class-screencast#screencast-start). + * [screencast.start(onFrame[, options])](https://playwright.dev/docs/api/class-screencast#screencast-start). * * **Usage** * * ```js - * await screencast.start(); + * await screencast.start(buffer => { /* handle frame *\/ }); * // ... perform actions ... * await screencast.stop(); * ``` diff --git a/packages/playwright-core/src/client/screencast.ts b/packages/playwright-core/src/client/screencast.ts index bd1ac3dd82daa..d4b29516ba38c 100644 --- a/packages/playwright-core/src/client/screencast.ts +++ b/packages/playwright-core/src/client/screencast.ts @@ -15,26 +15,29 @@ */ import { DisposableStub } from './disposable'; -import { EventEmitter } from './eventEmitter'; import type * as api from '../../types/types'; import type { Page } from './page'; -export class Screencast extends EventEmitter implements api.Screencast { +export class Screencast implements api.Screencast { private readonly _page: Page; + private _onFrame: ((buffer: Buffer) => any) | null = null; constructor(page: Page) { - super(page._platform); this._page = page; - this._page._channel.on('screencastFrame', ({ data }) => this.emit('screencastframe', { data })); + this._page._channel.on('screencastFrame', ({ data }) => { + this._onFrame?.(data); + }); } - async start(options: { maxSize?: { width: number, height: number } } = {}) { + async start(onFrame: (buffer: Buffer) => any, options: { maxSize?: { width: number, height: number } } = {}): Promise { + this._onFrame = onFrame; await this._page._channel.startScreencast(options); return new DisposableStub(() => this.stop()); } async stop(): Promise { + this._onFrame = null; await this._page._channel.stopScreencast(); } } diff --git a/packages/playwright-core/src/tools/dashboard/dashboardController.ts b/packages/playwright-core/src/tools/dashboard/dashboardController.ts index fd8bb68e1fa9c..1a7a99d8d3a00 100644 --- a/packages/playwright-core/src/tools/dashboard/dashboardController.ts +++ b/packages/playwright-core/src/tools/dashboard/dashboardController.ts @@ -199,11 +199,13 @@ export class DashboardConnection implements Transport, DashboardChannel { if (frame === page.mainFrame()) this._sendTabList(); }), - eventsHelper.addEventListener(page.screencast, 'screencastframe', ({ data }) => this._writeFrame(data, page.viewportSize()?.width ?? 0, page.viewportSize()?.height ?? 0)) ); const maxSize = { width: 1280, height: 800 }; - await page.screencast.start({ maxSize }); + await page.screencast.start( + data => this._writeFrame(data, page.viewportSize()?.width ?? 0, page.viewportSize()?.height ?? 0), + { maxSize }, + ); } private _deselectPage() { diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 98132f06fd933..55e9b0e7ab036 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -21664,127 +21664,22 @@ export interface Route { */ export interface Screencast { /** - * Emitted for each captured JPEG screencast frame while the screencast is running. + * Starts capturing screencast frames. * * **Usage** * * ```js - * const screencast = page.screencast; - * screencast.on('screencastframe', ({ data, width, height }) => { - * console.log(`frame ${width}x${height}, jpeg size: ${data.length}`); - * require('fs').writeFileSync('frame.jpg', data); - * }); - * await screencast.start({ maxSize: { width: 1200, height: 800 } }); - * // ... perform actions ... - * await screencast.stop(); - * ``` - * - */ - on(event: 'screencastframe', listener: (data: { - /** - * JPEG-encoded frame data. - */ - data: 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: 'screencastframe', listener: (data: { - /** - * JPEG-encoded frame data. - */ - data: Buffer; - }) => any): this; - - /** - * Emitted for each captured JPEG screencast frame while the screencast is running. - * - * **Usage** - * - * ```js - * const screencast = page.screencast; - * screencast.on('screencastframe', ({ data, width, height }) => { - * console.log(`frame ${width}x${height}, jpeg size: ${data.length}`); - * require('fs').writeFileSync('frame.jpg', data); - * }); - * await screencast.start({ maxSize: { width: 1200, height: 800 } }); + * await page.screencast.start(buffer => { + * console.log(`frame size: ${buffer.length}`); + * }, { maxSize: { width: 800, height: 600 } }); * // ... perform actions ... - * await screencast.stop(); - * ``` - * - */ - addListener(event: 'screencastframe', listener: (data: { - /** - * JPEG-encoded frame data. - */ - data: Buffer; - }) => any): this; - - /** - * Removes an event listener added by `on` or `addListener`. - */ - removeListener(event: 'screencastframe', listener: (data: { - /** - * JPEG-encoded frame data. - */ - data: Buffer; - }) => any): this; - - /** - * Removes an event listener added by `on` or `addListener`. - */ - off(event: 'screencastframe', listener: (data: { - /** - * JPEG-encoded frame data. - */ - data: Buffer; - }) => any): this; - - /** - * Emitted for each captured JPEG screencast frame while the screencast is running. - * - * **Usage** - * - * ```js - * const screencast = page.screencast; - * screencast.on('screencastframe', ({ data, width, height }) => { - * console.log(`frame ${width}x${height}, jpeg size: ${data.length}`); - * require('fs').writeFileSync('frame.jpg', data); - * }); - * await screencast.start({ maxSize: { width: 1200, height: 800 } }); - * // ... perform actions ... - * await screencast.stop(); - * ``` - * - */ - prependListener(event: 'screencastframe', listener: (data: { - /** - * JPEG-encoded frame data. - */ - data: Buffer; - }) => any): this; - - /** - * Starts capturing screencast frames. Frames are emitted as - * [screencast.on('screencastframe')](https://playwright.dev/docs/api/class-screencast#screencast-event-screencast-frame) - * events. - * - * **Usage** - * - * ```js - * const screencast = page.screencast; - * screencast.on('screencastframe', ({ data, width, height }) => { - * console.log(`frame ${width}x${height}, size: ${data.length}`); - * }); - * await screencast.start({ maxSize: { width: 800, height: 600 } }); - * // ... perform actions ... - * await screencast.stop(); + * await page.screencast.stop(); * ``` * + * @param onFrame Callback that receives JPEG-encoded frame data. * @param options */ - start(options?: { + start(onFrame: ((buffer: Buffer) => Promise|any), options?: { /** * Maximum screencast frame dimensions. The output frame may be smaller to preserve the page aspect ratio. Defaults to * 800×800. @@ -21804,12 +21699,12 @@ export interface Screencast { /** * Stops the screencast started with - * [screencast.start([options])](https://playwright.dev/docs/api/class-screencast#screencast-start). + * [screencast.start(onFrame[, options])](https://playwright.dev/docs/api/class-screencast#screencast-start). * * **Usage** * * ```js - * await screencast.start(); + * await screencast.start(buffer => { /* handle frame *\/ }); * // ... perform actions ... * await screencast.stop(); * ``` diff --git a/tests/library/screencast.spec.ts b/tests/library/screencast.spec.ts index 7ab98d300b008..9191361701cc3 100644 --- a/tests/library/screencast.spec.ts +++ b/tests/library/screencast.spec.ts @@ -20,16 +20,14 @@ import { rafraf } from '../config/utils'; test.skip(({ mode }) => mode !== 'default', 'screencast is not available in remote mode'); test.skip(({ video }) => video === 'on', 'conflicts with built-in video recording'); -test('screencast.start emits screencastframe events', async ({ browser, server, trace }) => { +test('screencast.start delivers frames via onFrame callback', async ({ browser, server, trace }) => { test.skip(trace === 'on', 'trace=on has different screencast image configuration'); const context = await browser.newContext({ viewport: { width: 1000, height: 400 } }); const page = await context.newPage(); - const frames: { data: Buffer }[] = []; - page.screencast.on('screencastframe', frame => frames.push(frame)); - + const frames: Buffer[] = []; const maxSize = { width: 500, height: 400 }; - await page.screencast.start({ maxSize }); + await page.screencast.start(frame => frames.push(frame), { maxSize }); await page.goto(server.EMPTY_PAGE); await page.evaluate(() => document.body.style.backgroundColor = 'red'); await rafraf(page, 100); @@ -38,9 +36,9 @@ test('screencast.start emits screencastframe events', async ({ browser, server, expect(frames.length).toBeGreaterThan(0); for (const frame of frames) { // Each frame must be a valid JPEG (starts with FF D8) - expect(frame.data[0]).toBe(0xff); - expect(frame.data[1]).toBe(0xd8); - const { width, height } = jpegDimensions(frame.data); + expect(frame[0]).toBe(0xff); + expect(frame[1]).toBe(0xd8); + const { width, height } = jpegDimensions(frame); // Frame should be scaled down to fit the maximum size. expect(width).toBe(500); expect(height).toBe(200); @@ -56,8 +54,8 @@ test('start throws if already running', async ({ browser, trace }) => { const context = await browser.newContext({ viewport: size }); const page = await context.newPage(); - await page.screencast.start({ maxSize: size }); - await expect(page.screencast.start({ maxSize: { width: 320, height: 240 } })).rejects.toThrow('Screencast is already running'); + await page.screencast.start(() => {}, { maxSize: size }); + await expect(page.screencast.start(() => {}, { maxSize: { width: 320, height: 240 } })).rejects.toThrow('Screencast is already running'); await page.screencast.stop(); await context.close(); @@ -69,10 +67,10 @@ test('start allows restart with different options after stop', async ({ browser, const context = await browser.newContext({ viewport: { width: 500, height: 400 } }); const page = await context.newPage(); - await page.screencast.start({ maxSize: { width: 500, height: 400 } }); + await page.screencast.start(() => {}, { maxSize: { width: 500, height: 400 } }); await page.screencast.stop(); // Different options should succeed once the previous screencast is stopped. - await page.screencast.start({ maxSize: { width: 320, height: 240 } }); + await page.screencast.start(() => {}, { maxSize: { width: 320, height: 240 } }); await page.screencast.stop(); await context.close(); }); @@ -85,43 +83,33 @@ test('start throws when video recording is running with different params', async const page = await context.newPage(); await page.video().start({ size: videoSize }); - await expect(page.screencast.start({ maxSize: { width: 320, height: 240 } })).rejects.toThrow('Screencast is already running with different options'); + await expect(page.screencast.start(() => {}, { maxSize: { width: 320, height: 240 } })).rejects.toThrow('Screencast is already running with different options'); await page.video().stop(); await context.close(); }); -test('screencast.start dispose stops screencast', async ({ browser, server, trace }) => { +test('start returns a disposable that stops screencast', async ({ browser, server, trace }) => { test.skip(trace === 'on', 'trace=on has different screencast image configuration'); - const context = await browser.newContext({ viewport: { width: 1000, height: 400 } }); + const context = await browser.newContext({ viewport: { width: 500, height: 400 } }); const page = await context.newPage(); - const frames: { data: Buffer }[] = []; - page.screencast.on('screencastframe', frame => frames.push(frame)); - - const disposable = await page.screencast.start({ maxSize: { width: 500, height: 400 } }); + const frames: Buffer[] = []; + const disposable = await page.screencast.start(frame => frames.push(frame), { maxSize: { width: 500, height: 400 } }); await page.goto(server.EMPTY_PAGE); await page.evaluate(() => document.body.style.backgroundColor = 'red'); await rafraf(page, 100); await disposable.dispose(); - expect(frames.length).toBeGreaterThan(0); - await context.close(); -}); + const frameCountAfterDispose = frames.length; + expect(frameCountAfterDispose).toBeGreaterThan(0); -test('video.start does not emit screencastframe events', async ({ page, server, trace }) => { - test.skip(trace === 'on', 'trace=on enables screencast frame events'); - - const frames = []; - page.screencast.on('screencastframe', frame => frames.push(frame)); - - await page.video().start({ size: { width: 320, height: 240 } }); - await page.goto(server.EMPTY_PAGE); - await page.evaluate(() => document.body.style.backgroundColor = 'red'); + // No more frames should arrive after dispose. + await page.evaluate(() => document.body.style.backgroundColor = 'blue'); await rafraf(page, 100); - await page.video().stop(); + expect(frames.length).toBe(frameCountAfterDispose); - expect(frames).toHaveLength(0); + await context.close(); }); function jpegDimensions(buffer: Buffer): { width: number, height: number } {