
环境:Raspberry Pi(CSI 摄像头 imx219,Debian 13,aarch64)· FFmpeg 7.1.5 · libcamera v0.7.2
目标:把"会抄命令"变成"看得懂命令,并且能用数据验证"。
先说结论
- 能
-c copy就不要重编码。 remux 速度约 66--72 倍实时,软编 720p30 只有 0.4--1.0 倍。 - CRF 每加 5 左右,体积大约减半。 真实摄像头画面比合成画面更敏感,CRF 28 是我这段素材上的"拐点前最优点"。
- 推流用
-b:v,不用 CRF。 硬编的码率控制很准(偏差不到 1%),SSIM 均值和同码率的 x264 基本相当。 - 我差点冤枉了硬编。 重编后第一帧有明显马赛克,我一路怀疑 I 帧预算、内容难度、码控起步;最后抽图才发现:源文件
test.mp4的第 1 帧自己就有马赛克,硬编和 x264 重编码一视同仁地继承了它。SSIM 看不出来,因为参照物自带同样的缺陷。 - SSIM 只能看趋势,而且参照本身有缺陷时它会"原谅"缺陷。 最后还是要看图。
1. 一条命令的骨架
text
ffmpeg [全局参数] [输入参数] -i 输入 [输出参数] 输出
参数跟着它后面 的文件走。内部流程是 demux → decode → filter → encode → mux,-c copy 直接跳过中间三步,所以又快又无损,代价是不能同时改分辨率、帧率、码率。
另外一个常被混淆的点:容器不等于编码。mp4、mkv 只是盒子,H.264、AAC 才是压缩方式。
2. 用 ffprobe 看清素材
先别急着转码,读一读素材本身。我手上有两份:
sample.mp4:testsrc2合成画面,720p30,10 秒(x264 默认参数)。test.mp4:树莓派摄像头硬编直出,1280x720@30,约 4.4 Mbps。
最有信息量的一项是 I 帧间隔:
| sample.mp4(x264 默认) | 摄像头硬编 | |
|---|---|---|
| I 帧间隔 | 8.33 s(keyint=250) |
2.0 s(每 60 帧) |
| 观众中途加入最坏要等 | 约 8.3 s | 约 2 s |
查看 I 帧位置的推荐写法(比解析帧类型列表更省事):
bash
ffprobe -v error -select_streams v:0 -skip_frame nokey \
-show_entries frame=pts_time -of csv=p=0 in.mp4
3. remux 与转码:差了 80 倍
| 操作 | 速度 |
|---|---|
mp4 ↔ mkv(-c copy) |
约 66--72x |
| 软编 720p30(x264 medium) | 0.8--0.99x |
在树莓派上,软编 720p30 已经跑不到实时,更别提同时做别的事。这就是后面"推流链路能不重编码就不重编码"的根据。
顺带几条转码实验的结论(输入 sample.mp4):
- 对已经压缩过的视频用同样的 CRF 重编码,只小了约 4%,还多损失一代画质。
-b:v 1M能把码率锁在 1040 kb/s,CRF 23 则是 2857 kb/s 且随画面浮动;代价是 1M 版的平均 QP 明显更高(I/P/B 为 21/33/37,对比 16.5/25/30)。- 分辨率砍到 1/4 面积,码率降到约 14%;帧率减半,码率只降约 35%,因为帧间差异变大,P/B 帧要多花比特。
- 降帧率后 GOP 的"秒数"会变:
fps=15之后keyint=250等于约 16.7 秒一个 I 帧,-g要按目标帧率换算。 - H.265 在 Pi 上不划算:体积小约 33%,但速度只有 0.38x。
4. CRF 到底选多少
我分别用合成画面和真实摄像头画面各做了一组 CRF 18/23/28/35。
合成画面 (体积相对 CRF 23):1.46x / 1.00x / 0.48x / 0.22x,SSIM 从 0.9985 掉到 0.9780。肉眼几乎看不出差别,因为画面只有色块和线条,没有细节可丢。这组素材不适合用来判断画质取舍。
真实画面 (test.mp4,原片 5227 KiB):
| CRF | 码率 | 占原片体积 | SSIM | 编码速度 | 主观画质 |
|---|---|---|---|---|---|
| 18 | 3545 kb/s | 81% | 0.9951 | 0.37x | 与 CRF 28 接近 |
| 23 | 1583 kb/s | 36% | 0.9924 | 0.50x | 未单独评 |
| 28 | 593 kb/s | 13% | 0.9890 | 0.69x | 与 CRF 18 接近 |
| 35 | 187 kb/s | 4% | 0.9826 | 0.90x | 明显较差 |
几个值得记住的点:
- 真实画面对 CRF 更敏感:CRF 18 与 35 的体积差 18.9 倍(合成画面是 6.8 倍),因为低 CRF 会连传感器噪点一起保留。
- 摄像头默认的 4.4 Mbps 很浪费:同一素材 CRF 28 软编只要 593 kb/s,我在 Mac 上对比,和 CRF 18 看不出区别。
- 软编耗时随 CRF 明显变化(10.8--26.3 s),真实 720p30 画面上常用 CRF 范围内全都跑不到实时。合成画面看不出这点(耗时几乎不变)。
- SSIM 不能单独拍板:CRF 18→28 的 SSIM 下降(0.0061)和 28→35(0.0064)几乎一样,但前者肉眼看不出,后者明显变差。
5. 硬编:码率很准,压缩效率和 x264 相当
树莓派的硬件编码器是 h264_v4l2m2m(驱动 bcm2835-codec)。同样是 test.mp4,我分别编成 600k / 1000k / 2000k:
| 目标码率 | 实际码率 | SSIM(全片均值) | 对照 |
|---|---|---|---|
| 600k | 约 604 kb/s | 0.9882 | x264 CRF 28:593 kb/s,0.9890 |
| 1000k | 约 1004 kb/s | 0.9906 | --- |
| 2000k | 约 2011 kb/s | 0.9935 | x264 CRF 23:1583 kb/s,0.9924 |
码率控制偏差不到 1%,SSIM 均值也和同码率的 x264 差不多(600k 对 CRF 28 只差 0.0008)。
不过我在 Mac 上播放时,觉得 600k / 1000k 的第一帧有明显的马赛克 ,当时还以为是低码率下硬编的毛病。下一节讲这个"案子",剧透一下:冤枉了硬编。
6. 破案记:第一帧的马赛克,其实不是硬编的错
剧透:马赛克在源文件里,不是编码器引入的。下面是我怎么一步步走到这里的,包括走错的三步。
现象
用 h264_v4l2m2m 把 test.mp4 重编成 600k / 1000k,播放时第一帧有明显的马赛克;2000k 看起来没问题。全片 SSIM 均值却和同码率的 x264 差不多。
弯路一:I 帧预算不足,周期性变糊?------排除
最直觉的猜测是"低码率下 I 帧分到的比特太少,所以每隔一段时间变糊一下"。我做了两个检验。
检验一:逐帧大小。
bash
for f in hw_600k hw_1000k hw_2000k; do
echo $f
ffprobe -v error -select_streams v:0 -show_entries frame=pict_type,pkt_size \
-of csv=p=0 $f.mp4 \
| awk -F, '{n[$2]++; s[$2]+=$1} END{for(k in n) printf "%s: %d 帧,平均 %.0f B\n",k,n[k],s[k]/n[k]}'
done
(小坑:ffprobe 的 csv 列顺序是它的内部顺序,这里是 pkt_size 在前、pict_type 在后,和 -show_entries 里写的顺序无关。)
| 目标码率 | I 帧平均 | P 帧平均 | I / P |
|---|---|---|---|
| 600k | 6819 B | 2108 B | 3.2x |
| 1000k | 9958 B | 3637 B | 2.7x |
| 2000k | 16315 B | 7628 B | 2.1x |
I 帧不但不小,反而码率越低,I 帧相对 P 帧分到的越多。(这组文件用的是 ffmpeg 默认的 -g 12,有 25 个 I 帧,和摄像头直出的每 60 帧一个不一致,后面重测时已经改成 -g 60。)
检验二:逐帧 SSIM。
bash
ffmpeg -hide_banner -loglevel error -i hw_600k.mp4 -i test.mp4 \
-lavfi ssim=stats_file=ssim_600k.log -f null -
awk 'NR==1||NR==2||NR==3||NR==61||NR==62||NR==63' ssim_600k.log | cut -d' ' -f1,5
第 1 帧(I)0.9788,第 2、3 帧§0.9757、0.9751;第 61 帧(I)0.9788,第 62、63 帧§0.9765、0.9772。I 帧并不比相邻 P 帧差,没有"周期性变糊"。
弯路二:开头的内容本身难编?------排除
用 -g 60 重编 600k 和 2000k,再加一个同码率的 x264 CRF 28(约 593 kb/s)做对照,看按秒平均 SSIM:
bash
awk '{gsub("All:","",$5); s+=$5; if(NR%30==0){printf "第%d秒 %.4f\n",NR/30,s/30; s=0}}' ssim60_600k.log
| 秒 | 硬编 600k | 硬编 2000k | x264 CRF 28 |
|---|---|---|---|
| 1 | 0.9855 | 0.9943 | 0.9904 |
| 2 | 0.9883 | 0.9930 | 0.9878 |
| 3 | 0.9880 | 0.9932 | 0.9890 |
| 4 | 0.9869 | 0.9928 | 0.9880 |
| 5 | 0.9894 | 0.9933 | 0.9886 |
| 6 | 0.9911 | 0.9944 | 0.9897 |
| 7 | 0.9899 | 0.9940 | 0.9896 |
| 8 | 0.9909 | 0.9943 | 0.9896 |
| 9 | 0.9905 | 0.9939 | 0.9892 |
x264 的曲线是平的,第 1 秒反而最高,所以开头的画面并不比后面难编。硬编 600k 的第 1 秒比 x264 低约 0.005,第 5 秒起追平并略高。-g 60 也确认生效(两个文件都是 5 个 I 帧)。
弯路三:硬件码控起步,第一个 I 帧给得太少?------只是相关,不是原因
再看 hw60_600k 和 x264_crf28 前 30 帧的字节数:
| 硬编 600k | x264 CRF 28 | |
|---|---|---|
| 第 1 帧(I) | 973 B | 4451 B |
| 第 1--10 帧平均 | 1193 B | 3066 B |
| 第 11--20 帧平均 | 1594 B | 1703 B |
| 第 21--30 帧平均 | 2493 B | 1822 B |
| 前 30 帧合计 | 约 52.8 KB(≈ 422 kb/s) | 约 65.9 KB(≈ 527 kb/s) |
第一个 I 帧只有 973 字节,不到 x264 的 1/4。我当时把它当成了头号嫌疑:硬件码控起步时给第一帧的比特极少,所以马赛克。听起来很顺,但还没看图。
破案:抽图
对四个文件各抽第 1 帧和第 61 帧:
bash
for f in test hw60_600k hw60_2000k x264_crf28; do
ffmpeg -hide_banner -loglevel error -y -i $f.mp4 -vf "select='eq(n,0)+eq(n,60)'" -fps_mode vfr ${f}_f%d.png
done
拷到 Mac 上并排看:
| 文件 | 第 1 帧 | 第 61 帧 |
|---|---|---|
test.mp4(源文件) |
马赛克明显 | 较好 |
hw60_600k |
马赛克明显 | 较好 |
hw60_2000k |
马赛克明显 | 较好 |
x264_crf28 |
马赛克明显 | 较好 |
四个文件的第 1 帧全都有马赛克,包括源文件和软编的 x264;第 61 帧全都正常。 马赛克在 test.mp4 的第 1 帧里就有,重编码(硬编、软编、600k 还是 2000k)只是忠实地继承了它。
为什么 SSIM 一直没发现
我计算 SSIM 时,拿 test.mp4 当参照。参照物的第 1 帧本身就带着马赛克,编码器把它复现得越忠实,得分反而越高。所以不管是全片均值、按秒平均,还是第 1 帧自己的 SSIM(0.9788,并不低),都看不出问题。指标只能衡量"和参照有多像",参照本身有缺陷时,它会连缺陷一起原谅。
需要收回的说法
- "600k / 1000k 有马赛克,2000k 没有":逐帧抽图不成立,2000k 的第 1 帧同样有。我之前的印象来自播放,可能是马赛克持续的时间或严重程度不同,但我没有逐帧证据。
- "硬件码控起步给第一个 I 帧极少":不再是主要解释。973 字节与"源头是一幅块状的画面(块状画面好压)"也相容,我没有进一步区分。
- "码率不要低于 1 Mbps,否则第一帧有马赛克":这个理由不成立。600k 的第 61 帧画质较好。(但我只抽了两帧,运动剧烈的画面还没验证。)
还没解释的
- 源头为什么有 :
test.mp4是rpicam-vid录的,默认码率约 4.4 Mbps,在这个码率下第 1 帧仍有马赛克,说明不只是"码率太低"。候选原因:相机刚启动(前 5 帧已经丢掉预热,但曝光/白平衡可能还没完全稳定),或者硬件编码器起步时第一个 I 帧偏粗。 - 硬编 600k 第 1 秒 SSIM 偏低(0.9855)而 2000k 不低:这个小差距是否另有原因,我没查清。
| 观察到 | 说明 |
|---|---|
| 2 Mbps 和 8 Mbps 的第 1 帧都有马赛克 | 与码率无关,更像相机刚启动或编码器起步 |
| 8 Mbps 干净、2 Mbps 有 | 起步时给的比特太少,提高码率有用 |
pipe_test_f1.png 也有 |
这条链路每次都会这样,不是偶然 |
| 马赛克只在第 0、1 帧,第 5 帧已经干净 | 只影响第一个 I 帧附近的极少几帧 |
实际影响
- 重编码救不了 :坏的参考帧会被一直继承,想要干净的开头,只能丢掉开头(比如用
-c copy从第二个 I 帧开始截,注意第 7 节裁剪的起点近似问题;这个做法我还没验证)。 - 推流影响有限:观众中途加入拿到的是后续的 I 帧;受影响的主要是录制文件的开头和起播的第一秒。
- 720p 主码流仍然沿用 2 Mbps (Day 07 的选择),但这次的马赛克不能再作为"1 Mbps 以下不可用"的证据。
- 方法上的教训:对比画质要看"独立于参照"的东西------抽图、看实际画面,而不是只信指标。
7. 裁剪、分段与 I 帧:同一个根源
-c copy 只能从 I 帧开始拷贝,所以 I 帧间隔会直接影响裁剪和分段:
- 裁剪 (
-ss 3 -t 4):-c copy得到 122 帧 / 4.1 s,文件 2681 KiB,速度 359x;重编码是 120 帧 / 4.0 s,1432 KiB,速度 0.89x。前者的预卷数据仍写在文件里,靠 mp4 编辑列表在播放时跳过,起点近似准确,但文件约 1.9 倍大。我一开始还以为"多出了 3 秒内容",用ffprobe -count_frames实测才发现是编辑列表隐藏了预卷帧。 - 分段 :sample.mp4 的 I 帧只有 0 s / 8.33 s,
segment_time 3只得到 2 个文件。摄像头素材每 2 秒一个 I 帧,segment_time 4却切出 6.0 s + 3.7 s,原因是第 2 个 I 帧的时间戳是 3.99996,差 0.00004 秒,没被算作到达 4 秒。加上-segment_time_delta 0.05就正常了。
8. 在树莓派上录制
- 不指定分辨率,
rpicam-vid默认录出 640x480 ,务必显式写--width/--height/--framerate,录完用 ffprobe 核对。 - 文件方式和管道方式都得到 292 帧、9.733 s、1280x720,全程约 30 fps、无丢帧(前 5 帧是相机预热,不录制)。
- 未指定
--bitrate时码率在 4.4--5.3 Mbps,偏高;指定--bitrate 2000000后 CPU 约 10%(单次采样),软编则是 342%。 - 管道直转 mp4 的写法:
rpicam-vid ... -o - | ffmpeg -f h264 -framerate 30 -i - -c copy out.mp4。
9. 我踩过的坑
| 现象 | 原因 / 解决 |
|---|---|
| `ffprobe ... | grep "^I"` 没有输出 |
SSH 下 rpicam-vid 报 Failed to create egl/drm preview |
没有显示环境,加 -n |
-vsync is deprecated |
改用 -fps_mode vfr |
| 硬编文件有 25 个 I 帧,预期 5 个 | h264_v4l2m2m 默认 -g 12,显式写 -g 60 |
| 重编码后第一帧有马赛克,指标却看不出 | 源文件第 1 帧本身就有,参照物自带缺陷;对比时要抽图看,别只信 SSIM |
10. 速查表
bash
# 看信息
ffprobe -hide_banner in.mp4
ffprobe -v error -select_streams v:0 -skip_frame nokey -show_entries frame=pts_time -of csv=p=0 in.mp4 # I 帧位置
# 转换
ffmpeg -i in.mkv -c copy out.mp4 # 转封装(无损)
ffmpeg -framerate 30 -i in.h264 -c copy out.mp4 # 裸流转 mp4
ffmpeg -i in.mp4 -c:v libx264 -preset medium -crf 23 -c:a aac out.mp4 # CRF 压缩
ffmpeg -i in.mp4 -c:v libx264 -b:v 1M -maxrate 1M -bufsize 2M out.mp4 # 指定码率
ffmpeg -i in.mp4 -c:v h264_v4l2m2m -b:v 2M -g 60 out.mp4 # Pi 硬编(务必写 -g)
# 截图与裁剪
ffmpeg -ss 5 -i in.mp4 -frames:v 1 -q:v 2 shot.jpg # 截一帧
ffmpeg -ss 00:00:03 -i in.mp4 -t 4 -c copy clip.mp4 # 快速裁剪(起点近似)
ffmpeg -ss 00:00:03 -i in.mp4 -t 4 -c:v libx264 -crf 23 -c:a aac clip.mp4 # 精确裁剪
# 摄像头录制
rpicam-vid -n -v 0 -t 10000 --width 1280 --height 720 --framerate 30 --bitrate 2000000 --codec h264 --inline -o cam.h264
# 分段录制
ffmpeg ... -c copy -f segment -segment_time 60 -segment_time_delta 0.05 -reset_timestamps 1 -strftime 1 "rec_%Y%m%d_%H%M%S.mp4"
小结
今天最大的收获不是某条命令,而是一个习惯:每一个结论都要有数据,而且数据要选对粒度。
这次的马赛克就是反面教材:全片 SSIM、按秒 SSIM、逐帧大小我都看了,还顺着数据编出了"硬件码控起步"这样顺口的解释;而真正的答案,一张 PNG 就够了------马赛克在源文件里,硬编是被冤枉的。
更要紧的是,指标是拿参照物来比的,参照物自己有缺陷时,指标会一起原谅它。
如果你也在树莓派上折腾视频,三条可以直接带走:能 -c copy 就别重编码;硬编一定显式写 -g;评价画质别只看一个数字,先抽几张图看看。