您的位置:首页 > 其它

<四> H264解码输出yuv文件

2016-01-08 15:50 302 查看
现在来写下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。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: