ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

RK3588上mpp硬解码视频叠加QT:实现与踩坑指南

RK3588上mpp硬解码视频叠加QT:实现与踩坑指南 简介本资源是一套基于RK3588平台实现视频解码与Qt图形界面叠加的完整开发方案面向嵌入式Linux开发者、音视频应用工程师及国产化平台学习者解决Rockchip平台下硬件加速解码MPP/Rockit与Qt UI同屏渲染的技术难点。压缩包共64个文件含33个头文件如mpp_frame.h、rk_mpi.h、drm_mode.h等支撑底层编解码与DRM显示、9个动态库librockchip_mpp.so、librockit.so、libdrm.so等核心依赖、3个关键源码文件main.cpp、myrk_decode.cpp、myrk_drm.cpp及Qt工程配置rkDecode.pro、qml.qrc、main.qml另有说明文档与备份文件整体大小为8.71MB。已有400人学习下载提供可直接交叉编译运行的工程结构支持TARGET自定义输出名附带DRMRGA显示通路与VPU硬解集成逻辑代码经实机验证可在RK3588开发板上稳定实现H.264/H.265视频流与Qt控件的低延迟叠加渲染。1. 项目背景RK3588上做视频叠加QT为什么值得单独聊聊RK3588芯片今年在边缘计算、智能终端和工业HMI里出现频率很高8核大小核架构加独立的NPU、GPU、VPU规格在同级别SoC里确实能打。我最近接手的一个项目要在RK3588平台上做视频画面和QT界面的叠加底层是摄像头实时画面或者本地视频流上层要动态显示状态信息、控制按钮、告警图标视频不能掉帧QT响应不能卡顿。这个需求听上去不复杂但在RK3588上落地时比x86平台麻烦不少主要原因是视频链路和GUI渲染在嵌入式平台上天然地分属不同硬件通路怎么把两条通路的输出“叠”到一个屏幕上是核心问题。搜资料的时候你会看到很多帖子都在提rockit和mpp这两个词容易让人混淆。我最初也以为它们是同一个东西的两种叫法实际做下来发现两者定位完全不同mpp是瑞芯微的媒体处理库负责硬编硬解rockit是更上层的视频通路SDK包含采集、处理、输出整套能力。本文会把这两条技术路线的区别讲清楚给出我实测可用的视频叠加QT的实现方案包括关键代码结构、参数配置和调试过程中踩过的坑希望对正在RK3588平台上做类似需求的工程师有帮助。这个内容适合谁看如果你正准备在RK3588上跑QT应用且应用需要显示视频画面或者你已经调通了mpp解码/rockit视频输出但不知道怎么和QT界面融合这篇文章应该能帮你省下不少试错时间。2. 选型分析rockit和mpp到底怎么选四种叠加方案的取舍2.1 先分清rockit和mpp它们是两个层级的东西mppMedia Process Platform是RK平台底层的多媒体编解码库纯C接口负责H.264/H.265/VP9等格式的硬件解码和编码也支持图像缩放、色彩转换等后期处理。它的粒度比较细适合只需要“解码出帧”的场景。你可以把mpp理解成一个高效的视频解码器输入码流输出裸帧。rockit是瑞芯微更上层的一套多媒体SDK基于mpp但也整合了VI视频输入、VPSS视频处理子系统、VENC编码、VO视频输出等模块。它更偏“通路”概念比如从MIPI摄像头采集RAW数据经过ISP处理再通过VO输出到HDMI整条链路rockit都能接管。rockit的定位是完整的视频流水线不是单一的编解码工具。知道了这个区别选型的逻辑就清楚了如果只需要解码视频文件或网络流拿到帧后自己处理用mpp就够了简单直接如果要做拍照、录像、RTSP推流、摄像头ISP处理、多路视频拼接这套能力rockit封装得更完整如果只做本地视频解码再叠加QT把rockit整个拉进来有些重但如果你同时还需要摄像头通路和显示输出管理rockit反而更省事。2.2 视频叠加QT的四种路径各有各的坑我在RK3588上试过多种视频和QT叠加的方式实际可用的路径大致有四条这里先把它们拉出来对比一下。实现路径原理性能集成难度适用场景方案Ampp解码帧转QImage在QLabel/自定义控件里显示解码线程拿帧转成RGB888 QImage通过信号抛给UI线程刷新1080P30下CPU占用偏高内存拷贝开销大低快速原型、对延迟不敏感方案Bmpp解码后通过RGA转格式走零拷贝或DMA-BUF方式喂给QT用RGA做颜色空间转换和缩放避免CPU转换性能较好GPU和RGA分担负载中对帧率有要求、资源较紧方案Crockit VO直接输出视频画面到显示层QT窗口透明悬浮在上面硬件视频层显示内容GUI层叠加最佳视频不占CPU/GPU高需要多路视频、低延迟、界面不复杂方案DQT自身走EGL渲染视频纹理QOpenGLWidget绑定外部纹理显示视频帧较好但实现复杂高需要特效、旋转缩放等方案A是很多初学者第一反应的做法也是最容易实现但性能最不理想的做法。方案C是rockit VO QT透明窗口这个组合在RK3588上效果很惊艳但配置过程繁琐因为涉及DRM显示平面的分配。我最终根据项目需求选择了“mpp解码 RGA转换 QImage显示”的折中路线同时把方案C的VO叠加跑通作为后续演进方向。2.3 我为什么放弃“只推rockit”的念头刚开始看rockit文档时我觉得直接用rockit的VO输出视频层QT只做一个透明窗覆盖上去这个方案最“正统”。但实际操作后我发现rockit VO的输出依赖显示链路需要你管理DRM/KMS的plane。如果你的QT窗口是普通的X11或Wayland窗口系统透明叠加的行为在不同桌面环境下并不一致调试成本很高。大部分RK3588的嵌入式场景是不跑完整桌面的测试板上往往直接跑Qt eglfs或者linuxfb。linuxfb不支持透明窗口eglfs下窗口叠加能力也很有限这就让“透明悬浮”方案在执行层面受限。相比之下方案A/B把视频帧直接纳入了QT的绘制体系逻辑统一跨平台迁移也容易。所以我把“解码由mpp负责”这个思路作为切入点把QT界面做成普通应用通过信号槽接收帧并绘制这条路是确定可行的。3. 核心细节mpp解码链路与QT显示的衔接3.1 mpp解码器初始化的几个关键参数mpp的解码接口位于rockchip_mpp.h虽然API整体比较底层但解码流程并不复杂核心是四步创建上下文、配置解码属性、送入码流数据、获取解码帧。下面是我整理的最小初始化逻辑。MppCtx ctx NULL; MppApi *mpi NULL; MppPollType timeout MPP_POLL_NON_BLOCK; // 1. 创建解码上下文 mpp_create(ctx, mpi); // 2. 初始化解码器H264 RK_U32 codingType MPP_VIDEO_CodingAVC; mpi-control(ctx, MPP_DEC_SET_CODEC_TYPE, codingType); // 3. 准备解码需要的buffer分组 MppBufferGroup frmGrp NULL; mpp_buffer_group_get_internal(frmGrp, MPP_BUFFER_TYPE_ION); mpi-control(ctx, MPP_DEC_SET_EXT_BUF_GROUP, frmGrp); // 4. 启动解码 mpp_init(ctx, MPP_CTX_DEC, MPP_VIDEO_CodingAVC);其中第3步特别关键。我一开始没有设置外部buffer group解码虽然能出帧但每次帧数据都维护在解码器内部后续需要拿到数据时要么拷贝、要么等内部回调效率会打折扣。用ION buffer直接映射到用户空间后续配合DMA-BUF或者直接CPU访问都很方便。编码格式需要根据你的码流动态决定MPP_VIDEO_CodingAVC是H.264如果是H.265就用MPP_VIDEO_CodingHEVCMPEG4、VP9等也有对应的枚举值。如果你解码的是网络流前面还有一层解协议的操作mpp不关心封装格式只吃纯ES流。注意mpp默认的解码输出帧格式是NV12即YUV420SP。这跟QT默认的RGB888差距很大不能直接显示必须做格式转换我在3.2节会重点说。3.2 NV12帧到QImage的转换关键是别在错误的地方做转换mpp解码器输出的帧数据在MppFrame结构体里你可以通过mpp_frame_get_buf拿到buffer地址通过mpp_frame_get_width和mpp_frame_get_height拿到宽高通过mpp_frame_get_hor_stride获得水平stride。这里的stride值得注意解码器为了内存对齐每行实际的字节数通常会大于图像宽度尤其当宽度不是16的倍数时这个差异很明显。直接按图像宽度去解析NV12数据结果就是画面有“斜纹”或者整体错位这是典型的新手坑。正确的做法是指针偏移按stride计算// 获取解码帧信息 RK_U32 width mpp_frame_get_width(frame); RK_U32 height mpp_frame_get_height(frame); RK_U32 h_stride mpp_frame_get_hor_stride(frame); RK_U32 v_stride mpp_frame_get_ver_stride(frame); MppBuffer buf mpp_frame_get_buffer(frame); RK_U8 *y_addr (RK_U8 *)mpp_buffer_get_ptr(buf); RK_U8 *uv_addr y_addr h_stride * v_stride;拿到Y和UV平面地址后再做NV12转RGB888。这个转换用CPU做很慢1080P单帧就有1920x1080个像素点逐点算RGB大概接近300万次浮点运算跑起来会占掉不少CPU。我推荐用RK3588的RGA硬件模块做转换RGA是瑞芯微自带的2D图形加速器既能做色彩空间转换又能做缩放。RGA的调用通常通过librga库完成接口比较简单核心是三步准备源图信息、准备目标图信息、调用rk_rga_blit。我在这里把源图尺寸填width/height但地址偏移已经按stride调整过这样RGA转换后的RGB数据就是连续且正确的。转换后的数据可以直接构造QImageQImage rgbImage(rgbBuf, width, height, QImage::Format_RGB888);这里还有一个容易出错的点QImage的构造虽然传入了数据指针但默认并不会拷贝数据而是直接使用这块内存。如果你的rgbBuf是解码器内部的buffer下一次解码循环可能被覆盖所以要么在QImage构造后立即调用copy()复制一份要么手动管理buffer的生命周期。我在代码里选择复制因为视频帧数据量1080P RGB888约6MB一次memcpy的耗时可以接受。3.3 解码线程和UI线程怎么协作这是整个项目的骨架视频叠加QT的架构本质上是一个典型的生产者-消费者模型解码线程是生产者QT主线程是消费者。我见过不少开发者把解码逻辑直接写在QT的定时器槽函数里结果界面一卡一卡的原因就在于解码是耗时操作阻塞了UI事件循环。我的做法是单独起一个std::thread做解码循环解码出帧后用信号槽或者事件机制通知UI线程解码线程拿到MppFrame通过RGA转成RGB888数据封装成一个自定义结构体VideoFrame内部包含QImage通过QMetaObject::invokeMethod或者emit frameReady(videoFrame)发送给UI线程UI线程在槽函数里更新QLabel或自定义绘制控件。这里要注意线程信号的连接方式默认情况下跨线程信号槽是QueuedConnection参数会被拷贝。如果VideoFrame里包含QImageQImage本身是隐式共享的跨线程传递时注意避免在原线程继续修改同一份数据否则会出现绘制错乱。如果想把延迟压得更低可以直接在UI线程里做轻量轮询通过原子变量标记新帧是否到达但信号槽的方式代码更清晰1080P30的帧率下完全够用。3.4 QT侧显示视频的控件选择显示视频的控件有两种常见选择QLabel嵌套QPixmap或者继承QWidget重写paintEvent。QLabel的方式最简单ui-videoLabel-setPixmap(QPixmap::fromImage(videoFrame.img));但这种方式每次setPixmap都会构造新的QPixmap在30fps下会有额外的内存分配和释放开销。更高效的方式是继承QWidget重写paintEvent直接绘制QImagevoid VideoWidget::paintEvent(QPaintEvent *event) { QPainter painter(this); painter.drawImage(this-rect(), m_image); }paintEvent里只做绘制不处理逻辑。当UI线程从解码线程接收到新帧后更新m_image并调用update()请求重绘。这样即便帧率稍高CPU开销也稳定可控。我实测在RK3588上跑1080P30的H.264视频源mpp解码 RGA转RGBUI线程paintEvent绘制整个流程CPU占用大约在12%到18%之间剩余资源足够跑其他业务逻辑。如果你想把CPU占用压得更低可以走方案C的VO输出视频完全由硬件层负责QT只做叠加层。4. 实操落地mpp QT叠加的最小工程4.1 编译环境准备和mpp集成RK3588的SDK里已经包含了mpp源码建议优先使用板子对应版本的mpp库避免API不匹配。你可以用源码编译也可以直接用SDK预编译的librockchip_mpp.so。如果你用的是Buildroot或Debian镜像一般会有mpp头文件在/usr/include/rockchip/目录下库文件在/usr/lib/librckmpp.so或/usr/lib/librockchip_mpp.so。编译时在CMakeLists.txt里加上find_library(MPP_LIB NAMES rockchip_mpp rkmp mpp) include_directories(/usr/include/rockchip) target_link_libraries(video_overlay ${MPP_LIB} ${RGA_LIB})RGA库通常是librga头文件在/usr/include/librga/RgaApi.h。如果你的SDK里没有librga可以单独编译安装源码位置在SDK的external/librga目录下。提示RK3588的librga和RK3399等老平台接口不兼容移植时注意检查头文件版本新版librga用的是rga_set_src_info和rga_set_dst_info这样的接口老的rk_rga_blit在新版本里成了兼容垫片。4.2 最小工程的代码骨架我整理了一个最小可运行的代码骨架包含解码线程和UI线程的协作方式。// VideoDecoder.h class VideoDecoder : public QObject { Q_OBJECT public: VideoDecoder(QObject *parent nullptr); ~VideoDecoder(); bool openFile(const QString filePath); // 打开本地视频文件 bool openStream(const QString rtspUrl); // 打开RTSP流 void start(); void stop(); signals: void frameReady(const QImage frame); private: void decodeLoop(); std::thread m_thread; std::atomicbool m_running{false}; MppCtx m_ctx nullptr; MppApi *m_mpi nullptr; std::string m_inputSource; };// VideoDecoder.cpp void VideoDecoder::decodeLoop() { // 打开文件或网络流读取ES流 while (m_running) { // 读取一帧码流数据 MppPacket packet nullptr; mpp_packet_init(packet, dataPtr, dataLen); // 送入解码器 mpi-decode_put_packet(m_ctx, packet); // 取回解码后的帧 MppFrame frame nullptr; int ret mpi-decode_get_frame(m_ctx, frame); if (ret MPP_OK frame) { // RGA转换NV12 - RGB888 RgaBuffer rgbBuf rgaConvert(frame); QImage img(rgbBuf.data, width, height, QImage::Format_RGB888); emit frameReady(img.copy()); // 深拷贝避免buffer重用问题 } if (frame) { mpp_frame_deinit(frame); } mpp_packet_deinit(packet); } }UI侧只需要在槽函数里更新控件MainWindow::MainWindow() { m_videoWidget new VideoWidget(this); setCentralWidget(m_videoWidget); m_decoder new VideoDecoder(this); connect(m_decoder, VideoDecoder::frameReady, m_videoWidget, VideoWidget::updateFrame); m_decoder-openFile(/data/test.h264); m_decoder-start(); }4.3 影响画面流畅度的几个参数细节最影响流畅度的是解码缓冲区数量和解码超时时间。mpp的解码循环如果使用阻塞模式MPP_POLL_BLOCK当码流输入稍有波动时解码线程会阻塞在等待帧上UI侧不会收到新帧表现就是画面停顿。我建议把解码线程设置成半阻塞或非阻塞模式然后配合sleep控制节奏。另外解码器内部会维持一个参考帧队列如果输入码流的GOP结构很大例如I帧间隔很长刚打开视频时会有一段“黑屏等待时间”。这个阶段可以通过设置mpp的MPP_DEC_SET_HEADER_MODE来跳过一些不关键的数据包加快首帧输出。我在实际测试中把解码线程的帧率控制和显示端解耦解码侧全速解码UI侧只显示最新帧而不是每帧都排队绘制。也就是说如果解码器已经解出第20帧UI还停留在第5帧直接丢弃中间帧显示第20帧。这样画面延迟会更低不过要处理好音频同步的话需要注意时间戳那就是另一个话题了。4.4 实测结果我的测试环境是RK3588开发板Linux系统QT5.15.2eglfs平台插件。视频源为H.264编码的1080P30测试文件解码走mpp硬解色彩转换走RGA绘制走QPainter。实测数据如下解码帧率稳定满30fpsUI刷新帧率约30fps偶有掉帧CPU占用率总计约15%其中解码线程约5%RGA转换约4%UI绘制约6%内存占用解码buffer RGB转换buffer约30MB延迟从解码完成到画面显示约20ms左右。这个结果在大多数HMI项目里是可用的。如果追求更低的延迟可以尝试把RGA转换后的buffer直接映射到QT的纹理里走OpenGL绘制但复杂度会高一些。5. 进阶rockit VO输出与QT透明窗口叠加怎么调通5.1 rockit VO的输出通路怎么理解rockit VO模块负责把视频帧输出到显示设备。在RK3588上显示链路通常由DRM管理一个显示画面可能由多个plane叠加而成video layer和GUI layer就是两个不同的plane。rockit VO的典型用法是通过VI/VENC等模块把视频帧送到VOVO将视频帧绑定到某个plane上该plane直接输出到HDMI、MIPI DSI或eDP等接口。如果要让QT窗口叠加在视频上面QT的窗口系统必须运行在另一个plane。如果QT走的是eglfs平台默认会占用主plane这样video plane就变成底层QT叠加在上层。5.2 透明窗口的关键配置在eglfs下让QT窗口透明有几个条件窗口需要调成无边框、置顶、带透明背景setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); setAttribute(Qt::WA_TranslucentBackground);对应的eglfs平台配置要支持alpha混合。有些板子的eglfs是配置成不透明背景的需要在启动参数或者配置文件里显式开启。我在RK3588上用eglfs方案踩过坑窗口虽然设置了WA_TranslucentBackground但显示出来仍然是黑底。后来发现是eglfs的显存格式设置不对需要把eglfs的配置从RGB565改成ARGB8888否则窗口系统根本不分配alpha通道透明属性就名存实亡了。注意这在X11下很容易实现但嵌入式平台上Qt eglfs对透明窗口的支持比较脆弱需要反复验证具体调试环境。5.3 rockit VO和mppQT方案怎么取舍如果只是单路视频叠加几个控件mppRGAQT的方案复杂度低稳定可控我更推荐先跑通这条路线。如果项目需要多路视频同时显示或者视频分辨率达到4K甚至8K单纯靠QPainter绘制RGB图像会显得吃力rockit VO的硬件叠加优势就体现出来了。另外rockit VO方案还有一个隐藏优势——视频内容不经过CPU/GPU安全性和稳定性更好。比如播放加密视频流或者需要长时间运行的显示终端硬件叠加不会受到上层应用崩溃的影响。但如果QT界面需要频繁变动、动画效果复杂rockit VO的叠加层配置可能会成为瓶颈因为每次改动视频层绑定的plane都要重新协商。对于复杂的动态UI还是老老实实让QT全屏显示视频作为内部子窗口渲染更为稳妥。6. 常见问题与排查实录6.1 花屏绿屏可能是stride对齐问题这是我在调试中最常见的问题。症状是画面内容能识别但整体有错位、有斜切感甚至全绿。排查顺序先确认mpp解码输出的width和stride是否一致。如果width是1920stride实际上是1920向上对齐到64字节通常是1920或1984等值不能用width直接代替stride去计算数据偏移确认NV12的UV平面偏移。UP地址是y_addr h_stride * v_stride别用width*height取代stride乘法否则UV平面错位画面颜色会严重偏色确认RGA源图信息的w/h是图像宽高而buffer size按stride换算两者要分开填写。6.2 视频黑屏/不显示先确认解码器是否正常出帧在decode_get_frame处打印宽高信息如果根本取不到帧排查码流是否完整确认RGA转换是否成功有些librga版本对某些尺寸会报错可以打印RGA返回码最后确认QImage是否有效可以用isNull()判断并检查buffer生命周期如果解码线程在下一轮覆盖了bufferUI绘制时就会黑屏甚至显示杂质。6.3 画面卡顿掉帧的排查方向卡顿不等于解码慢很多时候是UI线程阻塞导致。我遇到过QPainter绘制大尺寸图像时某些平台后端开销大导致UI线程来不及处理新到的帧表现出来就是播放一卡一卡。解决思路降低UI刷新频率用定时器以30fps的节奏刷新最新帧而不是每帧都触发绘制使用double buffer方案UI侧维护两个QImage交替绘制避免绘制过程中数据被覆盖检查是否启用了VSynceglfs默认可能开启垂直同步如果视频帧率不是屏幕刷新率的整数倍会出现周期性卡顿。6.4 长时间运行内存泄漏mpp的性能问题容易被忽略的是buffer释放。每次decode_get_frame返回的MppFrame用完必须mpp_frame_deinit每次mpp_packet_init创建的packet用完后必须mpp_packet_deinit。RGA侧也要注意rga_buffer的释放。我写过一个简单脚本循环10000帧监控进程内存增长可以快速验证是否有泄漏。另外如果使用信号槽传递QImage建议在emit之前固定Frame的内存大小并确保接收槽函数处理完后再发下一帧否则队列积压会导致内存峰值上升。7. 一些个人的体会和扩展方向折腾完这个项目我对RK3588在多媒体方向的开发有个直观感受芯片硬件能力很强解码、编码、2D加速、3D加速都齐全但最耗时间的往往是如何把不同硬件模块串联起来并且让上层框架能够合理调用它们。mpp和QT的叠加实现本质上是搞清楚数据流转换的问题这条路走通之后后续扩展会顺利很多。如果项目中后续要支持多路视频我建议考虑直接用rockit的VI-VPSS-VO通路管理整个视频采集和输出QT层只做OSD。SDK里其实还带了RGA做缩放、VPSS做图像处理的例子组合起来可以做出很多效果。网络流方面RK3588的mpp也支持直接解码RTSP拉流回来的H.264/H.265这块跟我前面讲的解码链路是相通的只是数据来源从文件换成网络缓冲区。这个项目后续我计划做两件事一是把视频叠加的逻辑封装成可复用的库给团队其他项目用二是尝试把视频帧通过OpenGL纹理渲染利用GPU做缩放和特效进一步降低CPU占用。如果你也在RK3588上做类似的视频叠加需求建议先把本文的mppQT最小工程跑通再根据实际性能数据决定是否升级到VO叠加路线这样风险最小。本文还有配套的精品资源点击获取
返回列表