openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
119 lines • 5.18 kB
JavaScript
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { zod } from '../third_party/index.js';
import { ToolCategory } from './categories.js';
import { definePageTool } from './ToolDefinition.js';
async function generateTempFilePath() {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'chrome-devtools-mcp-'));
return path.join(dir, `screencast.mp4`);
}
const supportedExtensions = ['.webm', '.mp4'];
export const startScreencast = definePageTool(args => ({
name: 'screencast_start',
description: `Starts recording a screencast (video) of the target page in specified format.`,
annotations: {
category: ToolCategory.DEBUGGING,
readOnlyHint: false,
conditions: ['experimentalScreencast'],
},
schema: {
filePath: zod
.string()
.optional()
.describe(`Output file path (${supportedExtensions.join(',')} are supported). Uses mkdtemp to generate a unique path if not provided.`),
},
blockedByDialog: false,
verifyFilesSchema: {
filePath: true,
},
handler: async (request, response, context) => {
if (context.getScreenRecorder() !== null) {
response.appendResponseLine('Error: a screencast recording is already in progress. Use screencast_stop to stop it before starting a new one.');
return;
}
const requestedFilePath = request.params.filePath;
const filePath = requestedFilePath ?? (await generateTempFilePath());
// Match the extension case-insensitively so e.g. `.WEBM` is recognized as
// WebM. An explicitly requested but unsupported extension is rejected
// rather than being silently rewritten to `.mp4` (which would change both
// the format and the output path from what was requested). A missing
// extension falls back to `.mp4`. The matched extension is normalized to
// lower case.
const requestedExtension = path.extname(filePath);
const matchedExtension = supportedExtensions.find(supportedExtension => supportedExtension === requestedExtension.toLowerCase());
if (!matchedExtension && requestedExtension !== '') {
throw new Error(`Unsupported screencast file extension "${requestedExtension}". ` +
`Supported formats: ${supportedExtensions.join(', ')} (case-insensitive).`);
}
const enforcedExtension = matchedExtension ?? '.mp4';
const format = (matchedExtension?.substring(1) ??
'mp4');
const resolvedPath = await context.ensureExtension(filePath, enforcedExtension);
const page = request.page;
let recorder;
try {
recorder = await page.pptrPage.screencast({
path: resolvedPath,
format: format,
ffmpegPath: args?.experimentalFfmpegPath,
fps: args?.experimentalScreencastFps,
});
}
catch (err) {
// If we generated a temporary directory for this recording, remove it so
// a failed start (e.g. ffmpeg missing) does not leak an empty directory.
if (requestedFilePath === undefined) {
try {
await fs.rm(path.dirname(resolvedPath), {
recursive: true,
force: true,
});
}
catch {
// no-op
}
}
const message = err instanceof Error ? err.message : String(err);
if (message.includes('ENOENT') && message.includes('ffmpeg')) {
throw new Error('ffmpeg is required for screencast recording but was not found. ' +
'Install ffmpeg (https://ffmpeg.org/) and ensure it is available in your PATH.');
}
throw err;
}
context.setScreenRecorder({ recorder, filePath: resolvedPath });
response.appendResponseLine(`Screencast recording started. The recording will be saved to ${resolvedPath}. Use ${stopScreencast.name} to stop recording.`);
},
}));
export const stopScreencast = definePageTool({
name: 'screencast_stop',
description: 'Stops the active screencast recording on the target page.',
annotations: {
category: ToolCategory.DEBUGGING,
readOnlyHint: false,
conditions: ['experimentalScreencast'],
},
schema: {},
blockedByDialog: false,
verifyFilesSchema: {},
handler: async (_request, response, context) => {
const data = context.getScreenRecorder();
if (!data) {
response.appendResponseLine('Error: no active screencast recording to stop.');
return;
}
try {
await data.recorder.stop();
response.appendResponseLine(`The screencast recording has been stopped and saved to ${data.filePath}.`);
}
finally {
context.setScreenRecorder(null);
}
},
});
//# sourceMappingURL=screencast.js.map