系列文章目录
- Android MediaCodec 简明教程(一):使用 MediaCodecList 查询 Codec 信息,并创建 MediaCodec 编解码器
- Android MediaCodec 简明教程(二):使用 MediaCodecInfo.CodecCapabilities 查询 Codec 支持的宽高,颜色空间等能力
- Android MediaCodec 简明教程(三):详解如何在同步与异步模式下,使用MediaCodec将视频解码到ByteBuffers,并在ImageView上展示
- Android MediaCodec 简明教程(四):使用 MediaCodec 将视频解码到 Surface,并使用 SurfaceView 播放视频
- Android MediaCodec 简明教程(五):使用 MediaCodec 编码 ByteBuffer 数据,并保存为 MP4 文件
- Android MediaCodec 简明教程(六):使用 EGL 和 OpenGL 绘制图像到 Surface 上,并通过 MediaCodec 编码 Surface 数据,并保存到 MP4 文件
- Android MediaCodec 简明教程(七):使用 MediaCodec 解码到 OES 纹理上
前言
在之前的教程中,我们已经学习了如何使用 MediaCodec 解码视频到 OES 纹理。在这篇文章中,我们将进一步探讨如何使用 OpenGL ES 将彩色图像转换为灰色图像,并在 GLSurfaceView 上显示。
本文所有代码你可以在 DecodeEditPlay.kt 中找到
数据流
上图描述了视频数据的生产-消费链路,其中生产者和消费者用不同颜色进行标识。
- MediaCodec 解码器将视频数据写入 Surface 的 Buffer 中;SurfaceTexture 作为消费者获取到 Buffer 后,将视频数据绘制到 OES 纹理上。
- 接着,在 GL 线程下,使用 OpenGL ES API 将 OES 纹理绘制到 Surface 上;SurfaceFinger 作为消费者从 GLSurfaceView 的 Buffer 中获取视频,并最终呈现在屏幕上。
Show me the code
GLSurfaceView
关于 GLSurfaceView 的使用本文就不再多说,网上有很多参考资料了,本人写过相关的博客有: LearnOpenGL - Android OpenGL ES 3.0 绘制三角形。简单来说:
- 你需要写一个 Render,它实现了 GLSurfaceView.Renderer 的三个接口:
onSurfaceCreated
、onSurfaceChanged
和onDrawFrame
- GLSurfaceView 会创建一个 GL 线程,并在该线程中去回调 GLSurfaceView.Renderer 三个接口
onDrawFrame
方法中,你可以调用 OpenGL ES 的绘制接口来实现画面的更新
看下我们的 Render 的逻辑:
inner class MyGLSurfaceRender : GLSurfaceView.Renderer {
override fun onSurfaceCreated(
gl: GL10?,
config: javax.microedition.khronos.egl.EGLConfig?
) {
mTextureRenderer = TextureRenderer2()
Matrix.setIdentityM(texMatrix, 0)
Thread {
decodeSync()
}.start()
}
override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
}
override fun onDrawFrame(gl: GL10?) {
Log.d(TAG, "onDrawFrame")
if(frameAvailable){
mSurfaceTexture?.updateTexImage()
mSurfaceTexture?.getTransformMatrix(texMatrix)
}
// draw oes texture to screen
mTextureRenderer?.draw(width, height, texMatrix, getMvp())
}
}
-
onSurfaceCreated
方法:当Surface被创建时,这个方法会被调用。在这个方法中,它创建了一个TextureRenderer2对象,并设置了一个单位矩阵。然后,它在一个新的线程中调用了decodeSync
方法。- TextureRenderer2 对象作用是将 OES 纹理绘制到 Surface 上。
- 起了一个新的线程,是为了不阻塞当前的 GL 回调线程。
-
onSurfaceChanged
方法:当Surface的大小发生改变时,这个方法会被调用。在这个方法中,它没有做任何事情。 -
onDrawFrame
方法:这个方法在每一帧需要被绘制时被调用。首先,它检查是否有新的帧可用,如果有,它会更新SurfaceTexture的图像,并获取变换矩阵。然后,它调用TextureRenderer的draw方法,将OES纹理绘制到屏幕上。
decodeSync
接下来,看 decodeSync
方法逻辑:
private fun decodeSync() {
mSurfaceTexture = SurfaceTexture(mTextureRenderer!!.texId)
mSurfaceTexture!!.setOnFrameAvailableListener(this)
mOutputSurface = Surface(mSurfaceTexture)
// create and configure media extractor
val mediaExtractor = MediaExtractor()
resources.openRawResourceFd(R.raw.h264_720p).use {
mediaExtractor.setDataSource(it)
}
val videoTrackIndex = 0
mediaExtractor.selectTrack(videoTrackIndex)
val videoFormat = mediaExtractor.getTrackFormat(videoTrackIndex)
width = videoFormat.getInteger(MediaFormat.KEY_WIDTH)
height = videoFormat.getInteger(MediaFormat.KEY_HEIGHT)
// create and configure media codec
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
val codecName = codecList.findDecoderForFormat(videoFormat)
val codec = MediaCodec.createByCodecName(codecName)
val maxInputSize = videoFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)
val inputBuffer = ByteBuffer.allocate(maxInputSize)
val bufferInfo = MediaCodec.BufferInfo()
val inputEnd = AtomicBoolean(false)
val outputEnd = AtomicBoolean(false)
// set codec callback in async mode
codec.setCallback(object : MediaCodec.Callback() {
override fun onInputBufferAvailable(codec: MediaCodec, inputBufferId: Int) {
Log.d(TAG, "onInputBufferAvailable")
val isExtractorReadEnd =
getInputBufferFromExtractor(mediaExtractor, inputBuffer, bufferInfo)
if (isExtractorReadEnd) {
inputEnd.set(true)
codec.queueInputBuffer(
inputBufferId, 0, 0, 0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM
)
} else {
val codecInputBuffer = codec.getInputBuffer(inputBufferId)
codecInputBuffer!!.put(inputBuffer)
codec.queueInputBuffer(
inputBufferId,
0,
bufferInfo.size,
bufferInfo.presentationTimeUs,
bufferInfo.flags
)
mediaExtractor.advance()
}
}
override fun onOutputBufferAvailable(
codec: MediaCodec,
outputBufferId: Int,
info: MediaCodec.BufferInfo
) {
if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
outputEnd.set(true)
}
if (info.size > 0) {
Log.i(TAG, "onOutputBufferAvailable")
codec.releaseOutputBuffer(outputBufferId, true)
// sleep for 30ms to simulate 30fps
Thread.sleep(30)
}
}
override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {
e.printStackTrace()
}
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
Log.e(TAG, "onOutputFormatChanged")
}
})
// configure with surface
codec.configure(videoFormat, mOutputSurface, null, 0)
// start decoding
codec.start()
// wait for processing to complete
while (!outputEnd.get()) {
Thread.sleep(10)
}
mediaExtractor.release()
codec.stop()
codec.release()
mOutputSurface!!.release()
mOutputSurface = null
mSurfaceTexture!!.release()
mSurfaceTexture = null
-
decodeSync方法首先创建了一个SurfaceTexture对象,并将其与TextureRenderer的纹理ID关联。然后,它设置了一个帧可用监听器,并使用SurfaceTexture创建了一个Surface对象。
-
接下来,它创建并配置了一个MediaExtractor对象,用于从资源文件中提取视频数据。它选择了视频轨道,并获取了视频格式,包括视频的宽度和高度。
-
然后,它创建并配置了一个MediaCodec对象,用于解码视频数据。它获取了视频格式的最大输入大小,并分配了一个ByteBuffer用于存储输入数据。它还创建了一个BufferInfo对象,用于存储关于输入数据的信息。然后,它设置了一个回调,用于处理输入缓冲区可用、输出缓冲区可用、错误和输出格式改变等事件。
-
在回调的onInputBufferAvailable方法中,它从MediaExtractor中获取输入数据,并将其放入输入缓冲区。然后,它将输入缓冲区的数据排入MediaCodec的输入队列。
-
在回调的onOutputBufferAvailable方法中,它检查输出数据是否已经结束,如果结束,它将设置outputEnd标志。如果输出数据的大小大于0,它将释放输出缓冲区,并使其在Surface上可见。然后,它将线程休眠30毫秒,以模拟30帧每秒的帧率。
-
在所有的输出数据都已经处理完毕后,它将释放MediaExtractor和MediaCodec对象,并释放Surface和SurfaceTexture对象。
线程模型
梳理下上述代码的线程模型,其中重要的线程有:
- GLSurfaceView 创建的 GL 线程,它负责调用 Render 的三个回调方法,其中 onDrawFrame 负责绘制图像
- MediaCodec 异步的回调线程,负责调用 onOutputBufferAvailable 和通知 onFrameAvailable 回调方法
理论上,顺利解码一帧数据后,也就是 onFrameAvailable 被调用后,GL 线程 onDrawFrame 方法中调用 updateTexImage
来更新 OES 中的数据,然后使用 TextureRenderer2.draw
方法将 OES 纹理绘制到 Surface 上。实际上,在现有的代码框架下按这个逻辑来实现的,你会发现画面会一卡一卡的。在我们的实现代码中,取了个巧,onFrameAvailable
被调用后 frameAvailable
被设置为 true,而接下来 frameAvailable
将一直都是 true,也就是说 onDrawFrame
方法中,每次都会更新 OES 纹理信息。这样一来画面会流畅很多。