前言
原始的视频数据(YUV格式)非常大,要进行存储或者传输之前一般都需要进行压缩处理,x264支持几乎所有h264的特性而且是速度最快的商用编码器之一。
ffmpeg编码流程图
根据官网的介绍,原始视频帧首先送入输入缓冲区,此时并没有立即进行编码,输入缓冲区默认存储gopsize+一个GOP内B帧数量+4 个原始视频帧后输出缓冲区才有输出
1、编码器有一个draining状态,当原始视频帧为NULL时,编码器进入draining状态,此时输入缓冲区不再接受新的输入
2、由于有输入缓冲区的存在,所以一般是输入gopsize+一个GOP内B帧数量+4 个原始视频帧后才可以从输出缓冲区获取到压缩的视频帧
关键代码
1、准备编码器上下文相关环境
void CodecBase::initEnCodecContext(MZCodecIDType encodeId)
{
LOGD("initEnCodecContext");
enum AVCodecID cId = getCodecIdWithId(encodeId);
pCodec = avcodec_find_encoder(cId);
if (pCodec == nullptr) {
LOGD("avcodec_find_encoder fail,encodeId %d 不存在",encodeId);
return;
}
// 去掉AV_CODEC_FLAG2_FAST选项和添加AV_CODEC_FLAG_LOW_DELAY选项
// pCodec->capabilities &= ~(AV_CODEC_FLAG2_FAST);
// pCodec->capabilities |= (AV_CODEC_FLAG_LOW_DELAY);
pCodecCtx = avcodec_alloc_context3(pCodec);
if (pCodecCtx == nullptr) {
LOGD("avcodec_alloc_context3 fail");
return;
}
}
2、设置x264相关参数
/** 遇到问题:avcodec_open2()出错
* 解决方案:在avcodec_open2()之前设置编码参数
*/
AVCodecID codeid = getCodecIdWithId(fCodeIdType);
// 编码方式Id 比如h264
pCodecCtx->codec_id = codeid;
// 类型,这里为视频
pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
// 原始视频的数据类型
pCodecCtx->pix_fmt = pram.avpixelformat();
// 编码后的平均码率; 单位bit/s
pCodecCtx->bit_rate = pram.getBitrate();
// 视频编码用的时间基单位,通常值为{1,fps},此项设置必须有,用于决定编码后的帧率
pCodecCtx->time_base = av_make_q(1, pram.getFps());
// 视频宽,高
pCodecCtx->width = pram.getWidth();
pCodecCtx->height = pram.getHeight();
// GOP size
pCodecCtx->gop_size = pram.getGOPSize();
// 一组 gop中b frame 的数目
pCodecCtx->max_b_frames = pram.getBFrameNum();
if ((pCodecCtx->codec->capabilities & AV_CODEC_FLAG_LOW_DELAY)){
LOGD("ddddd");
}
// x264编码特有的参数
if (codeid == AV_CODEC_ID_H264) {
av_opt_set(pCodecCtx->priv_data, "preset", "slow", 0);
/** 遇到问题:将H264视频码流封装到MP4中后,无法播放;
* 解决方案:加入如下代码
* Some formats want stream headers to be separate
*/
pCodecCtx->flags |= AV_CODEC_FLAG2_LOCAL_HEADER;
}
- pCodecCtx->bit_rate = pram.getBitrate();代表设置编码后的平均码率,对于h264编码,分辨率对应的推荐码率参考网址:h264编码码率对应表
- pCodecCtx->time_base = av_make_q(1, pram.getFps());// 视频编码用的时间基单位,通常值为{1,fps},此项设置必须有,用于决定编码后的帧率
- pCodecCtx->gop_size = pram.getGOPSize();// GOP size,它会影响输入缓冲区的大小
- pCodecCtx->max_b_frames = pram.getBFrameNum();// 一组 gop中b frame 的数目,它会影响输入缓冲区的大小
3、开启编码器
int ret = avcodec_open2(pCodecCtx, pCodec, NULL);
if (ret < 0) {
LOGD("avcodec_open2 fail %d",ret);
fOpenedEncoder = false;
return false;
}
4、编码
void VideoH264Encoder::doEncode(AVFrame *frame)
{
/** return
* AVERROR(EAGAIN); -35 编码器接收缓冲区已经满了,得先调用avcodec_receive_packet()清空一下
* AVERROR(EINVAL); -22 编码器没有打开
* AVERROR_EOF;编码器已经处于flushed状态了,无法再接收AVFrame了
* 其它错误
* 不管avcodec_send_frame()返回什么错误,这里可以不用做处理,所有的处理放到avcodec_receive_packet这一步进行
*/
/** 遇到问题:输入的原始视频帧的个数和输出的压缩视频帧的个数不一致
* 解决方案:由于输入和输出并不是依次对应的,再输入完所有的原始视频帧后,要想获得所有压缩的编码视频数据,则
* avcodec_send_frame()第二个参数传NULL即可
*/
int ret = avcodec_send_frame(pCodecCtx, frame);
if (ret != 0) {
LOGD("avcodec_send_frame fail %d",ret);
}
while (true) {
AVPacket *pkt = av_packet_alloc();
/** return
* AVERROR(EAGAIN); -35 编码器输出缓冲区已经空了(已无编码好的数据了)
* AVERROR_EOF;编码器已经处于flushed状态了,无法再输出编码数据了
* 其它错误
*/
ret = avcodec_receive_packet(pCodecCtx, pkt);
// 释放内存
av_packet_unref(pkt);
if (ret < 0) {
LOGD("avcodec_receive_packet fail %d",ret);
return;
}
LOGD("avcodec_receive_packet sucess");
}
}
编码各个分辨率的耗时时间以及最大内存消耗
1、640x480分辨率 30fps 0.96Mbps:
iPhone6(10秒,GOP 30,无B帧,总15秒,55ms/帧;含1个B帧,总16秒 59ms/帧;GOP 60,无B帧,总15.5秒,57ms/帧)最大内存64M
iPhoneX(11秒,GOP 30,无B帧,总3.5秒 13ms/帧;含1个B帧,总3.5秒 13ms/帧;GOP 60,无B帧,总4秒,14ms/帧)最大内存112M
2、1280x720 30fps 2.56Mbps:
iPhone6(10秒,GOP 30,无B帧,总28秒,140ms/帧;含1个B帧,总42秒 209ms/帧;GOP 60,无B帧,总36秒,179ms/帧)最大内存155M
iPhoneX(10秒,GOP 30,无B帧,总7秒 26ms/帧;含1个B帧,总11秒 43ms/帧;GOP 60,无B帧,总8秒,28ms/帧)最大内存222M
3、1920x1080 30fps 5.12Mbps:
iPhone6(10秒,GOP 30,无B帧,总28秒,309ms/帧;含1个B帧,总42秒 454ms/帧;GOP 60,无B帧,总32秒,348ms/帧)
iPhoneX(12秒,GOP 30,无B帧,总16秒 60ms/帧;含1个B帧,总23秒 87ms/帧;GOP 60,无B帧,总17秒,64ms/帧)
4、1920x1080 30fps 20Mbps:
iPhoneX(10秒,GOP 30,无B帧,总17秒 201ms/帧;含1个B帧,总35秒 408ms/帧;GOP 60,无B帧,内存不够终止) 最大内存1.5G
总结:
1、相同分辨率,码率越大,帧率越小,GOP越大,B帧越多,每帧编码越耗时
2、分辨率越大,每帧编码越耗时
遇到问题
1、avcodec_open2()出错
解决方案:
在avcodec_open2()之前设置x264的编码参数,否则就会打开失败
2、输入的原始视频帧的个数和输出的压缩视频帧的个数不一致
解决方案:
由于输入和输出并不是依次对应的,再输入完所有的原始视频帧后,要想获得所有压缩的编码视频数据,则avcodec_send_frame()第二个参数传NULL即可
3、当avcodec_send_frame()第二个参数传入NULL结束编码后,内存没有及时释放;
解决方案:
这是由于avcodec_send_frame()函数内部会copy一份到编码器缓冲中,所以编码结束后要释放AVCodecContext
4、x264编码警告[libx264 @ 0x112800c00] non-strictly-monotonic PTS
解决方案:
传入编码器的AVFrame中的pts没有依次递增;依次递增就好,如下
pFrame->pts = fFramecount++;
LOGD("编码 编号 %d",fFramecount);
项目地址
项目地址
对于ffmpeg的h264编码的C++封装位于VideoH264Encoder类中,最终OC使用的封装位于SFVideoEncoder类中。
项目运行后,点击 “视频录制“,然后点击”开启摄像头“先录制一段视频,停止录制视频后会将录制的原始视频以YUV的格式保存下来,再点击”软编码“,就会编码刚刚录制的视频