使用MediaCodeC將圖片集編碼爲視頻

原文地址 原創文章,轉載請聯繫做者java

綠生鶯啼春正濃,釵頭青杏小,綠成叢。 玉船風動酒鱗紅。歌聲咽,相見幾時重?android

MediaLearn

歡迎你們關注個人項目MediaLearn,這是一個以學習分享音視頻知識爲目的創建的項目,目前僅侷限於Android平臺,後續會逐漸擴展。
對音視頻領域知識感興趣的朋友,歡迎一塊兒來學習!!!git

提要

這是MediaCodeC系列的第三章,主題是如何使用MediaCodeC將圖片集編碼爲視頻文件。在Android多媒體的處理上,MediaCodeC是一套很是有用的API。這次實驗中,所使用的圖片集正是MediaCodeC硬解碼視頻,並將視頻幀存儲爲圖片文件文章中,對視頻解碼出來的圖片文件集,總共332張圖片幀。
如果對MediaCodeC視頻解碼感興趣的話,也能夠瀏覽以前的文章:MediaCodeC解碼視頻指定幀,迅捷、精確github

核心流程

MediaCodeC的常規工做流程是:拿到可用輸入隊列,填充數據;拿到可用輸出隊列,取出數據,如此往復直至結束。在通常狀況下,填充和取出兩個動做並非即時的,也就是說並非壓入一幀數據,就能拿出一幀數據。固然,除了編碼的視頻每一幀都是關鍵幀的狀況下。
通常狀況下,輸入和輸出都使用buffer的代碼寫法以下:緩存

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

本篇文章的編碼核心流程,和以上代碼相差很少。只是將輸入Buffer替換成了Surface,使用Surface代替InputBuffer來實現數據的填充。bash

爲何使用Surface

MediaCodeC官方文檔裏有一段關於Data Type的描述:app

CodeC接受三種類型的數據,壓縮數據(compressed data)、原始音頻數據(raw audio data)以及原始視頻數據(raw video data)。這三種數據都能被加工爲ByteBuffer。可是對於原始視頻數據,應該使用Surface去提高CodeC的性能。ide

在本次項目中,使用的是MediaCodeCcreateInputSurface函數創造出Surface,搭配OpenGL實現Surface數據輸入。
這裏我畫了一張簡單的工做流程圖:函數

總體流程上其實和普通的MediaCodeC工做流程差很少,只不過是將輸入源由Buffer換成了Surface。

知識點

在代碼中,MediaCodeC只負責數據的傳輸,而生成MP4文件主要靠的類是MediaMuxer。總體上,項目涉及到的主要API有:oop

  • MediaCodeC,圖片編碼爲幀數據
  • MediaMuxer,幀數據編碼爲Mp4文件
  • OpenGL,負責將圖片繪製到Surface

接下來,我將會按照流程工做順序,詳解各個步驟:

流程詳解

在詳解流程前,有一點要注意的是,工做流程中全部環節都必須處在同一線程。

配置

首先,啓動子線程。配置MediaCodeC:

var codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)
// mediaFormat配置顏色格式、比特率、幀率、關鍵幀間隔
// 顏色格式默認爲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, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
var inputSurface = codec.createInputSurface()
codec.start()
複製代碼

將編碼器配置好以後,接下來配置OpenGL的EGL環境以及GPU Program。因爲OpenGL涉及到比較多的知識,在這裏便再也不贅述。視頻編碼項目中,爲方便使用,我將OpenGL環境搭建以及GPU program搭建封裝在了GLEncodeCore類中,感興趣的能夠看一下。
EGL環境在初始化時,能夠選擇兩種和設備鏈接的方式,一種是eglCreatePbufferSurface;另外一種是eglCreateWindowSurface,建立一個可實際顯示的windowSurface,須要傳一個Surface參數,毫無疑問選擇這個函數。

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

fun buildEGLSurface(surface: Surface) {
        // 構建EGL環境
        eglEnv.setUpEnv().buildWindowSurface(surface)
        // GPU program構建
        encodeProgram.build()
}
複製代碼

圖片數據傳入,並開始編碼

在各類API配置好以後,開啓一個循環,將File文件讀取的Bitmap傳入編碼。

val videoEncoder = VideoEncoder(640, 480, 1800000, 24)
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()
複製代碼

在提要裏面也提到了,編碼項目使用的圖片集是以前MediaCodeC硬解碼視頻,並將視頻幀存儲爲圖片文件中的視頻文件解碼出來的,332張圖片。
循環代碼中,咱們逐次將圖片Bitmap傳入drainFrame(...)函數,用於編碼。當全部幀編碼完成後,使用drainEnd函數通知編碼器編碼完成。

視頻幀編碼

接着咱們再來看drameFrame(...)函數中的具體實現。

/**
     *
     * @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拿到輸出隊列數據,使用MediaMuxer編碼爲
        Mp4文件
    }
複製代碼

首先使用OpenGL將Bitmap繪製紋理上,將數據傳輸到Surface上,而且須要將這個Bitmap所表明的時間戳傳入。在傳入數據後使用drainCoder函數,從MediaCodeC讀取輸出數據,使用MediaMuxer編碼爲Mp4視頻文件。drainCoder函數具體實現以下:

loopOut@ while (true) {
        //  獲取可用的輸出緩存隊列
        val outputBufferId = dequeueOutputBuffer(bufferInfo, defTimeOut)
        Log.d("handleOutputBuffer", "output buffer id : $outputBufferId ")
        if (outputBufferId == MediaCodec.INFO_TRY_AGAIN_LATER) {
            if (needEnd) {
                // 輸出無響應
                break@loopOut
            }
        } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
            // 輸出數據格式改變,在這裏啓動mediaMuxer
        } else if (outputBufferId >= 0) {
            // 拿到相應的輸出數據
            if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
                break@loopOut
            }
        }
    }
複製代碼

就像以前提到過的,並非壓入一幀數據就能即時獲得一幀數據。在使用OpenGL將Bitmap繪製到紋理上,並傳到Surface以後。要想獲得輸出數據,必須在一個無限循環的代碼中,去拿MediaCodeC輸出數據。
也就是在這裏的代碼中,當輸出數據格式改變時,爲MediaMuxer加上視頻軌,並啓動。

trackIndex = mediaMuxer!!.addTrack(codec.outputFormat)
 mediaMuxer!!.start()
複製代碼

總體上的工做流程就是以上這些代碼了,傳入一幀數據到Surface-->MediaCodeC循環拿輸出數據--> MediaMuxer寫入Mp4視頻文件。
固然,後兩步的概念已經相對比較清晰,只有第一步的實現是一個難點,也是當時比較困擾個人一點。接下來咱們將會詳解,如何將一個Bitmap經過OpenGL把數據傳輸到Surface上。

Bitmap --> Surface

項目中,將Bitmap數據傳輸到Surface上,主要靠這一段代碼:

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

其中encodeProgram是顯卡繪製程序,它內部會生成一個紋理,而後將Bitmap繪製到紋理上。此時這個紋理就表明了這張圖片,再將紋理繪製到窗口上。
以後,使用EGL的swapBuffer提交當前渲染結果,在提交以前,使用setPresentationTime提交當前幀表明的時間戳。

更加具體的代碼實現,都在個人Github項目中。GLEncodeCore以及EncodeProgram GPU Program還有EGL 環境構建

結語

此處有項目地址,點擊傳送

相關文章
相關標籤/搜索