前言:第一阶段我手写 V4L2 代码点亮了摄像头,第二阶段的目标是把 FFmpeg 从"会敲命令"啃到"看懂源码结构":吃透命令行工具、从源码编译一遍、摸清树莓派的硬件编解码路径。这一晚踩了网络的坑、硬件的坑、还有自己给自己挖的坑,全部记录在此。
实验环境
项目 | 配置 |
|---|---|
硬件 | 树莓派 4(4GB),CSI 摄像头 IMX219(Camera Module V2),无头模式 SSH |
系统 | Debian 13 (trixie),arm64 |
FFmpeg | apt 版 7.1.5( |
关键设备 |
|
一、命令行工具吃透
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 | 体积 | 日志里的证据 |
|---|---|---|---|---|
| 3.1s | 3.66x | 8.4M |
|
| 9.5s | 1.08x | 3.5M |
|
| — | — | 5.0M / 1.3M | 平均 QP 17 vs 33 |
| — | — | 3.8M |
|
结论:
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.12.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 源码包 |
| configure 没成功/没进源码目录 | 先 configure 再 make |
v4l2 抓 CSI 摄像头只有 raw 格式 | CSI 传感器只输出 RAW Bayer |
|
裸 h264 封装 mp4 报 timestamps 警告 | 基本流无时间戳 |
|
推流不加 | UDP 缓冲溢出丢包 | 本地文件加 |
rpicam 报 preview 创建失败 | 无头模式 | 正常,自动后台录制 |
硬编传 | 硬件只支持码率模式 | 用 |
软编速度莫名减半 | 后台编译偷 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看一眼,说不定系统早就帮你装好了。😂