{"record":{"id":"c8ebd82b2c4b609e","repo":"microsoft/playwright","slug":"video-recording-has-already-been-started","errorCode":null,"errorMessage":"Video recording has already been started.","messagePattern":"Video recording has already been started\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/playwright-core/src/tools/backend/context.ts","lineNumber":219,"sourceCode":"    if (!tab)\n      throw new Error(`Tab ${index} not found`);\n    const url = tab.page.url();\n    await tab.page.close();\n    return url;\n  }\n\n  async workspaceFile(fileName: string, perCallWorkspaceDir: string | undefined): Promise<string> {\n    return await workspaceFile(this.options, fileName, perCallWorkspaceDir);\n  }\n\n  async outputFile(template: FilenameTemplate, options: { origin: 'code' | 'llm' }): Promise<string> {\n    const baseName = template.suggestedFilename || `${template.prefix}-${(template.date ?? new Date()).toISOString().replace(/[:.]/g, '-')}${template.ext ? '.' + template.ext : ''}`;\n    return await outputFile(this.options, baseName, options);\n  }\n\n  async startVideoRecording(fileName: string, params: VideoParams) {\n    if (this._video)\n      throw new Error('Video recording has already been started.');\n    this._video = { params, fileName, fileNames: [] };\n    const browserContext = await this.ensureBrowserContext();\n    for (const page of browserContext.pages())\n      await this._startPageVideo(page);\n  }\n\n  async stopVideoRecording(): Promise<string[]> {\n    if (!this._video)\n      return [];\n    const video = this._video;\n    for (const page of this._rawBrowserContext.pages())\n      await page.screencast.stop();\n    this._video = undefined;\n    return [...video.fileNames];\n  }\n\n  private async _startPageVideo(page: playwrightTypes.Page) {\n    if (!this._video)","sourceCodeStart":201,"sourceCodeEnd":237,"githubUrl":"https://github.com/microsoft/playwright/blob/c8fc3bf8d31542d59b4d4d9eaab1df93ff541dc6/packages/playwright-core/src/tools/backend/context.ts#L201-L237","documentation":"Thrown by Context.startVideoRecording when called while a recording is already in flight. The context holds a single _video slot that is set on start and only cleared by stopVideoRecording, so a second start is rejected as a state-machine violation.","triggerScenarios":"Calling startVideoRecording twice without an intervening stopVideoRecording; or invoking the browser_video_record_start MCP tool a second time while the first recording is still active.","commonSituations":"MCP/LLM agent retrying a video-capture step without first issuing stop; an orchestration script that re-enters a 'record' code path after an error path that never stopped the previous recording.","solutions":["Call context.stopVideoRecording() (or the browser_video_record_stop tool) before starting a new recording.","Guard the call: check whether a recording is already active via the tool/context API before invoking start.","If reusing one context across tasks, tear down video in a finally block so a failed run cannot leave _video set."],"exampleFix":"// before\nawait context.startVideoRecording(file, params);\nawait context.startVideoRecording(file, params); // throws\n\n// after\nif (await context.isVideoRecording?.() !== true) {\n  await context.startVideoRecording(file, params);\n}","handlingStrategy":"validation","validationCode":"// Before calling startVideoRecording, ensure no recording is active.\n// If you control the Context, track recording state externally:\nlet videoActive = false;\nasync function safeStart(ctx, file, params) {\n  if (videoActive) await ctx.stopVideoRecording();\n  await ctx.startVideoRecording(file, params);\n  videoActive = true;\n}\nasync function safeStop(ctx) {\n  if (!videoActive) return [];\n  videoActive = false;\n  return ctx.stopVideoRecording(); // safe to call when inactive (returns [])\n}","typeGuard":"// No public guard; mirror the internal state with a wrapper.\nconst videoState = new WeakMap<Context, boolean>();\nfunction isVideoRecording(ctx: Context): boolean {\n  return videoState.get(ctx) ?? false;\n}","tryCatchPattern":"// stopVideoRecording is safe to call when inactive (returns []). Prefer calling it\n// defensively rather than try/catch on start.\nawait context.stopVideoRecording(); // no-op if inactive\nawait context.startVideoRecording(file, params);","preventionTips":["Always pair startVideoRecording with a guaranteed stopVideoRecording in a finally block.","Treat the recorder as a single-flight resource: one active recording per context.","In MCP/agent flows, route video start/stop through a tool that tracks the active state."],"tags":["video","state-machine","mcp-tool","playwright-mcp"],"backgroundTag":null,"analyzedSha":"c8fc3bf8d31542d59b4d4d9eaab1df93ff541dc6","analyzedAt":"2026-08-12T07:26:36.950Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}