系列文章目录
- Android MediaCodec 简明教程(一):使用 MediaCodecList 查询 Codec 信息,并创建 MediaCodec 编解码器
- Android MediaCodec 简明教程(二):使用 MediaCodecInfo.CodecCapabilities 查询 Codec 支持的宽高,颜色空间等能力
- Android MediaCodec 简明教程(三):详解如何在同步与异步模式下,使用MediaCodec将视频解码到ByteBuffers,并在ImageView上展示
文章目录
- 系列文章目录
- 前言
- 一、Surface 是什么?
- 二、MediaCodec 解码到 Surface
- 2.1 Surface 从哪来?
- 2. 2 MediaCodec 与 Surface 共享内存,实现零拷贝
- 2.3 数据流动
- 2.4 ReleaseOutputBuffer,控制水流速度
- 2.5 Show me the code
- 参考
前言
在上一个教程 Android MediaCodec 简明教程(三) 中,我们学会了使用 MediaCodec 解码到 ByteBuffers 上,包括同步模式和异步模式。本章将讨论 MediaCodec 解码到 Surface 的相关知识点。Google 推荐使用 Surface 进行编解码操作,这样效率更高。
一、Surface 是什么?
Android Surface 是一个复杂的概念,它涉及到 Android 图形系统,网络已经有很多相关的博文,例如:
- 从整体上看Android图像显示系统
- Android图形系统综述(干货篇)
- 浅谈Android Surface机制
- 深入Android系统(十二)Android图形显示系统-1-显示原理与Surface
Surface 是 Android 图形架构的一部分,它与 SurfaceFlinger、SurfaceView、SurfaceTexture、SurfaceHolder 等组件一起,构成了 Android 的图形系统。
Surface 的主要作用是存储图形数据。当你在一个 Surface 上绘制图像时,这些图像会被存储在 Surface 的缓冲区中。然后,这个 Surface 可以被提交给 SurfaceFlinger,SurfaceFlinger 会将这个 Surface 的内容合成到屏幕上。
Surface 通常与其他组件一起使用。例如,你可以使用 Canvas 在一个 Surface 上绘制2D图像,或者使用 OpenGL ES 在一个 Surface 上绘制3D图像。你也可以使用 MediaCodec 或者 Camera 将视频帧输出到一个 Surface 上。
Surface 还有一些其他的特性。例如,它可以被多个进程共享,这使得跨进程的图形数据传输成为可能。它还支持 vsync 信号,这可以帮助你实现平滑的动画效果。
这部分对于我来说过于复杂了,并且这也不是目前要关心的内容,我们抓一个重点即可:Surface 中有一个 BufferQueue,里头存放着图像数据,生产者将数据送入 Queue 中,而消费者从 Queue 获取数据。在 Android 中,Surface 可以被看作是一个画布,你可以在上面绘制图像,然后这些图像会被显示到屏幕上。这个画布并不是一个实体,而是一个虚拟的概念,它代表了一个可以被绘制的区域。
你可以把 Surface 想象成一个电子屏幕上的窗户。你可以在这个窗户上画画,然后这些画就会显示在电子屏幕上。这个窗户就像是你和电子屏幕之间的一个桥梁,你通过这个窗户,将你的画送到电子屏幕上。在这个比喻中,你的画就是图像数据,电子屏幕就是 Android 设备的显示屏,而窗户就是 Surface。你将图像数据(即你的画)送入 Surface(即窗户),然后这些数据就会被渲染到显示屏(即电子屏幕)上。
在 Android Surface的理解和应用 中对 Surface 中 生产者 - 消费者 架构做了较为详细的说明,不再赘述。
二、MediaCodec 解码到 Surface
像上一个教程一样,我们将使用 MediaExtractor 和 MediaCodec 解码视频,不同的是,解码后的视频帧画面将使用 SurfaceView 显示。
2.1 Surface 从哪来?
获取 Surface 最简单的方式是:使用 SurfaceView。例如:
val surfaceView = findViewById<SurfaceView>(R.id.surface_view)
val surface = surfaceView.holder.surface
2. 2 MediaCodec 与 Surface 共享内存,实现零拷贝
MediaCodec 使用 Surface 进行解码时,效率更高的原因是因为 Android 使用了共享内存技术。具体来说,MediaCodec 解码后的数据会直接传递给 Surface 的 BufferQueue,而不是通过拷贝的方式传递给 Surface。这样就避免了从 MediaCodec 到 Surface 之间的数据拷贝,提高了解码效率。
为了实现这种共享内存的方式,我们在调用 configure 方法时需要传入一个 Surface 对象,这样才能让 MediaCodec 和 Surface 共享同一个 BufferQueue。这种优化方式可以有效地减少数据拷贝的次数,提高解码效率,从而更好地满足视频播放等应用的需求。
codec.configure(videoFormat, surface, null, 0)
2.3 数据流动
在解码的过程涉及到三个对象: MediaExtractor、MediaCodec 和 Surface,数据就像流水线一样从一个地方流向另一个地方,并最终被消费。以 SurfaceView 为例,最终的消费者是 SurfaceFlinger,流水线如下图所示:
2.4 ReleaseOutputBuffer,控制水流速度
MediaCodec 的 releaseOutputBuffer 方法就像一个水龙头,你可以通过它来控制解码的速度。这个方法有两个版本:
-
releaseOutputBuffer(int index, boolean render):这个版本的方法中,如果 render 参数为 true,那么解码后的数据(Buffer)会立即被送到 Surface 进行渲染(播放),播放完后,这个 Buffer 就会被标记为可用,返回给 MediaCodec。如果 render 参数为 false,那么这个 Buffer 不会被送到 Surface,而是直接被释放,然后被标记为可用,返回给 MediaCodec。
-
releaseOutputBuffer(int index, long renderTimestampNs):这个版本的方法中,你可以传入一个时间戳 renderTimestampNs。如果这个时间戳小于当前的系统时间,那么 Buffer 的内容会立即被渲染。如果这个时间戳大于当前的系统时间,那么系统会等待,直到系统时间达到这个时间戳,然后再进行渲染。这个特性非常有用,特别是当你需要精确控制音频或视频的播放时间,或者需要同步多个音频或视频流的播放时。
总的来说,releaseOutputBuffer 方法就像一个控制解码速度的调节器,你可以通过它来精确控制音视频的播放。
2.5 Show me the code
所有代码你可以在 DecodeUsingSurfaceActivity 中找到,下面代码中,给出了同步和异步两种实现。
private fun decodeToSurface(surface: Surface){
// 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)
// create and configure media codec
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
val codecName = codecList.findDecoderForFormat(videoFormat)
val codec = MediaCodec.createByCodecName(codecName)
// configure with surface
codec.configure(videoFormat, surface, null, 0)
// start decoding
val maxInputSize = videoFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)
val inputBuffer = ByteBuffer.allocate(maxInputSize)
val bufferInfo = MediaCodec.BufferInfo()
val timeoutUs = 10000L // 10ms
var inputEnd = false
var outputEnd = false
codec.start()
while (!outputEnd && !stopDecoding) {
val isExtractorReadEnd =
getInputBufferFromExtractor(mediaExtractor, inputBuffer, bufferInfo)
if (isExtractorReadEnd) {
inputEnd = true
}
// get codec input buffer and fill it with data from extractor
// timeoutUs is -1L means wait forever
val inputBufferId = codec.dequeueInputBuffer(-1L)
if (inputBufferId >= 0) {
if (inputEnd) {
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,
0
)
}
}
// get output buffer from codec and render it to image view
// NOTE! dequeueOutputBuffer with -1L is will stuck here, so wait 10ms here
val outputBufferId = codec.dequeueOutputBuffer(bufferInfo, timeoutUs)
if (outputBufferId >= 0) {
if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
outputEnd = true
}
if (bufferInfo.size > 0) {
val pts = bufferInfo.presentationTimeUs * 1000L + startTime
codec.releaseOutputBuffer(outputBufferId, pts)
}
}
mediaExtractor.advance()
}
mediaExtractor.release()
codec.stop()
codec.release()
}
private fun decodeToSurfaceAsync(surface: Surface) {
// 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)
// 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) {
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){
// render the decoded frame
val pts = info.presentationTimeUs * 1000L + startTime
codec.releaseOutputBuffer(outputBufferId, pts)
}
}
override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {
e.printStackTrace()
}
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
// do nothing
}
})
// configure with surface
codec.configure(videoFormat, surface, null, 0)
// start decoding
codec.start()
// wait for processing to complete
while (!outputEnd.get() && !stopDecoding) {
Thread.sleep(10)
}
mediaExtractor.release()
codec.stop()
codec.release()
}
参考
- Android Surface的理解和应用
- 从整体上看Android图像显示系统
- Android图形系统综述(干货篇)
- 浅谈Android Surface机制
- 深入Android系统(十二)Android图形显示系统-1-显示原理与Surface