摘要
转码服务里很常见的配置是一套参数吃所有上传的视频。这次拿一段自制的模拟网课录屏和一段摄像头视频,用同一组 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 节的思路判错的代价不对称。分不清的时候按摄像头档处理最稳妥。可这样一来画中画课程可能少省了不少。小窗只占画面一小角的话我猜它还是更接近录屏,但手里没有数据。阈值该划在哪我现在给不出数字。
参考资料
- x264 帮助文档(
x264 --fullhelp):--keyint、--scenecut、--tune各预设所改参数的说明。 - FFmpeg 文档:ffprobe 的
-show_entries、-select_streams与compact输出格式;ssim 滤镜。 - HandBrake 文档:内置预设 Fast 1080p30 的参数说明。
- Apple 支持:《在 Mac 上截屏或录屏》《使用 QuickTime Player 录制屏幕》(2026-11-09 查阅)。
- Xbox 支持:《在 Windows 上调整捕获设置》(2026-11-09 查阅)。
- 维基共享资源: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)。均经重新编码并已改动。