Andoid MediaCodec 解碼視頻快速取幀

MediaCodec 解碼視頻快速取幀

開發(fā)背景

所以考慮在需要 1s 視頻取 30 幀縮略圖時(shí),采取 MediaCodec 硬解視頻,獲取 YUV 數(shù)據(jù),再使用 libyuv 庫(kù),編碼 YUV 為 ARGB 生成 bitmap 的優(yōu)化方案,該方案輸出一幀 1080p 視頻幀耗時(shí)在 50ms 左右,并且還有優(yōu)化空間

MediaCodec 解碼取幀流程

mediaCodec 解碼流程

image.png

mediaCodec 的使用比較流程化,對(duì)于使用者來(lái)說(shuō),更關(guān)注輸入源與輸出源。

輸入源

使用 MediaExtractor 作為輸入源,首先區(qū)分軌道,選擇視頻軌進(jìn)行操作,然后在解碼線程循環(huán)中,不斷的從 MediaExtractor 中取出視頻 buffer 作為 MediaCodec 的輸入源即可

 mediaExtractor = MediaExtractor()
 mediaExtractor.setDataSource(path)
 var videoFormat: MediaFormat? = null
 for (i in 0..mediaExtractor.trackCount) {
        val mediaFormat = mediaExtractor.getTrackFormat(i)
        if (mediaFormat.getString(MediaFormat.KEY_MIME).contains("video")) {
               mediaExtractor.selectTrack(i)
               videoFormat = mediaFormat
               break
            }
        }
        if (videoFormat == null) {
            throw IllegalStateException("video format is null")
        }
}

讀取數(shù)據(jù)

val sampleSize = mediaExtractor.readSampleData(inputBuffer!!, 0)
val presentationTimeUs = mediaExtractor.sampleTime
mediaExtractor.advance()

輸出源

由于不需要渲染到屏幕上,這里選擇的輸出源是 ImageReader,并且 MediaCodec 使用 ImageReader 會(huì)更高效一些

you should use a Surface for raw video data to improve codec performance. Surface uses native video buffers without mapping or copying them to ByteBuffers; thus, it is much more efficient. You normally cannot access the raw video data when using a Surface, but you can use the ImageReader class to access unsecured decoded (raw) video frames. This may still be more efficient than using ByteBuffers

初始化 ImageLoader

 imageReader = ImageReader.newInstance(
            videoFormat.getInteger(MediaFormat.KEY_WIDTH),
            videoFormat.getInteger(MediaFormat.KEY_HEIGHT),
            ImageFormat.YUV_420_888,
            3)
 imageReaderThread = ImageReaderHandlerThread()
 codec.configure(videoFormat, imageReader.surface, null, 0)
 codec.start()
 imageReader.setOnImageAvailableListener(MyOnImageAvailableListener(path),imageReaderThread.handler)

然后調(diào)用 mediaCodec 的 releaseOutputBuffer 方法,在 ImageReader 回調(diào)之中就能拿到 Image 對(duì)象。

處理輸出得到 Bitmap

從 ImagerReader 的回調(diào)之中,我們能夠得到 一個(gè) ImagerReader 對(duì)象

            img = reader.acquireLatestImage()
            if (img != null) {
                val outputTime = readCount * intervalTime * 1000 * 1000L
                if (img.timestamp >= outputTime) {
                    if (debugLog) {
                        BLog.d(TAG, "start get bitmap $readCount timestamp is " + img.timestamp)
                    }
                    val planes = img.planes
                    if (planes[0].buffer == null) {
                        return
                    }
                    var bitmap: Bitmap? = null
                    val cacheKey = "$path#${readCount * intervalTime}"
                    if (DiskCacheProvider.diskCache.get(cacheKey)?.exists() != true) {
                        bitmap = getBitmapScale(img, rotation)
                        BLog.d(TAG, "write cache by cache key $cacheKey")
                        DiskCacheProvider.diskCache.put(cacheKey, object : IWriter {
                            override fun write(file: File): Boolean {
                                return FileUtil.saveBmpToFile(bitmap, file, Bitmap.CompressFormat.JPEG)
                            }
                        })
                    }
                    bitmap?.let {
                        callback?.invoke(readCount, bitmap)
                    }
                    readCount++
                    if (debugLog) {
                        BLog.d(TAG, "end get bitmap $readCount timestamp is " + img.timestamp + " cache key id " + cacheKey)
                    }
                }
            }

從 ImageReader 中,我們能夠取出 Image 對(duì)象,然后從 Image 對(duì)象中取得 YUV 數(shù)據(jù)的 三個(gè)分量,然后通過(guò) libyuv 庫(kù)將 yuv 數(shù)據(jù)轉(zhuǎn)換為 Bitmap 對(duì)象,寫入 LruDiskCache中。

MediaCodec 使用中遇到的問(wèn)題

整個(gè)方案的流程應(yīng)該是比較清晰簡(jiǎn)單的,但在實(shí)際做的過(guò)程中,遇到了很多阻塞的問(wèn)題,在解決這些問(wèn)題的過(guò)程中,才能更深入的了解到 MediaCodec 解碼與 YUV 數(shù)據(jù)處理。

  1. 使用 mediaExtractor.seekTo() 定位需要取幀時(shí)間戳的輸入源,發(fā)現(xiàn)會(huì)有很多的重復(fù)幀,并且,幀和預(yù)覽畫面對(duì)不上 (seekTo 是以關(guān)鍵幀為基準(zhǔn)的,當(dāng)視頻關(guān)鍵幀間隔較遠(yuǎn)的時(shí)候,會(huì)出現(xiàn)這樣的情況)
  2. 使用 mediaExtractor.advance() 與 mediaExtractor.seekTo() 配合取幀,會(huì)發(fā)現(xiàn)有些幀取出來(lái)時(shí)間特別長(zhǎng),不流暢 (同樣是視頻關(guān)鍵幀間隔較遠(yuǎn)的時(shí)候,會(huì)出現(xiàn)這樣的情況)
  3. 發(fā)現(xiàn)上述兩個(gè)問(wèn)題應(yīng)該是和關(guān)鍵幀有關(guān),突發(fā)奇想,使用 mediaExtractor.getSampleFlags() 來(lái)判斷幀是不是關(guān)鍵幀,把所有的關(guān)鍵幀與需要的時(shí)間戳的幀都扔進(jìn)解碼器,這樣確實(shí)效率高了很多,但也沒(méi)有得到正確的結(jié)果,很多幀是花的。(解碼不止需要關(guān)鍵幀,視頻幀分為)

這三個(gè)問(wèn)題都是由于對(duì)視頻的幀間編碼不夠了解導(dǎo)致的,向增輝學(xué)習(xí)了很多下,然后深入了解了一下幀間編碼,與解碼原理之后,換了一個(gè)方案,就解決了這幾個(gè)問(wèn)題。之后又遇到了新的問(wèn)題。

最終方案:關(guān)鍵是關(guān)鍵幀的間隔,如果視頻源比較可靠,關(guān)鍵幀間隔比較小,并且取幀的間隔比較大,可以直接seek到目標(biāo)時(shí)間取幀。

我們因?yàn)橐曨l源不確定,而且取幀間隔特別小,這樣的話會(huì)遇到上述的問(wèn)題,所以我就把所有的幀全都扔給解碼器,然后在輸出短,通過(guò) Image 的 pts 去過(guò)濾我要的幀,因?yàn)榻獯a一幀的耗時(shí)比較小,編碼 yuv 到 bitmap 的耗時(shí)可以省掉,性能還可以接受,但這個(gè)應(yīng)該還是可以優(yōu)化的點(diǎn)

  1. 在一些機(jī)型上,生成的 bitmap 色彩值不對(duì),有虛影。(不同機(jī)型上 YUV 格式不同)
  2. libyuv 轉(zhuǎn)換 yuv to bitmap 效率不高,耗時(shí)很大。 (libyuv 未開啟 neno 指令集優(yōu)化)

這兩個(gè)問(wèn)題,主要是對(duì) YUV 數(shù)據(jù)格式不太了解,對(duì) libyuv 的使用不夠熟悉導(dǎo)致,在向老敏學(xué)習(xí)了很多下,然后深入了解了 YUV 格式與 libyuv 的使用,解決了這兩個(gè)問(wèn)題。

幀格式

I frame :幀內(nèi)編碼幀 又稱 intra picture,I 幀通常是每個(gè) GOP(MPEG 所使用的一種視頻壓縮技術(shù))的第一個(gè)幀,經(jīng)過(guò)適度地壓縮,做為隨機(jī)訪問(wèn)的參考點(diǎn),可以當(dāng)成圖象。I幀可以看成是一個(gè)圖像經(jīng)過(guò)壓縮后的產(chǎn)物。

P frame: 前向預(yù)測(cè)編碼幀 又稱 predictive-frame,通過(guò)充分將低于圖像序列中前面已編碼幀的時(shí)間冗余信息來(lái)壓縮傳輸數(shù)據(jù)量的編碼圖像,也叫預(yù)測(cè)幀;

B frame: 雙向預(yù)測(cè)內(nèi)插編碼幀 又稱bi-directional interpolated prediction frame,既考慮與源圖像序列前面已編碼幀,也顧及源圖像序列后面已編碼幀之間的時(shí)間冗余信息來(lái)壓縮傳輸數(shù)據(jù)量的編碼圖像,也叫雙向預(yù)測(cè)幀;

兩個(gè)I frame之間形成一個(gè)GOP,在x264中同時(shí)可以通過(guò)參數(shù)來(lái)設(shè)定bf的大小,即:I 和p或者兩個(gè)P之間B的數(shù)量。

  1. I 幀自身可以通過(guò)視頻解壓算法解壓成一張單獨(dú)的完整視頻畫面
  2. P 幀需要參考前面一個(gè) I 幀或者 P 幀來(lái)解碼
  3. B 幀需要參考前一個(gè) I 幀 或者 P 幀,以及后面一個(gè) P 幀來(lái)解碼

PTS:Presentation Time Stamp。PTS 顯示時(shí)間戳

DTS:Decode Time Stamp。DTS 解碼時(shí)間戳。

image.png

YUV 格式與 Image

Image類在API 19中引入,Image作為相機(jī)得到的原始幀數(shù)據(jù)的載體(Camera 2);硬件編解碼的 MediaCodec類 加入了對(duì)Image和Image的封裝ImageReader的全面支持??梢灶A(yù)見,Image將會(huì)用來(lái)統(tǒng)一Android內(nèi)部混亂的中間圖片數(shù)據(jù)(這里中間圖片數(shù)據(jù)指如各式Y(jié)UV格式數(shù)據(jù),在處理過(guò)程中產(chǎn)生和銷毀)管理。

每個(gè)Image當(dāng)然有自己的格式,這個(gè)格式由ImageFormat確定。對(duì)于YUV420,ImageFormat在API 21中新加入了YUV_420_888類型,其表示YUV420格式的集合,888表示Y、U、V分量中每個(gè)顏色占8bit。s

YUV420格式分為 YUV420P 和 YUV420SP兩種。
其中YUV420P格式,分為 I420 和 YV12 兩種,YUV420SP格式分為 NV12 和 NV21 兩種。他們的存儲(chǔ)格式,區(qū)別是 Planar 的 uv 分量是平面型的,SemiPlanar 的 uv 分量是交織的。

  1. I420: YYYYYYYY UUVV => YUV420P (Android 格式)
  2. YV12: YYYYYYYY VVUU => YUV420P
  3. NV12: YYYYYYYY UVUV => YUV420SP
  4. NV21: YYYYYYYY VUVU => YUV420SP (Android 格式)

Image 中的 Y、U和V三個(gè)分量的數(shù)據(jù)分別保存在三個(gè)Plane類中,可以通過(guò)getPlanes()得到。Plane實(shí)際是對(duì)ByteBuffer的封裝。Image保證了plane #0一定是Y,#1一定是U,#2一定是V。且對(duì)于plane #0,Y分量數(shù)據(jù)一定是連續(xù)存儲(chǔ)的,中間不會(huì)有U或V數(shù)據(jù)穿插,也就是說(shuō)我們一定能夠一次性得到所有Y分量的值。

接下來(lái)看看U和V分量,我們考慮其中的兩類格式:Planar,SemiPlanar。

Planar 下U和V分量是分開存放的,所以我們也應(yīng)當(dāng)能夠一次性從plane #1和plane #2中獲得所有的U和V分量值,事實(shí)也是如此。

而SemiPlanar,此格式下U和V分量交叉存儲(chǔ),Image 并沒(méi)有為我們將U和V分量分離出來(lái)。

所以,看到這里,對(duì)于開發(fā)過(guò)程中遇到的問(wèn)題已經(jīng)有了答案,在一些機(jī)型上,生成的 bitmap 色彩值不對(duì),有虛影。是因?yàn)?,一些機(jī)型上,解碼獲得的 YUV 數(shù)據(jù)是交織的,也就是 NV21 格式的 YUV 數(shù)據(jù),而在使用libyuv 對(duì) yuv 數(shù)據(jù)進(jìn)行處理獲得 bitmap 對(duì)象的操作時(shí),由于認(rèn)為 Image 對(duì)象會(huì)完整的分離 uv 分量,所以并沒(méi)有考慮到這個(gè)問(wèn)題,導(dǎo)致對(duì) NV21 的數(shù)據(jù)格式不能正確處理,導(dǎo)致了色彩值不對(duì),并且有虛影。

優(yōu)化方向

由于項(xiàng)目時(shí)間比較趕,留下了幾個(gè)可以優(yōu)化的點(diǎn):

  1. 不同機(jī)型支持的 MediaCodec 實(shí)例數(shù)量不一樣,目前是把解碼器做成了單例,然后把需要解碼的視頻做成 List 傳入,串行解碼,后續(xù)可以嘗試判斷不同機(jī)型支持的實(shí)例數(shù)量,并行解碼。
  2. 使用 libyuv 庫(kù) 轉(zhuǎn)換 YUV 到 bitmap 過(guò)程中,先把 NV21 格式轉(zhuǎn)成了 I420,然后再進(jìn)行了縮放、旋轉(zhuǎn)的操作,可以更改接口,直接先用 NV21 縮放,然后再轉(zhuǎn)換,效率上能夠得到提升
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容