Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/playwright-core/src/client/video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export class Video implements api.Video {
private _artifactReadyPromise: ManualPromise<Artifact>;
private _isRemote = false;
private _page: Page;
private _path: string | undefined;

constructor(page: Page, connection: Connection) {
this._page = page;
Expand All @@ -39,7 +40,8 @@ export class Video implements api.Video {
}

async start(options: { size?: { width: number, height: number } } = {}): Promise<void> {
await this._page._channel.videoStart(options);
const result = await this._page._channel.videoStart(options);
this._path = result.path;
this._artifactReadyPromise = new ManualPromise<Artifact>();
this._artifact = this._page._closedOrCrashedScope.safeRace(this._artifactReadyPromise);
}
Expand All @@ -55,6 +57,9 @@ export class Video implements api.Video {
async path(): Promise<string> {
if (this._isRemote)
throw new Error(`Path is not available when connecting remotely. Use saveAs() to save a local copy.`);
if (this._path)
return this._path;

const artifact = await this._artifact;
if (!artifact)
throw new Error('Page did not produce any video frames');
Expand Down
4 changes: 3 additions & 1 deletion packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1495,7 +1495,9 @@ scheme.PageVideoStartParams = tObject({
height: tInt,
})),
});
scheme.PageVideoStartResult = tOptional(tObject({}));
scheme.PageVideoStartResult = tObject({
path: tString,
});
scheme.PageVideoStopParams = tOptional(tObject({}));
scheme.PageVideoStopResult = tOptional(tObject({}));
scheme.PageUpdateSubscriptionParams = tObject({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,12 +337,12 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageChannel, Brows
await progress.race(this._page.bringToFront());
}

async videoStart(params: channels.PageVideoStartParams, progress: Progress): Promise<void> {
await this._page.screencast.startExplicitVideoRecording(params);
async videoStart(params: channels.PageVideoStartParams, progress: Progress): Promise<channels.PageVideoStartResult> {
return await this._page.screencast.startExplicitVideoRecording(params);
}

async videoStop(params: channels.PageVideoStopParams, progress: Progress): Promise<channels.PageVideoStopResult> {
await this._page.screencast.stopVideoRecording();
await this._page.screencast.stopExplicitVideoRecording();
}

async startJSCoverage(params: channels.PageStartJSCoverageParams, progress: Progress): Promise<void> {
Expand Down
46 changes: 31 additions & 15 deletions packages/playwright-core/src/server/registry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -589,21 +589,37 @@ export class Registry {
const currentDockerVersion = readDockerVersionSync();
const preferredDockerVersion = currentDockerVersion ? dockerVersion(currentDockerVersion.dockerImageNameTemplate) : null;
const isOutdatedDockerImage = currentDockerVersion && preferredDockerVersion && currentDockerVersion.dockerImageName !== preferredDockerVersion.dockerImageName;
const prettyMessage = isOutdatedDockerImage ? [
`Looks like ${sdkLanguage === 'javascript' ? 'Playwright Test or ' : ''}Playwright was just updated to ${preferredDockerVersion.driverVersion}.`,
`Please update docker image as well.`,
`- current: ${currentDockerVersion.dockerImageName}`,
`- required: ${preferredDockerVersion.dockerImageName}`,
``,
`<3 Playwright Team`,
].join('\n') : [
`Looks like ${sdkLanguage === 'javascript' ? 'Playwright Test or ' : ''}Playwright was just installed or updated.`,
`Please run the following command to download new browser${installByDefault ? 's' : ''}:`,
``,
` ${installCommand}`,
``,
`<3 Playwright Team`,
].join('\n');
const isFfmpeg = name === 'ffmpeg';
let prettyMessage;
if (isOutdatedDockerImage) {
prettyMessage = [
`Looks like Playwright was just updated to ${preferredDockerVersion.driverVersion}.`,
`Please update docker image as well.`,
`- current: ${currentDockerVersion.dockerImageName}`,
`- required: ${preferredDockerVersion.dockerImageName}`,
``,
`<3 Playwright Team`,
].join('\n');
} else if (isFfmpeg) {
prettyMessage = [
`Video rendering requires ffmpeg binary.`,
`Downloading it will not affect any of the system-wide settings.`,
`Please run the following command:`,
``,
` ${buildPlaywrightCLICommand(sdkLanguage, 'install ffmpeg')}`,
``,
`<3 Playwright Team`,
].join('\n');
} else {
prettyMessage = [
`Looks like Playwright was just installed or updated.`,
`Please run the following command to download new browser${installByDefault ? 's' : ''}:`,
``,
` ${installCommand}`,
``,
`<3 Playwright Team`,
].join('\n');
}
throw new Error(`Executable doesn't exist at ${e}\n${wrapInASCIIBox(prettyMessage, 1)}`);
}
return e;
Expand Down
12 changes: 11 additions & 1 deletion packages/playwright-core/src/server/screencast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,16 @@ export class Screencast {

private _launchVideoRecorder(dir: string, size: { width: number, height: number }): types.VideoOptions {
assert(!this._videoId);
// Do this first, it likes to throw.
const ffmpegPath = registry.findExecutable('ffmpeg')!.executablePathOrDie(this._page.browserContext._browser.sdkLanguage());

this._videoId = createGuid();
const outputFile = path.join(dir, this._videoId + '.webm');
const videoOptions = {
...size,
outputFile,
};
const ffmpegPath = registry.findExecutable('ffmpeg')!.executablePathOrDie(this._page.browserContext._browser.sdkLanguage());

this._videoRecorder = new VideoRecorder(ffmpegPath, videoOptions);
this._frameListener = eventsHelper.addEventListener(this._page, Page.Events.ScreencastFrame, frame => this._videoRecorder!.writeFrame(frame.buffer, frame.frameSwapWallTime / 1000));
this._page.waitForInitializedOrError().then(p => {
Expand Down Expand Up @@ -125,6 +128,13 @@ export class Screencast {
const size = validateVideoSize(options.size, this._page.emulatedSize()?.viewport);
const videoOptions = this._launchVideoRecorder(this._page.browserContext._browser.options.artifactsDir, size);
await this.startVideoRecording(videoOptions);
return { path: videoOptions.outputFile };
}

async stopExplicitVideoRecording() {
if (!this._videoId)
throw new Error('Video is not being recorded');
await this.stopVideoRecording();
}

private async _setOptions(options: { width: number, height: number, quality: number } | null): Promise<void> {
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright/src/mcp/browser/tools/video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,13 @@ const stopVideo = defineTabTool({
},

handle: async (tab, params, response) => {
const tmpPath = await tab.page.video().path();
let videoPath: string | undefined;
if (params.filename) {
const suggestedFilename = params.filename ?? dateAsFileName('video', 'webm');
videoPath = await tab.context.outputFile(suggestedFilename, { origin: 'llm', title: 'Saving video' });
}
await tab.page.video().stop({ path: videoPath });
const tmpPath = await tab.page.video().path();
response.addTextResult(`Video recording stopped: ${videoPath ?? tmpPath}`);
},
});
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright/src/mcp/terminal/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ export async function program(options: { version: string }) {
export async function printResponse(response: StructuredResponse) {
const { sections } = response;
if (!sections) {
console.log('### Error\n' + response.text);
console.log(response.text);
return;
}

Expand Down
4 changes: 3 additions & 1 deletion packages/protocol/src/channels.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2602,7 +2602,9 @@ export type PageVideoStartOptions = {
height: number,
},
};
export type PageVideoStartResult = void;
export type PageVideoStartResult = {
path: string,
};
export type PageVideoStopParams = {};
export type PageVideoStopOptions = {};
export type PageVideoStopResult = void;
Expand Down
2 changes: 2 additions & 0 deletions packages/protocol/src/protocol.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2023,6 +2023,8 @@ Page:
properties:
width: int
height: int
returns:
path: string

videoStop:
title: Stop video recording
Expand Down
5 changes: 5 additions & 0 deletions tests/library/video.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -843,13 +843,18 @@ it.describe('screencast', () => {
const page = await context.newPage();

await page.video().start({ size });
const tmpPath1 = await page.video().path();
expect(tmpPath1).toBeDefined();
await page.evaluate(() => document.body.style.backgroundColor = 'red');
await rafraf(page, 100);
const videoPath1 = testInfo.outputPath('video1.webm');
await page.video().stop({ path: videoPath1 });
expectRedFrames(videoPath1, size);

await page.video().start({ size });
const tmpPath2 = await page.video().path();
expect(tmpPath2).toBeDefined();
expect(tmpPath2).not.toEqual(tmpPath1);
await page.evaluate(() => document.body.style.backgroundColor = 'rgb(100,100,100)');
await rafraf(page, 100);
const videoPath2 = testInfo.outputPath('video2.webm');
Expand Down
Loading