功能是把h264文件变成yuv文件,测试命令行:
./video_decoding ds_480x272.h264 res.ds_480x272.yuv
我用PotPlayer来播放h264文件,yuv文件通过ffplay或者YUVplayer来播放。
这次不从main函数开始了,从一个函数,属于ffmpeg函数初始化和结束的部分。
typedef struct
{
AVCodec *pCodec; //编解码器实例指针
AVCodecContext *pCodecContext; //编解码器上下文,指定了编解码的参数
AVCodecParserContext *pCodecParserCtx; //编解码解析器,从码流中截取完整的一个NAL Unit数据
AVFrame *frame; //封装图像对象指针
AVPacket pkt; //封装码流对象实例
} CodecCtx;
这是自定义的Codec上下文,从头到尾为了方便使用。看它如何初始化的,Open_deocder,一步一步的来。
bool Open_deocder(CodecCtx &ctx)
{
avcodec_register_all();
1 注册编解码器对象
av_init_packet(&(ctx.pkt));
2 初始化AVPacket对象,保存编码前的数据,也就是yuv数据。记住pkt不是一个指针,对于它的free比较纳闷。
ctx.pCodec = avcodec_find_decoder(AV_CODEC_ID_H264);
3 根据CODEC_ID查找AVCodec对象
ctx.pCodecContext = avcodec_alloc_context3(ctx.pCodec);
if (ctx.pCodec->capabilities & AV_CODEC_CAP_TRUNCATED)
ctx.pCodecContext->flags |= AV_CODEC_FLAG_TRUNCATED; // we do not send complete frames
4 根据AVCodec对象分配AVCodecContext
ctx.pCodecParserCtx = av_parser_init(AV_CODEC_ID_H264);
3 根据CODEC_ID初始化AVCodecParserContext对象
if (avcodec_open2(ctx.pCodecContext, ctx.pCodec, NULL) < 0)
6 打开AVCodec对象
ctx.frame = av_frame_alloc();
return true;
}
7 分配AVFrame对象,保存编码后的数据。
void Close_decoder(CodecCtx &ctx)
{
avcodec_close(ctx.pCodecContext);
av_free(ctx.pCodecContext);
av_frame_free(&(ctx.frame));
}
Close_decoder属于结尾部分,通过AVCodecContext来关闭codec。之后,free掉AVFrame。
对于codec初始化,涉及到三个结构:AVCodec,AVCodecContext和AVCodecParserContext。都需要进行初始化。之后开始进行循环部分,读取数据后编码。在main函数里面。
int main(int argc, char **argv)
{
...
while(1)
{
uDataSize = fread(inbuf,1,INBUF_SIZE, inputoutput.pFin);
将码流文件按某长度读入输入缓存区,uDataSize为读取的数据长度。
pDataPtr = inbuf;
while(uDataSize > 0)
{
len = av_parser_parse2(ctx.pCodecParserCtx, ctx.pCodecContext,
&(ctx.pkt.data), &(ctx.pkt.size),
pDataPtr, uDataSize,
AV_NOPTS_VALUE, AV_NOPTS_VALUE, AV_NOPTS_VALUE);
pDataPtr += len;
uDataSize -= len;
解析缓存区中的数据为AVPacket对象。最后pDataPtr指针偏移,说明已经解析了。
int ret = avcodec_decode_video2(ctx.pCodecContext, ctx.frame, &got_picture, &(ctx.pkt));
if (got_picture)
{
write_out_yuv_frame(ctx, inputoutput);
}
}
}
...
}
根据AVCodecContext的设置,解析AVPacket中的码流,输出到AVFrame。如果got_picture为真,则已经获得一帧完整的图像,写出到输出文件。通过 write_out_yuv_frame 输出的文件中。看一看:
void write_out_yuv_frame(const CodecCtx &ctx, IOParam &in_out)
{
uint8_t **pBuf = ctx.frame->data;
int* pStride = ctx.frame->linesize;
for (int color_idx = 0; color_idx < 3; color_idx++)
{
int nWidth = color_idx == 0 ? ctx.frame->width : ctx.frame->width / 2;
int nHeight = color_idx == 0 ? ctx.frame->height : ctx.frame->height / 2;
for(int idx=0;idx < nHeight; idx++)
{
fwrite(pBuf[color_idx],1, nWidth, in_out.pFout);
pBuf[color_idx] += pStride[color_idx];
}
fflush(in_out.pFout);
}
}
这个函数过一眼就ok。
最后上几个ffmpeg的重要函数:
int av_parser_parse2(AVCodecParserContext *s,
AVCodecContext *avctx,
uint8_t **poutbuf, int *poutbuf_size,
const uint8_t *buf, int buf_size,
int64_t pts, int64_t dts,
int64_t pos);
/*AVCodecParserContext *s:初始化过的AVCodecParserContext对象,决定了码流该以怎样的标准进行解析;
AVCodecContext *avctx:预先定义好的AVCodecContext对象;
uint8_t **poutbuf:AVPacket::data的地址,保存解析完成的包数据;
int *poutbuf_size:AVPacket的实际数据长度;如果没解析出完整的一个包,这个值为0;
const uint8_t *buf, int buf_size:输入参数,缓存的地址和长度;
int64_t pts, int64_t dts:显示和解码的时间戳;
nt64_t pos :码流中的位置;
返回值为解析所使用的比特位的长度;
*/
int avcodec_decode_video2(AVCodecContext *avctx, AVFrame *picture,
int *got_picture_ptr,
const AVPacket *avpkt);
//这个函数与前篇所遇到的编码函数avcodec_encode_video2有些类似,只是参数的顺序略有不同,解码函数的输入输出参数与编码函数相比交换了位置。该函数各个参数的意义:
//AVCodecContext *avctx:编解码器上下文对象,在打开编解码器时生成;
//AVFrame *picture: 保存解码完成后的像素数据;我们只需要分配对象的空间,像素的空间codec会为我们分配好;
//int *got_picture_ptr: 标识位,如果为1,那么说明已经有一帧完整的像素帧可以输出了
//const AVPacket *avpkt: 前面解析好的码流包;
最后解决一个问题,CodecCtx.pkt是保存yuv数据的,从文件中读取得来。CodecCtx.pkt.data没有new内存,使用的还是读数据时放数据的内存地址。所以不用delete掉。