好久不見,AiLo肥來了! 原文地址
原創文章,轉載請聯絡作者
醉拍春衫惜舊香,天將離恨惱疏狂。 年年陌上生秋草,日日樓中到夕陽。
目的
- 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的環境搭建和渲染程式碼不再贅述,只是強調幾個點:
- 渲染紋理的執行緒一定要和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
解碼來作為對比。
結語
以上
原創不易,大家走過路過看的開心,可以適當給個一毛兩毛聊表心意