简介本资源是一套基于RK3588平台实现视频解码与Qt图形界面叠加的完整交叉编译工程面向嵌入式Linux开发者、音视频应用工程师及国产化平台学习者解决Rockchip平台下硬件解码MPP/Rockit与Qt UI同屏渲染的技术难点。压缩包共64个文件包含33个头文件如mpp_frame.h、rk_vdec_cfg.h等核心MPP/DRM/RGA接口定义、9个动态库librockchip_mpp.so、librockit.so、libdrm.so等关键依赖、3个C源文件及多个Qt项目配置.pro、.qml、.qrc辅以说明文档与备份文件结构清晰便于理解底层调用链与UI集成逻辑。已有400人学习下载提供可直接编译运行的实测代码——修改TARGET即可定制输出名称支持rk3588交叉编译链一键构建涵盖从视频解码、DRM显示到Qt窗口合成的全流程实现是深入掌握RK3588多媒体框架与跨层渲染机制的实用参考范例。1. 项目背景与核心需求分析先说结论在RK3588上把视频画面叠加到QT界面用rockit或者mpp都可以做而且实测是能跑的通的。这个需求在嵌入式工控、智能终端、机器视觉领域非常常见。简单来说就是RK3588这颗芯片自带硬件视频编解码单元VPU解码出来的视频帧是YUV格式的原始数据不能直接被QT的QWidget/QML直接显示。你需要把YUV数据转成QImage或者QOpenGLTexture再塞进QT的界面体系里才能实现“视频窗口 业务控件”同时显示的效果。我最初接到这个需求的时候老板的需求描述就一句话“把摄像头画面显示到咱们的QT界面上要流畅别卡顿不要CPU占用太高。” 这句话背后其实藏着好几个技术问题视频源是MIPI摄像头还是RTSP流还是本地视频文件解码用硬件还是软件CPU软解在RK3588上虽说不至于不能用但多路4K就撑不住了。YUV转RGB怎么做纯CPU转换对带宽是灾难。显示走QWidget的paintEvent还是走OpenGL纹理前者适合低分辨率后者适合高分辨率和多路。标题里同时提了rockit和mpp说明这个项目不是二选一而是两条路都试过。Rockit是瑞芯微提供的一套多媒体解决方案把采集、编解码、显示、相机ISP全部封装起来更像工业级的SDKMPP则是底层的视频编解码库直接操作VPU灵活但裸。我需要先把这两者的定位说清楚后面才好理解叠加怎么做。一句话总结项目目标用RK3588的硬件能力做视频解码把解码后的视频帧高效率地呈现在QT界面上同时保证QT的业务功能比如按钮、数据曲线、告警弹窗正常工作。这个事情适合谁参考一类是刚拿到RK3588开发板、被官方SDK绕晕的嵌入式工程师另一类是QT开发熟练但对视频管线不熟、被YUV和RGB转换搞到头大的应用工程师。我下面分享的内容会同时照顾这两类人。2. 方案选型rockit vs mpp以及为什么两条路都走得通2.1 rockit和mpp到底是什么关系先把这个最基础的概念捋清楚。MPP全称是Media Process Platform是瑞芯微提供的视频编解码库直接跑在VPU上它的作用就是输入压缩码流输出原始图像数据。你可以把它理解为芯片上VPU的驱动程序接口它不管图像从哪来也不管图像往哪去只负责解码和编码。Rockit则是更上层的一套方案。它把采集ISP、编解码MPP、视频显示VO、AI推理等模块做了统筹管理自己维护一套软件链路。在RK3588的SDK里rockit和MPP是上下游的关系rockit的编解码模块底层调用的仍然是MPP。所以标题里说“rockit或mpp”其实是两种使用层级用rockit做整条视频链路采集/解码然后在回调里把YUV帧交给QT。直接用MPP做解码跳过rockit复杂的初始化流程自己控制输入码流和输出buffer。两条路各有各的好也各有各的坑。我实际测试下来的感受是MPP更直接出问题好排查rockit更完整功能全但学习曲线陡。2.2 为什么我不推荐纯CPU软解有一种最偷懒的方案用FFmpeg的软解拿到AVFrame再用QImage显示。这条方案在x86桌面上完全OK但在RK3588上做不了高分辨率多路场景。原因有几个第一CPU负担重。RK3588的CPU虽然是8核A76A55架构性能在ARM平台里属于猛兽级但软解一路4K60fps的H.265CPU占用率轻松超过40%如果还要跑QT的界面刷新、事件循环、绘图整体就会显得粘滞操作响应变慢。第二功耗和发热。嵌入式设备通常要考虑散热CPU长时间跑高负载降频是迟早的事一降频界面就更卡形成恶性循环。第三VPU闲着不用实在浪费。RK3588的VPU支持8K30fps解码硬解一路1080p60fps根本不吃力MPP的调用成本也非常低。所以正确的姿势一定是硬解拿到YUV帧然后想办法高效地显示到QT。2.3 显示通道的选型QWidget绘图还是OpenGL拿到解码帧之后显示到QT有两条主流路径一是把YUV手动转RGB生成QImage然后在QWidget的paintEvent里drawImage。这条路实现最简单逻辑也直白适合单路1080p以内、帧率要求不高的场景。但问题在于YUV420转RGB720p分辨率一秒30帧纯CPU转换大约要占用一个A76核心30到50%的负载分辨率越高越吃力。二是把YUV数据直接上传为OpenGL纹理利用GPU做格式转换和缩放。RK3588的GPU是Mali-G610 MP4硬件支持YUV到RGB的颜色空间转换效率远高于CPU。这条路更加推荐尤其是做多路视频拼接或者未来要上4K显示的一开始就奔着OpenGL去最稳妥。我最终的项目里两条路都写了代码框架实际启用的是OpenGL纹理方案。后面会详细讲这两种实现的代码结构和坑点。3. 基于MPP直接解码的QT显示方案3.1 为什么先试MPPRockit的函数封装层次更厚如果只是想做一个“把视频流解码到QT窗口”的功能它有点杀鸡用牛刀。有一个很现实的问题利用rockit建立虚拟通道链路时经常需要设备树配合、v4l2节点配置、VO显示时序设置一旦跑不起来排查链路很费劲底层的每一个环节都可能是问题根源。所以我先选MPP做了一条最小可用的解码链路。MPP对纯解码场景来说足够简洁API结构清晰踩坑也容易定位。3.2 MPP的工作流程和关键代码结构MPP解码的基本流程五大步初始化MPP解码上下文设置解码器类型H.264/H.265/VP9等。把编码码流喂给解码器通过mpp_mpi-decode_put_packet接口。从解码器取解码完成的帧通过mpp_mpi-decode_get_frame接口。对拿到的MppFrame做格式转换YUV转RGB或者直接拿YUV数据上传纹理。处理完释放帧循环往复。先展示核心初始化部分。// MppDecoder.h 核心成员 class MppDecoder { public: MppDecoder(); ~MppDecoder(); bool init(AVCodecID codecId); bool decode(const uint8_t* data, size_t size, uint64_t pts); void setFrameCallback(std::functionvoid(const VideoFrame) cb); void stop(); private: MppCtx ctx_ nullptr; MppApi* mpi_ nullptr; MppBufferGroup frameGroup_ nullptr; size_t frameSize_ 0; RK_U32 width_ 0; RK_U32 height_ 0; bool needDrain_ false; };初始化函数的细节bool MppDecoder::init(AVCodecID codecId) { MppCtxType type MPP_CTX_DEC; MppCodingType coding; switch (codecId) { case AV_CODEC_ID_H264: coding MPP_VIDEO_CodingAVC; break; case AV_CODEC_ID_HEVC: coding MPP_VIDEO_CodingHEVC; break; case AV_CODEC_ID_VP9: coding MPP_VIDEO_CodingVP9; break; default: return false; } MPP_RET ret mpp_create(ctx_, mpi_); if (ret ! MPP_OK) return false; ret mpi_-control(ctx_, MPP_CMD_SET_STREAMING, nullptr); ret mpp_init(ctx_, type, coding); if (ret ! MPP_OK) { mpp_destroy(ctx_); return false; } // 我们用的按帧送包模式不需要开启split_parse MppDecCfg cfg; memset(cfg, 0, sizeof(cfg)); mpp_dec_cfg_init(cfg); mpp_dec_cfg_set_u32(cfg, base:grp:wait_int, 100); mpp_dec_cfg_set_u32(cfg, base:grp:drain_mode, 0); mpi_-control(ctx_, MPP_CMD_SET_DEC_CFG, (MppParam)cfg); return true; }注意上面一个关键点MPP_CMD_SET_STREAMING这个控制命令在旧版本SDK里不一定有效它用来让解码器进入流式模式。如果某些场景下画面花屏或帧序错乱优先检查这个设置。解码一帧的流程bool MppDecoder::decode(const uint8_t* data, size_t size, uint64_t pts) { MppPacket packet nullptr; mpp_packet_init(packet, data, size); mpp_packet_set_pts(packet, pts); MPP_RET ret mpi_-decode_put_packet(ctx_, packet); mpp_packet_deinit(packet); if (ret ! MPP_OK) return false; MppFrame frame nullptr; ret mpi_-decode_get_frame(ctx_, frame); while (ret MPP_OK frame) { VideoFrame vf; vf.width mpp_frame_get_width(frame); vf.height mpp_frame_get_height(frame); vf.hor_stride mpp_frame_get_hor_stride(frame); vf.ver_stride mpp_frame_get_ver_stride(frame); vf.format mpp_frame_get_fmt(frame); vf.pts mpp_frame_get_pts(frame); MppBuffer buf mpp_frame_get_buffer(frame); vf.data[0] mpp_buffer_get_ptr(buf); vf.size mpp_buffer_get_size(buf); vf.bufferHandle buf; // 留着buffer引用callback结束后释放 // 关键必须在这里拷贝或同步处理不能把MppFrame带出callback外部再用 frameCallback_(vf); mpp_frame_deinit(frame); ret mpi_-decode_get_frame(ctx_, frame); } return true; }这段代码里有个非常容易踩坑的地方decode_get_frame拿到的MppFrame生命周期只到下一次调用decode_get_frame或者decode_put_packet为止同一时间只能有一个frame在外部引用。所以callback里面必须立刻做数据深拷贝或者立刻上传GPU纹理不能跨帧做异步任务。我最初把帧数据丢到线程池里去异步转RGB结果发现解码线程跑到第二帧buffer已经变了画面花的不成样子。后来老实了回调里只做必要处理绝不做异步延迟。3.3 YUV到RGB的转换到底是CPU转还是GPU转拿到MppFrame里的YUV数据后需要显示到QT。第一个选择是CPU手动转换。YUV420SPNV12的转RGB公式网上能搜到一堆但做嵌入式移植时要注意RK3588的VPU默认输出的格式通常是NV12而非I420这两个格式的排列方式不同。NV12的Y平面之后UV是交织排列的I420则是先U平面再V平面。转换时如果用错了排列方式画面会严重偏色、重影或者下半部分紫绿色。这个排查方法很明确一张图就能看出来。我自己写了个基于查表法优化的RGB转换函数利用C多线程将一帧1080p的转换时间控制在8毫秒左右。但即使在8毫秒帧率30fps也就意味着每帧CPU要花掉约24%的时间在转换上这还只转换一帧多路就不行了。所以做多路显示正确做法是用GPU。RK3588的Mali-G610支持从纹理直接采样YUV数据将Y、U、V分别作为三个纹理或者一个纹理中的不同区域传入shader在GPU里做颜色矩阵运算。EGL/OpenGL ES的YUV渲染shader大致为// vertex shader attribute vec4 aPosition; attribute vec2 aTexCoord; varying vec2 vTexCoord; void main() { gl_Position aPosition; vTexCoord aTexCoord; }// fragment shader precision mediump float; varying vec2 vTexCoord; uniform sampler2D uTextureY; uniform sampler2D uTextureU; uniform sampler2D uTextureV; void main() { float y texture2D(uTextureY, vTexCoord).r; float u texture2D(uTextureU, vTexCoord).r - 0.5; float v texture2D(uTextureV, vTexCoord).r - 0.5; float r y 1.402 * v; float g y - 0.344 * u - 0.714 * v; float b y 1.772 * u; gl_FragColor vec4(r, g, b, 1.0); }这种方案可以轻松做到4K60fps的实时显示CPU占用率接近零。3.4 单独解码不显示只用MPP的场景如果业务场景是“视频流解码后不是显示到QT窗口而是做AI分析、录像存储或者取帧做算法”那么MPP完全可以独立干活不需要QT参与。这时把上文的frameCallback_换成你自己的处理逻辑比如给YOLOv8的tensor喂数据、写文件、或者推给另一个编码器。这个“解码不显示”的场景在RK3588的项目里非常常见比如做AI盒子解码之后直接送NPU不需要实时预览。很多新手容易犯的错是明明不需要预览却非要把视频显示出来结果白白增加一倍的带宽和功耗。先想清楚你的业务到底需不需要预览这个决定会影响整个软件架构。4. 基于Rockit的完整链路集成4.1 Rockit能帮你省掉哪些脏活如果数据来源是MIPI摄像头或者需要做ISP处理、3A算法、多通道绑定直接用MPP就会发现一个尴尬的问题你还需要自己去配v4l2采集、打VDEC的buffer或者管理ISP的3A。这些活本身不难但组合起来代码量剧增而且很容易出设备树匹配和buffer管理的问题。Rockit的价值就在这它提供了从采集到输出的完整链路抽象。在Rockit的模型里一个典型的视频链路是VI视频输入/相机采集 - VENC编码 / VDEC解码 - VO视频输出/显示如果只是做解码到QT显示Rockit链路里比较关键的是VDEC模块。它其实是对MPP解码的进一步封装但好处是帮你管理了buffer池和帧同步。代码结构上Rockit的API风格更接近传统视频中间件int ret RK_MPI_VDEC_CreateChn(0, stVdecChnAttr); ret RK_MPI_VDEC_SendStream(0, stStream); ret RK_MPI_VDEC_GetFrame(0, stVideoFrame, 1000); // 处理... ret RK_MPI_VDEC_ReleaseFrame(0, stVideoFrame);RK_MPI_VDEC_GetFrame拿到的VIDEO_FRAME_INFO_S里有buffer的fd、虚拟地址、宽高、stride等信息这些信息可以直接传递到OPENGL纹理实现零拷贝显示。我知道现在很多人听Rockit这个名字会有点陌生因为它的SDK版本比较飘忽文档也不算友好。我的建议是如果项目里面需要操作MIPI摄像头、ISP或者多路视频流直接学Rockit是值得的如果只是解码RTSP或者本地视频文件来显示那MPP就够用了。4.2 Rockit QT叠加的实际架构Rockit链路初始化之后视频帧会通过VIDEO_FRAME_INFO_S结构体回调出来。我们要做的核心工作就是把这个结构体里的数据转换成QT可显示的纹理。这里我采用的方式是用QOpenGLTexture但是不立即上传而是把fd传到一个专用的纹理上传线程由该线程调用glEGLImageTargetTexture2DOES将硬件buffer挂载为纹理这样直接省掉了一次memcpy。简化流程如下Rockit VDEC 解码 - VIDEO_FRAME_INFO_S (含fd和地址) - EGLImage 创建 - QOpenGLTexture 绑定 - 绘制到QOpenGLWidget需要注意的是EGLImage方式不是随随便便就能用的。它要求你在创建EGL上下文时指定正确的扩展例如EGL_KHR_image_base。而且RK3588的Mali驱动对双buffer或者三buffer的绑定有严格要求如果创建EGLImage的线程和渲染线程不是同一个还必须做资源共享设置这往往是最坑的一环。实际代码里我封装了一个VideoFrameUploader的类class VideoFrameUploader : public QObject { Q_OBJECT public: VideoFrameUploader(); ~VideoFrameUploader(); bool initialize(EGLDisplay display, EGLContext context); bool upload(const VIDEO_FRAME_INFO_S* frame, GLuint textureId); private: EGLDisplay eglDisplay_ EGL_NO_DISPLAY; EGLContext eglContext_ EGL_NO_CONTEXT; PFNEGLCREATEIMAGEKHRPROC eglCreateImageKHR_ nullptr; PFNEGLDESTROYIMAGEKHRPROC eglDestroyImageKHR_ nullptr; PFNGLEGLIMAGETARGETTEXTURE2DOESPROC glEGLImageTargetTexture2DOES_ nullptr; };这个类做的核心事情初始化时获取三个EGL扩展函数指针。upload里把RK MPI的buffer fd通过eglCreateImageKHR转成EGLImage。绑定纹理后调用glEGLImageTargetTexture2DOES完成零拷贝挂载。这里有一个知识点要特别提醒Rockit给出的buffer不一定都是物理连续的。如果buffer不是物理连续的EGLImage创建会失败返回EGL_BAD_ACCESS。如果遇到这个错误先检查VIDEO_FRAME_INFO_S里的enCompressMode是不是COMPRESS_MODE_NONE有些压缩帧格式AFBC不适合直接做EGLImage需要先解压缩。4.3 用Rockit做MIPI摄像头直通显示如果你是接MIPI摄像头Rockit的优势就非常明显了。你可以用VI通道采集摄像头数据VPSS做缩放VDEC通道做视频流解码然后把最终需要显示的帧交给QT。这种链路写起来虽然长但官方都有样例代码照着改就好。需要注意MIPI摄像头的初始化这个和你的摄像头型号、设备树、ISP tuning文件强相关。RK3588 SDK里的rkaiq组件负责ISP的3A算法Rockit的VI通道启动后会自动把相机图像送去RKAIQ。我实际做下来的体会是MIPI转QT显示这类功能如果板子上只有一路相机Rockit的VI VO做直接预览最简单甚至不需要QT参与直接用Rockit的VO模块输出到HDMI画面就出来了。只有当你需要在这个画面上叠加业务控件才需要把帧抽出来交给QT。这个取舍很重要——不是所有的视频显示场景都非要经过QT。5. QT显示模块的详细实现与优化5.1 QT视频窗口的实现结构我这里把视频图像封装成了一个QVideoWidget控件内部继承QOpenGLWidget和QOpenGLFunctions_EGL。控件提供两个外部接口class QVideoWidget : public QOpenGLWidget, protected QOpenGLFunctions_3_2_Core { Q_OBJECT public: explicit QVideoWidget(QWidget* parent nullptr); ~QVideoWidget() override; public slots: void onFrameReady(const VideoFrame frame); void onFrameReadyFromRockit(const VIDEO_FRAME_INFO_S* frame); protected: void initializeGL() override; void resizeGL(int w, int h) override; void paintGL() override; private: void uploadYuvTexture(const VideoFrame frame); void drawTexture(); GLuint textureY_ 0; GLuint textureU_ 0; GLuint textureV_ 0; int videoW_ 0; int videoH_ 0; };在paintGL里要做两件事绑定纹理并绘制带纹理的四边形叠加绘制其他QT控件。这里重点说下为什么用QOpenGLWidget而不是在普通QWidget里嵌一个QOpenGLContext——QOpenGLWidget帮你解决了一堆平台窗口和上下文切换的问题它自己拥有独立的上下文内部把FBO和窗口上下文都管理好了你只需要关心渲染逻辑。5.2 纹理上传性能优化实例先说不推荐的方案把MppFrame里的数据memcpy到QImage再做一次QImage到纹理的转换。这样会多一次拷贝和一次像素格式转换性能损耗非常明显。推荐方案是直接上传YUV数据到三个纹理void QVideoWidget::uploadYuvTexture(const VideoFrame frame) { if (!textureY_) { glGenTextures(1, textureY_); // ... 同理生成 textureU_, textureV_ } glBindTexture(GL_TEXTURE_2D, textureY_); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, frame.hor_stride, frame.ver_stride, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, frame.data[0]); int uvHeight frame.ver_stride / 2; glBindTexture(GL_TEXTURE_2D, textureU_); glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, frame.hor_stride / 2, uvHeight, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, frame.data[1]); glBindTexture(GL_TEXTURE_2D, textureV_); glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE, frame.hor_stride / 2, uvHeight, 0, GL_LUMINANCE, GL_UNSIGNED_BYTE, frame.data[2]); }这里有个特别容易出问题的细节hor_stride不一定等于width。RK3588的VPU解码出来的Y平面一行实际的bytes数往往比宽度要大比如宽1920但hor_stride可能是1920或者1920256对齐到2048。在glTexImage2D里设置宽度时一定要用hor_stride而不是width否则图像会右移错位。而shader的纹理坐标采样范围需要按width / hor_stride去算不然画面会拉宽。UV平面的尺寸也同理是hor_stride / 2和ver_stride / 2。5.3 QWidget叠加的注意点QOpenGLWidget在Qt 5.4之后才逐渐成熟但它和普通QWidget的混合布局是有坑的。如果把QVideoWidget和普通QWidget控件放在同一个父窗口里普通QWidget会在视频控件上方或下方产生平台窗口覆盖问题导致视频区域黑块或者控件不刷新。为了解决这个渲染层叠问题有几种做法把业务控件都放进QVideoWidget内部让它们作为子控件显示在OpenGL表面之上。这种方式要求QOpenGLWidget开启setAttribute(Qt::WA_AlwaysStackOnTop)或WA_TransparentForMouseEvents实测大部分场景可行。使用独立的两个窗口一个显示视频另一个显示业务数据通过坐标对齐模拟叠加效果。这种方式多见于工业HMI两个窗口分别绑定不同的屏幕或者靠窗口管理器的Z序叠放。开启Qt::AA_ShareOpenGLContexts全局属性后将所有渲染都放在同一个OpenGL上下文中。这是最干净的方案但开发量最大。我是怎么做的呢我用了第一种加上QOpenGLWidget::setUpdateBehavior(QOpenGLWidget::PartialUpdate)只在视频数据更新时触发重绘。业务控件比如按钮、状态标签直接add进去事件穿透也正常这个方案是最简单且实用的。5.4 多路视频显示怎么办如果要多路摄像头或者多路RTSP建议不要在一个QVideoWidget里画多路而是创建多个QVideoWidget实例放到布局里。每个视频控件对应一个独立解码线程纹理各自独立互不干扰。这样代码结构干净每个控件只处理自己的帧。实际项目里我跑过四路1080pRK3588的VPU硬解四路解码线程 四个QVideoWidget 主线程UICPU占用率整体在15%以内非常宽裕。这个方案有比较强的可扩展性后面接8路只要内存和带宽够也能撑住。6. 常见问题与排查技巧实录这部分是我最想写的。因为不管是走MPP还是rockit真正耗时间的往往不是写代码而是查那些令人抓狂的疑难杂症。我把踩过的坑整理成了一份速查表。6.1 画面花屏、绿屏、紫屏花屏的原因一般有三类分辨率设置不对解码器给出来的宽高和实际码流不一致导致帧数据错位。stride没有对齐用width代替了hor_stride导致图像被拉宽或错位。YUV格式搞错了解码输出是NV12但你当成I420处理。排查顺序先确认输入的码流参数和MPP解码器的输出参数打印mpp_frame_get_fmt看看输出的像素格式。打印hor_stride和ver_stride对比width和height。写一个“暴力验证”的方法把第一帧Y数据直接保存成.yuv文件用YUV播放器打开确认解码器本身输出的原始数据是否正确。如果是正确的问题就出在显示环节如果文件本身颜色有问题那说明解码或者源流有问题。6.2 帧率上不去只有几帧每秒如果硬解但帧率上不去先排查解码线程和处理线程是不是互锁了。常见的一个死锁场景是解码线程在decode_put_packet和decode_get_frame之间做同步等待而处理线程又在等待解码线程释放buffer结果两头互相等。另一个常见原因是回调里做了重操作。我的经验是回调里只做三件最轻的事上传纹理、保存指针、投递事件。绝对不要在回调里做文件IO、打印日志、网络发送等操作。打印日志对帧率的毁灭性影响我是在实测中被狠狠教育过的——一旦在回调里加了qDebug30fps的流直接掉到10fps以下。6.3 Rockit初始化失败报buffer分配不成功Rockit初始化失败最常见的原因是buffer池的内存分配参数不合理。Rockit需要从系统内存或者dma heap申请buffer如果申请的buffer过大超过系统可用内存上限初始化就会失败。解决办法是调小VdecChnAttr里的buffer数。比如原来是16个改成8个内存占用会明显下降。还有一个技巧可以在RK_MPI_VDEC_CreateChn之前调用RK_MPI_SYS_SetMemoryConfig去调整MMZ多媒体内存的分配策略这个对解决buffer失败很管用。6.4 OpenGL纹理发黑或花屏OpenGL纹理画面发黑大概率是纹理上传时数据格式不匹配。你要检查几个点GL_LUMINANCE和GL_RED的选择Mali核心对这两种格式的支持程度不同。在GLES 3.x下优先用GL_RED加上swizzle设置或者干脆用GL_LUMINANCE如果驱动不支持会有日志报错。纹理过滤方式建议GL_LINEAR不要用GL_NEAREST否则画面锯齿严重尤其是文字内容。确保每次glTexImage2D都设置了GL_UNPACK_ALIGNMENT为1否则宽度不是4的倍数时会出现像素错位。我这里分享一个判断技巧如果画面整体偏暗但能看清轮廓说明RGB转换的系数有误尤其是YUV到RGB的颜色空间BT.601 vs BT.709选错了。MPP解码H.264和H.265时得到的YUV数据通常对应BT.709而老式MPEG2对应BT.601用错矩阵就会偏色。6.5 QT窗口被视频控件盖住这是一个非常典型的QT层叠问题。现象是视频控件把按钮盖住了鼠标点击无效或者按钮能显示但背景被视频覆盖。我的解决办法如下videoWidget-setAttribute(Qt::WA_TransparentForMouseEvents, false); button-raise(); button-setAttribute(Qt::WA_AlwaysStackOnTop, true);如果依然无效可以尝试把按钮放到一个独立的顶层窗口然后让这个顶层窗口保持Qt::Tool或者Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint用move方法跟随主窗口移动。6.6 帧发布到UI线程后刷新不及时QT的UI线程事件循环如果被耗时操作阻塞视频画面自然会卡顿。这里要注意不应该把视频刷新塞到paintEvent之外的事件里强行触发而应该使用QTimer或者QOpenGLWidget::update()再结合QThread解码线程的信号槽通知主线程。我采用的是解码线程通过信号发VideoFrame给QVideoWidgetQVideoWidget内部开一个队列缓存最新一帧只保留最新帧丢弃旧帧。这样即使UI线程卡顿一下视频也会自动跳帧不会造成事件堆叠和延迟递增。// QVideoWidget.cpp void QVideoWidget::onFrameReady(const VideoFrame frame) { latestFrame_ frame; // 只保留最新 update(); // 请求重绘 } void QVideoWidget::paintGL() { if (!latestFrame_.valid) return; uploadYuvTexture(latestFrame_); drawTexture(); }6.7 常见问题速查表现象可能的根因排查/解决方向画面右移错位stride未对齐用hor_stride代替width画面花屏且颜色像水彩YUV格式错确认NV12还是I420画面偏色BT.601/BT.709矩阵错误检查颜色矩阵帧率上不去回调里有重操作或互锁回调只做轻量处理检查锁视频控件盖住按钮QOpenGLWidget层级问题raise() WA_AlwaysStackOnTop黑屏但解码正常纹理格式不匹配检查GL_LUMINANCE、GL_UNPACK_ALIGNMENTRockit初始化失败buffer内存不足调小buffer数或调整MMZ分配画面延迟逐渐增大事件队列堆积UI线程做非阻塞刷新丢弃旧帧视频控件区域闪烁平台窗口混合渲染开启AA_ShareOpenGLContexts全局共享上下文双屏显示卡顿VO和QT抢GPU资源精简VO链路把显示统一交给QT6.8 我自己走过的弯路写到最后我真心觉得有几个习惯可以帮大家少走弯路第一不要一上来就追最新版本的SDK。RK3588的SDK迭代很快但很多潜在问题在旧版本里已经有人趟平了。先在稳定的老版本上跑通基础功能再评估是否升级。我最初基于某个接近5.10内核的老SDK开发稳定运行了大半年期间只做局部移植。第二把调试信息做成可开关的宏。解码和渲染环节的调试打印太关键了但也不能全程开。建议用宏统一控制#ifdef DEBUG_VIDEO_PIPELINE #define VLOG(fmt, ...) qDebug(%s:%d fmt, __FILE__, __LINE__, ##__VA_ARGS__) #else #define VLOG(fmt, ...) #endif这样量产机上关闭打印开发机上打开打印不会影响帧率。第三保留一个纯软件解码的调试模式。就算最终方案是MPP硬解也建议留一个FFmpeg软解的开关。这样做对比对照非常有用当硬解画面有问题时切到软解看画面是否正常。如果软解正常说明是硬件解码或者buffer管理的问题如果软解也异常那问题可能在视频源头或显示链路。7. 集成测试与性能数据7.1 测试环境硬件平台RK3588开发板8GB内存版电源使用标准12V/3A适配器。系统版本Ubuntu 22.04内核5.10。显示终端1080p HDMI输出。软件栈MPP在SDK的external/mpp下编译入库QT基于5.15.2使用-opengl es2编译选项。测试视频H.265编码的4K30fps本地MP4文件。7.2 参数测试结果单独用MPP硬解4K视频CPU占用率极低。整机CPU占用约8%其中四核A55基本全部空闲A76核心只有两个在工作。加上QT显示之后CPU占用率上升到约12%主要是UI线程和QT自身的绘制开销。这个数值比软解方案低了至少三倍软解方案在4K下的CPU占用率稳定在35%以上。从帧延迟来看硬解到纹理显示的整体链路延迟大约在20到30毫秒。这个延迟包括了解码、纹理上传和渲染三个环节对常规的视频显示场景完全够用。如果是做视频预览不需要超高实时性这个延迟完全在可接受范围内。如果要做零延迟直播那你需要进一步优化比如降低buffer数量、直接使用DRM显示不走QT渲染。7.3 多路性能数据四路1080p30fps同时解码显示CPU占用率约18%到22%单路内存占用约200MB。因为RK3588的VPU对多路解码支持得很好这个数据在实际项目中非常实用。八路1080p我尝试过CPU占用率约30%左右内存占用上了一个台阶但运行依然稳定。7.4 实测感受如果只做一个视频显示功能用MPP QT OpenGL纹理这条路线从代码量、调试复杂度、后期维护三个维度看都是最优解。Rockit适合那种还需要对接相机ISP、做3A、跑AI的闭环系统从它下手可以让整个软件框架更加统一。但仅仅为了把视频显示到QTRockit有点重了。我个人建议是优先把MPP这条链路跑通它能覆盖大约80%的“视频显示”需求如果后续确实需要相机采集、ISP处理、编码推流等功能再在现有框架里引入Rockit把MPP的buffer直接交给Rockit通道使用。8. 扩展思路视频叠加的更高级玩法QT和视频叠加这件事不只是“把YUV转成QImage”这么窄。在RK3588上我们还能做很多有意思的扩展我把有实际价值的几个方向列出来。8.1 视频上叠加OSD文字和图形现在的方案里视频和QT控件是分层渲染的QT控件在上视频在下。如果你想要文字或者图形融入视频画面一起编码输出到HDMI或者推流就不能只靠QT控件的透明叠层了。正确做法是解码YUV帧后把需要OSD的内容用OpenGL直接绘制到YUV纹理上的对应区域或者通过Rockit的RGN模块在VO输出时叠加。RGN是Rockit自带的区域叠加模块支持位图OSD、颜色填充、边框绘制对做工业HMI特别有用。它的一个好处是叠加不经过GPU渲染在VO输出硬件层面完成不会占用额外的渲染资源。8.2 视频帧送NPU做AI识别结果实时回显RK3588的NPU能跑YOLOv8等模型。你可以把MPP解码出的YUV帧直接转成RGB888然后通过rknn的零拷贝接口喂给NPU识别结果再通过QT界面画出检测框。这是目前做AI盒子最热门的玩法。注意这里的转换要高效从YUV到RGB的转换要尽量简单。如果不想自己写转换函数可以用Rockit的VO通道把YUV转成RGB后再送NPU。这样虽然绕了一下但省掉了大量手写代码的时间。8.3 视频录制与回放MPP解出来的YUV帧可以直接送给MPP的编码器重新编码实现“解码 转码 录像”的功能。这在录像机、边缘计算网关里很常见。RK3588硬编H.265 4K30fps的码率控制在8Mbps左右画质和存储空间的平衡比软编好很多。8.4 双屏异显RK3588支持两路HDMI同时输出。一个屏幕显示业务QT界面另一个屏幕可以通过Rockit的VO显示视频两个屏幕互不干扰。或者两个屏幕都显示QT其中一个窗口播放视频。这些都是RK3588的多通道特性带来的能力值得在前期架构设计时纳入考虑。就我个人的实际体验来说这个RK3588 QT 视频叠加的方案最终会演进成一整套多媒体中控系统。从最初的“把视频画面塞进QT窗口”到后来的多路输入、多路输出、OSD叠加、AI识别结果可视化整个过程非常有价值。核心技术点并没有想象中那么难难的是把每一步的细节和异常处理做扎实。希望这篇记录能帮到正在填这个坑的各位。本文还有配套的精品资源点击获取