From 2a6c6abc31a6b08c8cf8dfa26a85a87750a1c31e Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 5 May 2026 14:22:50 -0700 Subject: [PATCH] fix(screencast): plumb viewport size through onFrame callback Browsers already report deviceWidth/deviceHeight with each screencast frame. Forward them through the protocol event into the client onFrame callback so consumers do not need a separate viewport query (which can race with frame delivery and stall when the page is unresponsive). Use the new fields in the dashboard controller instead of calling page.viewportSize() inside onFrame. Fixes: https://github.com/microsoft/playwright/issues/40625 --- docs/src/api/class-screencast.md | 8 +++++-- packages/playwright-client/types/types.d.ts | 6 +++-- .../playwright-core/src/client/screencast.ts | 8 +++---- .../playwright-core/src/protocol/validator.ts | 2 ++ .../src/server/dispatchers/pageDispatcher.ts | 2 +- .../tools/dashboard/dashboardController.ts | 5 ++-- packages/playwright-core/types/types.d.ts | 6 +++-- packages/protocol/spec/page.yml | 2 ++ packages/protocol/src/channels.d.ts | 2 ++ tests/library/screencast.spec.ts | 23 +++++++++++++++++++ utils/generate_types/overrides.d.ts | 2 +- 11 files changed, 51 insertions(+), 15 deletions(-) diff --git a/docs/src/api/class-screencast.md b/docs/src/api/class-screencast.md index 7b28daf8b1ff3..4d1ba1843ea8e 100644 --- a/docs/src/api/class-screencast.md +++ b/docs/src/api/class-screencast.md @@ -22,7 +22,9 @@ await page.screencast.stop(); ```js // Capture frames await page.screencast.start({ - onFrame: ({ data }) => console.log(`frame size: ${data.length}`), + onFrame: ({ data, viewportWidth, viewportHeight }) => { + console.log(`frame size: ${data.length} (${viewportWidth}x${viewportHeight})`); + }, size: { width: 800, height: 600 }, }); // ... perform actions ... @@ -34,8 +36,10 @@ await page.screencast.stop(); - `onFrame` <[function]\([Object]\): [Promise]> - alias: ScreencastFrame - `data` <[Buffer]> JPEG-encoded frame data. + - `viewportWidth` <[int]> Width of the page viewport at the time the frame was captured. + - `viewportHeight` <[int]> Height of the page viewport at the time the frame was captured. -Callback that receives JPEG-encoded frame data. +Callback that receives JPEG-encoded frame data along with the page viewport size at the time of capture. ### option: Screencast.start.path * since: v1.59 diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index 4a33756068ddb..db9b9739919a0 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -16557,7 +16557,9 @@ export interface Screencast { * ```js * // Capture frames * await page.screencast.start({ - * onFrame: ({ data }) => console.log(`frame size: ${data.length}`), + * onFrame: ({ data, viewportWidth, viewportHeight }) => { + * console.log(`frame size: ${data.length} (${viewportWidth}x${viewportHeight})`); + * }, * size: { width: 800, height: 600 }, * }); * // ... perform actions ... @@ -16567,7 +16569,7 @@ export interface Screencast { * @param options */ start(options?: { - onFrame?: (frame: { data: Buffer }) => Promise|any; + onFrame?: (frame: { data: Buffer, viewportWidth: number, viewportHeight: number }) => Promise|any; path?: string; size?: { width: number; diff --git a/packages/playwright-core/src/client/screencast.ts b/packages/playwright-core/src/client/screencast.ts index 83c21db702457..e4a775688e20d 100644 --- a/packages/playwright-core/src/client/screencast.ts +++ b/packages/playwright-core/src/client/screencast.ts @@ -24,17 +24,17 @@ export class Screencast implements api.Screencast { private _page: Page; private _started = false; private _savePath: string | undefined; - private _onFrame: ((frame: { data: Buffer }) => Promise) | null = null; + private _onFrame: ((frame: { data: Buffer, viewportWidth: number, viewportHeight: number }) => Promise) | null = null; private _artifact: Artifact | undefined; constructor(page: Page) { this._page = page; - this._page._channel.on('screencastFrame', ({ data }) => { - void this._onFrame?.({ data }); + this._page._channel.on('screencastFrame', ({ data, viewportWidth, viewportHeight }) => { + void this._onFrame?.({ data, viewportWidth, viewportHeight }); }); } - async start(options: { onFrame?: (frame: { data: Buffer }) => Promise|any, path?: string, size?: { width: number, height: number }, quality?: number } = {}): Promise { + async start(options: { onFrame?: (frame: { data: Buffer, viewportWidth: number, viewportHeight: number }) => Promise|any, path?: string, size?: { width: number, height: number }, quality?: number } = {}): Promise { if (this._started) throw new Error('Screencast is already started'); this._started = true; diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 831dfc6f19d24..8c0a3fcb23b84 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -2189,6 +2189,8 @@ scheme.PageRouteEvent = tObject({ }); scheme.PageScreencastFrameEvent = tObject({ data: tBinary, + viewportWidth: tInt, + viewportHeight: tInt, }); scheme.PageWebSocketRouteEvent = tObject({ webSocketRoute: tChannel(['WebSocketRoute']), diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index 867476914df60..56dc88c2cecf1 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -396,7 +396,7 @@ export class PageDispatcher extends Dispatcher { - this._dispatchEvent('screencastFrame', { data: frame.buffer }); + this._dispatchEvent('screencastFrame', { data: frame.buffer, viewportWidth: frame.viewportWidth, viewportHeight: frame.viewportHeight }); }, dispose: () => {}, size: params.size, diff --git a/packages/playwright-core/src/tools/dashboard/dashboardController.ts b/packages/playwright-core/src/tools/dashboard/dashboardController.ts index d67269d184e0a..79e0c503c5474 100644 --- a/packages/playwright-core/src/tools/dashboard/dashboardController.ts +++ b/packages/playwright-core/src/tools/dashboard/dashboardController.ts @@ -448,11 +448,10 @@ class AttachedPage { private async _startScreencast(page: api.Page) { await page.screencast.start({ - onFrame: ({ data }: { data: Buffer }) => { + onFrame: ({ data, viewportWidth, viewportHeight }) => { if (this._disposed) return; - const vp = page.viewportSize(); - this._owner.emitFrame(data.toString('base64'), vp?.width ?? 0, vp?.height ?? 0); + this._owner.emitFrame(data.toString('base64'), viewportWidth, viewportHeight); }, size: { width: 1280, height: 800 }, ...(this._recordingPath ? { path: this._recordingPath } : {}), diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 4a33756068ddb..db9b9739919a0 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -16557,7 +16557,9 @@ export interface Screencast { * ```js * // Capture frames * await page.screencast.start({ - * onFrame: ({ data }) => console.log(`frame size: ${data.length}`), + * onFrame: ({ data, viewportWidth, viewportHeight }) => { + * console.log(`frame size: ${data.length} (${viewportWidth}x${viewportHeight})`); + * }, * size: { width: 800, height: 600 }, * }); * // ... perform actions ... @@ -16567,7 +16569,7 @@ export interface Screencast { * @param options */ start(options?: { - onFrame?: (frame: { data: Buffer }) => Promise|any; + onFrame?: (frame: { data: Buffer, viewportWidth: number, viewportHeight: number }) => Promise|any; path?: string; size?: { width: number; diff --git a/packages/protocol/spec/page.yml b/packages/protocol/spec/page.yml index 4d9b1793b94ec..a5e6e16488e81 100644 --- a/packages/protocol/spec/page.yml +++ b/packages/protocol/spec/page.yml @@ -652,6 +652,8 @@ Page: screencastFrame: parameters: data: binary + viewportWidth: int + viewportHeight: int webSocketRoute: parameters: diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index edb828825bf8a..b1aede0f72880 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -3908,6 +3908,8 @@ export type PageRouteEvent = { }; export type PageScreencastFrameEvent = { data: Binary, + viewportWidth: number, + viewportHeight: number, }; export type PageWebSocketRouteEvent = { webSocketRoute: WebSocketRouteChannel, diff --git a/tests/library/screencast.spec.ts b/tests/library/screencast.spec.ts index 24e345ca0740d..867766fb2d163 100644 --- a/tests/library/screencast.spec.ts +++ b/tests/library/screencast.spec.ts @@ -52,6 +52,29 @@ test('screencast.start delivers frames via onFrame callback', async ({ browser, await context.close(); }); +test('onFrame receives viewport size', 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: { viewportWidth: number, viewportHeight: number }[] = []; + await page.screencast.start({ + onFrame: ({ viewportWidth, viewportHeight }) => frames.push({ viewportWidth, viewportHeight }), + size: { width: 500, height: 400 }, + }); + await page.goto(server.EMPTY_PAGE); + await ensureSomeFrames(page); + await page.screencast.stop(); + + expect(frames.length).toBeGreaterThan(0); + for (const frame of frames) { + expect(frame.viewportWidth).toBe(1000); + expect(frame.viewportHeight).toBe(400); + } + + await context.close(); +}); + test('start throws if screencast is already started', async ({ browser }) => { const context = await browser.newContext({ viewport: { width: 500, height: 400 } }); const page = await context.newPage(); diff --git a/utils/generate_types/overrides.d.ts b/utils/generate_types/overrides.d.ts index dba016b023342..f7f10b66a3b3b 100644 --- a/utils/generate_types/overrides.d.ts +++ b/utils/generate_types/overrides.d.ts @@ -245,7 +245,7 @@ export interface WebSocketRoute { export interface Screencast { start(options?: { - onFrame?: (frame: { data: Buffer }) => Promise|any; + onFrame?: (frame: { data: Buffer, viewportWidth: number, viewportHeight: number }) => Promise|any; path?: string; size?: { width: number;