Android:MediaCodeC硬編碼解碼視訊,並將視訊幀儲存為圖片檔案
- 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輸入佇列都是通用步驟。
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處理了。步驟如下:
1.使用MediaCodeC的 getOutputImage (int index)
函式,得到一個只讀的Image物件,其包含原始視訊幀資訊。
By:當MediaCodeC配置了輸出Surface時,此值返回null
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的環境搭建和渲染程式碼不再贅述,只是強調幾個點:
releaseOutputBuffer
獲得可用的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() } } 複製程式碼