MediaCodeC解碼視頻指定幀,迅捷、精確

原創文章,轉載請聯繫做者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的工做流程,是一個線性任務隊列串行的方式。其工做流程圖以下:

具體流程:

  • 1.當執行getFrame函數時,首先從緩存從獲取這一幀的圖片緩存。
  • 2.若是緩存中沒有這一幀的緩存,那麼首先判斷任務隊列中正在執行的任務是否和此時須要的任務重複,若是不重複,則建立一個DecodeFrame任務加入隊列。
  • 3.任務隊列的任務是在一個特定的子線程內,線性執行。新的任務會被加入隊列尾端,而已有任務則會被提升優先級,移到隊列中index爲1的位置。
  • 四、DecodeFrame獲取到這一幀的Bitmap後,會將這一幀緩存爲內存緩存,並在會在緩存線程內做磁盤緩存,方便二次讀取。

接下來分析一下,實現過程當中的幾個重要的點。

實現過程

  • 如何定位和目標時間戳相近的採樣點
  • 如何使用MediaCodeC獲取視頻特定時間幀
  • 緩存是如何工做,起到的做用有哪些
定位精確幀

精確實際上是一個相對而言的概念,MediaExtractorseekTo函數,有三個可供選擇的標記: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解碼了。首先,使用MediaExtractorseekTo函數定位到目標採樣點。

mediaExtractor.seekTo(time, MediaExtractor.SEEK_TO_PREVIOUS_SYNC)
複製代碼

而後MediaCodeCMediaExtractor讀取的數據壓入輸入隊列,不斷循環,直到拿到想要的目標幀的數據。

/*
* 持續壓入數據,直到拿到目標幀
* */
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

LruCache自不用多說,磁盤緩存使用的是著名的DiskLruCache。緩存在VideoDecoder2中佔有很重要的位置,它有效的提升瞭解碼器二次讀取的效率,從而不用屢次解碼以及使用OpenGL繪製。

以前在Oppo R15的測試機型上,進行了一輪解碼測試。
使用MediaCodeC解碼一幀到到的Bitmap,大概須要100~200ms的時間。
而使用磁盤緩存的話,讀取時間大概在50~60ms徘徊,效率增長了一倍。

在磁盤緩存使用的過程當中,有對DiskLruCache進行二次封裝,內部使用單線程隊列形式。進行磁盤緩存,對外提供了異步和同步兩種方式獲取緩存。能夠直接搭配DiskLruCache使用——DiskCacheAssist.kt

總結

到目前爲止,視頻解碼的部分已經完成。上一篇是對視頻完整解碼並存儲爲圖片文件,MediaCodeC硬解碼視頻,並將視頻幀存儲爲圖片文件,這一篇是解碼指定幀。音視頻相關的知識體系還很大,會繼續學習下去。

結語

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

相關文章
相關標籤/搜索