Qt QML VideoOutput 显示自定义的 YUV420P 数据流

在一些传统应用中,如果想使用 Qt 在 QWidget 或者 QML 中显示自定义的视频数据流,需要引入 OpenGL 来实现。而实际 Qt 已经准备了 VideoOutput 类型可以很方便的调用系统摄像头和使用自定义数据流。在 Qt 官网中,VideoOutput 的介绍中说明,source 属性可以是一个自定义派生于 QObject 的子类,并提供一个类型为 QMediaObject 的属性命名为 mediaObject,或者是一个派生与 QObject 的子类并提供一个类型为 QAbstractVideoSurface 的属性命名为 videoSurface。其中任意一个方法都可以实现自定义视频数据流的播放,本文介绍第二种方法。

从 QObject 继承并提供 videoSurface 属性给 QML

像 Stackoverflow 中的介绍,你需要这样一个类,该类用 Q_PROPERTY 宏提供了一个名字为 videoSurface 的属性(符合 source 属性的第二个要求)

#include <QObject>  #include <QAbstractVideoSurface>  #include <QVideoSurfaceFormat>    class FrameProvider : public QObject  {      Q_OBJECT      Q_PROPERTY(QAbstractVideoSurface *videoSurface READ videoSurface WRITE setVideoSurface)      public:      QAbstractVideoSurface* videoSurface() const { return m_surface; }    private:      QAbstractVideoSurface *m_surface = NULL;      QVideoSurfaceFormat m_format;    public:          void setVideoSurface(QAbstractVideoSurface *surface)      {          if (m_surface && m_surface != surface  && m_surface->isActive()) {              m_surface->stop();          }            m_surface = surface;            if (m_surface && m_format.isValid())          {              m_format = m_surface->nearestFormat(m_format);              m_surface->start(m_format);            }      }        void setFormat(int width, int heigth, int format)      {          QSize size(width, heigth);          QVideoSurfaceFormat format(size, format);          m_format = format;            if (m_surface)          {              if (m_surface->isActive())              {                  m_surface->stop();              }              m_format = m_surface->nearestFormat(m_format);              m_surface->start(m_format);          }      }    public slots:      void onNewVideoContentReceived(const QVideoFrame &frame)      {          if (m_surface)              m_surface->present(frame);      }  };

将对象注入到全局 Context 提供 QML 使用

Stackoverflow 的方法是将 FrameProvider 注册成一个 QML 可以使用的类型,这种方法也可以,但是你可以看到在 main 函数中需要去从 QML 中搜索该类实例化的对象句柄,然后再绑定信号和槽,这个相对麻烦一些。我们换一种方式就是先 new 对象然后绑定信号和槽函数,最后再把对象注入到全局上下文中,让 QML 在任意位置都可以访问这个对象。方法如下:

FrameProvider* provider = new FrameProvider();  CustomFramesource source;    // Set the correct format for the video surface (Make sure your selected format is supported by the surface)  provider->setFormat(source.width,source.height, source.format);    // Connect your frame source with the provider  QObject::connect(&source, SIGNAL(newFrameAvailable(const QVideoFrame &)), provider, SLOT(onNewVideoContentReceived(const QVideoFrame &)));    QQmlApplicationEngine engine;  engine.rootContext()->setContextProperty("frameProvider", frameProvider);    // .... Other code

QML 中引用

由于全局注入了 frameProvider 类型,在 VideoOutput 中直接指定给 source 属性就可以了。

import QtQuick 2.9  import QtQuick.Controls 2.2  import QtQuick.Controls.Material 2.2  import QtMultimedia 5.4    ApplicationWindow {      objectName: "mainWindow"      visible: true      width: 640      height: 480        VideoOutput {          id: display          anchors.top: parent.top          anchors.bottom: parent.bottom          width: parent.width          source: frameProvider      }  }

构造数据源

当你收到一帧 YUV 数据的时候(上面代码中 CustomFramesource 类要做的事情),你需要把这一帧数据转换为 QVideoFrame 并发送信号 newFrameAvailable,这样 frameProvider 类收到信号后就会将这一帧的数据通知给 QML 来显示。具体转换的代码如下:

QVideoFrame f(size, QSize(width, height), width, QVideoFrame::Format_YUV420P);  if (f.map(QAbstractVideoBuffer::WriteOnly)) {      memcpy(f.bits(), data, size);      f.setStartTime(0);      f.unmap();      emit newFrameAvailable(f);  }

代码中使用了 QVideoFrame 的第二个构造函数,先根据视频数据大小创建一个空闲位置,然后 map 这块位置到内存,拷贝数据进去,最后 unmap 并发送信号给 provider 使用。这里如果考虑性能问题,可以将 QVideoFrame 使用智能指针传递,这样可以减少数据的拷贝过程。