首页
/ Playwright 视频录制实战:video 模式、动作标注与 WebM 编码底层实现

Playwright 视频录制实战:video 模式、动作标注与 WebM 编码底层实现

2026-09-06 17:24:13作者:史锋燃Gardner

本篇基于 Playwright 官方文档 docs/src/videos.md 展开,讲解 Playwright Test 与库 API 两套视频录制机制:video 配置项的全部模式、recordVideo 参数、动作/测试信息标注,并结合 videoRecorder.ts 源码剖析 Playwright 如何把浏览器帧流编码成 25fps 的 WebM 文件。读完你可以为失败用例精准留存视频证据,理解「viewport 缩放至 800x800」「关闭 context 才落盘」等行为背后的实现原理。

一、用 video 配置项为测试录制视频

Playwright Test 通过 Playwright 配置中的 video 选项控制视频录制,默认关闭'off')。文档列出的核心模式如下:

  • 'off' —— 不录制视频。
  • 'on' —— 为每个测试录制视频。
  • 'retain-on-failure' —— 为每次运行录制视频,但删除所有成功运行产生的视频。
  • 'on-first-retry' —— 只在测试第一次重试时录制视频。
import { defineConfig } from '@playwright/test';
export default defineConfig({
  use: {
    video: 'on-first-retry',
  },
});

视频文件会出现在测试输出目录(通常是 test-results)中。更高级的粒度控制(如按浏览器/项目覆盖)参考 TestOptions.video

源码补充:完整的模式集合与「录制/保留」分离逻辑。 从源码结构看,当前版本的 VideoMode 已扩展到 7 种,完整定义见 test.d.ts

video: VideoMode | /** deprecated */ 'retry-with-video' | {
  mode: VideoMode,
  size?: ViewportSize,
  show?: {
    actions?: { duration?: number, position?: 'top-left' | 'top' | 'top-right' | 'bottom-left' | 'bottom' | 'bottom-right', fontSize?: number, cursor?: 'none' | 'pointer' },
    test?: { level?: 'file' | 'title' | 'step', position?: /* 同上 */ , fontSize?: number }
  }
};

其中 VideoMode 包括 'off''on''on-first-retry''on-all-retries''retain-on-failure''retain-on-first-failure''retain-on-failure-and-retries'(旧写法 'retry-with-video' 已标记 deprecated,会被规范化为 'on-first-retry')。每种模式的行为由两个函数决定,见 index.ts

function shouldCaptureVideo(videoMode: VideoMode, testInfo: TestInfo) {
  return videoMode === 'on'
    || videoMode === 'retain-on-failure'
    || videoMode === 'retain-on-failure-and-retries'
    || (videoMode === 'on-first-retry' && testInfo.retry === 1)
    || (videoMode === 'on-all-retries' && testInfo.retry > 0)
    || (videoMode === 'retain-on-first-failure' && testInfo.retry === 0);
}

function shouldPreserveVideo(videoMode: VideoMode, testInfo: TestInfo) {
  const testFailed = testInfo.status !== testInfo.expectedStatus;
  switch (videoMode) {
    case 'on':
    case 'on-first-retry':
    case 'on-all-retries':
      return true;
    case 'retain-on-failure':
    case 'retain-on-first-failure':
      return testFailed;
    case 'retain-on-failure-and-retries':
      return testFailed || testInfo.retry > 0;
    default:
      return false;
  }
}

可以看到设计把「是否录制」(shouldCaptureVideo)和「是否保留」(shouldPreserveVideo)拆开了:retain-* 系列模式每次都录制,但在测试结束时按结果决定是否删除视频文件——这正是文档中 'retain-on-failure'「录制但只保留失败视频」描述的底层实现。

二、库 API 录制:recordVideo 与 context.close 的时序契约

不使用 Playwright Test 时,直接通过浏览器上下文选项录制视频,关键参数是 recordVideo: { dir, size }视频在 browser context 关闭时保存——如果你手动创建了 browser context,必须 await context.close(),否则视频丢失。各语言写法:

const context = await browser.newContext({ recordVideo: { dir: 'videos/' } });
// 视频依赖关闭动作落盘,务必 await close
await context.close();
context = browser.newContext(new Browser.NewContextOptions().setRecordVideoDir(Paths.get("videos/")));
// 必须 close,视频才会保存
context.close();
context = await browser.new_context(record_video_dir="videos/")
# 必须 await close,视频才会保存
await context.close()
context = browser.new_context(record_video_dir="videos/")
# 必须 close,视频才会保存
context.close()
var context = await browser.NewContextAsync(new()
{
    RecordVideoDir = "videos/"
});
// 必须 close,视频才会保存
await context.CloseAsync();

保存的视频文件出现在指定目录下,文件名由框架生成的唯一 ID 命名。多页面场景下,可以通过 page.video() 获取该页面对应的视频文件:

const path = await page.video().path();
path = page.video().path();
path = await page.video.path()   # async
var path = await page.Video.PathAsync();

注意:视频只有在页面或 browser context 关闭后才可用path() 在运行中调用会返回 null

为什么必须关闭 context 才有视频

从源码看,视频录制是「边录边写」的流式过程。videoRecorder.ts 中的 VideoRecorderstart() 时即通过 createGuid() + '.webm' 生成输出文件路径,并把每帧写入 ffmpeg 子进程的 stdin;而 stop() 才执行真正的收尾(关闭 stdin、等待 ffmpeg 完成 muxing、artifact.reportFinished() 通知产物就绪)。自动录制的入口是 startAutomaticVideoRecording(page)L87-L97):

export function startAutomaticVideoRecording(page: Page) {
  const recordVideo = page.browserContext._options.recordVideo;
  if (!recordVideo)
    return;
  const recorder = new VideoRecorder(page.screencast);
  if (page.browserContext._options.recordVideo?.showActions)
    page.screencast.showActions(page.browserContext._options.recordVideo?.showActions);
  const dir = recordVideo.dir ?? page.browserContext._browser.options.artifactsDir;
  const artifact = recorder.start({ size: recordVideo.size, fileName: path.join(dir, page.guid + '.webm') });
  page.video = artifact;
}

三个关键事实由此得到印证:

  1. recordVideo.dir 未指定时回退到浏览器实例的 artifacts 目录(测试场景即 test-results 下的工件目录);
  2. 每个页面独立创建 VideoRecorder 实例并挂到 page.video,这解释了 page.video() 的可用性与「按页面」的粒度;
  3. 录制是挂在页面的 screencast 帧流上的,页面/上下文关闭触发 screencast 的 gracefulClose,进而调用 recorder.stop() 完成落盘——这就是文档强调「关闭前视频不可用」的机制根源。

三、视频尺寸:800x800 缩放规则与 pad/crop 实现

可以显式指定视频尺寸;默认尺寸是把 viewport 缩放以适配 800x800,viewport 画面被放在输出视频的左上角、必要时等比缩小。若想让视频尺寸与预期一致,可能需要把 viewport 尺寸设置成匹配的值(从类型定义看,viewport 未显式配置时视频尺寸默认为 800x450)。

import { defineConfig } from '@playwright/test';
export default defineConfig({
  use: {
    video: {
      mode: 'on-first-retry',
      size: { width: 640, height: 480 },
    },
  },
});
const context = await browser.newContext({
  recordVideo: {
    dir: 'videos/',
    size: { width: 640, height: 480 },
  }
});

Java 示例:

BrowserContext context = browser.newContext(new Browser.NewContextOptions()
  .setRecordVideoDir(Paths.get("videos/"))
  .setRecordVideoSize(640, 480));

Python 示例:

context = await browser.new_context(
    record_video_dir="videos/",
    record_video_size={"width": 640, "height": 480}
)

.NET 示例:

var context = await browser.NewContextAsync(new()
{
    RecordVideoDir = "videos/",
    RecordVideoSize = new RecordVideoSize() { Width = 640, Height = 480 }
});
await context.CloseAsync();

左上角放置与缩放是怎么做的

这一行为直接对应 videoRecorder.ts 中 ffmpeg 的 -vf 滤镜参数:

const w = this._size.width;
const h = this._size.height;
const videoFilterArgs = page.getFFmpegVideoFilterArgs?.({ width: w, height: h })
  ?? `pad=${w}:${h}:0:0:gray,crop=${w}:${h}:0:0`;

即对每一帧先 pad 到目标尺寸(超出部分以灰色填充,起点 0,0 保证原画面锚定在左上角),再 crop 回目标尺寸裁掉多余边——与文档「viewport 置于左上角、必要时缩小」的描述一致。另外在 start() 中有一行注释说明了尺寸优先级:“For video files only, prioritize encoding into the given size, regardless of the actual pixel data.” 即显式 size 会强制输出分辨率,与截图类产物不同。

四、视频标注:actions 高亮与测试信息水印

文档中的完整标注配置示例:

import { defineConfig } from '@playwright/test';
export default defineConfig({
  use: {
    video: {
      mode: 'on-first-retry',
      size: { width: 640, height: 480 },
      show: {
        actions: {
          duration: 500,
          position: 'top-right',
          fontSize: 14,
        },
        test: {
          level: 'step',
          position: 'top-left',
          fontSize: 12,
        }
      },
    },
  },
});

参数说明(结合 test.d.ts 的类型定义):

参数 作用 取值/默认
show.actions 每次操作在视频中高亮:元素轮廓 + 操作标题字幕 开启即生效
show.actions.duration 每个标注持续显示的时长 默认 500ms
show.actions.position 标注位置 top-left / top / top-right / bottom-left / bottom / bottom-right
show.actions.fontSize 标注字号 数值(px)
show.actions.cursor 是否显示光标 'none' / 'pointer'
show.test.level 测试信息水印的详细级别 'file' / 'title' / 'step'step 显示当前步骤)
show.test.position / fontSize 水印位置与字号 同 actions

实现路径上,showActions 选项在 startAutomaticVideoRecording 中传给 page.screencast.showActions(...)L92-L93),也就是说标注是渲染在 screencast 帧采集阶段的,而非后期叠加——所以录制出来的画面里天然包含高亮轮廓与字幕。

五、底层实现:从浏览器帧到 WebM 文件

FfmpegVideoRecordervideoRecorder.ts)完整描述了编码管线,源码中的注释是理解参数设计的最佳材料:

  1. 帧输入:screencast 捕获的每一帧是一张 MJPEG 图。Playwright 不把裸图丢给 ffmpeg,而是用 ebml.ts 里的 writeHeader / writeClusterHeader 把每帧包进一个带显式时间戳的最小 Matroska 流,从 stdin(-f matroska -i pipe:0)喂给 ffmpeg,由它负责帧时序;
  2. 编码参数(固定 25fps,VP8 编码):
-loglevel error -f matroska -fpsprobesize 0 -probesize 32 -analyzeduration 0 -i pipe:0
-y -an -r 25 -c:v vp8 -qmin 0 -qmax 50 -crf 8
-deadline realtime -speed 8 -b:v 1M -threads 1
-vf pad=W:H:0:0:gray,crop=W:H:0:0

各参数意图(引自源码注释 L123-L163):-crf 8 恒定质量模式;-deadline realtime -speed 8 限制 CPU 占用以跟上实时帧率;-b:v 1M 提高 VP8 默认码率;-threads 1 单线程避免 CPU 超配时的卡顿;-r 25 恒定输出帧率,帧重复交给 ffmpeg 根据时间戳处理(const fps = 25 定义于 L35); 3. 优雅收尾_stop()L220-L239):

  • 若整个会话一帧都没有收到,先写入一帧纯白图(createWhiteImage,用 jpegjs 编码,L242-L245)——因为「ffmpeg 只有在收到非空输入时才会创建文件」,这保证零活动页面也会产出一个合法视频;
  • 随后在最后一帧之后追加至少 1000ms 时长(Math.max(monotonicTime() - this._lastWriteNodeTime, 1000)),让视频结尾有可感知的停顿而不是戛然而止。

此外,ffmpeg 可执行文件本身由 Playwright 浏览器安装器随浏览器一起分发——start() 第一行就通过 registry.findExecutable('ffmpeg') 定位(L50),所以无需系统预装 ffmpeg,这也是 video 配置开箱即用的前提。

六、实用建议与验证路径

  • CI 场景推荐 'retain-on-failure':每次都录,只保留失败视频,兼顾诊断价值与磁盘占用;源码中该模式对应 shouldCaptureVideo 恒真 + shouldPreserveVideo 仅在 testFailed 时保留(index.ts);
  • 'on-first-retry'retries: 0 互斥:没有重试就不会触发录制,配置 video: 'on-first-retry' 时记得配合 retries 使用;
  • 手动 context 一定要 close:库 API 下忘记 await context.close() 会导致视频未落盘(见 videoRecorder.tsstop() 收尾流程);
  • page.video().path() 只在关闭后可用:测试断言中若需附件化视频,应在 context 关闭后处理;
  • 行为回归验证可以参考仓库自带的端到端测试:tests/library/video.spec.tstests/library/screencast.spec.ts 与标注相关的 tests/library/screencast-overlay.spec.ts
  • 与视频相关的姊妹能力(截图、trace 的产物机制)分别见 docs/src/screenshots.mddocs/src/trace-viewer.md

适用前提:本文结论基于当前仓库版本(含 show.actions/test 标注与 7 种 VideoMode),video 选项属于 TestOptions,可通过 use 全局设置并按 project/浏览器覆盖;库 API 的 recordVideo 需要浏览器安装器分发的 ffmpeg 可执行文件,使用 executablePath 手动指定非 Playwright 安装的浏览器构建时,请自行确认 ffmpeg 可用。

登录后查看全文
热门项目推荐
相关项目推荐