首页
/ Playwright Video 类详解:recordVideo 之下的视频录制 API 与 ffmpeg 编码管线

Playwright Video 类详解:recordVideo 之下的视频录制 API 与 ffmpeg 编码管线

2026-09-06 12:02:23作者:薛曦旖Francesca

当使用 recordVideo 选项创建浏览器上下文后,Playwright 会为每个页面自动关联一个 Video 对象,用于获取、保存或删除该页面录制的视频文件。本文以 Playwright 官方 API 文档 class-video.md 为主体,完整讲解 Video 类的 path()saveAs()delete() 三个方法及其在多语言下的调用方式,并结合仓库中 客户端实现服务端录制器 的源码,深入剖析视频从屏幕截帧到 .webm 文件落盘的完整链路。读完本文,你可以掌握如何在测试与自动化脚本中可靠地收集、归档测试视频,并理解 path() 在远程连接下抛错、saveAs() 等待语义背后的实现原因。

一、Video 类概述:什么时候会存在 Video 对象

Video 类自 v1.8 起可用。官方文档的核心描述是:当浏览器上下文以 recordVideo 选项创建时,每个页面都会有一个与之关联的 video 对象。四语言下的标准用法是:

console.log(await page.video().path());
System.out.println(page.video().path());
# async
print(await page.video.path())
# sync
print(page.video.path())
Console.WriteLine(await page.Video.GetPathAsync());

需要注意的是,page.video() 并非总是返回对象。从 page.ts 的客户端实现看,Page 在构造时依据协议 initializer.video 是否下发来决定是否创建 Video 实例(initializer.video 存在时执行 new Video(this._connection, Artifact.from(initializer.video))),而 video() 方法在未关联视频时返回 null。这一点也被测试明确验证:video.spec.ts 中在未配置录制的上下文里断言 expect(page.video()).toBeNull()

换句话说,Video 对象的存在性完全由上下文的 recordVideo 配置驱动。启用录制的标准写法(参见 videos.md)是:

// JS 库模式:通过 newContext 传入 recordVideo
const context = await browser.newContext({ recordVideo: { dir: 'videos/' } });
// 务必 await close,视频才会真正落盘
await context.close();
// Java
context = browser.newContext(new Browser.NewContextOptions().setRecordVideoDir(Paths.get("videos/")));
context.close();
# Python(async / sync 写法一致,仅 await 差异)
context = await browser.new_context(record_video_dir="videos/")
await context.close()
// C#
var context = await browser.NewContextAsync(new() { RecordVideoDir = "videos/" });
await context.CloseAsync();

官方文档特别强调了一条语义:视频在浏览器上下文关闭时才被保存。如果你手动创建了上下文,一定要 await browserContext.close(),否则拿不到完整的视频文件。这条规则直接解释了 Video.path() / saveAs() 的诸多等待行为。

二、async method: Video.path(since: v1.8)

官方定义:

返回该视频将被写入的文件系统路径。视频保证在关闭浏览器上下文时被写入文件系统。远程连接时该方法会抛错。

多语言调用:

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

2.1 参数与返回值

说明
返回 path(字符串),视频将要写入的绝对文件系统路径
写入时机 浏览器上下文关闭时保证写入完成
异常 远程连接(如 connect() 到远端 Playwright Server)时抛出错误;录制尚未启动时抛出 Video recording has not been started.

2.2 源码印证:为什么远程连接会抛错

client/video.tspath() 的实现非常直白:

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._artifact)
    throw new Error('Video recording has not been started.');
  return this._artifact._initializer.absolutePath;
}

三个关键事实可以确认:

  1. _isRemote 在构造函数中取自 connection.isRemote(),即只要客户端是通过 connect() 等方式远程连接,path() 一律抛错,并明确提示"请改用 saveAs() 保存本地副本"。这是因为路径指向的是远端服务器的文件系统,对本地进程没有意义;
  2. path() 返回的是 _artifact._initializer.absolutePath——即录制启动时就已分配好的目标路径。这与 Video 类文档"返回视频将(will be)写入的路径"的措辞一致:文件句柄在上下文关闭时才真正完成写入,但路径在开始时就是确定的;
  3. 未启用录制时(_artifact 为空)调用会抛出 Video recording has not been started.

测试 video.spec.ts 中对 path() 的覆盖相当充分,包括 recordVideo.dir 指定目录、默认 artifactsDir、多页面 / popup 各自持有独立 Video 对象(popup.video())等场景。

三、async method: Video.saveAs(since: v1.11)

官方定义(JS/Python 异步):

将视频保存到用户指定的路径。即使视频仍在录制中,或页面已关闭,调用该方法都是安全的。该方法会等待页面关闭且视频完整保存后才返回。

await page.video().saveAs('test-results/my-video.webm');

参数只有一个:

  • path(string):视频应保存到的目标路径。

3.1 各语言的语义差异(重要)

原文档对 saveAs 按语言分别给出说明,这一点容易被忽略:

Java(同步 API):必须在 page.close()(或 browserContext.close()之后调用,否则抛出错误。调用后会等待视频完整保存。

page.close();
page.video().saveAs(Paths.get("my-video.webm"));

Python 同步 API:同样必须在 page.close() / context.close() 之后调用,否则抛错;Python 异步 API 则与 JS 一致,录制进行中或页面关闭后调用都是安全的,方法会等待页面关闭且视频完整保存。

JS / C# / Python 异步 API:录制进行中或页面关闭后均可安全调用,Promise 会在页面关闭且视频完全写盘后 resolve。

这种差异的根源在于同步 API 无法在调用线程上等待异步的"上下文关闭"事件,因此把"先关闭"作为前置约束;而异步 API 可以把"等待关闭 + 等待落盘"折叠进 Promise 中。

3.2 实现链路:saveAs 委托给 Artifact

client/video.ts 看,saveAs 本身极薄:

async saveAs(path: string): Promise<void> {
  if (!this._artifact)
    throw new Error('Video recording has not been started.');
  return await this._artifact.saveAs(path);
}

它把等待与拷贝逻辑全部委托给 Artifactartifact.ts)。从源码结构看,Artifact 在录制开始时即注册为目标产物,并在服务端完成(reportFinished)后把文件流式传回客户端写入用户指定路径——这正对应文档中"等待页面关闭且视频完整保存"的语义。测试 video.spec.ts 中有 should saveAs video 用例验证 saveAs 后目标文件确实存在(expect(fs.existsSync(saveAsPath)).toBeTruthy()),同时也有在录制尚未完成/页面已关闭等边界条件下调用 saveAs 的断言。

对 CI 场景的典型用法是:远端执行测试后,用 saveAs() 把视频拉回本地供 HTML Reporter 或制品归档使用——这正是 path() 在远程模式下抛错时给出的官方替代方案。

四、async method: Video.delete(since: v1.11)

官方定义:

删除视频文件。如视频仍在录制,会先等待视频录制结束再删除。

await page.video().delete();
page.video().delete();
await page.video.delete()   # async / sync 写法一致
await page.Video.DeleteAsync();

从客户端实现看,delete() 对未启动的录制是静默的(_artifact 为空时直接返回),否则透传给 this._artifact.delete()

async delete(): Promise<void> {
  if (this._artifact)
    await this._artifact.delete();
}

"等待视频结束再删除"的保证由 Artifact 服务端逻辑提供。测试中对 delete 的覆盖包括先持有 delete() 的 Promise、再关闭上下文,确认文件最终被移除。delete() 适合在"确认不需要该视频"时主动清理磁盘,避免 artifactsDir 中视频文件堆积。

五、服务端纵深:VideoRecorder 与 ffmpeg 编码管线

Video 对象背后的真实工作发生在服务端。videoRecorder.ts 中可以看到完整实现,几个关键事实如下:

5.1 启动时机:录制先于页面恢复

startAutomaticVideoRecording(page) 在上下文配置了 recordVideo 时被调用:它读取 recordVideo.dir(缺省回退到 browser.options.artifactsDir),以 page.guid + '.webm' 作为文件名创建 Artifact,并赋给 page.video。源码注释明确指出顺序约束:必须先启动视频录制器,再发送 Screencast.startScreencast,之后再 Target.resume,保证首帧不丢失。showActions 选项则通过 page.screencast.showActions(...) 注入元素高亮标注。

5.2 ffmpeg 进程与编码参数

FfmpegVideoRecorder 通过 registry.findExecutable('ffmpeg') 找到随 Playwright 分发的 ffmpeg 可执行文件,并以 stdin 管道方式向其喂帧。关键常量与参数(videoRecorder.ts_launch 内的参数列表):

  • 帧率固定 fps = 25-r 25),ffmpeg 基于输入时间戳自动复制帧;
  • 容器与编码:WebM + VP8(-c:v vp8),恒定质量模式 -crf 8,质量区间 -qmin 0 -qmax 50,码率 -b:v 1M
  • 低延迟与稳定性:-deadline realtime -speed 8(不过度占用 CPU 以跟上帧率)、-threads 1(CPU 超订时显著降低卡顿)、-an(无音频);
  • 输入帧被封装进最小 Matroska 流(见 ebml.tswriteHeader / writeClusterHeader),每帧携带显式时间戳,让 ffmpeg 直接读取帧时序:-f matroska -i pipe:0 -fpsprobesize 0 -probesize 32 -analyzeduration 0
  • 尺寸适配:默认用 pad=${w}:${h}:0:0:gray,crop=${w}:${h}:0:0 滤镜把视口帧居中裁剪/补边到目标尺寸——这解释了文档中"视口画面被放在输出视频左上角、必要时等比缩小"的行为;
  • 输出文件必须以 .webm 结尾(构造函数中 assert 校验),视频元数据写入 creation_time
  • 特殊处理:若整个会话没有任何帧(_lastFrame 为空),停止时会写入一帧白图,保证 ffmpeg 产出非空文件;停止时还会补一帧尾帧并追加至少 1 秒时长,避免视频在编码器缓冲区还有数据时戛然而止。

5.3 尺寸规则:默认 800x800 缩放

start()videoSize = options.size ?? size(screencast 实际尺寸),注释写明"对视频文件,无论实际像素数据如何,优先编码为指定尺寸"。对应 types.d.tsrecordVideo.size 的文档:未指定时尺寸等于 viewport 缩小到 800x800 以内;若 viewport 未显式配置,视频尺寸默认为 800x450。recordVideo.dir 未指定时视频存入 artifactsDir(即 browserType.launch() 选项),这也与 Video.path() 返回路径在录制启动时即确定的实现一致。

六、配置参数速查:recordVideo 选项

汇总 videos.md 与类型定义 types.d.ts 中的 recordVideo 配置(browser.newContext() 选项):

选项 类型 / 取值 默认值 说明
dir string artifactsDir(launch 选项) 视频保存目录
size { width, height } 视口缩放到 800x800 以内;未配置视口时为 800x450 视频帧尺寸,页面画面必要时等比缩小
showActions { duration, position, fontSize, cursor } 关闭 交互元素视觉标注:duration 默认 500ms,position 默认 top-rightfontSize 默认 24,cursor 默认 pointer

Playwright Test 场景下则由配置文件的 use.video 控制('off' / 'on' / 'retain-on-failure' / 'on-first-retry'),视频文件默认出现在 test-results 测试输出目录;多页面场景通过 page.video()(即本文的 Video 类)拿到与页面绑定的视频对象:

// 多页面场景:每个 page(含 popup)有独立 video 对象
const path = await page.video().path();
// 注意:视频仅在页面或浏览器上下文关闭后才可用

七、最佳实践小结

  1. 务必 await context.close():视频在上下文关闭时才保证写盘,这是 path()/saveAs()/delete() 一切等待语义的前提;
  2. 本地用 path(),远程用 saveAs()path()connect() 远程模式下必然抛错,官方推荐路径是 saveAs() 拉回本地副本;
  3. Java / Python 同步 API 的 saveAs() 必须放在 close 之后,异步 API 则任意时机调用均可安全等待;
  4. 不需要视频时调用 delete(),它会等待录制结束后删除文件,且不要求录制一定已启动(未启动时静默返回);
  5. 控制成本可调 size:尺寸直接决定 VP8 编码开销与文件体积,源码中的 -deadline realtime -speed 8 -threads 1 表明 Playwright 已针对实时性做了保守调参,但仍建议为批量测试设置合理分辨率。

参考文件:API 文档视频录制指南客户端 Video 实现服务端 VideoRecorderArtifact 实现类型定义视频测试

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