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

0 阅读41分钟

摘要

转码服务里很常见的配置是一套参数吃所有上传的视频。这次拿一段自制的模拟网课录屏和一段摄像头视频,用同一组 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,文件名换成了占位名。

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
== 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 秒那段静止画面里非关键帧的中位大小。

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}")
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 秒到不限分成六档,各编一次录屏,每档打印关键帧个数、字节和平均需解码帧数。

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

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

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

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"]))
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 帧。

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
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)。均经重新编码并已改动。