FFMpeg SDK使用3调用FFmpeg SDK实现视频编码
Posted 叮咚咕噜
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了FFMpeg SDK使用3调用FFmpeg SDK实现视频编码相关的知识,希望对你有一定的参考价值。
前言
- 视频解码是视频处理的一项基本操作之一,是播放、分析内容等后续工作的基础
- 视频编码是编码的逆过程,将视频由压缩域的码流解码为像素域的图像信号
- 视频解码的实际实现由针对不同编码格式的解码器实现,每一种解码器可以针对某一种特定标准格式的视频进行解码,并支持符合该格式的所欲配置的码流
- FFMPEG视频解码的主要步骤
- 解析输入参数——获取待解码的码流数据
- 初始化相应的FFMPEG结构
- 循环读取并解析输入码流数据——由2进制码流解析为FFMPEG可以处理的包数据(注意是整包数据)
- 解码解析出的包为像素数据
- 写出像素数据
- 收尾工作
一、FFMpeg视频解码器所包含的结构
同FFMpeg编码器类似,FFMpeg解码器也需要编码时的各种结构,除此之外,解码器还需要另一个结构——编解码解析器——用于从码流中截取出一帧完整的码流数据单元。因此我们定义一个编解码上下文结构为:
/*************************************************
Struct: CodecCtx
Description: FFMpeg编解码器上下文
*************************************************/
typedef struct
{
AVCodec *pCodec; //编解码器实例指针
AVCodecContext *pCodecContext; //编解码器上下文,指定了编解码的参数
AVCodecParserContext *pCodecParserCtx; //编解码解析器,从码流中截取完整的一个NAL Unit数据
AVFrame *frame; //封装图像对象指针
AVPacket pkt; //封装码流对象实例
} CodecCtx;
二、FFMpeg进行解码操作的主要步骤
2.1参数传递和解析
同编码器类似,解码器也需要传递参数。不过相比编码器,解码器在运行时所需要的大部分信息都包含在输入码流中,因此输入参数一般只需要指定一个待解码的视频码流文件即可
2.2按照要求初始化需要的FFMpeg结构
首先,所有涉及到编解码的的功能,都必须要注册音视频编解码器之后才能使用。注册编解码调用下面的函数:
avcodec_register_all();
//初始化AVPacket对象,保存待解码的码流数据
av_init_packet(&(ctx.pkt));
编解码器注册完成之后,根据指定的CODEC_ID查找指定的codec实例。CODEC_ID通常指定了编解码器的格式,在这里我们使用当前应用最为广泛的H.264格式为例。查找codec调用的函数为avcodec_find_encoder,其声明格式为:
AVCodec *avcodec_find_decoder(enum AVCodecID id);
该函数的输入参数为一个AVCodecID的枚举类型,返回值为一个指向AVCodec结构的指针,用于接收找到的编解码器实例。如果没有找到,那么该函数会返回一个空指针。调用方法如下:
/* find the mpeg1 video encoder */
ctx.pCodec = avcodec_find_decoder(AV_CODEC_ID_H264); //根据CODEC_ID查找编解码器对象实例的指针
if (!ctx.codec)
{
fprintf(stderr, "Codec not found\\n");
return false;
}
AVCodec查找成功后,下一步是分配AVCodecContext实例。分配AVCodecContext实例需要我们前面查找到的AVCodec作为参数,调用的是avcodec_alloc_context3函数。其声明方式为:
AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);
其特点同avcodec_find_encoder类似,返回一个指向AVCodecContext实例的指针。如果分配失败,会返回一个空指针。调用方式为:
ctx.c = avcodec_alloc_context3(ctx.codec); //分配AVCodecContext实例
if (!ctx.c)
{
fprintf(stderr, "Could not allocate video codec context\\n");
return false;
}
我们应该记得,在FFMpeg视频编码的实现中,AVCodecContext对象分配完成后,下一步实在该对象中设置编码的参数。而在解码器的实现中,基本不需要额外设置参数信息,因此这个对象更多地作为输出参数接收数据。因此对象分配完成后,不需要进一步的初始化操作。
解码器与编码器实现中不同的一点在于,解码器的实现中需要额外的一个AVCodecParserContext结构,用于从码流中截取一个完整的NAL单元。因此我们需要分配一个AVCodecParserContext类型的对象,使用函数av_parser_init,声明为:
AVCodecParserContext *av_parser_init(int codec_id);
调用方式为:
ctx.pCodecParserCtx = av_parser_init(AV_CODEC_ID_H264);
if (!ctx.pCodecParserCtx)
{
printf("Could not allocate video parser context\\n");
return false;
}
随后,打开AVCodec对象,然后分配AVFrame对象:
//打开AVCodec对象
if (avcodec_open2(ctx.pCodecContext, ctx.pCodec, NULL) < 0)
{
fprintf(stderr, "Could not open codec\\n");
return false;
}
//分配AVFrame对象
ctx.frame = av_frame_alloc();
if (!ctx.frame)
{
fprintf(stderr, "Could not allocate video frame\\n");
return false;
}
2.3解码循环体
完成必须的codec组件的建立和初始化之后,开始进入正式的解码循环过程。解码循环通常按照以下几个步骤实现:
首先按照某个指定的长度读取一段码流保存到缓存区中。
由于H.264中一个包的长度是不定的,我们读取一段固定长度的码流通常不可能刚好读出一个包的长度。所以我们就需要使用AVCodecParserContext结构对我们读出的码流信息进行解析,直到取出一个完整的H.264包。对码流解析的函数为av_parser_parse2,声明方式如:
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 :码流中的位置;
- 返回值为解析所使用的比特位的长度;并不是解析到一包就返回,仅仅是返回已经解析了的数据
具体的调用方式为:
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);
如果参数poutbuf_size的值为0,那么应继续解析缓存中剩余的码流;如果缓存中的数据全部解析后依然未能找到一个完整的包,那么继续从输入文件中读取数据到缓存,继续解析操作,直到pkt.size不为0为止。
在最终解析出一个完整的包之后,我们就可以调用解码API进行实际的解码过程了。解码过程调用的函数为avcodec_decode_video2,该函数的声明为:
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: 前面解析好的码流包;
实际调用的方法为:
int ret = avcodec_decode_video2(ctx.pCodecContext, ctx.frame, &got_picture, &(ctx.pkt));
if (ret < 0)
{
printf("Decode Error.\\n");
return ret;
}
if (got_picture)
{
//获得一帧完整的图像,写出到输出文件
write_out_yuv_frame(ctx, inputoutput);
printf("Succeed to decode 1 frame!\\n");
}
最后,同编码器一样,解码过程的最后一帧可能也存在延迟。处理最后这一帧的方法也跟解码器类似:将AVPacket::data设为NULL,AVPacket::size设为0,然后在调用avcodec_encode_video2完成最后的解码过程:
ctx.pkt.data = NULL;
ctx.pkt.size = 0;
while(1)
{
//将编码器中剩余的数据继续输出完
int ret = avcodec_decode_video2(ctx.pCodecContext, ctx.frame, &got_picture, &(ctx.pkt));
if (ret < 0)
{
printf("Decode Error.\\n");
return ret;
}
if (got_picture)
{
write_out_yuv_frame(ctx, inputoutput);
printf("Flush Decoder: Succeed to decode 1 frame!\\n");
}
else
{
break;
}
} //while(1)
一定要注意,写文件是按照跨距写,而不是宽
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);
}
}
有可能循环完成了,但是还没有完全输出解码完之后的数据,将编码码流设为空包,则解码器就会输出对应的剩余的解码完之后的数据
ctx.pkt.data = NULL;
ctx.pkt.size = 0; //解码空包,就会输出剩余的码流数据
while(1)
{
//将编码器中剩余的数据继续输出完
int ret = avcodec_decode_video2(ctx.pCodecContext, ctx.frame, &got_picture, &(ctx.pkt));
if (ret < 0)
{
printf("Decode Error.\\n");
return ret;
}
if (got_picture)
{
write_out_yuv_frame(ctx, inputoutput);
printf("Flush Decoder: Succeed to decode 1 frame!\\n");
}
else
{
break;
}
}
2.4收尾工作
收尾工作主要包括关闭输入输出文件、关闭FFMpeg解码器各个组件。其中关闭解码器组件需要:
avcodec_close(ctx.pCodecContext);
av_free(ctx.pCodecContext);
av_frame_free(&(ctx.frame));
三、总结
解码器的流程与编码器类似,只是中间需要加入一个解析的过程。整个流程大致为:
1.读取码流数据 -> 2.解析数据,是否尚未解析出一个包就已经用完?是返回1,否继续 -> 3.解析出一个包?是则继续,否则返回上一步继续解析 -> 4.调用avcodec_decode_video2进行解码 -> 5.是否解码出一帧完整的图像?是则继续,否则返回上一步继续解码 -> 6.写出图像数据 -> 返回步骤2继续解析。
以上是关于FFMpeg SDK使用3调用FFmpeg SDK实现视频编码的主要内容,如果未能解决你的问题,请参考以下文章
FFMpeg SDK使用8调用FFmpeg SDK实现视频缩放
FFMpeg SDK使用7调用FFmpeg SDK实现视频水印
FFMpeg视频开发与应用基础五调用FFMpeg SDK封装音频和视频为视频文件
FFMpeg SDK使用6调用FFmpeg SDK实现视频文件的转封装