news 2026/10/26 12:53:15

V4L2+mmap 到 Qt 显示与 ffmpeg 管道录像实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
V4L2+mmap 到 Qt 显示与 ffmpeg 管道录像实战

简介:这份资源面向具备一定 Linux 与 C/C++ 基础的嵌入式开发者、摄像头应用开发初学者,围绕「v4l2 采集 + Qt 显示 + 录像」这一常见需求,提供一套可参考的工程源码。资源包共 210 个文件,以 145 个 .h 头文件、30 个 .c 与 12 个 .cpp 源文件为主体,另有 .pro/.pri 工程配置、.ui 界面文件、libjpeg 相关 .so/.a 库及 readme、todo 等说明文档,压缩后约 3.03MB。内容覆盖 v4l2 接口取流、MJPEG 逐帧解析、借助 libjpeg/tinyjpeg 转 RGB、Qt 界面刷新显示与事件触发的录像逻辑,并附 libv4l2、libv4lconvert 等转换库源码,便于理解从设备节点读取原始帧到上屏的完整链路。已有 553 人学习,适合作为摄像头预览与录像功能的二次开发底稿,也能帮助读者梳理 V4L2 与 Qt 协同的目录结构与模块划分。

1. 从 /dev/video0 到 QWidget:v4l2 的显示与录像,难点到底在哪里

接入 USB 摄像头做本地预览并同步录像,多数人的第一反应是用 OpenCV 的 VideoCapture。虽然能跑通,但把分辨率提到 720p、要求端到端延迟控制在几十毫秒,并且还要同时录制一份文件时,CPU 占用和掉帧就会带来麻烦。这个项目把中间层全部拆掉,直接对/dev/video0发 ioctl,用 v4l2 拿原始帧,转成 QImage 交给 qt 控件绘制,录像则把裸流灌进 ffmpeg 管道。它适合手上有 UVC 摄像头、想在 Linux 桌面或嵌入式板子上做一个轻量预览录像工具的人,也适合想搞清楚 mmap 缓冲队列到底怎么转、什么时候该深拷贝的人。

2. 工程骨架与技术选型:四层分层、CMake 配置和三条依赖边界

先把工程拆开看。这类项目最容易写成一坨:main 里 open 设备、开线程、往 QLabel 里 setPixmap、顺手 fopen 写文件,两百行能跑,加一个曝光调节就全乱。分层不是为了好看,是为了让「换编码器」和「换显示控件」这两件事互不影响。

2.1 四层分层与文件清单

采集层只碰 fd 和 ioctl,不 include 任何 Qt GUI 头文件;转换层只做像素格式到 QImage 的搬运;显示层只认 QImage;录像层只认(const uint8_t*, len)。四层之间靠信号槽和纯函数连接,任何一层都能单独拿去做单元测试。

路径职责关键类/函数
src/main.cpp启动 QApplication,解析设备节点参数main
src/MainWindow.*工具栏、设备选择、开始/停止录像MainWindow
src/CameraWidget.*预览控件,paintEvent 里 drawImageCameraWidget
src/V4L2Capture.*采集线程,ioctl 调用与取流循环V4L2Capture::run
src/FrameConverter.*YUYV / MJPEG 转 RGB32yuyvToRgb32
src/Recorder.*QProcess 拉起 ffmpeg,写管道Recorder::writeFrame
CMakeLists.txt构建入口-

V4L2Capture继承QThread,把run()整个重写成阻塞循环;CameraWidget只暴露一个setFrame(const QImage&)槽。中间用Qt::QueuedConnection连接,跨线程投递靠 Qt 的事件队列完成,不需要自己加锁保护画面。

2.2 构建配置:CMake 里怎么对齐 Qt 与内核头

V4L2 的头文件来自内核,linux/videodev2.h在装了内核头文件的机器上直接能 include,不需要 find_package,也不需要额外链接库。真正要小心的是 AUTOMOC:类里只要出现Q_OBJECT,就必须开,否则信号槽在链接期报 undefined reference。

cmake_minimum_required(VERSION 3.16) project(v4l2qtcam LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) # 有 Q_OBJECT 就必须开 set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) find_package(Qt5 5.12 REQUIRED COMPONENTS Widgets) find_package(Threads REQUIRED) add_executable(v4l2qtcam src/main.cpp src/MainWindow.cpp src/CameraWidget.cpp src/V4L2Capture.cpp src/FrameConverter.cpp src/Recorder.cpp ) # V4L2 走 ioctl,不需要 link 任何库;Threads 是给 QThread 之外的 std::thread 兜底 target_link_libraries(v4l2qtcam PRIVATE Qt5::Widgets Threads::Threads) target_compile_options(v4l2qtcam PRIVATE -Wall -Wextra)

CMAKE_AUTOMOC打开后,CMake 会扫描头文件里的Q_OBJECT生成moc_*.cpp并自动加进编译单元。-Wall -Wextra建议留着,ioctl 的参数是void*,类型传错编译器不会拦,只能靠-Werror=format之类间接发现。Qt6 用户把Qt5换成Qt6、Qt5::Widgets换成Qt6::Widgets即可,其余代码不用动。

2.3 选型取舍:为什么是 mmap + Widget + ffmpeg 管道

不选 OpenCV 的理由很直接:它的VideoCapture在 Linux 上底层也是走 V4L2,但会多一次内部缓冲拷贝,并且默认帮你做色彩空间转换,延迟不可控。要精确控制「这一帧从 dqbuf 到上屏花了多久」,只能自己拿。

缓冲方式选V4L2_MEMORY_MMAP而不是USERPTR。MMAP 是驱动分配、用户态映射,零拷贝;USERPTR 要用户自己 malloc 再传给驱动,很多 UVC 驱动对它支持不完整,REQBUFS会直接返回EINVAL。这是血泪经验:换机器测试时,USERPTR 在三个摄像头里只有一个能跑。

显示用 QWidget 而不是 QML。QML 的VideoOutput绑定的是 MediaPlayer,接不了自己喂的 QImage;用QQuickImageProvider又要每帧走一次 URL 解析,720p 30fps 下这条路的开销不比转换本身小。QWidget::paintEvent里一句drawImage就够了。

录像选 ffmpeg 管道而不是自己写 AVI 封装。MJPEG 直写.mjpg确实简单,但产物大多数播放器不认,还得二次转封装;而 x264 的码率控制、关键帧间隔、时间戳这些细节,自己写封装迟早要翻车。用QProcess把 ffmpeg 拉起来,stdin 喂裸流,剩下的交给它。

3. V4L2 采集链路拆解:从 open 到 dqbuf 的六个 ioctl

这条链路是固定的:open→VIDIOC_QUERYCAP→VIDIOC_S_FMT→VIDIOC_REQBUFS→VIDIOC_QUERYBUF+mmap→VIDIOC_QBUF→VIDIOC_STREAMON→poll+VIDIOC_DQBUF/VIDIOC_QBUF循环 →VIDIOC_STREAMOFF。少一步就跑不起来,顺序错了报错信息还特别含糊。

3.1 打开设备与能力协商:QUERYCAP 之后必须校验的两个标志

open用O_RDWR | O_NONBLOCK。非阻塞是必须的,否则DQBUF在没帧的时候会把线程挂死,停止按钮点了没反应。打开失败最常见的原因是权限,/dev/video0一般属于video组,当前用户不在组里就是EACCES。

#include <linux/videodev2.h> #include <sys/ioctl.h> #include <sys/mman.h> #include <poll.h> #include <fcntl.h> #include <unistd.h> #include <errno.h> #include <cstring> bool V4L2Capture::openDevice(const QString &path, QString *err) { fd_ = ::open(path.toLocal8Bit().constData(), O_RDWR | O_NONBLOCK, 0); if (fd_ < 0) { *err = QString::fromLocal8Bit(strerror(errno)); return false; } v4l2_capability cap{}; if (::ioctl(fd_, VIDIOC_QUERYCAP, &cap) < 0) { *err = "VIDIOC_QUERYCAP failed"; return false; } // 部分驱动把真实能力放在 device_caps 里,capabilities 只是个超集 const uint32_t caps = (cap.capabilities & V4L2_CAP_DEVICE_CAPS) ? cap.device_caps : cap.capabilities; if (!(caps & V4L2_CAP_VIDEO_CAPTURE)) { *err = "not a video capture device"; return false; } if (!(caps & V4L2_CAP_STREAMING)) { *err = "device does not support streaming I/O"; return false; } return true; }

V4L2_CAP_DEVICE_CAPS这个分支要写。有些设备同时是采集端和输出端,capabilities会把两类能力都置位,直接判断会误判成支持采集。真正的能力位在device_caps里,只有置了V4L2_CAP_DEVICE_CAPS才有效。V4L2_CAP_STREAMING决定能不能用mmap那套,不支持的话只能走read()系统调用,性能差一个量级。

3.2 格式协商与缓冲区申请:S_FMT、REQBUFS、QUERYBUF、mmap

S_FMT是「请求」,不是「命令」。驱动完全有权把你的 1280x720 YUYV 改成 640x480 MJPG,而且返回值照样是 0。所以设完必须G_FMT读回来,把实际的宽高、像素格式、bytesperline全部记下来,后面算行偏移只用bytesperline,绝不能用width * 2。

bool V4L2Capture::configure(int w, int h, uint32_t pixfmt) { v4l2_format fmt{}; fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; fmt.fmt.pix.width = w; fmt.fmt.pix.height = h; fmt.fmt.pix.pixelformat = pixfmt; // V4L2_PIX_FMT_YUYV / V4L2_PIX_FMT_MJPEG fmt.fmt.pix.field = V4L2_FIELD_NONE; // 逐行扫描,UVC 基本都是这个 if (::ioctl(fd_, VIDIOC_S_FMT, &fmt) < 0) return false; // 关键:读回驱动真正生效的参数 if (::ioctl(fd_, VIDIOC_G_FMT, &fmt) < 0) return false; width_ = static_cast<int>(fmt.fmt.pix.width); height_ = static_cast<int>(fmt.fmt.pix.height); pixelFormat_ = fmt.fmt.pix.pixelformat; bytesPerLine_= static_cast<int>(fmt.fmt.pix.bytesperline); // 行跨度,不是 width*2 sizeImage_ = static_cast<int>(fmt.fmt.pix.sizeimage); return pixelFormat_ == pixfmt; // 格式被降级就返回 false,让上层决定要不要继续 }

缓冲申请用一个固定数量起步,通常是 4。有些 UVC 驱动对count很敏感,给 8 就返回EINVAL,所以申请完要检查req.count的实际值,驱动可能悄悄降到 2。

bool V4L2Capture::requestBuffers(int count) { v4l2_requestbuffers req{}; req.count = static_cast<uint32_t>(count); req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; req.memory = V4L2_MEMORY_MMAP; if (::ioctl(fd_, VIDIOC_REQBUFS, &req) < 0) return false; if (req.count < 2) // 少于 2 个缓冲必然卡顿 return false; buffers_.resize(req.count); for (uint32_t i = 0; i < req.count; ++i) { v4l2_buffer buf{}; buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; buf.index = i; if (::ioctl(fd_, VIDIOC_QUERYBUF, &buf) < 0) return false; buffers_[i].length = buf.length; buffers_[i].start = ::mmap(nullptr, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, buf.m.offset); if (buffers_[i].start == MAP_FAILED) { buffers_[i].start = nullptr; return false; } // 先全部入队,STREAMON 之后才能 dqbuf if (::ioctl(fd_, VIDIOC_QBUF, &buf) < 0) return false; } return true; }

buf.m.offset是驱动给的物理偏移,mmap的最后一个参数必须是它,不能填 0。MMAP失败常见于内核开启了CONFIG_STRICT_DEVMEM之类的限制,换到普通用户态跑一般就好了。

3.3 取流循环:poll + DQBUF + QBUF 的节奏控制

STREAMON之后进入死循环。用poll等可读事件,超时设 1000ms——这个超时不是为了防丢帧,是为了给停止按钮留出口。如果写-1永久阻塞,关窗口的时候线程停不下来,程序会挂在那里。

void V4L2Capture::run() { v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if (::ioctl(fd_, VIDIOC_STREAMON, &type) < 0) { emit errorOccurred(QStringLiteral("VIDIOC_STREAMON failed: %1") .arg(QString::fromLocal8Bit(strerror(errno)))); return; } running_ = true; while (running_) { pollfd pfd{fd_, POLLIN, 0}; const int r = ::poll(&pfd, 1, 1000); if (r < 0 && errno == EINTR) continue; if (r <= 0) continue; // 超时或被信号打断,回头检查 running_ if (!(pfd.revents & POLLIN)) continue; v4l2_buffer buf{}; buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; buf.memory = V4L2_MEMORY_MMAP; if (::ioctl(fd_, VIDIOC_DQBUF, &buf) < 0) { if (errno == EAGAIN) continue; // 非阻塞下的正常空转 break; } // 这里必须先把数据拷走或转换完,才能把缓冲还给驱动 handleFrame(static_cast<const uint8_t *>(buffers_[buf.index].start), static_cast<int>(buf.bytesused)); if (::ioctl(fd_, VIDIOC_QBUF, &buf) < 0) break; } ::ioctl(fd_, VIDIOC_STREAMOFF, &type); running_ = false; }

buf.bytesused是这一帧的字节数。YUYV 下它应该等于bytesPerLine * height;MJPEG 下每帧长度不一样,必须用它,不能用sizeImage_。handleFrame里绝对不要做耗时操作——写文件、发信号给 GUI 都可以,但写文件要快,否则缓冲池很快被抽干,DQBUF开始返回EAGAIN,画面就卡住不动了。

4. Qt 侧显示与录像:QImage 转换、线程投递和写入文件策略

采集线程拿到裸帧之后,Qt 这边还有三件事要做:转成 RGB32、跨线程送到 GUI、同时喂一份给录像。这三步里最容易出问题的是第二步,因为 QImage 的隐式共享和 mmap 缓冲区的生命周期正好冲突。

4.1 YUYV 转 RGB32:查表法比逐像素公式快在哪

YUYV 的排列是Y0 U Y1 V,两个像素共用一组 UV。标准转换公式里每个像素要做 4 次浮点乘加,720p 一帧就是 92 万像素、接近 400 万次浮点运算,30fps 下 CPU 直接被吃满。但注意,公式里的乘数只跟 U、V 的取值有关,U 和 V 都是 0~255,一共只有 256 种情况,完全可以提前算好。

// 256 项查找表:把 (u-128)、(v-128) 的乘加结果提前算完 void FrameConverter::initTables() { for (int i = 0; i < 256; ++i) { const int u = i - 128; const int v = i - 128; rTab_[i] = static_cast<int>( 1.402 * v); gTabU_[i] = static_cast<int>(-0.344136 * u); gTabV_[i] = static_cast<int>(-0.714136 * v); bTab_[i] = static_cast<int>( 1.772 * u); } } static inline int clamp8(int v) { return v < 0 ? 0 : (v > 255 ? 255 : v); } QImage FrameConverter::yuyvToRgb32(const uint8_t *src, int w, int h, int stride) { QImage img(w, h, QImage::Format_RGB32); // 这里已经分配了新内存 for (int y = 0; y < h; ++y) { const uint8_t *p = src + y * stride; // 用 stride,不是 w*2 auto *line = reinterpret_cast<QRgb *>(img.scanLine(y)); for (int x = 0; x < w; x += 2, p += 4) { const int y0 = p[0], u = p[1], y1 = p[2], v = p[3]; const int cr = rTab_[v]; const int gu = gTabU_[u], gv = gTabV_[v]; const int cb = bTab_[u]; line[x] = qRgb(clamp8(y0 + cr), clamp8(y0 + gu + gv), clamp8(y0 + cb)); line[x + 1] = qRgb(clamp8(y1 + cr), clamp8(y1 + gu + gv), clamp8(y1 + cb)); } } return img; }

scanLine(y)比bits() + y * bytesPerLine()更安全,因为 QImage 每行的实际跨度可能被 Qt 对齐过,bytesPerLine()返回的才是真实值。x += 2, p += 4的步进对应 YUYV 的 4 字节一组。查表之后每像素只剩 3 次整数加法和 3 次比较,720p 单帧转换能压到 3~5ms。

提示:Qt 5.4 之后有QImage::Format_YUYV,可以直接用裸指针构造 QImage 跳过转换。但QPainter往 RGB32 的窗口上画时会再做一次隐式转换,整体反而更慢,我一般还是自己转。

4.2 跨线程投递:为什么 QImage 必须深拷贝

这里有个玄学问题:代码看着没问题,跑起来画面偶尔撕裂,上半帧是这一帧的、下半帧是下一帧的。根因在 QImage 的隐式共享——它只是个数据描述符,构造时不一定拷贝像素。

// 错误做法:QImage 只包了一层指针,emit 之后 QBUF 立刻把缓冲还给驱动, // GUI 线程稍后真正绘制时,读到的已经是填充中的新一帧 QImage wrap(ptr, w, h, stride, QImage::Format_YUYV); emit frameReady(wrap); // 撕裂的源头 // 正确做法一:转换时就分配新内存(推荐,上面 yuyvToRgb32 已经这么做) emit frameReady(converter_.yuyvToRgb32(ptr, w, h, stride)); // 正确做法二:确实要复用外部缓冲时,显式断开共享 emit frameReady(wrap.copy());

copy()会真正分配一块内存并把像素拷过去,代价是每帧多一次 3.6MB 的 memcpy(720p RGB32),大概 1ms 左右,可以接受。如果是 MJPEG 格式,QImage::fromData(ptr, len, "JPG")内部已经解码到新内存,天然安全,不用再 copy。

还有一点容易被忽略:QImage跨线程传递走的是QueuedConnection,信号发出后帧会进事件队列排队。如果 GUI 线程被别的事情卡住,队列会堆积,内存涨得飞快。稳妥做法是在CameraWidget::setFrame里记录上一帧时间戳,间隔小于1000/fps就直接丢弃,保证队列长度始终是 1。

4.3 录像写入文件:ffmpeg 管道 vs 裸流直写

录像有两条路。MJPEG 格式下最简单:每帧本来就是一张完整 JPEG,直接fwrite追加到.mjpg文件,零 CPU 开销,缺点是产物需要 ffmpeg 转封装才能被播放器认。YUYV 格式下就必须编码,自己写 x264 调用太重,用QProcess把 ffmpeg 拉起来最省事。

bool Recorder::start(const QString &path, int w, int h, int fps, uint32_t pixfmt) { QStringList args; if (pixfmt == V4L2_PIX_FMT_YUYV) { // 未压缩裸流:必须显式告诉 ffmpeg 格式、分辨率、帧率 args << "-f" << "rawvideo" << "-pix_fmt" << "yuyv422" << "-s" << QString("%1x%2").arg(w).arg(h) << "-r" << QString::number(fps); } else { // MJPEG:每帧自带 SOI/EOI,ffmpeg 能自己切帧 args << "-f" << "mjpeg" << "-r" << QString::number(fps); } args << "-i" << "pipe:0" << "-c:v" << "libx264" << "-preset" << "ultrafast" // 本地录像优先不掉帧,不追求压缩率 << "-tune" << "zerolatency" << "-pix_fmt" << "yuv420p" // 兼容性最好的输出格式 << "-y" << path; proc_.setProgram("ffmpeg"); proc_.setArguments(args); proc_.start(QIODevice::WriteOnly); return proc_.waitForStarted(3000); }

写管道的时候有两个坑。一是 ffmpeg 处理不过来时write()会阻塞,如果用 GUI 线程调用会直接界面假死,所以writeFrame只能在采集线程里调。二是必须检查返回值。

void Recorder::writeFrame(const uint8_t *data, int len) { if (!recording_ || proc_.state() != QProcess::Running) return; // 堆积超过 4MB 说明磁盘或编码器跟不上,丢帧比堆内存好 if (proc_.bytesToWrite() > 4 * 1024 * 1024) return; const qint64 n = proc_.write(reinterpret_cast<const char *>(data), len); if (n != len) emit errorOccurred(QStringLiteral("视频帧写入不完整: %1/%2").arg(n).arg(len)); }

bytesToWrite()是 QProcess 内部缓冲里还没被 ffmpeg 读走的字节数,超过阈值就丢帧是一个很实用的削峰手段——录像里偶尔少一帧,比内存一路涨到 OOM 好得多。-preset ultrafast是关键参数,它把 x264 的搜索范围压到最小,720p30 在普通四核上单核就能吃住。

5. 踩坑与排查:五条把画面跑黑的真实记录

这条链路上的报错信息极其吝啬,ioctl失败基本只给你一个EINVAL,剩下的全靠自己定位。下面五条是按发生频率排的,每条都按「现象 → 原因 → 解决」写,遇到问题可以直接对照。

5.1 打开与协商阶段的坑

现象一:open成功,QUERYCAP也成功,但VIDIOC_STREAMON返回 -1,errno是EINVAL。

原因:REQBUFS申请的缓冲数量超过了驱动支持的上限。很多 UVC 驱动内部只维护 2~4 个 MMAP 缓冲,代码里写死count = 8就会在STREAMON阶段炸掉,而且报错位置和真正的原因差了三步,极难联想。

解决:count从 4 起步,REQBUFS之后一定要读req.count的实际返回值并用它来 resize 数组,不要用自己传进去的那个数。如果 4 还是失败,降到 2 再试一次。

现象二:S_FMT返回 0,看日志一切正常,但收到的帧是绿屏或者斜条纹。

原因:驱动把 YUYV 降级成了别的格式,或者改了bytesperline。代码里还用width * 2当行偏移,实际驱动给的是width * 2对齐到 32 字节之后的值,每行错几个字节,累积起来就是斜纹。

解决:S_FMT之后必须G_FMT读回实际参数,行偏移只用fmt.fmt.pix.bytesperline。同时校验fmt.fmt.pix.pixelformat是否等于请求的格式,不等就打印出来,别硬着头皮往下跑。

5.2 取流与显示阶段的坑

现象三:预览跑三四秒后画面定住,日志里DQBUF一直返回EAGAIN,但进程没崩。

原因:缓冲池被抽干了。某个分支拿到了buf却没还回去——比如handleFrame里抛了异常、或者提前return、或者QBUF的返回值没检查。四个缓冲全部被拿走不还,驱动自然没帧可给。

解决:把QBUF用 RAII 包起来,构造时接管buf,析构时无条件归还;或者在DQBUF成功之后立刻算出index,所有分支统一走一个goto/lambda 归还。我一般会在QBUF前后加计数日志,正常应该严格 1:1。

现象四:画面能出来,但偶发撕裂,快速移动的物体边缘有明显断层。

原因:emit出去的是包着 mmap 裸指针的 QImage,GUI 线程还没画,采集线程已经QBUF把缓冲还给驱动,驱动往里灌下一帧,读到一半新旧混在一起。

解决:转换函数内部就分配新内存(上面yuyvToRgb32的做法),或者在emit前显式.copy()。判断标准很简单——只要 QImage 是用外部指针构造的,就必须 copy。

5.3 录像阶段的坑

现象五:录出来的 mp4 时长看着对,但画面明显快进或慢放,声音(如果有)也对不上。

原因:写进管道的帧率和 ffmpeg 的-r参数不一致。摄像头上报 30fps,实际驱动可能只给 15fps,rawvideo又没有时间戳,ffmpeg 只能按-r硬算,于是 15 帧被当成 30 帧播,速度直接翻倍。

解决:用VIDIOC_G_PARM读回真实的timeperframe,用它去设置-r;或者给 ffmpeg 加-use_wallclock_as_timestamps 1,让它按到达时间打时间戳。验证方法是录一段带秒表的画面,播出来对一下。

6. 二次开发:从能跑到好用,扩展点在哪

能跑通之后,需求一般会往三个方向长:调图像参数、加多路、换硬编码。这三个方向的改法完全不同,我按投入产出比排一下。

扩展方向主要接口改动量注意点
亮度/对比度/曝光VIDIOC_QUERYCTRL+VIDIOC_S_CTRL小,半天每个控制项要先查范围和步长
多路摄像头每路一个 fd + 一个 V4L2Capture 实例中,两天带宽和 USB 控制器分配是瓶颈
硬件编码V4L2 M2M(/dev/video11之类)大,一周起各平台节点名和格式都不同

调图像参数是最容易见效的。UVC 摄像头基本都暴露了一组标准控制项,用VIDIOC_QUERYCTRL枚举一遍,把名字、最小值、最大值、步长、默认值全打出来,直接生成滑条。

// 枚举设备支持的所有控制项,喂给界面生成滑条 void V4L2Capture::enumerateControls() { v4l2_queryctrl qc{}; qc.id = V4L2_CTRL_FLAG_NEXT_CTRL; // 从第一个有效控制项开始遍历 while (::ioctl(fd_, VIDIOC_QUERYCTRL, &qc) == 0) { if (!(qc.flags & V4L2_CTRL_FLAG_DISABLED)) { qDebug() << QString::fromLocal8Bit((const char *)qc.name) << qc.minimum << qc.maximum << qc.step << qc.default_value; } qc.id |= V4L2_CTRL_FLAG_NEXT_CTRL; // 继续找下一个 } } // 设置曝光:注意 V4L2_CID_EXPOSURE_ABSOLUTE 的单位是 100us bool V4L2Capture::setControl(uint32_t id, int value) { v4l2_control c{}; c.id = id; c.value = value; return ::ioctl(fd_, VIDIOC_S_CTRL, &c) == 0; }

V4L2_CTRL_FLAG_NEXT_CTRL这个用法要记牢,配合按位或不断推进qc.id,是标准遍历姿势。V4L2_CID_EXPOSURE_ABSOLUTE的单位是 100 微秒,传 100 表示 10ms,第一次写很容易把单位搞错,导致画面要么死黑要么过曝。

多路摄像头的坑在 USB 带宽。同一个 USB 控制器下面挂两个 720p YUYV 摄像头基本跑不动,因为 YUYV 是未压缩的,720p30 单路就要 220MB/s 以上,远超 USB 2.0 的 480Mbps 理论上限。解决办法是切到 MJPEG 格式,帧率能保住,代价是每帧要解码一次。

验证改动的习惯:每次改完格式协商相关代码,先别开 GUI,用ffmpeg -f v4l2 -input_format yuyv422 -video_size 1280x720 -i /dev/video0 -t 5 test.mp4单独跑一遍,确认设备本身能出正确的流,再去排 Qt 那边的代码。这一步能把问题范围砍掉一半。

从那以后我每次碰新摄像头,第一件事都是把VIDIOC_ENUM_FMT和VIDIOC_G_PARM的结果完整打一遍日志,把「设备声称支持的」和「实际生效的」两列摆在一起看,再写采集代码。这个习惯省下来的排查时间,比任何调试技巧都值。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 12:52:29

epoll原理与实战:高并发网络编程的核心机制

1. 这不是另一个select/poll教程&#xff1a;epoll到底在解决什么真实问题&#xff1f;你刚接手一个日均百万连接的实时消息中台&#xff0c;上线第三天凌晨两点&#xff0c;监控告警疯狂闪烁——CPU飙到98%&#xff0c;连接数卡在4096不再增长&#xff0c;新用户进不来&#x…

作者头像 李华
网站建设 2026/10/26 12:49:39

AI Agent在金融风险评估中的核心技术与应用实践

1. AI Agent如何重塑金融风险评估的底层逻辑 金融行业每天产生的数据量相当于一个小型图书馆的藏书&#xff0c;传统风险评估模型就像用算盘处理这些数据——效率低下且容易出错。AI Agent的出现彻底改变了这一局面&#xff0c;它不仅是简单的算法升级&#xff0c;而是构建了一…

作者头像 李华
网站建设 2026/10/26 12:47:07

贝叶斯优化LSTM模型在时间序列预测中的应用

1. 项目概述&#xff1a;贝叶斯优化LSTM时间序列预测模型 这个项目实现了一个端到端的时间序列预测解决方案&#xff0c;核心是用贝叶斯优化自动调参的LSTM神经网络处理单变量时间序列数据。我在金融、气象、工业设备监测等多个领域实际应用过这类模型&#xff0c;最大的优势是…

作者头像 李华
网站建设 2026/10/26 12:46:58

Transformer核心逻辑与魔改实践:注意力、残差与位置编码的细节

一个经常魔改神经网络架构的人&#xff0c;自然免不了反复拆解和重组各种模型组件。Transformer这个架构&#xff0c;可以说是绕不开的“工作对象”。说实话&#xff0c;拆过的网络多了&#xff0c;你会发现很多结构都是纸老虎&#xff0c;换几个数据集就现原形。但Transformer…

作者头像 李华
网站建设 2026/10/26 12:46:15

大模型预训练与微调核心技术解析

1. 大模型技术概述&#xff1a;预训练与微调的核心逻辑 在人工智能领域&#xff0c;大模型技术已经成为推动行业发展的核心驱动力。作为从业者&#xff0c;我见证了大模型从实验室走向产业落地的全过程。预训练与微调这两个关键环节&#xff0c;构成了大模型技术栈的基石。理解…

作者头像 李华
网站建设 2026/10/26 12:45:56

STM32 PWM精确输出N个脉冲的硬件同步控制方法

简介&#xff1a;本资源是面向嵌入式开发初学者与STM32电机控制实践者的完整技术方案包&#xff0c;聚焦于利用STM32定时器PWM功能实现步进电机与舵机的精确脉冲数定位控制——解决传统PWM仅调速不控位、脉冲计数易丢失等工程痛点。压缩包含196个文件&#xff0c;以23个.h头文件…

作者头像 李华