1. 先弄清V4L2这层皮:为什么HDMI-IN在系统里长得像摄像头
1.1 V4L2的抽象:各类“视频输入端”被统一成一套接口
很多刚接触HDMI采集的开发者,第一次在Linux下执行ls /dev/video*时都会犯同一个错:下意识把video0当成摄像头来读。这不是你的问题,是V4L2本身的设计——V4L2(Video for Linux 2)是内核为所有视频输入输出设备提供的统一框架:USB摄像头是V4L2设备,PCIe采集卡是V4L2设备,HDMI-IN接收器同样被包装成一个V4L2 Video Capture设备。
这意味着你面对的操作接口完全一样:打开设备节点、设置格式、申请缓冲区、把帧取出来。但是接口一样,不代表底层逻辑一样。USB摄像头是设备主动把数据推给SoC,HDMI-IN则是SoC被动等外部信号源送时序和数据,两者在“谁来产生时钟和分辨率”这件事上有本质区别。我后来在项目中总结了一句话:把HDMI-IN的操作想简单了,后面必然要花三倍时间排查为什么没有帧。
这套框架的核心抽象可以类比成“文件描述符”:你不需要关心底层是SATA硬盘还是U盘,读写API是同一套;V4L2也一样,不管你接的是CMOS传感器、模拟视频解码器还是HDMI RX芯片,只要驱动注册了video节点,你就用open/ioctl/mmap/poll这一组通用动作来操作它。关键差异被藏在设备的capability和格式枚举里,这也是接下来要重点讲的地方。
1.2 HDMI-IN采集卡在内部到底走了哪条路
我们经常说“HDMI-IN设备”,但物理上它往往不是一块独立卡,而是一个完整的接收链路:HDMI源设备通过线缆把TMDS信号送进来,板上有一颗HDMI接收芯片(简称RX)完成信号解码、均衡、EDID握手,然后把数据转换成SoC能吃到的并行接口或DPI/CVBS类型信号,最后进到SoC的V4L2驱动中注册成video节点。
这条链路中真正影响采集质量的环节,往往不在V4L2层,而是在RX芯片的配置上。常见的问题是EDID信息没配好,导致信号源认为显示器不支持该分辨率,输出一个模糊的VGA回退模式;或者HDCP加密链路没有处理,画面直接黑屏。这些错误几乎看不出是V4L2的问题,但排查时都要回到“查看dmesg、查看时序”这一步。
V4L2层看到的是已经完成协商的结果:宽高多少、帧率多少、像素格式是YUYV还是NV12。所以操作HDMI-IN设备有一个和普通摄像头截然不同的心法:先确认信号链路通了,再谈软件配置;而不是一上来就调用VIDIOC_S_FMT指定一个你想要的任意分辨率。这条心法会在后面各个章节反复出现。
1.3 谁需要读这篇文章,能解决什么问题
如果你正在做以下方向,这篇指南的覆盖面基本都能帮上忙:嵌入式板卡上接HDMI视频源做实时算法分析、直播推流;机器视觉现场需要抓取另一个屏幕或设备的输出画面;或者你在为某套多路视频系统做V4L2采集层调试。文章内容按“识别设备→配置时序→建立缓冲→采集循环→排查坑位→接入下游应用”这条实操路径展开,代码基于标准V4L2 API,换到大多数带V4L2驱动的Linux平台都能直接参考。
2. 设备枚举:从一堆/dev/videoX里准确认出你的HDMI-IN节点
2.1 先看--list-devices和sysfs,别用猜的
一台Linux设备上如果同时有摄像头、HDMI-IN、编码器等多个多媒体节点,/dev/video0往往不是你想要的。我的习惯是第一步永远先跑v4l2-ctl --list-devices:
v4l2-ctl --list-devices输出里会按设备名列出对应的video节点,还会给出总线位置信息(bus_info)。不少HDMI-IN驱动会在名称上写明HDMI、HDMI RX、V4L2 Capture等关键词。但注意,有些板卡的HDMI输入被包装成了普通的“Video Capture”名称,这时候直接靠名字判断可能不准。可靠的做法是进sysfs进一步确认:
cat /sys/class/video4linux/video0/name cat /sys/class/video4linux/video0/device/driver/name第一个文件是驱动设定的节点名,第二个能看出驱动模块名。如果你看到类似“xxx-hdmi”的驱动名,基本可以确定这是HDMI-IN设备。如果你的系统用的是新版udev,还可以用udevadm info /dev/video0把设备路径、driver、型号全部拉出来,写脚本时也建议用稳定的设备路径而非videoN,防止热插拔后节点漂移。
2.2 用QUERYCAP确认设备类型与I/O方式
仅靠/dev节点名还不够,因为V4L2设备还能细分为Capture、Output、Metadata、M2M等类型。打开设备后用VIDIOC_QUERYCAP查询capabilities是最不容易被骗的确认方式。下面这段代码可以直接编译运行:
#include <stdio.h> #include <fcntl.h> #include <sys/ioctl.h> #include <linux/videodev2.h> int main(int argc, char **argv) { int fd = open(argc > 1 ? argv[1] : "/dev/video0", O_RDWR); if (fd < 0) { perror("open"); return 1; } struct v4l2_capability cap; if (ioctl(fd, VIDIOC_QUERYCAP, &cap) < 0) { perror("QUERYCAP"); return 1; } printf("driver: %s\n", cap.driver); printf("card: %s\n", cap.card); printf("bus: %s\n", cap.bus_info); printf("cap: 0x%x\n", cap.capabilities); close(fd); return 0; }capabilities里有一个V4L2_CAP_VIDEO_CAPTURE标志,这是HDMI-IN设备在应用层最常出现的能力。如果驱动只暴露了V4L2_CAP_VIDEO_CAPTURE_MPLANE,后面代码里的buf类型要改用V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE,这和传统单平面的capture接口不一样,较新的平台很常见。这里容易折腾人,后面会专门提到。
2.3 像素格式列表怎么读:支持的不代表当前能用
枚举格式是理解这台设备能力的关键,但也是新手最容易误读的内容:
v4l2-ctl -d /dev/video0 --list-formats-ext你会看到驱动声明支持的像素格式,常见HDMI-IN设备一般是YUYV、NV12、BGR3,也有少数设备直接输出MJPEG或H.264(内部有压缩硬件)。这张列表的意思是“硬件或驱动能处理的格式集合”,不是“当前HDMI信号正在输出的格式”。分辨率字段也同理:很多驱动会把支持的宽高枚举得非常多,但实际生效的分辨率会在HDMI源接入后根据时序自动切换,你设置的目标格式如果和当前信号时序对不上,后续获取的帧就可能出现花屏或者S_FMT失败。
还有一种常见误区:以为所有格式都能直接从HDMI源转换过来。实际上像YUYV转NV12这类色彩空间转换,驱动和硬件不一定承担,甚至很多“NV12”是带额外限制的子类型。我的建议是:先用v4l2-ctl --all查看当前驱动的实际状态,特别注意"Selected Device Properclear"里的width、height、pixelformat和bytesperline,这比看格式列表准确得多。
3. 配置协商顺序:先看时序,再谈格式,最后开闸放流
3.1 没有HDMI信号时,整条采集链都在空转
我见过太多人把摄像头那套流程直接套在HDMI-IN上:打开节点、S_FMT设置1920x1080、REQBUFS、STREAMON,然后DQBUF直接阻塞,等多久都不回来。排查到最后发现,HDMI源根本没接,或者源在休眠状态。HDMI接口本身没有“给我一帧数据”的驱动能力,它只能被动接收,信号源不输出就是空等。
所以操作前要养成一个习惯:先确认有没有信号。常见手段有三个:
- 看dmesg是否有hdmi hotplug相关日志;
- 用v4l2-ctl --query-dv-timings查看当前时序,返回“no signal”说明源没有输出;
- 如果驱动支持事件机制,用V4L2_EVENT_SOURCE_CHANGE注册事件,信号接入时内核会主动通知应用。
第3种方式最可靠,但要求驱动实现了好这一份“事件上报”。多数现代HDMI IN驱动都支持,后面会给出poll事件代码。
3.2 先从DV时序拿到真实分辨率,再设置V4L2格式
确认有信号之后,第一步不是马上VIDIOC_S_FMT,而是拿“真实时序”。标准V4L2 API里有专门针对数字视频接口的子协议:VIDIOC_SUBSCRIBE_EVENT、VIDIOC_QUERY_DV_TIMINGS、VIDIOC_S_DV_TIMINGS。很多HDMI驱动会在vid设备节点上暴露这些ioctl。
建议配置顺序:
- 查询当前时序(VIDIOC_QUERY_DV_TIMINGS,返回v4l2_dv_timings,里面有像素时钟、水平/垂直有效像素、polarity等);
- 把时序通过VIDIOC_S_DV_TIMINGS通知驱动“我接受了这个时序”;
- 再用VIDIOC_S_FMT把你想要的pixelformat加上宽高填进去,宽高要和timings一致;
- 最后申请缓冲区并STREAMON。
典型代码片段:
struct v4l2_dv_timings dv; if (xioctl(fd, VIDIOC_QUERY_DV_TIMINGS, &dv) == 0) { xioctl(fd, VIDIOC_S_DV_TIMINGS, &dv); fmt.fmt.pix.width = dv.bt.width; fmt.fmt.pix.height = dv.bt.height; } xioctl(fd, VIDIOC_S_FMT, &fmt);不要忽略这个步骤带来的另一个好处:有些驱动在S_FMT之前没有接收过有效时序,内部link是断开状态,你申请了buffer也不会有帧。先把timings定下来,等于先把硬件的“闸门”对准了。很多“为什么STREAMON之后DQBUF一直阻塞”的问题,就卡在这一步。
3.3 缓冲方案三选一:mmap、read、dmabuf
V4L2支持多种内存模式,但HDMI-IN场景里实际常用的就三种:
- mmap:用户态映射驱动分配的内核缓冲区,采集流程中数据拷贝最少,应用最广泛。
- read/write:驱动内部帮你处理缓冲,代码简单,但每帧多一次拷贝,高分辨率下很容易掉帧,不建议用于1080p以上。
- dmabuf:把缓冲区导出成dma-buf fd,直接传给GPU或编码器,零拷贝衔接下游。高带宽场景的必选,代码会复杂一些。
这是我个人在边缘设备上实践后得出的选择优先级:先跑通mmap,确认链路和画面没问题;要推给硬件编码器时再升级到dmabuf。直接一上来啃dmabuf,既难排查信号问题,又容易被驱动细节卡住。
3.4 buffer数量怎么定:队列深度与帧率抖动
申请缓冲区时通过VIDIOC_REQBUFS的count字段指定数量。这个数字不要随意写,HDMI-IN设备因为是外部时钟同步,帧到达时间本身不稳定,如果缓冲区数量太少,DQBUF取出一个、QBUF放回一个,而硬件还没填完下一个,应用就会在poll上干等。一般建议至少4个buffer,如果你要做长时间录制,6个也不算浪费。过多buffer会带来显式延迟,这个要根据你的实时性需求权衡。
队列机制归纳成一句话:V4L2就是“内核把帧填进某个buffer→你取走→你处理完再还回去”的循环。HDMI-IN因为源不受控,队列水位管理比USB摄像头敏感得多,宁可多申请一个buffer,也不要等到DQBUF超时了才回头改。
4. 一个能跑起来的mmap采集循环:从帧头到保存文件
4.1 完整的C采集代码
这里给一份可以编译、可以直接跑的完整示例。它做的事情是:打开设备、查询时序、设置YUYV格式、申请4个mmap缓冲、启动采集、循环取300帧并打印每帧大小和序号。去掉特定环境依赖,任何V4L2 Capture设备都应该能跑通,但宽高务必根据你的HDMI信号实际值修改。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <sys/poll.h> #include <linux/videodev2.h> #define DEV_PATH "/dev/video0" #define NUM_BUFS 4 static int xioctl(int fd, unsigned long req, void *arg) { int r; do { r = ioctl(fd, req, arg); } while (r == -1 && errno == EINTR); return r; } int main(int argc, char **argv) { const char *dev = argc > 1 ? argv[1] : DEV_PATH; int fd = open(dev, O_RDWR | O_NONBLOCK); if (fd < 0) { perror("open"); return 1; } struct v4l2_dv_timings dv; if (xioctl(fd, VIDIOC_QUERY_DV_TIMINGS, &dv) == 0) xioctl(fd, VIDIOC_S_DV_TIMINGS, &dv); struct v4l2_format fmt; memset(&fmt, 0, sizeof(fmt)); fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width = 1920; fmt.fmt.pix.height = 1080; fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV; fmt.fmt.pix.field = V4L2_FIELD_NONE; if (xioctl(fd, VIDIOC_S_FMT, &fmt) < 0) { perror("S_FMT"); return 1; } struct v4l2_requestbuffers req; memset(&req, 0, sizeof(req)); req.count = NUM_BUFS; req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory = V4L2_MEMORY_MMAP; if (xioctl(fd, VIDIOC_REQBUFS, &req) < 0) { perror("REQBUFS"); return 1; } void *bufs[NUM_BUFS]; size_t lens[NUM_BUFS]; memset(bufs, 0, sizeof(bufs)); for (int i = 0; i < NUM_BUFS; i++) { struct v4l2_buffer buf; memset(&buf, 0, sizeof(buf)); buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; buf.index = i; if (xioctl(fd, VIDIOC_QUERYBUF, &buf) < 0) { perror("QUERYBUF"); return 1; } lens[i] = buf.length; bufs[i] = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset); if (bufs[i] == MAP_FAILED) { perror("mmap"); return 1; } if (xioctl(fd, VIDIOC_QBUF, &buf) < 0) { perror("QBUF"); return 1; } } int type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if (xioctl(fd, VIDIOC_STREAMON, &type) < 0) { perror("STREAMON"); return 1; } for (int frame = 0; frame < 300; frame++) { struct pollfd pfd = {.fd = fd, .events = POLLIN}; int pr = poll(&pfd, 1, 3000); if (pr == 0) { fprintf(stderr, "frame %d: DQBUF timeout\n", frame); break; } if (pr < 0) { perror("poll"); break; } struct v4l2_buffer buf; memset(&buf, 0, sizeof(buf)); buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; if (xioctl(fd, VIDIOC_DQBUF, &buf) < 0) { perror("DQBUF"); break; } fprintf(stderr, "frame %d: idx=%d bytesused=%u\n", frame, buf.index, buf.bytesused); if (xioctl(fd, VIDIOC_QBUF, &buf) < 0) { perror("QBUF"); break; } } xioctl(fd, VIDIOC_STREAMOFF, &type); for (int i = 0; i < NUM_BUFS; i++) { if (bufs[i]) munmap(bufs[i], lens[i]); } close(fd); return 0; }4.2 主循环里的dequeue、process、requeue到底在干什么
很多人第一次看上面这段代码,会忽略掉最关键的那个动作循环:DQBUF取出一个buffer到用户态,处理完,立刻QBUF放回去。这个“还”的动作才是V4L2队列正常运转的引擎。如果只取不还,内核的可用缓冲区很快耗尽,下一个DQBUF就会永久阻塞。在HDMI-IN场景里尤其危险,因为源不会因为你处理慢就减速,缓冲一耗尽,驱动会直接丢帧,你还察觉不到。
process阶段可以做什么?把上面代码中的fprintf换成你的实际逻辑,比如:把YUYV转换成RGB交给算法模块;把buf.bytesused长度的裸数据塞给编码器;或者直接以buf.index为序号写入环形队列。这里要特别注意buf.bytesused才是真实数据长度,不是WIDTHHEIGHT2一定有,因为驱动可能按对齐填充。
4.3 清理与重连:释放顺序错了会连带驱动失灵
我之前在某个项目的采集服务退出流程上踩过一个大坑:不调STREAMOFF直接close(),结果下次启动时驱动停在异常状态,DQBUF一直返回EIO。V4L2虽然规定close会隐式停止,但很多HDMI驱动的状态机没有那么健壮,靠close兜底很容易留下脏状态,尤其是播放期间拔掉信号源再插回的情况。
正确的清理顺序:STREAMOFF → munmap所有buffer → close。如果后续还要重连,建议STREAMOFF之后同一fd上重新走一遍S_FMT→REQBUFS流程。重连场景下的轮询策略在第六章展开。
5. 信号链路里最常翻车的六个坑(按踩坑概率排序)
5.1 S_FMT直接报错或返回奇怪尺寸
很多人都试过打开HDMI-IN设备后,把S_FMT的宽高设置成4K分辨率,然后得到EINVAL或EIO,接着就怀疑驱动有bug。绝大多数情况是时序没协商。HDMI是同步接口,S_FMT里指定的宽高必须和信号源实际送出的时序在驱动看来一致。驱动会根据当前DV时序校验宽高,不一致就报错。
也有相反的表现:S_FMT成功,但返回给你的实际宽高和设置值不一致,驱动默默改成了当前信号分辨率。所以S_FMT之后一定要回读fmt.fmt.pix.width/height,用实际值分配后续逻辑,不要迷信你传进去的值。我习惯在所有采集代码里都强制打印S_FMT后的实际格式,它能在很多问题上救你一命。
5.2 颜色发灰、发绿:limited range和像素顺序谋杀画面
HDMI-IN采集最令人崩溃的现象是什么?画面能出来,但是整体发灰、发绿、暗部发闷,或者靠近红色物体全是彩色噪点。这些问题几乎都和两个细节有关:量化范围(quantization range)和YUV顺序。
HDMI传输视频信号通常遵循BT.709或BT.601的有限范围(limited range),亮度分量范围是16到235,而很多算法模块假设0到255的空全范围(full range)。V4L2驱动有时会在采集时做转换,有时不做,导致你看到灰蒙蒙的画面。自检方法很简单:抓一帧纯黑和纯白画面,打印Y分量的最大值/最小值,如果接近0和255就是full range,如果接近16和235就是limited range。然后根据下游处理器的要求做量化范围转换。
YUV顺序问题更隐蔽:同样是YUYV,驱动内存布局是Y0 U0 Y1 V0,如果你按UYVY解析或者直接索引错误,画面就会出现红绿错位。最简单的排除法是先抓一帧已知彩色画面(比如静态菜单),把R/G/B三通道分别输出,看哪个通道异常,基本能定位顺序写错。
5.3 热插拔后分辨率切换让缓冲区对不上
HDMI视频源不是永远固定分辨率的。用户把另一个设备接进来,或者源端播放分辨率从1080p切到4K,帧的宽高就变了。如果你没有监听SOURCE_CHANGE事件,还在按旧的宽高处理新帧,轻则一帧花屏,重则buffer长度越界崩溃。
规范做法:订阅V4L2_EVENT_SOURCE_CHANGE事件(VIDIOC_SUBSCRIBE_EVENT),在事件到达后STREAMOFF,重新查询timings和格式,重新REQBUFS,再STREAMON。这里的关键是不要尝试用一个掩耳盗铃的方式“继续采”,分辨率变了后续所有DQBUF的buffer布局都不对。我见过有同事只在应用层做宽高重排,不去动底层,结果每切一次分辨率,画面就出现半帧残影。
5.4 带宽不够:4K60下YUYV裸数据量远超你的直觉
下面这段计算值得贴在你的工程笔记里:
- 1080p60 YUYV:1920×1080×2字节×60 ≈ 248.8 MB/s,接近2Gbps;
- 4K60 YUYV:3840×2160×2字节×60 ≈ 995 MB/s,接近8Gbps。
这个数字意味着,如果你用一条普通USB 3.0口连接采集卡,或者板子内存带宽吃紧,传输环节本身就变成了瓶颈。很多“每秒只能拿到30帧”的现象,其实是总线或驱动没跟上,不是HDMI源的问题。工程上的解法就三条:改用NV12等半尺寸格式(每像素1.5字节);压缩交给硬件编码器,不在采集端硬扛;或者把采集循环绑到独立CPU核上并提高进程优先级。实际项目中我一般先看dmesg里有没有lost frames或overrun相关的驱动警告,比猜带宽更直接。
5.5 设备被多个进程占用,S_FMT返回EBUSY
V4L2设备的排他性往往被人忽略。一个设备节点在同一时刻通常只允许一个应用持有采集流,其他进程打开成功不代表你能S_FMT成功,返回EBUSY是常见表现。我排查这类问题最爱用的命令是:
lsof /dev/video0把占用进程找出来再决定kill还是让它退出。还有一个阴间情况是:有些驱动对“打开但没STREAMON”宽容,允许另一个应用配置格式,但两方同时DQBUF时行为就开始混乱。工程上建议自己做统一的应用层锁,或者用udev规则保证只有一个服务持权。
5.6 采集不到图像,先查EDID协商
如果你面对的是一个HDMI-IN开发板,信号源又是笔记本、电视盒子这类消费设备,它们会在连接时读取采集端的EDID,以此决定输出什么分辨率。如果驱动没有配置EDID,或者EDID数据被锁死成原始分辨率,常见现象是:采集端一切代码都正常,但源端只输出640x480低分辨率,或者干脆没有输出。
排查手段多数不是抓V4L2代码,而是看v4l2-ctl --get-edid能不能读到数据,读不到就该用驱动提供的EDID上传接口,把一份合理的EDID(包含你要的1080p/4K模型)写入采集端。这一步经常被当成“硬件不行”,其实只是HPD和EDID握手的固件配置问题。
6. 把采集帧送进下游:零拷贝、ISP链路和断流恢复的衔接
6.1 dmabuf导出:把帧描述符交给下游设备
mmap方案适合先验证链路。真正做产品时,如果下游是GPU解码、硬件编码器或者ISP,你会希望数据不经过CPU内存拷贝,直接从驱动缓冲区交给下游硬件。V4L2为此提供了VIDIOC_EXPBUF,把一块mmap缓冲区导出成dma-buf fd,然后把这个fd传给编码器或GPU驱动。
接口逻辑不复杂,就是VIDIOC_EXPBUF后得到一个fd,但要确保下游组件支持dma-buf导入。在嵌入式平台上,硬件编码器驱动一般都有V4L2 M2M接口,它们接收的输入buffer可以就是来自采集设备的dmabuf fd。这时候采集服务的buffer数量要按下游处理速度留足余量,比如编码端吞吐只有30fps,采集端就别只开2个buffer。
6.2 subdev链路:HDMI IN到ISP再到video节点
现代SoC上,HDMI-IN从物理信号到video节点往往不是一步到位的,中间可能有RX芯片、ISP、scaler等多个V4L2 subdev。你需要用media-ctl查看整条media pipeline:
media-ctl -d /dev/media0 -p如果发现video节点前面挂了多个subdev,S_FMT行为往往受链路配置影响。比如ISP做3A算法时需要从sensor域读取格式,链路里某个subdev没设对,video节点S_FMT就会失败。这种场景的调试思路是:先从video节点往回走,把每个subdev的当前格式、power状态、link状态全部拉出来,再决定改哪一层。不要只盯着video节点折腾,链路中任何一环断掉都会表现为采集不到画面。
6.3 poll循环与断流恢复:把采集服务做成不会死的服务
实际生产里,HDMI源掉电、换设备、线缆松动都是常态。采集服务如果只在启动时做一次格式协商,后面遇到断流基本就卡死或异常退出。一个耐用的采集线程应该做这些事:
- 启动后订阅V4L2_EVENT_SOURCE_CHANGE;
- poll事件和POLLIN同时监听;
- 收到SOURCE_CHANGE后主动STREAMOFF,重新QUERY_DV_TIMINGS,再S_FMT、REQBUFS、STREAMON;
- DQBUF超时连续出现N次后,认为断流,进入重连状态;
- 重连不仅要在应用层做指数退避,还要定期用VIDIOC_QUERY_DV_TIMINGS探测信号是否恢复。
这套循环的代码量不大,但能把“采集服务跑几天就死”这种研发期问题彻底终结。信号的恢复检测尤其值得写完善。
6.4 接主流多媒体框架:先跑通一条验证命令
调试阶段我建议直接用系统里的v4l2工具链先证明“硬件链路是好的”,再上自己的代码。一条实用的预览命令是:
ffplay -f v4l2 -input_format yuyv -video_size 1920x1080 /dev/video0或者用FFmpeg直接录制:
ffmpeg -f v4l2 -framerate 60 -video_size 1920x1080 -input_format yuyv -i /dev/video0 -c:v libx264 -f mp4 out.mp4如果这条命令能流畅出画面,说明采集链路完全正常,问题一定在应用层;如果这条命令本身就报错,你就知道该往驱动层、EDID或者时序配置上排查。我也是靠这种“先工具后代码”的习惯,省下过很多不必要的调试时间。
最后再分享一个挺实用的体会:HDMI-IN调试最大的敌人不是API复杂,而是“信号是否真的存在、格式是否真的匹配”这种前提性信息不透明。我踩过的所有坑,几乎都能靠三句话定位——用v4l2-ctl --list-devices确认设备、用--query-dv-timings确认信号、用FFmpeg预览确认整链路。这三点通了,后面写多复杂的采集代码都不慌。反过来,如果信号没确认就埋头写代码,再好的框架也救不了你。