x264开源工程实现H.264的视频编码,但没有提供对应的解码器。ffmpeg开源多媒体编解码集合汇集了市面上几乎所有媒体格式的编解码的源代码。其中的H264.c就是一个能正常解码x264编码码流的独立的源文件,其使用步骤也与上述的编码或解码CODEC应用案例基本相同。这一节通过自顶向下的方式,讲述H264.c如何实现H.264视频解码过程。
H264.c源文件有几千行,代码量庞大,很不便于浏览、分析和移植。同时该文件还依赖其他源文件,组织结构较复杂,实现平台由于不是基于Windows的VC++,对编译、跟踪等带来了很多不便。光盘中“chapter7\ffmpeg_h264”目录给出了一个从ffmpeg抽取的C语言实现H.264视频解码的项目案例“ffmpeg_h264”。“ffmpeg_h264”的文件功能分类明确,在VC++2005平台下编译,且剔除了MMX的支持,纯C语言工程便于嵌入式平台的移植。下面从main主函数入手,逐步深入分析实现H.264视频解码的过程。
ffmpeg_h264工程
1 解码主程序
函数main实现H.264视频解码的控制台应用,包括了解码工作的所有过程。其函数调用关系如图所示。
上述应用中,拓扑结构流程与前述的MPEG-1视频解码过程基本相同,包括了avcodec_init、avcodec_register_all、avcodec_find_decoder、avcodec_all_context、avcodec_open、avcodec_all_frame、avcodec_decode_video、avcodec_close等ffmpeg的几大公共模块,其中avcodec_decode_video是解码器的核心,在图中做了标注。main函数的代码实现如下:
int main(){
FILE * inp_file;
FILE * out_file;
int i;
int nalLen; /*NAL 长度*/
unsigned char* Buf; /*H.264码流*/
int got_picture; /*是否解码一帧图像*/
int consumed_bytes; /*解码器消耗的码流长度*/
int cnt=0;
AVCodec *codec; /* 编解码CODEC*/
AVCodecContext *c; /* 编解码CODEC context*/
AVFrame *picture; /* 解码后的图像*/
/*输出和输出的文件*/
inp_file = fopen("e:\\bitavc\\busn_dsp.264", "rb");
out_file = fopen("e:\\bitavc\\dsp_dec.yuv", "wb");
nalLen = 0;
/*分配内存,并初始化为0*/
Buf = (unsigned char*)calloc ( 500*1024, sizeof(char));
/*CODEC的初始化,初始化一些常量表*/
avcodec_init();
/*注册CODEC*/
avcodec_register_all();
/*查找 H264 CODEC*/
codec = avcodec_find_decoder(CODEC_ID_H264);
if (!codec) return 0;
/*初始化CODEC的默认参数*/
c = avcodec_alloc_context();
if(!c) return 0;
/*1. 打开CODEC,这里初始化H.264解码器,调用decode_init本地函数*/
if (avcodec_open(c, codec) < 0) return 0;
/*为AVFrame申请空间,并清零*/
picture = avcodec_alloc_frame();
if(!picture) return 0;
/*循环解码*/
while(!feof(inp_file)) {
/*从码流中获得一个NAL包*/
nalLen = getNextNal(inp_file, Buf);
/*2. NAL解码,调用decode_frame本地函数*/
consumed_bytes= avcodec_decode_video(c, picture, &got_picture, Buf, nalLen);
cnt++;
/*输出当前的解码信息*/
printf("No:=%4d, length=%4d\n",cnt,consumed_bytes);
/*返回<0 表示解码数据头,返回>0,表示解码一帧图像*/
if(consumed_bytes > 0)
{
/*从二维空间中提取解码后的图像*/
for(i=0; i<c->height; i++)
fwrite(picture->data[0] + i * picture->linesize[0], 1, c->width, out_file);
for(i=0; i<c->height/2; i++)
fwrite(picture->data[1] + i * picture->linesize[1], 1, c->width/2, out_file);
for(i=0; i<c->height/2; i++)
fwrite(picture->data[2] + i * picture->linesize[2], 1, c->width/2, out_file);
}
}
/*关闭文件*/
if(inp_file) fclose(inp_file);
if(out_file) fclose(out_file);
/*3. 关闭CODEC,释放资源,调用decode_end本地函数*/
if(c) {
avcodec_close(c);
av_free(c);
c = NULL;
}
/*释放AVFrame空间*/
if(picture) {
av_free(picture);
picture = NULL;
}
/*释放内存*/
if(Buf) {
free(Buf);
Buf = NULL;
}
return 0;
}
上述过程中,步骤1~8为ffmpeg在编码或解码等工作时的基本步骤。分析函数名称的特点会发现,CODEC的处理函数均以“avcodec_”为前缀。为了使用H.264解码器CODEC,首先需要定义CODEC,如下所示:
AVCodec h264_decoder = {
"h264", //解码器名称
CODEC_TYPE_VIDEO, //解码器类型,视频
CODEC_ID_H264, //解码器ID
sizeof(H264Context), //解码器上下文大小
decode_init, //解码器初始化
NULL, //编码器,禁用
decode_end, //销毁解码器
decode_frame, //解码
0, //CODEC兼容性,禁用
};
因此,实际的解码处理模块采用decode_init、decode_frame、decode_end三个本地函数来实现。而main函数中使用公共的函数接口访问这里的函数。实例如注册CODEC。
/**
* simple call to register all the codecs.
*/
void avcodec_register_all(void)
{
static int inited = 0;
if (inited != 0)
return;
inited = 1;
register_avcodec(&h264_decoder);
//av_register_codec_parser(&h264_parser);
}
所以,CODEC解码器实际调用H264.c中的三个子函数来完成解码任务。
2 配置解码器
上述是注册和定义解码器,其中ffmpeg的avcodec_open模块打开CODEC,并实际调用decode_init本地函数创建、配置H.264解码器。
int avcodec_open(AVCodecContext *avctx, AVCodec *codec)
{
int ret;
if(avctx->codec) return -1; /*CODEC指针有效*/
avctx->codec = codec; /*指向codec*/
avctx->codec_id = codec->id; /*CODEC 名称*/
avctx->frame_number = 0;
#if 1
if (codec->priv_data_size > 0) {
/*CODEC的结构体不为0*/
/*为CODEC申请空间*/
avctx->priv_data = av_mallocz(codec->priv_data_size);
if (!avctx->priv_data)
return -ENOMEM;
} else {
avctx->priv_data = NULL;
}
#endif
/*CODEC的初始化*/
ret = avctx->codec->init(avctx);
/*若失败,则释放context结构体*/
if (ret < 0) {
av_freep(&avctx->priv_data);
return ret;
}
return 0;
}
上述实现解码器的初始化,包括设置默认的解码参数、图像宽度和高度、I帧预测函数的初始化等;然后是像素格式为I420的平面模式;最后为可变长度编码VLC表的初始化。
3 H.264视频解码
视频解码模块由ffmpeg的avcodec_decode_video()来完成。由于H.264码流是经过了NAL打包,所以在调用解码器之前,需要调用getNextNal()模块以从码流文件中读取分析一个NAL包,即寻找NAL的头标识0x00 00 00 01,然后将两个头标识之间的数据传给VCL解码器avcodec_decode_video()实现解码:
/**
* decode a frame.
* @param buf bitstream buffer, must be FF_INPUT_BUFFER_PADDING_SIZE larger then the actual read bytes
* because some optimized bitstream readers read 32 or 64 bit at once and could read over the end
* @param buf_size the size of the buffer in bytes
* @param got_picture_ptr zero if no frame could be decompressed, Otherwise, it is non zero
* @return -1 if error, otherwise return the number of
* bytes used.
*/
int avcodec_decode_video(AVCodecContext *avctx, AVFrame *picture,
int *got_picture_ptr,
uint8_t *buf, int buf_size)
{
int ret;
/*当前没有解码的标识*/
*got_picture_ptr= 0;
/*H.264解码*/
ret = avctx->codec->decode(avctx, //解码器上下文
picture, //解码的图像
got_picture_ptr, //图像解码标识
buf, //待解码的码流
buf_size); //码流长度
/*如果使用了MMX等指令,需要调用该函数*/
emms_c();
/*解码的是图像,不是头数据*/
if (*got_picture_ptr)
avctx->frame_number++;
return ret;
}
上述的VCL解码器根据传入的码流及长度调用ffmpeg的公共函数接口decode,即实际的解码函数decode_frame实现H.264的VCL解码。若解码过程中使用了MMX指令且退出后又要使用float/double类型,则需要使用emms指令(Empty mmx state)清空MMX状态,以恢复以前的CPU状态。实际的解码函数decode_frame实现VCL的真正解码工作,该模块的拓扑结构如图所示。
图中的decode_frame解码视频编码层VCL数据,模块核心是decode_nal_units。
static int decode_frame(AVCodecContext *avctx, /*解码器上下文*/
void *data, /*解码图像的结构*/
int *data_size, /*结构大小*/
uint8_t *buf, /*码流空间*/
int buf_size) /*码流长度*/
{
H264Context *h = avctx->priv_data; /*解码器上下文*/
MpegEncContext *s = &h->s; /*MpegEncContext指针*/
AVFrame *pict = data; /*图像空间*/
int buf_index; /*当前的码流位置*/
s->flags= avctx->flags; /*CODEC的标志*/
s->flags2= avctx->flags2; /*CODEC的标志2*/
if (buf_size == 0) { /*码流不为空*/
return 0;
}
/*截断的码流解码*/
if(s->flags&CODEC_FLAG_TRUNCATED){
int next= find_frame_end(&s->parse_context, buf, buf_size);
if( ff_combine_frame(&s->parse_context, next, &buf, &buf_size) < 0 )
return buf_size;
}
/*码流中特别数据的解码*/
if(s->avctx->extradata_size && s->picture_number==0){
if(0 < decode_nal_units(h, s->avctx->extradata, s->avctx->extradata_size) )
return -1;
}
/*正常的解码*/
buf_index=decode_nal_units(h, buf, buf_size);
if(buf_index < 0)
return -1;
#if 0 /*B帧*/
if(s->pict_type==B_TYPE || s->low_delay){
*pict= *(AVFrame*)&s->current_picture;
} else {
*pict= *(AVFrame*)&s->last_picture;
}
#endif
/*没有解码输出图像,解码头数据*/
if(!s->current_picture_ptr){
av_log(h->s.avctx, AV_LOG_DEBUG, "error, NO frame\n");
return -1;
}
/*有解码图像输出*/
*pict= *(AVFrame*)&s->current_picture;
assert(pict->data[0]);
/*表明解码输出*/
*data_size = sizeof(AVFrame);
/*返回当前消耗的码流*/
return get_consumed_bytes(s, buf_index, buf_size);
}
上述解码过程中,首先如果是截断的码流,则将解码后的码流与一帧图像组合;判断码流中是否有特殊的数据如Huffman表等;然后正常解码NAL;最后输出解码图像及消耗的码流长度。其中核心是NAL解码decode_nal_units。
4 NAL包解码
H.264码流中的基本单位表现为各个独立的NAL包,因此解码的处理函数
decode_nal_units()实现解码NAL包。该模块的拓扑结构如图所示: