9

Qt6 QML 中渲染自定义视频帧的改进

 1 year ago
source link: https://www.mycode.net.cn/language/cpp/3189.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

最近在升级音视频的项目 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,示例代码如下:

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 方法就可以了:

void FrameProvider::deliverFrame(const QVideoFrame& frame) {
    if (!m_videoSink)
        return;
    m_videoSink->setVideoFrame(frame);
}

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

FrameProvider {
    id: frameProvider
    videoSink: videoContainer.videoSink
}
VideoOutput {
    id: videoContainer
    anchors.fill: parent
    fillMode: VideoOutput.PreserveAspectFit
}

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

QVideoFrame 数据拷贝方式的变动

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

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 的宽度,按需拷贝,实现如下:

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 对视频处理数据的要求更加严谨了,虽然处理问题过程中浪费了比较多的时间,但总算总结下了一些宝贵的经验。

相关

2015-06-23_164236

Qt 信号和槽机制详解

2015年6月23日

在“C/C++”中


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK