<四> H264解码输出yuv文件

<四> H264解码输出yuv文件现在来写下s5pv210的h264解码,这一章有些部分我理解的不是很透彻,只能写个大概了。希望看到的人能给出些意见,有些地方写错的还望指正出来!  解码过程与编码过程类似,编码过程是先初始化编码器,然后从编码器输出buf中读出h264文件头数据,写入输出文件,然后开始不断地将一帧帧NV12格式的图像写入到编码器的输入buf,启动编码,从编码器输出buf中将h264视频数据写入到输出文件。解

大家好,又见面了,我是你们的朋友全栈君。

    现在来写下s5pv210的h264解码,这一章有些部分我理解的不是很透彻,只能写个大概了。希望看到的人能给出些意见,有些地方写错的还望指正出来!

    解码过程与编码过程类似,编码过程是先初始化编码器,然后从编码器输出buf中读出h264文件头数据,写入输出文件,然后开始不断地将一帧帧NV12格式的图像写入到编码器的输入buf,启动编码,从编码器输出buf中将h264视频数据写入到输出文件。解码是首先打开一个h264格式的文件作为输入文件,从这个文件中先读出文件头数据,写入到解码器的输入buf中,再初始化解码器,之后就是不断地将H264格式输入文件中的一段段NALU数据写入到解码器的输入buf,启动解码,从解码器输出buf中读取NV12格式的数据,然后转换成YUV420p格式写入到输出文件中。

    上面一段中所提到的H264文件头数据其实是一段包含SPS(序列参数集)、PPS(图像参数集)的数据,里面的参数用来配置解码器的初始化。与编码过程中读取一帧帧NV12格式的图像数据不同,因为NV12格式每一帧长度是一样的。而H264格式文件中每一段NALU的长度不是固定的,这就需要在读取文件中做判断。下面给出一个h264格式文件的前160个字节(文件用Hex模式查看)。

00 00 00 01 67 64 00 28 ac d3 05 07 e4 00 00 00
01 68 ea 40 6f 2c 00 00 00 01 65 b8 40 57 8a b4
03 0e 39 4a 43 8f 20 fb db 09 bb ae 57 d1 94 e4
20 8c e7 8b 44 b0 03 1c 72 59 78 bf 57 a6 f1 f8
9f 33 ce 4a 5c b4 e1 be 52 03 3d 0b 64 74 37 a7
57 42 8e a1 39 75 03 d6 68 a3 2f e0 a3 0b 26 e3
a1 74 5a e5 b6 34 85 e6 10 c9 82 0f 53 12 47 cc
c8 0f 28 1d 9e 26 7c ac ed 4b e4 00 ea 64 ca 8a
3b 2c 4f f4 05 84 8d cd 6f 96 02 d1 92 be 0b dc
1f e5 5a 35 ea ed 87 a9 1b 7f ca 3c b3 53 a1 89

    里面有几个特殊的字段“00 00 00 01”,这个即是h264格式文件中每一段NALU数据中各个数据单元的头部,这些数据单元可以是SPS、PPS、SEI等,具体如下。

enum H264NALTYPE{ 
    H264NT_NAL = 0, 
    H264NT_SLICE,        //1 非IDR图像的编码条带 
    H264NT_SLICE_DPA,    //2 编码条带数据分割块A
    H264NT_SLICE_DPB,    //3 编码条带数据分割块B
    H264NT_SLICE_DPC,    //4 编码条带数据分割块C
    H264NT_SLICE_IDR,    //5 IDR图像的编码条带
    H264NT_SEI,          //6 增强信息
    H264NT_SPS,          //7 序列参数集
    H264NT_PPS,          //8 图像参数集
}; 

    区分这些数据单元,可以取“00 00 00 01”字段后一字节的数据,与0x1f相&获得。比如上面第一个数据单元:

00 00 00 01 67 64 00 28 ac d3 05 07 e4

    说明这个是一段SPS(67&1f = 7)。既然解码是是以一段NALU数据为单位的,那么如何区分一段NALU中有几个数据单元呢?这是根据数据单元的类型定义的。其中SEI、SPS与PPS如果相邻则放在一段NALU数据中,给编码器做初始化用。SLICE和SLICE_IDR分别属于单独的NALU数据段,但SLICE_IDR为关键帧,SLICE为P帧,P帧为单向预测编码或帧内预测编码,依赖于关键帧。也即是说,解码是,在P帧的前面一般至少要有一帧关键帧发给解码器,否则不能正常解码图像信息。

    接下来既可以说下这个h264格式的文件怎么读取了。首先是读取文件的头部,从SPS/PPS/SEI数据单元开始读,遇到SLICE/SLICE_IDR数据单元时停止,将读到的数据写入到解码器的输入buf中,然后初始化解码器。之后开始不断读取一段段NALU数据(可以是SPS/PPS/SE连续数据单元+SLICE/SLICE_IDR数据单元,也可以是一个SLICE数据单元,或者是一个SLICE_IDR数据单元)。

    下面看h264格式文件读取的代码。这个函数返回读取一段NALU数据的长度,数据会拷贝到buf指针处,当header为1是是读取文件头信息,为0时时正常读取一段NALU数据。

int read_one_frame(FILE *fp, uint8_t **buf, int header)
{
    static int end_of_file = 0;
    int ustart, uend;
    int cstart, cend;
    int found;
    uint8_t nal_unit_type;
    
    // 一、从文件中读取一段数据到fbuf缓冲区中,读取的长度是缓冲区最大长度的一半
    // fstart==fend : empty
    // we keep fstart<=fend. whenever fend goes beyond fbufsz, we move the data back to [0 ...)
    int rsz;
    if(!end_of_file && fend-fstart<fbufsz/2) { // fbuf is less than half full
        if (fstart>fbufsz/2) { 	// move back to [0 ...)
            memcpy(fbuf,fbuf+fstart, fend-fstart);
            fend-=fstart;
            fstart=0;
        }
        // fill up to half: fbufsz/2-fend+fstart
        rsz = fread(fbuf+fend, 1, fbufsz/2-fend+fstart, fp);
        if(rsz<(int)(fbufsz/2-fend+fstart)) { // end of file
            printf("We have read all data from the input file\n");
            end_of_file = 1;
        }
        if(rsz>0)
            fend += rsz;
    }
    if(fend>fbufsz) {
        fprintf(stderr,"Opps: this should never happen!\n");
        return -1;
    }
    
    // 二、读取文件头数据
    // now either fbuf is half full or it is end of file
    if(header) { // find header
        // find the first SPS,PPS,SEI header
        found = 0;
        cstart = cend = -1;
        while (find_nal_unit(fbuf+fstart, fend-fstart, &ustart, &uend)>0) {

        	nal_unit_type = fbuf[fstart+ustart] & 0x1f;
            if(nal_unit_type==(uint8_t)6 || nal_unit_type==(uint8_t)7 || nal_unit_type==(uint8_t)8) {
                // SEI, SPS or PPS
                if(!found){
                    found = 1;
                    cstart = fstart+ustart-3; // the start of first SPS, PPS or SEI, fbuf[cstart]: 00 00 01
                    if(cstart>0 && !fbuf[cstart-1])
                        cstart--;
                }
            }else {
                if(found) {
                    cend = fstart+ustart-3; // the end of header before the following picture slice NAL. fbuf[cend]: 00 00 01
                    if (!fbuf[cend-1]) { // the following picture slice has a long start code 00 00 00 01
                        cend--;
                    }
                    break;
                }
            }
            fstart+=uend; // now fbuf[fstart] is the first byte of start code of next NAL
        }
        
        if(cstart<0 || cend<0) {
            fprintf(stderr,"Error: cannot find a NAL header.\n");
            buf = NULL;
            if(!end_of_file)
                fprintf(stderr,"You should consider increase fbufsz. Current fbufsz=%d.\n",fbufsz);
            return -1;
        }
        
        fstart = cend;
        
        // now fbuf[cstart,cend) should contain the first SPS,PPS,SEI header
        printf("Header: cstart=%x, cend=%x, length=%d\n",cstart,cend,cend-cstart);
        *buf=fbuf+cstart;
        
        return cend-cstart;
        
    }   
    
    // 三、读取一段NALU数据
    cstart = cend = -1;
    found = 0;
    while (find_nal_unit(fbuf+fstart, fend-fstart, &ustart, &uend)>0) {
        nal_unit_type = fbuf[fstart+ustart] & 0x1f;
        if(nal_unit_type==(uint8_t)6 || nal_unit_type==(uint8_t)7 || nal_unit_type==(uint8_t)8) {
            // SEI, SPS or PPS
            if(!found){
                found = 1;
                cstart = fstart+ustart-3; // the start of first SPS, PPS or SEI, fbuf[cstart]: 00 00 01
                if(cstart>0 && !fbuf[cstart-1])
                    cstart--;
            }
        }else if(nal_unit_type==(uint8_t)1 || nal_unit_type==(uint8_t)5) { // IDR or non-IDR
            if(!found) { // no header
                cstart = fstart+ustart-3;
                if(cstart>0 && !fbuf[cstart-1])
                    cstart--;
            }
            cend = fstart+uend;
            break;
        }
        fstart+=uend; // now fbuf[fstart] is the first byte of start code of next NAL
    }
    
    if(cstart<0 || cend<0) {
        //printf("No more NALs. Exiting\n");
        buf = NULL;
        if(!end_of_file)
            fprintf(stderr,"You should consider increase fbufsz. Current fbufsz=%d.\n",fbufsz);
        return -1;
    }
    
    fstart = cend;
    
    *buf=fbuf+cstart;
    return cend - cstart;
}

    函数有点长,不过总体上分为三部分。第一部分是从文件中读入数据到fbuf缓冲区,并使缓冲区数据保持一半空间存有数据。第二部分是读取文件头数据,find_nal_unit()函数为读取一个数据单元,即两个“00 00 00 01”字段之间的数据,然后判断数据单元类型,当为SPS(7),PPS(8),SEI(6)时则继续读,直到遇到其它类型数据单元时,将fbuf中前面几个数据单元的起始地址赋给buf,然后返回前面几个数据单元(不包含其它数据类型)的长度,即完成了文件头数据的读取。

    当header不等于1时,会执行第三部分程序,读取一段NALU数据。可以看到第三部分程序,先是用find_nal_unit()函数读取一个数据单元,接着判断单元类型,是SPS(7),PPS(8),SEI(6)时则继续读,读到SLICE/SLICE_IDR数据单元时停止,将这端NALU数据的起始地址赋给buf,然后返回NALU数据段(包含一个SLICE/SLICE_IDR数据单元)的长度。

    好了,知道文件怎么读取了,接下来解码就简单多了。首先是解码器初始化的代码。

    unsigned int buf_type = CACHE;
    void *openHandle;
    SSBSIP_MFC_ERROR_CODE err;
    SSBSIP_MFC_DEC_OUTPUT_INFO oinfo;
    FILE *fpi, *fpo; 					// input and output files
    
    // 打开输入输出文件
    char *ifile=DEFAULT_INPUT_FILE, *ofile=DEFAULT_OUTPUT_FILE;
    if(!(fpi = fopen(ifile,"rb"))) {
        fprintf(stderr,"Error: open input file %s.\n",ifile);
        return 1;
    }
    if(!(fpo = fopen(ofile,"wb"))) {
        fprintf(stderr,"Error: open output file %s.\n",ofile);
        goto clr_fpi;
    }
    printf("Input file: %s. Output file: %s.\n", ifile,ofile);
    
    //初始化文件读入buf
    if(init_frame_parser()<0) {
        fprintf(stderr,"Error: init frame parser\n");
        goto clr_fpo;
    }
    
    // find the first SPS,PPS,SEI header -> 读取h264文件头到frmbuf中
    int frmlen;
	uint8_t * frmbuf;
    if((frmlen=read_one_frame(fpi,&frmbuf,1))<=0) {
        fprintf(stderr,"Error: cannot find header\n");
        goto clr_parser;
    }
    
    // 打开解码器
    openHandle = SsbSipMfcDecOpen(&buf_type);
    if(!openHandle) {
        fprintf(stderr,"Error: SsbSipMfcDecOpen.\n");
        goto clr_parser;
    }
    printf("SsbSipMfcDecOpen succeeded.\n");
    
    // 获得解码器输入buf地址->virInBuf
    void * phyInBuf;
	void * virInBuf;
    virInBuf = SsbSipMfcDecGetInBuf(openHandle, &phyInBuf, MAX_DECODER_INPUT_BUFFER_SIZE);
    if(!virInBuf) {
        fprintf(stderr,"Error: SsbSipMfcDecGetInBuf.\n");
        goto clr_mfc;
    }
    printf("SsbSipMfcDecGetInBuf succeeded.\n");
    // 将文件头数据拷贝到解码器输入buf
    memcpy(virInBuf,frmbuf,frmlen);
    
    // 初始化解码器
    err = SsbSipMfcDecInit(openHandle, H264_DEC, frmlen);
    if(err<0) {
        fprintf(stderr,"Error: SsbSipMfcDecInit. Code %d\n",err);
        goto clr_mfc;
    }
    printf("SsbSipMfcDecInit succeeded..\n");

    程序首先打开了输入文件和输出文件,输出文件fpo 在解码部分才会使用。输入文件即fpi 就是H264格式文件了,程序首先通过调用read_one_frame(fpi,&frmbuf,1)) 函数读出文件头数据,然后将数据拷贝入解码器输入buf,最后初始化了解码器。

    解码器初始化完成后,接下来是正式的解码过程了。代码如下。

    // now start decoding
    status = MFC_GETOUTBUF_STATUS_NULL;
    read_cnt = 0;
    show_cnt = 0;
    do {
        if (status != MFC_GETOUTBUF_DISPLAY_ONLY) {
            // read one frame
            if((frmlen = read_one_frame(fpi,&frmbuf,0))<=0) {
                printf("No more NALs. Exiting\n");
                break;
            }else{
            	printf("%d frames len %d!\n", ++read_cnt, frmlen);
            }
            memcpy(virInBuf, frmbuf, frmlen);
        }
        err = SsbSipMfcDecExe(openHandle, frmlen);
        if(err<0) {
            fprintf(stderr,"Error: SsbSipMfcDecExe. Code %d\n",err);
            break;
        }
        
        memset(&oinfo, 0, sizeof(oinfo));
        status = SsbSipMfcDecGetOutBuf(openHandle,&oinfo);

        if(status==MFC_GETOUTBUF_DISPLAY_DECODING || status==MFC_GETOUTBUF_DISPLAY_ONLY) {
            if(!ylin)
                ylin = (uint8_t *)malloc(oinfo.img_width*oinfo.img_height);
            if(!ylin) {
                fprintf(stderr,"Out of memory.\n");
                break;
            }
            // converted tiled to linear nv12 format - Y plane
            csc_tiled_to_linear(ylin, (uint8_t *)oinfo.YVirAddr, oinfo.img_width, oinfo.img_height);
            fwrite(ylin,1, oinfo.img_width*oinfo.img_height, fpo);
            
            if(!clin)
                clin = (uint8_t *)malloc(oinfo.img_width*oinfo.img_height/2);
            if(!clin) {
                fprintf(stderr,"Out of memory.\n");
                break;
            }

            p_U = (uint8_t *)clin;
            p_V = (uint8_t *)clin;
            p_V += ((oinfo.img_width * oinfo.img_height) >> 2);
            // converted tiled to linear uv format - C plane
            csc_tiled_to_linear_deinterleave(p_U, p_V, (uint8_t *)oinfo.CVirAddr, oinfo.img_width, oinfo.img_height/2);

            fwrite(clin,1,oinfo.img_width*oinfo.img_height/2,fpo);
            show_cnt++;
        }
    } while (1);
    
    printf("Decoding completed! Total number of decoded frames: %d.\nThe video has a dimension of: ", show_cnt);
    printf("img %dx%d, buf %dx%d\n",oinfo.img_width,oinfo.img_height, oinfo.buf_width,oinfo.buf_height);

    解码过程与编码过程类似,首先read_one_frame(fpi,&frmbuf,0)) 函数读取一段NALU数据,然后用memcpy(virInBuf, frmbuf, frmlen) 函数将数据拷贝到解码器输入buf,接着调用SsbSipMfcDecExe(openHandle, frmlen) 函数来启动一次解码,最后用SsbSipMfcDecGetOutBuf(openHandle,&oinfo) 函数获取解码的输出数据,由于解码器输出的格式是NV12,而且是tiled类型的,这里需要进行格式转换。转换时先转换Y分量,然后转换UV分量。

    csc_tiled_to_linear(ylin, (uint8_t *)oinfo.YVirAddr, oinfo.img_width, oinfo.img_height);
    fwrite(ylin,1, oinfo.img_width*oinfo.img_height, fpo);
    csc_tiled_to_linear_deinterleave(p_U, p_V, (uint8_t *)oinfo.CVirAddr, oinfo.img_width, oinfo.img_height/2);             
    fwrite(clin,1,oinfo.img_width*oinfo.img_height/2,fpo);

    这样就完成了写一帧解码后YUV格式图像到输出文件,这个文件可以用YUV
格式播放器打开,播放器下载地址为http://www.yuvplayer.com/。

    要注意的是,测试这个程序是,所选的h264格式文件不要太大,因为解码后的yuv格式文件很大,所以编码h264格式文件时,尺寸要小于640*480,帧数小于200帧最好。其实是smart210板子上可用的存储空间太小了,不到180M,不够用啊!下面一章我会写一个解码后直接用液晶显示的,不存储就不会有这个问题了。顺便调整下编码参数,使编码后的图像足够清晰。

    整个工程的代码我上传到了http://download.csdn.net/detail/westlor/9396310。

    

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/147851.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号