提要
最近在整理硬編碼MediaCodec相關的學習筆記,以及代碼文檔,分享出來以供參考。本人水平有限,項目難免有思慮不當之處,若有問題可以提Issues
。項目地址傳送門
此篇文章,主要是分享如何用MediaCodeC
解碼視頻指定時間的一幀,回調Bitmap對象。之前還有一篇MediaCodeC硬解碼視頻,并將視頻幀存儲為圖片文件,主要內容是將視頻完整解碼,并存儲為JPEG文件,大家感興趣可以去看一看。
如何使用
VideoDecoder2
上手簡單直接,首先需要創(chuàng)建一個解碼器對象:
val videoDecoder2 = VideoDecoder2(dataSource)
dataSoure就是視頻文件地址
解碼器會在對象創(chuàng)建的時候,對視頻文件進行分析,得出時長、幀率等信息。有了解碼器對象后,在需要解碼幀的地方,直接調用函數:
videoDecoder2.getFrame(time, { it->
//成功回調,it為對應幀Bitmap對象
}, {
//失敗回調
})
time 接受一個Float數值,級別為秒
getFrame
函數式一個異步回調,會自動回調到主線程里來。同時這個函數也沒有過度調用限制。也就是說——,你可以頻繁調用而不用擔心出現其他問題。
代碼結構、實現過程
代碼結構
VideoDecoder2
目前只支持硬編碼解碼,在某些機型或者版本下,可能會出現兼容問題。后續(xù)會繼續(xù)補上軟解碼的功能模塊。
先來看一下VideoDecoder2
的代碼框架,有哪些類構成,以及這些類起到的作用。
在VideoDecoder2
中,DecodeFrame
承擔著核心任務,由它發(fā)起這一幀的解碼工作。獲取了目標幀的YUV數據后;由GLCore
來將這一幀轉為Bitmap對象,它內部封裝了OpenGL環(huán)境的搭建,以及配置了Surface
供給MediaCodeC
使用。
FrameCache
主要是做著緩存的工作,內部有內存緩存LruCache以及磁盤緩存DiskLruCache,因為緩存的存在,很大程度上提高了二次讀取的效率。
工作流程
VideoDecoder2
的工作流程,是一個線性任務隊列串行的方式。其工作流程圖如下:
具體流程:
1.當執(zhí)行
getFrame
函數時,首先從緩存從獲取這一幀的圖片緩存。2.如果緩存中沒有這一幀的緩存,那么首先判斷任務隊列中正在執(zhí)行的任務是否和此時需要的任務重復,如果不重復,則創(chuàng)建一個
DecodeFrame
任務加入隊列。3.任務隊列的任務是在一個特定的子線程內,線性執(zhí)行。新的任務會被加入隊列尾端,而已有任務則會被提高優(yōu)先級,移到隊列中
index為1
的位置。4、
DecodeFrame
獲取到這一幀的Bitmap后,會將這一幀緩存為內存緩存,并在會在緩存線程內作磁盤緩存,方便二次讀取。
接下來分析一下,實現過程中的幾個重要的點。
實現過程
- 如何定位和目標時間戳相近的采樣點
- 如何使用
MediaCodeC
獲取視頻特定時間幀 - 緩存是如何工作,起到的作用有哪些
定位精確幀
精確其實是一個相對而言的概念,MediaExtractor
的seekTo
函數,有三個可供選擇的標記:SEEK_TO_PREVIOUS_SYNC, SEEK_TO_CLOSEST_SYNC, SEEK_TO_NEXT_SYNC,分別是seek指定幀的上一幀,最近幀和下一幀。
其實,seekTo
并無法每次都準確的跳到指定幀,這個函數只會seek到目標時間的最接近的(CLOSEST)、上一幀(PREVIOUS)和下一幀(NEXT)。因為視頻編碼的關系,解碼器只會從關鍵幀開始解碼,也就是I幀。因為只有I幀才包含完整的信息。而P幀和B幀包含的信息并不完全,只有依靠前后幀的信息才能解碼。所以這里的解決辦法是:先定位到目標時間的上一幀,然后advance
,直到讀取的時間和目標時間的差值最小,或者讀取的時間和目標時間的差值小于幀間隔。
val MediaFormat.fps: Int
get() = try {
getInteger(MediaFormat.KEY_FRAME_RATE)
} catch (e: Exception) {
0
}
/*
*
* return : 每一幀持續(xù)時間,微秒
* */
val perFrameTime by lazy {
1000000L / mediaFormat.fps
}
/*
*
* 查找這個時間點對應的最接近的一幀。
* 這一幀的時間點如果和目標時間相差不到 一幀間隔 就算相近
*
* maxRange:查找范圍
* */
fun getValidSampleTime(time: Long, @IntRange(from = 2) maxRange: Int = 5): Long {
checkExtractor.seekTo(time, MediaExtractor.SEEK_TO_PREVIOUS_SYNC)
var count = 0
var sampleTime = checkExtractor.sampleTime
while (count < maxRange) {
checkExtractor.advance()
val s = checkExtractor.sampleTime
if (s != -1L) {
count++
// 選取和目標時間差值最小的那個
sampleTime = time.minDifferenceValue(sampleTime, s)
if (Math.abs(sampleTime - time) <= perFrameTime) {
//如果這個差值在 一幀間隔 內,即為成功
return sampleTime
}
} else {
count = maxRange
}
}
return sampleTime
}
幀間隔其實就是:1s/幀率
使用MediaCodeC
解碼指定幀
獲取到相對精確的采樣點(幀)后,接下來就是使用MediaCodeC
解碼了。首先,使用MediaExtractor
的seekTo
函數定位到目標采樣點。
mediaExtractor.seekTo(time, MediaExtractor.SEEK_TO_PREVIOUS_SYNC)
然后MediaCodeC
將MediaExtractor
讀取的數據壓入輸入隊列,不斷循環(huán),直到拿到想要的目標幀的數據。
/*
* 持續(xù)壓入數據,直到拿到目標幀
* */
private fun handleFrame(time: Long, info: MediaCodec.BufferInfo, emitter: ObservableEmitter<Bitmap>? = null) {
var outputDone = false
var inputDone = false
videoAnalyze.mediaExtractor.seekTo(time, MediaExtractor.SEEK_TO_PREVIOUS_SYNC)
while (!outputDone) {
if (!inputDone) {
decoder.dequeueValidInputBuffer(DEF_TIME_OUT) { inputBufferId, inputBuffer ->
val sampleSize = videoAnalyze.mediaExtractor.readSampleData(inputBuffer, 0)
if (sampleSize < 0) {
decoder.queueInputBuffer(inputBufferId, 0, 0, 0L,
MediaCodec.BUFFER_FLAG_END_OF_STREAM)
inputDone = true
} else {
// 將數據壓入到輸入隊列
val presentationTimeUs = videoAnalyze.mediaExtractor.sampleTime
Log.d(TAG, "${if (emitter != null) "main time" else "fuck time"} dequeue time is $presentationTimeUs ")
decoder.queueInputBuffer(inputBufferId, 0,
sampleSize, presentationTimeUs, 0)
videoAnalyze.mediaExtractor.advance()
}
}
decoder.disposeOutput(info, DEF_TIME_OUT, {
outputDone = true
}, { id ->
Log.d(TAG, "out time ${info.presentationTimeUs} ")
if (decodeCore.updateTexture(info, id, decoder)) {
if (info.presentationTimeUs == time) {
// 遇到目標時間幀,才生產Bitmap
outputDone = true
val bitmap = decodeCore.generateFrame()
frameCache.cacheFrame(time, bitmap)
emitter?.onNext(bitmap)
}
}
})
}
decoder.flush()
}
需要注意的是,解碼的時候,并不是壓入一幀數據,就能得到一幀輸出數據的。
常規(guī)的做法是,持續(xù)不斷向輸入隊列填充幀數據,直到拿到想要的目標幀數據。
原因還是因為視頻幀的編碼,并不是每一幀都是關鍵幀,有些幀的解碼必須依靠前后幀的信息。
緩存
- LruCache,內存緩存
- DiskLruCache
LruCache自不用多說,磁盤緩存使用的是著名的DiskLruCache。緩存在VideoDecoder2
中占有很重要的位置,它有效的提高了解碼器二次讀取的效率,從而不用多次解碼以及使用OpenGL
繪制。
之前在
Oppo R15
的測試機型上,進行了一輪解碼測試。
使用MediaCodeC
解碼一幀到到的Bitmap,大概需要100~200ms的時間。
而使用磁盤緩存的話,讀取時間大概在50~60ms徘徊,效率增加了一倍。
在磁盤緩存使用的過程中,有對DiskLruCache進行二次封裝,內部使用單線程隊列形式。進行磁盤緩存,對外提供了異步和同步兩種方式獲取緩存。可以直接搭配DiskLruCache
使用——DiskCacheAssist.kt
總結
到目前為止,視頻解碼的部分已經完成。上一篇是對視頻完整解碼并存儲為圖片文件,MediaCodeC硬解碼視頻,并將視頻幀存儲為圖片文件,這一篇是解碼指定幀。音視頻相關的知識體系還很大,會繼續(xù)學習下去。