前言:第一阶段我手写 V4L2 代码点亮了摄像头,第二阶段的目标是把 FFmpeg 从"会敲命令"啃到"看懂源码结构":吃透命令行工具、从源码编译一遍、摸清树莓派的硬件编解码路径。这一晚踩了网络的坑、硬件的坑、还有自己给自己挖的坑,全部记录在此。
实验环境
| 项目 | 配置 |
|---|---|
| 硬件 | 树莓派 4(4GB),CSI 摄像头 IMX219(Camera Module V2),无头模式 SSH |
| 系统 | Debian 13 (trixie),arm64 |
| FFmpeg | apt 版 7.1.5(deb13u1+rpt1,树莓派特供版)+ 自编译 n7.1 |
| 关键设备 | /dev/video0(unicam CSI)、/dev/video11(bcm2835-codec 硬件编码) |
一、命令行工具吃透
1.1 ffprobe:容器、编码器、时间基是三件不同的事
ffprobe -v error -show_entries format=format_name,duration,bit_rate \
-show_entries stream=codec_name,time_base in.mp4
ffmpeg -i in.mp4 -c copy in.ts # 无损换壳,不重编码
ffprobe -v error -show_entries stream=codec_name,time_base in.ts
结果:同一个 H.264 流,mp4 里 time_base=1/15360,塞进 ts 后变成 1/90000。
结论 :format_name 是壳(容器),codec_name 是内容(编码器),time_base 是壳自己定义的时间刻度------MPEG-TS 协议硬性规定 90kHz。换壳不换内容(-c copy 时 speed=771x,瞬间完成),但时间基跟着壳走。时间戳是容器层的属性,不是编码器的属性,这个认知在后面反复救了我。
1.2 preset / CRF / GOP:编码器的三个旋钮
同一段 10 秒 720p 测试视频(testsrc2 生成),三组对照实验:
| 实验 | 耗时(real) | speed | 体积 | 日志里的证据 |
|---|---|---|---|---|
-preset ultrafast |
3.1s | 3.66x | 8.4M | cabac=0 bframes=0 ref=1,profile 退化为 Constrained Baseline |
-preset medium |
9.5s | 1.08x | 3.5M | cabac=1 bframes=3 ref=3,profile High |
-crf 18 / -crf 30 |
--- | --- | 5.0M / 1.3M | 平均 QP 17 vs 33 |
-g 15(默认 250) |
--- | --- | 3.8M | frame I:20 vs 默认的 frame I:2 |
结论:
-
preset 是"拿 CPU 时间换压缩率":ultrafast 放弃高级工具,算得快但文件大了一倍多;
-
CRF 是画质标尺,经验法则 数值每 +6,体积约减半;
-
GOP 缩小 → I 帧暴增(I 帧体积是 P 帧的 2~3 倍)→ 文件略变大,但播放器能更快"开门见山",直播延迟更低。
1.3 -re 与推流:一次 UDP 丢包实验
# 终端1:ffplay udp://127.0.0.1:8888
# 终端2:不带 -re 全速灌流
ffmpeg -i in.mp4 -c copy -f mpegts udp://127.0.0.1:8888?pkt_size=1316
播放器端立刻刷出 [mpegts @ ...] Packet corrupt,画面花屏卡死。
原理 :不带 -re 时 FFmpeg 以几百倍速把数据灌进网卡,UDP"只管发不管收",接收缓冲区瞬间溢出,大量丢包;H.264 前后帧强依赖,丢一包倒一片。-re 的作用就是给 FFmpeg 踩刹车,按原生帧率发流。
延伸思考 :为什么推摄像头不需要 -re?因为摄像头是"活"的------传感器物理上就是 30fps 出图,自带物理节流 ;本地文件是"死"的,才需要 -re 模拟实时。
1.4 CSI 摄像头:为什么我的摄像头没有 H.264?
ffmpeg -f v4l2 -list_formats all -i /dev/video0
输出里全是 Raw: yuyv422 / bayer_*,没有任何 Compressed 格式,直接抓取报错。
破案 :我的摄像头是 CSI 接口 ,它只是裸传感器,只输出 RAW Bayer 数据;USB 摄像头才自带 ISP+编码芯片直接吐 H.264/MJPEG。CSI 的正确流水线是:传感器 RAW → ISP(/dev/video13+)→ 硬件编码器(bcm2835-codec)。手动用 FFmpeg 串联这条流水线太折磨,工程上的标准做法是让官方工具干苦力:
rpicam-vid -t 10000 --width 1280 --height 720 -o cam_raw.h264
ffmpeg -framerate 30 -i cam_raw.h264 -c copy cam.mp4
日志里能看到 libcamera 自动选择了 1920x1080-SBGGR10/RAW 传感器格式并输出 1280x720-YUV420------ISP 在默默干活。两个无害的"假报警":
-
Failed to create egl/drm preview:无头模式没显示器,自动退回后台录制,正常; -
Timestamps are unset in a packet:基本流(ES)天生没有时间戳 ,时间戳是容器层赋予的。用-framerate声明帧率、-fflags +genpts生成时间戳可缓解;就算警告还在,文件也完全能播------它只是封装器的"牢骚",不是错误。这恰好是 1.1 结论的二次验证。
二、从源码编译:与网络斗智斗勇的一晚
2.1 git clone 的两种死法
-
第一种:
GnuTLS recv error (-110): The TLS connection was non-properly terminated,直接失败; -
第二种:重试后卡在
Receiving objects: 41% ... 19.00 KiB/s假死。
解法:放弃 git 协议,改下源码压缩包,走镜像代理,十几秒下完:
wget https://<镜像代理>/https://github.com/FFmpeg/FFmpeg/archive/refs/tags/n7.1.tar.gz
tar -xf n7.1.tar.gz && cd FFmpeg-n7.1
2.2 configure 是 FFmpeg 的"功能开关面板"
./configure --enable-v4l2-m2m --enable-libx264 --enable-gpl --disable-doc
输出里 Enabled encoders 出现了 h264_v4l2m2m、hevc_v4l2m2m--------enable-v4l2-m2m 这个开关被"实体化"了。随后 make -j2 > make.log 2>&1 & 扔后台(树莓派内存有限,-j2 防 OOM)。
2.3 源码漫步:三个"寻宝"
grep -A 5 "typedef struct AVRational" libavutil/rational.h # 时间基的真面目:{num, den} 分数
grep -n "VIDIOC_REQBUFS" libavdevice/v4l2.c # 第一阶段手写的 ioctl,被封装在这里
grep v4l2 libavcodec/codec_list.c # 编译生成的编解码器注册表
AVRational 就是一个分子/分母结构体------FFmpeg 用整数分数彻底避开浮点误差,ffprobe 里的 1/90000就是它。而 v4l2.c 里躺着我第一阶段写过的同一批 VIDIOC_* ioctl。"命令行 → 库 → 内核驱动"这条链,至此在脑子里闭环了。
2.4 反转:我杀掉了编译一小时的 make
做硬件实验前随手一查:
ffmpeg -hide_banner -encoders | grep v4l2m2m
# V..... h264_v4l2m2m V4L2 mem2mem H.264 encoder wrapper ...
树莓派官方 apt 源早就默认开启了 v4l2-m2m! 我对抗龟速网络、配 configure、让 CPU 满载编译一小时......其实根本不需要编译。于是 kill %1,并用 htop 确认:两个 100% 的 cc1(gcc 编译本体)正是 -j2 的两个工人,load≈2.0 与之一一对应。
编译是"无用功"吗?不是。configure 开关机制、make 后台任务管理、源码与命令行的映射------这些是 apt install 永远学不到的。但实用层面,今晚确实本可以省下一小时去喝茶。😄
三、硬件 vs 软件编码终极对决
time ffmpeg -i in.mp4 -c:v h264_v4l2m2m -b:v 4M -y out_hw.mp4 # 硬编
time ffmpeg -i in.mp4 -c:v libx264 -preset medium -y out_sw.mp4 # 软编
硬编日志里写着:Using device /dev/video11, driver 'bcm2835-codec'------活确实是专用芯片干的。
| 维度 | h264_v4l2m2m | libx264 medium |
|---|---|---|
| 耗时(real) | 3.9s | 18.1s* |
| speed | 2.82x | 0.563x* |
| CPU 时间(user) | 6.0s | 35.5s |
| 体积 | 4.8M | 3.5M |
* 带星号是因为这次软编时后台 make 还在偷 CPU(空载时是 9.5s / 1.08x)------意外收获的一条教训:做基准测试必须控制变量,后台任务会让数据失真。
两个隐藏细节:
-
硬件编码器不吃
-crf。bcm2835-codec 是"死脑筋",只接受-b:v码率模式,所以硬编文件反而更大(4M 固定码率 vs 软编 crf23 跑出的 2.8M)。嵌入式硬件加速最容易踩的坑之一。 -
硬编 user 时间只有 6 秒:CPU 基本在"搬运",计算全在芯片里。跑
htop对比两次 CPU 占用,是理解"硬件加速"最直观的一课。
里程碑达成 :现在我能对着一份转码日志逐行标注归属------Input #0 ... 是 demuxer(libavformat)在拆壳;Stream mapping 是解码→编码管线;Output #0 是 muxer;带 [libx264 @] 前缀的才是 encoder(libavcodec)在说话;frame= ... speed= 是运行统计。
四、踩坑清单
| 现象 | 原因 | 解法 |
|---|---|---|
| git clone 报 GnuTLS 错误 / 卡 19KB/s | 网络问题 | 镜像代理 wget 源码包 |
make: no makefile found |
configure 没成功/没进源码目录 | 先 configure 再 make |
| v4l2 抓 CSI 摄像头只有 raw 格式 | CSI 传感器只输出 RAW Bayer | rpicam-vid 走 ISP+硬编流水线 |
| 裸 h264 封装 mp4 报 timestamps 警告 | 基本流无时间戳 | -framerate / +genpts;理解警告非错误 |
推流不加 -re 播放端 Packet corrupt |
UDP 缓冲溢出丢包 | 本地文件加 -re,摄像头不用 |
| rpicam 报 preview 创建失败 | 无头模式 | 正常,自动后台录制 |
硬编传 -crf 无效 |
硬件只支持码率模式 | 用 -b:v |
| 软编速度莫名减半 | 后台编译偷 CPU | 基准测试要空载 |
五、第二阶段自检清单
-
说清 format_name / codec_name / time_base 三者关系,及 mp4 与 ts 时间基差异
-
一句话说清 preset / crf / g 各自影响
-
说清
-re的作用与"何时不需要" -
说清 CSI 与 USB 摄像头的架构差异、ES 无时间戳的本质
-
在源码里找到 AVRational、VIDIOC ioctl、codec 注册表
-
对着日志逐行标注 demuxer / muxer / encoder 归属
六、写在最后
这一晚最大的收获不是某个参数,而是三个认知升级:时间戳属于容器不属于流 、摄像头架构决定数据形态 、硬件编码器有自己的脾气 。哦对,还有第四条:动手前先 ffmpeg -encoders | grep v4l2 看一眼,说不定系统早就帮你装好了。😂