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(¶m, "preset", "slow", 0);
av_dict_set(¶m, "tune", "zerolatency", 0);
//h265编码器
_pCodec = avcodec_find_encoder(AV_CODEC_ID_HEVC);
av_dict_set(¶m, "preset", "ultrafast", 0);
av_dict_set(¶m, "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;idata[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源代码结构图 - 编码

你可能感兴趣的:(iOS 使用FFmpeg 实现音视频软编码)