Skip to content

Commit fe81118

Browse files
authored
feat(api): add Screencast.stop() and enforce single listener (#39775)
1 parent 31d901f commit fe81118

6 files changed

Lines changed: 50 additions & 49 deletions

File tree

docs/src/api/class-screencast.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ Starts capturing screencast frames.
1313
**Usage**
1414

1515
```js
16-
const disposable = await page.screencast.start(({ data }) => {
16+
await page.screencast.start(({ data }) => {
1717
console.log(`frame size: ${data.length}`);
1818
}, { preferredSize: { width: 800, height: 600 } });
1919
// ... perform actions ...
20-
await disposable.dispose();
20+
await page.screencast.stop();
2121
```
2222

2323
### param: Screencast.start.onFrame
@@ -39,3 +39,8 @@ Specifies the preferred maximum dimensions of screencast frames. The actual fram
3939
If a screencast is already active (e.g. started by tracing or video recording), the existing configuration takes precedence and the frame size may exceed these bounds or this option may be ignored.
4040

4141
Defaults to 800×800.
42+
43+
## async method: Screencast.stop
44+
* since: v1.59
45+
46+
Stops the screencast started with [`method: Screencast.start`].

packages/playwright-client/types/types.d.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16163,11 +16163,11 @@ export interface Screencast {
1616316163
* **Usage**
1616416164
*
1616516165
* ```js
16166-
* const disposable = await page.screencast.start(({ data }) => {
16166+
* await page.screencast.start(({ data }) => {
1616716167
* console.log(`frame size: ${data.length}`);
1616816168
* }, { preferredSize: { width: 800, height: 600 } });
1616916169
* // ... perform actions ...
16170-
* await disposable.dispose();
16170+
* await page.screencast.stop();
1617116171
* ```
1617216172
*
1617316173
* @param onFrame Callback that receives JPEG-encoded frame data.
@@ -16179,7 +16179,11 @@ export interface Screencast {
1617916179
height: number;
1618016180
};
1618116181
}): Promise<Disposable>;
16182-
16182+
/**
16183+
* Stops the screencast started with
16184+
* [screencast.start(onFrame[, options])](https://playwright.dev/docs/api/class-screencast#screencast-start).
16185+
*/
16186+
stop(): Promise<void>;
1618316187
}
1618416188

1618516189
type DeviceDescriptor = {

packages/playwright-core/src/client/screencast.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,25 @@ import type { Page } from './page';
2121

2222
export class Screencast implements api.Screencast {
2323
private readonly _page: Page;
24-
private _listeners = new Set<(frame: { data: Buffer }) => any>();
24+
private _onFrame: ((frame: { data: Buffer }) => Promise<any>) | null = null;
2525

2626
constructor(page: Page) {
2727
this._page = page;
2828
this._page._channel.on('screencastFrame', ({ data }) => {
29-
for (const listener of this._listeners)
30-
void listener({ data });
29+
void this._onFrame?.({ data });
3130
});
3231
}
3332

3433
async start(onFrame: (frame: { data: Buffer }) => Promise<any>|any, options: { preferredSize?: { width: number, height: number } } = {}): Promise<DisposableStub> {
35-
this._listeners.add(onFrame);
36-
if (this._listeners.size === 1)
37-
await this._page._channel.startScreencast(options);
38-
return new DisposableStub(async () => {
39-
this._listeners.delete(onFrame);
40-
if (!this._listeners.size)
41-
await this._page._channel.stopScreencast();
42-
});
34+
if (this._onFrame)
35+
throw new Error('Screencast is already started');
36+
this._onFrame = onFrame;
37+
await this._page._channel.startScreencast(options);
38+
return new DisposableStub(() => this.stop());
39+
}
40+
41+
async stop(): Promise<void> {
42+
this._onFrame = null;
43+
await this._page._channel.stopScreencast();
4344
}
4445
}

packages/playwright-core/src/tools/dashboard/dashboardController.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ export class DashboardConnection implements Transport, DashboardChannel {
3232
private _lastFrameData: string | null = null;
3333
private _lastViewportSize: { width: number, height: number } | null = null;
3434
private _pageListeners: { dispose: () => Promise<void> }[] = [];
35-
private _screencastSession: { dispose: () => Promise<void> } | null = null;
3635
private _contextListeners: { dispose: () => Promise<void> }[] = [];
3736
private _eventListeners = new Map<string, Set<Function>>();
3837

@@ -180,8 +179,7 @@ export class DashboardConnection implements Transport, DashboardChannel {
180179
if (this.selectedPage) {
181180
this._pageListeners.forEach(d => d.dispose());
182181
this._pageListeners = [];
183-
await this._screencastSession?.dispose();
184-
this._screencastSession = null;
182+
await this.selectedPage.screencast.stop();
185183
}
186184

187185
this.selectedPage = page;
@@ -204,7 +202,7 @@ export class DashboardConnection implements Transport, DashboardChannel {
204202
);
205203

206204
const preferredSize = { width: 1280, height: 800 };
207-
this._screencastSession = await page.screencast.start(
205+
await page.screencast.start(
208206
({ data }) => this._writeFrame(data, page.viewportSize()?.width ?? 0, page.viewportSize()?.height ?? 0),
209207
{ preferredSize },
210208
);
@@ -215,8 +213,7 @@ export class DashboardConnection implements Transport, DashboardChannel {
215213
return;
216214
this._pageListeners.forEach(d => d.dispose());
217215
this._pageListeners = [];
218-
this._screencastSession?.dispose().catch(() => {});
219-
this._screencastSession = null;
216+
this.selectedPage.screencast.stop().catch(() => {});
220217
this.selectedPage = null;
221218
this._lastFrameData = null;
222219
this._lastViewportSize = null;

packages/playwright-core/types/types.d.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16163,11 +16163,11 @@ export interface Screencast {
1616316163
* **Usage**
1616416164
*
1616516165
* ```js
16166-
* const disposable = await page.screencast.start(({ data }) => {
16166+
* await page.screencast.start(({ data }) => {
1616716167
* console.log(`frame size: ${data.length}`);
1616816168
* }, { preferredSize: { width: 800, height: 600 } });
1616916169
* // ... perform actions ...
16170-
* await disposable.dispose();
16170+
* await page.screencast.stop();
1617116171
* ```
1617216172
*
1617316173
* @param onFrame Callback that receives JPEG-encoded frame data.
@@ -16179,7 +16179,11 @@ export interface Screencast {
1617916179
height: number;
1618016180
};
1618116181
}): Promise<Disposable>;
16182-
16182+
/**
16183+
* Stops the screencast started with
16184+
* [screencast.start(onFrame[, options])](https://playwright.dev/docs/api/class-screencast#screencast-start).
16185+
*/
16186+
stop(): Promise<void>;
1618316187
}
1618416188

1618516189
type DeviceDescriptor = {

tests/library/screencast.spec.ts

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ test('screencast.start delivers frames via onFrame callback', async ({ browser,
2727

2828
const frames: Buffer[] = [];
2929
const preferredSize = { width: 500, height: 400 };
30-
const disposable = await page.screencast.start(({ data }) => frames.push(data), { preferredSize });
30+
await page.screencast.start(({ data }) => frames.push(data), { preferredSize });
3131
await page.goto(server.EMPTY_PAGE);
3232
await page.evaluate(() => document.body.style.backgroundColor = 'red');
3333
await rafraf(page, 100);
34-
await disposable.dispose();
34+
await page.screencast.stop();
3535

3636
expect(frames.length).toBeGreaterThan(0);
3737
for (const frame of frames) {
@@ -47,24 +47,14 @@ test('screencast.start delivers frames via onFrame callback', async ({ browser,
4747
await context.close();
4848
});
4949

50-
test('supports multiple onFrame listeners', async ({ browser, server, trace }) => {
51-
test.skip(trace === 'on', 'trace=on has different screencast image configuration');
52-
50+
test('start throws if screencast is already started', async ({ browser }) => {
5351
const context = await browser.newContext({ viewport: { width: 500, height: 400 } });
5452
const page = await context.newPage();
5553

56-
const frames1: Buffer[] = [];
57-
const frames2: Buffer[] = [];
58-
const disposable1 = await page.screencast.start(({ data }) => frames1.push(data), { preferredSize: { width: 500, height: 400 } });
59-
const disposable2 = await page.screencast.start(({ data }) => frames2.push(data));
60-
61-
await page.goto(server.EMPTY_PAGE);
62-
await rafraf(page, 100);
63-
await disposable1.dispose();
64-
await disposable2.dispose();
54+
await page.screencast.start(() => {});
55+
await expect(page.screencast.start(() => {})).rejects.toThrow('Screencast is already started');
6556

66-
expect(frames1.length).toBeGreaterThan(0);
67-
expect(frames2.length).toBeGreaterThan(0);
57+
await page.screencast.stop();
6858
await context.close();
6959
});
7060

@@ -74,11 +64,11 @@ test('start allows restart with different options after stop', async ({ browser,
7464
const context = await browser.newContext({ viewport: { width: 500, height: 400 } });
7565
const page = await context.newPage();
7666

77-
const disposable1 = await page.screencast.start(() => {}, { preferredSize: { width: 500, height: 400 } });
78-
await disposable1.dispose();
67+
await page.screencast.start(() => {}, { preferredSize: { width: 500, height: 400 } });
68+
await page.screencast.stop();
7969
// Different options should succeed once the previous screencast is stopped.
80-
const disposable2 = await page.screencast.start(() => {}, { preferredSize: { width: 320, height: 240 } });
81-
await disposable2.dispose();
70+
await page.screencast.start(() => {}, { preferredSize: { width: 320, height: 240 } });
71+
await page.screencast.stop();
8272
await context.close();
8373
});
8474

@@ -92,10 +82,10 @@ test('start reuses existing screencast when video recording is running', async (
9282
await page.video().start({ size: videoSize });
9383

9484
const frames: Buffer[] = [];
95-
const disposable = await page.screencast.start(({ data }) => frames.push(data), { preferredSize: { width: 320, height: 240 } });
85+
await page.screencast.start(({ data }) => frames.push(data), { preferredSize: { width: 320, height: 240 } });
9686
await page.goto(server.EMPTY_PAGE);
9787
await rafraf(page, 100);
98-
await disposable.dispose();
88+
await page.screencast.stop();
9989

10090
expect(frames.length).toBeGreaterThan(0);
10191
for (const frame of frames) {
@@ -118,11 +108,11 @@ test('start returns a disposable that stops screencast', async ({ browser, serve
118108
const page = await context.newPage();
119109

120110
const frames: Buffer[] = [];
121-
const disposable = await page.screencast.start(({ data }) => frames.push(data), { preferredSize: { width: 500, height: 400 } });
111+
await page.screencast.start(({ data }) => frames.push(data), { preferredSize: { width: 500, height: 400 } });
122112
await page.goto(server.EMPTY_PAGE);
123113
await page.evaluate(() => document.body.style.backgroundColor = 'red');
124114
await rafraf(page, 100);
125-
await disposable.dispose();
115+
await page.screencast.stop();
126116

127117
const frameCountAfterDispose = frames.length;
128118
expect(frameCountAfterDispose).toBeGreaterThan(0);

0 commit comments

Comments
 (0)