[imx6 VPU]硬解码+示例[ffmpeg获取海康rtsp h264流 QT显示]

imx6 专栏收录该内容
1 篇文章 0 订阅

0、说明:

1,代码基于imx6q、imx6dl已验证。
2,网上关于imx6 VPU的资料很少,遂从官方例程mxc_vpu_test里面活生生抽出来。主要是dec_test()里面提取,因为我只要解码。
3,不涉及硬件IPU,GPU的图像处理显示
4,已封装成类,可直接调用,大部分为C代码
5,给出使用示例,rtsp+vpu+QT显示,你可以改成任何 [输入源+vpu+输出]
废话写在最后,先进主题……

1、头文件及相关定义

  1. 包含两个VPU头,vpu_io.h,vpu_lib.h,链接两个动态库libvpu.so,libvpu.so.4
  2. 包含若干ffmpeg头,链接若干ffmpeg库(只是示例中使用)
  3. 对外接口
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最后一点废话

  1. 最后释放资源的代码,好像似乎可能有点问题,好像重复释放了,没去管……
  2. 解码过程中,总是有一些解码错误块通知,目测没有什么影响,但是看着不爽,知道原因的请留言告诉我。
  3. 使用硬件VPU还有一个简单方式,gstreamer+imx插件。借用NXP社区其中一个老外的话:"Forget about mxc-test and just use gstreamer. "。mxc_test例程好用,但是要分离出你需要的,确实很麻烦。
  4. 代码下载地址
    https://download.csdn.net/download/qq_30659437/13045690
  • 1
    点赞
  • 4
    评论
  • 8
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 游动-白 设计师:白松林 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值