【Android 音視頻開發(fā)打怪升級:FFmpeg音視頻編解碼篇】七、Andro...

聲 明
首先,這一系列文章均基于自己的理解和實踐,可能有不對的地方,歡迎大家指正。
其次,這是一個入門系列,涉及的知識也僅限于夠用,深入的知識網(wǎng)上也有許許多多的博文供大家學習了。
最后,寫文章過程中,會借鑒參考其他人分享的文章,會在文章最后列出,感謝這些作者的分享。
碼字不易,轉(zhuǎn)載請注明出處!
目錄
一、Android音視頻硬解碼篇:
二、使用OpenGL渲染視頻畫面篇
- 1,初步了解OpenGL ES
- 2,使用OpenGL渲染視頻畫面
- 3,OpenGL渲染多視頻,實現(xiàn)畫中畫
- 4,深入了解OpenGL之EGL
- 5,OpenGL FBO數(shù)據(jù)緩沖區(qū)
- 6,Android音視頻硬編碼:生成一個MP4
三、Android FFmpeg音視頻解碼篇
- 1,F(xiàn)Fmpeg so庫編譯
- 2,Android 引入FFmpeg
- 3,Android FFmpeg視頻解碼播放
- 4,Android FFmpeg+OpenSL ES音頻解碼播放
- 5,Android FFmpeg+OpenGL ES播放視頻
- 6,Android FFmpeg簡單合成MP4:視屏解封與重新封裝
- 7,Android FFmpeg視頻編碼
本文你可以了解到
如何使用 FFmepg 對編輯好的視頻進行重新編碼,生成可以播放的音視頻文件。
寫在前面
本文是音視頻系列文章的最后一篇了,也是拖了最久的一篇(懶癌發(fā)作-_-!!),終于下定決心,把坑填完。話不多說了,馬上進入正文。
在【上一篇文章】中,介紹了如何對音視頻文件進行解封和重新封裝,這個過程不涉及音視頻的解碼和編碼,也就是沒有對音視頻進行編輯,這無法滿足日常的開發(fā)需求。
因此,本文將填上編輯過程的空缺,為本系列畫上句號。
一、整體流程說明
在前面的幾篇文章中,我們已經(jīng)做好了 解碼器, OpenGL 渲染器,因此,編碼的時候,除了需要 編碼器 外,還需要將之前的內(nèi)容做好整合。下面通過一張圖做一下簡要說明:

模塊
首先可以關(guān)注到,這個過程有三個大模塊,也是三個 獨立又互相關(guān)聯(lián) 的線程,分別負責:
- 原視頻解碼
- OpenGL 畫面渲染
- 目標視頻編碼
數(shù)據(jù)流向
看下視頻數(shù)據(jù)是如何流轉(zhuǎn)的:
原視頻經(jīng)過
解碼器解碼后,得到YUV數(shù)據(jù),經(jīng)過格式轉(zhuǎn)換,成為RGB數(shù)據(jù)。解碼器將RGB數(shù)據(jù)傳遞給繪制器,等待OpenGL 渲染器使用。OpenGL 渲染器通過內(nèi)部的線程循環(huán),在適當?shù)臅r候,調(diào)用繪制器渲染畫面。畫面繪制完畢以后,得到經(jīng)過
OpenGL渲染(編輯過)的畫面,送到編碼器進行編碼。最后,將編碼好的數(shù)據(jù),寫入本地文件。
說明:
本文將主要講音視頻的
編碼知識,由于整個過程涉及到解碼、OpenGL 渲染這兩個前面介紹過的知識點,我們將復用之前封裝好的工具,并在一些特殊地方根據(jù)編碼的需要做一些適配。因此接下來在涉及到解碼和OpenGL的地方,至貼出適配的代碼,具體可以查看之前的文章,或者直接查看源碼。
二、關(guān)于 x264 so 庫編譯和引入
由于 x264 是基于 GPL 開源協(xié)議的,而 FFmpeg 默認是基于 LGPL 協(xié)議的,當引入 x264 時,由于 GPL 的傳染性,導致我們的代碼也必須開源,你可以使用 OpenH264 來代替。
這里仍然使用 x264 來學習相關(guān)的編碼過程。
另外,限于篇幅,本文不會介紹關(guān)于 x264 的編譯,會另外寫文章介紹。
x264 so 庫的引入和其他 so 引入是一樣的,具體請參考之前的文章,或者查看源碼中的 CMakeList.txt 。
FFmpeg已經(jīng)內(nèi)置了 h264 解碼器,所以如果只是解碼,并不需要引入x264。
三、封裝編碼器
編碼過程和解碼過程是非常類似的,其實就是解碼的逆過程,因此整個代碼框架流程和解碼器 BaseDecoder 基本是一致的。
定義 BaseEncoder
//?BaseEncoder.h
class?BaseEncoder:?public?IEncoder?{
private:
????//?編碼格式?ID
????AVCodecID?m_codec_id;
????//?線程依附的JVM環(huán)境
????JavaVM?*m_jvm_for_thread?=?NULL;
????//?編碼器
????AVCodec?*m_codec?=?NULL;
????//?編碼上下文
????AVCodecContext?*m_codec_ctx?=?NULL;
????//?編碼數(shù)據(jù)包
????AVPacket?*m_encoded_pkt?=?NULL;
????//?寫入Mp4的輸入流索引
????int?m_encode_stream_index?=?0;
????//?原數(shù)據(jù)時間基
????AVRational?m_src_time_base;
????//?緩沖隊列
????std::queue<OneFrame?*>?m_src_frames;
????//?操作數(shù)據(jù)鎖
????std::mutex?m_frames_lock;
????//?狀態(tài)回調(diào)
????IEncodeStateCb?*m_state_cb?=?NULL;
????bool?Init();
????/**
?????*?循環(huán)拉去已經(jīng)編碼的數(shù)據(jù),直到?jīng)]有數(shù)據(jù)或者編碼完畢
?????*?@return true 編碼結(jié)束;false 編碼未完成
?????*/
????bool?DrainEncode();
????/**
?????*?編碼一幀數(shù)據(jù)
?????*?@return?錯誤信息
?????*/
????int?EncodeOneFrame();
????//?新建編碼線程
????void?CreateEncodeThread();
????//?解碼靜態(tài)方法,給線程調(diào)用
????static?void?Encode(std::shared_ptr<BaseEncoder>?that);
????void?OpenEncoder();
????//?循環(huán)編碼
????void?LoopEncode();
????void?DoRelease();
????
????//?省略一些非重點代碼(具體請查看源碼)
????//?.......
protected:
????//?Mp4?封裝器
????Mp4Muxer?*m_muxer?=?NULL;
//-------------子類需要復寫的方法?begin-----------
????//?初始化編碼參數(shù)(上下文)
????virtual?void?InitContext(AVCodecContext?*codec_ctx)?=?0;
????//?配置Mp4?混淆通道信息
????virtual?int?ConfigureMuxerStream(Mp4Muxer?*muxer,?AVCodecContext?*ctx)?=?0;
????//?處理一幀數(shù)據(jù)
????virtual?AVFrame*?DealFrame(OneFrame?*one_frame)?=?0;
????//?釋放資源
????virtual?void?Release()?=?0;
????
????virtual?const?char?*const?LogSpec()?=?0;
//-------------子類需要復寫的方法?end-----------
public:
????BaseEncoder(JNIEnv?*env,?Mp4Muxer?*muxer,?AVCodecID?codec_id);
????//?壓入一幀待編碼數(shù)據(jù)(由外部調(diào)用)
????void?PushFrame(OneFrame?*one_frame)?override?;
????//?判斷是否緩沖數(shù)據(jù)過多,用于控制緩沖隊列大小
????bool?TooMuchData()?override?{
????????return?m_src_frames.size()?>?100;
????}
????//?設置編碼狀態(tài)監(jiān)聽器
????void?SetStateReceiver(IEncodeStateCb?*cb)?override?{
????????this->m_state_cb?=?cb;
????}
};
編碼器定義并不復雜,無非就是編碼需要用到的編碼器 m_codec、解碼上下文 m_codec_id 等,以及封裝對應的函數(shù)方法來拆分編碼過程中的幾個步驟。這里主要強調(diào)幾點:
- 控制編碼緩沖隊列大小
由于編碼過程中,編碼速度遠遠小于解碼速度,因此需要控制緩沖隊列大小,避免大量的數(shù)據(jù)堆積,導致內(nèi)容溢出或申請內(nèi)存失敗問題。
- 時間戳轉(zhuǎn)換
時間戳轉(zhuǎn)換在上篇文章中已經(jīng)有說明,具體請查看上篇文章??傊?,由于原視頻和目標視頻時間基是不一樣的,因此需要對時間戳進行轉(zhuǎn)換,才能保證編碼保存后的時間是正常的。
- 確保 MP4 軌道索引是正確的
MP4 有音頻和視頻兩個軌道,需要在寫入的時候,對應好,具體查看代碼中的
m_encode_stream_index。
實現(xiàn) BaseEncoder
初始化
//?BaseEncoder.cpp
BaseEncoder::BaseEncoder(JNIEnv?*env,?Mp4Muxer?*muxer,?AVCodecID?codec_id)
:?m_muxer(muxer),
m_codec_id(codec_id)?{
????if?(Init())?{
????????env->GetJavaVM(&m_jvm_for_thread);
????????CreateEncodeThread();
????}
}
bool?BaseEncoder::Init()?{
????//?1.?查找編碼器
????m_codec?=?avcodec_find_encoder(m_codec_id);
????if?(m_codec?==?NULL)?{
????????LOGE(TAG,?"Fail?to?find?encoder,?code?id?is?%d",?m_codec_id)
????????return?false;
????}
????//?2.?分配編碼上下文
????m_codec_ctx?=?avcodec_alloc_context3(m_codec);
????if?(m_codec_ctx?==?NULL)?{
????????LOGE(TAG,?"Fail?to?alloc?encoder?context")
????????return?false;
????}
????//?3.?初始化編碼數(shù)據(jù)包
????m_encoded_pkt?=?av_packet_alloc();
????av_init_packet(m_encoded_pkt);
????return?true;
}
void?BaseEncoder::CreateEncodeThread()?{
????//?使用智能指針,線程結(jié)束時,自動刪除本類指針
????std::shared_ptr<BaseEncoder>?that(this);
????std::thread?t(Encode,?that);
????t.detach();
}
編碼需要兩個參數(shù),m_muxer 和 m_codec_id,既:MP4 混合器和編碼格式ID。
其中,編碼格式 ID 根據(jù)音頻和視頻需要來設置,比如視頻 H264 為:AV_CODEC_ID_H264 ,音頻 AAC 為:AV_CODEC_ID_AAC。
接著,調(diào)用 Init() 方法:
- 根據(jù)編碼格式 ID 查找編碼器
- 分配編碼上下文
- 初始化編碼數(shù)據(jù)包
最后,創(chuàng)建編碼線程。
封裝編碼流程
//?BaseEncoder.cpp
void?BaseEncoder::Encode(std::shared_ptr<BaseEncoder>?that)?{
????JNIEnv?*?env;
????//將線程附加到虛擬機,并獲取env
????if?(that->m_jvm_for_thread->AttachCurrentThread(&env,?NULL)?!=?JNI_OK)?{
????????LOG_ERROR(that->TAG,?that->LogSpec(),?"Fail?to?Init?encode?thread");
????????return;
????}
????that->OpenEncoder();?//?1
????that->LoopEncode();??//?2
????that->DoRelease();???//?3
????
????//解除線程和jvm關(guān)聯(lián)
????that->m_jvm_for_thread->DetachCurrentThread();
}
過程和解碼非常類似。
第1步,打開編碼器
//?BaseEncoder.cpp
void?BaseEncoder::OpenEncoder()?{
????//?調(diào)用子類方法,根據(jù)音頻和視頻的不同,初始化編碼上下文
????InitContext(m_codec_ctx);
????int?ret?=?avcodec_open2(m_codec_ctx,?m_codec,?NULL);
????if?(ret?<?0)?{
????????LOG_ERROR(TAG,?LogSpec(),?"Fail?to?open?encoder?:?%d",?m_codec);
????????return;
????}
????m_encode_stream_index?=?ConfigureMuxerStream(m_muxer,?m_codec_ctx);
}
第2步,開啟編碼循環(huán)
編碼的核心方法只有兩個:
avcodec_send_frame: 數(shù)據(jù)發(fā)到編碼隊列
avcodec_receive_packet: 接收編碼好的數(shù)據(jù)
編碼過程主要有 5 個步驟:
- 從緩沖隊列中獲取待解碼數(shù)據(jù)
- 將原始數(shù)據(jù)交給子類處理(音頻和視頻根據(jù)自己的需求處理)
- 通過
avcodec_send_frame將數(shù)據(jù)發(fā)送到編碼器編碼 - 將編碼好的數(shù)據(jù)抽取出來
還有一點,既第 5 點,重新發(fā)送數(shù)據(jù)。
需要說明一下這里采取的 雙循環(huán) 編碼邏輯:除了最外層的 while(tue) 循環(huán)以外,里面還有一個 while (m_src_frames.size() > 0) 循環(huán)。
在緩沖隊列有數(shù)據(jù),并且 FFmpeg 內(nèi)部編碼隊列未滿 的情況下,會不斷地往 FFmpeg 發(fā)送數(shù)據(jù),直到發(fā)現(xiàn) FFmpeg 編碼返回 AVERROR(EAGAIN) ,則說明內(nèi)部隊列已滿,需要先將編碼的數(shù)據(jù)抽取出來,也就是調(diào)用 DrainEncode() 方法。
還有一點需要說明的是:如何判讀所有數(shù)據(jù)已經(jīng)都發(fā)送給編碼器了?
這里通過
one_frame->line_size來判斷。當監(jiān)聽到解碼器通知解碼完成的時候,則把一個空的幀數(shù)據(jù)
OneFrame的line_size設置為0,并壓入緩沖隊列。
BaseEncoder拿到這個空數(shù)據(jù)幀時,往FFmpeg的avcodec_send_frame()發(fā)送一個NULL數(shù)據(jù),則FFmpeg會自動結(jié)束編碼。
具體請看以下代碼:
//?BaseEncoder.cpp
void?BaseEncoder::LoopEncode()?{
????if?(m_state_cb?!=?NULL)?{
????????m_state_cb->EncodeStart();
????}
????while?(true)?{
????????if?(m_src_frames.size()?==?0)?{
????????????Wait();
????????}
????????while?(m_src_frames.size()?>?0)?{
????????????//?1.?獲取待解碼數(shù)據(jù)
????????????m_frames_lock.lock();
????????????OneFrame?*one_frame?=?m_src_frames.front();
????????????m_src_frames.pop();
????????????m_frames_lock.unlock();
????????????AVFrame?*frame?=?NULL;
????????????if?(one_frame->line_size?!=?0)?{
????????????????m_src_time_base?=?one_frame->time_base;
????????????????//?2.?子類處理數(shù)據(jù)
????????????????frame?=?DealFrame(one_frame);
????????????????delete?one_frame;
????????????????if?(m_state_cb?!=?NULL)?{
????????????????????m_state_cb->EncodeSend();
????????????????}
????????????????if?(frame?==?NULL)?{
????????????????????continue;
????????????????}
????????????}?else?{?//如果數(shù)據(jù)長度為0,說明編碼已經(jīng)結(jié)束,壓入空frame,使編碼器進入結(jié)束狀態(tài)
????????????????delete?one_frame;
????????????}
????????????//?3.?將數(shù)據(jù)發(fā)送到編碼器
????????????int?ret?=?avcodec_send_frame(m_codec_ctx,?frame);
????????????switch?(ret)?{
????????????????case?AVERROR_EOF:
????????????????????LOG_ERROR(TAG,?LogSpec(),?"Send?frame?finish?[AVERROR_EOF]")
????????????????????break;
????????????????case?AVERROR(EAGAIN):?//編碼編碼器已滿,先取出已編碼數(shù)據(jù),再嘗試發(fā)送數(shù)據(jù)
????????????????????while?(ret?==?AVERROR(EAGAIN))?{
????????????????????????LOG_ERROR(TAG,?LogSpec(),?"Send?frame?error[EAGAIN]:?%s",?av_err2str(AVERROR(EAGAIN)));
????????????????????????//?4.?將編碼好的數(shù)據(jù)榨干
????????????????????????if?(DrainEncode())?return;?//編碼結(jié)束
????????????????????????//?5.?重新發(fā)送數(shù)據(jù)
????????????????????????ret?=?avcodec_send_frame(m_codec_ctx,?frame);
????????????????????}
????????????????????break;
????????????????case?AVERROR(EINVAL):
????????????????????LOG_ERROR(TAG,?LogSpec(),?"Send?frame?error[EINVAL]:?%s",?av_err2str(AVERROR(EINVAL)));
????????????????????break;
????????????????case?AVERROR(ENOMEM):
????????????????????LOG_ERROR(TAG,?LogSpec(),?"Send?frame?error[ENOMEM]:?%s",?av_err2str(AVERROR(ENOMEM)));
????????????????????break;
????????????????default:
????????????????????break;
????????????}
????????????if?(ret?!=?0)?break;
????????}
????????if?(DrainEncode())?break;?//編碼結(jié)束
????}
}
接下來看下上面提到的 DrainEncode() 方法:
//?BaseEncoder.cpp
bool?BaseEncoder::DrainEncode()?{
????int?state?=?EncodeOneFrame();
????while?(state?==?0)?{
????????state?=?EncodeOneFrame();
????}
????return?state?==?AVERROR_EOF;
}
int?BaseEncoder::EncodeOneFrame()?{
????int?state?=?avcodec_receive_packet(m_codec_ctx,?m_encoded_pkt);
????switch?(state)?{
????????case?AVERROR_EOF:?//解碼結(jié)束
????????????LOG_ERROR(TAG,?LogSpec(),?"Encode?finish")
????????????break;
????????case?AVERROR(EAGAIN):?//編碼還未完成,待會再來
????????????LOG_INFO(TAG,?LogSpec(),?"Encode?error[EAGAIN]:?%s",?av_err2str(AVERROR(EAGAIN)));
????????????break;
????????case?AVERROR(EINVAL):
????????????LOG_ERROR(TAG,?LogSpec(),??"Encode?error[EINVAL]:?%s",?av_err2str(AVERROR(EINVAL)));
????????????break;
????????case?AVERROR(ENOMEM):
????????????LOG_ERROR(TAG,?LogSpec(),?"Encode?error[ENOMEM]:?%s",?av_err2str(AVERROR(ENOMEM)));
????????????break;
????????default:?//?成功獲取到一幀編碼好的數(shù)據(jù),寫入?MP4
????????????//將視頻pts/dts轉(zhuǎn)換為容器pts/dts
????????????av_packet_rescale_ts(m_encoded_pkt,?m_src_time_base,
?????????????????????????????????m_muxer->GetTimeBase(m_encode_stream_index));
????????????if?(m_state_cb?!=?NULL)?{
????????????????m_state_cb->EncodeFrame(m_encoded_pkt->data);
????????????????long?cur_time?=?(long)(m_encoded_pkt->pts*av_q2d(m_muxer->GetTimeBase(m_encode_stream_index))*1000);
????????????????m_state_cb->EncodeProgress(cur_time);
????????????}
????????????m_encoded_pkt->stream_index?=?m_encode_stream_index;
????????????m_muxer->Write(m_encoded_pkt);
????????????break;
????}
????av_packet_unref(m_encoded_pkt);
????return?state;
}
同樣是一個 while 循環(huán),根據(jù)接收數(shù)據(jù)的狀態(tài)來判斷是否結(jié)束循環(huán)。
主要邏輯在 EncodeOneFrame() 中,通過 avcodec_receive_packet() 獲取 FFmpeg 中已經(jīng)完成編碼的數(shù)據(jù),如果該方法返回 0 說明獲取成功,可以將數(shù)據(jù)寫入 MP4 中。
EncodeOneFrame() 返回的就是 avcodec_receive_packet 的返回值,那么當其為 0 時,循環(huán)獲取下一幀數(shù)據(jù),直到返回值為 AVERROR(EAGAIN) 或 AVERROR_EOF,既:沒有數(shù)據(jù) 或 編碼結(jié)束。
如此,通過以上幾個循環(huán),不斷往編碼器塞入數(shù)據(jù),和拉取數(shù)據(jù),直到完成所有數(shù)據(jù)編碼,結(jié)束編碼。
第3步,結(jié)束編碼,釋放資源
完成編碼后,需要釋放相關(guān)的資源
//?BaseEncoder.cpp
void?BaseEncoder::DoRelease()?{
????if?(m_encoded_pkt?!=?NULL)?{
????????av_packet_free(&m_encoded_pkt);
????????m_encoded_pkt?=?NULL;
????}
????if?(m_codec_ctx?!=?NULL)?{
????????avcodec_close(m_codec_ctx);
????????avcodec_free_context(&m_codec_ctx);
????}
????//?調(diào)用子類方法,釋放子類資源
????Release();
????if?(m_state_cb?!=?NULL)?{
????????m_state_cb->EncodeFinish();
????}
}
封裝視頻編碼器
視頻編碼器繼承自上面定義好的基礎編碼器 BaseEncoder。
//?VideoEncoder.h
class?VideoEncoder:?public?BaseEncoder?{
private:
????const?char?*?TAG?=?"VideoEncoder";
????//?視頻格式轉(zhuǎn)化工具
????SwsContext?*m_sws_ctx?=?NULL;
????//?一陣?YUV?數(shù)據(jù)
????AVFrame?*m_yuv_frame?=?NULL;
????//?目標視頻寬高
????int?m_width?=?0,?m_height?=?0;
????void?InitYUVFrame();
protected:
????const?char?*const?LogSpec()?override?{
????????return?"視頻";
????};
????void?InitContext(AVCodecContext?*codec_ctx)?override;
????int?ConfigureMuxerStream(Mp4Muxer?*muxer,?AVCodecContext?*ctx)?override;
????AVFrame*?DealFrame(OneFrame?*one_frame)?override;
????void?Release()?override;
public:
????VideoEncoder(JNIEnv?*env,?Mp4Muxer?*muxer,?int?width,?int?height);
};
具體實現(xiàn):
1. 構(gòu)造方法:
//?VideoEncoder.cpp
VideoEncoder::VideoEncoder(JNIEnv?*env,?Mp4Muxer?*muxer,?int?width,?int?height)
:?BaseEncoder(env,?muxer,?AV_CODEC_ID_H264),
m_width(width),
m_height(height)?{
????m_sws_ctx?=?sws_getContext(width,?height,?AV_PIX_FMT_RGBA,
???????????????????????????????width,?height,?AV_PIX_FMT_YUV420P,?SWS_FAST_BILINEAR,
???????????????????????????????NULL,?NULL,?NULL);
}
這里根據(jù)目標輸出視頻的寬高,原格式(OpenGL輸出的RGBA數(shù)據(jù))/目標格式(YUV),初始化格式轉(zhuǎn)換器,這個與解碼剛好是相反的過程。
2. 編碼參數(shù)初始化:
2.1 初始化上下文和子類內(nèi)部數(shù)據(jù),主要時配置編碼視頻的 寬高、碼率、幀率、時間基 等。
還有一個比較重要的參數(shù)就是 qmin和qmax,其值范圍為 [0~51],用于配置編碼畫面質(zhì)量,值越大,畫面質(zhì)量越低,視頻文件越小??梢愿约旱男枨笈渲谩?/p>
還有就是 InitYUVFrame() 申請轉(zhuǎn)碼需要用到的 YUV 數(shù)據(jù)內(nèi)存空間。
//?VideoEncoder.cpp
void?VideoEncoder::InitContext(AVCodecContext?*codec_ctx)?{
????codec_ctx->bit_rate?=?3*m_width*m_height;
????codec_ctx->width?=?m_width;
????codec_ctx->height?=?m_height;
????//把1秒鐘分成fps個單位
????codec_ctx->time_base?=?{1,?ENCODE_VIDEO_FPS};
????codec_ctx->framerate?=?{ENCODE_VIDEO_FPS,?1};
????//畫面組大小
????codec_ctx->gop_size?=?50;
????//沒有B幀
????codec_ctx->max_b_frames?=?0;
????codec_ctx->pix_fmt?=?AV_PIX_FMT_YUV420P;
????codec_ctx->thread_count?=?8;
????av_opt_set(codec_ctx->priv_data,?"preset",?"ultrafast",?0);
????av_opt_set(codec_ctx->priv_data,?"tune",?"zerolatency",?0);
????//這是量化范圍設定,其值范圍為0~51,
????//越小質(zhì)量越高,需要的比特率越大,0為無損編碼
????codec_ctx->qmin?=?28;
????codec_ctx->qmax?=?50;
????//全局的編碼信息
????codec_ctx->flags?|=?AV_CODEC_FLAG_GLOBAL_HEADER;
????InitYUVFrame();
????LOGI(TAG,?"Init?codec?context?success")
}
void?VideoEncoder::InitYUVFrame()?{
????//設置YUV輸出空間
????m_yuv_frame?=?av_frame_alloc();
????m_yuv_frame->format?=?AV_PIX_FMT_YUV420P;
????m_yuv_frame->width?=?m_width;
????m_yuv_frame->height?=?m_height;
????//分配空間
????int?ret?=?av_frame_get_buffer(m_yuv_frame,?0);
????if?(ret?<?0)?{
????????LOGE(TAG,?"Fail?to?get?yuv?frame?buffer");
????}
}
2.2 根據(jù)解碼器信息,寫入對應的 MP4 軌道信息。
//?VideoEncoder.cpp
int?VideoEncoder::ConfigureMuxerStream(Mp4Muxer?*muxer,?AVCodecContext?*ctx)?{
????return?muxer->AddVideoStream(ctx);
}
3. 處理數(shù)據(jù)
還記得父類定義的子類數(shù)據(jù)處理方法嗎?
視頻編碼器需要將 OpenGL 輸出到 RGBA 數(shù)據(jù)轉(zhuǎn)化為 YUV 數(shù)據(jù),才能送進編碼器編碼。
//?VideoEncoder.cpp
AVFrame*?VideoEncoder::DealFrame(OneFrame?*one_frame)?{
????uint8_t?*in_data[AV_NUM_DATA_POINTERS]?=?{?0?};
????in_data[0]?=?one_frame->data;
????int?src_line_size[AV_NUM_DATA_POINTERS]?=?{?0?};
????src_line_size[0]?=?one_frame->line_size;
????int?h?=?sws_scale(m_sws_ctx,?in_data,?src_line_size,?0,?m_height,
??????????????????????m_yuv_frame->data,?m_yuv_frame->linesize);
????if?(h?<=?0)?{
????????LOGE(TAG,?"轉(zhuǎn)碼出錯");
????????return?NULL;
????}
????m_yuv_frame->pts?=?one_frame->pts;
????return?m_yuv_frame;
}
4. 釋放子類資源
編碼結(jié)束后,父類回調(diào)子類方法,方法資源,通知 Mp4Muxer 結(jié)束視頻通道寫入。
//?VideoEncoder.cpp
void?VideoEncoder::Release()?{
????if?(m_yuv_frame?!=?NULL)?{
????????av_frame_free(&m_yuv_frame);
????????m_yuv_frame?=?NULL;
????}
????if?(m_sws_ctx?!=?NULL)?{
????????sws_freeContext(m_sws_ctx);
????????m_sws_ctx?=?NULL;
????}
????//?結(jié)束視頻通道數(shù)據(jù)寫入
????m_muxer->EndVideoStream();
}
封裝音頻編碼器
音頻編碼器基本視頻是一樣的,只是參數(shù)配置有所不同,直接來看實現(xiàn)就好。
常規(guī)的音頻參數(shù)配置:比特率,編碼格式,通道數(shù)量等
重點看下 InitFrame() 方法,這里需要通過通道數(shù)、編碼格式等,借助 av_samples_get_buffer_size() 方法,計算用來保存目標幀數(shù)據(jù)的內(nèi)存大小。
//?AudioEncoder.cpp
AudioEncoder::AudioEncoder(JNIEnv?*env,?Mp4Muxer?*muxer)
:?BaseEncoder(env,?muxer,?AV_CODEC_ID_AAC)?{
}
void?AudioEncoder::InitContext(AVCodecContext?*codec_ctx)?{
????codec_ctx->codec_type?=?AVMEDIA_TYPE_AUDIO;
????codec_ctx->sample_fmt?=?ENCODE_AUDIO_DEST_FORMAT;
????codec_ctx->sample_rate?=?ENCODE_AUDIO_DEST_SAMPLE_RATE;
????codec_ctx->channel_layout?=?ENCODE_AUDIO_DEST_CHANNEL_LAYOUT;
????codec_ctx->channels?=?ENCODE_AUDIO_DEST_CHANNEL_COUNTS;
????codec_ctx->bit_rate?=?ENCODE_AUDIO_DEST_BIT_RATE;
????InitFrame();
}
void?AudioEncoder::InitFrame()?{
????m_frame?=?av_frame_alloc();
????m_frame->nb_samples?=?1024;
????m_frame->format?=?ENCODE_AUDIO_DEST_FORMAT;
????m_frame->channel_layout?=?ENCODE_AUDIO_DEST_CHANNEL_LAYOUT;
????int?size?=?av_samples_get_buffer_size(NULL,?ENCODE_AUDIO_DEST_CHANNEL_COUNTS,?m_frame->nb_samples,
??????????????????????????????????????????ENCODE_AUDIO_DEST_FORMAT,?1);
????uint8_t?*frame_buf?=?(uint8_t?*)?av_malloc(size);
????avcodec_fill_audio_frame(m_frame,?ENCODE_AUDIO_DEST_CHANNEL_COUNTS,?ENCODE_AUDIO_DEST_FORMAT,
?????????????????????????????frame_buf,?size,?1);
}
int?AudioEncoder::ConfigureMuxerStream(Mp4Muxer?*muxer,?AVCodecContext?*ctx)?{
????return?muxer->AddAudioStream(ctx);
}
AVFrame*?AudioEncoder::DealFrame(OneFrame?*one_frame)?{
????m_frame->pts?=?one_frame->pts;
????memcpy(m_frame->data[0],?one_frame->data,?4096);
????memcpy(m_frame->data[1],?one_frame->ext_data,?4096);
????return?m_frame;
}
void?AudioEncoder::Release()?{
????m_muxer->EndAudioStream();
}
最后,DealFrame 需要將 one_frame 中保存的左右聲道的數(shù)據(jù)復制到 m_frame 申請的內(nèi)存中,并返回給 父類 送到編碼器編碼。
四、獲取 OpenGL 渲染的視頻數(shù)據(jù)
我們知道,視頻數(shù)據(jù)經(jīng)過 OpenGL 編輯以后,是無法直接送到編碼器進行編碼的,需要通過 OpenGL 的 glReadPixels 方法來獲取。
下面就改造一下原來定義的 OpenGLRender 來實現(xiàn)。
完整代碼請查看工程源碼。
在渲染方法 Render() 中,增加獲取的畫面的方法:
//?OpenGLRender.cpp
void?OpenGLRender::Render()?{
????if?(RENDERING?==?m_state)?{
????????m_drawer_proxy->Draw();
????????m_egl_surface->SwapBuffers();
????????if?(m_need_output_pixels?&&?m_pixel_receiver?!=?NULL)?{//輸出畫面rgba
????????????m_need_output_pixels?=?false;
????????????Render();?//再次渲染最新的畫面
????????????size_t?size?=?m_window_width?*?m_window_height?*?4?*?sizeof(uint8_t);
????????????uint8_t?*rgb?=?(uint8_t?*)?malloc(size);
????????????if?(rgb?==?NULL)?{
????????????????realloc(rgb,?size);
????????????????LOGE(TAG,?"內(nèi)存分配失敗:?%d",?rgb)
????????????}
????????????glReadPixels(0,?0,?m_window_width,?m_window_height,?GL_RGBA,?GL_UNSIGNED_BYTE,?rgb);
????????????
????????????//?將數(shù)據(jù)發(fā)送出去
????????????m_pixel_receiver->ReceivePixel(rgb);
????????}
????}
}
增加一個請求方法,用于通知 OpenGLRender 將數(shù)據(jù)輸發(fā)送出來:
//?OpenGLRender.cpp
void?OpenGLRender::RequestRgbaData()?{
????m_need_output_pixels?=?true;
}
原理很簡單,在解碼器解碼一幀數(shù)據(jù)送入
OpenGL渲染以后,就馬上通知OpenGLRender將畫面發(fā)送出來。
當然了,還需要定義一個接收器:
//?OpenGLPixelReceiver.h
class?OpenGLPixelReceiver?{
public:
????virtual?void?ReceivePixel(uint8_t?*rgba)?=?0;
};
五、MP4 封裝器
該部分內(nèi)容基本就是【上一篇文章】的定義的重打包 FFRepack 工具的重新封裝,這里不再贅述,請查看上一篇文章,或源碼。
//?Mp4Muxer.cpp
void?Mp4Muxer::Init(JNIEnv?*env,?jstring?path)?{
????const?char?*u_path?=?env->GetStringUTFChars(path,?NULL);
????int?len?=?strlen(u_path);
????m_path?=?new?char[len];
????strcpy(m_path,?u_path);
????//新建輸出上下文
????avformat_alloc_output_context2(&m_fmt_ctx,?NULL,?NULL,?m_path);
????//?釋放引用
????env->ReleaseStringUTFChars(path,?u_path);
}
int?Mp4Muxer::AddVideoStream(AVCodecContext?*ctx)?{
????int?stream_index?=?AddStream(ctx);
????m_video_configured?=?true;
????Start();
????return?stream_index;
}
int?Mp4Muxer::AddAudioStream(AVCodecContext?*ctx)?{
????int?stream_index?=?AddStream(ctx);
????m_audio_configured?=?true;
????Start();
????return?stream_index;
}
int?Mp4Muxer::AddStream(AVCodecContext?*ctx)?{
????AVStream?*video_stream?=?avformat_new_stream(m_fmt_ctx,?NULL);
????avcodec_parameters_from_context(video_stream->codecpar,?ctx);
????video_stream->codecpar->codec_tag?=?0;
????return?video_stream->index;
}
void?Mp4Muxer::Start()?{
????if?(m_video_configured?&&?m_audio_configured)?{
????????av_dump_format(m_fmt_ctx,?0,?m_path,?1);
????????//打開文件輸入
????????int?ret?=?avio_open(&m_fmt_ctx->pb,?m_path,?AVIO_FLAG_WRITE);
????????if?(ret?<?0)?{
????????????LOGE(TAG,?"Open?av?io?fail")
????????????return;
????????}?else?{
????????????LOGI(TAG,?"Open?av?io:?%s",?m_path)
????????}
????????//寫入頭部信息
????????ret?=?avformat_write_header(m_fmt_ctx,?NULL);
????????if?(ret?<?0)?{
????????????LOGE(TAG,?"Write?header?fail")
????????????return;
????????}?else?{
????????????LOGI(TAG,?"Write?header?success")
????????}
????}
}
void?Mp4Muxer::Write(AVPacket?*pkt)?{
????int?ret?=?av_interleaved_write_frame(m_fmt_ctx,?pkt);
//????uint64_t?time?=?uint64_t?(pkt->pts*av_q2d(GetTimeBase(pkt->stream_index))*1000);
//????LOGE(TAG,?"Write?one?frame?pts:?%lld,?ret?=?%s",?time?,?av_err2str(ret))
}
void?Mp4Muxer::EndAudioStream()?{
????LOGI(TAG,?"End?audio?stream")
????m_audio_end?=?true;
????Release();
}
void?Mp4Muxer::EndVideoStream()?{
????LOGI(TAG,?"End?video?stream")
????m_video_end?=?true;
????Release();
}
void?Mp4Muxer::Release()?{
????if?(m_video_end?&&?m_audio_end)?{
????????if?(m_fmt_ctx)?{
????????????//寫入文件尾部
????????????av_write_trailer(m_fmt_ctx);
????????????//關(guān)閉輸出IO
????????????avio_close(m_fmt_ctx->pb);
????????????//釋放資源
????????????avformat_free_context(m_fmt_ctx);
????????????m_fmt_ctx?=?NULL;
????????}
????????delete?[]?m_path;
????????LOGI(TAG,?"Muxer?Release")
????????if?(m_mux_finish_cb)?{
????????????m_mux_finish_cb->OnMuxFinished();
????????}
????}
}
六、整合調(diào)用
有了以上工具的定義和封裝,加上之前的解碼器和渲染器,就萬事俱備,只欠東風了!
我們需要將他們整合在一起,串聯(lián)起整個【解碼--編輯--編碼--寫入MP4】流程。
定義合成器 Synthesizer。
初始化
//?Synthesizer.cpp
//?這里直接寫死視頻寬高了,?需要根據(jù)自己的需求動態(tài)配置
static?int?WIDTH?=?1920;
static?int?HEIGHT?=?1080;
Synthesizer::Synthesizer(JNIEnv?*env,?jstring?src_path,?jstring?dst_path)?{
????//?封裝器
????m_mp4_muxer?=?new?Mp4Muxer();
????m_mp4_muxer->Init(env,?dst_path);
????m_mp4_muxer->SetMuxFinishCallback(this);
????
????//?--------------------------視頻配置--------------------------
????//?【視頻編碼器】
????m_v_encoder?=?new?VideoEncoder(env,?m_mp4_muxer,?WIDTH,?HEIGHT);
????m_v_encoder->SetStateReceiver(this);
????//?【繪制器】
????m_drawer_proxy?=?new?DefDrawerProxyImpl();
????VideoDrawer?*drawer?=?new?VideoDrawer();
????m_drawer_proxy->AddDrawer(drawer);
????//?【OpenGL?渲染器】
????m_gl_render?=?new?OpenGLRender(env,?m_drawer_proxy);
????//?設置離屏渲染畫面寬高
????m_gl_render->SetOffScreenSize(WIDTH,?HEIGHT);
????//?接收經(jīng)過(編輯)渲染的畫面數(shù)據(jù)
????m_gl_render->SetPixelReceiver(this);
????//?【視頻解碼器】
????m_video_decoder?=?new?VideoDecoder(env,?src_path,?true);
????m_video_decoder->SetRender(drawer);
????//?監(jiān)聽解碼狀態(tài)
????m_video_decoder->SetStateReceiver(this);
????//--------------------------音頻配置--------------------------
????//?【音頻編碼器】
????m_a_encoder?=?new?AudioEncoder(env,?m_mp4_muxer);
????//?監(jiān)聽編碼狀態(tài)
????m_a_encoder->SetStateReceiver(this);
????//?【音頻解碼器】
????m_audio_decoder?=?new?AudioDecoder(env,?src_path,?true);
????//?監(jiān)聽解碼狀態(tài)
????m_audio_decoder->SetStateReceiver(this);
}
可以看到,解碼流程和以前幾乎時一模一樣的,三個不一樣的地方是:
需要告訴解碼器,這是合成過程,無需在解碼后加入時間同步。
OpenGL 渲染是離屏渲染,需要設置渲染尺寸
音頻無需渲染到 OpenSL 中,直接發(fā)送出來壓入編碼即可。
啟動
初始化完畢后,解碼器進入等待,需要外面觸發(fā)進入循環(huán)解碼流程。
//?Synthesizer.cpp
void?Synthesizer::Start()?{
????m_video_decoder->GoOn();
????m_audio_decoder->GoOn();
}
當調(diào)用了 BaseDecoder 的 GoOn() 方法以后,整個【解碼-->編碼】流程將被啟動。
而將它們粘合起來的,就是解碼器的狀態(tài)回調(diào)方法 DecodeOneFrame()。
//?Synthesizer.cpp
bool?Synthesizer::DecodeOneFrame(IDecoder?*decoder,?OneFrame?*frame)?{
????if?(decoder?==?m_video_decoder)?{
????????//?等待上一幀畫面數(shù)據(jù)壓入編碼緩沖隊列?
????????while?(m_cur_v_frame)?{
????????????av_usleep(2000);?//?2ms
????????}
????????m_cur_v_frame?=?frame;
????????m_gl_render->RequestRgbaData();
????????return?m_v_encoder->TooMuchData();
????}?else?{
????????m_cur_a_frame?=?frame;
????????m_a_encoder->PushFrame(frame);
????????return?m_a_encoder->TooMuchData();
????}
}
void?Synthesizer::ReceivePixel(uint8_t?*rgba)?{
????OneFrame?*rgbFrame?=?new?OneFrame(rgba,?m_cur_v_frame->line_size,
??????????????????????????????????????m_cur_v_frame->pts,?m_cur_v_frame->time_base);
????m_v_encoder->PushFrame(rgbFrame);
????//?清空上一幀數(shù)據(jù)信息
????m_cur_v_frame?=?NULL;
}
當接收到解碼器的一幀數(shù)據(jù)后,
- 如果是音頻數(shù)據(jù),直接將數(shù)據(jù)通過
BaseDecoder的PushFrame()方法壓入隊列。 - 如果是視頻數(shù)據(jù),將當前幀數(shù)據(jù)信息保存下來,并通知
OpenGLRender將畫面數(shù)據(jù)發(fā)送出來。在ReceivePixel()方法中接收到畫面數(shù)據(jù)后,將數(shù)據(jù)PushFrame()到視頻編碼器中。
直到解碼完畢,在 DecodeFinish() 方法中,壓入空數(shù)據(jù)幀,通知編碼器結(jié)束編碼。
//?Synthesizer.cpp
void?Synthesizer::DecodeFinish(IDecoder?*decoder)?{
????//?編碼結(jié)束,壓入一幀空數(shù)據(jù),通知編碼器結(jié)束編碼
????if?(decoder?==?m_video_decoder)?{
????????m_v_encoder->PushFrame(new?OneFrame(NULL,?0,?0,?AVRational{1,?25},?NULL));
????}?else?{
????????m_a_encoder->PushFrame(new?OneFrame(NULL,?0,?0,?AVRational{1,?25},?NULL));
????}
}
void?Synthesizer::EncodeFinish()?{
????LOGI("Synthesizer",?"EncodeFinish?...");
}
void?Synthesizer::OnMuxFinished()?{
????LOGI("Synthesizer",?"OnMuxFinished?...");
????m_gl_render->Stop();
????if?(m_mp4_muxer?!=?NULL)?{
????????delete?m_mp4_muxer;
????}
????m_drawer_proxy?=?NULL;
}
至此,整個流程就完整了?。?!
本系列文章終于完結(jié)啦,總算是把坑填完,為自己撒花~ 哈哈哈~? ??????????????????
