前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Qt6 QML 中渲染自定义视频帧的改进 2023-05-30 更新

Qt6 QML 中渲染自定义视频帧的改进 2023-05-30 更新

作者头像
我与梦想有个约会
发布2023-10-21 15:40:14
6462
发布2023-10-21 15:40:14
举报
文章被收录于专栏:jiajia_dengjiajia_deng

最近在升级音视频的项目 Qt 版本,从 5.15.0 升级到 6.4.3(6.5 也一样),除了一些 QML 中删除了一些 Qt Quick Controls 1 的控件以外,最重要的就是自定义视频渲染的改进。

QAbstractVideoSurface 变为 QVideoSink

Qt5 中在 QML 上渲染自定义视频帧时需要在 C++ 层实现一个派生于 QObject 的子类,内部使用 QAbstractVideoSurface 来给 VideoOutput 提供数据,具体方法这里就不讨论了,可以参考我之前写的文章 Qt QML VideoOutput 显示自定义的 YUV420P 数据流 在 Qt6 中,QAbstractVideoSurface 被 QVideoSink 替代,提供了更简单的方式来投递一个 QVideoFrame,示例代码如下:

代码语言:javascript
复制
class FrameProvider : public QObject {
    Q_OBJECT
    Q_PROPERTY(QVideoSink* videoSink READ videoSink WRITE setVideoSink NOTIFY videoSinkChanged)

public:
    explicit FrameProvider(QObject* parent = nullptr);
    ~FrameProvider();

    QVideoSink* videoSink() const { return m_videoSink; }
    void setVideoSink(QVideoSink* videoSink);

signals:
    void videoSinkChanged();

public slots:
    void deliverFrame(const QVideoFrame& frame);

private:
    QPointer<QVideoSink> m_videoSink;
};

类声明一个槽函数 deliverFrame 提供视频帧提供的模块绑定并投递帧数据。在 cpp 实现中只如果有新的视频流,则直接调用 m_videoSink 的 setVideoFrame 方法就可以了:

代码语言:javascript
复制
void FrameProvider::deliverFrame(const QVideoFrame& frame) {
    if (!m_videoSink)
        return;
    m_videoSink->setVideoFrame(frame);
}

将 FrameProvider 按上面文章中的方法一样,注册给到 QML 端,与 VideoOutput 配合使用时也稍微有一些变动:

代码语言:javascript
复制
FrameProvider {
    id: frameProvider
    videoSink: videoContainer.videoSink
}
VideoOutput {
    id: videoContainer
    anchors.fill: parent
    fillMode: VideoOutput.PreserveAspectFit
}

这样 VideoOutput 与新的 FrameProvider 配合使用就完成了,接下来我们说一下 QVideoFrame 的变动:

QVideoFrame 数据拷贝方式的变动

在 Qt5 中,如拷贝 YUV 数据到 QVideoFrame 的方式非常暴力,通过 videoFrame.bits() 拿到地址算好位置无脑拷贝就可以了:

代码语言:javascript
复制
int frameSize = static_cast<int>(frame.width * frame.height * frame.count / 2);
QVideoFrame videoFrame(frameSize, QSize(static_cast<int>(rotationWidth), static_cast<int>(rotationHeight)), static_cast<int>(rotationWidth),
                       QVideoFrame::Format_YUV420P);

if (videoFrame.map(QAbstractVideoBuffer::WriteOnly)) {
    auto src = reinterpret_cast<uint8_t*>(frame.data);
    auto dest = reinterpret_cast<uint8_t*>(videoFrame.bits());

    libyuv::I420Rotate(src + frame.offset[0], static_cast<int>(frame.stride[0]),
                       src + frame.offset[1], static_cast<int>(frame.stride[1]),
                       src + frame.offset[2], static_cast<int>(frame.stride[2]),
                       dest, static_cast<int>(rotationWidth),
                       dest + rotationWidth * rotationHeight, rotationWidth / 2,
                       dest + rotationWidth * rotationHeight + rotationWidth * rotationHeight / 4, rotationWidth / 2,
                       static_cast<int>(frame.width), static_cast<int>(frame.height), rotate_mode);

    videoFrame.setStartTime(0);
    videoFrame.unmap();

    QSize size = QSize(static_cast<int>(rotationWidth), static_cast<int>(rotationHeight));
    emit VideoManager::m_videoFrameDelegate->receivedVideoFrame(QString::fromStdString(accountId), videoFrame, size, bSub);
}

但 Qt6 中出现了较大的变动,首先 bits 函数要求传递目标数据的 plane,比如 Y plane 为 0,U 和 V 依次为 1 和 2。这看起来跟 Qt5 中没有什么太大区别,但如果你按 bits(0)、bits(1)、bits(1) 的地址按原来的逻辑拷贝时会发现部分分辨率的图像会渲染错乱,这基本上是因为原始的 YUV 数据宽度并不是 16 的倍数。而 QVideoFrame 一旦调用了 map 函数,则每个 plane 的 stride(在 Qt 中称为 bytesPerLine) 将会是 16 的倍数,如果你按原始数据宽度拷贝,就会导致画面错乱。 正确的做法是通过 QVideoFrame 提供的 bytesPerLine() 方法算出具体每个 plane 的宽度,按需拷贝,实现如下:

代码语言:javascript
复制
QVideoFrameFormat format(QSize(rotationWidth, rotationHeight), QVideoFrameFormat::Format_YUV420P);
format.setViewport(QRect(0, 0, rotationWidth, rotationHeight));
QVideoFrame videoFrame(format);
if (videoFrame.map(QVideoFrame::WriteOnly)) {
    auto src = reinterpret_cast<uint8_t*>(frame.data);
    // If the aspect ratio of the original data is not a multiple of 16,
    // when mappedBytes(n) is called after frame mapping, the returned size will be expanded to the nearest multiple of 16.
    // When copying the data, bytesPerLine(n) should be used to get the actual stride that needs to be copied.
    libyuv::I420Rotate(src + frame.offset[0], frame.stride[0],
                       src + frame.offset[1], frame.stride[1],
                       src + frame.offset[2], frame.stride[2],
                       videoFrame.bits(0), videoFrame.bytesPerLine(0),
                       videoFrame.bits(1), videoFrame.bytesPerLine(1),
                       videoFrame.bits(2), videoFrame.bytesPerLine(2),
                       frame.width, frame.height, rotate_mode);
    videoFrame.setStartTime(0);
    videoFrame.unmap();
    QSize size = QSize(static_cast<int>(rotationWidth), static_cast<int>(rotationHeight));
    emit VideoManager::m_videoFrameDelegate->receivedVideoFrame(QString::fromStdString(accountId), videoFrame, size, bSub);
}

其中 frame.data 是 YUV 的原始数据。通过改动后的 QVideoFrame API 我们可以看到,Qt 对视频处理数据的要求更加严谨了,虽然处理问题过程中浪费了比较多的时间,但总算总结下了一些宝贵的经验。

2023-05-30 更新

以上拷贝方式当使用 Qt 6.x 版本默认的渲染引擎(OpenGL)时一些奇葩的分辨率会出现花屏的问题。修改 Qt 的渲染引擎为各平台特有引擎后得以解决:

代码语言:javascript
复制
int main(int argc, char* argv[]) {
    QGuiApplication app(argc, argv);

#if defined(Q_OS_MACX)
    QQuickWindow::setGraphicsApi(QSGRendererInterface::Metal);
#else
    QQuickWindow::setGraphicsApi(QSGRendererInterface::Direct3D11);
#endif
    ..... other code
}
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023-04-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • QAbstractVideoSurface 变为 QVideoSink
  • QVideoFrame 数据拷贝方式的变动
  • 2023-05-30 更新
相关产品与服务
媒体处理
媒体处理(Media Processing Service,MPS)是一种云端音视频处理服务。基于腾讯多年音视频领域的深耕,为您提供极致的编码能力,大幅节约存储及带宽成本、实现全平台播放,同时提供视频截图、音视频增强、内容理解、内容审核等能力,满足您在各种场景下对视频的处理需求。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档