<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ā)打怪升級(jí):FFmpeg音視頻編解碼篇】七、Android FFmpeg 視頻編碼

          共 45073字,需瀏覽 91分鐘

           ·

          2021-03-24 16:30












          聲 明

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

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

          教程代碼:【Github傳送門:https://github.com/ChenLittlePing/LearningVideo

          目錄

          一、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)容做好整合。下面通過一張圖做一下簡(jiǎn)要說明:

          模塊

          首先可以關(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)查看上篇文章。總之,由于原視頻和目標(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ù)長(zhǎng)度為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 != 0break;
                  }

                  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,
                                         NULLNULLNULL);
          }

          這里根據(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(00, 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;
          }

          原理很簡(jiǎn)單,在解碼器解碼一幀數(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, NULLNULL, 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(NULL00, AVRational{125}, NULL));
              } else {
                  m_a_encoder->PushFrame(new OneFrame(NULL00, AVRational{125}, 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è)流程就完整了!!!


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

          手機(jī)掃一掃分享

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

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  台湾中文无码 | 黄色电影网站社区视频 | 豆花免费在线视频 | 青青爽视频 | 愉拍 |