From acfc26a9885f4f229d196680a928afb11a9bf1b5 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 28 Jan 2026 12:24:11 -0800 Subject: [PATCH] chore(cli): hint to install ffmpeg --- packages/playwright-core/src/client/video.ts | 7 ++- .../playwright-core/src/protocol/validator.ts | 4 +- .../src/server/dispatchers/pageDispatcher.ts | 6 +-- .../src/server/registry/index.ts | 46 +++++++++++++------ .../playwright-core/src/server/screencast.ts | 12 ++++- .../playwright/src/mcp/browser/tools/video.ts | 2 +- .../playwright/src/mcp/terminal/program.ts | 2 +- packages/protocol/src/channels.d.ts | 4 +- packages/protocol/src/protocol.yml | 2 + tests/library/video.spec.ts | 5 ++ 10 files changed, 66 insertions(+), 24 deletions(-) diff --git a/packages/playwright-core/src/client/video.ts b/packages/playwright-core/src/client/video.ts index ad3f767c4af59..6a022647c65d2 100644 --- a/packages/playwright-core/src/client/video.ts +++ b/packages/playwright-core/src/client/video.ts @@ -26,6 +26,7 @@ export class Video implements api.Video { private _artifactReadyPromise: ManualPromise; private _isRemote = false; private _page: Page; + private _path: string | undefined; constructor(page: Page, connection: Connection) { this._page = page; @@ -39,7 +40,8 @@ export class Video implements api.Video { } async start(options: { size?: { width: number, height: number } } = {}): Promise { - await this._page._channel.videoStart(options); + const result = await this._page._channel.videoStart(options); + this._path = result.path; this._artifactReadyPromise = new ManualPromise(); this._artifact = this._page._closedOrCrashedScope.safeRace(this._artifactReadyPromise); } @@ -55,6 +57,9 @@ export class Video implements api.Video { async path(): Promise { 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'); diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index a0f2bee78264d..3ce0a40bbf339 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -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({ diff --git a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts index 2979fb061b3b7..6eef750677f20 100644 --- a/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/pageDispatcher.ts @@ -337,12 +337,12 @@ export class PageDispatcher extends Dispatcher { - await this._page.screencast.startExplicitVideoRecording(params); + async videoStart(params: channels.PageVideoStartParams, progress: Progress): Promise { + return await this._page.screencast.startExplicitVideoRecording(params); } async videoStop(params: channels.PageVideoStopParams, progress: Progress): Promise { - await this._page.screencast.stopVideoRecording(); + await this._page.screencast.stopExplicitVideoRecording(); } async startJSCoverage(params: channels.PageStartJSCoverageParams, progress: Progress): Promise { diff --git a/packages/playwright-core/src/server/registry/index.ts b/packages/playwright-core/src/server/registry/index.ts index 0024a9eede2ca..7a62ea36d4db4 100644 --- a/packages/playwright-core/src/server/registry/index.ts +++ b/packages/playwright-core/src/server/registry/index.ts @@ -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; diff --git a/packages/playwright-core/src/server/screencast.ts b/packages/playwright-core/src/server/screencast.ts index b8c1a803bfaed..a51996c14de38 100644 --- a/packages/playwright-core/src/server/screencast.ts +++ b/packages/playwright-core/src/server/screencast.ts @@ -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 => { @@ -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 { diff --git a/packages/playwright/src/mcp/browser/tools/video.ts b/packages/playwright/src/mcp/browser/tools/video.ts index defa36cdef4b1..6474accdf65e1 100644 --- a/packages/playwright/src/mcp/browser/tools/video.ts +++ b/packages/playwright/src/mcp/browser/tools/video.ts @@ -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}`); }, }); diff --git a/packages/playwright/src/mcp/terminal/program.ts b/packages/playwright/src/mcp/terminal/program.ts index 71c17c7228aad..db033f67f09f2 100644 --- a/packages/playwright/src/mcp/terminal/program.ts +++ b/packages/playwright/src/mcp/terminal/program.ts @@ -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; } diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 406072af18226..395a2527331ef 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -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; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index b202cd7d2a48c..67428bf8d4873 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -2023,6 +2023,8 @@ Page: properties: width: int height: int + returns: + path: string videoStop: title: Stop video recording diff --git a/tests/library/video.spec.ts b/tests/library/video.spec.ts index 4c87d78c5e7a5..a260ba6ae4838 100644 --- a/tests/library/video.spec.ts +++ b/tests/library/video.spec.ts @@ -843,6 +843,8 @@ 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'); @@ -850,6 +852,9 @@ it.describe('screencast', () => { 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');