简介一份基于Qt6.3.1与取景器帧机制的自定义视频源测试工程适合正在学习Qt6/QML视频处理或需要自定义视频输入源的开发者。工程展示了如何将捕获会话中的取景器帧接入QML界面并形成完整的视频源使用流程帮助读者绕开常见帧格式转换与线程同步障碍。压缩包共13个文件以cpp源文件、qml界面文件、h头文件为主另有qrc资源文件与pro工程文件其中cpp与h负责C侧视频帧获取及业务封装qml承担界面展示qrc集中管理资源pro便于直接构建整体体积仅11KB内容精简适合快速阅读与二次修改。已有348人学习下载作者为aggs1990。通过阅读该测试源码可重点掌握QML与C交互、QVideoFrame帧获取与传递、视频源组件注册调用方式以及取景器帧的生命周期管理尤其在自定义视频源场景下能帮助理解帧回调触发时机、QML层纹理更新和C对象暴露等关键点减少重复调试整体结构清晰可作为中小型Qt视频项目的起步模板。1. 从取景器到 QML搞懂 Qt6 自定义视频源的数据通路用 Qt 做客户端时摄像头预览通常用一个VideoOutput指向系统默认源就完了但一旦你想在每帧里做滤镜、叠加文字或接入虚拟摄像头信号就不能再走系统自带通路。Qt6 把取景器帧的获取和输出分成了两个独立环节QMediaCaptureSession负责采集QVideoSink负责收发原始帧。这次拿到的测试源码正是围绕这条通路展开它演示了如何用 C 抓取摄像头取景帧经过自绘处理后重新注入 QML 的VideoOutput。这个工程适合两类人一类是刚转 Qt6 发现 API 变化踩坑的 QML 开发者另一类是想理解视频帧生命周期、打算做帧级处理或推流前置处理的人。接下来会按文件结构、核心实现、编译排错的顺序把它拆开。2. 拆解 frameprovidertestqt6 工程文件结构与模块职责2.1 工程文件与 Qt6 模块依赖拿到压缩包解压后第一件事是看.pro和.pro.user。.pro.user是 Qt Creator 的本地配置不同机器上路径不同可以忽略。关键在frameprovidertestqt6.pro它决定了链接哪些模块。QT quick quickwidgets multimedia multimediawidgets CONFIG c17 TARGET frameprovidertestqt6 SOURCES \ main.cpp \ tool.cpp \ tool_mediacapturesession.cpp HEADERS \ tool.h \ tool_mediacapturesession.h RESOURCES qml.qrcmultimedia和multimediawidgets是视频采集与帧处理的核心缺了multimediawidgets会导致QVideoSink相关符号找不到。quickwidgets在纯 QML 工程里不是必须的但测试源码里通过QQuickWidget混用所以保留。编译平台选 MSVC 2019 64bit 或 MinGW 11.2 都可以Qt 6.3.1 对两者的支持都没问题。2.2 C 侧QMediaCaptureSession 与视频帧获取工程里最值得读的文件是tool_mediacapturesession.h它是个中介层。tool.h里的Tool类负责 UI 交互逻辑而ToolMediaCaptureSession专门封装摄像头会话。// tool_mediacapturesession.h class ToolMediaCaptureSession : public QObject { Q_OBJECT Q_PROPERTY(QVideoSink* videoSink READ videoSink CONSTANT) public: explicit ToolMediaCaptureSession(QObject *parent nullptr); QVideoSink* videoSink() const { return m_sink; } public slots: void start(); void stop(); private: QCamera *m_camera; QMediaCaptureSession *m_session; QVideoSink *m_sink; };注意videoSink用Q_PROPERTY暴露这样 QML 里可以直接把它绑到VideoOutput的source属性上。m_session负责把摄像头和视频输出关联起来构造函数里这样写ToolMediaCaptureSession::ToolMediaCaptureSession(QObject *parent) : QObject(parent) , m_camera(new QCamera(this)) , m_session(new QMediaCaptureSession(this)) , m_sink(new QVideoSink(this)) { m_session-setCamera(m_camera); m_session-setVideoSink(m_sink); connect(m_sink, QVideoSink::videoFrameChanged, this, ToolMediaCaptureSession::onFrameReady); }setVideoSink是 Qt6 替代旧版setViewfinder的关键 API。它接受一个QVideoSink所有采集到的帧都会以QVideoFrame的形式从这个 sink 的videoFrameChanged信号吐出来。你在槽函数里能拿到QVideoFrame的像素格式、大小、数据指针这也是后续自定义视频源的基础。2.3 QML 侧VideoOutput 与 frameProvider 桥接QML 里不用关心摄像头是怎么打开的只需要拿一个VideoOutput控件。// Home.qml import QtQuick 2.15 import QtMultimedia 5.15 Rectangle { color: #1e1e1e VideoOutput { id: viewfinder anchors.fill: parent source: frameProvider.videoSink } }注意source属性接受的是QVideoSink*不是QMediaCaptureSession*。也就是说你可以在任意时刻把videoSink从 provider 里取出来重新指向另一个采集源QML 端的控件不需要改动。这比 Qt5 时代用QAbstractVideoSurface注册ImageProvider后绕道QQuickImageProvider要直接得多少了两层封装帧延迟也低。3. 核心实现在 C 里产出自定义视频帧并推给 QML3.1 自定义帧处理槽函数VideoSink收到的原始帧可能是 NV12、YUV420P 或 ARGB32不同驱动差异很大。为了在 QML 里稳定显示最稳妥的做法是先把原始帧转成QVideoFrameFormat::Format_ARGB8888再做自定义处理最后通过m_sink-setVideoFrame(processedFrame)发出去。void ToolMediaCaptureSession::onFrameReady(const QVideoFrame frame) { if (!frame.isValid()) return; QVideoFrame f frame; if (f.map(QVideoFrame::ReadOnly)) { // 转换成 QImage 便于画线和文本 QImage image(f.bits(0), f.width(), f.height(), QImage::Format_RGBA8888); QPainter painter(image); painter.setPen(Qt::yellow); painter.setFont(QFont(Consolas, 24)); painter.drawText(10, 40, FrameProviderDebug); painter.end(); f.unmap(); // 构造新帧并发送 QVideoFrame outFrame(image, QVideoFrameFormat(f.size(), QVideoFrameFormat::Format_ARGB8888)); m_sink-setVideoFrame(outFrame); } }这段代码里有几个坑QVideoFrame::map成功后bits(0)只对单个平面有效对于 NV12 这种多平面的格式需要遍历所有平面。上面直接假设Format_RGBA8888但在实际工程里要先判frame.pixelFormat()否则会拿到空指针。另外QVideoFrameFormat构造函数的参数顺序是(QSize, PixelFormat)不要写反。setVideoFrame是QVideoSink的自定义输入入口它不只接受摄像头传进来的帧自己构造的帧也能通过它注入到 VideoOutput 里。这也是“自定义视频源”的核心你可以完全绕开摄像头从一个QVideoFrame生成器里推送任意内容。3.2 帧格式转换与 QVideoFrame 构造在 3.1 的例子里我们用了QImage作为中间层因为它能直接交给QPainter做绘制。但注意从QVideoFrame到QImage并不总是能零拷贝。推荐的路径是先判断frame.pixelFormat()如果是Format_NV12或Format_YUV420P就先转成QImage再做后续操作。// 工具函数带像素格式检查 static QImage frameToImage(const QVideoFrame frame) { if (frame.pixelFormat() QVideoFrameFormat::Format_ARGB8888) { return QImage(frame.bits(0), frame.width(), frame.height(), QImage::Format_ARGB32); } // YUV - RGB 转换交给 QtVideoHelper QVideoFrameFormat fmt(frame.size(), QVideoFrameFormat::Format_RGBX8888); QVideoFrame conv(fmt); // 实际工程会调用 QVideoFrame::map 逐像素转换 return QImage(conv.bits(0), conv.width(), conv.height(), QImage::Format_RGB32); }真实的相机帧通常是Format_NV12你可以在自己的测试里加一个qWarning() frame.pixelFormat()输出看下实际值。然后写对应的转换函数。如果只是想在 QML 里看效果也可以不去转换直接原样推给 sinkVideoOutput的 shader 能处理常见格式但那样就没法QPainter画叠加信息。3.3 在 QML 中注册与调用工程里的HomeForm.ui.qml是 Qt Design Studio 生成的模板Home.qml是它的填充实例。在 QML 里访问 C 对象需要先注册上下文属性也可以像测试源码一样用qmlRegisterSingletonInstance。// main.cpp #include QGuiApplication #include QQmlApplicationEngine #include QQmlContext #include tool_mediacapturesession.h int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); qmlRegisterTypeToolMediaCaptureSession(FrameProvider, 1, 0, FrameProvider); QQmlApplicationEngine engine; engine.load(QUrl(QStringLiteral(qrc:/main.qml))); return app.exec(); }在 QML 里使用import FrameProvider 1.0 FrameProvider { id: frameProvider }注意这种写法要求类有无参构造函数。如果还要给摄像头做参数配置可以在类里加Q_PROPERTY暴露选择设备 ID 的接口。qmlRegisterType的模块名和类型名可以自定义但必须和 QML 里import的模块名一致否则会报module not found。4. 编译、运行与常见坑4.1 Qt6.3.1 环境配置与编译步骤用 Qt Creator 打开frameprovidertestqt6.pro选择 Qt 6.3.1 套件直接构建。如果报无法打开 QVideoSink.h检查.pro里有没有加multimediawidgets。如果报moc: tool_mediacapturesession.h: No such file确认手动添加的文件路径和HEADERS一致。# 命令行编译示例Windows MSVC mkdir build cd build qmake ..\frameprovidertestqt6.pro nmakeqmake会生成 Makefilenmake执行编译。如果使用 MinGW则用mingw32-make。编译成功后在 Qt Creator 里把start槽函数连到一个按钮上摄像头才会真正打开。源码里可能默认没调用start()你需要检查Home.qml中是否有一个Component.onCompleted或按钮事件。4.2 运行时 QML 加载失败排查VideoOutput的source必须是QVideoSink*。如果你在 QML 里写了source: frameProvider.videoSink但运行时提示Cannot assign [undefined]说明videoSink属性返回为空。在构造函数里setVideoSink(m_sink)之后一定要把m_sink声明成成员变量否则被编译器优化掉后返回悬空指针。错误现象可能原因排查方法module FrameProvider not found注册模块名与 import 不一致检查qmlRegisterType参数Cannot assign [undefined] to QVideoSink*videoSink属性未初始化在构造函数中断点调试黑屏无画面摄像头权限未开启Windows 设置里允许应用访问相机编译报错QVideoSink: No such file缺少 multimediawidgets在 .pro 中追加 QT multimediawidgets4.3 帧率控制与内存拷贝问题摄像头帧率可能高于你期望的处理帧率常见做法是丢弃中间帧if (frame.isValid() frame.startTime() - m_lastTs (1000000000 / m_targetFps) ) { // 处理该帧 m_lastTs frame.startTime(); }startTime()返回的是微秒单位要除以 1000000 才是毫秒。直接用QVideoFrame::setStartTime也可以但用时间戳做节流比用QTimer更准确。注意m_lastTs要在start()槽里重置为 0否则第一次进槽时差值可能非常大导致第一帧被丢弃。5. 进阶把帧处理挂进渲染循环5.1 使用 QAbstractVideoBuffer 避免拷贝每次setVideoFrame都构造新对象如果帧尺寸是 4K每帧拷贝 8MB 内存性能极差。Qt6 提供了QAbstractVideoBuffer的接口可以让我们把QVideoFrame的缓冲区复用。class SharedBuffer : public QAbstractVideoBuffer { public: SharedBuffer(QImage img) : QAbstractVideoBuffer(QVideoFrame::NoHandle) , m_image(img) {} MapData map(QVideoFrame::MapMode mode) override { MapData data; data.nPlanes 1; data.bytesPerLine[0] m_image.bytesPerLine(); data.data[0] m_image.bits(); data.size[0] m_image.sizeInBytes(); return data; } void unmap() override {} private: QImage m_image; };实际使用时把m_image作为成员重复利用只在第一次分配后续帧处理直接在这个 image 上画然后构造QVideoFrame时传入 buffer。QVideoFrame frame(new SharedBuffer(m_img), QVideoFrameFormat(size, QVideoFrameFormat::Format_ARGB8888)); m_sink-setVideoFrame(frame);这能省掉至少一次memcpy。注意QVideoFrame的生命周期它内部持有 buffer 的引用计数可以安全地在多个线程间传递。SharedBuffer的map返回的指针会一直被QVideoFrame引用所以m_image必须在类中存活足够长的时间推荐用std::shared_ptrQImage管理。5.2 扩展多路视频源切换如果你想在 QML 里动态切换摄像头和自定义生成器可以把多个QVideoSink封装成不同 provider然后在 QML 里用Connections动态改source。VideoOutput { id: video source: cameraProvider.videoSink } Button { onClicked: { video.source generatorProvider.videoSink } }VideoOutput的source不是常量绑定支持运行时更换。切换时会自动断开旧 sink 的帧更新不需要手动 release。这里有一个小技巧如果切换后出现短暂黑屏可以在切换前后各强制走一帧空数据比如先把source置为null再赋新值强制更新渲染节点。最后补充一个调试技巧在onFrameReady槽里加一条qDebug() frame.pixelFormat() frame.width() frame.height();先确认帧能够到达 C 侧再判断是转换问题还是 QML 绑定问题。帧数据通路通常是顺时针的问题多数出在像素格式不匹配导致的黑屏。本文还有配套的精品资源点击获取