首先,不涉及視頻解碼,僅涉及解碼之後的視頻縮放、顏色空間轉換(如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有個間隔拷貝的方法間接實現,不然軟件實現效率很低。