x264编码YUV420P为H264格式ffmpeg(七)

前言

原始的视频数据(YUV格式)非常大,要进行存储或者传输之前一般都需要进行压缩处理,x264支持几乎所有h264的特性而且是速度最快的商用编码器之一。

ffmpeg编码流程图

image.png

根据官网的介绍,原始视频帧首先送入输入缓冲区,此时并没有立即进行编码,输入缓冲区默认存储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的格式保存下来,再点击”软编码“,就会编码刚刚录制的视频

你可能感兴趣的:(x264编码YUV420P为H264格式ffmpeg(七))