当前位置:网站首页><四> H264解码输出yuv文件
<四> H264解码输出yuv文件
2022-07-02 13:39:00 【全栈程序员站长】
大家好,又见面了,我是你们的朋友全栈君。
现在来写下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。
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/147851.html原文链接:https://javaforall.cn
边栏推荐
- PWM控制舵机
- Day 18 of leetcode dynamic planning introduction
- 【云原生】简单谈谈海量数据采集组件Flume的理解
- 中国信通院《数据安全产品与服务图谱》,美创科技实现四大板块全覆盖
- 触发器:Mysql实现一张表添加或删除一条数据,另一张表同时添加
- LeetCode 6. Z 字形变换 (N字形变换)
- unity Hub 登录框变得很窄 无法登录
- Global and Chinese markets for slotting milling machines 2022-2028: Research Report on technology, participants, trends, market size and share
- TCP congestion control details | 2 background
- Kubernetes three open interfaces first sight
猜你喜欢
Classifier visual interpretation stylex: Google, MIT, etc. have found the key attributes that affect image classification
Seal Library - installation and introduction
Masa framework - DDD design (1)
Yyds dry inventory uses thread safe two-way linked list to realize simple LRU cache simulation
Recalling the college entrance examination and becoming a programmer, do you regret it?
Rock PI Development Notes (II): start with rock PI 4B plus (based on Ruixing micro rk3399) board and make system operation
Day 18 of leetcode dynamic planning introduction
Foreign enterprise executives, continuous entrepreneurs, yoga and skiing masters, and a program life of continuous iteration and reconstruction
TCP congestion control details | 2 background
做机器视觉哪个软件好?
随机推荐
Which software is good for machine vision?
Leetcode1380: lucky numbers in matrix
What if the win11 app store cannot load the page? Win11 store cannot load page
Penetration tool - intranet permission maintenance -cobalt strike
基于多元时间序列对高考预测分析案例
Global and Chinese market of desktop hot melt equipment 2022-2028: Research Report on technology, participants, trends, market size and share
Data security industry series Salon (III) | data security industry standard system construction theme Salon
Global and Chinese markets for disposable insulin pumps 2022-2028: Research Report on technology, participants, trends, market size and share
LeetCode 2. Add two numbers
Typescript array out of order output
TCP server communication process (important)
Understand the key technology of AGV -- the difference between laser slam and visual slam
历史上的今天:支付宝推出条码支付;分时系统之父诞生;世界上第一支电视广告...
曆史上的今天:支付寶推出條碼支付;分時系統之父誕生;世界上第一支電視廣告...
Rock PI Development Notes (II): start with rock PI 4B plus (based on Ruixing micro rk3399) board and make system operation
虚假的暑假
Written by unity Jason
Yyds dry goods inventory student attendance system based on QT design
Unity使用UGUI设置一个简单多级水平方向下拉菜单(不需要代码)
隐私计算技术创新及产业实践研讨会:学习