关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%

摘要

转码服务里很常见的配置是一套参数吃所有上传的视频。这次拿一段自制的模拟网课录屏和一段摄像头视频,用同一组 x264 参数各压了一遍。三个参数在两类素材上走向相反:关键帧间隔从 1 秒拉到 10 秒时录屏小了 87.3%、摄像头视频只小 10.8%;CRF 从 23 调到 28 时录屏只小 17.2%、摄像头视频小了 43.4%。-tune stillimage 让录屏小 2.3%,放到摄像头视频上体积反倒多出 21.5%。

按这组数据我给的方案是分两档并且只差关键帧间隔一项:录屏档拉到 10 秒左右,其余参数和摄像头档完全相同,CRF 和 tune 也不跟着分档。这样分的好处是判错素材的代价很小。录屏误进摄像头档只是少省一些;摄像头视频误进录屏档体积基本不变,多出来的只有拖动时要多解的帧。机制部分凡是没有单独验证过的,文中都标了推断。

版本声明

数字都出自 2026-11-09 这一轮实测,机器是一台 Apple M4 的 Mac 电脑,系统 macOS 26.5.2。编码用 ffmpeg / ffprobe 9.0.2 里的 x264 core 165。H.265 对照用 x265 4.3。现成预设的参照有两个:HandBrake 命令行版 1.11.2 和 macOS 自带的 avconvert。没有特别说明时编码参数统一是 x264 preset medium、CRF 23、yuv420p、去掉音轨。每一档只跑一次。文中 2.3、3.1、4.1、4.2、5.4 这五节的代码都在这台电脑上实跑过。重跑出来的字节和首轮逐项对过也完全相同。体积一律按十进制换算,1 MB 等于 1,000,000 字节。

浏览器端那一路对照我用的是图映 ImgIng imging.cn/ 的视频压缩,在桌面 Chromium 151 里跑,选的是智能推荐档。视频压缩不是我参与的模块,文中只引用它在这段样本上输出的字节数和文字指标。它内部怎么定参数我不清楚。

适用边界

这篇回答的是转码服务的一个配置问题:录屏和摄像头视频该不该用不同的 x264 参数,要分的话分在哪一项。按目标体积反推码率和哪些文件不值得再压都不在这篇里。录屏样本是程序合成的模拟画面,比真实录屏干净而且静止段更多;摄像头样本只有两段公开视频。这些数字只能当方向看,不能当成任何一门真实课程的预估。

摄像头对照只测了 x264 的几个参数,降帧率、降分辨率和换 x265 都没有测。文中凡是讲帧率、分辨率和 H.265 的结论,都只针对录屏。

文章目录

一、录屏和摄像头视频能共用一个转码档吗

  • 1.1 三个参数在两类素材上走向相反
  • 1.2 分档只需要改关键帧间隔一项

二、这次对照用了哪些样本

  • 2.1 录屏样本是自制的模拟网课
  • 2.2 摄像头对照来自维基共享资源
  • 2.3 录屏原片的码率是摄像头的两倍

三、录屏的字节都花在哪类帧上

  • 3.1 两类素材按帧类型拆字节
  • 3.2 静止帧压到只剩几十字节
  • 3.3 摄像头视频的大头在P帧
  • 3.4 三次翻页都没有触发关键帧

四、录屏关键帧间隔多少合适

  • 4.1 关键帧间隔拉长对录屏省得最多
  • 4.2 同样拉长摄像头视频只省一成
  • 4.3 关键帧越稀跳转要解的帧越多
  • 4.4 文字清晰度不随关键帧间隔变化
  • 4.5 录屏档的关键帧放在10秒左右

五、CRF和tune为什么在两类素材上反着走

  • 5.1 调高CRF对录屏省得不多
  • 5.2 摄像头视频对CRF敏感得多
  • 5.3 给摄像头视频加stillimage反而变大
  • 5.4 录屏降帧率省下的其实是关键帧
  • 5.5 录屏降到720p会先伤到小字

六、两个转码档具体怎么设

  • 6.1 录屏档保留原分辨率只拉长关键帧
  • 6.2 摄像头档靠CRF控制体积
  • 6.3 两档只差关键帧间隔一项
  • 6.4 彩色小字要另看色度采样

七、测量方法自身有哪些坑

  • 7.1 关键帧间隔要按秒写进配置
  • 7.2 整帧SSIM会被静止背景冲淡
  • 7.3 跳转耗时只能看相对大小
  • 7.4 帧包字节和文件字节要分开记
  • 7.5 模拟样本会把录屏的收益估高

八、适用边界与风险提示

  • 8.1 数字只代表这段样本和x264 medium
  • 8.2 长关键帧会拖慢拖动和切片
  • 8.3 播放端兼容性没有测

九、还没解决的问题

  • 9.1 真实系统录屏是不是恒定码率没有测
  • 9.2 带画中画的课程归哪一档还没有答案

参考资料

一、录屏和摄像头视频能共用一个转码档吗

1.1 三个参数在两类素材上走向相反

结论表放在最前面。下表每一行只改一个参数,体积变化都相对各自的基准来算:关键帧那一行的基准是 1 秒一个关键帧,其余几行的基准是 x264 默认参数下的 CRF 23。s1 是第二段摄像头视频,只有 7.87 秒长,放在最后一列做旁证。

只改一项 录屏 摄像头 s4 摄像头 s1
关键帧间隔 1 秒改成 10 秒 −87.3% −10.8% −28%
CRF 23 改成 28 −17.2% −43.4% −50.4%
加 -tune stillimage −2.3% +21.5% +12.8%
加 -tune animation +5.2% −6.9% −0.5%
关键帧不限,全片只留一个 −75.1% −0.3% 0%

这张表先看符号和量级,不急着找哪一格最大。录屏对关键帧间隔极其敏感、对 CRF 不太敏感。摄像头视频正好倒过来。stillimage 是表里唯一在两类素材上符号相反的参数。它在录屏上几乎不起作用,在摄像头视频上却让体积多出两成。按摄像头视频的习惯把关键帧设在 2 秒,录屏编出来是 10 秒档的 4.1 倍。反过来把录屏的参数套到摄像头视频上,省不了多少,碰上 stillimage 这种参数还会变大。

1.2 分档只需要改关键帧间隔一项

这篇最后给出的方案很短。录屏档和摄像头档用同一个 CRF 和同一个 preset 且都不加 tune。两档只有关键帧间隔不同:录屏档放到 10 秒左右,摄像头档留在 2 到 5 秒。为什么不顺手把 CRF 和 tune 也分开,第六章会把这笔账算一遍。中间几章先把数据和原因摆出来:录屏的字节花在哪类帧上、关键帧间隔怎么同时影响体积和拖动、CRF 和 tune 为什么在两类素材上反着走。

二、这次对照用了哪些样本

2.1 录屏样本是自制的模拟网课

录屏一侧用的是自制的 4 分钟模拟网课录屏(课件翻页、鼠标、打字、滚动,1920×1080、30 fps,按 12 Mbps 恒定码率生成,360.0 MB)。画面来源是一个自写的课件网页,按时间轴逐帧截图后编码成视频。画面左边的 4 页课件里有中文标题、要点、红蓝绿三色小字、代码块和表格;右边是 15 像素的讲义小字和一块深色的代码编辑区;顶上还有菜单栏和鼠标指针。所有文字都是自写的。时间轴上安排了 3 次翻页、两段鼠标移动、一段打字和一段讲义滚动。其余时间画面完全不动。静止段合计约 187 秒,占全片约 78%。

12 Mbps 恒定码率是样本设定的假设,不是哪个系统的默认值。Apple 和微软的帮助页(2026-11-09 查阅)都查不到系统录屏的默认码率和帧率,Windows 那边只说质量档会连带改分辨率和码率。设成恒定码率是为了模拟画面不动也照样按码率写盘的那种录法。于是原文件里静止段的非关键帧每帧中位 50,002 字节,跟打字、滚动时没什么两样,这些静止段吃掉了全片 71.1% 的字节。作为反证,同一份母版交给这台 Mac 的硬件 H.264 编码器,目标码率也给 12 Mbps 但不强制恒定,产物是 23,737,333 字节,平均约 0.79 Mbps。录屏不一定都大。大不大取决于录屏软件怎么编码,第九章还会回来讲这件事。

这段样本有几处比真实录屏好压。它是截图合成的,没有噪点和抗锯齿带来的细微抖动,静止段几乎是零变化。它没有音轨,分辨率是 1080p 而不是 Mac 上常见的 Retina 两倍尺寸,也没有 60 fps 的版本。

2.2 摄像头对照来自维基共享资源

摄像头对照有两段,都取自维基共享资源的 CC BY-SA 视频。它们只用来统计体积和帧类型,文中不放画面。主对照 s4 是一段 2019 年的街头集会视频。它的规格是 1920×1080、25 fps、29.81 秒的 VP9。辅对照 s1 拍的是路边的一头驼鹿。它是 VP9 10-bit 的 1080p30,只有 7.87 秒。两段都经过重新编码并已改动。作者和许可写在文末参考资料里。

s1 太短了,关键帧 10 秒和不限这两档在它身上都是全片一个关键帧,表里这两格数字会一样。它的作用只是确认 s4 的方向不是个例,主结论都以 s4 为准。

2.3 录屏原片的码率是摄像头的两倍

拿到素材先看原片参数。下面这段在两个原片上各跑一次 ffprobe,文件名换成了占位名。

bash 复制代码
for f in rec.mp4 cam.webm; do
  echo "== $f"
  ffprobe -v error -select_streams v:0 \
    -show_entries stream=codec_name,width,height,avg_frame_rate,bit_rate:format=duration,size,bit_rate \
    -of compact=p=0:nk=0 $f
done
ini 复制代码
== rec.mp4
codec_name=h264|width=1920|height=1080|avg_frame_rate=30/1|bit_rate=11995489
duration=240.000000|size=359952205|bit_rate=11998406
== cam.webm
codec_name=vp9|width=1920|height=1080|avg_frame_rate=25/1|bit_rate=N/A
duration=29.811000|size=22264465|bit_rate=5974832

代码说明:-show_entries 一次取两组字段。冒号前面是视频流的编码、尺寸、帧率和码率。冒号后面是整个文件的时长、字节和码率。compact 格式把一组字段压成一行,两份素材上下对着看比较方便。WebM 的视频流读不到 bit_rate,只能退回整个文件的码率。这个 5,974,832 里还含着一条 Opus 音轨。纯视频只会更低。

录屏原片每秒约 12 Mbps,摄像头原片整个文件还不到 6 Mbps。录屏的码率是摄像头的两倍还多。画面复杂的是摄像头视频,占字节多的却是录屏。原因就在恒定码率:录屏的码率是按预算花掉的,没有按画面的需要花。重编码时编码器改成按画质分配,两类素材的走向从这一步开始分开。下一章看字节具体落在哪里。

三、录屏的字节都花在哪类帧上

3.1 两类素材按帧类型拆字节

两段素材先用 CRF 23 的默认参数重编码(x264 默认的关键帧间隔是 250 帧),再按帧类型统计每类帧的个数和字节。下面这段脚本读 ffprobe 的 JSON 输出,算每类帧的字节占比和平均大小;录屏额外算一下第 131 到 180 秒那段静止画面里非关键帧的中位大小。

python 复制代码
import json, statistics, subprocess, sys

def frames(path):
    out = subprocess.run(["ffprobe", "-v", "error", "-select_streams", "v:0",
                          "-show_entries", "frame=pict_type,pkt_size,pts_time",
                          "-of", "json", path], capture_output=True, text=True, check=True)
    return json.loads(out.stdout)["frames"]

for path in sys.argv[1:]:
    fr = frames(path)
    total = sum(int(f["pkt_size"]) for f in fr)
    print(path, f"{len(fr)} frames, {total:,} bytes")
    for t in "IPB":
        sz = [int(f["pkt_size"]) for f in fr if f["pict_type"] == t]
        print(f"  {t}: n={len(sz):>5}  share={sum(sz) / total:6.1%}  mean={sum(sz) // len(sz):>7,}")
    quiet = [int(f["pkt_size"]) for f in fr
             if f["pict_type"] != "I" and 131 <= float(f["pts_time"]) < 180]
    if quiet:
        print(f"  non-I frames in 131-180 s: n={len(quiet)}  median={statistics.median(quiet):g}")
ini 复制代码
out/rec_default.mp4 7200 frames, 5,135,679 bytes
  I: n=   29  share= 79.4%  mean=140,557
  P: n= 1918  share= 10.9%  mean=    290
  B: n= 5253  share=  9.8%  mean=     95
  non-I frames in 131-180 s: n=1464  median=77
out/cam_default.mp4 745 frames, 18,192,593 bytes
  I: n=    8  share=  4.5%  mean=102,005
  P: n=  221  share= 59.4%  mean= 48,886
  B: n=  516  share= 36.1%  mean= 12,737

代码说明:pkt_size 是每一帧压缩后的包大小。脚本按 pict_type 把它分成 I、P、B 三类求和。占比的分母是全部帧包之和,不含 MP4 容器的开销,第七章会单独说这个差。静止窗口取第 131 到 180 秒,是第 3 页翻完、讲义滚完以后的那段画面。起点往后让了 1 秒来避开滚动刚停的那几帧。摄像头这段只有 30 秒长。窗口里没有帧,脚本就跳过这一行不打。

录屏 7200 帧里只有 29 个关键帧,却占了 79.4% 的字节。平均每个关键帧 140,557 字节。剩下 7171 个 P 帧和 B 帧加起来只占两成。摄像头视频正好反过来:8 个关键帧只占 4.5%,P 帧占 59.4%。s1 那段的 P 帧占比更高,是 77.7%。

3.2 静止帧压到只剩几十字节

静止窗口里的 1,464 个非关键帧中位只有 77 字节。把全片所有静止段合起来算也一样:5,483 帧的中位是 77 字节,平均 85 字节,和十四万字节一个的关键帧差了三个数量级。其他动作的帧也不贵。鼠标移动那段每帧中位 98 字节,打字那段 80 字节。只有讲义滚动那段到了 906 字节。滚动时一整块文字都在位移。回头对照 2.1 节的原片,同样的静止帧在那里是 50,002 字节,重编码前后差了六百多倍。

原理这部分是推断,这次没有单独去验证。P 帧和 B 帧编码的是当前帧和参考帧之间的差。画面不动的宏块会走 skip 模式。它只需要几个比特来表示沿用参考帧、没有残差。整帧几乎全是这种宏块的时候,包里剩下的主要就是帧头和这些跳过标记。按这个理解静止录屏的 P 帧和 B 帧已经接近能压到的下限。再怎么调 CRF 也挤不出多少。录屏能省的空间几乎全在关键帧那一侧。

3.3 摄像头视频的大头在P帧

s4 每个 P 帧平均 48,886 字节,录屏的 P 帧平均 290 字节,两边差了一百多倍。就算镜头不动,传感器噪点和人物的细微动作也会让几乎每个宏块都带着残差。这是推断,但和数字对得上。s4 的 B 帧平均 12,737 字节,比录屏的 B 帧大了一百多倍。双向预测也没能把差值消掉。

关键帧在摄像头视频里不是大头。砍关键帧能省的天花板就很低。s4 的 8 个关键帧全加起来也只占 4.5%。就算把它们全换成 P 帧,最多也只能省下这 4.5% 里的一部分。录屏的体积压在关键帧上,摄像头视频的体积压在 P 帧上。第四章里两类素材对关键帧间隔反应差那么多,前提就在这里。

3.4 三次翻页都没有触发关键帧

有一个看着顺理成章的判断在这段样本上不成立。翻页是整块课件区换了内容,照直觉 x264 的场景切换检测应该在这里插一个关键帧。实际上第 40、120、180 秒三次翻页都没有插,新页面是用 P 帧编码的。翻页前后那几个 P 帧在 22~36 KB 之间,离平均 140 KB 的关键帧还差一大截。三次翻页那一刻本身的帧分别是 153、22,039 和 161 字节,承载新页内容的是紧挨着它的 P 帧。

原因只能推断。翻页只换了左半边课件,画面另一半原封不动。帧间预测还是比整帧重画划算,场景切换检测的打分就没过线。这件事对分档有直接影响。不限档把场景切换关掉(scenecut=0)再编一次,1,299,049 对 1,299,050,差 1 字节。在这段样本上关键帧完全由间隔参数决定,没有被画面变化额外插进来。真实录屏里如果切到全屏播放视频或者整屏切换窗口,场景切换有可能被触发。这一条这次没测。

四、录屏关键帧间隔多少合适

4.1 关键帧间隔拉长对录屏省得最多

下面这段脚本把关键帧间隔从 1 秒到不限分成六档,各编一次录屏,每档打印关键帧个数、字节和平均需解码帧数。

python 复制代码
import os, subprocess

LADDER = {"1s": ["-g", "30"], "2s": ["-g", "60"], "5s": ["-g", "150"], "10s": ["-g", "300"],
          "30s": ["-g", "900"], "inf": ["-x264-params", "keyint=infinite"]}

def keyflags(path):
    out = subprocess.run(["ffprobe", "-v", "error", "-select_streams", "v:0",
                          "-show_entries", "frame=key_frame", "-of", "csv=p=0", path],
                         capture_output=True, text=True, check=True).stdout
    return [line.strip().rstrip(",") == "1" for line in out.splitlines() if line.strip()]

for name, args in LADDER.items():
    dst = f"out/rec_ladder_{name}.mp4"
    if not os.path.exists(dst):
        subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", "rec.mp4", "-an", "-c:v", "libx264",
                        "-preset", "medium", "-pix_fmt", "yuv420p", "-crf", "23", *args, dst], check=True)
    flags, since, cost = keyflags(dst), 0, 0
    for k in flags:
        since = 1 if k else since + 1
        cost += since
    print(f"{name:>4}  keyframes={sum(flags):>4}  bytes={os.path.getsize(dst):>11,}  avg_decode={cost / len(flags):7.1f}")
ini 复制代码
  1s  keyframes= 240  bytes= 34,550,226  avg_decode=   15.5
  2s  keyframes= 120  bytes= 18,065,145  avg_decode=   30.5
  5s  keyframes=  48  bytes=  7,835,468  avg_decode=   75.5
 10s  keyframes=  24  bytes=  4,402,121  avg_decode=  150.5
 30s  keyframes=   8  bytes=  2,167,967  avg_decode=  450.5
 inf  keyframes=   1  bytes=  1,299,050  avg_decode= 3600.5

代码说明:-g 按帧数设关键帧的最大间隔,30 fps 下 30、60、150、300、900 对应 1、2、5、10、30 秒。不限用的是 keyint=infinite,全片只在开头有一个关键帧。avg_decode 是一个代理指标:对每一帧数一数从前面最近的关键帧解到这一帧要解几帧,再对全片取平均。它按显示顺序近似而没管 B 帧实际的解码顺序,只用来看跳转代价的量级。已经编过的档位脚本会直接跳过。

六档的体积从 34,550,226 字节一路降到 1,299,050 字节。1 秒到 2 秒少了将近一半。2 秒到 10 秒又少了 75.6%。10 秒以后还在降,但每一步省下的绝对字节越来越少:10 秒到 30 秒少了约 2.2 MB,30 秒到不限只少了约 0.9 MB。曲线的形状可以直接从上一章算出来。录屏每少一个关键帧就少一个十几万字节的包。换进来的是几十个只有百来字节的 P 帧。x264 默认的 250 帧在 30 fps 下是 8.3 秒。默认档 5,222,157 字节正好落在 5 秒和 10 秒两档之间。

这张图要看两条线从 1 秒档往右怎么分开。蓝线是录屏的体积并以 1 秒档为 100%。到 2 秒它就掉到 52%。10 秒是 13%。不限只剩 4%。橙线是摄像头 s4。它从 100% 走到 94% 和 90%,10 秒以后一直平在 89%。灰柱用右边的对数刻度,表示录屏跳到任意位置平均要解多少帧。它从 16 一路涨到 3600。摄像头曲线对应的两段视频来自维基共享资源,CC BY-SA,经重新编码。

4.2 同样拉长摄像头视频只省一成

录屏和摄像头要放在同一个脚本里比,参数才不会有出入。下面这段对两类素材跑同一组改动,每一行只改一项,打印字节和相对默认档的变化;最后一行单独算关键帧 1 秒到 10 秒的变化。

python 复制代码
import os, subprocess, sys

SRC = {"rec": ("rec.mp4", 30), "cam": ("cam.webm", 25)}
X264 = ["-c:v", "libx264", "-preset", "medium", "-pix_fmt", "yuv420p", "-an"]

def cases(fps):
    return {
        "default":    ["-crf", "23"],
        "gop_1s":     ["-crf", "23", "-g", str(fps)],
        "gop_10s":    ["-crf", "23", "-g", str(fps * 10)],
        "crf28":      ["-crf", "28"],
        "stillimage": ["-crf", "23", "-tune", "stillimage"],
        "animation":  ["-crf", "23", "-tune", "animation"],
    }

def encode(tag, name, src, args):
    dst = f"out/{tag}_{name}.mp4"
    subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", src, *X264, *args, dst], check=True)
    return os.path.getsize(dst)

size = {}
for tag, (src, fps) in SRC.items():
    for name, args in cases(fps).items():
        size[tag, name] = encode(tag, name, src, args)

pct = lambda a, b: f"{(a / b - 1) * 100:+.1f}%"
print(f"{'case':<11} {'rec bytes':>11} {'rec':>7} {'cam bytes':>11} {'cam':>7}")
for name in cases(30):
    r, c = size["rec", name], size["cam", name]
    print(f"{name:<11} {r:>11,} {pct(r, size['rec', 'default']):>7} {c:>11,} {pct(c, size['cam', 'default']):>7}")
print("gop 1s -> 10s:",
      "rec", pct(size["rec", "gop_10s"], size["rec", "gop_1s"]),
      "cam", pct(size["cam", "gop_10s"], size["cam", "gop_1s"]))
erlang 复制代码
case          rec bytes     rec   cam bytes     cam
default       5,222,157   +0.0%  18,202,432   +0.0%
gop_1s       34,550,226 +561.6%  20,402,885  +12.1%
gop_10s       4,402,121  -15.7%  18,202,432   +0.0%
crf28         4,326,262  -17.2%  10,311,046  -43.4%
stillimage    5,099,781   -2.3%  22,122,711  +21.5%
animation     5,492,582   +5.2%  16,939,187   -6.9%
gop 1s -> 10s: rec -87.3% cam -10.8%

代码说明:两类素材共用 X264 那一串基础参数,区别只在帧率(录屏 30 fps、摄像头 25 fps)。关键帧间隔按各自的帧率换算成秒。cases 里每一项都在 CRF 23 的基础上只加一个改动。变化一律相对各自的 default 算。最后一行另外拿 10 秒档去比 1 秒档。摄像头那一列 gop_10s 和 default 字节完全相同。x264 默认的 250 帧在 25 fps 下刚好就是 10 秒,这两行编出来是同一个文件。

摄像头 s4 从 1 秒拉到 10 秒,体积从 20,402,885 字节降到 18,202,432 字节,只小了 10.8%。从 2 秒拉到 10 秒只小 5.0%。再拉到不限也只比默认档小 0.3%。s1 那段在 10 秒档是 1 秒档的 72%,比 s4 省得多一些。可它全片只有 7.87 秒,10 秒档已经等于全片一个关键帧。这个数偏乐观。两段摄像头视频的共同点是一样的:关键帧间隔过了 5 秒再往上拉几乎就不省了。

同一张输出里录屏那一列完全是另一番样子:1 秒档比默认档大了 561.6%。如果有人给所有视频统一设 1 秒一个关键帧,录屏会变成默认体积的六倍多,摄像头视频同样的设置只大 12.1%。这两个数放在一起基本就是这篇想说的事。

4.3 关键帧越稀跳转要解的帧越多

体积不是白省的。平均需解码帧数从 1 秒档的 15.5 涨到 10 秒档的 150.5,30 秒档是 450.5,不限档是 3,600.5。不限档等于跳到片中任何位置,平均都要从片头解 3,600 帧。

另一组数是单线程精确跳转的耗时。测法是在全片 30 个固定的随机时间点上各跳 3 次取中位。1 秒档 51.9 毫秒、2 秒档 62.2 毫秒、5 秒档 98.0 毫秒、10 秒档 163.4 毫秒、30 秒档 369.1 毫秒。不限档 2,623.5 毫秒,最慢的一次 4,867.7 毫秒。默认档的 250 帧落在中间:平均需解码 124.8 帧,跳转中位 111.9 毫秒。单独跳到第 200 秒解出一帧时 2 秒档和 10 秒档都是 0.04 秒。不限档是 4.07 秒。这组毫秒数只能看相对大小,第七章会说为什么。

从 2 秒到 10 秒体积少了四分之三。跳转中位从 62.2 毫秒涨到 163.4 毫秒,大约 2.6 倍。从 10 秒到不限体积再少 70% 出头。跳转却从 163.4 毫秒涨到 2.6 秒,翻了十几倍。曲线两头的斜率到这里完全不对称了。取舍点大概就落在这一段。

4.4 文字清晰度不随关键帧间隔变化

拉长间隔之前要先排除一个问题:关键帧变少会不会让画质变差。这次在第 60 秒那一帧裁出同一块英文小字和无损的原始画面比 SSIM。三档依次是 0.9686(1 秒)、0.9689(10 秒)、0.9677(不限),差在千分位。全片 SSIM(对原文件)也是同样的结论:1 秒档 0.99932、默认档 0.99934、不限档 0.99899。

画质由 CRF 管,关键帧间隔在这段样本上只管体积和跳转。不限档的全片 SSIM 低了一点点,可能和长距离预测累积的误差有关,这是推断。差值在小数点后第四位而且文字区的指标没跟着掉,这次就不往下追了。

4.5 录屏档的关键帧放在10秒左右

把 4.1 到 4.4 的数放在一起,录屏档我会选 10 秒左右。10 秒档 4,402,121 字节,比 2 秒档小 75.6%。文字指标不变。跳转要解的帧数是 150,单线程跳转中位 163.4 毫秒。往 30 秒拉还能再省约 2.2 MB。代价是要解的帧数变成三倍,跳转中位到 369.1 毫秒。一门要来回拖着看的课程我不太愿意付这个代价。不限档只适合从头看到尾、从不拖动的场景。要拖动回看的课程不该用它。

现成的转码软件里也能看到这个取值。HandBrake 内置预设 Fast 1080p30 用的是 RF 22 和 300 帧的关键帧间隔,30 fps 下正好是 10 秒。用它压这段录屏得到 6,006,940 字节,英文小字 SSIM 0.969,原文件自己是 0.971。

回到章标题里那个问题:关键帧间隔越大越好吗。体积上是,跳转上不是。录屏的拐点在 10 秒附近,摄像头视频过了 5 秒就没什么可省的了。如果服务端还要切 HLS 或 DASH,关键帧间隔还得和切片时长对齐,这一条放在第八章。

五、CRF和tune为什么在两类素材上反着走

5.1 调高CRF对录屏省得不多

CRF 从 23 调到 28 时录屏体积从 5,222,157 字节降到 4,326,262 字节,只小了 17.2%。全片 SSIM 从 0.99934 掉到 0.99870。和 4.5 节的 10 秒档相比两个文件差不多大(4,326,262 对 4,402,121)。可 10 秒档的画质指标完全没动。在录屏上省下同样一截体积时拉长关键帧不碰画质。调高 CRF 却要拿画质去换。

调 CRF 在录屏上为什么省得少,下面是我的理解,属于推断。录屏的字节将近八成在关键帧上。关键帧的内容是大面积纯色背景加锐利的文字边缘。纯色背景本来就没多少系数可省。文字边缘的高频系数一被拿掉就会出现振铃和发虚,x264 的心理视觉优化还会尽量保着这部分。静止的 P 帧和 B 帧只剩几十字节的跳过标记。量化粗一档还是细一档对它们几乎没有影响。

再往高调就开始伤字。另一组单测从无损母版截了第 2 页的 20 秒静止画面来编码。黑色代码区的 SSIM 在 CRF 33 以后掉得明显加快:CRF 33 是 0.9967,CRF 43 是 0.9619。存储量可以按 100 小时课程线性外推,这一步是推断:CRF 18 约 9.4 GB、CRF 23 默认关键帧约 7.8 GB、CRF 28 约 6.5 GB、CRF 33 约 5.2 GB;关键帧 10 秒约 6.6 GB、30 秒约 3.3 GB。录屏想省存储就先动关键帧,CRF 不必往低了压,也不必往高了推。

5.2 摄像头视频对CRF敏感得多

同样从 CRF 23 调到 28,s4 从 18,202,432 字节降到 10,311,046 字节,小了 43.4%,s1 小了 50.4%。代价是 s4 的全片 SSIM 从 0.98892 掉到 0.98196。

原因和第三章接得上,也是推断。摄像头视频的大头在 P 帧的残差里。CRF 决定这些残差量化得多粗,量化放粗一档每一个 P 帧都跟着变小。录屏的 P 帧只有跳过标记,没有残差可量化。同一个旋钮在一类素材上连着主要的字节,在另一类素材上连着的几乎是空的。摄像头视频要控体积就该动 CRF 和分辨率。

5.3 给摄像头视频加stillimage反而变大

「录屏加 -tune stillimage 能小一半」这个判断在这次的数据里不成立。stillimage 让录屏从 5,222,157 字节变成 5,099,781 字节,只小了 2.3%。同一个参数放到摄像头 s4 上,体积从 18,202,432 字节涨到 22,122,711 字节,大了 21.5%。s1 也大了 12.8%。s4 的全片 SSIM 从 0.98892 掉到 0.98805。体积变大的同时画质还略差了一点。录屏这边 SSIM 从 0.99934 到 0.99938,几乎没变。

tune 改的是一组默认值。按 x264 的帮助文档,stillimage 和 animation 调的是去块滤波、自适应量化和心理视觉优化这几项,animation 还会多给 B 帧和参考帧。具体是哪一项让摄像头视频变大这次没有拆开单测。animation 在录屏上大了 5.2%,在 s4 上小了 6.9%,在 s1 上只小 0.5%。两个 tune 在这两类素材上都谈不上稳定的收益。

在录屏上单改一个参数就能小一半的,这次只找到关键帧间隔。拉到 30 秒(-g 900)比默认档小 58.5%,全片一个关键帧小 75.1%。preset 换成 slow 反而大了 2.1%,veryslow 小 3.6%,这两项只在录屏上测过。

5.4 录屏降帧率省下的其实是关键帧

降帧率是录屏转码里另一个常被提起的办法。下面这段把录屏降到 15 fps 编两次:一次保留默认的 250 帧关键帧间隔,一次按秒对齐成 125 帧。

bash 复制代码
for g in 250 125; do
  ffmpeg -v error -y -i rec.mp4 -an -c:v libx264 -preset medium -pix_fmt yuv420p -crf 23 -r 15 -g $g out/rec_15fps_g$g.mp4
  printf '15 fps  -g %-4s %11s bytes\n' $g "$(stat -f %z out/rec_15fps_g$g.mp4)"
done
python 复制代码
15 fps  -g 250      3124752 bytes
15 fps  -g 125      5310807 bytes

代码说明:-r 15 把输出帧率降到 15。x264 的关键帧间隔按帧数算,250 帧在 30 fps 下是 8.3 秒,到了 15 fps 就变成 16.7 秒;125 帧在 15 fps 下才回到 8.3 秒,和 30 fps 默认档的时长对齐。stat -f %z 是 macOS 上取文件字节数的写法,Linux 上换成 stat -c %s。

只看第一行的话降到 15 fps 比 30 fps 默认档的 5,222,157 字节小了 40.2%。这看上去很划算。把关键帧间隔按秒对齐以后变成 5,310,807 字节,反而大了 1.7%。降到 10 fps 也一样:表面小 57.4%,对齐后大 3.0%。省下来的几乎全是关键帧。帧率本身没省什么。

对齐以后反而略大,我的推断是这样。静止帧本来只有几十字节,删掉一半也省不了几个字节。帧距拉大以后鼠标、打字、滚动那几段的相邻两帧差得更多,预测变贵了。这一条的前提是画面大部分时间静止,摄像头视频的降帧这次没测。第 60 秒是静止帧,降帧前后文字指标没变。鼠标和滚动的流畅度没有量化。

5.5 录屏降到720p会先伤到小字

降分辨率在录屏上是一笔亏本买卖。1080p 降到 720p 时体积从 5,222,157 字节降到 3,635,482 字节,小了 30.4%。代价是英文小字 SSIM 从 0.970 掉到 0.909、讲义小字从 0.998 掉到 0.880。放大看能看出发虚。macOS 自带 avconvert 的 720p 预设也是这样,讲义小字 SSIM 只有 0.849。同样是想省 30% 左右,关键帧从默认拉到 10 秒就能拿到其中一半,而且字一点不糊。

换编码也不是录屏档的主要手段。x265 用 CRF 28 时文字 SSIM 和 x264 CRF 23 持平,体积只小了 3.1%。x265 直接用 CRF 23 反而大了 16.0%。摄像头视频的 720p 和 x265 这次都没测,录屏的数不能推到摄像头视频上。

六、两个转码档具体怎么设

6.1 录屏档保留原分辨率只拉长关键帧

录屏档的参数是 x264 medium、CRF 23、yuv420p、不加 tune。分辨率和帧率保持原样。关键帧间隔设成帧率乘 10。在这段样本上它的输出就是 4.1 节的 10 秒档:4,402,121 字节,比 360.0 MB 的原文件小 98.8%。按 100 小时课程线性外推大约 6.6 GB。原录屏同样外推是 540 GB。外推的前提是课程画面和这段样本一样大部分静止。真实课程有讲师画中画、动画和视频播放时会大很多。单价按各自的存储价格去乘。

几种现成工具在同一段样本上的结果可以放在旁边对照。HandBrake 的 Fast 1080p30 预设是 6,006,940 字节,文字指标和 x264 默认档同档。图映 ImgIng 智能推荐档输出的 H.264 是 20,167,573 字节,英文小字 SSIM 0.969,文字指标与 HandBrake 持平,体积约为它的 3.4 倍。同一档选 H.265 输出是 30,519,188 字节,在这段录屏上比 H.264 还大。图映的视频压缩在电脑浏览器里本地处理,支持批量和 H.265。实测那几轮压缩里没看到上传请求。放在这里只是想提醒一件事:同样的文字清晰度下不同工具的体积能差到三倍多。差在哪一项参数要拿产物自己去量。

6.2 摄像头档靠CRF控制体积

摄像头档用同一个 x264 medium 和 CRF 23,同样不加 tune,关键帧间隔留在 2 到 5 秒。这个区间是我对数据的读法:s4 的 2 秒档 19,159,323 字节、5 秒档 18,319,474 字节、10 秒档 18,202,432 字节,从 2 秒拉到 10 秒只小 5.0%,5 秒以后几乎不省了。既然拉长不省就把关键帧留密一点让拖动更快。

摄像头视频要控体积就动 CRF 和分辨率。CRF 23 到 28 能省 43.4%,画质损失也是实打实的。能接受到哪一档要看业务。stillimage 不用加,它会让 s4 大 21.5%。animation 在 s4 上能省 6.9%、在 s1 上只省 0.5%。收益不稳定,我也不会放进默认档。摄像头视频的降分辨率这次没测。具体降到哪一档要自己补一组数据。

6.3 两档只差关键帧间隔一项

分档时最顺手的做法是给录屏档配上 stillimage、给摄像头档配上 animation,每一档都调到最优。按这次的数据不建议这么做。stillimage 在录屏上只省 2.3%。一旦摄像头视频被误判进录屏档,体积就多出 21.5%。录屏的 CRF 往上调能省 17.2%。可拉长关键帧能省到差不多的体积,还不碰画质。在录屏这一侧这两个参数的收益都抵不过它们带来的风险。

只差关键帧间隔一项以后,判错的代价可以直接算出来。录屏误进摄像头档时按 2 秒关键帧编出来是 18,065,145 字节。是录屏档 4,402,121 字节的约 4.1 倍。但它只有原文件的 5% 左右,画质也不受影响。摄像头视频误进录屏档时按 10 秒关键帧编出来是 18,202,432 字节。这比 2 秒档还小一点。代价只是拖动时要多解一些帧。两头都不伤画质。最坏的结果是少省一些存储或者拖动慢一点。

判错的代价这么小,素材分类就可以做得很粗。比如按上传入口打标签:录屏工具和会议软件导出的走录屏档,手机拍摄和相机导入的走摄像头档,分不清的先按摄像头档处理。这是我根据上面的数字做的设计判断,这套分类规则本身没有在真实上传流量上验证过。

6.4 彩色小字要另看色度采样

还有一类问题不在分档里,是课件里的彩色小字。从无损母版截一段第 2 页的静止画面按 4:2:0 编码,红色小字的 SSIM 在 CRF 23 时只有 0.924,CRF 18 也只有 0.926。改成 4:4:4 后同样 CRF 23 到了 0.988,体积只多了 2.0%(473,123 字节到 482,793 字节)。黑色代码区换不换 4:4:4 分数基本不动。

这说明彩色小字先糊的主因是 4:2:0 把颜色分辨率横竖各砍了一半,1 到 2 像素宽的彩色笔画被背景色平均掉了,降 CRF 救不回来。这一项没有放进录屏档,原因有两个。第一个是播放端对 4:4:4 H.264 的支持这次没测。第二个是它只对还没被 4:2:0 压过的源有用:拿已经是 4:2:0 的模拟录屏原文件转 4:4:4,英文小字 SSIM 只从 0.9696 到 0.9716。录屏软件输出的如果已经是 4:2:0 这一项就帮不上忙,它只对能拿到无损或 4:4:4 源的场景有意义。

七、测量方法自身有哪些坑

7.1 关键帧间隔要按秒写进配置

x264 的关键帧间隔按帧数算,同一个数字在不同帧率下是不同的秒数:默认的 250 帧在 30 fps 下是 8.3 秒,在 25 fps 下是 10 秒。4.2 节的输出里摄像头的 gop_10s 和 default 编出来字节完全相同,就是这个原因。拿「默认档」去比两类素材,比的其实是两个不同的关键帧间隔。

5.4 节的降帧率是同一个坑的另一面。帧率减半而帧数不变的时候关键帧间隔在时间上就翻了倍,体积的变化全被算到了帧率头上。转码服务的配置里可以把关键帧间隔写成秒,编码前再按源帧率换算成帧数。配置项的名字也直接带上秒。这样下一个人改帧率时不会顺带把关键帧间隔也改掉。

7.2 整帧SSIM会被静止背景冲淡

录屏的大部分像素是纯色背景,整帧 SSIM 很容易很高。CRF 28 的全片 SSIM 是 0.99870,看上去几乎无损;从无损母版截出来单测的那段画面里,黑色代码区在 CRF 33 以后就开始明显掉分。4.4 节里文字区的 SSIM 在 0.968 上下,整帧却在 0.999 以上,两者离 1 的距离差了几十倍。

判断录屏的画质要裁出同一块文字区域和原始画面比。这次的做法是在第 60 秒那一帧裁出英文小字、讲义小字、代码块三块分别算 SSIM,低于 1080p 的输出先放大回 1920×1080 再比,因为观看时也是放大看的。只看整帧 SSIM 的话,5.5 节里 720p 把小字弄糊这件事是看不出来的。

7.3 跳转耗时只能看相对大小

4.3 节的毫秒数是用 ffmpeg 单线程做输入端跳转测出来的:先跳到目标时间之前最近的关键帧,再一帧一帧解到目标时间。播放器有多线程解码和缓冲,有的还会先显示最近的关键帧再补画面,真实拖动的体验和这个数不是一回事。

平均需解码帧数也是近似。它按显示顺序数帧,没管 B 帧的解码顺序和每一帧解码的轻重,静止帧解起来很快,翻页附近的帧要慢一些。两个指标都只适合比较档位之间的相对差距,拿它们回答「拖一下要等多久」是不合适的。

7.4 帧包字节和文件字节要分开记

3.1 节的录屏默认档帧包合计 5,135,679 字节,文件 5,222,157 字节,中间差的是 MP4 的容器开销,索引之类的都算在里面。按帧类型算占比时分母要用帧包合计,算压缩率时要用文件字节。两个口径混在一张表里会差出一点几个百分点。

原片那边也有类似的问题:WebM 的视频流读不到码率,只能拿整个文件算,里面混着音轨。这次所有转码都去掉了音轨,比较的是纯视频。真实服务里音轨要单独算一份,课程录屏里音轨占多大比例这次没测。

7.5 模拟样本会把录屏的收益估高

这段样本是截图合成的,静止段每一帧和上一帧逐像素相同。真实录屏会有光标闪烁、时钟跳动和通知弹窗,屏幕采集本身也可能带来细微的像素抖动。这些都会让静止帧不再只有几十字节。关键帧占的比例会降下来,拉长关键帧间隔的收益也会跟着变小。

能确定的只是方向,幅度定不下来。这段样本里关键帧占 79.4%,真实录屏会低多少,这次没有真实样本去量。要把 10 秒这个取值搬到自己的服务里,最好先拿自己的录屏跑一遍 3.1 节那段统计,看看关键帧占比是不是也压倒性地高。

八、适用边界与风险提示

8.1 数字只代表这段样本和x264 medium

所有体积和画质指标都来自这一段模拟录屏和两段摄像头视频,编码器是 x264 preset medium,每档单次。x264 在同一台机器、同样参数下的输出可以逐字节复现。这次重跑的字节和首轮完全一致。换一台机器、换一个 x264 版本或者换一个 preset,数字都会变;方向变不变没有验证。

摄像头一侧只有两段 1080p 的 VP9 原片,一段 30 秒,一段不到 8 秒。手机原片、HEVC 原片、4K 和带剧烈运动的素材都没测。录屏一侧只有一段 4 分钟的样本,课件加讲义加代码这种组合也只是常见课程的一种。

8.2 长关键帧会拖慢拖动和切片

关键帧拉到 10 秒,拖动时比 2 秒档平均多解 120 帧,跳转中位从 62.2 毫秒涨到 163.4 毫秒。点播课程可以接受。要频繁精确定位的场景就得再权衡,比如按时间戳跳到某一行代码的讲解。不限档的跳转中位 2.6 秒、最慢一次接近 4.9 秒,不该用在任何要拖动的场景。

服务端如果还要切 HLS 或 DASH,每个切片一般要以关键帧开头,关键帧间隔就不能大于切片时长。切片按 6 秒切的话 10 秒的关键帧间隔会和切片冲突。这一条是推断,这次没有测切片。要用长关键帧的话切片时长和关键帧间隔得一起定。

8.3 播放端兼容性没有测

4:4:4 的 H.264 在浏览器和手机播放器上能不能正常播放这次没测,6.4 节的建议只能停在「先确认播放端支持」。关键帧不限这一档在各种播放器上拖动的实际表现也没测,这里只有 ffmpeg 单线程解码的代理数字。

还有几项也没覆盖:60 fps 的录屏、Retina 分辨率的录屏、带音轨的录屏,以及 Mac 上用 QuickTime 或照片 App 的图形界面导出。真实的系统录屏这次也没录,第九章接着讲。

九、还没解决的问题

9.1 真实系统录屏是不是恒定码率没有测

整篇的前提是录屏原片按恒定码率写盘。2.1 节那个硬件编码的反证说明,换成不强制恒定的码率控制,原片本身就已经很小。真实的 macOS 截屏工具、QuickTime、Windows 截图工具和 Xbox Game Bar 用的是哪一种,帮助页里查不到,这次也没有实录。

如果真实录屏本来就是可变码率,录屏档相对原片能省的比例会小很多。但关键帧间隔对录屏敏感这件事不依赖原片怎么编码,它取决于画面大部分静止,重编码的时候应该照样成立。这一句是推断。哪种原片更常见需要拿几款录屏软件的真实输出去补。

9.2 带画中画的课程归哪一档还没有答案

很多网课是课件录屏加右下角一个讲师的摄像头小窗。小窗里每一帧都在变,带着残差。整帧的 P 帧会比纯录屏大得多,关键帧占比会往下走。它更像录屏还是更像摄像头视频取决于小窗占画面的比例,这次没有这种样本。

按 6.3 节的思路判错的代价不对称。分不清的时候按摄像头档处理最稳妥。可这样一来画中画课程可能少省了不少。小窗只占画面一小角的话我猜它还是更接近录屏,但手里没有数据。阈值该划在哪我现在给不出数字。

参考资料

  1. x264 帮助文档(x264 --fullhelp):--keyint、--scenecut、--tune 各预设所改参数的说明。
  2. FFmpeg 文档:ffprobe 的 -show_entries、-select_streams 与 compact 输出格式;ssim 滤镜。
  3. HandBrake 文档:内置预设 Fast 1080p30 的参数说明。
  4. Apple 支持:《在 Mac 上截屏或录屏》《使用 QuickTime Player 录制屏幕》(2026-11-09 查阅)。
  5. Xbox 支持:《在 Windows 上调整捕获设置》(2026-11-09 查阅)。
  6. 维基共享资源:Demonstration of Ethiopian residents in Israel, July 2019.webm(作者 Israel Police,CC BY-SA 3.0);Moose by the road.webm(作者 Forza1000,CC BY-SA 4.0)。均经重新编码并已改动。
相关推荐
补码缩写12 分钟前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李14 分钟前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易28 分钟前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马39 分钟前
前沿视界
前端
kyriewen43 分钟前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易1 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员
IT_陈寒1 小时前
Vue的这个响应式陷阱,差点让我加班到天亮
前端·人工智能·后端
Flynt2 小时前
别写拖拽行为,只声明"能拖到哪":UIST 最佳论文的拖拽方案我实测了
前端·svg·preact
Moment3 小时前
代码都能让 Agent 写了,我们为什么还要学前端?
前端·后端·面试