很久不見,AiLo肥來了! 原文地址html
原創文章,轉載請聯繫做者java
醉拍春衫惜舊香,天將離恨惱疏狂。 年年陌上生秋草,日日樓中到夕陽。git
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()
}
}
}
複製代碼
能夠大體畫一個流程圖以下:
緩存
經過以上通用的步驟後,接下來就是對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格式以下:
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
}
複製代碼
val rect = image.cropRect
val yuvImage = YuvImage(image.getDataByte(), ImageFormat.NV21, rect.width(), rect.height(), null)
yuvImage.compressToJpeg(rect, 100, fileOutputStream)
fileOutputStream.close()
複製代碼
OpenGL的環境搭建和渲染代碼再也不贅述,只是強調幾個點:
releaseOutputBuffer
函數,將輸出數據及時渲染到輸出Surface上,不然Surface內的紋理將不會收到任何數據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
解碼來做爲對比。
以上
原創不易,你們走過路過看的開心,能夠適當給個一毛兩毛聊表心意