0、说明:
1,代码基于imx6q、imx6dl已验证。
2,网上关于imx6 VPU的资料很少,遂从官方例程mxc_vpu_test里面活生生抽出来。主要是dec_test()里面提取,因为我只要解码。
3,不涉及硬件IPU,GPU的图像处理显示
4,已封装成类,可直接调用,大部分为C代码
5,给出使用示例,rtsp+vpu+QT显示,你可以改成任何 [输入源+vpu+输出]
废话写在最后,先进主题……
1、头文件及相关定义
- 包含两个VPU头,vpu_io.h,vpu_lib.h,链接两个动态库libvpu.so,libvpu.so.4
- 包含若干ffmpeg头,链接若干ffmpeg库(只是示例中使用)
- 对外接口
public:
VpuDecode(); //构造函数
~VpuDecode(); //析构
int init(void); //vpu初始化
int poll(void); //vpu解码主函数
int flush(void); //更新源码流数据
void exit(void); //退出vpu
u8 *stream_buf; //输入,h264码流数据
int stream_size; //输入,码流大小
u8 *dec_buf; //输出,解码后的YUV数据
int dec_size; //输出,解码数据大小
int dec_width; //输出,解码图像宽
int dec_height; //输出,解码图像高
int isIDR; //输出,当前帧是否I帧标志
4.主要的结构体
//一些参数配置时使用,不需要太关心
struct parameter
{
vpu_mem_desc mem_desc;
vpu_mem_desc ps_mem_desc;
vpu_mem_desc slice_mem_desc;
int eos; //未使用
int fill_end_bs; //未使用
DecInitialInfo initinfo;
DecOutputInfo outinfo;
DecParam decparam ;
PhysicalAddress pa_read_ptr;
PhysicalAddress pa_write_ptr;
u32 space;
int frame_id;
int totalNumofErrMbs;
};
//解码器主要的结构体
struct decode
{
DecHandle Handle; //解码器句柄
PhysicalAddress phy_bsbuf_addr; //码流buf物理地址
PhysicalAddress phy_ps_buf; //sps和pps缓冲区
PhysicalAddress phy_slice_buf; //slice缓冲区
u32 virt_bsbuf_addr; //码流buf虚拟地址
int phy_slicebuf_size; //slice缓冲区大小
int picwidth; //图像宽
int picheight; //图像高
Rect picCropRect; //裁剪
int stride; //跨度=图片宽
FrameBuffer *fb; //解码帧buf
struct frame_buf **pfbpool;
int lastPicWidth; //解码器解析出的上一个图像宽
int lastPicHeight; //解码器解析出的上一个图像高
int minfbcount; //解码所需的最小帧缓冲区数
int regfbcount; //用于注册的 帧缓冲区数,=最小帧缓冲区数+VPU_EXTENDED_BUFFER_COUNT
struct frame_buf fbpool[MAX_BUF_NUM];
};
5.代码注释中api即表示官方vpu库接口函数
6.文章只贴出类中public接口,其它子函数及其它相关定义自行下载代码分析。
2、初始化init()
调用前先将stream_buf指向原始码流,stream_size等于原始码流数据大小,并且一定要有至少一帧码流数据,因为vpu初始化的时候,需要解析码流信息,以便为解码器分配缓存空间
int VpuDecode::init(void)
{
int ret=-1;
dec=NULL;
memset(&par,0,sizeof(struct parameter)); //初始化参数结构体
ret = vpu_Init(NULL); //api,初始化VPU
if(ret!=RETCODE_SUCCESS)
{
printf("vpu_Init faild! ret=%d\n",ret);
ret=-1;
goto err1;
}
dec = (struct decode *)calloc(1,sizeof(struct decode)); //申请内存
if(dec ==NULL)
{
printf("decode memory malloc faild! ret=%d\n",ret);
ret=-1;
goto err2;
}
memset(dec, 0, sizeof(struct decode)); //初始化解码器结构体
par.mem_desc.size = STREAM_BUF_SIZE; //这是宏0x200000
ret = IOGetPhyMem(&par.mem_desc); //api,分配连续的物理内存
if (ret)
{
printf("mem_desc PhyMem malloc faild! ret=%d\n",ret);
ret = -1;
goto err3;
}
ret = IOGetVirtMem(&par.mem_desc); //api,获取物理内存对应的虚拟地址
if (ret==-1)
{
printf("mem_desc VirtMem malloc faild! ret=%d\n",ret);
ret = -1;
goto err4;
}
dec->phy_bsbuf_addr = par.mem_desc.phy_addr;
dec->virt_bsbuf_addr = par.mem_desc.virt_uaddr;
par.ps_mem_desc.size = PS_SAVE_SIZE;
ret = IOGetPhyMem(&par.ps_mem_desc); //api,为ps_mem_desc分配物理地址
if (ret)
{
printf("ps_mem_desc PhyMem malloc faild! ret=%d\n",ret);
ret = -1;
goto err5;
}
dec->phy_ps_buf = par.ps_mem_desc.phy_addr;
ret = decoder_open(); //主要配置解码器参数,并调用api vpu_DecOpen()打开解码器
if(ret)
{
printf("decoder_open faild! ret=%d\n",ret);
ret=-1;
goto err6;
}
ret = flush(); //更新码流数据到解码缓冲区
if(ret<0)
{
printf("dec_fill_bsbuffer faild! ret=%d\n",ret);
ret=-1;
goto err7;
}
//解析码流。主要调用api vpu_DecGetInitialInfo(),获取流信息 配置一些解码参数
ret = decoder_parse();
if (ret)
{
printf("decoder parse failed\n");
ret=-1;
goto err7;
}
par.slice_mem_desc.size = dec->phy_slicebuf_size;
ret = IOGetPhyMem(&par.slice_mem_desc); //api
if (ret) {
printf("Unable to obtain physical slice save mem\n");
ret = -1;
goto err7;
}
dec->phy_slice_buf = par.slice_mem_desc.phy_addr;
//根据解析出的参数,为解码之后的帧缓冲申请内存
ret = decoder_allocate_framebuffer();
if(ret)
{
printf("decoder_allocate_framebuffer faild\n");
ret = -1;
goto err8;
}
return 0;
err8:
IOFreePhyMem(&par.slice_mem_desc);
err7:
if(dec->Handle)
decoder_close();
err6:
IOFreePhyMem(&par.ps_mem_desc);
err5:
IOFreeVirtMem(&par.mem_desc);
err4:
IOFreePhyMem(&par.mem_desc);
err3:
free(dec);
err2:
vpu_UnInit();
err1:
return ret;
}
3、更新码流数据flsuh()
int VpuDecode::flush(void)
{
int ret;
u32 target_addr;
int size;
int nread, room;
u32 bs_va_startaddr = dec->virt_bsbuf_addr;
u32 bs_va_endaddr = dec->virt_bsbuf_addr + STREAM_BUF_SIZE;
u32 bs_pa_startaddr = dec->phy_bsbuf_addr;
// int fill_end_bs = 0;
int eos = 0;
//api 获取解码缓冲区可读可写的地址,以及可用空间
ret = vpu_DecGetBitstreamBuffer(dec->Handle, &par.pa_read_ptr, &par.pa_write_ptr, &par.space);
if (ret != RETCODE_SUCCESS)
{
printf("vpu_DecGetBitstreamBuffer failed\n");
return -1;
}
if (par.space <= 0) //判断当前解码器可用的缓冲区大小
{
printf("space %lu <= 0\n", par.space);
return -1;
}
size = ((par.space >> 9) << 9); //取整
if (size == 0) {
printf("size == 0, space %lu\n", par.space);
return -1;
}
//缓冲区可写起始地址
target_addr = bs_va_startaddr + (par.pa_write_ptr - bs_pa_startaddr);
//判断要去读多少数据
if ( (target_addr + size) > bs_va_endaddr)
{
room = bs_va_endaddr - target_addr;
//从stream_buf去读取room个数据到target_addr
nread = vpu_read((char *)target_addr, room);
if (nread <= 0)
{
/* EOF or error */
if (nread < 0)
{
printf("nread %d < 0\n", nread);
return -1;
}
eos = 1;
}
else
{
if (nread != room) //认为读完了
goto update;
/* read the remaining */
par.space = nread;
nread = vpu_read((char *)bs_va_startaddr, (size - room));
if (nread <= 0)
{
/* EOF or error */
if (nread < 0)
{
printf("nread %d < 0\n", nread);
return -1;
}
eos = 1;
}
nread += par.space;
}
}
else
{
nread = vpu_read((char *)target_addr, size);
if (nread <= 0)
{
/* EOF or error */
if (nread < 0)
{
printf("nread %d < 0\n", nread);
return -1;
}
eos = 1;
}
}
update:
if (eos == 0) //读取成功
{
ret = vpu_DecUpdateBitstreamBuffer(dec->Handle, nread); //api 通知解码器更新了nread大小的数据
if (ret != RETCODE_SUCCESS)
{
printf("vpu_DecUpdateBitstreamBuffer failed\n");
return -1;
}
}
else
{
ret = vpu_DecUpdateBitstreamBuffer(dec->Handle,
STREAM_END_SIZE); //宏STREAM_END_SIZE 0
if (ret != RETCODE_SUCCESS)
{
printf("vpu_DecUpdateBitstreamBuffer failed\n");
return -1;
}
}
return nread;
}
4、解码poll()
主角来了
int VpuDecode::poll(void)
{
DecHandle handle = dec->Handle;
RetCode ret;
int loop_id;
int is_waited_int = 0;
memset(&par.decparam,0,sizeof(DecParam));
par.decparam.dispReorderBuf = 0;
par.decparam.skipframeMode = 0; //跳帧模式关
par.decparam.skipframeNum = 0;
/*
* once iframeSearchEnable is enabled, prescanEnable, prescanMode
* and skipframeMode options are ignored.
*/
par.decparam.iframeSearchEnable = 0; //I帧搜索关闭
ret = vpu_DecStartOneFrame(handle, &par.decparam); //api 开始解码一帧
if (ret == RETCODE_JPEG_EOS) //rtsp其实不会到最后一帧
{
printf(" JPEG bitstream is end\n");
return -1;
}
else if (ret == RETCODE_JPEG_BIT_EMPTY) //图片位为空
{
printf(" RETCODE_JPEG_BIT_EMPTY\n");
return -1;
}
if (ret != RETCODE_SUCCESS)
{
printf("DecStartOneFrame failed, ret=%d\n", ret);
return -1;
}
is_waited_int = 0;
loop_id = 0;
while (vpu_IsBusy()) //api 等待解码完成
{
if (loop_id == 50)
{
vpu_SWReset(handle, 0);//api
return -1;
}
if (vpu_WaitForInt(100) == 0)//api
is_waited_int = 1;
loop_id ++;
}
if (!is_waited_int)
vpu_WaitForInt(100); //api
ret = vpu_DecGetOutputInfo(handle, &par.outinfo); //api 读取解码信息
// usleep(0); //让出线程时间片
if (ret != RETCODE_SUCCESS) {
printf("vpu_DecGetOutputInfo failed Err code is %d\n"
"\tframe_id = %d\n", ret, (int)par.frame_id);
return -1;
}
if (par.outinfo.decodingSuccess == 0)
{
printf("Incomplete finish of decoding process.\n"
"\tframe_id = %d\n", (int)par.frame_id);
return -1;
}
if (par.outinfo.decodingSuccess & 0x10)
{
printf("vpu needs more bitstream in rollback mode\n"
"\tframe_id = %d\n", (int)par.frame_id);
return 0;
}
if (par.outinfo.notSufficientPsBuffer) {
printf("PS Buffer overflow\n");
return -1;
}
if (par.outinfo.notSufficientSliceBuffer) {
printf("Slice Buffer overflow\n");
return -1;
}
if(par.outinfo.indexFrameDecoded >= 0)
{
//如果有错误(解码图片时出现的许多错误宏块,实测大概101帧出现一次,原因未知,不影响效果)
if (par.outinfo.numOfErrMBs) {
par.totalNumofErrMbs += par.outinfo.numOfErrMBs;
printf("Num of Error Mbs : %d, in Frame : %d \n",
par.outinfo.numOfErrMBs, par.frame_id);
}
par.frame_id++; //帧计数
dec_width = par.outinfo.decPicWidth;
dec_height = par.outinfo.decPicHeight;
isIDR = par.outinfo.idrFlg;
int index = par.outinfo.indexFrameDecoded; //获取帧在缓存中的索引
//获取帧buf
dec_buf = (u8 *)(dec->pfbpool[index]->addrY +
dec->pfbpool[index]->desc.virt_uaddr -
dec->pfbpool[index]->desc.phy_addr);
/*清除显示标志,解码数据才能再次放到此索引下的pfbpool缓存池中.相当于告诉vpu,此帧已经使用,下次解码数据可以覆盖此数据
*未启用图片重新排序功能,所以indexFrameDisplay == indexFrameDecoded*/
vpu_DecClrDispFlag(handle,par.outinfo.indexFrameDisplay);
return 0;
}
vpu_DecClrDispFlag(handle,par.outinfo.indexFrameDisplay);
return -1;
}
5、退出解码exit()
主要进行资源释放,具体见代码
6、主函数
QT+ffmpeg播放使用示例
void VideoPlayer::run()
{
#define VIDEO_WIDTH 1280
#define VIDEO_HEIGHT 720
#define INPUT_PIX_FMT AV_PIX_FMT_YUV420P
#define OUTPUT_PIX_FMT AV_PIX_FMT_RGB32
AVFormatContext *pFormatCtx=NULL;
VpuDecode *vpu=NULL;
AVPacket packet;
int videoStreamIndex=-1;
unsigned int i;
int numBytes;
int ret;
int first_frame=1;
AVFrame *pFrame;
AVFrame *pFrameRGB;
static struct SwsContext *img_convert_ctx;
uint8_t *out_buffer;
AVDictionary *avdic=NULL;
avformat_network_init(); //初始化FFmpeg网络模块
// av_register_all(); //(已废弃)
av_init_packet(&packet); //初始化一个avpack
pFormatCtx = avformat_alloc_context(); //申请一个视频结构体上下文
if(pFormatCtx == NULL) goto err1;
char url[]="rtsp://admin:123456@192.168.0.64:554/h264/ch1/main/av_stream";
//---------视频流获取----------//
av_dict_set(&avdic,"max_delay","100",0); //连接最大延时
av_dict_set(&avdic,"rtsp_transport","tcp",0); //连接方式
//探测码流信息的buf大小,如果太大,会导致播放延时
//如果不设置,会使用默认值,大概有3-5秒数据缓存(720P情况),那么,显示的时候,显示的是3-5秒之前的数据,导致不实时
av_dict_set(&avdic,"probesize","2048",0);
av_dict_set(&avdic,"max_analyze_duration","10",0); //最大间隔10ms
ret = avformat_open_input(&pFormatCtx, url, NULL, &avdic); //打开摄像头连接
if ( ret )
{
emit printinf(QString("连接失败!请检查信息!"),false);
goto err2;
}
ret = avformat_find_stream_info(pFormatCtx, NULL); //查找流
if ( ret )
{
emit printinf(QString("未发现流信息"),false);
goto err3;
}
//查找视频流索引
for (i = 0; i < pFormatCtx->nb_streams; i++)
{
if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
{
videoStreamIndex = i; //获取视频流索引
}
}
//如果videoStream为-1 说明没有找到视频流
if (videoStreamIndex == -1) {
emit printinf(QString("未有发现视频流"),false);
goto err3;
}
//av_dump_format(pFormatCtx, 0, url, 0); //打印视频信息
//----图片格式转换相关-----//
pFrame = av_frame_alloc(); //yuv数据帧
if(pFrame == NULL)
{
emit printinf(QString("内存不足"),false);
goto err3;
}
pFrameRGB = av_frame_alloc(); //RGB数据帧
if(pFrameRGB == NULL)
{
emit printinf(QString("内存不足"),false);
goto err4;
}
// numBytes = avpicture_get_size(AV_PIX_FMT_RGB32, VIDEO_WIDTH,VIDEO_HEIGHT); //获取图片大小(已弃用)
numBytes = av_image_get_buffer_size(OUTPUT_PIX_FMT, VIDEO_WIDTH,VIDEO_HEIGHT,1);
out_buffer = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t)); //申请解码转换RGB后的一帧buf
if(out_buffer ==NULL)
{
emit printinf(QString("内存不足"),false);
goto err5;
}
//填充RGB frame
av_image_fill_arrays(pFrameRGB->data,pFrameRGB->linesize,
out_buffer, OUTPUT_PIX_FMT,VIDEO_WIDTH,VIDEO_HEIGHT,1);
//申请图片格式转换上下文结构体
img_convert_ctx = sws_getContext(VIDEO_WIDTH,VIDEO_HEIGHT,
INPUT_PIX_FMT, VIDEO_WIDTH,VIDEO_HEIGHT,
OUTPUT_PIX_FMT, SWS_FAST_BILINEAR, NULL, NULL, NULL);
if(img_convert_ctx==NULL)
{
emit printinf(QString("内存不足"),false);
goto err6;
}
//vpu解码类
vpu=new VpuDecode();
emit printinf(QString("正在显示摄像头画面"),false); //发送信号
while (a_toStop == false)
{
ret = av_read_frame(pFormatCtx, &packet); //读取一帧
if (ret < 0)
{
continue;
}
if (packet.stream_index == videoStreamIndex) //如果是视频流
{
vpu->stream_buf = packet.data; //获取视频流数据
vpu->stream_size = packet.size; //数据大小
if( first_frame ) //如果是第一帧
{
ret = vpu->init(); //根据视频流初始化vpu
if(ret)
{
printf("VPU init faild\n");
goto err7;
}
first_frame = 0;
continue; //第一帧只用于配置vpu,不解码(也可以去解码)
}
else //如果不是第一帧,vpu已经初始化好了,后面只管更新数据即可
{
ret = vpu->flush(); //更新stream_buf数据到解码缓冲区
if(ret<0)
{
printf("vpu dec_fill_bsbuffer faild\n");
continue;
}
}
ret = vpu->poll(); //解码
if(ret)
{
continue;
}
//---到这里得到yuv数据,就可以直接取vpu->dec_buf和vpu->dec_size使用。
//---1.write到文件---//
//---2.调用ipu转换成RGB去显示---//
//这里我使用ffmpeg进行软解,比较耗时,且占CPU高//
//填充转换图片数据
av_image_fill_arrays(pFrame->data,pFrame->linesize,
(uint8_t *)vpu->dec_buf, INPUT_PIX_FMT,VIDEO_WIDTH,VIDEO_HEIGHT,1);
//转换成RGB图片
sws_scale(img_convert_ctx, (uint8_t const * const *) pFrame->data, pFrame->linesize, 0,
VIDEO_HEIGHT,
pFrameRGB->data,
pFrameRGB->linesize);
//送给QT去显示图片
QImage tmpImg((u8*)out_buffer,vpu->dec_width,vpu->dec_height,QImage::Format_RGB32);
emit sig_GetOneFrame(tmpImg); //发送信号
}
msleep(1);
} //while end
vpu->exit(); //退出解码器
printf("vpu exit!\n");
err7:
sws_freeContext(img_convert_ctx);
err6:
av_free(out_buffer);
err5:
av_frame_free(&pFrameRGB);
err4:
av_frame_free(&pFrame);
err3:
avformat_close_input(&pFormatCtx);
err2:
avformat_free_context(pFormatCtx);
err1:
emit printinf(QString("已结束播放"),false); //发送信号
}
7最后一点废话
- 最后释放资源的代码,好像似乎可能有点问题,好像重复释放了,没去管……
- 解码过程中,总是有一些解码错误块通知,目测没有什么影响,但是看着不爽,知道原因的请留言告诉我。
- 使用硬件VPU还有一个简单方式,gstreamer+imx插件。借用NXP社区其中一个老外的话:"Forget about mxc-test and just use gstreamer. "。mxc_test例程好用,但是要分离出你需要的,确实很麻烦。
- 代码下载地址