系列文章目录
- 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 纹理上
- Android MediaCodec 简明教程(八):使用 MediaCodec 解码到纹理,使用 OpenGL ES 进行处理并显示在 GLSurfaceView 上
前言
在上一章节,我们已经探讨了如何使用 OpenGL ES 处理解码后的纹理,将彩色画面转换为灰色画面,并在 GLSurfaceView 上展示。在本章节,我们将研究如何将处理后的视频帧保存为本地的 MP4 文件。
本文所有代码可以在 DecodeEditEncodeActivity.kt 找到
数据流
整体流程可以大致描述为: Demuxer -> MediaCodec Decoder -> Edit -> MediaCodec Encoder -> Muxer
我们选择 Surface 作为视频数据传递的介质,其中 Surface 中的 Buffer Queue 起着关键作用。在这个流程中,我们需要关注每个 Surface 的生产者和消费者,以便清晰地理解数据的流向。
- Demuxer 负责解封装,将压缩数据传递给 MediaCodec 解码器。
- MediaCodec 解码器负责解码,将解码后的数据写入 Surface 的 Buffer Queue 中。
- SurfaceTexture 作为消费者获取到 Buffer 后,将视频数据绘制到 OES 纹理上。
- 使用 OpenGL ES API 将 OES 纹理绘制到编码器的 Surface 上,绘制过程中可以进行图像处理工作。此时,OpenGL 是该 Surface Buffer Queue 的生产者。
- MediaCodec 编码器收到 Buffer 后负责将其编码压缩。
- 编码压缩后的数据由 Muxer 进行封装,最终写入 MP4 文件中。
通过以上流程,视频数据经过解封装、解码、编辑、编码和封装等步骤,最终生成了一个完整的视频文件。
发生了编码卡死的问题
我在编写本章代码时遇到了卡死的问题,线程卡在 glColor
或者 glDrawElements
等 OpenGL 绘制 API 上,并且在华为手机上是必现的,但在小米手机上却没能复现。经过排查,我找到了原因:编码器的 Surface Buffer Queue 满了,导致在调用绘制 api 时,阻塞了当前线程。
那么,问题一:为什么编码器的 Surface 满了?这是因为我们使用的是 MediaCodec 的异步模式,无论是编码还是解码;并且通过 Debug 你就会知道,编码器和解码器虽然是两个 MediaCodec 实例,但它们的回调函数却在同一个线程中执行。于是乎,当出现解码器任务比较多的时候,编码器的 Surface 就可能满,导致卡死。如下图。
问题二,为什么华为手机上必现,小米手机却是正常的。通过日志我发现华为手机上 Surface Buffer Queue 大小为 5,而小米手机是 15,这就导致了小米手机上比较难出现 Buffer Quque 满了导致卡死的问题,但实际上也只是概率比较小,在极限情况仍然可能出现卡死的问题。
知道卡死的原因后如何修复?其实也很简单,我们让编解码器的回调函数执行在不同线程下即可,这部分在代码中会有说明。
Show me the code
先看下整体流程的代码:
private fun decodeASync() {
var done = AtomicBoolean(false)
// setup extractor
val mediaExtractor = MediaExtractor()
resources.openRawResourceFd(R.raw.h264_720p).use {
mediaExtractor.setDataSource(it)
}
val videoTrackIndex = 0
mediaExtractor.selectTrack(videoTrackIndex)
val inputVideoFormat = mediaExtractor.getTrackFormat(videoTrackIndex)
val videoWidth = inputVideoFormat.getInteger(MediaFormat.KEY_WIDTH)
val videoHeight = inputVideoFormat.getInteger(MediaFormat.KEY_HEIGHT)
Log.i(TAG, "get video width: $videoWidth, height: $videoHeight")
// setup muxer
val outputDir = externalCacheDir
val outputName = "decode_edit_encode_test.mp4"
val outputFile = File(outputDir, outputName)
val muxer = MediaMuxer(outputFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
var muxerSelectVideoTrackIndex = 0
// create encoder
val mimeType = MediaFormat.MIMETYPE_VIDEO_AVC
val outputFormat = MediaFormat.createVideoFormat(mimeType, videoWidth, videoHeight)
val colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
val videoBitrate = 2000000
val frameRate = 30
val iFrameInterval = 60
outputFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat)
outputFormat.setInteger(MediaFormat.KEY_BIT_RATE, videoBitrate)
outputFormat.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate)
outputFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval)
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
val encodeCodecName = codecList.findEncoderForFormat(outputFormat)
val encoder = MediaCodec.createByCodecName(encodeCodecName)
Log.i(TAG, "create encoder with format: $outputFormat")
// set encoder callback
encoder.setCallback(...)
encoder.configure(outputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
// create input surface and egl context for opengl rendering
val inputSurface = InputSurface(encoder.createInputSurface())
inputSurface.makeCurrent()
// create decoder
val decodeCodecName = codecList.findDecoderForFormat(inputVideoFormat)
val decoder = MediaCodec.createByCodecName(decodeCodecName)
// create output surface texture
val textureRenderer = TextureRenderer2()
val surfaceTexture = SurfaceTexture(textureRenderer.texId)
val outputSurface = Surface(surfaceTexture)
inputSurface.releaseEGLContext()
val thread = HandlerThread("FrameHandlerThread")
thread.start()
surfaceTexture.setOnFrameAvailableListener({
Log.d(TAG, "setOnFrameAvailableListener")
synchronized(lock) {
if (frameAvailable)
Log.d(
TAG,
"Frame available before the last frame was process...we dropped some frames"
)
frameAvailable = true
lock.notifyAll()
}
}, Handler(thread.looper))
val texMatrix = FloatArray(16)
// set callback
val maxInputSize = inputVideoFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)
val inputBuffer = ByteBuffer.allocate(maxInputSize)
val bufferInfo = MediaCodec.BufferInfo()
val videoDecoderHandlerThread = HandlerThread("DecoderThread")
videoDecoderHandlerThread.start()
decoder.setCallback(..., Handler(videoDecoderHandlerThread.looper))
// config decoder
decoder.configure(inputVideoFormat, outputSurface, null, 0)
decoder.start()
encoder.start()
// wait for done
while(!done.get())
{
Thread.sleep(10)
}
Log.d(TAG, "finished")
// release resources
Log.d(TAG, "release resources...")
mediaExtractor.release()
decoder.stop()
decoder.release()
surfaceTexture.release()
outputSurface.release()
encoder.stop()
encoder.release()
muxer.stop()
muxer.release()
Log.d(TAG, "release resources end...")
}
- 创建一个MediaExtractor实例,用于从原始资源文件中提取视频轨道。
- 选择要处理的视频轨道,并获取其格式、宽度和高度。
- 创建一个 MediaMuxer 实例,用于将编码后的视频数据写入到输出文件。
- 创建一个 MediaCodec 实例,用于编码视频数据。编码器的配置包括视频格式、颜色格式、比特率、帧率和关键帧间隔。
- 利用 MediaCodec Encoder 创建一个输入 Surface 和一个 EGL Context,用于 OpenGL 渲染。注意这里,我们创建了一个 EGL Context,也就意味着可以在当前线程调用 OpenGL 相关的 API。
- 创建一个 MediaCodec 解码器,用于解码输入视频数据。
- 创建一个 SurfaceTexture,并通过它创建一个解码输出的 Surface。注意,创建 SurfaceTexture 前我们创建了 TextureRenderer2,而 TextureRenderer2.texId 是通过 OpenGL API 来创建的,我们要确保当前线程有 EGL Context 才能够调用 GL API;此外,我们还创建了一个线程,用来
setOnFrameAvailableListener
回调函数,原因在上一章中我已经解释过了,不再赘述。 - 设置解码器的回调函数,用于处理解码后的视频帧。注意,我们创建了一个解码线程用来处理解码器的回调函数,原因正如我在分析卡死问题时提到的那样。
- 配置解码器,并启动解码器和编码器。
- 在一个循环中等待解码和编码过程完成。
- 释放所有使用的资源,包括MediaExtractor、解码器、表面纹理、输出表面、编码器和MediaMuxer。
上面的过程除了一些 GL Context、线程等细节外,整体上还是比较容易理解的。接下来,我们看解码器和编码器的回调函数,这才是真正干活的地方。
encoder.setCallback(object : MediaCodec.Callback() {
override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {
}
override fun onOutputBufferAvailable(
codec: MediaCodec,
index: Int,
info: MediaCodec.BufferInfo
) {
val isEncodeDone = (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0
if (isEncodeDone) {
info.size = 0
done.set(true)
}
// got encoded frame, write it to muxer
if (info.size > 0) {
val encodedData = codec.getOutputBuffer(index)
muxer.writeSampleData(muxerSelectVideoTrackIndex, encodedData!!, info)
codec.releaseOutputBuffer(index, info.presentationTimeUs * 1000)
}
}
override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {
}
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
muxerSelectVideoTrackIndex = muxer.addTrack(format)
muxer.start()
}
});
编码器的回调函数逻辑比较简单:
onOutputBufferAvailable
,当编码器的输出缓冲区有数据可用时,此函数会被调用。在这个函数中,你可以从输出缓冲区获取编码后的数据。在这段代码中,首先检查是否已经到达流的结束,如果是,则设置done标志为true。然后,如果输出缓冲区的数据大小大于0,就将编码后的数据写入到muxer,然后释放输出缓冲区。onOutputFormatChanged
,当编码器的输出格式发生改变时,此函数会被调用。在这段代码中,当输出格式改变时,将新的格式添加到muxer,然后启动muxer。
decoder.setCallback(object : MediaCodec.Callback() {
override fun onInputBufferAvailable(codec: MediaCodec, inputBufferId: Int) {
val isExtractorReadEnd =
getInputBufferFromExtractor(mediaExtractor, inputBuffer, bufferInfo)
if (isExtractorReadEnd) {
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,
index: Int,
info: MediaCodec.BufferInfo
) {
if (info.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) {
codec.releaseOutputBuffer(index, false)
return
}
val render = info.size > 0
codec.releaseOutputBuffer(index, render)
if (render) {
waitTillFrameAvailable()
val ptsNs = info.presentationTimeUs * 1000
inputSurface.makeCurrent()
surfaceTexture.updateTexImage()
surfaceTexture.getTransformMatrix(texMatrix)
// draw oes text to input surface
textureRenderer.draw(videoWidth, videoWidth, texMatrix, getMvp())
inputSurface.setPresentationTime(ptsNs)
inputSurface.swapBuffers()
inputSurface.releaseEGLContext()
}
if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
encoder.signalEndOfInputStream()
}
}
override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {
}
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
}
}, Handler(videoDecoderHandlerThread.looper))
onInputBufferAvailable
,当解码器需要输入数据时调用。在该回调函数中,首先通过调用getInputBufferFromExtractor()方法从MediaExtractor中获取输入数据,并将数据放入解码器的输入缓冲区中。如果已经读取到了Extractor的末尾,则向解码器的输入缓冲区发送结束标志。否则,将输入数据放入解码器的输入缓冲区,并调用advance()方法继续读取下一帧数据。onOutputBufferAvailable
,当解码器的输出缓冲区有数据可用时,此函数会被调用。在这个函数中,你可以从输出缓冲区获取解码后的数据。在这段代码中,首先检查输出缓冲区的数据是否是编解码器配置数据,如果是,则释放输出缓冲区并返回。然后,如果输出缓冲区的数据大小大于0,就将解码后的数据渲染到 Surface。最后,如果已经到达流的结束,就向编码器发送流结束的信号。注意,为了绘制数据到 Surface 上,我们要确保当前线程有 EGL Context 环境,因此调用了inputSurface.makeCurrent()
;接着,inputSurface.setPresentationTime
设置 PTS,然后使用inputSurface.swapBuffers()
来交换 Buffer,告诉编码器来了一帧数据;最后inputSurface.releaseEGLContext
来解除当前的 EGL 环境。
参考
- DecodeEditEncodeActivity.kt
- android-decodeencodetest