Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Qt6 QML 中渲染自定义视频帧的改进 2023-05-30 更新

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

作者头像
我与梦想有个约会
发布于 2023-10-21 07:40:14
发布于 2023-10-21 07:40:14
1.5K20
代码可运行
举报
文章被收录于专栏:jiajia_dengjiajia_deng
运行总次数:0
代码可运行

最近在升级音视频的项目 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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
void FrameProvider::deliverFrame(const QVideoFrame& frame) {
    if (!m_videoSink)
        return;
    m_videoSink->setVideoFrame(frame);
}

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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
代码运行次数:0
运行
AI代码解释
复制
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 删除。

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

评论
登录后参与评论
2 条评论
热度
最新
楼主你好,可以提供一份这个工程的源码学习吗? 最近在捣鼓Qt 6.3 qml播放视频的东西,但是发现qml这边的MediaPlayer不能设置解码器,感觉需要从c++这边进行解码在放给qml播放。 仅自学!十分感谢!
楼主你好,可以提供一份这个工程的源码学习吗? 最近在捣鼓Qt 6.3 qml播放视频的东西,但是发现qml这边的MediaPlayer不能设置解码器,感觉需要从c++这边进行解码在放给qml播放。 仅自学!十分感谢!
回复回复点赞举报
兄弟我很想学习学习你的这个工程的源码
兄弟我很想学习学习你的这个工程的源码
回复回复点赞举报
推荐阅读
编辑精选文章
换一批
RFID-RC522的使用[通俗易懂]
射频识别技术RFID(Radio Frequency Identification),又称为电子标签、无线射频识别,是一种非接触式的自动识别技术,通过无线电讯号识别特定目标并读写相关数据而无需识别系统与特定目标之间建立机械或光学接触。可用于识别高速运动物体并可同时识别多个标签,过程中无需人工干预,操作快捷方便。可工作于各种环境,实现对各类物体或设备(人员、物品)在不同状态(移动、静止或恶劣环境)下的自动识别和管理。
全栈程序员站长
2022/07/29
1.6K0
RFID-MFRC522射频识别模块,S50卡M1
1、M1卡分为16个扇区,每个扇区由4块(块0、块1、块2、块3)组成,(我们也将16个扇区的64个块按绝对地址编号为0~63,存贮结构如下图所示:
全栈程序员站长
2022/09/14
1.5K0
RFID-MFRC522射频识别模块,S50卡M1
STM32+MFRC522完成IC卡号读取、密码修改、数据读写
完整工程源码下载: https://download.csdn.net/download/xiaolong1126626497/18905806
DS小龙哥
2022/01/17
4.7K0
STM32+MFRC522完成IC卡号读取、密码修改、数据读写
RC522(RFID模块)实践总结
此次使用RC522模块和S50卡实现近场通讯功能(开发板与RC522通讯方式为硬件SPI),就实践过程中的一些知识点进行总结:
全栈程序员站长
2022/07/22
3.7K0
RC522(RFID模块)实践总结
STM32–RFID无线射频技术(RC522刷卡模块)
 射频识别,即RFID是Radio Frequency Ident ificat ion的缩写,又称无线射频识别,是一.种通信技术,可通过无线电讯号识别特定目标并读写相关数据,而无需识别系统与特定目标之间建立机械或光学接触。一套完整RFID硬件统由Reader 与Transponder 两部份组成,其动作原理为由Reader 发射一特定频率之无限电波能量给Transponder,用以驱动Transponder电路将內部之ID Code送出,此时Reader便接收此ID Code Transponder的特殊在于免用电池、免接触、免刷卡故不怕脏污,且晶片密码为世界唯一无法复制, 安全性高、长寿命。
全栈程序员站长
2022/09/17
3.7K0
STM32–RFID无线射频技术(RC522刷卡模块)
rfid-rc522使用教程_RFID读写方式是什么
我们常见的RC522大概如下所示,PCB部分是主机,然后白色的和绿色的都是IC卡,IC卡可以存储信息,通过靠近PCB主机部分就可以被感应到从而触发主机做出相应的动作,比如读取IC卡信息,写入数据等操作。
全栈程序员站长
2022/10/02
2.3K0
rfid-rc522使用教程_RFID读写方式是什么
RFID RC522门禁系统「建议收藏」
RFID RC522门禁系统说明: 基于51单片机的RFID RC522门禁系统的主要功能是,在本系统中主要是演示了RFID RC522门禁系统,在单片机内部的RAM中IDBUFFER中存了学号:1505106001对应 16进制就是5b b6 18 51 ;然后在RFID RC522开卡系统中,为第一张卡在块地址0x08处,写入了学号,当卡中的内容和单片机上的内容相配时,门打开。 具体程序如下所示: MAIN.C
全栈程序员站长
2022/10/02
9350
ESP32开发之旅——RC522模块的使用
​ 射频识别RFID(Radio Frequency Identification)是一种无线数据传输系统,用于在标签和读取器设备之间传输数据,而RC522模块则是用于读取和写入RFID卡和标签,该模块的工作频率为13.56MHz。
全栈程序员站长
2022/09/14
2.1K0
STM32F103+RFID-RC522模块 实现简单读卡写卡demo「建议收藏」
本文不含任何广告性质,仅供学习参考。写卡需谨慎!!!,不然可能会玩崩了。血的教训!!!
全栈程序员站长
2022/07/23
3.7K0
STM32F103+RFID-RC522模块 实现简单读卡写卡demo「建议收藏」
RFID-RC522/STM32F103RB/KEIL5 简单实现读取卡片ID[通俗易懂]
在这篇文章【 https://blog.csdn.net/qq_28877125/article/details/80437095 】的基础上修改完成!
全栈程序员站长
2022/09/29
1.7K0
RFID-RC522/STM32F103RB/KEIL5 简单实现读取卡片ID[通俗易懂]
rc522 nfc_基于单片机的门禁系统
(2021/11/1编辑) 在项目需要做一个NFC门禁功能的时候,突然发现有个RC522丢在我的桌面,甚至不知道它上面的引脚什么意思(还不会SPI通讯),搜索关键词“RC522”去看博客搜索资料,发现了很多都在说扇区,块,S50(M1)卡,然后就给代码,一开始我还以为S50是内嵌在这个模块里面的一个存储器,然后越看越怪,后面去淘宝搜索S50,才发现S50其实是我们的门禁卡,RC522是用来感应和判断的。
全栈程序员站长
2022/09/29
1.1K0
rc522 nfc_基于单片机的门禁系统
单片机_MFRC522射频模块使用方法(含代码)
522模块总共有8个引脚,除去复位、GND接地、3.3V电源、NC端悬空、SCK时钟端,剩余3个引脚,起数据作用。
全栈程序员站长
2022/09/17
2.3K0
单片机_MFRC522射频模块使用方法(含代码)
基于HL-1开发板开发RFID(RC522模块)射频电路基础
图片中重点写出引脚的相应接口名称。 —————————————————————手动分割线———————————————————–
全栈程序员站长
2022/09/14
1.3K0
基于HL-1开发板开发RFID(RC522模块)射频电路基础
STM32F103ZET–RFID-RC522使用例程(战舰版)
每每有陌生人加我就是问我要这个的工程,心累,文末有工程下载链接。希望再有人加我QQ是跟我聊技术,而不是:“大神,能发个工程给我吗?”
全栈程序员站长
2022/07/22
6780
基于esp8266开发板实现宿舍门禁
菜菜有点菜
2023/12/11
2270
rfid-rc522模块中文资料_驱动模块
1.每张卡有唯一的序列号,32位 2.卡的容量是8Kbit的EEPROM 3.分为16个扇区,每个扇区分为4块,每块16个字节,以块为存取单位 4.每个扇区都有独立的一组密码和访问控制
全栈程序员站长
2022/11/04
3.6K1
rfid-rc522模块中文资料_驱动模块
PS2手柄遥控Arduino小车[通俗易懂]
使用手柄遥控小车是经常要用到的,看到PS2手柄很6,就拿来尝试一下。 PS2手柄是索尼的PlayStation2游戏机的遥控手柄,因为这款手柄性价比较高,按键丰富,方便扩展到其它应用中,后来有人将其通讯协议破解,使得手柄可以用在遥控其他电器上,比如遥控控制机器人小车。
全栈程序员站长
2022/07/01
2.9K0
PS2手柄遥控Arduino小车[通俗易懂]
基于STM32的RC522模块读写数据块以及电子钱包充值扣款系统的设计
本人也是正在学习单片机知识的萌新一枚,在这里记录下自己完成这个小设计的过程跟大家分享一下,也请大家指出我哪里还有不足可以改进的地方。秉着和大家一起学习进步发布了这篇文章
全栈程序员站长
2022/10/02
3.1K2
基于STM32的RC522模块读写数据块以及电子钱包充值扣款系统的设计
基于stm32门禁系统_老式门禁
RC522射频门禁识别模块非常常用,某宝卖家提供的程序基本都是使用软件模拟SPI的方式进行驱动的,但是实测使用软件模拟SPI识别速率、准确性没有硬件SPI驱动时高,因此本篇博客用于记录使用STM32硬件SPI驱动RC522门禁模块。
全栈程序员站长
2022/09/30
1K0
基于stm32门禁系统_老式门禁
RC522 射频读卡器模块(MINI型)
二、[主芯片介绍] MF RC522是应用于13.56MHz非接触式通信中高集成度的读写卡芯片,是NXP公司针对“三表”应用推出的一款低电压、低成本、体积小的非接触式读写卡芯片,是智能仪表和便携式手持设备研发的较好选择。 MF RC522利用了先进的调制和解调概念,完全集成了在13.56MHz下所有类型的被动非接触式通信方式和协议。支持14443A兼容应答器信号。数字部分处理ISO14443A帧和错误检测。此外,还支持快速CRYPTO1加密算法,用语验证MIFARE系列产品。MFRC522支持MIFARE系列更高速的非接触式通信,双向数据传输速率高达424kbit/s。 作为13.56MHz高集成度读写卡系列芯片家族的新成员,MF RC522与MF RC500和MF RC530有不少相似之处,同时也具备许多特点和差异。它与主机间通信采用连线较少的串行通信,且可根据不同的用户需求,选取SPI、IIC或串行UART模式之一,有利于减少连线,缩小PCB板体积,降低成本。
全栈程序员站长
2022/09/06
1.3K0
RC522 射频读卡器模块(MINI型)
推荐阅读
相关推荐
RFID-RC522的使用[通俗易懂]
更多 >
交个朋友
加入前端学习入门群
前端基础系统教学 经验分享避坑指南
加入前端工作实战群
前端工程化实践 组件库开发经验分享
加入前端趋势交流群
追踪前端新趋势 交流学习心得
换一批
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验