Android:MediaCodeC硬編碼解碼視頻,並將視頻幀存儲爲圖片文件

很久不見,AiLo肥來了! 原文地址html

原創文章,轉載請聯繫做者java

醉拍春衫惜舊香,天將離恨惱疏狂。 年年陌上生秋草,日日樓中到夕陽。git

目的

  • MediaCodeC搭配MediaExtractor將視頻完整解碼
  • 視頻幀存儲爲JPEG文件
  • 使用兩種方式達成
    • 硬編碼輸出數據二次封裝爲YuvImage,並直接輸出爲JPEG格式文件
    • 硬編碼搭配Surface,用OpenGL封裝爲RGBA數據格式,再利用Bitmap壓縮爲圖片文件
    • 兩者皆能夠調整圖片輸出質量

參考

  • YUV的處理方式,強推你們觀看這篇文章高效率獲得YUV格式幀,絕對整的明明白白
  • OpenGL的處理方式,固然是最出名的BigFlake,硬編碼相關的示例代碼非常詳細

解碼效率分析

  • 參考對象爲一段約爲13.8s,H.264編碼,FPS爲24,72*1280的MPEG-4的視頻文件。鴨鴨戲水視頻
    • 此視頻的視頻幀數爲332
  • 略好點的設備解碼時間稍短一點。但兩種解碼方式的效率對比下來,OpenGl渲染耗費的時間比YUV轉JPEG多。
    • 另:差一點的設備上,這個差值會被提升,約爲一倍多。較好的設備,則小於一倍。

實現過程

對整個視頻的解析,以及壓入MediaCodeC輸入隊列都是通用步驟。github

mediaExtractor.setDataSource(dataSource)
// 查看是否含有視頻軌
val trackIndex = mediaExtractor.selectVideoTrack()
if (trackIndex < 0) {
    throw RuntimeException("this data source not video")
}
mediaExtractor.selectTrack(trackIndex)
      
       
fun MediaExtractor.selectVideoTrack(): Int {
    val numTracks = trackCount
    for (i in 0 until numTracks) {
        val format = getTrackFormat(i)
        val mime = format.getString(MediaFormat.KEY_MIME)
        if (mime.startsWith("video/")) {
            return i
        }
    }
    return -1
}

複製代碼

配置MediaCodeC解碼器,將解碼輸出格式設置爲COLOR_FormatYUV420Flexible,這種模式幾乎全部設備都會支持。
使用OpenGL渲染的話,MediaCodeC要配置一個輸出Surface。使用YUV方式的話,則不須要配置數組

outputSurface = if (isSurface) OutputSurface(mediaFormat.width, mediaFormat.height) else null

        // 指定幀格式COLOR_FormatYUV420Flexible,幾乎全部的解碼器都支持
        if (decoder.codecInfo.getCapabilitiesForType(mediaFormat.mime).isSupportColorFormat(defDecoderColorFormat)) {
            mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, defDecoderColorFormat)
            decoder.configure(mediaFormat, outputSurface?.surface, null, 0)
        } else {
            throw RuntimeException("this mobile not support YUV 420 Color Format")
        }

        val startTime = System.currentTimeMillis()
        Log.d(TAG, "start decode frames")
        isStart = true
        val bufferInfo = MediaCodec.BufferInfo()
        // 是否輸入完畢
        var inputEnd = false
        // 是否輸出完畢
        var outputEnd = false
        decoder.start()
        var outputFrameCount = 0

        while (!outputEnd && isStart) {
            if (!inputEnd) {
                val inputBufferId = decoder.dequeueInputBuffer(DEF_TIME_OUT)
                if (inputBufferId >= 0) {
                    // 得到一個可寫的輸入緩存對象
                    val inputBuffer = decoder.getInputBuffer(inputBufferId)
                    // 使用MediaExtractor讀取數據
                    val sampleSize = videoAnalyze.mediaExtractor.readSampleData(inputBuffer, 0)
                    if (sampleSize < 0) {
                        // 2019/2/8-19:15 沒有數據
                        decoder.queueInputBuffer(inputBufferId, 0, 0, 0L,
                                MediaCodec.BUFFER_FLAG_END_OF_STREAM)
                        inputEnd = true
                    } else {
                        // 將數據壓入到輸入隊列
                        val presentationTimeUs = videoAnalyze.mediaExtractor.sampleTime
                        decoder.queueInputBuffer(inputBufferId, 0,
                                sampleSize, presentationTimeUs, 0)
                        videoAnalyze.mediaExtractor.advance()
                    }
                }
            }
複製代碼

能夠大體畫一個流程圖以下:
緩存

YUV

經過以上通用的步驟後,接下來就是對MediaCodeC的輸出數據做YUV處理了。步驟以下:bash

1.使用MediaCodeC的getOutputImage (int index)函數,獲得一個只讀的Image對象,其包含原始視頻幀信息。app

By:當MediaCodeC配置了輸出Surface時,此值返回nullide

2.將Image獲得的數據封裝到YuvImage中,再使用YuvImage的compressToJpeg方法壓縮爲JPEG文件函數

YuvImage的封裝,官方文檔有這樣一段描述:Currently only ImageFormat.NV21 and ImageFormat.YUY2 are supported。 YuvImage只支持NV21或者YUY2格式,因此還可能須要對Image的原始數據做進一步處理,將其轉換爲NV21的Byte數組

讀取Image信息並封裝爲Byte數組

這次演示的機型,反饋的Image格式以下:

getFormat = 35
getCropRect().width()=720
getCropRect().height()=1280

35表明ImageFormat.YUV_420_888格式。Image的getPlanes會返回一個數組,其中0表明Y,1表明U,2表明V。因爲是420格式,那麼四個Y值共享一對UV份量,比例爲4:1。
代碼以下,參考YUV_420_888編碼Image轉換爲I420和NV21格式byte數組,不過我這裏只保留了NV21格式的轉換

fun Image.getDataByte(): ByteArray {
    val format = format
    if (!isSupportFormat()) {
        throw RuntimeException("image can not support format is $format")
    }
    // 指定了圖片的有效區域,只有這個Rect內的像素纔是有效的
    val rect = cropRect
    val width = rect.width()
    val height = rect.height()
    val planes = planes
    val data = ByteArray(width * height * ImageFormat.getBitsPerPixel(format) / 8)
    val rowData = ByteArray(planes[0].rowStride)

    var channelOffset = 0
    var outputStride = 1
    for (i in 0 until planes.size) {
        when (i) {
            0 -> {
                channelOffset = 0
                outputStride = 1
            }
            1 -> {
                channelOffset = width * height + 1
                outputStride = 2
            }
            2 -> {
                channelOffset = width * height
                outputStride = 2
            }
        }

        // 此時獲得的ByteBuffer的position指向末端
        val buffer = planes[i].buffer
        //  行跨距
        val rowStride = planes[i].rowStride
        // 行內顏色值間隔,真實間隔值爲此值減一
        val pixelStride = planes[i].pixelStride

        val TAG = "getDataByte"

        Log.d(TAG, "planes index is $i")
        Log.d(TAG, "pixelStride $pixelStride")
        Log.d(TAG, "rowStride $rowStride")
        Log.d(TAG, "width $width")
        Log.d(TAG, "height $height")
        Log.d(TAG, "buffer size " + buffer.remaining())

        val shift = if (i == 0) 0 else 1
        val w = width.shr(shift)
        val h = height.shr(shift)
        buffer.position(rowStride * (rect.top.shr(shift)) + pixelStride +
                (rect.left.shr(shift)))
        for (row in 0 until h) {
            var length: Int
            if (pixelStride == 1 && outputStride == 1) {
                length = w
                // 2019/2/11-23:05 buffer有時候遺留的長度,小於length就會報錯
                buffer.getNoException(data, channelOffset, length)
                channelOffset += length
            } else {
                length = (w - 1) * pixelStride + 1
                buffer.getNoException(rowData, 0, length)
                for (col in 0 until w) {
                    data[channelOffset] = rowData[col * pixelStride]
                    channelOffset += outputStride
                }
            }

            if (row < h - 1) {
                buffer.position(buffer.position() + rowStride - length)
            }
        }
    }
    return data
}
複製代碼
最後封裝YuvImage並壓縮爲文件
val rect = image.cropRect
    val yuvImage = YuvImage(image.getDataByte(), ImageFormat.NV21, rect.width(), rect.height(), null)
    yuvImage.compressToJpeg(rect, 100, fileOutputStream)
    fileOutputStream.close()
複製代碼

MediaCodeC配置輸出Surface,使用OpenGL渲染

OpenGL的環境搭建和渲染代碼再也不贅述,只是強調幾個點:

  • 渲染紋理的線程必定要和MediaCodeC配置Surface的線程保持一致
  • 在渲染紋理代碼前,必定要調用MediaCodeC的releaseOutputBuffer函數,將輸出數據及時渲染到輸出Surface上,不然Surface內的紋理將不會收到任何數據
得到可用的RGBA數據,使用Bitmap壓縮爲指定格式文件
fun saveFrame(fileName: String) {
        pixelBuf.rewind()
        GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, pixelBuf)
        var bos: BufferedOutputStream? = null
        try {
            bos = BufferedOutputStream(FileOutputStream(fileName))
            val bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
            pixelBuf.rewind()
            bmp.copyPixelsFromBuffer(pixelBuf)
            bmp.compress(Bitmap.CompressFormat.JPEG, 100, bos)
            bmp.recycle()
        } finally {
            bos?.close()
        }
    }
複製代碼

結果分析

到目前爲止,針對樣例視頻,YUV解碼出來的視頻幀亮度會稍低一點,且圖片邊緣處有細微的失真。OpenGL渲染解碼的視頻幀會明亮一些,放大三四倍邊緣無失真。後續會繼續追蹤這個問題,會使用FFmpeg解碼來做爲對比。

結語

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


以上
原創不易,你們走過路過看的開心,能夠適當給個一毛兩毛聊表心意

相關文章
相關標籤/搜索