很久不见,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
解码来做为对比。
以上
原创不易,你们走过路过看的开心,能够适当给个一毛两毛聊表心意