iOS 使用FFmpeg 实现音视频软编码

此文中的音频编码部分存在问题,详见下一篇:
OS使用FFmpeg进行音频编码

一.背景说明

在iOS开发中,音视频采集原始数据后,一般使用系统库VideoToolboxAudioToolbox进行音视频的硬编码。而本文将使用FFmpeg框架实现音视频的软编码,音频支持acc编码,视频支持h264,h265编码。

软件编码(简称软编):使用CPU进行编码。
硬件编码(简称硬编):不使用CPU进行编码,使用显卡GPU,专用的DSP、FPGA、ASIC芯片等硬件进行编码。

优缺点:
软编:实现直接、简单,参数调整方便,升级易,但CPU负载重,性能较硬编码低,低码率下质量通常比硬编码要好一点。
硬编:性能高,低码率下通常质量低于硬编码器,但部分产品在GPU硬件平台移植了优秀的软编码算法(如X264)的,质量基本等同于软编码。

二.编码流程

编码流程图.png

三.初始化编码环境,配置编码参数。

1.初始化AVFormatContext

_pFormatCtx = avformat_alloc_context();

2.初始化音频流/视频流AVStream

_pStream = avformat_new_stream(_pFormatCtx, NULL);

3.创建编码器AVCodec

//aac编码器
_pCodec = avcodec_find_encoder(AV_CODEC_ID_AAC);
//h264编码器
_pCodec = avcodec_find_encoder(AV_CODEC_ID_H264);
av_dict_set(&param, "preset", "slow", 0);
av_dict_set(&param, "tune", "zerolatency", 0);
//h265编码器
_pCodec = avcodec_find_encoder(AV_CODEC_ID_HEVC);
av_dict_set(&param, "preset", "ultrafast", 0);
av_dict_set(&param, "tune", "zero-latency", 0);

4.初始化编码器上下文AVCodecContext,并配置参数:需要注意的是旧版是通过_pStream->codec来获取编码器上下文,新版此方法已废弃,使用avcodec_alloc_context3方法来创建,配置完参数后使用avcodec_parameters_from_context方法将参数复制到AVStream->codecpar中。

//设置acc编码器上下文参数
    _pCodecContext = avcodec_alloc_context3(_pCodec);
    _pCodecContext->codec_type = AVMEDIA_TYPE_AUDIO;
    _pCodecContext->sample_fmt = AV_SAMPLE_FMT_S16;
    _pCodecContext->sample_rate = 44100;
    _pCodecContext->channel_layout = AV_CH_LAYOUT_STEREO;
    _pCodecContext->channels = av_get_channel_layout_nb_channels(_pCodecContext->channel_layout);
    _pCodecContext->bit_rate = 64000;

//设置h264,h265编码器上下文参数
    _pCodecContext->codec_type = AVMEDIA_TYPE_VIDEO;
    _pCodecContext->width = 720;
    _pCodecContext->height = 1280;
    (省略)

5.打开编码器:

    if (avcodec_open2(_pCodecContext, _pCodec, NULL) < 0) {
        return ;
    }

6.将AVCodecContext中设置的参数复制到AVStream->codecpar

    avcodec_parameters_from_context(_audioStream->codecpar, _pCodecContext);

7.初始化AVFrameAVPacket:其中需要注意的是avpicture_get_size方法被av_image_get_buffer_size方法替代,avpicture_fill方法被av_image_fill_arrays方法替代。

//aac
    _pFrame = av_frame_alloc();
    _pFrame->nb_samples = _pCodecContext->frame_size;
    _pFrame->format = _pCodecContext->sample_fmt;
    
    int size = av_samples_get_buffer_size(NULL, _pCodecContext->channels, _pCodecContext->frame_size, _pCodecContext->sample_fmt, 1);
    uint8_t *buffer = av_malloc(size);
    avcodec_fill_audio_frame(_pFrame, _pCodecContext->channels, _pCodecContext->sample_fmt, buffer, size, 1);
    av_new_packet(&_packet, size);

//h264 h265
    _pFrame = av_frame_alloc();
    _pFrame->width = _pCodecContext->width;
    _pFrame->height = _pCodecContext->height;
   _pFrame->format =  _pCodecContext->sample_fmt;
    
    int size = av_image_get_buffer_size(_pCodecContext->pix_fmt, _pCodecContext->width, _pCodecContext->width, 1);
    uint8_t *buffer = av_malloc(size);
    av_image_fill_arrays(_pFrame->data, NULL, buffer, _pCodecContext->pix_fmt, _pCodecContext->width,  _pCodecContext->height, 1);
    av_new_packet(&_packet, size);

四.音视频编码

1.音频编码,将采集到的pcm数据存入AVFrame->data[0],然后通过avcodec_send_frameavcodec_receive_packet方法编码,从AVPacket中获取编码后数据。旧版本的avcodec_encode_audio2方法已经废弃。

- (void)encodeAudioWithSourceBuffer:(void *)sourceBuffer
                   sourceBufferSize:(UInt32)sourceBufferSize
                                pts:(int64_t)pts
{
    int ret;
    _pFrame->data[0] = sourceBuffer;
    _pFrame->pts = pts;
    ret = avcodec_send_frame(_pCodecContext, _pFrame);
    if (ret < 0) {
        return;
    }
    while (1) {
        ret = avcodec_receive_packet(_pCodecContext, &_packet);
        if (ret < 0) {
            break;
        }
        if ([self.delegate respondsToSelector:@selector(receiveAudioEncoderData:size:)]) {
            [self.delegate receiveAudioEncoderData:_packet.data size:_packet.size];
        }
         av_packet_unref(&_packet);
    }
}

2.视频编码:需要从采集到的CMSampleBufferRef中提取YUV或RGB数据,如果是YUV格式,则将YUV分量分别存入AVFrame->data[0]AVFrame->data[1]AVFrame->data[2]中;如是RGB格式,则存入AVFrame->data[0]

    CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    
    // 锁定imageBuffer内存地址开始进行编码
    if (CVPixelBufferLockBaseAddress(imageBuffer, 0) == kCVReturnSuccess) {
        // Y
        UInt8 *bufferPtr = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,0);
        // UV
        UInt8 *bufferPtr1 = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,1);
        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);
        
        // 将NV12数据转成YUV420P数据
        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;
        }
        
        // 分别读取YUV的数据
        picture_buf = yuv420_data;
        _pFrame->data[0] = picture_buf;                   // Y
        _pFrame->data[1] = picture_buf + width * height;          // U
        _pFrame->data[2] = picture_buf + width * height * 5 / 4;  // V
        
        // 设置当前帧
        _pFrame->pts = frameCount;

        int ret = avcodec_send_frame(_pCodecCtx, _pFrame);
        if (ret < 0) {
            printf("Failed to encode! \n");
            CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
            return;
        }
        
        while (1) {
          _packet.stream_index = _pStream->index;
          ret = avcodec_receive_packet(_pCodecContext, &_packet);
          if (ret < 0) {
              break;
          }
          frameCount ++;
          if ([self.delegate respondsToSelector:@selector(receiveAudioEncoderData:size:)]) {
            [self.delegate receiveAudioEncoderData:_packet.data size:_packet.size];
          }
          av_packet_unref(&_packet);
        }
        // 释放yuv数据
        free(yuv420_data);
    }
    CVPixelBufferUnlockBaseAddress(imageBuffer, 0);

五.结束编码

1.冲洗编码器:目的是将编码器上下文中的数据冲洗出来,避免造成丢帧。方法是使用avcodec_send_frame方法向编码器上下文发送NULL,如果avcodec_receive_packet方法返回值是0,则从AVPacket中取出编码后数据,如果返回值是AVERROR_EOF,则表示冲洗完成。

- (void)flushEncoder
{
    int ret;
    AVPacket packet;
    if (_pCodec->capabilities & AV_CODEC_CAP_DELAY) {
        return;
    }
    ret = avcodec_send_frame(_pCodecContext, NULL);
    if (ret < 0) {
        return;
    }
    while (1) {
        packet.data = NULL;
        packet.size = 0;
        ret = avcodec_receive_packet(_pCodecContext, &packet);
        if (ret < 0) {
            break;
        }
        if ([self.delegate respondsToSelector:@selector(receiveAudioEncoderData:size:)]) {
            [self.delegate receiveAudioEncoderData:packet.data size:packet.size];
        }
        av_packet_unref(&packet);
    }
}

2.释放内存:

    if (_pStream) {
        avcodec_close(_pCodecContext);
        av_free(_pFrame);
    }
    avformat_free_context(_pFormatCtx);

六.总结

1.FFmpeg中的编码是将采集到的pcmyuv等原始数据存入AVFrame中,然后将其发送给编码器,从AVPacket中获取编码后的数据。

FFmpeg中的解码是编码的逆过程,使用av_read_frame方法从音视频文件中获取AVPacket,然后将其发送给解码器,从AVFrame中获取解码后的pcmyuv数据。

2.以上视频的编码,获取的是Annex B格式的H264/H265码流,其中SPS,PPS,(VPS)和IDR帧等都是在AVPacket里面返回,此方式适合写入文件。
如果是推流场景,要获取SPS,PPS,(VPS)等信息,则需要设置:

_pCodecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;

这样在编码返回时,会将视频头信息放在extradata中,而不是每个关键帧前面。可以通过AVCodecContext中的extradataextradata_size获取SPS,PPS,(VPS)的数据和长度。数据也是Annex B格式,按照H264/H265的相关协议提取即可。

参考资料:
雷霄骅:Fmpeg源代码结构图 - 编码

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

推荐阅读更多精彩内容