首先,不涉及视频解码,仅涉及解码之后的视频缩放、颜色空间转换(如YUV转RGB)、贴图。本文主要说明的是在QT框架没有使用OpenGl的情况下,如何让解码后的视频更高效的展示出来。
海思底层解码出来的视频都是YUV格式的,而QT的贴图格式是RGB,并且QT的视频展示框的宽高往往和解码出来的YUV不一样。所以要把解码出来的视频展示出来需要对源YUV做缩放、然后转RGB,QT的QImage类自带缩放功能,但是使用软缩放效率太低,相关的YUV转RGB的代码做转换也是一样效率很低,特别对于一些CPU能力比较低的芯片视频完全没法看。本文中的方案主要通过以下流程来实现:
图 1.视频播放流程
从解码器拿出来YUV图片,使用VGS缩放到与展示框大小差不多(由于对齐的原因一般不会完全相同),缩放之后的YUV经过IVE模块转成BGR,最后用TDE将BGR转成RGB。整个流程都使用硬件模块实现,大大提高效率。
一. 缩放
利用VGS做YUV的缩放,代码片段如下:
#include <hi_comm_vgs.h>
#include <mpi_vgs.h>
// src: 源图像
// dst:目标图像
// 将src指向的源图像缩放到由dst指向的宽高的大小,并且图像输入到dst对应的内存中
// note: 都需要开辟好空间
int ImgScale(VIDEO_FRAME_INFO_S *src,VIDEO_FRAME_INFO_S *dst) {
HI_S32 ret;
VGS_HANDLE vgs_handle;
ret = HI_MPI_VGS_BeginJob(&vgs_handle);
if (ret != HI_SUCCESS) {
printf("HI_MPI_VGS_BeginJob failed:%#x\n", ret);
return ret;
}
VGS_TASK_ATTR_S vgs_task;
memset(&vgs_task, 0, sizeof(vgs_task));
memcpy(&vgs_task.stImageIn,src,sizeof(vgs_task.stImageIn));
memcpy(&vgs_task.stImgOut,dst,sizeof(vgs_task.stImgOut));
ret = HI_MPI_VGS_AddScaleTask(vgs_handle, &vgs_task);
if (ret != HI_SUCCESS) {
printf("HI_MPI_VGS_AddScaleTask failed:%#x\n", ret);
HI_MPI_VGS_CancelJob(vgs_handle);
return ret;
}
ret = HI_MPI_VGS_EndJob(vgs_handle);
if (ret != HI_SUCCESS) {
printf("HI_MPI_VGS_EndJob failed:%#x\n", ret);
HI_MPI_VGS_CancelJob(vgs_handle);
return ret;
}
return 0;
}
关于VGS缩放,也可以参考海思的sample和文档,这方面资料比较多。
二. YUV转BGR
利用IVE做YUV转BGR,要注意的是IVE支持的YUV格式不多,如果解码器输出的YUV格式对于IVE来说不支持,可以在缩放的时候让VGS顺便转以下格式,VGS支持YUV之间的格式转换。这里转出来的是BGR,也就是按蓝绿红排列的,按我使用的海思芯片的说明来看,IVE输出是BGR不是RGB,YUV转BGR代码如下:
#include <hi_comm_ive.h>
#include <hi_ive.h>
#include <mpi_ive.h>
// src: 源图像
// dst:目标图像
// 将src指向的源YUV图像转换成BGR图像
// note: 都需要开辟好空间,并且源图像与目标图像大小一致
int ImgCsc(VIDEO_FRAME_INFO_S *src, VIDEO_FRAME_INFO_S *dst) {
int ret;
IVE_SRC_IMAGE_S src_img, dst_img;
memset(&src_img,0,sizeof(src_img));
memset(&dst_img,0,sizeof(dst_img));
src_img.enType = IVE_IMAGE_TYPE_YUV420SP; // 源图像格式,这里是举例
src_img.pu8VirAddr[0] = src->stVFrame.pVirAddr[0];
src_img.pu8VirAddr[1] = src->stVFrame.pVirAddr[1]; // YUV410SP 格式只有两片内存
src_img.u32PhyAddr[0] = src->stVFrame.u32PhyAddr[0];
src_img.u32PhyAddr[1] = src->stVFrame.u32PhyAddr[1];
src_img.u16Stride[0] = src->stVFrame.u32Stride[0];
src_img.u16Stride[1] = src->stVFrame.u32Stride[1];
src_img.u16Height = src->stVFrame.u32Height;
src_img.u16Width = src->stVFrame.u32Width;
dst_img.enType = IVE_IMAGE_TYPE_U8C3_PACKAGE; // 源图像格式BGR888
dst_img.pu8VirAddr[0] = dst->stVFrame.pVirAddr[0]; // BGR888格式只有一片内存
dst_img.u32PhyAddr[0] = dst->stVFrame.u32PhyAddr[0];
dst_img.u16Stride[0] = dst->stVFrame.u32Stride[0];
dst_img.u16Height = dst->stVFrame.u32Height;
dst_img.u16Width = dst->stVFrame.u32Width;
IVE_HANDLE handle;
HI_BOOL instant = HI_TRUE;
IVE_CSC_CTRL_S ctrl;
ctrl.enMode = IVE_CSC_MODE_PIC_BT601_YUV2RGB;
ret = HI_MPI_IVE_CSC(&handle, &src_img, &dst_img, &ctrl, instant);
if (ret != HI_SUCCESS) {
printf("HI_MPI_IVE_CSC failed:%#x\n", ret);
return -1;
}
HI_BOOL finish = HI_FALSE;
ret = HI_MPI_IVE_Query(handle, &finish, HI_TRUE);
if (ret != HI_SUCCESS) {
printf("HI_MPI_IVE_Query failed:%#x\n", ret);
return -2;
}
if (finish != HI_TRUE) {
DEBUG("not finish\n");
return -3;
}
return 0;
}
三. BGR转RGB
QT层需要用的格式是RGB,所以上一步出来的BGR还需要用TDE转换成RGB,代码如下:
#include <hi_tde_api.h>
// src: 源图像
// dst:目标图像
// 将src指向的源BGR888图像转换成RGB888图像
// note: 都需要开辟好空间,并且源图像与目标图像大小一致
int Bgr888ToRgb888(VIDEO_FRAME_INFO_S *src, VIDEO_FRAME_INFO_S *dst) {
HI_S32 ret;
TDE_HANDLE handle = HI_TDE2_BeginJob();
if (handle < 0) {
DEBUG("HI_TDE2_BeginJob failed:%#x\n", handle);
return -1;
}
TDE2_SURFACE_S src_surface, dst_surface;
TDE2_RECT_S src_rect, dst_rect;
memset(&src_surface, 0, sizeof(src_surface));
src_surface.u32PhyAddr = src.stVFrame.u32PhyAddr[0];
src_surface.enColorFmt = TDE2_COLOR_FMT_BGR888;
src_surface.u32Height = src.stVFrame.u32Height;
src_surface.u32Width = src.stVFrame.u32Width;
src_surface.u32Stride = src.stVFrame.u32Stride[0] * 3; //这里的跨距是以字节为单位,所以是宽度乘以3
src_rect.s32Xpos = 0;
src_rect.s32Ypos = 0;
src_rect.u32Height = src.stVFrame.u32Height;
src_rect.u32Width = src.stVFrame.u32Width;
memset(&dst_surface, 0, sizeof(dst_surface));
dst_surface.u32PhyAddr = dst.stVFrame.u32PhyAddr[0];
dst_surface.enColorFmt = TDE2_COLOR_FMT_RGB888;
dst_surface.u32Height = dst.stVFrame.u32Height;
dst_surface.u32Width = dst.stVFrame.u32Width;
dst_surface.u32Stride = dst.stVFrame.u32Stride[0] * 3;
dst_rect.s32Xpos = 0;
dst_rect.s32Ypos = 0;
dst_rect.u32Height = dst.stVFrame.u32Height;
dst_rect.u32Width = dst.stVFrame.u32Width;
TDE2_OPT_S opt;
memset(&opt, 0, sizeof(opt));
opt.enAluCmd = TDE2_ALUCMD_NONE;
opt.enRopCode_Color = TDE2_ROP_BUTT;
opt.enRopCode_Alpha = TDE2_ROP_BUTT;
opt.enColorKeyMode = TDE2_COLORKEY_MODE_NONE;
opt.enClipMode = TDE2_CLIPMODE_NONE;
ret = HI_TDE2_Bitblit(handle, NULL, NULL, &src_surface, &src_rect, &dst_surface, &dst_rect, &opt);
if (ret != HI_SUCCESS) {
printf("HI_TDE2_Bitblit failed:%#x\n", ret);
HI_TDE2_CancelJob(handle);
return -2;
}
ret = HI_TDE2_EndJob(handle, HI_TRUE, HI_TRUE, 200);
if (ret != HI_SUCCESS) {
HI_TDE2_CancelJob(handle);
printf("HI_TDE2_EndJob failed:%#x\n", ret);
return -3;
}
return 0;
}
这里需要注意的是,对于TDE来说跨距都是以字节为单位的,所以如果是RGB888的图片,对于TDE的跨距就是以像素为单位的跨距的三倍,一般跨距都是等于图像宽度的,所以tde的跨距也就是宽度乘以3。本人之前就是这个搞错了调了好久都没调出来。
四. QT贴图
一般可以用QLabel来做,但是在没有支持显卡的情况下效率很低,所以最好使用QWidget来贴图,这个可以参考这篇文章:
https://worthsen.blog.csdn.net/article/details/80969451
需要注意的是,各个处理模块VGS、IVE、TDE都需要有物理地址的内存,我都是使用HI_MPI_SYS_MmzAlloc开辟出来的。不过这种内存在做memcpy时和Qt底层贴图时,效率会很低,不知道为什么。所以尽量少的对这种内存与malloc出来的内存做拷贝,如果要拷贝的也是mmz的内存可以使用IVE中的DMA拷贝比较快。我视频播放整个流程只在贴图的时候做了memcpy到虚拟内存,然后在QWidget的paintEvent函数里将虚拟内存贴到Widget上,比较好一些。
还有一点,其实TDE是支持从YUV直接转到RGB的,考虑到转BGR再到RGB效率也够就没再弄了。据我目前尝试的现象,TDE要将YUV转成RGB,除了YUV需要是422格式外,似乎还得是planar格式的,也就是Y、U、V分别是一片内存,另外要注意YUV的帧结构体的跨距应该是宽度乘以2。如果确实是plannar格式的,那么需要将YUVSP422转到YUVP422,这个VGS无法转换,可以考虑使用IVE的DMA有个间隔拷贝的方法间接实现,不然软件实现效率很低。