现在来写下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
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; }
当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。