原創文章,轉載請聯繫做者java
若待明朝風雨過,人在天涯!春在天涯git
原文地址github
最近在整理硬編碼MediaCodec相關的學習筆記,以及代碼文檔,分享出來以供參考。本人水平有限,項目不免有思慮不當之處,如有問題能夠提Issues
。項目地址傳送門
此篇文章,主要是分享如何用MediaCodeC
解碼視頻指定時間的一幀,回調Bitmap對象。以前還有一篇MediaCodeC硬解碼視頻,並將視頻幀存儲爲圖片文件,主要內容是將視頻完整解碼,並存儲爲JPEG文件,你們感興趣能夠去看一看。緩存
VideoDecoder2
上手簡單直接,首先須要建立一個解碼器對象:bash
val videoDecoder2 = VideoDecoder2(dataSource)
複製代碼
dataSoure就是視頻文件地址框架
解碼器會在對象建立的時候,對視頻文件進行分析,得出時長、幀率等信息。有了解碼器對象後,在須要解碼幀的地方,直接調用函數:異步
videoDecoder2.getFrame(time, { it->
//成功回調,it爲對應幀Bitmap對象
}, {
//失敗回調
})
複製代碼
time 接受一個Float數值,級別爲秒ide
getFrame
函數式一個異步回調,會自動回調到主線程裏來。同時這個函數也沒有過分調用限制。也就是說——,你能夠頻繁調用而不用擔憂出現其餘問題。函數
VideoDecoder2
目前只支持硬編碼解碼,在某些機型或者版本下,可能會出現兼容問題。後續會繼續補上軟解碼的功能模塊。
先來看一下VideoDecoder2
的代碼框架,有哪些類構成,以及這些類起到的做用。 學習
VideoDecoder2
中,
DecodeFrame
承擔着核心任務,由它發起這一幀的解碼工做。獲取了目標幀的YUV數據後;由
GLCore
來將這一幀轉爲Bitmap對象,它內部封裝了
OpenGL環境的搭建,以及配置了
Surface
供給
MediaCodeC
使用。
FrameCache
主要是作着緩存的工做,內部有內存緩存
LruCache以及磁盤緩存
DiskLruCache,由於緩存的存在,很大程度上提升了二次讀取的效率。
VideoDecoder2
的工做流程,是一個線性任務隊列串行的方式。其工做流程圖以下:
getFrame
函數時,首先從緩存從獲取這一幀的圖片緩存。DecodeFrame
任務加入隊列。index爲1
的位置。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 : 每一幀持續時間,微秒
* */
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
讀取的數據壓入輸入隊列,不斷循環,直到拿到想要的目標幀的數據。
/*
* 持續壓入數據,直到拿到目標幀
* */
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()
}
複製代碼
須要注意的是,解碼的時候,並非壓入一幀數據,就能獲得一幀輸出數據的。
常規的作法是,持續不斷向輸入隊列填充幀數據,直到拿到想要的目標幀數據。
緣由仍是由於視頻幀的編碼,並非每一幀都是關鍵幀,有些幀的解碼必須依靠先後幀的信息。
LruCache自不用多說,磁盤緩存使用的是著名的DiskLruCache。緩存在VideoDecoder2
中佔有很重要的位置,它有效的提升瞭解碼器二次讀取的效率,從而不用屢次解碼以及使用OpenGL
繪製。
以前在
Oppo R15
的測試機型上,進行了一輪解碼測試。
使用MediaCodeC
解碼一幀到到的Bitmap,大概須要100~200ms的時間。
而使用磁盤緩存的話,讀取時間大概在50~60ms徘徊,效率增長了一倍。
在磁盤緩存使用的過程當中,有對DiskLruCache進行二次封裝,內部使用單線程隊列形式。進行磁盤緩存,對外提供了異步和同步兩種方式獲取緩存。能夠直接搭配DiskLruCache
使用——DiskCacheAssist.kt
到目前爲止,視頻解碼的部分已經完成。上一篇是對視頻完整解碼並存儲爲圖片文件,MediaCodeC硬解碼視頻,並將視頻幀存儲爲圖片文件,這一篇是解碼指定幀。音視頻相關的知識體系還很大,會繼續學習下去。