Android MediaCodec 编解码

672 阅读8分钟

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

工作流程

image.png

这是谷歌官方的图,input 为输入端,output 为输出端,输入和输出端各有若干个 buffer,输入端不断拿到一个空 buffer,装上数据,再传入 MediaCodec 直到所有数据输入为止,输出端不断从 MediaCodec 获取到 buffer,每次得到处理好的数据后,再将 buffer 交还给 MediaCodec。

MediaCodec 接收压缩数据和原始音视频数据,压缩数据一般指解码端的输入和编码端的输出,原始音视频数据一般是编码端的输入和解码端的输出。

生命周期

image.png

MediaCodec 总共有三个状态:Stopped,Executing 和 Released,其中 Stopped 包含 Configured,Uninitialized 和 Error 三个小状态,Executing 包含 Flushed,Running 和 End of Stream 三个小状态。

转换过程如下:

  1. 当 MediaCodec 对象实例刚创建好的时候,处于 Stopped 状态中的 Uninitialized 状态。
  2. 调用 configure 方法,就会进入 Configured 状态。
  3. 调用 start 方法,进入 Executing 状态,目前暂时是处于 Flushed 状态的。
  4. dequeueInputBuffer 方法返回 bufferIndex,根据这个 bufferIndex 获取 buffer,再通过 queueInputBuffer 进入 Running 状态。
  5. MediaCodec 工作阶段大部分时间处于 Running 状态,不断由 input 端 queueInputBuffer,output 端 dequeueOutputBuffer,形成一个循环,直到 input 端加上 BUFFER_FLAG_END_OF_STREAM 标签,MediaCodec 拿到此状态后不再接受任何新的数据输入,即进入 End of Stream 状态。
  6. 调用 stop 则变成 Stopped 中的 Uninitialized 状态,调用 release 释放所有资源则进入 Released 状态。
  7. 其中可能会出现一些意外,就会进入 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

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
    )
}

初始化编码器

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()
}

开始录制与编码

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 头,否则无法正常播放,如果与视频流合并则不用添加。

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 进行播放。

初始化解码器

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

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()
}

执行解码并播放

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 的计算能力,相对于硬编码而言,软编码的性能会较差一些。由于软编码不依赖特定的硬件支持,因此可以在广泛的设备上运行,兼容性较好。