原文地址 原創文章,轉載請聯絡作者
綠生鶯啼春正濃,釵頭青杏小,綠成叢。 玉船風動酒鱗紅。歌聲咽,相見幾時重?
提要
這是MediaCodeC
系列的第三章,主題是如何使用MediaCodeC將圖片集編碼為視訊檔案。在Android多媒體的處理上,MediaCodeC是一套非常有用的API。此次實驗中,所使用的圖片集正是MediaCodeC硬解碼視訊,並將視訊幀儲存為圖片檔案文章中,對視訊解碼出來的圖片檔案集,總共332張圖片幀。
若是對MediaCodeC
視訊解碼感興趣的話,也可以瀏覽之前的文章:MediaCodeC解碼視訊指定幀,迅捷、精確
核心流程
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來實現資料的填充。
為什麼使用Surface
在MediaCodeC官方文件裡有一段關於Data Type的描述:
CodeC接受三種型別的資料,壓縮資料(compressed data)、原始音訊資料(raw audio data)以及原始視訊資料(raw video data)。這三種資料都能被加工為ByteBuffer。但是對於原始視訊資料,應該使用Surface去提升CodeC的效能。
在本次專案中,使用的是MediaCodeCcreateInputSurface
函式創造出Surface,搭配OpenGL實現Surface資料輸入。
這裡我畫了一張簡單的工作流程圖:
知識點
在程式碼中,MediaCodeC只負責資料的傳輸,而生成MP4檔案主要靠的類是MediaMuxer。整體上,專案涉及到的主要API有:
- 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 環境構建