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

        共 23451字,需瀏覽 47分鐘

         ·

        2021-04-18 03:31












        6693bd208db457278b74eb6632b8900c.webp

        聲 明

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

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

        目錄

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

        本文你可以了解到

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

        寫在前面

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

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

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

        一、整體流程說明

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

        a4fab5c34541111479ac2ede90f47efa.webp

        模塊

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

        • 原視頻解碼
        • OpenGL 畫面渲染
        • 目標(biāo)視頻編碼

        數(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),在適當(dāng)?shù)臅r(shí)候,調(diào)用 繪制器 渲染畫面。

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

        5. 最后,將編碼好的數(shù)據(jù),寫入本地文件。

        說明:

        本文將主要講音視頻的 編碼 知識(shí),由于整個(gè)過程涉及到解碼OpenGL 渲染 這兩個(gè)前面介紹過的知識(shí)點(diǎn),我們將復(fù)用之前封裝好的工具,并在一些特殊地方根據(jù)編碼的需要做一些適配。

        因此接下來在涉及到解碼和OpenGL的地方,至貼出適配的代碼,具體可以查看之前的文章,或者直接查看源碼。

        二、關(guān)于 x264 so 庫編譯和引入

        由于 x264 是基于 GPL 開源協(xié)議的,而 FFmpeg 默認(rèn)是基于 LGPL 協(xié)議的,當(dāng)引入 x264 時(shí),由于 GPL 的傳染性,導(dǎo)致我們的代碼也必須開源,你可以使用 OpenH264 來代替。

        這里仍然使用 x264 來學(xué)習(xí)相關(guān)的編碼過程。

        另外,限于篇幅,本文不會(huì)介紹關(guān)于 x264 的編譯,會(huì)另外寫文章介紹。

        x264 so 庫的引入和其他 so 引入是一樣的,具體請(qǐng)參考之前的文章,或者查看源碼中的 CMakeList.txt 。

        FFmpeg 已經(jīng)內(nèi)置了 h264 解碼器,所以如果只是解碼,并不需要引入 x264 。

        三、封裝編碼器

        編碼過程和解碼過程是非常類似的,其實(shí)就是解碼的逆過程,因此整個(gè)代碼框架流程和解碼器 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ù)時(shí)間基
        ????AVRational?m_src_time_base;

        ????//?緩沖隊(duì)列
        ????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?錯(cuò)誤信息
        ?????*/

        ????int?EncodeOneFrame();

        ????//?新建編碼線程
        ????void?CreateEncodeThread();

        ????//?解碼靜態(tài)方法,給線程調(diào)用
        ????static?void?Encode(std::shared_ptr<BaseEncoder>?that);

        ????void?OpenEncoder();

        ????//?循環(huán)編碼
        ????void?LoopEncode();

        ????void?DoRelease();
        ????
        ????//?省略一些非重點(diǎn)代碼(具體請(qǐng)查看源碼)
        ????//?.......

        protected:

        ????//?Mp4?封裝器
        ????Mp4Muxer?*m_muxer?=?NULL;

        //-------------子類需要復(fù)寫的方法?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;
        //-------------子類需要復(fù)寫的方法?end-----------

        public:
        ????BaseEncoder(JNIEnv?*env,?Mp4Muxer?*muxer,?AVCodecID?codec_id);

        ????//?壓入一幀待編碼數(shù)據(jù)(由外部調(diào)用)
        ????void?PushFrame(OneFrame?*one_frame)?override?;

        ????//?判斷是否緩沖數(shù)據(jù)過多,用于控制緩沖隊(duì)列大小
        ????bool?TooMuchData()?override?{
        ????????return?m_src_frames.size()?>?100;
        ????}

        ????//?設(shè)置編碼狀態(tài)監(jiān)聽器
        ????void?SetStateReceiver(IEncodeStateCb?*cb)?override?{
        ????????this->m_state_cb?=?cb;
        ????}
        };

        編碼器定義并不復(fù)雜,無非就是編碼需要用到的編碼器 m_codec、解碼上下文 m_codec_id 等,以及封裝對(duì)應(yīng)的函數(shù)方法來拆分編碼過程中的幾個(gè)步驟。這里主要強(qiáng)調(diào)幾點(diǎn):

        • 控制編碼緩沖隊(duì)列大小

        由于編碼過程中,編碼速度遠(yuǎn)遠(yuǎn)小于解碼速度,因此需要控制緩沖隊(duì)列大小,避免大量的數(shù)據(jù)堆積,導(dǎo)致內(nèi)容溢出或申請(qǐng)內(nèi)存失敗問題。

        • 時(shí)間戳轉(zhuǎn)換

        時(shí)間戳轉(zhuǎn)換在上篇文章中已經(jīng)有說明,具體請(qǐng)查看上篇文章??傊捎谠曨l和目標(biāo)視頻時(shí)間基是不一樣的,因此需要對(duì)時(shí)間戳進(jìn)行轉(zhuǎn)換,才能保證編碼保存后的時(shí)間是正常的。

        • 確保 MP4 軌道索引是正確的

        MP4 有音頻和視頻兩個(gè)軌道,需要在寫入的時(shí)候,對(duì)應(yīng)好,具體查看代碼中的 m_encode_stream_index 。

        實(shí)現(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é)束時(shí),自動(dòng)刪除本類指針
        ????std::shared_ptr<BaseEncoder>?that(this);
        ????std::thread?t(Encode,?that);
        ????t.detach();
        }

        編碼需要兩個(gè)參數(shù),m_muxerm_codec_id,既:MP4 混合器和編碼格式ID。

        其中,編碼格式 ID 根據(jù)音頻和視頻需要來設(shè)置,比如視頻 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;

        ????//將線程附加到虛擬機(jī),并獲取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)

        編碼的核心方法只有兩個(gè):

        avcodec_send_frame: 數(shù)據(jù)發(fā)到編碼隊(duì)列

        avcodec_receive_packet: 接收編碼好的數(shù)據(jù)

        編碼過程主要有 5 個(gè)步驟:

        1. 從緩沖隊(duì)列中獲取待解碼數(shù)據(jù)
        2. 將原始數(shù)據(jù)交給子類處理(音頻和視頻根據(jù)自己的需求處理)
        3. 通過 avcodec_send_frame 將數(shù)據(jù)發(fā)送到編碼器編碼
        4. 將編碼好的數(shù)據(jù)抽取出來

        還有一點(diǎn),既第 5 點(diǎn),重新發(fā)送數(shù)據(jù)。

        需要說明一下這里采取的 雙循環(huán) 編碼邏輯:除了最外層的 while(tue) 循環(huán)以外,里面還有一個(gè) while (m_src_frames.size() > 0) 循環(huán)。

        在緩沖隊(duì)列有數(shù)據(jù),并且 FFmpeg 內(nèi)部編碼隊(duì)列未滿 的情況下,會(huì)不斷地往 FFmpeg 發(fā)送數(shù)據(jù),直到發(fā)現(xiàn) FFmpeg 編碼返回 AVERROR(EAGAIN) ,則說明內(nèi)部隊(duì)列已滿,需要先將編碼的數(shù)據(jù)抽取出來,也就是調(diào)用 DrainEncode() 方法。

        還有一點(diǎn)需要說明的是:如何判讀所有數(shù)據(jù)已經(jīng)都發(fā)送給編碼器了?

        這里通過 one_frame->line_size 來判斷。

        當(dāng)監(jiān)聽到解碼器通知解碼完成的時(shí)候,則把一個(gè)空的幀數(shù)據(jù) OneFrameline_size 設(shè)置為 0,并壓入緩沖隊(duì)列。

        BaseEncoder 拿到這個(gè)空數(shù)據(jù)幀時(shí),往 FFmpegavcodec_send_frame() 發(fā)送一個(gè) NULL 數(shù)據(jù),則 FFmpeg 會(huì)自動(dòng)結(jié)束編碼。

        具體請(qǐng)看以下代碼:

        //?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,使編碼器進(jìn)入結(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):?//編碼還未完成,待會(huì)再來
        ????????????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;
        }

        同樣是一個(gè) 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 的返回值,那么當(dāng)其為 0 時(shí),循環(huán)獲取下一幀數(shù)據(jù),直到返回值為 AVERROR(EAGAIN)AVERROR_EOF,既:沒有數(shù)據(jù) 或 編碼結(jié)束。

        如此,通過以上幾個(gè)循環(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();
        ????}
        }

        封裝視頻編碼器

        視頻編碼器繼承自上面定義好的基礎(chǔ)編碼器 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;

        ????//?目標(biāo)視頻寬高
        ????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);

        };

        具體實(shí)現(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ù)目標(biāo)輸出視頻的寬高,原格式(OpenGL輸出的RGBA數(shù)據(jù))/目標(biāo)格式(YUV),初始化格式轉(zhuǎn)換器,這個(gè)與解碼剛好是相反的過程。

        2. 編碼參數(shù)初始化:

        2.1 初始化上下文和子類內(nèi)部數(shù)據(jù),主要時(shí)配置編碼視頻的 寬高、碼率、幀率、時(shí)間基 等。

        還有一個(gè)比較重要的參數(shù)就是 qminqmax,其值范圍為 [0~51],用于配置編碼畫面質(zhì)量,值越大,畫面質(zhì)量越低,視頻文件越小??梢愿约旱男枨笈渲?。

        還有就是 InitYUVFrame() 申請(qǐng)轉(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個(gè)單位
        ????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);

        ????//這是量化范圍設(shè)定,其值范圍為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()?{
        ????//設(shè)置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ù)解碼器信息,寫入對(duì)應(yīng)的 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ù),才能送進(jìn)編碼器編碼。


        //?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)碼出錯(cuò)");
        ????????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ù)配置有所不同,直接來看實(shí)現(xiàn)就好。

        常規(guī)的音頻參數(shù)配置:比特率,編碼格式,通道數(shù)量等

        重點(diǎn)看下 InitFrame() 方法,這里需要通過通道數(shù)、編碼格式等,借助 av_samples_get_buffer_size() 方法,計(jì)算用來保存目標(biāo)幀數(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ù)復(fù)制到 m_frame 申請(qǐng)的內(nèi)存中,并返回給 父類 送到編碼器編碼。

        四、獲取 OpenGL 渲染的視頻數(shù)據(jù)

        我們知道,視頻數(shù)據(jù)經(jīng)過 OpenGL 編輯以后,是無法直接送到編碼器進(jìn)行編碼的,需要通過 OpenGLglReadPixels 方法來獲取。

        下面就改造一下原來定義的 OpenGLRender 來實(shí)現(xiàn)。

        完整代碼請(qǐng)查看工程源碼。

        在渲染方法 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);
        ????????}
        ????}
        }

        增加一個(gè)請(qǐng)求方法,用于通知 OpenGLRender 將數(shù)據(jù)輸發(fā)送出來:

        //?OpenGLRender.cpp

        void?OpenGLRender::RequestRgbaData()?{
        ????m_need_output_pixels?=?true;
        }

        原理很簡單,在解碼器解碼一幀數(shù)據(jù)送入 OpenGL 渲染以后,就馬上通知 OpenGLRender 將畫面發(fā)送出來。

        當(dāng)然了,還需要定義一個(gè)接收器:

        //?OpenGLPixelReceiver.h

        class?OpenGLPixelReceiver?{
        public:
        ????virtual?void?ReceivePixel(uint8_t?*rgba)?=?0;
        };

        五、MP4 封裝器

        該部分內(nèi)容基本就是上一篇文章的定義的重打包 FFRepack 工具的重新封裝,這里不再贅述,請(qǐng)查看上一篇文章,或源碼。

        //?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)用

        有了以上工具的定義和封裝,加上之前的解碼器和渲染器,就萬事俱備,只欠東風(fēng)了!

        我們需要將他們整合在一起,串聯(lián)起整個(gè)【解碼--編輯--編碼--寫入MP4】流程。

        定義合成器 Synthesizer。

        初始化

        //?Synthesizer.cpp

        //?這里直接寫死視頻寬高了,?需要根據(jù)自己的需求動(dòng)態(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);
        ????//?設(shè)置離屏渲染畫面寬高
        ????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);
        }

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

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

        2. OpenGL 渲染是離屏渲染,需要設(shè)置渲染尺寸

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

        啟動(dòng)

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

        //?Synthesizer.cpp

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

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

        而將它們粘合起來的,就是解碼器的狀態(tài)回調(diào)方法 DecodeOneFrame()。

        //?Synthesizer.cpp

        bool?Synthesizer::DecodeOneFrame(IDecoder?*decoder,?OneFrame?*frame)?{
        ????if?(decoder?==?m_video_decoder)?{
        ????????//?等待上一幀畫面數(shù)據(jù)壓入編碼緩沖隊(duì)列?
        ????????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;
        }

        當(dāng)接收到解碼器的一幀數(shù)據(jù)后,

        • 如果是音頻數(shù)據(jù),直接將數(shù)據(jù)通過 BaseDecoderPushFrame() 方法壓入隊(duì)列。
        • 如果是視頻數(shù)據(jù),將當(dāng)前幀數(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;
        }

        至此,整個(gè)流程就完整了?。。?/p>

        本系列文章終于完結(jié)啦,總算是把坑填完,為自己撒花~ 哈哈哈~? ??????????????????


        瀏覽 68
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. 免费福利在线观看 | 榴莲香蕉苹果哈密瓜水蜜桃 | 日本天堂在线 | 粉嫩av蜜臀一区二区三区 | 1024人妻一区二区三区 |