Android音视频系列(五):使用MediaCodec播放视频文件
前言
本片博客我们一起来研究Android系统音视频api中,应该算是最难、最复杂的类:MediaCodec。
相对于之前介绍过的MediaPlayer,AudioRecod等等来说,MediaCodec用法稍微复杂了一些,而且有一些小坑值得踩一踩。
首先熟悉一个MediaCodec的常用方法:
createEncoderByType(@NonNul String type) :静态构造方法,type为指定的音视频格式,创建指定格式的编码器
createDecoderByType(@NonNull String type):静态构造方法,type为指定的音视频格式,创建指定格式的解码器
MediaCodec的设置
configure(
@Nullable MediaFormat format, // 绑定编解码的媒体格式
@Nullable Surface surface, // 绑定surface,可以直接完成数据的渲染
@Nullable MediaCrypto crypto, // 加密算法
@ConfigureFlag int flags) // 加密的格式,如果不需要直接设置0即可
int dequeueInputBuffer(long timeoutUs) :timeoutUs等待时间,返回可以使用的输入buffer的索引
// 设置指定索引位置的buffer的信息
queueInputBuffer(
int index, // 数组的索引值
int offset, // 写入buffer的起始位置
int size, // 写入的输出的长度
long presentationTimeUs, // 该数据显示的时间戳
int flags // 该数据的标记位,例如关键帧,结束帧等等
)
timeoutUs等待时间,返回可以读取的buffer的索引
int dequeueOutputBuffer(
@NonNull BufferInfo info, // 这个BufferInfo需要自己手动创建,调用后,会把该索引的数据的信息写在里面
long timeoutUs // 等待时间
)
releaseOutputBuffer(int index, boolean render):释放指定索引位置的buffer
index:索引
render:如果绑定了surface,该数据是否要渲染到画布上
MediaCodec是系统级别的编解码库,底层还是调用native方法,使用MediaCodec的基本流程是:
创建与文件相匹配的MediaCodec -> MediaCodec写入数据,进行编码/解码 -> 读取MediaCodec编/解码结果
过程主要是分以上三步,今天我们以MediaCodec播放视频文件为例,学习MediaCodec的用法。
正文
首先我在我的手机提前录好了一个视频文件,大家可以自己下载demo,设置自己的视频路径。
private val filePath = "${Environment.getExternalStorageDirectory()}/DCIM/Camera/test.mp4"
首先我们完成必要的准备工作:
创建一个SurfaceView用于显示显示视频:
val surfaceView = SurfaceView(this)
// 设置Surface不维护自己的缓冲区,等待屏幕的渲染引擎将内容推送到用户面前
// 该api已经废弃,这个编辑会自动设置
// surfaceView.holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS)
surfaceView.holder.addCallback(this)
setContentView(surfaceView)
/**
* 开始播放视频
*/
override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {
// 视频解码
if (workerThread == null) {
workerThread = VideoMediaCodecWorker(holder!!.surface, filePath)
workerThread!!.start()
}
// 音频解码
if (audioMediaCodecWorker == null) {
audioMediaCodecWorker = AudioMediaCodecWorker(filePath)
audioMediaCodecWorker!!.start()
}
}
/**
* 停止播放视频
*/
override fun surfaceDestroyed(holder: SurfaceHolder?) {
if (workerThread != null) {
workerThread!!.interrupt()
workerThread = null
}
if (audioMediaCodecWorker != null) {
audioMediaCodecWorker!!.interrupt()
audioMediaCodecWorker = null
}
}
我们先看视频解码,因为MediaCodec可以直接和Surface绑定,自动完成画面的渲染,相对来说比较简单,我们之前已经分析了主要的三步:
创建指定格式的MediaCodec
我们必须要知道视频文件的编码格式,才能解码,所以我们借助MediaExtractor:
// 设置要解析的视频文件地址
try {
mediaExtractor.setDataSource(filePath)
} catch (e: IOException) {
e.printStackTrace()
}
// 遍历数据视频轨道,创建指定格式的MediaCodec
for (i in 0 until mediaExtractor.trackCount) {
val mediaFormat = mediaExtractor.getTrackFormat(i)
Log.e(TAG, ">> format i $i : $mediaFormat")
val mime = mediaFormat.getString(MediaFormat.KEY_MIME)
Log.e(TAG, ">> mime i $i : $mime")
// 找到视频轨道,并创建MediaCodec解码器
if (mime.startsWith("video/")) {
mediaExtractor.selectTrack(i)
try {
mediaCodec = MediaCodec.createDecoderByType(mime)
} catch (e: IOException) {
e.printStackTrace()
}
mediaCodec!!.configure(mediaFormat, surface, null, 0)
}
}
// 没找到音频轨道,直接返回
mediaCodec?.start() ?: return
MediaCodec写入数据,进行编码/解码
在第一步中,我们已经找到了视频的格式,并选中了文件的中的视频轨道,第二步,要把数据写入到MediaCodec中。
// 是否已经读到了结束的位置
var isEOS = false
while (!interrupted()) {
// 开始写入解码器
if (!isEOS) {
// 返回使用有效输出的Buffer索引,如果没有相关Buffer可用,就返回-1
// 如果传入的timeoutUs为0, 将立马返回
// 如果输入的buffer可用,就无限期等待,timeoutUs的单位是us
val inIndex = mediaCodec!!.dequeueInputBuffer(10000)
if (inIndex > 0) {
// 找到指定索引的buffer
val buffer = mediaCodec!!.getInputBuffer(inIndex)?: continue
Log.e(TAG, ">> buffer $buffer")
// 把视频的数据写入到buffer中
val sampleSize = mediaExtractor.readSampleData(buffer, 0)
// 已经读取结束
if (sampleSize < 0) {
Log.e(TAG, "InputBuffer BUFFER_FLAG_END_OF_STREAM")
mediaCodec!!.queueInputBuffer(
inIndex,
0,
0,
0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM
)
isEOS = true
}
// 把buffer放入队列中
else {
mediaCodec!!.queueInputBuffer(
inIndex,
0,
sampleSize,
mediaExtractor.sampleTime,
0
)
mediaExtractor.advance()
}
}
}
....
}
MediaCodec的写入的过程有点类似IO流,通过while循环,我们已经把视频文件中的视频数据都写入到解码器中了。
读取MediaCodec编/解码结果
想要知道解码的结果,我们要再重新读取一遍,过程和第二步几乎是一样的:
// 用于对准视频的时间戳
val startMs = System.currentTimeMillis()
while (!interrupted()) {
// 开始写入解码器
......
// 每个buffer的元数据包括具体范围的偏移及大小,以及有效数据中相关的解码的buffer
val info = MediaCodec.BufferInfo()
when (val outIndex = mediaCodec!!.dequeueOutputBuffer(info, 10000)) {
// 此类型已经废弃,如果使用的是getOutputBuffer()可以忽略此状态
MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
// 当buffer的格式发生改变,须指向新的buffer格式
}
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
// 当buffer的格式发生改变,须指向新的buffer格式
}
MediaCodec.INFO_TRY_AGAIN_LATER -> {
// 当dequeueOutputBuffer超时时,会到达此case
Log.e(TAG, ">> dequeueOutputBuffer timeout")
}
else -> {
// val buffer = outputBuffers[outIndex]
// 这里使用简单的时钟方式保持视频的fps,不然视频会播放的很快
sleepRender(info, startMs)
mediaCodec!!.releaseOutputBuffer(outIndex, true)
}
}
// 在所有解码后的帧都被渲染后,就可以停止播放了
if ((info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Log.e(TAG, "OutputBuffer BUFFER_FLAG_END_OF_STREAM")
break
}
}
mediaCodec!!.stop()
mediaCodec!!.release()
mediaExtractor.release()
/*
* 数据的时间戳对齐
*/
private fun sleepRender(audioBufferInfo: MediaCodec.BufferInfo, startMs: Long) {
// 这里的时间是 毫秒 presentationTimeUs 的时间是累加的 以微秒进行一帧一帧的累加
val timeDifference = audioBufferInfo.presentationTimeUs / 1000 - (System.currentTimeMillis() - startMs)
if (timeDifference > 0) {
try {
sleep(timeDifference)
} catch (e: InterruptedException) {
e.printStackTrace()
}
}
}
因为是我们是绑定的Surface,所以处理数据的过程我们不需要写,第三步主要是注意数据时间戳的对齐,否否则画面的显示速度会很快,这里使用了sleep来保持时间戳,并且使用后的数据要及时调用releaseOutputBuffer释放资源。
以上步骤我们完成了视频文件播放视频的功能,接下来是音频,其实音频的解码过程大同小异,唯一的区别是播放音频需要自己使用AudioTrack。
创建AudioTrack
for (i in 0 until mediaExtractor.trackCount) {
// 遍历数据音视频轨迹
val mediaFormat = mediaExtractor.getTrackFormat(i)
val mime = mediaFormat.getString(MediaFormat.KEY_MIME)
if (mime.startsWith("audio/")) {
mediaExtractor.selectTrack(i)
try {
mediaCodec = MediaCodec.createDecoderByType(mime)
} catch (e: IOException) {
e.printStackTrace()
}
mediaCodec!!.configure(mediaFormat, null, null, 0)
// 声道数
val audioChannels = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT)
// 音轨的采样率
val mSampleRate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE)
// 创建音轨
audioTrack = AudioTrack(
AudioAttributes.Builder()
.setLegacyStreamType(AudioManager.STREAM_MUSIC)
.build(),
Builder()
.setChannelMask(if (audioChannels == 1) CHANNEL_OUT_MONO else CHANNEL_OUT_STEREO)
.setEncoding(ENCODING_PCM_16BIT)
.setSampleRate(mSampleRate)
.build(),
AudioRecord.getMinBufferSize(
mSampleRate,
if (audioChannels == 1) CHANNEL_IN_MONO else CHANNEL_IN_STEREO,
ENCODING_PCM_16BIT
),
AudioTrack.MODE_STREAM,
AudioManager.AUDIO_SESSION_ID_GENERATE
)
}
}
首先找到音轨的格式,我们需要知道音频的采样率和声道数,如果信息不准备,则会出现声道播放异常的情况(过快、噪音、过慢等),至于编码位数我们直接使用16位。另外注意:Builder参数中的setChannelMask要使用CHANNEL_OUT_XXX,在AudioRecord.getMinBufferSize中要是用CHANNEL_IN_XXX,千万不要用错了。
播放音频
音频我们要手动从MediaCode从中得出来写到AudioTrack中:
// 每个buffer的元数据包括具体范围的偏移及大小,以及有效数据中相关的解码的buffer
val info = MediaCodec.BufferInfo()
when (val outIndex = mediaCodec!!.dequeueOutputBuffer(info, 0)) {
// 此类型已经废弃,如果使用的是getOutputBuffer()可以忽略此状态
MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
// 当buffer的格式发生改变,须指向新的buffer格式
}
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
// 当buffer的格式发生改变,须指向新的buffer格式
}
MediaCodec.INFO_TRY_AGAIN_LATER -> {
// 当dequeueOutputBuffer超时时,会到达此case
Log.e(TAG, ">> dequeueOutputBuffer timeout")
}
else -> {
val buffer = mediaCodec!!.getOutputBuffer(outIndex)?: [email protected]
//用来保存解码后的数据
buffer.position(0)
val outData = ByteArray(info.size)
buffer.get(outData)
//清空缓存
buffer.clear()
audioTrack?.write(outData, 0, outData.size)
sleepRender(info, startMs)
mediaCodec!!.releaseOutputBuffer(outIndex, true)
}
}
处了AudioTrack.write,其他的都是一样的,也要注意音频戳的对齐。
总结
经过五个章节的学习,我们已经把Android系统的音频API已经掌握的差不多了,接下来我们写几个小案例,进一步理解和加深他们的使用方法。