美文网首页音视频从入门到放弃ios developers
iOS 使用FFmpeg 实现音视频软编码

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

作者: 陆离o | 来源:发表于2020-03-01 17:07 被阅读0次

    此文中的音频编码部分存在问题,详见下一篇:
    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源代码结构图 - 编码

    相关文章

      网友评论

        本文标题:iOS 使用FFmpeg 实现音视频软编码

        本文链接:https://www.haomeiwen.com/subject/hnyxkhtx.html