使用 MediaCodec 將圖片集編碼為視頻
使用 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í)點(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, null, null, 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(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()
在提要里面也提到了,編碼項(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)資料和源碼
推薦:
全網(wǎng)最全的 Android 音視頻和 OpenGL ES 干貨,都在這了
所有你想要的圖片轉(zhuǎn)場(chǎng)效果,都在這了
面試官:如何利用 Shader 實(shí)現(xiàn) RGBA 到 NV21 圖像格式轉(zhuǎn)換?
