背景
直播SDK一開始使用的FFmpeg 2.8版本的,現在的FFmpeg最新版已經是4.4了。播放器編輯器使用的FFmpeg都是4.0的版本;新版本FFmpeg在内部結構也做了優化,效率、穩定性相比較舊版本都提升了不少。是以直播SDK FFmpeg也要必須更新了。
簡介
直播SDK内部主要3部分使用了FFmpeg:
- 使用libavcodec 編碼Audio;
- 使用libavcodec 編碼Video;
- 使用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的作用與功能。
編碼器注冊
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 等等)有需要的可以點選加群免費領取~
編碼
編碼的過程是一個持續的循環過程。
- 從PCM隊列中擷取一幀音頻資料到pcm_buffer
- 把pcm_buffer填充到AFrame中
- 音頻編碼,擷取到編碼後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在總編碼過程中思想是不變的,隻是調用的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調用流程圖,流程圖過程中的每一步就不再詳細分析了。你隻要看懂了上面的分析,這裡是非常簡單的。
libavcodec新API編碼Video
FFmpeg中通過libavcode新API編碼Video,它的過程與libavcodec新API編碼Audio是非常相似的。我這裡隻給出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才表示所有音視訊幀編碼完成。
舉個例子:
總計有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個輸出的資料包。
音視訊編解碼器基本原理參考文檔:
https://zhuanlan.zhihu.com/p/346010443
原文 FFmpeg 新舊API編碼_ffmpeg版本差異_BetterDaZhang的部落格-CSDN部落格