<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>

          使用 MediaCodec 將圖片集編碼為視頻

          共 10107字,需瀏覽 21分鐘

           ·

          2023-11-01 10:16

          使用 MediaCodec 將圖片集編碼為視頻

          提要

          這是MediaCodeC系列的第三章,主題是如何使用MediaCodeC將圖片集編碼為視頻文件。在Android多媒體的處理上,MediaCodeC是一套非常有用的API。

          此次實(shí)驗(yàn)中,所使用的圖片集正是MediaCodeC硬解碼視頻,并將視頻幀存儲(chǔ)為圖片文件文章中,對(duì)視頻解碼出來的圖片文件集,總共332張圖片幀。  

          若是對(duì)MediaCodeC視頻解碼感興趣的話,也可以瀏覽之前的文章:MediaCodeC解碼視頻指定幀,迅捷、精確

          核心流程

          MediaCodeC的常規(guī)工作流程是:拿到可用輸入隊(duì)列,填充數(shù)據(jù);拿到可用輸出隊(duì)列,取出數(shù)據(jù),如此往復(fù)直至結(jié)束。在一般情況下,填充和取出兩個(gè)動(dòng)作并不是即時(shí)的,也就是說并不是壓入一幀數(shù)據(jù),就能拿出一幀數(shù)據(jù)。當(dāng)然,除了編碼的視頻每一幀都是關(guān)鍵幀的情況下。

          一般情況下,輸入和輸出都使用buffer的代碼寫法如下:

          for (;;) {
              //拿到可用InputBuffer的id
            int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
            if (inputBufferId >= 0) {
              ByteBuffer inputBuffer = codec.getInputBuffer(…);
              // inputBuffer 填充數(shù)據(jù)
              codec.queueInputBuffer(inputBufferId, …);
            }
            // 查詢是否有可用的OutputBuffer
            int outputBufferId = codec.dequeueOutputBuffer(…);

          本篇文章的編碼核心流程,和以上代碼相差不多。只是將輸入Buffer替換成了Surface,使用Surface代替InputBuffer來實(shí)現(xiàn)數(shù)據(jù)的填充。

          為什么使用Surface

          在MediaCodeC官方文檔里有一段關(guān)于Data Type的描述:

          Codec 接受三種類型的數(shù)據(jù),壓縮數(shù)據(jù)(compressed data)、原始音頻數(shù)據(jù)(raw audio data)以及原始視頻數(shù)據(jù)(raw video data)。這三種數(shù)據(jù)都能被加工為ByteBuffer。但是對(duì)于原始視頻數(shù)據(jù),應(yīng)該使用Surface去提升CodeC的性能。

          在本次項(xiàng)目中,使用的是MediaCodec createInputSurface 函數(shù)創(chuàng)造出Surface,搭配OpenGL實(shí)現(xiàn)Surface數(shù)據(jù)輸入。  

          這里我畫了一張簡(jiǎn)單的工作流程圖:


          整體流程上其實(shí)和普通的MediaCodeC工作流程差不多,只不過是將輸入源由Buffer換成了Surface。

          知識(shí)點(diǎn)

          在代碼中,MediaCodeC只負(fù)責(zé)數(shù)據(jù)的傳輸,而生成MP4文件主要靠的類是MediaMuxer。整體上,項(xiàng)目涉及到的主要API有:

          • MediaCodeC,圖片編碼為幀數(shù)據(jù)

          • MediaMuxer,幀數(shù)據(jù)編碼為Mp4文件

          • OpenGL,負(fù)責(zé)將圖片繪制到Surface

          接下來,我將會(huì)按照流程工作順序,詳解各個(gè)步驟:

          流程詳解

          在詳解流程前,有一點(diǎn)要注意的是,工作流程中所有環(huán)節(jié)都必須處在同一線程。

          配置

          首先,啟動(dòng)子線程。配置MediaCodeC:

          var codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)
          // mediaFormat配置顏色格式、比特率、幀率、關(guān)鍵幀間隔
          // 顏色格式默認(rèn)為MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
          var mediaFomat = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, size.width, size.height)
                      .apply {
                          setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat)
                          setInteger(MediaFormat.KEY_BIT_RATE, bitRate)
                          setInteger(MediaFormat.KEY_FRAME_RATE, frameRate)
                          setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval)
                      }
          codec.configure(mediaFormat, nullnull, MediaCodec.CONFIGURE_FLAG_ENCODE)
          var inputSurface = codec.createInputSurface()
          codec.start()

          將編碼器配置好之后,接下來配置OpenGL的EGL環(huán)境以及GPU Program。由于OpenGL涉及到比較多的知識(shí),在這里便不再贅述。

          視頻編碼項(xiàng)目中,為方便使用,我將OpenGL環(huán)境搭建以及GPU program搭建封裝在了GLEncodeCore類中,感興趣的可以看一下。  

          EGL環(huán)境在初始化時(shí),可以選擇兩種和設(shè)備連接的方式,一種是eglCreatePbufferSurface;另一種是eglCreateWindowSurface,創(chuàng)建一個(gè)可實(shí)際顯示的windowSurface,需要傳一個(gè)Surface參數(shù),毫無疑問選擇這個(gè)函數(shù)。

          var encodeCore = GLEncodeCore(...)
          encodeCore.buildEGLSurface(inputSurface)

          fun buildEGLSurface(surface: Surface) {
                  // 構(gòu)建EGL環(huán)境
                  eglEnv.setUpEnv().buildWindowSurface(surface)
                  // GPU program構(gòu)建
                  encodeProgram.build()
          }
          圖片數(shù)據(jù)傳入,并開始編碼

          在各種API配置好之后,開啟一個(gè)循環(huán),將File文件讀取的Bitmap傳入編碼。

          val videoEncoder = VideoEncoder(640480180000024)
          videoEncoder.start(Environment.getExternalStorageDirectory().path
                              + "/encodeyazi640${videoEncoder.bitRate}.mp4")
          val file = File(圖片集文件夾地址)
          file.listFiles().forEachIndexed { index, it ->
              BitmapFactory.decodeFile(it.path)?.apply {
                      videoEncoder.drainFrame(this, index)
                  }
          }
          videoEncoder.drainEnd()

          在提要里面也提到了,編碼項(xiàng)目使用的圖片集是之前MediaCodeC硬解碼視頻,并將視頻幀存儲(chǔ)為圖片文件中的視頻文件解碼出來的,332張圖片。  

          循環(huán)代碼中,我們逐次將圖片Bitmap傳入 drainFrame(...) 函數(shù),用于編碼。當(dāng)所有幀編碼完成后,使用 drainEnd 函數(shù)通知編碼器編碼完成。

          視頻幀編碼

          接著我們?cè)賮砜?drameFrame(...) 函數(shù)中的具體實(shí)現(xiàn)。

           /**
               *
               * @b : draw bitmap to texture
               *
               * @presentTime: frame current time
               * */

              fun drainFrame(b: Bitmap, presentTime: Long) {
                  encodeCore.drainFrame(b, presentTime)
                  drainCoder(false)
              }

              fun drainFrame(b: Bitmap, index: Int) {
                  drainFrame(b, index * mediaFormat.perFrameTime * 1000)
              }

              fun drainCoder(...){
                  偽代碼:MediaCodeC拿到輸出隊(duì)列數(shù)據(jù),使用MediaMuxer編碼為
                  Mp4文件
              }

          首先使用OpenGL將Bitmap繪制紋理上,將數(shù)據(jù)傳輸?shù)絊urface上,并且需要將這個(gè)Bitmap所代表的時(shí)間戳傳入。

          在傳入數(shù)據(jù)后使用drainCoder函數(shù),從MediaCodeC讀取輸出數(shù)據(jù),使用MediaMuxer編碼為Mp4視頻文件。drainCoder數(shù)具體實(shí)現(xiàn)如下:

          loopOut@ while (true) {
                  //  獲取可用的輸出緩存隊(duì)列
                  val outputBufferId = dequeueOutputBuffer(bufferInfo, defTimeOut)
                  Log.d("handleOutputBuffer""output buffer id : $outputBufferId ")
                  if (outputBufferId == MediaCodec.INFO_TRY_AGAIN_LATER) {
                      if (needEnd) {
                          // 輸出無響應(yīng)
                          break@loopOut
                      }
                  } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                      // 輸出數(shù)據(jù)格式改變,在這里啟動(dòng)mediaMuxer
                  } else if (outputBufferId >= 0) {
                      // 拿到相應(yīng)的輸出數(shù)據(jù)
                      if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
                          break@loopOut
                      }
                  }
              }


          就像之前提到過的,并不是壓入一幀數(shù)據(jù)就能即時(shí)得到一幀數(shù)據(jù)。在使用OpenGL將Bitmap繪制到紋理上,并傳到Surface之后。要想得到輸出數(shù)據(jù),必須在一個(gè)無限循環(huán)的代碼中,去拿MediaCodeC輸出數(shù)據(jù)。 


          也就是在這里的代碼中,當(dāng)輸出數(shù)據(jù)格式改變時(shí),為MediaMuxer加上視頻軌,并啟動(dòng)。


           trackIndex = mediaMuxer!!.addTrack(codec.outputFormat)
           mediaMuxer!!.start()

          整體上的工作流程就是以上這些代碼了,傳入一幀數(shù)據(jù)到Surface–>MediaCodeC循環(huán)拿輸出數(shù)據(jù)–> MediaMuxer寫入Mp4視頻文件。  

          當(dāng)然,后兩步的概念已經(jīng)相對(duì)比較清晰,只有第一步的實(shí)現(xiàn)是一個(gè)難點(diǎn),也是當(dāng)時(shí)比較困擾我的一點(diǎn)。接下來我們將會(huì)詳解,如何將一個(gè)Bitmap通過OpenGL把數(shù)據(jù)傳輸?shù)絊urface上。

          Bitmap --> Surface

          項(xiàng)目中,將Bitmap數(shù)據(jù)傳輸?shù)絊urface上,主要靠這一段代碼:

          fun drainFrame(b: Bitmap, presentTime: Long) {
                  encodeProgram.renderBitmap(b)
                  // 給渲染的這一幀設(shè)置一個(gè)時(shí)間戳
                  eglEnv.setPresentationTime(presentTime)
                  eglEnv.swapBuffers()
          }


          其中encodeProgram是顯卡繪制程序,它內(nèi)部會(huì)生成一個(gè)紋理,然后將Bitmap繪制到紋理上。此時(shí)這個(gè)紋理就代表了這張圖片,再將紋理繪制到窗口上。 


          之后,使用EGL的swapBuffer提交當(dāng)前渲染結(jié)果,在提交之前,使用setPresentationTime提交當(dāng)前幀代表的時(shí)間戳。

          更加具體的代碼實(shí)現(xiàn),都在我的Github項(xiàng)目中。

          結(jié)語(yǔ)

          此處有項(xiàng)目地址:https://github.com/JadynAi/MediaLearn

          原文鏈接: https://blog.csdn.net/JadynAi/article/details/89847026


          -- END --


          進(jìn)技術(shù)交流群,掃碼添加我的微信:Byte-Flow



          獲取相關(guān)資料和源碼


          推薦:

          Android FFmpeg 實(shí)現(xiàn)帶濾鏡的微信小視頻錄制功能

          全網(wǎng)最全的 Android 音視頻和 OpenGL ES 干貨,都在這了

          一文掌握 YUV 圖像的基本處理

          抖音傳送帶特效是怎么實(shí)現(xiàn)的?

          所有你想要的圖片轉(zhuǎn)場(chǎng)效果,都在這了

          面試官:如何利用 Shader 實(shí)現(xiàn) RGBA 到 NV21 圖像格式轉(zhuǎn)換?

          我用 OpenGL ES 給小姐姐做了幾個(gè)抖音濾鏡


          項(xiàng)目疑難問題解答、大廠內(nèi)部推薦、面試指導(dǎo)、簡(jiǎn)歷指導(dǎo)、代碼指導(dǎo)、offer 選擇建議、學(xué)習(xí)路線規(guī)劃,可以點(diǎn)擊找我一對(duì)一解答。


          瀏覽 814
          點(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>
                  我色五月天 | av手机天堂 | 亚洲精品乱码久久久久久蜜桃91 | 日本中文字幕在线 | 国产a级久久 |