iOS FFmpeg+x264 编码

本文介绍iOS下使用FFmpeg+x264进行软编码。
x264是一个开源的H.264/MPEG-4 AVC视频编码函数库,我们可以直接使用x264的API进行编码,也可以将x264编译到FFmpeg中,使用FFmpeg提供的API进行编码。


一、编译x264
1、下载gas-preprocessor文件:
gas-preprocessor
2、下载x264源码:
3、下载x264编译脚本文件:
4、将源码与脚本放在一起:
  • 新建一个文件夹,将编译脚本build-x264.sh与x264源码文件夹放入这个新建文件夹中,并将x264文件夹(x264-snapshot-xxxx)改名为"x264"


    x264源码文件与x264编译脚本
5、修改权限、执行脚本:
  • sudo chmod u+x build-x264.sh
  • sudo ./build-x264.sh
  • 编译过程中会生成scratch-x264文件夹与thin-x264文件夹
编译中
  • 编译完成最终会生成"x264-iOS"文件夹
x264-iOS
6、编译遇到的问题:
  • No working C compiler found.
    可能是xcode路径问题,终端输入命令:
    sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer/

  • Found no assembler,Minimum version is yasm-x.x.x
    Found no assembler,Minimum version is nasm-x.x.x
    Found yasm x.x.x.xxxx,Minimum version is yasm-x.x.x
    Found nasm x.x.x.xxxx,Minimum version is nasm-x.x.x
    原因是没有安装yasm/nasm或yasm/nasm版本太低,需要重新安装yasm/nasm。
    安装yasm/nasm可通过Homebrew安装,Homebrew下载地址:https://brew.sh/
    Homebrew安装yasm命令:brew install yasm
    Homebrew安装nasm命令:brew install nasm

  • 如果已经安装了yasm/nasm并且是最新版本,仍然提示上面的问题,那么可能是yasm/nasm安装的路径没有识别到,which yasmwhich nasm查看下路径,并将最新版本的yasm/nasm拷贝到此目录下,如果使用"sudo"命令也没有权限,那么需要按照下面的步骤关闭rootless:
    (1) 关机、重启进入恢复模式
    重启系统。按住Command + R进入恢复模式, 在菜单中打开Terminal
    (2) 关闭rootless
    输入:csrutil disable,重启设备
    (3) 拷贝完成,重新打开rootless

  • 如果编译i386遇到No working C compiler found
    可以直接将i386略过编译,即编译脚本中ARCHS="arm64 x86_64 i386 armv7 armv7s"将i386去掉重新编译,或终端输入./build-x264.sh arm64 x86_64 armv7 armv7s进行编译


二、编译FFmpeg+x264
1、下载FFmpeg编译脚本:
2、x264修改
  • 将build-ffmpeg.sh中的#X264=`pwd`/fat-x264注释去掉,即X264=`pwd`/fat-x264
    X264=`pwd`/fat-x264
  • 将x264编译出来的lib库文件夹放入ffmpeg编译脚本的文件夹中,并改名为"fat-x264"
fat-x264
3、编译FFmpeg
  • ./build-ffmpeg.sh
    注:如果出现i386问题,脚本中同样将ARCHS中的i386去掉
    ARCHS="arm64 armv7 x86_64"
  • 编译完成,在目录下会生成"FFmpeg-iOS"文件夹
FFmpeg-iOS

三、编码实现
1、将编译好的FFmpeg-iOSx264-iOS导入工程中
lib
2、导入系统库
3、Code
  • X264Encoder.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface X264Encoder : NSObject
@property (assign, nonatomic) CGSize videoSize;
@property (assign, nonatomic) CGFloat frameRate;
@property (assign, nonatomic) CGFloat maxKeyframeInterval;
@property (assign, nonatomic) CGFloat bitrate;
@property (strong, nonatomic) NSString *profileLevel;
+ (instancetype)defaultX264Encoder;
- (instancetype)initX264Encoder:(CGSize)videoSize
                                 frameRate:(NSUInteger)frameRate
                       maxKeyframeInterval:(CGFloat)maxKeyframeInterval
                                   bitrate:(NSUInteger)bitrate
                              profileLevel:(NSString *)profileLevel;
- (void)encoding:(CVPixelBufferRef)pixelBuffer timestamp:(CGFloat)timestamp;
- (void)teardown;
@end

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

@implementation X264Encoder
{
    AVCodecContext *pCodecCtx;
    AVCodec *pCodec;
    AVPacket packet;
    AVFrame *pFrame;
    int pictureSize;
    int frameCounter;
    int frameWidth;
    int frameHeight;
}
    
+ (instancetype)defaultX264Encoder
{
    X264Encoder *x264encoder = [[X264Encoder alloc] initX264Encoder:CGSizeMake(720, 1280) frameRate:30 maxKeyframeInterval:25 bitrate:1024*1000 profileLevel:@""];
    return x264encoder;
}

- (instancetype)initX264Encoder:(CGSize)videoSize
                      frameRate:(NSUInteger)frameRate
            maxKeyframeInterval:(CGFloat)maxKeyframeInterval
                        bitrate:(NSUInteger)bitrate
                   profileLevel:(NSString *)profileLevel
{
    self = [super init];
    if (self) {
        _videoSize = videoSize;
        _frameRate = frameRate;
        _maxKeyframeInterval = maxKeyframeInterval;
        _bitrate = bitrate;
        _profileLevel = profileLevel;
        [self setupEncoder];
    }
    return self;
}
    
- (void)setupEncoder
{
    avcodec_register_all();
    frameCounter = 0;
    frameWidth = self.videoSize.width;
    frameHeight = self.videoSize.height;
    // Param that must set
    pCodecCtx = avcodec_alloc_context3(pCodec);
    pCodecCtx->codec_id = AV_CODEC_ID_H264;
    pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
    pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
    pCodecCtx->width = frameWidth;
    pCodecCtx->height = frameHeight;
    pCodecCtx->time_base.num = 1;
    pCodecCtx->time_base.den = self.frameRate;
    pCodecCtx->bit_rate = self.bitrate;
    pCodecCtx->gop_size = self.maxKeyframeInterval;
    pCodecCtx->qmin = 10;
    pCodecCtx->qmax = 51;
    AVDictionary *param = NULL;
    if(pCodecCtx->codec_id == AV_CODEC_ID_H264) {
        av_dict_set(&param, "preset", "slow", 0);
        av_dict_set(&param, "tune", "zerolatency", 0);
    }
    pCodec = avcodec_find_encoder(pCodecCtx->codec_id);
    if (!pCodec) {
        NSLog(@"Can not find encoder!");
    }
    if (avcodec_open2(pCodecCtx, pCodec, &param) < 0) {
        NSLog(@"Failed to open encoder!");
    }
    pFrame = av_frame_alloc();
    pFrame->width = frameWidth;
    pFrame->height = frameHeight;
    pFrame->format = AV_PIX_FMT_YUV420P;
    avpicture_fill((AVPicture *)pFrame, NULL, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height);
    pictureSize = avpicture_get_size(pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height);
    av_new_packet(&packet, pictureSize);
}

- (void)encoding:(CVPixelBufferRef)pixelBuffer timestamp:(CGFloat)timestamp
{
    CVPixelBufferLockBaseAddress(pixelBuffer, 0);
    UInt8 *pY = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
    UInt8 *pUV = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);
    size_t width = CVPixelBufferGetWidth(pixelBuffer);
    size_t height = CVPixelBufferGetHeight(pixelBuffer);
    size_t pYBytes = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0);
    size_t pUVBytes = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1);
    UInt8 *pYUV420P = (UInt8 *)malloc(width * height * 3 / 2);
    UInt8 *pU = pYUV420P + (width * height);
    UInt8 *pV = pU + (width * height / 4);
    for(int i = 0; i < height; i++) {
        memcpy(pYUV420P + i * width, pY + i * pYBytes, 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 += pUVBytes;
    }
    pFrame->data[0] = pYUV420P;
    pFrame->data[1] = pFrame->data[0] + width * height;
    pFrame->data[2] = pFrame->data[1] + (width * height) / 4;
    pFrame->pts = frameCounter;
    int got_picture = 0;
    if (!pCodecCtx) {
        CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
        return;
    }
    int ret = avcodec_encode_video2(pCodecCtx, &packet, pFrame, &got_picture);
    if(ret < 0) {
        NSLog(@"Failed to encode!");
    }
    if (got_picture == 1) {
        NSLog(@"Succeed to encode frame: %5d\tsize:%5d", frameCounter, packet.size);
        frameCounter++;
        av_free_packet(&packet);
    }
    free(pYUV420P);
    CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
}

- (void)teardown
{
    avcodec_close(pCodecCtx);
    av_free(pFrame);
    pCodecCtx = NULL;
    pFrame = NULL;
}

@end
  • use
- (void)initX264Encoder
{
    dispatch_sync(encodeQueue, ^{
        self->x264encoder = [X264Encoder defaultX264Encoder];
    });
}

- (void)teardown
{
    dispatch_sync(encodeQueue, ^{
        [self->x264encoder teardown];
    });
}
    
- (void)videoWithSampleBuffer:(CMSampleBufferRef)sampleBuffer
{
    dispatch_sync(encodeQueue, ^{
        if (self->isRecording) {
            CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
            CMTime ptsTime = CMSampleBufferGetOutputPresentationTimeStamp(sampleBuffer);
            CGFloat pts = CMTimeGetSeconds(ptsTime);
            [self->x264encoder encoding:pixelBuffer timestamp:pts];
        }
    });
}

以上,则实现了iOS下使用FFmpeg+x264进行软编码的整个流程。
demo:https://github.com/XuningZhai/VideoEncode_x264

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

推荐阅读更多精彩内容