从零开始学习音视频编程技术(41) H.264播放器
时间:05月18日 人气:...


现在,我们已经简单的掌握了h.264数据的结构。是时候干点什么了,那就先来写一个H.264视频播放器吧。。

前面我们开发视频播放器的时候是通过:

avformat_open_input打开视频文件,然后再调用av_read_frame就可以读到一帧帧的数据了,

当然用这样的方法也可以直接打开并读取一个h.264文件,但是这样就违背了我们的初衷了,我们的目的是对上一节《H264数据格式讲解》的实践,因此我们采用C语言的文件操作直接读取文件然后再解析。


一个H264播放器的实现步骤大致如下:


一、从H.264文件中获取一个NALU

从上节可以知道,h264的NALU直接是用帧头(0x00000001或0x000001)隔开的,因此我们就逐个字节搜索,直到遇到h264的帧头,2个帧头之间的数据就是一个Nalu,既视为获取到了一帧h264视频数据。

查找一个nalu数据的代码大致如下:

NALU_t* h264Reader::getNextNal()\n{\n\n    ///首先查找第一个起始码\n    int pos = 0; //记录当前处理的数据偏移量\n    int StartCode = 0;\n\n    while(1)\n    {\n        unsigned char* Buf = mH264Buffer + pos;\n        int lenth = mBufferSize - pos; //剩余没有处理的数据长度\n        if (lenth <= 4)\n        {\n            return NULL;\n        }\n\n        ///查找起始码(0x000001或者0x00000001)\n\n        if(Buf[0]==0 && Buf[1]==0 && Buf[2] ==1)\n            //Check whether buf is 0x000001\n        {\n            StartCode = 3;\n            break;\n        }\n        else if(Buf[0]==0 && Buf[1]==0 && Buf[2] ==0 && Buf[3] ==1)\n         //Check whether buf is 0x00000001\n        {\n            StartCode = 4;\n            break;\n        }\n        else\n        {\n            //否则 往后查找一个字节\n            pos++;\n        }\n    }\n\n\n    ///然后查找下一个起始码查找第一个起始码\n    int pos_2 = pos + StartCode; //记录当前处理的数据偏移量\n    int StartCode_2 = 0;\n\n    while(1)\n    {\n        unsigned char* Buf = mH264Buffer + pos_2;\n        int lenth = mBufferSize - pos_2; //剩余没有处理的数据长度\n        if (lenth <= 4)\n        {\n            return NULL;\n        }\n\n        ///查找起始码(0x000001或者0x00000001)\n\n        if(Buf[0]==0 && Buf[1]==0 && Buf[2] ==1)\n            //Check whether buf is 0x000001\n        {\n            StartCode_2 = 3;\n            break;\n        }\n        else if(Buf[0]==0 && Buf[1]==0 && Buf[2] ==0 && Buf[3] ==1)\n         //Check whether buf is 0x00000001\n        {\n            StartCode_2 = 4;\n            break;\n        }\n        else\n        {\n            //否则 往后查找一个字节\n            pos_2++;\n        }\n    }\n\n    /// 现在 pos和pos_2之间的数据就是一个Nalu了\n    /// 把他取出来\n\n    ///由于传递给ffmpeg解码的数据 需要带上起始码 因此这里的nalu带上了起始码\n    unsigned char* Buf = mH264Buffer + pos; //这帧数据的起始数据(包含起始码)\n    int naluSize = pos_2 - pos; //nalu数据大小 包含起始码\n\n    NALU_HEADER *nalu_header = (NALU_HEADER *)Buf;\n\n    NALU_t * nalu = AllocNALU(naluSize);//分配nal 资源\n\n    nalu->startcodeprefix_len = StartCode;      //! 4 for parameter sets and first slice in picture, 3 for everything else (suggested)\n    nalu->len = naluSize;                 //! Length of the NAL unit (Excluding the start code, which does not belong to the NALU)\n    nalu->forbidden_bit = 0;            //! should be always FALSE\n    nalu->nal_reference_idc = nalu_header->NRI;        //! NALU_PRIORITY_xxxx\n    nalu->nal_unit_type = nalu_header->TYPE;            //! NALU_TYPE_xxxx\n    nalu->lost_packets = false;  //! true, if packet loss is detected\n\n    memcpy(nalu->buf, Buf, naluSize);  //! contains the first byte followed by the EBSP\n\n    /// 将这一帧数据去掉\n    /// 把后一帧数据覆盖上来\n    int leftSize = mBufferSize - pos_2;\n    memcpy(mH264Buffer, mH264Buffer + pos_2, leftSize);\n    mBufferSize = leftSize;\n\n    return nalu;\n}


二、使用ffmpeg解码上面获取到的NALU


1.h264解码器初始化

int H264Decorder::decoder_Init()\n{\n    /* find the h264 video decoder */\n    pCodec = avcodec_find_decoder(AV_CODEC_ID_H264);\n    if (!pCodec) {\n        fprintf(stderr, "codec not found\n");\n    }\n    pCodecCtx = avcodec_alloc_context3(pCodec);\n\n    /* open the coderc */\n    if (avcodec_open2(pCodecCtx, pCodec,NULL) < 0) {\n        fprintf(stderr, "could not open codec\n");\n    }\n\n    // Allocate video frame\n    pFrame = avcodec_alloc_frame();\n    if(pFrame == NULL)\n        return -1;\n\n    pFrameRGB = avcodec_alloc_frame();\n    if(pFrameRGB == NULL)\n            return -1;\n\n    return 0;\n}


2.解码并转成rgb32

int H264Decorder::decodeH264(uint8_t *inputbuf, int frame_size, uint8_t *&outBuf, int &outWidth, int &outHeight)\n{\n\n    int             got_picture;\n    int             av_result;\n\n    AVPacket pkt;\n    av_init_packet(&pkt);\n    pkt.data = inputbuf;\n    pkt.size = frame_size;\n\n    av_result = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, &pkt); //解码\n\n    if (av_result < 0)\n    {\n        fprintf(stderr, "decode failed: inputbuf = 0x%x , input_framesize = %d\n", inputbuf, frame_size);\n        return -1;\n    }\n    av_free_packet(&pkt);\n\n    //前面初始化解码器的时候 并没有设置视频的宽高信息,\n    //因为h264的每一帧数据都带有编码的信息,当然也包括这些宽高信息了,因此解码完之后,便可以知道视频的宽高是多少\n    //这就是为什么 初始化编码器的时候 需要初始化高度,而初始化解码器却不需要。\n    //解码器可以直接从需要解码的数据中获得宽高信息,这样也才会符合道理。\n    //所以一开始没有为bufferRGB分配空间 因为没办法知道 视频宽高\n    //一旦解码了一帧之后 就可以知道宽高了  这时候就可以分配了\n    if (bufferRGB == NULL)\n    {\n        int width = pCodecCtx->width;\n        int height = pCodecCtx->height;\n        \n        int numBytes = avpicture_get_size(PIX_FMT_RGB32, width,height);    \n        bufferRGB = (uint8_t *)av_malloc(numBytes*sizeof(uint8_t));\n    avpicture_fill((AVPicture *)pFrameRGB, bufferRGB, PIX_FMT_RGB32,width, height);\n\n    img_convert_ctx = sws_getContext(width,height,pCodecCtx->pix_fmt,width,height,PIX_FMT_RGB32,SWS_BICUBIC, NULL,NULL,NULL);\n\n        \n    }\n\n    if (got_picture)\n    {\n        //格式转换 解码之后的数据是yuv420p的 把她转换成 rgb的图像数据\n        sws_scale(img_convert_ctx,\n                (uint8_t const * const *) pFrame->data,\n                pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data,\n                pFrameRGB->linesize);\n\n        outBuf = bufferRGB;\n        outWidth = pCodecCtx->width;\n        outHeight = pCodecCtx->height;\n\n    }\n\n    return got_picture;\n}

三、使用Qt显示图像

直接用QImage加载得到的rgb32数据即可:

//把这个RGB数据 放入QIMage            \nQImage image = QImage((uchar *)bufferRGB, width, height, QImage::Format_RGB32);\n\n//然后传给主线程显示\nemit sig_GetOneFrame(image.copy(), ++frameNum);

然后在主线程显示出这个QImage即可。


四、播放速度控制

由于H264数据里面没有包含时间戳信息,因此只能根据帧率来做同步,举个栗子:比如视频帧率是15那么我们就每秒钟播放15张图像,既在显示完一帧图像后,延时(1000/15)毫秒,当然严格来说这个延时不大合理,因为他没有考虑解码消耗的时间,但我们不管他,有兴趣的自己去完善修改。

另外需要注意的是:视频帧率是在h264的Nalu数据里面的,因此需要成功解码一帧图像后才能获取到帧率信息。


主要代码大致如下:

void ReadH264FileThread::run()\n{\n    mH264Decorder->decoder_Init();\n\n    char fileName[512] = {0};\n    strcpy(fileName, mFileName.toLocal8Bit()); //GBK编码\n\n    FILE *fp = fopen(fileName,"rb");\n    if (fp == NULL)\n    {\n        qDebug("H264 file not exist!");\n    }\n\n    int frameNum = 0; //当前播放的帧序号\n\n    while(!feof(fp))\n    {\n        char buf[10240];\n        int size = fread(buf, 1, 1024, fp);//从h264文件读1024个字节 (模拟从网络收到h264流)\n        int nCount = mH264Reader->inputH264Data((uchar*)buf,size);\n\n        while(1)\n        {\n            //从前面读到的数据中获取一个nalu\n            NALU_t* nalu = mH264Reader->getNextNal();\n            if (nalu == NULL) break;\n\n            uint8_t *bufferRGB;\n            int width;\n            int height;\n\n            mH264Decorder->decodeH264(nalu->buf, nalu->len, bufferRGB, width, height);\n\n            int frameRate = mH264Decorder->getFrameRate(); //获取帧率\n\n            /// h264裸数据不包含时间戳信息  因此只能根据帧率做同步\n            /// 需要成功解码一帧后 才能获取到帧率\n            /// 为0说明还没获取到 则直接显示\n            if (frameRate != 0)\n            {\n                msleep(1000/frameRate);\n            }\n\n            //把这个RGB数据 放入QIMage\n            QImage image = QImage((uchar *)bufferRGB, width, height, QImage::Format_RGB32);\n\n            //然后传给主线程显示\n            emit sig_GetOneFrame(image.copy(), ++frameNum);\n\n        }\n    }\n\n    mH264Decorder->decoder_UnInit();\n}


至此,一个完整的h264播放器就完成了。



H264测试文件下载地址:https://download.csdn.net/download/qq214517703/10422777

完整工程下载地址:https://download.csdn.net/download/qq214517703/10423384


学习音视频技术欢迎访问 http://blog.yundiantech.com  

音视频技术交流讨论欢迎加 QQ群 121376426