使用 QQuickFramebufferObject 实现高性能实时视频显示
在开发 Qt/QML 应用时经常会遇到需要显示实时视频流摄像头、网络流、采集卡的场景。直接使用 QML 的Image或VideoOutput可能无法满足低延迟、高帧率或自定义渲染的需求。这时QQuickFramebufferObject就是你的最佳选择。本文将从零开始搭建一个完整的实时视频显示框架涵盖多线程数据读取、跨进程共享内存通信、FBO 渲染以及C/QML 混合编程并解释每个设计背后的原理。一、整体架构概览我们设计的系统分为以下五个层次视频采集设备捕获每一帧如相机、文件、网络流将帧数据写入共享内存QSharedMemory发送一条“新帧就绪”消息给主程序通过QLocalSocket、QDBus或自定义消息主程序接收采集设备的消息将消息转发给负责显示的 QML 组件通过信号/槽或直接调用视频显示界面VideoDisplayItem继承自QQuickFramebufferObject拥有一个独立的工作线程FrameReaderThread提供槽函数onNewFrameAvailable()供主程序调用工作线程FrameReaderThread从共享内存中读取原始帧数据转换为QImage通过信号将帧发送回主线程的VideoDisplayItemFBO 渲染器VideoFrameRenderer继承自QQuickFramebufferObject::Renderer在 Qt Quick 场景图渲染线程中运行将QImage上传为 OpenGL 纹理并绘制到屏幕下图展示了线程模型与数据流向二、为什么需要分离QQuickFramebufferObject和Renderer很多初学者会问为什么要写成两个类而不是直接在QQuickFramebufferObject里实现绘制核心原因线程模型分离QQuickFramebufferObject运行在GUI 主线程负责处理属性绑定、信号槽、用户交互、数据更新等逻辑。QQuickFramebufferObject::Renderer运行在Qt Quick 场景图渲染线程这是一个独立于 GUI 线程的专用渲染线程。如果把 OpenGL 绘制代码写在QQuickFramebufferObject里会导致渲染与主线程事件循环竞争造成界面卡顿或崩溃。无法与 Qt Quick 的渲染管线同步可能出现撕裂或闪烁。多线程安全问题Qt Quick 可能在任意时刻要求重建 FBO。通过Renderer的synchronize()虚函数可以在渲染线程中安全地复制GUI 线程的最新数据例如新视频帧然后在render()中执行实际绘制。这是保证高性能和线程安全的基石。三、详细设计与代码实现3.1 共享内存访问工作线程FrameReaderThread工作线程负责从共享内存中读取帧数据它运行在独立的QThread中。使用QSharedMemory需要配合跨进程锁这里为了简洁只演示基本用法实际项目建议使用QSystemSemaphore。// FrameReaderThread.h #include QObject #include QImage #include QSharedMemory class FrameReaderThread : public QObject { Q_OBJECT public: explicit FrameReaderThread(QObject *parent nullptr); ~FrameReaderThread(); void setSharedMemoryKey(const QString key); public slots: void init(); // 在工作线程启动后调用attach 共享内存 void readFrame(int frameId, qint64 timestamp); // 主程序通知新帧时调用 signals: void frameReady(const QImage frame); // 将读取到的帧发回主线程 private: QSharedMemory *m_sharedMem; QString m_key; bool m_initialized; };// FrameReaderThread.cpp #include FrameReaderThread.h #include QDebug FrameReaderThread::FrameReaderThread(QObject *parent) : QObject(parent), m_sharedMem(nullptr), m_initialized(false) {} FrameReaderThread::~FrameReaderThread() { if (m_sharedMem) { m_sharedMem-detach(); delete m_sharedMem; } } void FrameReaderThread::setSharedMemoryKey(const QString key) { m_key key; } void FrameReaderThread::init() { if (m_initialized || m_key.isEmpty()) return; m_sharedMem new QSharedMemory(m_key); if (!m_sharedMem-attach()) { qWarning() Failed to attach to shared memory: m_sharedMem-errorString(); delete m_sharedMem; m_sharedMem nullptr; return; } m_initialized true; } void FrameReaderThread::readFrame(int frameId, qint64 timestamp) { if (!m_initialized || !m_sharedMem) return; // 实际使用中需要加锁此处假设采集端已保护共享内存 if (!m_sharedMem-lock()) return; const uchar *data static_castconst uchar*(m_sharedMem-constData()); if (data) { // 假设共享内存结构[帧大小(uint)] [图像数据(RGB32)] uint size *reinterpret_castconst uint*(data); const uchar *imageData data sizeof(uint); QImage received(imageData, 640, 480, QImage::Format_RGB32); // 宽高已知 QImage copy received.copy(); // 深拷贝因为共享内存马上解锁 emit frameReady(copy); } m_sharedMem-unlock(); }3.2 视频显示界面VideoDisplayItem这个类继承自QQuickFramebufferObject并管理工作线程的生命周期。它暴露给 QML 使用并提供onNewFrameAvailable槽供主程序调用。// VideoDisplayItem.h #include QQuickFramebufferObject #include QThread #include QMutex class FrameReaderThread; class VideoDisplayItem : public QQuickFramebufferObject { Q_OBJECT Q_PROPERTY(QString sharedMemoryKey READ sharedMemoryKey WRITE setSharedMemoryKey NOTIFY sharedMemoryKeyChanged) public: explicit VideoDisplayItem(QQuickItem *parent nullptr); ~VideoDisplayItem(); Renderer *createRenderer() const override; QString sharedMemoryKey() const { return m_key; } void setSharedMemoryKey(const QString key); public slots: // 由主程序调用通知新帧到达 void onNewFrameAvailable(int frameId, qint64 timestamp); signals: void sharedMemoryKeyChanged(); private slots: void onFrameReady(const QImage frame); // 接收工作线程发来的帧 private: QString m_key; QThread *m_workerThread; FrameReaderThread *m_reader; QImage m_currentFrame; QMutex m_mutex; };// VideoDisplayItem.cpp #include VideoDisplayItem.h #include FrameReaderThread.h VideoDisplayItem::VideoDisplayItem(QQuickItem *parent) : QQuickFramebufferObject(parent) { m_workerThread new QThread(this); m_reader new FrameReaderThread(); m_reader-moveToThread(m_workerThread); connect(m_workerThread, QThread::started, m_reader, FrameReaderThread::init); connect(m_reader, FrameReaderThread::frameReady, this, VideoDisplayItem::onFrameReady); m_workerThread-start(); } VideoDisplayItem::~VideoDisplayItem() { m_workerThread-quit(); m_workerThread-wait(); } void VideoDisplayItem::setSharedMemoryKey(const QString key) { if (m_key key) return; m_key key; m_reader-setSharedMemoryKey(key); emit sharedMemoryKeyChanged(); } void VideoDisplayItem::onNewFrameAvailable(int frameId, qint64 timestamp) { // 跨线程调用工作线程的 readFrame 槽 QMetaObject::invokeMethod(m_reader, readFrame, Qt::QueuedConnection, Q_ARG(int, frameId), Q_ARG(qint64, timestamp)); } void VideoDisplayItem::onFrameReady(const QImage frame) { QMutexLocker locker(m_mutex); m_currentFrame frame; update(); // 通知 FBO 需要重绘 } QQuickFramebufferObject::Renderer *VideoDisplayItem::createRenderer() const { return new VideoFrameRenderer(const_castVideoDisplayItem*(this)); }3.3 FBO 渲染器VideoFrameRenderer这是真正执行 OpenGL 绘制的类运行在渲染线程中。它通过synchronize()从VideoDisplayItem获取最新的m_currentFrame然后上传纹理并绘制。// 内嵌在 VideoDisplayItem.cpp 中 class VideoFrameRenderer : public QQuickFramebufferObject::Renderer, protected QOpenGLFunctions { public: VideoFrameRenderer(VideoDisplayItem *item) : m_item(item) { initializeOpenGLFunctions(); } void synchronize(QQuickFramebufferObject *item) override { VideoDisplayItem *display static_castVideoDisplayItem*(item); QMutexLocker locker(display-m_mutex); m_currentFrame display-m_currentFrame; if (!m_currentFrame.isNull()) { if (!m_texture || m_texture-width() ! m_currentFrame.width()) { m_texture.reset(new QOpenGLTexture(m_currentFrame)); m_texture-setMinificationFilter(QOpenGLTexture::Linear); m_texture-setMagnificationFilter(QOpenGLTexture::Linear); } else { m_texture-setData(m_currentFrame); } } } void render() override { if (!m_texture) return; QOpenGLFunctions *f QOpenGLContext::currentContext()-functions(); f-glClearColor(0,0,0,1); f-glClear(GL_COLOR_BUFFER_BIT); // 绑定纹理并绘制全屏四边形简化示例实际需要 shader 和顶点数据 m_texture-bind(); // ... 绘制代码略 ... m_texture-release(); } private: VideoDisplayItem *m_item; QImage m_currentFrame; std::unique_ptrQOpenGLTexture m_texture; };3.4 暴露给 QML 并让 C 主程序获取实例为了让主程序能够调用VideoDisplayItem::onNewFrameAvailable我们需要在 QML 中创建VideoDisplay实例并让主程序持有它的指针。常用方法有三种通过objectName查找、通过上下文属性传递、或采用注册代理对象的方式。这里推荐一种清晰且安全的方案定义一个代理类MyVideo由它持有VideoDisplayItem*并暴露给 QML。QML 中的VideoDisplay在Component.onCompleted时将自己注册到MyVideo中。// MyVideo.h #include QObject #include QPointer #include VideoDisplayItem.h class MyVideo : public QObject { Q_OBJECT public: explicit MyVideo(QObject *parent nullptr); Q_INVOKABLE void setVideoDisplay(QQuickItem *item); public slots: void onNewFrameAvailable(int frameId, qint64 timestamp); private: QPointerVideoDisplayItem m_display; };// MyVideo.cpp void MyVideo::setVideoDisplay(QQuickItem *item) { m_display qobject_castVideoDisplayItem*(item); } void MyVideo::onNewFrameAvailable(int frameId, qint64 timestamp) { if (m_display) m_display-onNewFrameAvailable(frameId, timestamp); }在main.cpp中注册类型并注入MyVideo实例#include QGuiApplication #include QQmlApplicationEngine #include QQmlContext #include VideoDisplayItem.h #include MyVideo.h int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); qmlRegisterTypeVideoDisplayItem(MyApp, 1, 0, VideoDisplay); MyVideo myVideo; QQmlApplicationEngine engine; engine.rootContext()-setContextProperty(myVideo, myVideo); engine.load(QUrl(qrc:/main.qml)); // 主程序收到采集设备消息时只需调用 myVideo.onNewFrameAvailable() return app.exec(); }在 QML 中import MyApp 1.0 VideoDisplay { id: videoView anchors.fill: parent sharedMemoryKey: MyVideoStream Component.onCompleted: myVideo.setVideoDisplay(videoView) }这样主程序就可以安全地通过myVideo对象将消息转发给实际的显示界面。四、关键设计点总结线程分离VideoDisplayItem主线程数据接收、属性更新FrameReaderThread工作线程阻塞式共享内存读取Renderer渲染线程OpenGL 绘制三者通过QMutex和跨线程信号槽安全通信。共享内存访问采集端和显示端使用相同的QSharedMemory键名务必配合跨进程锁如QSystemSemaphore避免数据竞争性能优化工作线程读取后立即深拷贝QImage避免后续访问共享内存纹理上传在synchronize()中同步进行若需要更高性能可改用 PBO 异步传输通过update()的频率节流例如 60fps 定时器防止过度重绘生命周期安全使用QPointer或QWeakPointer持有 QML 对象的指针防止悬挂工作线程必须在VideoDisplayItem析构前停止并等待五、运行与测试编写好上述代码后在 QML 中放置一个VideoDisplay元素并实现一个模拟的采集设备例如定时器写入共享内存。运行时你会看到视频帧流畅地显示在窗口中并且 UI 操作如拖动窗口、点击按钮不会导致画面卡顿。六、扩展与进阶多视频源可以创建多个VideoDisplayItem实例并使用不同的共享内存键名。格式支持如果采集端是 YUV 格式渲染器需要编写对应的片段着色器进行色彩转换。Qt 6 与 QRhi从 Qt 6 开始推荐使用QQuickRhiItem它不依赖 OpenGL支持 Vulkan/Metal/Direct3D。但原理类似只是将 OpenGL 函数替换为 QRhi 命令。