MediaCodec 是 Android 提供的用于音视频编解码的类,可以实现对音视频数据进行硬件加速的编解码操作。使用 MediaCodec 可以直接操作底层硬件编解码器,利用硬件加速的优势来提高音视频处理的效率和性能,同时降低 CPU 的使用率和功耗。MediaCodec 支持多种音视频格式的编解码,包括常见的音频格式如 AAC,MP3 和视频格式如 H264,H265 等。
工作流程

这是谷歌官方的图,input 为输入端,output 为输出端,输入和输出端各有若干个 buffer,输入端不断拿到一个空 buffer,装上数据,再传入 MediaCodec 直到所有数据输入为止,输出端不断从 MediaCodec 获取到 buffer,每次得到处理好的数据后,再将 buffer 交还给 MediaCodec。
MediaCodec 接收压缩数据和原始音视频数据,压缩数据一般指解码端的输入和编码端的输出,原始音视频数据一般是编码端的输入和解码端的输出。
生命周期

MediaCodec 总共有三个状态:Stopped,Executing 和 Released,其中 Stopped 包含 Configured,Uninitialized 和 Error 三个小状态,Executing 包含 Flushed,Running 和 End of Stream 三个小状态。
转换过程如下:
- 当 MediaCodec 对象实例刚创建好的时候,处于 Stopped 状态中的 Uninitialized 状态。
- 调用 configure 方法,就会进入 Configured 状态。
- 调用 start 方法,进入 Executing 状态,目前暂时是处于 Flushed 状态的。
- dequeueInputBuffer 方法返回 bufferIndex,根据这个 bufferIndex 获取 buffer,再通过 queueInputBuffer 进入 Running 状态。
- MediaCodec 工作阶段大部分时间处于 Running 状态,不断由 input 端 queueInputBuffer,output 端 dequeueOutputBuffer,形成一个循环,直到 input 端加上 BUFFER_FLAG_END_OF_STREAM 标签,MediaCodec 拿到此状态后不再接受任何新的数据输入,即进入 End of Stream 状态。
- 调用 stop 则变成 Stopped 中的 Uninitialized 状态,调用 release 释放所有资源则进入 Released 状态。
- 其中可能会出现一些意外,就会进入 Stopped 中的 Error 状态,这时有俩选择,一个是直接 Released,一个是从 Stopped 中的 Uninitialized 状态重新开始。
主要 API
- MediaCodec createEncoderByType(String type): 创建编码器,type 为 mime 类型。
- MediaCodec createDecoderByType(String type): 创建解码器,type 为 mime 类型。
- void configure(MediaFormat format, Surface surface, MediaCrypto crypto, int flags): 配置编解码器。
- void start(): 启动编码器或解码器。
- int dequeueInputBuffer(long timeoutUs): 获取输入队列的一个空闲索引,timeoutUs 为最多等待时间,单位是微秒,0表示立即返回,不会阻塞。如果没有可用的输入缓冲区,方法会立即返回-1,-1表示无限阻塞,直到有可用的输入缓冲区为止,其他正整数值表示阻塞的最大时间,如果在指定的时间内没有可用的输入缓冲区,方法会返回-1。
- ByteBuffer getInputBuffer(int index): 获取输入队列的一个空闲缓存区,index 传入 dequeueInputBuffer 方法的返回值。
- void queueInputBuffer(int index, int offset, int size, long presentationTimeUs, int flags): 用于将输入数据放入输入缓冲区队列中进行编码。
index:指输入缓冲区的索引,传入 dequeueInputBuffer 方法的返回值。
offset:指输入数据在输入缓冲区中的偏移量,一般设置为0,表示从输入数据的起始位置开始。
size:指输入数据的大小,以字节为单位。
presentationTimeUs 指输入数据的显示时间戳,以微秒为单位。一般情况下,可以使用音视频数据的时间戳作为显示时间戳。
flags:输入缓冲区的标志位,可以用来指示输入数据的特性。常用的标志位包括: MediaCodec.BUFFER_FLAG_KEY_FRAME:表示输入数据是关键帧。 MediaCodec.BUFFER_FLAG_END_OF_STREAM:表示输入数据是流的结束。 - int dequeueOutputBuffer(BufferInfo info, long timeoutUs): 获取输出队列的一个缓存区的索引,并将格式信息保存在 BufferInfo 中,timeoutUs 为最多等待时间,单位是微秒,-1 表示一直等待。
- ByteBuffer getOutputBuffer(int index): 获取输出队列的一个缓存区,index 传入 dequeueOutputBuffer 方法的返回值。
- void releaseOutputBuffer(int index, boolean render): 释放 index 指向的缓存区数据,render 表示是否要渲染该输出缓冲区,true 表示该输出缓冲区将用于渲染到屏幕上,false 则表示该输出缓冲区将被丢弃或用于其他目的。
- void stop(): 结束编解码会话
- void release(): 释放资源
代码示例
我们知道,aac 数据是经过有损压缩的音频数据,具有更小的文件大小和较好的音质表现,而 pcm 数据是原始的无损音频数据,文件较大但保持了较高的音质。这里就以录音为案例来讲解吧!
编码
一般情况下,我们会将音频数据编码为 aac 格式进行传输和存储,使用 AudioRecord 录制的就是原始 pcm 数据,这里对其进行编码,变成 aac 数据,并存入文件中。
初始化 AudioRecord
kotlin
private fun initAudioRecord() {
bufferSizeInBytes = AudioRecord.getMinBufferSize(
44100,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT
)
audioRecord = AudioRecord(
MediaRecorder.AudioSource.MIC,
44100,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
bufferSizeInBytes
)
}
初始化编码器
kotlin
private fun initEncoder() {
val format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, 44100, 1)
//比特率,每秒传输的数据量,通常以比特(bit)为单位。
format.setInteger(MediaFormat.KEY_BIT_RATE, 128000)
format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC)
audioEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC)
audioEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
// 启动MediaCodec,等待传入数据。
audioEncoder.start()
}
开始录制与编码
kotlin
private fun startRecord() {
initAudioRecord()
initEncoder()
//开始录制
isRecording = true
audioRecord.startRecording()
// aac 文件存放路径
aacFilePath = "${getExternalFilesDir(null)}/audio_${System.currentTimeMillis()}.aac"
val aacFile = File(aacFilePath)
aacFile.createNewFile()
val fileOutputStream = FileOutputStream(aacFile)
bufferedOutputStream = BufferedOutputStream(fileOutputStream)
while (isRecording) {
val inputBufferIndex = audioEncoder.dequeueInputBuffer(-1)
if (inputBufferIndex >= 0) {
val inputBuffer = audioEncoder.getInputBuffer(inputBufferIndex)
inputBuffer?.let {
it.clear()
val bytesRead = audioRecord.read(it, it.capacity())
if (bytesRead > 0) {
audioEncoder.queueInputBuffer(
inputBufferIndex,
0,
bytesRead,
System.nanoTime() / 1000,
0
)
}
}
}
val bufferInfo = MediaCodec.BufferInfo()
var outputBufferIndex = audioEncoder.dequeueOutputBuffer(bufferInfo, 0)
while (outputBufferIndex >= 0) {
val outputBuffer = audioEncoder.getOutputBuffer(outputBufferIndex)
outputBuffer?.let {
//设置输出缓冲区中有效数据的偏移量,是为了确保我们从正确的位置开始读取数据。
it.position(bufferInfo.offset)
//限制位置在输出缓冲区中有效数据的偏移量加上数据的大小,是为了确保我们只读取有效数据的部分。
it.limit(bufferInfo.offset + bufferInfo.size)
// 7为 ADTS 头部大小
val outData = ByteArray(bufferInfo.size + 7)
addADTStoPacket(outData, bufferInfo.size + 7)
it.get(outData, 7, bufferInfo.size)
bufferedOutputStream.write(outData)
//释放已处理完的输出缓冲区,并获取下一个可用的输出缓冲区。
audioEncoder.releaseOutputBuffer(outputBufferIndex, false)
outputBufferIndex = audioEncoder.dequeueOutputBuffer(bufferInfo, 0)
}
}
}
audioRecord.stop()
audioRecord.release()
audioEncoder.stop()
audioEncoder.release()
bufferedOutputStream.flush()
bufferedOutputStream.close()
}
MediaCodec.BufferInfo 类主要用于描述音视频编解码器的缓冲区信息,提供了一些字段和方法,用于获取和设置编解码器缓冲区的相关参数,例如:
- size:表示缓冲区中有效数据的大小,以字节为单位。
- offset:表示缓冲区中有效数据的偏移量,以字节为单位。
- flags:表示缓冲区的标志位,例如是否为关键帧等。
- presentationTimeUs:以微秒为单位,用于表示媒体帧在播放时应该被展示的时间。对于音频帧而言,它通常对应于音频帧的采样时间,对于视频帧而言,它通常对应于视频帧的显示时间。该字段的值可以用于同步音视频流,确保音频和视频按照正确的时间顺序进行播放。
需要注意的是,单独 aac 文件需要添加 ADTS 头,否则无法正常播放,如果与视频流合并则不用添加。
kotlin
private fun addADTStoPacket(packet: ByteArray, packetLen: Int) {
val profile = 2
val freqIdx = 4
val chanCfg = 1
packet[0] = 0xFF.toByte()
packet[1] = 0xF9.toByte()
packet[2] = ((profile - 1 shl 6) + (freqIdx shl 2) + (chanCfg shr 2)).toByte()
packet[3] = ((chanCfg and 3 shl 6) + (packetLen shr 11)).toByte()
packet[4] = (packetLen and 0x7FF shr 3).toByte()
packet[5] = ((packetLen and 7 shl 5) + 0x1F).toByte()
packet[6] = 0xFC.toByte()
}
解码
由上我们就能得到一个 aac 文件,现在再对这个 aac 文件进行解码操作,使其重新转变为 pcm 数据,再用 AudioTrack 进行播放。
初始化解码器
kotlin
private fun initDecoder() {
mediaExtractor = MediaExtractor()
// aacFilePath 为音频文件路径
mediaExtractor.setDataSource(aacFilePath)
val mediaFormat = mediaExtractor.getTrackFormat(0)
val mime = mediaFormat.getString(MediaFormat.KEY_MIME)
if (mime != null && mime.startsWith("audio")) { //获取音频轨道
mediaExtractor.selectTrack(0) //选择音频轨道
audioDecoder = MediaCodec.createDecoderByType(mime)
audioDecoder.configure(mediaFormat, null, null, 0)
audioDecoder.start()
}
}
初始化 AudioTrack
kotlin
private fun initAudioTrack() {
val sampleRateInHz = 44100 // 设置采样率,单位为赫兹(Hz)
val channelConfig = AudioFormat.CHANNEL_OUT_MONO // 设置通道配置为单声道
val bufferSizeInBytes = AudioTrack.getMinBufferSize(
sampleRateInHz,
channelConfig,
AudioFormat.ENCODING_PCM_16BIT
) // 计算缓冲区最小大小
val audioAttributes = AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA)
.setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
.build()
val audioFormat = AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_PCM_16BIT)
.setSampleRate(sampleRateInHz)
.setChannelMask(channelConfig)
.build()
audioTrack = AudioTrack(
audioAttributes,
audioFormat,
bufferSizeInBytes,
AudioTrack.MODE_STREAM,
AudioManager.AUDIO_SESSION_ID_GENERATE
)
audioTrack.play()
}
执行解码并播放
kotlin
private fun decodeAndPlay() {
initDecoder()
initAudioTrack()
var isEOS = false
val bufferInfo = MediaCodec.BufferInfo()
while (!isEOS) {
val inputBufferIndex = audioDecoder.dequeueInputBuffer(10000)
if (inputBufferIndex >= 0) {
val inputBuffer = audioDecoder.getInputBuffer(inputBufferIndex)
inputBuffer?.let {
var sampleSize = mediaExtractor.readSampleData(it, 0)
var presentationTimeUs = 0L
if (sampleSize < 0) {
isEOS = true
sampleSize = 0
} else {
presentationTimeUs = mediaExtractor.sampleTime
}
audioDecoder.queueInputBuffer(
inputBufferIndex,
0,
sampleSize,
presentationTimeUs,
if (isEOS) MediaCodec.BUFFER_FLAG_END_OF_STREAM else 0
)
//将 MediaExtractor 推进到媒体数据的下一个样本位置
mediaExtractor.advance()
}
}
val outputBufferIndex = audioDecoder.dequeueOutputBuffer(bufferInfo, 10000)
if (outputBufferIndex >= 0) {
val outputBuffer = audioDecoder.getOutputBuffer(outputBufferIndex)
outputBuffer?.let {
// 将解码的 PCM 数据写入 AudioTrack 的播放缓冲区
val pcmData = ByteArray(bufferInfo.size)
it.get(pcmData)
audioTrack.write(pcmData, 0, bufferInfo.size)
audioDecoder.releaseOutputBuffer(outputBufferIndex, false)
}
if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
// 解码完成,退出循环
break
}
}
}
}
这里从录音案例出发,将 AudioRecord 录制的 pcm 数据编码成 aac 数据,然后再将 aac 数据解码成 pcm 数据,最后使用 AudioTrack 播放。当然,在实际开发中,一般不会这么干,这里纯粹是为了演示 MediaCodec 编解码处理的过程。
当然,有些人也会选择软编,主要还得看具体情况。硬编码和软编码是两种不同的编码方式。硬编码利用设备的硬件编码器来执行编码操作,通常是 GPU 或专用的硬件编码芯片,它使用硬件加速技术,能够实时高效地将原始音视频数据转换为压缩格式,如 H.264,H.265 等,具有较低的延迟和较高的性能,适合处理大量的音视频数据流。而软编码是通过软件算法来执行编码操作,它依赖于 CPU 的计算能力,相对于硬编码而言,软编码的性能会较差一些。由于软编码不依赖特定的硬件支持,因此可以在广泛的设备上运行,兼容性较好。