前言
上一章,我们了解ffmpeg的解封装,解码过程,这一章我们来了解一下ffmpeg是怎样进行编码,和封装工作的,工作流程如下图所示:
音视频为什么需要编码?
音视频编码主要是为了做压缩,如果音视频不经过压缩编码的话,体积通常是非常大的,一部电影可能就要上百G的空间。
视频编码的主要作用是将视频像素数据(RGB,YUV等)压缩成为视频码流,音频编码的主要作用是将音频采样数据(PCM等)压缩成为音频码流。所以是对原始数据的加工,是对输入源进行处理,然后输出的过程。简单说,就是对图像和声音的压缩方法。
视频编码主要有:H263、H264、H265、MPEG系列等。
FFmpeg 音视频编码流程
FFmpeg音视频编码
通过上文,我们知道每一份音视频数据在被封装文件前主要经过了两个关键步骤,分别是编码和封装。而在ffmpeg中,使用相关接口实现编码和封装流程如下图:
由上图可知,我们需要重点关注下面这些FFmpeg的API接口:
1、首先使用av_register_all()第一个执行函数注册所有的编码器和复用器。
2、avformat_alloc_output_context2():初始化包含有输出码流(AVStream)和解复用器(AVInputFormat)的AVFormatContext
3、avio_open( )打开输出文件
4、av_new_stream() 创建视频码流 。
5、av_fine_encoder()和av_open2()查找并打开编码器,根据前一步设置的编码器参数信息,来查找初始化一个编码其,并将其打开。
6、写头文件 avformat_write_header()。这一步主要是将封装格式的信息写入文件头部位置。
7、编码帧。函数 avcodec_encode_video2() 将AVFrame编码为AVPacket
8、av_write_frame()编码帧写入文件
9、flush_encoder():输入的像素数据读取完成后调用此函数。用于输出编码器中剩余的AVPacket。
10、av_write_trailer():写文件尾。
FFmpeg编码接口使用
1.这步必须放在所有ffmpeg代码前第一个执行在使用FFmpeg解码媒体文件之前,首先需要注册了容器和编解码器有关的组件
代码语言:javascript复制//注册所有ffmpeg组件
avcodec_register_all();
av_register_all();
2、avformat_alloc_output_context2():初始化包含有输出码流(AVStream)和解复用器(AVInputFormat)的AVFormatContext
代码语言:javascript复制//-初始化AVFormatContext结构体,根据文件名获取到合适的封装格式
avformat_alloc_output_context2(&pFormatCtx, NULL, NULL, out_file);
fmt = pFormatCtx->oformat;
3.avio_open( )打开输出文件
代码语言:javascript复制 //打开文件
if (avio_open(&pFormatCtx->pb, out_file, AVIO_FLAG_READ_WRITE)) {
cout << "output file open fail!";
return -1;
}
4.av_new_stream() 创建视频码流 该函数生成一个空AVstream 该结构存放编码后的视频码流 。视频码流被拆分为AVPacket新式保存在AVStream中。
代码语言:javascript复制 //[-初始化视频码流
video_st = avformat_new_stream(pFormatCtx, 0);
if (video_st == NULL)
{
printf("failed allocating output stramn");
return -1;
}
video_st->time_base.num = 1;
video_st->time_base.den = 25;
5.设置编码器信息,该步骤主要是为AVCodecContext(从AVStream->codec 获取指针)结构体设置一些参数,包括codec_id、codec_type、width、height、pix_fmt ..... 根据编码器的不同,还要额外设置一些参数(如 h264 要设置qmax、qmin、qcompress参数才能正常使用h264编码)
代码语言:javascript复制// --编码器Context设置参数
pCodecCtx = video_st->codec;
pCodecCtx->codec_id = fmt->video_codec;
pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
pCodecCtx->width = in_w;
pCodecCtx->height = in_h;
pCodecCtx->time_base.num = 1;
pCodecCtx->time_base.den = 25;
pCodecCtx->bit_rate = 400000;
pCodecCtx->gop_size = 12;
if (pCodecCtx->codec_id == AV_CODEC_ID_H264)
{
pCodecCtx->qmin = 10;
pCodecCtx->qmax = 51;
pCodecCtx->qcompress = 0.6;
}
if (pCodecCtx->codec_id == AV_CODEC_ID_MPEG2VIDEO)
pCodecCtx->max_b_frames = 2;
if (pCodecCtx->codec_id == AV_CODEC_ID_MPEG1VIDEO)
pCodecCtx->mb_decision = 2;
6.查找并打开编码器,根据前一步设置的编码器参数信息,来查找初始化一个编码其,并将其打开。用到函数为av_fine_encoder()和av_open2()。
代码语言:javascript复制 //寻找编码器并打开编码器
pCodec = avcodec_find_encoder(pCodecCtx->codec_id);
if (!pCodec)
{
cout << "no right encoder!" << endl;
return -1;
}
if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0)
{
cout << "open encoder fail!" << endl;
return -1;
}
//[6]
7.写头文件 avformat_write_header()。这一步主要是将封装格式的信息写入文件头部位置。
代码语言:javascript复制 //写头文件
avformat_write_header(pFormatCtx, NULL);
8.用函数 avcodec_encode_video2() 将AVFrame编码为AVPacket
代码语言:javascript复制//[8] --循环编码每一帧
for (int i = 0; i < framenum; i )
{
//读入YUV
if (fread(picture_buf, 1, y_size * 3 / 2, in_file) < 0)
{
cout << "read file fail!" << endl;
return -1;
}
else if (feof(in_file))
break;
picture->data[0] = picture_buf; //亮度Y
picture->data[1] = picture_buf y_size; //U
picture->data[2] = picture_buf y_size * 5 / 4; //V
//AVFrame PTS
picture->pts = i;
int got_picture = 0;
//编码
int ret = avcodec_encode_video2(pCodecCtx, &pkt, picture, &got_picture);
if (ret < 0)
{
cout << "encoder fail!" << endl;
return -1;
}
if (got_picture == 1)
{
cout << "encoder success!" << endl;
// parpare packet for muxing
pkt.stream_index = video_st->index;
av_packet_rescale_ts(&pkt, pCodecCtx->time_base, video_st->time_base);
pkt.pos = -1;
ret = av_interleaved_write_frame(pFormatCtx, &pkt);
av_free_packet(&pkt);
}
}
9.编码帧写入文件 av_write_frame()
代码语言:javascript复制 if (got_picture == 1)
{
cout << "encoder success!" << endl;
// parpare packet for muxing
pkt.stream_index = video_st->index;
av_packet_rescale_ts(&pkt, pCodecCtx->time_base, video_st->time_base);
pkt.pos = -1;
ret = av_interleaved_write_frame(pFormatCtx, &pkt);
av_free_packet(&pkt);
}
10.在flush_encoder()输入的像素数据读取完成后调用此函数。用于输出编码器中剩余的AVPacket。
代码语言:javascript复制//Flush encoder
int ret = flush_encoder(pFormatCtx, 0);
if (ret < 0)
{
cout << "flushing encoder failed!" << endl;
goto end;
}
11.在av_write_trailer():写文件尾。
代码语言:javascript复制//写文件尾
av_write_trailer(pFormatCtx);
小结:
至此,我们了解在Android中使用ffmpeg对音视频文件进行编码的具体过程,有兴趣的读者建议通过实践加深对使用ffmpeg进行编码的体会,把抽象的内容具体化,实例化。