天天看點

FFmpeg 新舊API編碼

背景

直播SDK一開始使用的FFmpeg 2.8版本的,現在的FFmpeg最新版已經是4.4了。播放器編輯器使用的FFmpeg都是4.0的版本;新版本FFmpeg在内部結構也做了優化,效率、穩定性相比較舊版本都提升了不少。是以直播SDK FFmpeg也要必須更新了。

簡介

直播SDK内部主要3部分使用了FFmpeg:

  1. 使用libavcodec 編碼Audio;
  2. 使用libavcodec 編碼Video;
  3. 使用libavformat 合成/推流;

我會先講解使用舊AP Ilibavcodec編碼Audio、Video的過程。然後再講解使用新API編碼Audio、Video的過程,通過前後的對比就可以很容易的知道直播SDK FFmpeg更新點在哪裡?這裡隻是通過分析新舊API的使用來對比出我們直播SDK的更新點,同時也可以學習如何通過FFmpeg新舊API編碼Audio、Video、Muxer。因為篇幅有限,這裡并不會具體去講解每個API内部的源碼邏輯,感興趣可以自行了解。

libavcodec舊API編碼Audio

文本闡述感覺是比較枯燥的,這裡先上圖了解下通過FFmpeg libavcodec子產品舊API如何編碼Audio。後面在具體介紹API的作用與功能。

FFmpeg 新舊API編碼

編碼器注冊

av_register_all()也可以使用avcodec_register_all()代替。檢視源碼可以發現av_register_all()内部調用了avcodec_register_all()。它的作用更就是注冊所有的編解碼器。

查找編碼器

編碼器注冊好了之後就可以通過avcodec_find_encoder_by_name()、avcodec_find_encoder()擷取我們想要的編碼器。例如:通過avcodec_find_encoder(AV_CODEC_ID_AAC), 如果我們想使用libfdk-aac編碼器編碼音頻必須在FFmpeg交叉編譯的時候連結進去。否則我們在擷取編碼器的時候會使用FFmpeg内部預設的AAC編碼器。

建立 AVCodecContext

當編碼器建立好了之後,就需要根據編碼器建立AVCodecContext,并初始化編碼參數:采樣裡、聲道數、采樣格式等。

AVCodecContext *avCodecContext = avcodec_alloc_context3(codec);
avCodecContext->codec_type = AVMEDIA_TYPE_AUDIO;
avCodecContext->sample_rate = 44100;
avCodecContext->bit_rate = 64000;
avCodecContext->sample_fmt = AV_SAMPLE_FMT_S16;
avCodecContext->channel_layout = AV_CH_LAYOUT_STEREO;
avCodecContext->channels = av_get_channel_layout_nb_channels(avCodecContext->channel_layout);
avCodecContext->profile = FF_PROFILE_AAC_LOW;
avCodecContext->flags |= CODEC_FLAG_GLOBAL_HEADER;
avCodecContext->codec_id = codec->id;
           

打開編碼器

編碼器的參數設定好了之後,就可以打開編碼器了。

if (avcodec_open2(avCodecContext, codec, NULL) < 0) {
   return -1;
}
           

建立AVFrame,并申請一塊PCM記憶體

在FFmpeg 中編碼前、解碼後的資料用AVFrame表示;編碼後,解碼前的資料使用AVPacket表示;

這裡需要為我們即将編碼的資料建立一個AVFrame,并為它建立一個存放資料的空間。

// 建立AVFrame
AVFrame *encode_frame = av_frame_alloc();
encode_frame->nb_samples = avCodecContext->frame_size;
encode_frame->format = avCodecContext->sample_fmt;
encode_frame->channel_layout = avCodecContext->channel_layout;
encode_frame->sample_rate = avCodecContext->sample_rate;

// 申請一塊PCM記憶體
int ret = av_samples_alloc_array_and_samples(&pcm_buffer, &src_samples_linesize, avCodecContext->channels, audio_nb_samples, avCodecContext->sample_fmt, 0);
if (ret < 0) {
    return -1;
}
           

相關學習資料推薦,點選下方連結免費報名,先碼住不迷路~】

【免費分享】音視訊學習資料包、大廠面試題、技術視訊和學習路線圖,資料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以點選加群免費領取~

FFmpeg 新舊API編碼

編碼

編碼的過程是一個持續的循環過程。

  1. 從PCM隊列中擷取一幀音頻資料到pcm_buffer
  2. 把pcm_buffer填充到AFrame中
  3. 音頻編碼,擷取到編碼後AVPacket資料
// 從PCM隊列中擷取一幀音頻資料到pcm_buffer
pcm_frame_callback(pcm_buffer);

int ret;
int got_packet;
AVPacket *pkt = av_packet_alloc();
pkt->duration = (int) AV_NOPTS_VALUE;
pkt->pts = pkt->dts = 0;
  
// 把pcm_buffer填充到AFrame中
avcodec_fill_audio_frame(encode_frame, avCodecContext->channels, avCodecContext->sample_fmt, pcm_buffer[0], audioSamplesSize, 0);

// 音頻編碼,擷取到編碼後AVPacket資料
ret = avcodec_encode_audio2(avCodecContext, pkt, encode_frame, &got_packet);
if (ret < 0 || !got_packet) {
    av_packet_free(&pkt);
    return ret;
}

// write、enqueue
           

銷毀

if (NULL != pcm_buffer) {
    av_free(pcm_buffer);
}
if (NULL != encode_frame) {
    av_frame_free(&encode_frame);
}
if (NULL != avCodecContext) {
   avcodec_close(avCodecContext);
   av_free(avCodecContext);
}
           

libavcodec 新API編碼Audio

這裡先上圖了解下通過FFmpeg libavcodec子產品新API如何編碼Audio。後面在具體介紹核心API的作用與功能。

FFmpeg 新舊API編碼

通過上圖可以知道,FFmpeg新API在總編碼過程中思想是不變的,隻是調用的API改變了。這裡重點介紹新API的使用,與舊API相同的部分就不再講解。

建立AVFrame,并申請一塊PCM記憶體

舊API建立方式:先申請一塊記憶體,待填充好了pcm資料之後,再把該記憶體挂載到AVFrame上。

新API直接可以通過av_frame_get_buffer()為AVFrame建立好了記憶體。不過在調用av_frame_get_buffer()之前必須為AVFrame設定好采樣率、聲道數、采樣格式、采樣大小。

// 建立AVFrame
AVFrame *encode_frame = av_frame_alloc();
encode_frame->nb_samples = avCodecContext->frame_size;
encode_frame->format = avCodecContext->sample_fmt;
encode_frame->channel_layout = avCodecContext->channel_layout;
encode_frame->sample_rate = avCodecContext->sample_rate;

// 申請一塊PCM記憶體
int ret = av_frame_get_buffer(encode_frame, 0);
if (ret < 0) {
    return -1;
}
           

編碼

FFmpeg新API編碼通過avcodec_send_frame()、avcodec_receive_packet()實作。他們内部實作原理可以參考文章底部的介紹。

AVPacket pkt = { 0 };
    av_init_packet(&pkt);
    pkt.duration = (int) AV_NOPTS_VALUE;
    pkt.pts = pkt.dts = 0;
   
    while (true){
        do{
            ret = avcodec_receive_packet(avCodecContext, &pkt);
            // ret >= 0 擷取編碼後的視訊流
            if(ret >= 0){
                
                av_free_packet(&pkt);
                return ret;
            }
            //
            if (ret == AVERROR(EAGAIN)) {
                // 跳出該循環。
                break;
            }
            // 編碼出錯
            if (ret < 0) {
                av_free_packet(&pkt);
                return ret;
            }
        }while (true);

        // 擷取pcm資料
        pcm_frame_callback(encode_frame->data);
        ret = avcodec_send_frame(avCodecContext, encode_frame);
        if(ret >= 0){
//            LOGI("avcodec_send_frame success");
        }else{
            LOGI("avcodec_send_frame error: %s\n", av_err2str(ret));
        }
        av_packet_unref(&pkt);
    }
           

銷毀

if (NULL != encode_frame) {
    av_frame_free(&encode_frame);
}
if (NULL != avCodecContext) {
   avcodec_free_context(avCodecContext);
}
           

libavcodec 舊API編碼Video

FFmpeg中通過libavcodec舊API編碼Video,它的過程與libavcodec舊API編碼Audio是非常相似的。我這裡隻給出API調用流程圖,流程圖過程中的每一步就不再詳細分析了。你隻要看懂了上面的分析,這裡是非常簡單的。

FFmpeg 新舊API編碼

libavcodec新API編碼Video

FFmpeg中通過libavcode新API編碼Video,它的過程與libavcodec新API編碼Audio是非常相似的。我這裡隻給出API調用流程圖,流程圖過程中的每一步就不再詳細分析了。你隻要看懂了上面的分析,這裡是非常簡單的。

FFmpeg 新舊API編碼

音視訊編解碼器基本原理

在FFMPEG中 avcodec_send_frame() 和 avcodec_receive_packet() 通常是同時使用的,先調用 avcodec_send_frame() 送入要編碼的音視訊幀,然後調用 avcodec_receive_packet()擷取編碼後的資料包。但是需要注意的是:編碼器内部是有緩沖區資料處理的,是以并不保證每送入一個音視訊幀,就一定有相應的編碼資料包輸出,這兩個函數對于資料處理,在時序上并不同步,這一點特别需要注意。

通常解碼開始,通過avcodec_send_frame()送入幾十個音視訊幀,對應的avcodec_receive_packet()都沒有資料包輸出。等送入的幀足夠多後,avcodec_receive_packet()才開始輸出前面一開始送入進行編碼的資料包。最後幾十沒有資料幀送入了,也要調用avcodec_send_frame()送入空幀,以驅動編碼子產品繼續編碼緩沖區中的資料,此時avcodec_receive_packet()還是會有資料包輸出,直到傳回AVERROR_EOF才表示所有音視訊幀編碼完成。

FFmpeg 新舊API編碼

舉個例子:

總計有100個視訊幀要送入編碼器編碼,最終輸出視訊幀也是100個資料包輸出。

前面20次調用 avcodec_send_frame()可以不斷的送入第1~20個視訊幀,但是前面20次調用avcodec_receive_packet()函數總是傳回AVERROR(EAGAIN),沒有資料包輸出。

從第21次調用avcodec_send_frame()送入第21個視訊幀開始,這次再調用avcodec_receive_packet()函數可以傳回0,并且有資料包輸出,但是輸出的資料包pts是0(也即第一個資料包對應的視訊幀),之後avcodec_send_frame()不斷送入第22、23…個視訊幀,avcodec_receive_packet()不斷輸出第1、2…個資料

最後第100個資料包通過avcodec_send_frame()送入完成了,但是此時avcodec_receive_packet()才擷取到第82幀輸出資料包,此時需要繼續不斷調用avcodec_send_frame()送入空幀,同時不斷調用avcodec_receive_packet()擷取輸出資料包,直到傳回AVERROR_EOF,可以擷取到最後第100個輸出的資料包。

FFmpeg 新舊API編碼

音視訊編解碼器基本原理參考文檔:

https://zhuanlan.zhihu.com/p/346010443

原文 FFmpeg 新舊API編碼_ffmpeg版本差異_BetterDaZhang的部落格-CSDN部落格