Playwright Video 类详解:recordVideo 之下的视频录制 API 与 ffmpeg 编码管线
当使用 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.ts 中 path() 的实现非常直白:
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;
}
三个关键事实可以确认:
_isRemote在构造函数中取自connection.isRemote(),即只要客户端是通过connect()等方式远程连接,path()一律抛错,并明确提示"请改用saveAs()保存本地副本"。这是因为路径指向的是远端服务器的文件系统,对本地进程没有意义;path()返回的是_artifact._initializer.absolutePath——即录制启动时就已分配好的目标路径。这与Video类文档"返回视频将(will be)写入的路径"的措辞一致:文件句柄在上下文关闭时才真正完成写入,但路径在开始时就是确定的;- 未启用录制时(
_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);
}
它把等待与拷贝逻辑全部委托给 Artifact(artifact.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.ts 的
writeHeader/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.ts 中 recordVideo.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-right,fontSize 默认 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();
// 注意:视频仅在页面或浏览器上下文关闭后才可用
七、最佳实践小结
- 务必 await
context.close():视频在上下文关闭时才保证写盘,这是path()/saveAs()/delete()一切等待语义的前提; - 本地用
path(),远程用saveAs():path()在connect()远程模式下必然抛错,官方推荐路径是saveAs()拉回本地副本; - Java / Python 同步 API 的
saveAs()必须放在 close 之后,异步 API 则任意时机调用均可安全等待; - 不需要视频时调用
delete(),它会等待录制结束后删除文件,且不要求录制一定已启动(未启动时静默返回); - 控制成本可调
size:尺寸直接决定 VP8 编码开销与文件体积,源码中的-deadline realtime -speed 8 -threads 1表明 Playwright 已针对实时性做了保守调参,但仍建议为批量测试设置合理分辨率。
参考文件:API 文档、视频录制指南、客户端 Video 实现、服务端 VideoRecorder、Artifact 实现、类型定义、视频测试。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0626
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00