用 ffprobe 拆录屏,八成字节在 29 个关键帧上

一段 4 分钟的录屏转码前是 360.0 MB。用 x264 按 CRF 23 转一遍只剩 5.2 MB,差了将近 70 倍,文字清晰度基本没动。把两份文件的帧逐个列出来,扔掉的几乎全是「画面没变也照样写满」的那部分字节。下面是三条 ffprobe 命令:按帧类型统计帧数和字节、只列关键帧、再盯住一小段静止画面看每帧多大。命令都在 2026-11-09 跑过,输出都原样贴出来没有删改。

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

样本是自制的 4 分钟模拟网课录屏。内容有课件翻页、鼠标移动、打字和讲义滚动。规格是 1920×1080、30 fps、12 Mbps 恒定码率,文件 360.0 MB 且没有音轨。它不是录屏软件录的,而是把自写的课件页面逐帧截图再合成出来的一段视频。画面比真实屏幕干净得多,约 78% 的时间完全静止。这意味着它比真实录屏更好压,后面读每个数字时都得记着这一点。12 Mbps 也只是样本设定的假设,Apple 和微软都没公布系统录屏的默认码率。跑测试的是 Apple M4 芯片的 Mac。工具是 ffmpeg / ffprobe 9.0.2 和 x264 preset medium。每个数都只跑了一次。不开命令行时我用的是图映 ImgIng imging.cn/ 的视频压缩,它在电脑浏览器里本地处理,支持批量和 H.265。同一段样本用它的智能推荐 H.264 档出来是 20.2 MB,小字清晰度和 x264 这份同档,体积约是 HandBrake 默认预设的 3 倍。更要紧的是这几次实测压缩时没有上传请求。公司里的录屏多少带点内部资料,能不出电脑就不出。下面的帧统计都只针对 x264 按 CRF 23 转出来的那一份文件。

原文件连静止画面都按满码率写

原文件用 ffprobe 读出来的码率是 11,995,489 bps。时长 240 秒,一共 359,952,205 字节,和生成时的设定对得上。把逐帧输出的时间戳对照时间轴分组,按时间段拆开帧大小。静止段的非关键帧每帧中位数是 50,002 字节,和打字段、滚动段差不多大。这些静止时段占了整个原文件 71.1% 的字节数。恒定码率就是这么记账的。编码器每秒都要交够 12 Mbps,画面不动的时候也得拿填充数据把这笔账补齐。同一段画面交给 x264 按 CRF 23 重编,静止段的非关键帧中位数降到 77 字节。

这里有个反例得先放着。同一段无损母版交给本机硬件编码器,目标码率也设成 12 Mbps 但不强制恒定。出来的文件平均只有约 0.79 Mbps。所以录屏大不大,要看录的时候怎么编码。大不大要看录屏软件是按固定码率写盘,还是按画面变化分配。真实的系统录屏走哪种,这次没测。

按帧类型统计时先认准列顺序

转码后的文件用下面这条命令按帧类型汇总。pkt_size 是这一帧压缩后数据包的字节数。pict_type 是 I、P、B 三种帧类型。后面的 awk 负责按类型累加帧数和字节再算出占比和平均值。

bash 复制代码
ffprobe -v error -select_streams v:0 -show_entries frame=pict_type,pkt_size -of csv=p=0 input.mp4 \
 | awk -F, '{n[$2]++; s[$2]+=$1; t+=$1} END {for (k in n) printf "%s  frames=%d  bytes=%d  share=%.1f%%  avg=%d\n", k, n[k], s[k], s[k]*100/t, s[k]/n[k]}' | sort
B  frames=5253  bytes=502079  share=9.8%  avg=95
I  frames=29  bytes=4076157  share=79.4%  avg=140557
P  frames=1918  bytes=557443  share=10.9%  avg=290

输出每行一种帧类型:frames 是帧数,bytes 是这类帧的字节合计,share 是占全部视频流字节的比例,avg 是平均每帧字节。sort 只是让 B、I、P 按字母排好,看的时候先找 I 那一行的 share。

注意 awk 里 $1 当字节、$2 当类型。这和命令里写的 pict_type,pkt_size 正好反着。ffprobe 的 csv 按它内部的字段顺序输出,不管你在命令里怎么排。后面看静止段那条命令也能印证。照着命令顺序去写 $1 的话 awk 不会报错。它只会把字节数当成类型名去分组,输出的结果看着还挺像回事。三行字节数加起来是 5,135,679。这正好是这份文件视频流的大小,拿来自检很方便。这条命令要把每一帧都解出来才读得到帧类型,片子越长跑得越久,长录屏建议先截一段试。

近八成字节落在关键帧上

7,200 帧里只有 29 个 I 帧。它们平均每个 140,557 字节,加起来占了全片 79.4% 的字节。剩下 7,171 个 P 帧和 B 帧合计只占两成出头,其中 B 帧平均 95 字节。P 帧平均 290 字节,比静止帧高,推测是被翻页和滚动那些帧拉高的。同一条命令跑摄像头视频,分布完全是另一个样子:

素材 I 帧字节占比 P 帧字节占比 B 帧字节占比
模拟网课录屏(7,200 帧) 79.4% 10.9% 9.8%
摄像头视频 A(745 帧) 4.5% 59.4% 36.1%
摄像头视频 B(236 帧) 6.0% 77.7% 16.3%

摄像头视频 A 的 P 帧平均 48,886 字节。这是录屏 P 帧的一百多倍。录屏和摄像头视频虽然都叫视频,字节却花在完全不同的帧上。同一套转码参数套在两种素材上得到的收益不会一样。两段摄像头对照视频来自维基共享资源的 CC BY-SA 视频,经重新编码后只拿来做帧类型统计。

接下来这一段是推断,没有单独做实验验证。P 帧和 B 帧编的是「和参考帧差在哪」。录屏静止时绝大多数宏块和参考帧一模一样。编码器大概率走跳过模式,只用几个比特说明「沿用参考帧、没有残差」。摄像头就算架着不动,传感器噪点和细小晃动也会让每个宏块带点残差。字节自然都压在 P 帧上。录屏的钱几乎全花在定期整屏重画一遍的关键帧上。想验证这个推断可以去看编码器统计里跳过宏块所占的比例,这一步本次没做。

翻页没有触发新的关键帧

想知道关键帧落在哪,就在第一条命令的基础上加 -skip_frame nokey 让 ffprobe 跳过非关键帧。输出只要 pts_time 和 pkt_size 两列,再接一个 head -5。前五行输出是 0 秒 109,061 字节,往后 8.333333 秒 123,468、16.666667 秒 123,420、25 秒 123,497、33.333333 秒 123,474。间隔整整 8.33 秒。换成帧数是 250 帧,正是 x264 默认的关键帧间隔。想数全片有几个关键帧,把 head -5 换成 wc -l 就行,这份文件数出来应当和第一条统计里 I 帧的 29 对得上。第一行末尾还多了一个逗号,写成「0.000000,109061,」。后面几行都没有这个多出来的逗号,原因没查。用 awk 按逗号切的时候第一行会多出一个空字段,好在不影响取前两列。

翻页发生在第 40、120、180 秒。x264 默认开着场景切换检测,画面突变时可以插一个关键帧。但全片统计下来这三处一个新 I 帧都没有,新页面是用 P 帧编进去的。翻页前后那几个 P 帧大约 22~36 KB,只有关键帧的四分之一到六分之一。为什么没判成场景切换?推断是只有左边的课件区变了。右侧讲义、编辑区和菜单栏都没动,帧间预测仍然划算。这个推断没有用整屏翻页的样本验证过。

静止段的帧只有几十到几百字节

最后挑一段静止画面放大看。-read_intervals 50%+0.3 表示从第 50 秒开始读 0.3 秒,比把整段都解一遍再用 awk 过滤快得多。输出里的 pts_time 是这一帧的显示时间,单位秒,30 fps 下相邻两帧差 0.033 秒,0.3 秒正好九帧。第 40 到 70 秒这段课件没有任何变化,第 50 秒落在这个静止区间里。这 0.3 秒前后都没有鼠标移动或打字。

bash 复制代码
ffprobe -v error -select_streams v:0 -read_intervals 50%+0.3 -show_entries frame=pts_time,pict_type,pkt_size -of csv=p=0 input.mp4
50.000000,141595,I
50.033333,212,B
50.066667,230,B
50.100000,75,B
50.133333,1221,P
50.166667,84,B
50.200000,92,B
50.233333,69,B
50.266667,168,P

第 50 秒恰好是第 7 个关键帧,141,595 字节,比开头那几个 12 万字节上下的关键帧大,这时已经翻到第 2 页,整屏重画的内容不同,成本也跟着变。列顺序又是 pts_time,pkt_size,pict_type,跟上面那条对上了。帧类型的排列是 B、B、B、P 一组一组往下走,P 帧之间夹着三个 B 帧,这和 x264 默认最多连续 3 个 B 帧的设置一致。后面八帧全在几十到一千多字节之间,最小的一帧只有 69 字节。紧跟关键帧的第一个 P 帧有 1,221 字节,是后面那个 P 帧的七倍多。画面明明没变,这个差别从哪来本次没查清。九帧的样本太小,单看它很容易误判出一个并不存在的规律来。按时间段把全片非关键帧拆开更稳:

时间段 帧数 每帧中位数 每帧平均
静止段 5,483 77 B 85 B
鼠标移动 592 98 B 115 B
打字 657 80 B 107 B
讲义滚动 296 906 B 1,211 B

鼠标移动和打字几乎不比静止画面贵多少。讲义滚动贵了十来倍,整块讲义区域都在位移。

拿自己的录屏先跑这三条

这三条命令只读本地文件的帧信息和包大小,在公司电脑上跑也不涉及任何文件外发。一段要入库的录屏,可以先对原文件按时间段看帧大小。静止画面每帧也有几万字节的话,多半是按固定码率录的,这种重编一遍基本稳赚。

转码之后再跑第一条统计看 I 帧占比,原文件和转码后的文件各跑一遍就能对比出差别。占比像这段样本一样接近八成时,能动的就是关键帧间隔,代价是拖动进度条时要解的帧变多,间隔该按播放端能接受的拖动延迟去定,不能只看体积。要是 P 帧占大头,它更像摄像头视频,调关键帧没多少油水,该去看 CRF。还有一件事没解决:这段样本比真实录屏干净太多,真实屏幕有抗锯齿抖动,Mac 录屏还常是 Retina 分辨率。换成真录屏后 79.4% 推测会往下走,掉到多少本次没有数据。

相关推荐
百思不得小李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 写了,我们为什么还要学前端?
前端·后端·面试
Hilaku3 小时前
MCP 协议会成为前端 AI 工程的标准接口吗?
前端·javascript·程序员