<四> H264解码输出yuv文件

2022-07-02 13:12:44 浏览数 (1)

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

现在来写下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模式查看)。

代码语言:javascript复制
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等,具体如下。

代码语言:javascript复制
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相&获得。比如上面第一个数据单元:

代码语言:javascript复制
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数据。

代码语言:javascript复制
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缓冲区中,读取的长度是缓冲区最大长度的一半
代码语言:javascript复制
    // 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 filen");
            end_of_file = 1;
        }
        if(rsz>0)
            fend  = rsz;
    }
    if(fend>fbufsz) {
        fprintf(stderr,"Opps: this should never happen!n");
        return -1;
    }
代码语言:javascript复制
    // 二、读取文件头数据
    // 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=%dn",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. Exitingn");
        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数据单元)的长度。

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

代码语言:javascript复制
    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 parsern");
        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 headern");
        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 %dn",err);
        goto clr_mfc;
    }
    printf("SsbSipMfcDecInit succeeded..n");

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

代码语言:javascript复制
    // 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. Exitingn");
                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 %dn",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%dn",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分量。

代码语言:javascript复制
    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。

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

0 人点赞