iOS音视频开发-视频软编码(FFmpeg+x264编码H.264文件)

上篇记录了利用x264编码的实现,这里记录一下FFmpeg+x264编码为H.264的实现过程,为什么使用FFmpeg+x264,因为两种框架互补组成了强大的编解码器(FFmpeg解码,x246编码)。


编译FFmpeg+x264

FFmpeg+x264编译步骤:

  • 1、将编译好的x264文件夹放置在FFmpeg脚本目录下,并将文件夹改名为fat-x264(因为脚本中定义的引用x264文件夹的名称为fat-x264);
  • 2、执行脚本文件:./build-ffmpeg.sh。
    这里需要注意的问题,之前编译并没有遇到,此次总结重新编译了一次,遇到一些问题记录一下。
    • 1)最新的FFmpeg版本为:n3.4.2,脚本中使用版本号为:n3.4。执行脚本的时候会出现诸如此类的错误:
    libavcodec/libx264.c: In function 'x264_init_static':
    libavcodec/libx264.c:892.9 error: 'x264_bit_depth' undeclared(first use in this     function) 
               if(x264_bit_depth== 8)  
    
    解决办法:修改脚本文件中的版本号:FF_VERSION="3.4->FF_VERSION="3.4.2"
    • 2)要将x264编译进FFmpeg中,需要取消脚本中对该句代码的注销:
      #X264=`pwd`/fat-x264 ->X264=`pwd`/fat-x264
    • 3)关于bitcode,现阶段在编译库文件的时候,支持bitcode还是有必要的(毕竟某个工程因为使用了此编译库文件放弃bitcode功能,向上层开发者提供了功能,是不使用是他们的事了),脚本中已经实现了支持bitcode功能,不必修改。
    • 4)将编译好的文件拖拽到工程中,需要添加的依赖库为:libiconv.dylib/libz.dylib/libbz2.dylib/CoreMedia.framework/AVFoundation.framework/VideoToolbox.framework,不然会报并未支持arm64......架构的错误。

命令行命令:

//查看是否支持相应的架构:arm64 i386....
lipo -info libxx.a
//查看是够支持bitcode  >=0
otool -l libx264.a | grep __bitcode | wc -l

介绍:不错的编译过程文章

视频捕获和编码配置代码在前面已记录,这里就不赘述了。FFmpeg+x264编码实现代码如下:

#import <Foundation/Foundation.h>
#import <CoreMedia/CoreMedia.h>
@class BBVideoConfig;
@interface BBH264SoftEncoder : NSObject
/*
 * 设置编码后文件的保存路径
 */
- (void)setFilePath:(NSString *)path;

/*
 * 初始化编码配置
 */
- (void)setupEncodeWithConfig:(BBVideoConfig *)config;

/*
 * 将CMSampleBufferRef格式的数据编码成h264并写入文件
 */
- (void)encoderToH264:(CMSampleBufferRef)sampleBuffer;

/*
 * 释放资源
 */
- (void)freeX264Resource;
@end

#import "BBH264SoftEncoder.h"
#import "BBVideoConfig.h"

#ifdef __cplusplus
extern "C" {
#endif
    
#include <libavutil/opt.h>
#include <libavutil/imgutils.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
    
#ifdef __cplusplus
};
#endif

@implementation BBH264SoftEncoder
{
    AVFormatContext                     *pFormatCtx;
    AVOutputFormat                      *fmt;
    AVStream                            *video_st;
    AVCodecContext                      *pCodecCtx;
    AVCodec                             *pCodec;
    AVPacket                             pkt;
    uint8_t                             *picture_buf;
    AVFrame                             *pFrame;
    int                                  picture_size;
    int                                  y_size;
    int                                  framecnt;
    char                                *out_file;
    
    int                                  encoder_h264_frame_width; // 编码的图像宽度
    int                                  encoder_h264_frame_height; // 编码的图像高度
}

/*
 * 设置编码后文件的文件名,保存路径
 */
- (void)setFilePath:(NSString *)path;
{
    out_file = [self nsstring2char:path];
}

/*
 * 将路径转成C语言字符串(传入路径为C字符串)
 */
- (char*)nsstring2char:(NSString *)path
{
    
    NSUInteger len = [path length];
    char *filepath = (char*)malloc(sizeof(char) * (len + 1));
    
    [path getCString:filepath maxLength:len + 1 encoding:[NSString defaultCStringEncoding]];
    
    return filepath;
}


/*
 *  设置X264
 */
- (void)setupEncodeWithConfig:(BBVideoConfig *)config
{
    // 1.默认从第0帧开始(记录当前的帧数)
    framecnt = 0;
    
    // 2.记录传入的宽度&高度
    encoder_h264_frame_width = config.videoSize.width;
    encoder_h264_frame_height = config.videoSize.height;
    
    // 3.注册FFmpeg所有编解码器(无论编码还是解码都需要该步骤)
    av_register_all();
    
    // 4.初始化AVFormatContext: 用作之后写入视频帧并编码成 h264,贯穿整个工程当中(释放资源时需要销毁)
    pFormatCtx = avformat_alloc_context();
    
    // 5.设置输出文件的路径,fmt初始化的时候根据传入的参数猜出video_codec、mime_type、extensions等等信息。
    fmt = av_guess_format(NULL, out_file, NULL);
    pFormatCtx->oformat = fmt;
    
    // 6.打开文件的缓冲区输入输出,flags 标识为  AVIO_FLAG_READ_WRITE ,可读写
    if (avio_open(&pFormatCtx->pb, out_file, AVIO_FLAG_READ_WRITE) < 0){
        printf("Failed to open output file! \n");
    }
    
    // 7.创建新的输出流, 用于写入文件
    video_st = avformat_new_stream(pFormatCtx, 0);
    
    if (video_st==NULL){ printf("Failed to setup stream! \n"); return; }
    
    // 8.pCodecCtx 用户存储编码所需的参数格式等等
    // 8.1.从媒体流中获取到编码结构体,他们是一一对应的关系,一个 AVStream 对应一个  AVCodecContext
    AVCodec *codec = avcodec_find_encoder(pFormatCtx->oformat->video_codec);
    pCodecCtx = avcodec_alloc_context3(codec);
    
    // 8.2.设置编码器的编码格式(是一个id),每一个编码器都对应着自己的 id,例如 h264 的编码 id 就是 AV_CODEC_ID_H264
    pCodecCtx->codec_id = fmt->video_codec;
    
    // 8.3.设置编码类型为 视频编码
    pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
    
    // 8.4.设置像素格式为 yuv 格式
    pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
    
    // 8.5.设置视频的宽高
    pCodecCtx->width = encoder_h264_frame_width;
    pCodecCtx->height = encoder_h264_frame_height;
    
    // 8.6.设置帧率
    pCodecCtx->time_base.num = 1;
    pCodecCtx->time_base.den = 25;
    
    // 8.7.设置码率(比特率)
    pCodecCtx->bit_rate = config.bitrate;
    
    // 8.8.视频质量度量标准(常见qmin=10, qmax=51)
    pCodecCtx->qmin = 10;
    pCodecCtx->qmax = 51;
    
    // 8.9.设置图像组层的大小(GOP-->两个I帧之间的间隔)
    pCodecCtx->gop_size = 30;
    
    // 8.10.设置 B 帧最大的数量,B帧为视频图片空间的前后预测帧, B 帧相对于 I、P 帧来说,压缩率比较大,也就是说相同码率的情况下,
    // 越多 B 帧的视频,越清晰,现在很多打视频网站的高清视频,就是采用多编码 B 帧去提高清晰度,
    // 但同时对于编解码的复杂度比较高,比较消耗性能与时间
    pCodecCtx->max_b_frames = 5;
    
    // 9.可选设置
    AVDictionary *param = 0;
    // H.264
    if(pCodecCtx->codec_id == AV_CODEC_ID_H264) {
        // 通过--preset的参数调节编码速度和质量的平衡。
        av_dict_set(&param, "preset", "slow", 0);
        
        // 通过--tune的参数值指定片子的类型,是和视觉优化的参数,或有特别的情况。
        // zerolatency: 零延迟,用在需要非常低的延迟的情况下,比如视频直播的编码
        av_dict_set(&param, "tune", "zerolatency", 0);
    }
    
    // 10.输出打印信息,内部是通过printf函数输出(不需要输出可以注释掉)
    av_dump_format(pFormatCtx, 0, out_file, 1);
    
    // 11.通过 codec_id 找到对应的编码器
    pCodec = avcodec_find_encoder(pCodecCtx->codec_id);
    if (!pCodec) {
        printf("Can not find encoder! \n");
    }
    
    // 12.打开编码器,并设置参数 param
    if (avcodec_open2(pCodecCtx, pCodec,&param) < 0) {
        printf("Failed to open encoder! \n");
    }
    
    // 13.将AVCodecContext的成员复制到AVCodecParameters结构体
    avcodec_parameters_from_context(video_st->codecpar, pCodecCtx);
    
    // 14.真实帧率
    AVRational rational = {1, 25};
    av_stream_set_r_frame_rate(video_st, rational);
    
    // 15.初始化原始数据对象: AVFrame
    pFrame = av_frame_alloc();
    
    // 16.通过像素格式(这里为 YUV)获取图片的真实大小,例如将 480 * 720 转换成 int 类型
    av_image_fill_arrays(pFrame->data, pFrame->linesize, picture_buf, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, 1);
    
    // 17.h264 封装格式的文件头部,基本上每种编码都有着自己的格式的头部。
    if (avformat_write_header(pFormatCtx, NULL) < 0) { printf("Failed to write! \n"); return; }
    
    // 18.创建编码后的数据 AVPacket 结构体来存储 AVFrame 编码后生成的数据
    av_new_packet(&pkt, picture_size);
    
    // 19.设置 yuv 数据中 y 图的宽高
    y_size = pCodecCtx->width * pCodecCtx->height;
    
}

/*
 * 将CMSampleBufferRef格式的数据编码成h264并写入文件
 *
 */
- (void)encoderToH264:(CMSampleBufferRef)sampleBuffer
{
    // 1.通过CMSampleBufferRef对象获取CVPixelBufferRef对象
    CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    
    // 2.锁定imageBuffer内存地址开始进行编码
    if (CVPixelBufferLockBaseAddress(imageBuffer, 0) == kCVReturnSuccess) {
        // 3.从CVPixelBufferRef读取YUV的值
        // NV12和NV21属于YUV格式,是一种two-plane模式,即Y和UV分为两个Plane,但是UV(CbCr)为交错存储,而不是分为三个plane
        // 3.1.获取Y分量的地址
        UInt8 *bufferPtr = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,0);
        // 3.2.获取UV分量的地址
        UInt8 *bufferPtr1 = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,1);
        
        // 3.3.根据像素获取图片的真实宽度&高度
        size_t width = CVPixelBufferGetWidth(imageBuffer);
        size_t height = CVPixelBufferGetHeight(imageBuffer);
        // 获取Y分量长度
        size_t bytesrow0 = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,0);
        size_t bytesrow1  = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,1);
        UInt8 *yuv420_data = (UInt8 *)malloc(width * height * 3 / 2);
        
        // 3.4.将NV12数据转成YUV420P(I420)数据
        UInt8 *pY = bufferPtr;
        UInt8 *pUV = bufferPtr1;
        UInt8 *pU = yuv420_data + width * height;
        UInt8 *pV = pU + width * height / 4;
        for(int i =0;i<height;i++)
        {
            memcpy(yuv420_data+i*width,pY+i*bytesrow0,width);
        }
        for(int j = 0;j<height/2;j++)
        {
            for(int i =0;i<width/2;i++)
            {
                *(pU++) = pUV[i<<1];
                *(pV++) = pUV[(i<<1) + 1];
            }
            pUV += bytesrow1;
        }
        
        // 3.5.分别读取YUV的数据
        picture_buf = yuv420_data;
        pFrame->data[0] = picture_buf;                   // Y
        pFrame->data[1] = picture_buf + y_size;          // U
        pFrame->data[2] = picture_buf + y_size * 5 / 4;  // V
        
        // 4.设置当前帧
        pFrame->pts = framecnt;
        
        // 4.设置宽度高度以及YUV格式
        pFrame->width = encoder_h264_frame_width;
        pFrame->height = encoder_h264_frame_height;
        pFrame->format = AV_PIX_FMT_YUV420P;
        
        // 5.对编码前的原始数据(AVFormat)利用编码器进行编码,将 pFrame 编码后的数据传入pkt 中
        int ret = avcodec_send_frame(pCodecCtx, pFrame);
        if (ret != 0) {
            printf("Failed to encode! \n");
            return;
        }
        
        while (avcodec_receive_packet(pCodecCtx, &pkt) == 0) {
            framecnt++;
            pkt.stream_index = video_st->index;
            //也可以使用C语言函数:fwrite()、fflush()写文件和清空文件写入缓冲区。
            ret = av_write_frame(pFormatCtx, &pkt);
            if (ret < 0) {
                printf("Failed write to file!\n");
            }
            //释放packet
            av_packet_unref(&pkt);
        }
        
        // 7.释放yuv数据
        free(yuv420_data);
    }
    
    CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
}

/*
 * 释放资源
 */
- (void)freeX264Resource
{
    
    // 1.将还未输出的AVPacket输出出来
    av_write_trailer(pFormatCtx);
    
    // 2.关闭资源
    avcodec_close(pCodecCtx);
    av_free(pFrame);
    
    avio_close(pFormatCtx->pb);
    avformat_free_context(pFormatCtx);
}
@end

编码使用FFmpeg新版API实现,网上很多都是较旧的API代码,但不影响,只是个别接口变更而已,整体的实现思路一致。
学习过程中,查阅了大量的资料,收获颇丰,非常感谢学习路上各位coder的无私分享,尤其coderWhy先森、七牛的深爱、雷霄骅(致敬)。
参考链接:
https://depthlove.github.io/2015/09/18/use-ffmpeg-and-x264-encode-iOS-camera-video-to-h264/
http://blog.csdn.net/leixiaohua1020/article/details/25430425

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

推荐阅读更多精彩内容