
1. 从 hyperframes 说起一个被低估的 HTML 转 MP4 思路第一次看到 hyperframes 这个词是在一个做自动化内容分发的朋友那里。他当时的需求很具体手里有一批用 HTML 写的动态页面想批量转成 MP4 视频丢到各个渠道去分发。市面上的录屏工具要么太重要么没法批量跑要么在服务器上根本装不起来。他试了一圈最后落到了 hyperframes 这个方向上。hyperframes 本质上不是一个具体的软件包而是一类技术思路的统称把 HTML 页面当作视频的“帧源”通过程序化的方式逐帧渲染、合成最终输出 MP4 文件。它和传统的“录屏”有本质区别——录屏是抓取屏幕上已经显示出来的画面而 hyperframes 是让渲染引擎按照你指定的帧率、分辨率、时间轴去主动生成每一帧。这意味着你可以精确控制每一帧的内容可以在服务器端无头运行可以批量处理成百上千个页面。这个思路解决的核心问题是HTML 是最容易程序化生成的视觉内容格式而 MP4 是最通用的视频分发格式两者之间一直缺一座轻量、可编程的桥。设计师用 After Effects 做动画开发者用代码做动画但开发者做的动画往往停留在浏览器里没法直接变成视频文件。hyperframes 这类方案就是让开发者用自己最熟悉的 HTML/CSS/JS 写“视频”然后一键导出成 MP4。适合谁来参考三类人最有用一是做自动化内容生产的开发者比如批量生成数据可视化视频、电商商品展示视频二是做 AI coding agents 相关工具链的人因为现在很多 agent 的输出是 HTML 报告或可视化页面需要转成视频归档或分发三是需要把网页内容做成视频教程、演示材料的技术博主和产品团队。2. 整体设计思路为什么是 HTML 而不是别的2.1 帧源选择的逻辑HTML 的天然优势做视频生成第一步要决定“每一帧长什么样、怎么来”。常见的选择有几种用 Canvas 直接画、用 SVG 做矢量帧、用图片序列拼、用 HTML 页面渲染。hyperframes 这类方案选 HTML 作为帧源背后有几个很实际的考量。HTML 的表达能力足够强。文字排版、CSS 动画、渐变、阴影、圆角、flex 布局、grid 布局这些东西用 HTML/CSS 写起来极其顺手而用 Canvas 或 SVG 从头画则要费很大劲。尤其是当你要生成的内容本身就有大量文本、表格、图表的时候HTML 的排版能力是碾压性的。你想想一个数据报告页面用 HTML 写可能半小时搞定用 Canvas 画可能要一整天。HTML 的生态足够丰富。ECharts、D3、Chart.js、Three.js 这些可视化库都是基于浏览器环境的它们输出的就是 DOM 或 Canvas 内容。如果你选 HTML 作为帧源这些库可以直接用不需要重写。而如果你选别的帧源这些库的产出还得再转换一层。HTML 的调试足够方便。你可以在浏览器里直接打开页面看效果改一行 CSS 刷新就能看到变化。这种即时反馈的调试体验是视频生成这种“慢反馈”场景里非常宝贵的。相比之下如果你用纯代码生成图片序列每次调整都要跑一遍完整流程才能看到结果效率差很多。注意选 HTML 做帧源有一个前提就是你的渲染环境必须能完整支持你用的 CSS 特性和 JS 库。无头浏览器和真实浏览器之间偶尔会有渲染差异尤其是涉及字体、渐变、滤镜的时候需要提前验证。2.2 渲染管线的设计从页面到帧再到视频hyperframes 的渲染管线拆开来看是三个阶段页面准备、逐帧渲染、视频合成。每个阶段都有它的设计取舍。页面准备阶段核心问题是“怎么让页面在指定时间点呈现出指定状态”。最直接的做法是用setTimeout或requestAnimationFrame控制动画进度然后截图。但这种方式有个致命问题时间不可控。你没法保证第 30 帧的时候动画刚好走到 1 秒的位置。更可靠的做法是把动画“参数化”——用 JS 控制一个全局的时间变量每一帧渲染前把这个变量设成对应的时间值然后触发页面重绘。这样第 N 帧对应的时间就是N / fps完全确定。逐帧渲染阶段核心问题是“怎么高效地截取每一帧”。无头浏览器提供了截图接口但每次截图都有开销。如果视频是 30fps、时长 10 秒那就是 300 次截图每次哪怕只花 50ms总共也要 15 秒。实际项目中单帧渲染时间往往在 100ms 到 500ms 之间取决于页面复杂度。所以优化方向很明确减少单帧渲染时间或者并行渲染多帧。视频合成阶段核心问题是“怎么把帧序列变成 MP4”。最常用的工具是 FFmpeg它可以把图片序列按指定帧率编码成 H.264 或 H.265 的 MP4。这里有个关键参数是 CRFConstant Rate Factor控制画质和文件大小的平衡。CRF 值越低画质越好文件越大一般 18 到 28 之间比较常用23 是默认值。如果你要压缩成 H.265同样的画质下文件能小 30% 到 50%但编码时间会更长兼容性也稍差一些。2.3 与 AI coding agents 的结合点现在 AI coding agents 越来越普及codex cli、claude code 这类工具能直接生成 HTML 页面。这就产生了一个很自然的组合agent 生成 HTML 内容hyperframes 把 HTML 转成 MP4。比如你让 agent 生成一个数据报告页面然后自动转成视频发到群里或者让 agent 生成一个产品演示页面转成视频放到官网。这个组合的关键在于“自动化闭环”。agent 的输出是文本文本可以写成 HTML 文件HTML 文件可以喂给 hyperframes 管线管线输出 MP4。整个过程不需要人工干预适合做批量内容生产。我在实际项目里试过用 codex cli 生成一批 HTML 报告然后用脚本批量转 MP4整个流程跑下来很顺。提示agent 生成的 HTML 往往带有一些内联样式和脚本转视频前最好做一次清理把不必要的交互脚本去掉只保留渲染相关的部分。否则截图时可能会因为脚本执行顺序问题导致画面不一致。3. 核心细节解析帧率、分辨率与时间轴控制3.1 帧率选择的实际考量帧率决定了视频的流畅度也直接决定了渲染工作量。常见的帧率有 24fps、25fps、30fps、60fps。选哪个取决于你的内容类型和性能预算。如果你的内容是静态页面加淡入淡出、位移这类简单动画24fps 或 25fps 就够了人眼看起来是连贯的。如果是滚动字幕、快速切换的场景30fps 会更顺滑。如果是游戏画面或高动态内容才需要 60fps。对于大多数 HTML 转 MP4 的场景30fps 是一个比较稳妥的默认值。但帧率不是越高越好。30fps 意味着每秒要渲染 30 帧60fps 就是 60 帧渲染时间直接翻倍。如果你的页面单帧渲染要 200ms30fps 下 10 秒视频要 60 秒渲染60fps 下就要 120 秒。所以在画质和效率之间要做一个权衡。我的经验是先按 30fps 跑一版如果动画明显卡顿再考虑提高否则没必要。帧率适用场景10秒视频帧数相对渲染耗时24fps静态展示、简单淡入淡出240基准25fps通用展示、文字动画250约 1.04 倍30fps滚动、切换、中等动态300约 1.25 倍60fps高动态、游戏画面600约 2.5 倍3.2 分辨率与设备像素比的处理分辨率的选择相对直接1080p1920x1080是最通用的720p1280x720适合文件大小敏感的场景4K3840x2160适合高质量输出。但这里有个容易被忽略的点设备像素比devicePixelRatio。在无头浏览器里默认的 devicePixelRatio 通常是 1。如果你在 CSS 里用了 2x 的图片或者高分辨率 canvas截图出来的清晰度可能不够。解决办法是在启动无头浏览器时设置deviceScaleFactor参数把它设成 2 或更高。这样截图出来的像素密度更高文字和线条更锐利。但 deviceScaleFactor 提高也会增加渲染负担。设成 2 意味着实际渲染面积是 4 倍单帧渲染时间可能增加 2 到 3 倍。所以如果你的输出是 1080pdeviceScaleFactor 设 1 通常够用如果输出是 4K 或者需要高 DPI 显示再考虑设 2。3.3 时间轴控制的三种实现方式时间轴控制是 hyperframes 的核心技术点。要让页面在每一帧呈现出正确的时间状态有三种常见实现方式。第一种是 CSS 动画配合animation-delay的负值技巧。把动画暂停然后用负的 delay 把动画“拨”到指定时间点。这种方式适合纯 CSS 动画性能好但控制粒度粗不适合复杂的时间轴编排。第二种是 JS 驱动的参数化动画。定义一个全局的currentTime变量每一帧渲染前设置它然后页面里的动画逻辑根据这个变量计算当前状态。这种方式最灵活可以精确控制每一个元素在每一时刻的状态适合复杂场景。缺点是页面里的动画逻辑要自己写不能用现成的 CSS 动画。第三种是 Web Animations API。浏览器原生支持可以用animation.currentTime精确设置动画进度。这种方式介于前两者之间既能用声明式的动画定义又能精确控制时间。兼容性在现代浏览器里已经很好。实操心得如果你的页面里混用了 CSS 动画和 JS 动画建议统一用第二种方式重写。混用的时候时间同步很容易出问题尤其是 CSS 动画的animation-fill-mode和 JS 的时间计算对不上会导致某些帧画面错位。4. 实操过程从零搭一条 HTML 转 MP4 的流水线4.1 环境准备与依赖安装先说一下环境。我用的是一台 Ubuntu 的机器Node.js 18 以上装了 FFmpeg。如果你在 Windows 上做建议用 WSL原生 Windows 下无头浏览器的路径和权限问题会多一些。核心依赖是 Puppeteer它自带了一个 Chromium省去了单独装浏览器的麻烦。安装命令很简单npm init -y npm install puppeteerFFmpeg 的安装看系统# Ubuntu/Debian sudo apt install ffmpeg # macOS brew install ffmpeg验证一下 FFmpeg 是否支持 H.265ffmpeg -codecs | grep hevc如果输出里有hevc相关的编码器说明支持。没有的话H.264 也够用兼容性还更好。4.2 页面准备写一个可参数化的 HTML关键点是让页面能被外部控制时间。我一般会在页面里暴露一个全局函数window.setFrameTime function(t) { // t 是当前时间单位秒 const progress t / totalDuration; // 根据 progress 更新页面元素状态 document.querySelector(.bar).style.width (progress * 100) %; document.querySelector(.counter).textContent Math.floor(t * 10) / 10; // 触发重绘 return new Promise(resolve requestAnimationFrame(resolve)); };这个函数接收时间参数更新页面状态然后等一帧重绘完成再返回。这样外部渲染脚本就可以精确控制每一帧的内容。注意requestAnimationFrame在无头模式下也能工作但如果你在页面里用了setInterval或setTimeout做动画一定要改成由setFrameTime驱动。否则截图时动画进度不可控。4.3 渲染脚本逐帧截图渲染脚本的核心逻辑是启动浏览器、打开页面、循环设置时间并截图、保存帧序列。const puppeteer require(puppeteer); const fs require(fs); const path require(path); async function renderFrames(htmlPath, outputDir, fps, duration) { const browser await puppeteer.launch({ headless: new, args: [--no-sandbox, --disable-setuid-sandbox] }); const page await browser.newPage(); await page.setViewport({ width: 1920, height: 1080, deviceScaleFactor: 1 }); await page.goto(file:// path.resolve(htmlPath)); await page.waitForFunction(typeof window.setFrameTime function); const totalFrames Math.ceil(fps * duration); for (let i 0; i totalFrames; i) { const t i / fps; await page.evaluate((time) window.setFrameTime(time), t); const framePath path.join(outputDir, frame_${String(i).padStart(5, 0)}.png); await page.screenshot({ path: framePath, type: png }); if (i % 30 0) console.log(已渲染 ${i}/${totalFrames} 帧); } await browser.close(); }这个脚本跑起来会在 outputDir 里生成一堆 PNG 文件。文件名用零填充的序号方便 FFmpeg 按顺序读取。4.4 视频合成FFmpeg 参数详解帧序列有了接下来用 FFmpeg 合成 MP4。基础命令ffmpeg -framerate 30 -i frame_%05d.png -c:v libx264 -crf 23 -pix_fmt yuv420p output.mp4几个关键参数解释一下。-framerate 30是输入帧率要和渲染时的 fps 一致。-c:v libx264指定 H.264 编码器。-crf 23是画质参数18 到 28 之间越小越好。-pix_fmt yuv420p是像素格式这个很重要不加的话某些播放器可能不认。如果要 H.265 压缩ffmpeg -framerate 30 -i frame_%05d.png -c:v libx265 -crf 28 -pix_fmt yuv420p -tag:v hvc1 output.mp4H.265 的 CRF 一般比 H.264 高 5 左右画质相当的情况下文件能小不少。-tag:v hvc1是为了让 QuickTime 和 Safari 能识别。实操心得如果帧序列很多PNG 文件会占很大空间。可以在截图时直接输出 JPEG质量设 90 以上肉眼几乎看不出差别但文件大小能小一半以上。FFmpeg 合成时把输入改成frame_%05d.jpg就行。4.5 批量处理与自动化串联单条视频跑通之后批量处理就是加一层循环。我一般会写一个任务清单每行是一个 HTML 文件路径和对应的输出配置然后脚本读清单逐个处理。const tasks [ { html: report1.html, fps: 30, duration: 10, output: report1.mp4 }, { html: report2.html, fps: 30, duration: 15, output: report2.mp4 }, ]; for (const task of tasks) { await renderFrames(task.html, ./frames, task.fps, task.duration); await execFFmpeg(task.fps, task.output); await cleanFrames(./frames); }和 AI coding agents 串联的话可以在前面加一步调用 codex cli 或类似工具生成 HTML然后直接进入渲染流程。这样从“一句话需求”到“MP4 文件”就是全自动的。5. 常见问题与排查技巧实录5.1 画面闪烁或元素错位这是最常见的问题原因通常是截图时页面还没完成重绘。page.screenshot默认会等页面稳定但如果你在setFrameTime里用了异步操作截图可能在异步完成前就执行了。解决办法是在setFrameTime里返回一个 Promise等requestAnimationFrame回调后再 resolve然后渲染脚本里await这个 Promise。另外可以在截图前加一个很小的延迟比如await page.evaluate(() new Promise(r setTimeout(r, 16)))给浏览器一帧的时间完成绘制。5.2 字体不一致或文字模糊无头浏览器里的字体渲染和真实浏览器可能有差异尤其是中文字体。如果服务器上没装对应字体会 fallback 到默认字体导致排版错位。解决办法是在服务器上安装你需要的字体或者在 HTML 里用font-face嵌入字体文件。文字模糊通常是 deviceScaleFactor 太低导致的设成 2 可以明显改善。5.3 渲染速度太慢单帧渲染时间过长通常是页面里有大量 DOM 操作或者复杂滤镜。优化方向有几个减少不必要的 DOM 节点避免在每一帧里重新创建元素把box-shadow、filter: blur这类高开销的 CSS 属性换成预渲染的图片降低 deviceScaleFactor。如果还是慢可以考虑并行渲染。把帧序列分成几段每段用一个独立的浏览器实例渲染最后合并。但要注意内存占用每个 Chromium 实例大概吃 200MB 到 500MB 内存。5.4 FFmpeg 合成报错常见的报错有“找不到输入文件”和“像素格式不支持”。前者检查文件名格式是否匹配%05d对应的是 5 位零填充的数字。后者加上-pix_fmt yuv420p基本能解决。还有一个坑是帧率不一致。如果渲染时是 30fpsFFmpeg 输入也必须是 30否则视频速度会不对。如果帧序列的帧率不确定可以用-r参数强制指定输出帧率。问题现象可能原因排查方向解决方法画面闪烁截图时未完成重绘检查 setFrameTime 是否 await返回 Promise 并等待文字模糊deviceScaleFactor 过低查看截图分辨率设为 2 或更高字体错位服务器缺字体对比本地和服务器渲染安装字体或嵌入 font-face渲染慢页面复杂或滤镜多单帧计时简化 DOM、降低 scaleFactor合成报错文件名或像素格式检查 FFmpeg 输出加 -pix_fmt yuv420p视频速度不对帧率不匹配核对渲染和合成帧率统一为相同值5.5 与 CLI 工具链的配合问题现在很多 AI coding agents 都提供 CLI比如 codex cli、zcode cli 这些。用它们生成 HTML 的时候要注意输出路径和编码。有些 CLI 默认输出到 stdout需要重定向到文件有些在 Windows 下会有换行符问题导致 HTML 里的\r\n影响渲染。我的做法是在 CLI 输出和渲染之间加一个清洗步骤用 Node.js 读一遍 HTML把多余的空白和注释去掉再写回文件。这样既减小了文件体积也避免了编码问题。提示如果你用 claude code 这类工具遇到internetopenurl() failed这类网络错误通常是环境配置问题和 hyperframes 本身无关。检查一下代理设置和网络权限就行。6. 性能优化与扩展玩法6.1 渲染性能的瓶颈定位要优化先定位。我一般会在渲染脚本里加计时记录每一帧的渲染耗时。如果发现某几帧特别慢通常是那一帧对应的页面状态触发了重排或重绘。用 Chrome DevTools 的 Performance 面板可以录一段渲染过程看看时间花在哪里。常见的大头是布局计算Layout、绘制Paint和合成Composite。如果 Layout 占比高说明 DOM 结构太复杂如果 Paint 占比高说明有大量重绘区域如果 Composite 占比高说明图层太多。针对性的优化减少 DOM 层级用transform和opacity做动画这两个属性不触发 Layout把静态背景单独放一层。6.2 用 H.265 压缩减小文件体积H.265 在同等画质下比 H.264 省 30% 到 50% 的码率。对于批量生成的视频这个节省很可观。但 H.265 的编码速度慢不少如果视频量大可以考虑用硬件加速。FFmpeg 支持 NVENC 和 QSV 硬件编码命令里把libx265换成hevc_nvenc或hevc_qsv就行。前提是机器上有对应的显卡。没有硬件加速的话H.265 的编码时间可能是 H.264 的 2 到 3 倍需要权衡。6.3 扩展方向从静态页面到动态数据hyperframes 这套思路不限于静态 HTML。你可以把动态数据注入页面比如从数据库读一批数据生成对应的 HTML再转成视频。这样就能做数据日报视频、监控大屏回放、批量报表视频。再进一步可以结合 AI coding agents 做“一句话生成视频”。用户输入一段描述agent 生成 HTMLhyperframes 转 MP4全程自动化。这个方向在内容生产领域很有想象空间尤其是需要批量产出个性化视频的场景。6.4 一些踩过的坑和对应技巧第一个坑是内存泄漏。长时间跑批量渲染Chromium 实例可能不释放内存。解决办法是每处理完一批任务就重启浏览器实例或者用page.close()显式关闭页面。第二个坑是并发控制。同时开太多浏览器实例会把内存吃满导致系统卡死。我一般限制并发数在 CPU 核心数的一半左右比如 8 核机器最多开 4 个实例。第三个坑是文件句柄。大量 PNG 文件同时读写可能触发系统的文件句柄限制。处理完一批就及时清理帧文件不要攒着。第四个坑是时区问题。如果页面里有new Date()相关的逻辑服务器时区和本地不一致会导致画面内容不同。解决办法是在页面里固定时区或者把时间作为参数传进去。7. 我在这套流程上的一些个人体会这套 HTML 转 MP4 的流程我从最早用录屏软件手动操作到后来写脚本半自动再到现在全自动跑批量前后折腾了挺长时间。最大的感受是能程序化的事情尽量不要手动做。录屏看起来简单但一旦量上来手动操作的错误率和时间成本都受不了。另一个体会是参数化时间轴这个设计是整套方案的关键。没有它渲染就是不可控的有了它每一帧都是确定的可以重跑、可以对比、可以调试。这个思路其实和做前端动画是一回事只是把“实时播放”换成了“逐帧生成”。如果你刚开始做建议先用一个简单的 HTML 页面跑通全流程确认环境没问题再逐步增加复杂度。不要一上来就搞很复杂的页面那样出了问题很难定位是环境问题还是页面问题。先把管线跑顺再优化画质和速度这个顺序比较稳妥。