前面我们了解了 MediaCodec 解码的具体使用流程,包括异步和同步模式、解码到 ByteBuffers 或者 Surface。本章开始,我们将开始学习如何使用 MediaCodec 进行编码。
与解码类似,MediaCodec 编码的输入支持 ByteBuffer 或者 Surface。 遵循循序渐进的原则,我们从最简单的一种情况开始讲起:MediaCodec 编码过程中,输入的图像数据存放在 ByteBuffer 中。
首先,我们需要创建对应的 MediaCodec 编码器,并进行正确的 configure。这一步中,你要考虑一些编码的参数,包括视频的分辨率、帧率、比特率、color format 等。其中 color format 非常重要,它描述了送给编码器的数据是如何排列的,编码器根据这个属性来读取数据。
接着,为了将编码后的数据保存为 MP4 文件,我们创建 MediaMuxer 来进行封装的工作。
当 MediaCodec 编码器和 MediaMuxer 准备好后,就能够开始编码了:将视频数据送给 Codec,Codec 将编码后的数据吐给 MediaMuxer,Muxer 将这些压缩后的数据写入本地文件。一切都很简单。
接下来我将对具体的代码进行说明,本文完整代码你可以在 EncodeUsingBuffersActivity 找到,该代码使用异步模式进行编码,异步模式更加简洁,我更喜欢这种模式。如果你想看同步模式是如何实现的,可以参考 CTS - EncodeDecodeTest 中的 doEncodeDecodeVideoFromBuffer 函数。
val mimeType = MediaFormat.MIMETYPE_VIDEO_AVC
val format = MediaFormat.createVideoFormat(mimeType, videoWidth, videoHeight)
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
val encodeCodecName = codecList.findEncoderForFormat(format)
val encoder = MediaCodec.createByCodecName(encodeCodecName)
val mimeType = MediaFormat.MIMETYPE_VIDEO_AVC
:定义了一个字符串常量mimeType,其值为MediaFormat.MIMETYPE_VIDEO_AVC,表示我们将使用的是AVC(即H.264)编码格式。val format = MediaFormat.createVideoFormat(mimeType, videoWidth, videoHeight)
:创建一个MediaFormat对象,该对象描述了我们想要的视频格式,包括编码格式、视频宽度和高度。val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
:获取系统中所有常规(非硬件加速)的编解码器列表。val encodeCodecName = codecList.findEncoderForFormat(format)
:在编解码器列表中查找能够处理我们指定格式的编码器。val encoder = MediaCodec.createByCodecName(encodeCodecName)
:通过编码器的名称创建一个MediaCodec对象,这个对象就是我们的视频编码器。当然,也可以更简单:
val mimeType = MediaFormat.MIMETYPE_VIDEO_AVC
val encoder = MediaCodec.createEncoderByType(encodeCodecName)
encoder.setCallback(object: MediaCodec.Callback(){
override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {
//
}
override fun onOutputBufferAvailable(
codec: MediaCodec,
index: Int,
info: MediaCodec.BufferInfo
) {
//
}
override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {
//
}
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
//
}
})
MediaCodec类中的setCallback()方法用于设置一个回调接口,这个接口将在编解码操作的各个阶段被调用。这个方法接收一个MediaCodec.Callback对象作为参数。
MediaCodec.Callback是一个抽象类,它定义了四个方法:
onInputBufferAvailable(MediaCodec codec, int index)
:当输入缓冲区可用时,此方法被调用。参数index指示了哪个输入缓冲区已经变得可用。
onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info)
:当输出缓冲区可用时,此方法被调用。参数index指示了哪个输出缓冲区已经变得可用,info包含了关于这个缓冲区的元数据,如其包含的数据的大小,时间戳等。
onError(MediaCodec codec, MediaCodec.CodecException e)
:当编解码器发生错误时,此方法被调用。参数e是一个MediaCodec.CodecException对象,包含了关于错误的详细信息。
onOutputFormatChanged(MediaCodec codec, MediaFormat format)
:当输出格式发生变化时,此方法被调用。参数format是一个MediaFormat对象,包含了新的输出格式。
回调中的代码是我们具体的编码逻辑,这个放后面详细讲。
val colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible
assert(encoder.codecInfo.getCapabilitiesForType(mimeType).colorFormats.contains(colorFormat))
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat)
format.setInteger(MediaFormat.KEY_BIT_RATE, videoBitrate)
format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE)
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL)
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
COLOR_FormatYUV420Flexible
这是一种最常用的像素格式。COLOR_FormatYUV420Flexible
的,否则我需要写额外的代码来兼容,这会使得代码变得负责。configure
函数,这行代码用上面设置的参数来配置编码器,最后一个参数指定了这是一个编码器,而不是解码器。val outputDir = externalCacheDir
val outputName = "test.mp4"
val outputFile = File(outputDir, outputName)
muxer = MediaMuxer(outputFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)
现在我们有 encoder 和 muxer 组件,要开始编码视频的任务,需要启动这两个组件,但两者启动时机有差别。
首先,我们先启动 encoder
encoder.start()
那么 muxer 何时启动呢?在启动 muxer 之前我们需要明确知道 output format 的信息。
在使用MediaCodec进行编码时,onOutputFormatChanged 方法会在开始编码后首次调用。这是因为在开始编码后,MediaCodec 会根据你设置的参数(如分辨率、比特率等)来确定最终的输出格式。一旦输出格式确定,就会触发onOutputFormatChanged方法。
这个方法的调用表示编码器的输出格式已经准备好,你可以获取到这个新的输出格式,并用它来配置你的MediaMuxer。这是必要的,因为MediaMuxer需要知道它正在混合的音频和视频的具体格式。
基于上述原因,在异步模式下我们可以在 onOutputFormatChanged 回调函数中启动 muxer:
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
videoTrackIndex = muxer.addTrack(format)
muxer.start()
}
让我们来看回调函数中的具体逻辑,这些逻辑表明了我们是如何进行编码的
override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {
val pts = computePresentationTime(generateIndex)
// input eos
if(generateIndex == NUM_FRAMES)
{
codec.queueInputBuffer(index, 0, 0, pts, MediaCodec.BUFFER_FLAG_END_OF_STREAM)
}else
{
val frameData = ByteArray(videoWidth * videoHeight * 3 / 2)
generateFrame(generateIndex, codec.inputFormat.getInteger(MediaFormat.KEY_COLOR_FORMAT), frameData)
val inputBuffer = codec.getInputBuffer(index)
inputBuffer.put(frameData)
codec.queueInputBuffer(index, 0, frameData.size, pts, 0)
generateIndex++
}
}
override fun onOutputBufferAvailable(
codec: MediaCodec,
index: Int,
info: MediaCodec.BufferInfo
) {
// output eos
val isDone = (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0
if(isDone)
{
outputEnd.set(true)
info.size = 0
}
if(info.size > 0){
val encodedData = codec.getOutputBuffer(index)
muxer.writeSampleData(videoTrackIndex, encodedData!!, info)
codec.releaseOutputBuffer(index, false)
}
}
override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {
e.printStackTrace()
}
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
//...
}
首先看 onInputBufferAvailable 回调:
需要说明的是,我们使用 generateFrame 来生成 YUV 数据,而不是从某个图片或者视频读取,这是为了示例代码更简单。这部分代码参考了 CTS - EncodeDecodeTest 中的代码。生成的视频如下: