视频解码

96 阅读2分钟

之前的文章中介绍了Camera2、CameraX的使用和视频编码,今天就一起来看看视频是如何解码的。

视频解码的实现

思路逻辑:

  1. 使用MediaExtractor 解复用,拿到音频数据
  2. 使用MediaCodec 解码器解码h264数据
  3. 把数据输出到 Surface 上,就可以在与之关联的view现实出来了。

下面开始代码实现:

  1. 创建 MediaExtractor 对象,解复用MP4
val extractor = MediaExtractor()
extractor.setDataSource(inputPath)
  1. 获取视频轨,并选择视频轨
val extractorVideoIndex = extractor.getTrackIndex(MediaType.VIDEO)
if (extractorVideoIndex == -1) return
extractor.selectTrack(extractorVideoIndex)
  1. 通过MediaExtractor拿到 视频格式,创建解码器
val mediaCodec =
    MediaCodec.createDecoderByType(videoFormat.getString(MediaFormat.KEY_MIME) ?: "")
mediaCodec.configure(videoFormat, surface, null, 0)
  1. 开始解码,然后写入到Surface
mediaCodec.start()

    var isInputFinished = false
    var inputBufferIndex: Int
    var outputBufferIndex: Int
    val bufferInfo = MediaCodec.BufferInfo()
    while (isPlay) {
        if (!isInputFinished) {
            // 开始解码
            // 获取输入索引号
            inputBufferIndex = mediaCodec.dequeueInputBuffer(1000)
            if (inputBufferIndex >= 0) {
                // 获取输入buffer
                val inputBuffer = mediaCodec.getInputBuffer(inputBufferIndex)
                    ?: throw RuntimeException("MediaCodec.getInputBuffer returned null")
                // 添加数据到buffer 中
                val sampleSize = extractor.readSampleData(inputBuffer, 0)
                if (sampleSize >= 0) {
                    // 放到解码器
                    mediaCodec.queueInputBuffer(
                        inputBufferIndex, 0, sampleSize, extractor.sampleTime, 0
                    )
                    extractor.advance()
                } else {
                    isInputFinished = true
                    mediaCodec.queueInputBuffer(
                        inputBufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM
                    )
                }
            }
            // 拿到输出索引号
            outputBufferIndex = mediaCodec.dequeueOutputBuffer(bufferInfo, 1000)

            if (outputBufferIndex >= 0) {
                currentTimestamp = bufferInfo.presentationTimeUs

                mediaCodec.releaseOutputBuffer(outputBufferIndex, true)
                //休眠
                if (previousTimestamp!=-1L){
                    Thread.sleep((currentTimestamp-previousTimestamp)/1000)
                }
                previousTimestamp = currentTimestamp
                if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                    "getPCMDataFromMP4 end BUFFER_FLAG_END_OF_STREAM".log()
                    break
                }

            } else if (outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                // 格式发生变化,可在此处进行相关处理
            }
        }
    }
    "getPCMDataFromMP4 end---".log()
    mediaCodec.stop()
    mediaCodec.release()
} catch (e: Exception) {
    "playAudio error----$e".log()
} finally {
    extractor.unselectTrack(extractorVideoIndex)
    extractor.release()
}

这样视频解码就完成了,并且把解码后的数据渲染到了Surface 上,这个Surface是用SurfaceView 获取的,所以就渲染到了SurfaceView 上了。其中关键的有两点:

  1. 创建解码器的时候输入 Surface 对象:mediaCodec.configure(videoFormat, surface, null, 0)
  2. 解码拿到数据后,mediaCodec.releaseOutputBuffer(outputBufferIndex, true) 第二个参数为true,表示渲染到Surface上。

总结

  • 视频的解码 和音频解码差不多,只是解码后的数据处理不一样,音频解码后把数据写到AudioTrack里面,而视频解码后是把数据渲染到Surface中,然后对应的view就展示出画面了。
  • 可以把音频解码和视频解码结合起来,做一个简单的播放器,音频解码和视频解码在单独的线程,可能比较困难的一点是音视频同步的问题。
  • 我们做视频播放器音视频同步,一般是以音频为主,视频跟着音频走,这样做的原因是,音频一般都是线性的,丢失了部分数据很容易被发现,但是视频一般不那么敏感,丢这几帧数据也是没太多问题。后面在结束使用FFMpeg实现播放器的时候,再详细介绍了。