之前的文章中介绍了Camera2、CameraX的使用和视频编码,今天就一起来看看视频是如何解码的。
视频解码的实现
思路逻辑:
- 使用MediaExtractor 解复用,拿到音频数据
- 使用MediaCodec 解码器解码h264数据
- 把数据输出到 Surface 上,就可以在与之关联的view现实出来了。
下面开始代码实现:
- 创建 MediaExtractor 对象,解复用MP4
val extractor = MediaExtractor()
extractor.setDataSource(inputPath)
- 获取视频轨,并选择视频轨
val extractorVideoIndex = extractor.getTrackIndex(MediaType.VIDEO)
if (extractorVideoIndex == -1) return
extractor.selectTrack(extractorVideoIndex)
- 通过MediaExtractor拿到 视频格式,创建解码器
val mediaCodec =
MediaCodec.createDecoderByType(videoFormat.getString(MediaFormat.KEY_MIME) ?: "")
mediaCodec.configure(videoFormat, surface, null, 0)
- 开始解码,然后写入到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 上了。其中关键的有两点:
- 创建解码器的时候输入 Surface 对象:
mediaCodec.configure(videoFormat, surface, null, 0) - 解码拿到数据后,
mediaCodec.releaseOutputBuffer(outputBufferIndex, true)第二个参数为true,表示渲染到Surface上。
总结
- 视频的解码 和音频解码差不多,只是解码后的数据处理不一样,音频解码后把数据写到AudioTrack里面,而视频解码后是把数据渲染到Surface中,然后对应的view就展示出画面了。
- 可以把音频解码和视频解码结合起来,做一个简单的播放器,音频解码和视频解码在单独的线程,可能比较困难的一点是音视频同步的问题。
- 我们做视频播放器音视频同步,一般是以音频为主,视频跟着音频走,这样做的原因是,音频一般都是线性的,丢失了部分数据很容易被发现,但是视频一般不那么敏感,丢这几帧数据也是没太多问题。后面在结束使用FFMpeg实现播放器的时候,再详细介绍了。