<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 23451字,需瀏覽 47分鐘

           ·

          2021-04-18 03:31












          6693bd208db457278b74eb6632b8900c.webp

          聲 明

          首先,這一系列文章均基于自己的理解和實踐,可能有不對的地方,歡迎大家指正。
          其次,這是一個入門系列,涉及的知識也僅限于夠用,深入的知識網(wǎng)上也有許許多多的博文供大家學習了。
          最后,寫文章過程中,會借鑒參考其他人分享的文章,會在文章最后列出,感謝這些作者的分享。

          碼字不易,轉(zhuǎn)載請注明出處!

          目錄

          一、Android音視頻硬解碼篇:
          二、使用OpenGL渲染視頻畫面篇
          三、Android FFmpeg音視頻解碼篇

          本文你可以了解到

          如何使用 FFmepg 對編輯好的視頻進行重新編碼,生成可以播放的音視頻文件。

          寫在前面

          本文是音視頻系列文章的最后一篇了,也是拖了最久的一篇(懶癌發(fā)作-_-!!),終于下定決心,把坑填完。話不多說了,馬上進入正文。

          上一篇文章中,介紹了如何對音視頻文件進行解封和重新封裝,這個過程不涉及音視頻的解碼和編碼,也就是沒有對音視頻進行編輯,這無法滿足日常的開發(fā)需求。

          因此,本文將填上編輯過程的空缺,為本系列畫上句號。

          一、整體流程說明

          在前面的幾篇文章中,我們已經(jīng)做好了 解碼器, OpenGL 渲染器,因此,編碼的時候,除了需要 編碼器 外,還需要將之前的內(nèi)容做好整合。下面通過一張圖做一下簡要說明:

          a4fab5c34541111479ac2ede90f47efa.webp

          模塊

          首先可以關(guān)注到,這個過程有三個大模塊,也是三個 獨立又互相關(guān)聯(lián) 的線程,分別負責:

          • 原視頻解碼
          • OpenGL 畫面渲染
          • 目標視頻編碼

          數(shù)據(jù)流向

          看下視頻數(shù)據(jù)是如何流轉(zhuǎn)的:

          1. 原視頻經(jīng)過 解碼器 解碼后,得到 YUV 數(shù)據(jù),經(jīng)過格式轉(zhuǎn)換,成為 RGB 數(shù)據(jù)。

          2. 解碼器RGB 數(shù)據(jù)傳遞給 繪制器,等待 OpenGL 渲染器 使用。

          3. OpenGL 渲染器 通過內(nèi)部的線程循環(huán),在適當?shù)臅r候,調(diào)用 繪制器 渲染畫面。

          4. 畫面繪制完畢以后,得到經(jīng)過 OpenGL 渲染(編輯過)的畫面,送到 編碼器 進行編碼。

          5. 最后,將編碼好的數(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_muxerm_codec_id,既:MP4 混合器和編碼格式ID。

          其中,編碼格式 ID 根據(jù)音頻和視頻需要來設置,比如視頻 H264 為:AV_CODEC_ID_H264 ,音頻 AAC 為:AV_CODEC_ID_AAC。

          接著,調(diào)用 Init() 方法:

          1. 根據(jù)編碼格式 ID 查找編碼器
          2. 分配編碼上下文
          3. 初始化編碼數(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 個步驟:

          1. 從緩沖隊列中獲取待解碼數(shù)據(jù)
          2. 將原始數(shù)據(jù)交給子類處理(音頻和視頻根據(jù)自己的需求處理)
          3. 通過 avcodec_send_frame 將數(shù)據(jù)發(fā)送到編碼器編碼
          4. 將編碼好的數(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ù) OneFrameline_size 設置為 0,并壓入緩沖隊列。

          BaseEncoder 拿到這個空數(shù)據(jù)幀時,往 FFmpegavcodec_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ù)就是 qminqmax,其值范圍為 [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 編輯以后,是無法直接送到編碼器進行編碼的,需要通過 OpenGLglReadPixels 方法來獲取。

          下面就改造一下原來定義的 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);
          }

          可以看到,解碼流程和以前幾乎時一模一樣的,三個不一樣的地方是:

          1. 需要告訴解碼器,這是合成過程,無需在解碼后加入時間同步。

          2. OpenGL 渲染是離屏渲染,需要設置渲染尺寸

          3. 音頻無需渲染到 OpenSL 中,直接發(fā)送出來壓入編碼即可。

          啟動

          初始化完畢后,解碼器進入等待,需要外面觸發(fā)進入循環(huán)解碼流程。

          //?Synthesizer.cpp

          void?Synthesizer::Start()?{
          ????m_video_decoder->GoOn();
          ????m_audio_decoder->GoOn();
          }

          當調(diào)用了 BaseDecoderGoOn() 方法以后,整個【解碼-->編碼】流程將被啟動。

          而將它們粘合起來的,就是解碼器的狀態(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ù)通過 BaseDecoderPushFrame() 方法壓入隊列。
          • 如果是視頻數(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é)啦,總算是把坑填完,為自己撒花~ 哈哈哈~? ??????????????????


          瀏覽 68
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  在线中文字幕亚洲 | 看黄免费在线 | 亚洲综合成人在线 | 逼特逼视频网站 | 国产高清激情 |