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的格式保存下来,再点击”软编码“,就会编码刚刚录制的视频

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352