iOS 使用FFmpeg 實現(xiàn)音視頻軟編碼

此文中的音頻編碼部分存在問題,詳見下一篇:
OS使用FFmpeg進行音頻編碼

一.背景說明

在iOS開發(fā)中,音視頻采集原始數(shù)據(jù)后,一般使用系統(tǒng)庫VideoToolboxAudioToolbox進行音視頻的硬編碼。而本文將使用FFmpeg框架實現(xiàn)音視頻的軟編碼惹挟,音頻支持acc編碼,視頻支持h264,h265編碼缝驳。

軟件編碼(簡稱軟編):使用CPU進行編碼甜害。
硬件編碼(簡稱硬編):不使用CPU進行編碼到逊,使用顯卡GPU,專用的DSP只恨、FPGA灵巧、ASIC芯片等硬件進行編碼。

優(yōu)缺點:
軟編:實現(xiàn)直接夏伊、簡單摇展,參數(shù)調(diào)整方便,升級易溺忧,但CPU負載重咏连,性能較硬編碼低,低碼率下質(zhì)量通常比硬編碼要好一點鲁森。
硬編:性能高祟滴,低碼率下通常質(zhì)量低于硬編碼器,但部分產(chǎn)品在GPU硬件平臺移植了優(yōu)秀的軟編碼算法(如X264)的歌溉,質(zhì)量基本等同于軟編碼垄懂。

二.編碼流程

編碼流程圖.png

三.初始化編碼環(huán)境,配置編碼參數(shù)。

1.初始化AVFormatContext

_pFormatCtx = avformat_alloc_context();

2.初始化音頻流/視頻流AVStream

_pStream = avformat_new_stream(_pFormatCtx, NULL);

3.創(chuàng)建編碼器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埠偿,并配置參數(shù):需要注意的是舊版是通過_pStream->codec來獲取編碼器上下文,新版此方法已廢棄榜晦,使用avcodec_alloc_context3方法來創(chuàng)建冠蒋,配置完參數(shù)后使用avcodec_parameters_from_context方法將參數(shù)復(fù)制到AVStream->codecpar中。

//設(shè)置acc編碼器上下文參數(shù)
    _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;

//設(shè)置h264,h265編碼器上下文參數(shù)
    _pCodecContext->codec_type = AVMEDIA_TYPE_VIDEO;
    _pCodecContext->width = 720;
    _pCodecContext->height = 1280;
    (省略)

5.打開編碼器:

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

6.將AVCodecContext中設(shè)置的參數(shù)復(fù)制到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數(shù)據(jù)存入AVFrame->data[0],然后通過avcodec_send_frameavcodec_receive_packet方法編碼识窿,從AVPacket中獲取編碼后數(shù)據(jù)斩郎。舊版本的avcodec_encode_audio2方法已經(jīng)廢棄。

- (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數(shù)據(jù)喻频,如果是YUV格式缩宜,則將YUV分量分別存入AVFrame->data[0]AVFrame->data[1]甥温,AVFrame->data[2]中锻煌;如是RGB格式,則存入AVFrame->data[0]姻蚓。

    CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    
    // 鎖定imageBuffer內(nèi)存地址開始進行編碼
    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數(shù)據(jù)轉(zhuǎn)成YUV420P數(shù)據(jù)
        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的數(shù)據(jù)
        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
        
        // 設(shè)置當(dāng)前幀
        _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數(shù)據(jù)
        free(yuv420_data);
    }
    CVPixelBufferUnlockBaseAddress(imageBuffer, 0);

五.結(jié)束編碼

1.沖洗編碼器:目的是將編碼器上下文中的數(shù)據(jù)沖洗出來宋梧,避免造成丟幀。方法是使用avcodec_send_frame方法向編碼器上下文發(fā)送NULL狰挡,如果avcodec_receive_packet方法返回值是0捂龄,則從AVPacket中取出編碼后數(shù)據(jù),如果返回值是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.釋放內(nèi)存:

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

六.總結(jié)

1.FFmpeg中的編碼是將采集到的pcmyuv等原始數(shù)據(jù)存入AVFrame中,然后將其發(fā)送給編碼器它匕,從AVPacket中獲取編碼后的數(shù)據(jù)刀脏。

FFmpeg中的解碼是編碼的逆過程,使用av_read_frame方法從音視頻文件中獲取AVPacket超凳,然后將其發(fā)送給解碼器愈污,從AVFrame中獲取解碼后的pcmyuv數(shù)據(jù)。

2.以上視頻的編碼轮傍,獲取的是Annex B格式的H264/H265碼流暂雹,其中SPS,PPS,(VPS)和IDR幀等都是在AVPacket里面返回,此方式適合寫入文件创夜。
如果是推流場景杭跪,要獲取SPS,PPS,(VPS)等信息,則需要設(shè)置:

_pCodecContext->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;

這樣在編碼返回時,會將視頻頭信息放在extradata中涧尿,而不是每個關(guān)鍵幀前面系奉。可以通過AVCodecContext中的extradataextradata_size獲取SPS,PPS,(VPS)的數(shù)據(jù)和長度姑廉。數(shù)據(jù)也是Annex B格式缺亮,按照H264/H265的相關(guān)協(xié)議提取即可。

參考資料:
雷霄驊:Fmpeg源代碼結(jié)構(gòu)圖 - 編碼

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末桥言,一起剝皮案震驚了整個濱河市萌踱,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌号阿,老刑警劉巖并鸵,帶你破解...
    沈念sama閱讀 222,000評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異扔涧,居然都是意外死亡园担,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評論 3 399
  • 文/潘曉璐 我一進店門枯夜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來粉铐,“玉大人,你說我怎么就攤上這事卤档◎茫” “怎么了?”我有些...
    開封第一講書人閱讀 168,561評論 0 360
  • 文/不壞的土叔 我叫張陵劝枣,是天一觀的道長汤踏。 經(jīng)常有香客問我,道長舔腾,這世上最難降的妖魔是什么溪胶? 我笑而不...
    開封第一講書人閱讀 59,782評論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮稳诚,結(jié)果婚禮上哗脖,老公的妹妹穿的比我還像新娘。我一直安慰自己扳还,他們只是感情好才避,可當(dāng)我...
    茶點故事閱讀 68,798評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著氨距,像睡著了一般桑逝。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上俏让,一...
    開封第一講書人閱讀 52,394評論 1 310
  • 那天楞遏,我揣著相機與錄音茬暇,去河邊找鬼。 笑死寡喝,一個胖子當(dāng)著我的面吹牛糙俗,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播预鬓,決...
    沈念sama閱讀 40,952評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼巧骚,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了珊皿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,852評論 0 276
  • 序言:老撾萬榮一對情侶失蹤巨税,失蹤者是張志新(化名)和其女友劉穎蟋定,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體草添,經(jīng)...
    沈念sama閱讀 46,409評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡驶兜,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,483評論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了远寸。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片抄淑。...
    茶點故事閱讀 40,615評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖驰后,靈堂內(nèi)的尸體忽然破棺而出肆资,到底是詐尸還是另有隱情,我是刑警寧澤灶芝,帶...
    沈念sama閱讀 36,303評論 5 350
  • 正文 年R本政府宣布郑原,位于F島的核電站,受9級特大地震影響夜涕,放射性物質(zhì)發(fā)生泄漏犯犁。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,979評論 3 334
  • 文/蒙蒙 一女器、第九天 我趴在偏房一處隱蔽的房頂上張望酸役。 院中可真熱鬧,春花似錦驾胆、人聲如沸涣澡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,470評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽暑塑。三九已至,卻和暖如春锅必,著一層夾襖步出監(jiān)牢的瞬間事格,已是汗流浹背惕艳。 一陣腳步聲響...
    開封第一講書人閱讀 33,571評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留驹愚,地道東北人远搪。 一個月前我還...
    沈念sama閱讀 49,041評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像逢捺,于是被迫代替她去往敵國和親谁鳍。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,630評論 2 359

推薦閱讀更多精彩內(nèi)容