简介
MediaCodec是Android提供的用于对音视频进行编解码的类,即编码器/解码器组件。它通过访问底层的Codec来实现编解码的功能。是Android media基础框架的一部分,通常和 MediaExtractor, MediaSync, MediaMuxer, MediaCrypto, MediaDrm, Image, Surface和AudioTrack 一起使用,在视频播放和视频压缩编码中起到重要作用。详细的api介绍请看官方文档
工作流程
整体的流程上看,MediaCodec编解码器是对输入数据进行处理然后生成输出数据,这个过程是异步的,并使用了一组输入和输出缓冲区。
流程如下图:
- 客户端从MediaCodec请求或接收一个空的输入缓冲区(ByteBuffer),填充数据后将其发送到MediaCodec进行处理。
- 编解码器处理数据后将其输出到一个空的输出缓冲区(ByteBuffer)。
- 客户端从MediaCodec获取已填充的输出缓冲区,获取其内容并使用,然后将其释放回编解码器。
常用api介绍
-
createDecoderByType
根据MimeType创建一个解码器 -
configure
配置MediaCodec -
getInputBuffer(index)
获取需要编码数据的输入流队列,返回一个ByteBuffer -
dequeueInputBuffer(long timeoutUs)
返回有效数据填充的输入缓冲区的索引;如果当前没有可用的缓冲区,则返回-1。如果timeoutUs == 0,则此方法将立即返回;如果timeoutUs <0,则无限期等待输入缓冲区的可用性;如果timeoutUs> 0,则等待直至“ timeoutUs”微秒。 -
queueInputBuffer
在指定索引处填充一定范围的数据到输入缓冲区 -
dequeueOutputBuffer
从输出缓冲区取出数据,返回数据索引或一些定义的状态常量 -
getOutputBuffer(index)
返回输出缓冲区队列中指定索引的ByteBuffer -
releaseOutputBuffer
处理完成,释放ByteBuffer数据
使用
下面介绍下MediaCodec+MediaExrtractor+SurfaceView进行视频播放的示例:
注意的是:音频和视频需要分开播放
第一步,初始化MediaExrtractor,从视频文件中获取视频轨道信息
private fun init() {
try {
//创建MediaExtractor对象
videoExtractor = MediaExtractor()
//设置视频数据源,可以是本地文件也可以是网络文件
//注意,安卓9.0以上不允许http明文链接请求的地址,需要适配
videoExtractor?.setDataSource(videoPath!!)
val count = videoExtractor!!.trackCount //获取轨道数量
//视频
for (i in 0 until count) {
val mediaFormat = videoExtractor!!.getTrackFormat(i)
val mimeType = mediaFormat.getString(MediaFormat.KEY_MIME)
if (mimeType.startsWith("video/")) {
//获取到视频轨道
videoExtractor?.selectTrack(i)
initVideo(mediaFormat)
break
}
}
} catch (e: Exception) {
Log.e("Test", "出错了", e)
}
}
第二步,初始化MediaCodec视频解码器
private fun initVideo(mediaFormat: MediaFormat) {
val mimeType = mediaFormat.getString(MediaFormat.KEY_MIME)
val codecInfo = getCodecInfo(mimeType)//获取支持的解码格式
if (codecInfo != null) {
handler.sendMessage(Message.obtain(handler, 100, mediaFormat))
//根据MimeType创建解码器
videoCodec = MediaCodec.createDecoderByType(mimeType)
//配置,在此关联SurfaceView
//参数说明:mediaFormat:视频信息,surface:surface容器,crypto:数据加密 flags:解码器/编码器
videoCodec?.configure(mediaFormat, sv_video.holder.surface, null, 0)
videoCodec?.start()//开始解码
} else {
Log.e("Test", "格式错误")
}
}
第三步,创建线程,进行解码播放操作
private val decodeVideoRunnable = Runnable {
try {
//存放目标文件的数据
var byteBuffer: ByteBuffer? = null
//解码后的数据,包含每一个buffer的元数据信息,例如偏差,在相关解码器中有效的数据大小
val info = MediaCodec.BufferInfo()
// videoCodec!!.getOutputBuffers()
var first = false
var startWhen: Long = 0
var isInput = true
while (isDecoding) {
// Thread.sleep(20)//可以控制慢放
if (isInput) {
//1.准备一个输入缓冲区
val inIndex = videoCodec!!.dequeueInputBuffer(TIMEOUT)
if (inIndex >= 0) {
//2.准备填充数据
byteBuffer = videoCodec!!.getInputBuffer(inIndex)
//使用MediaExtractor读取视频数据
val sampleSize = videoExtractor!!.readSampleData(byteBuffer!!, 0)
if (videoExtractor!!.advance() && sampleSize > 0) {
//有数据,数据可用
//3.写入数据到输入缓冲区
videoCodec!!.queueInputBuffer(
inIndex,
0,
sampleSize,
videoExtractor!!.sampleTime,
0
)
} else {
//没有数据了,停止输入处理
videoCodec!!.queueInputBuffer(
inIndex,
0,
0,
0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM
)
isInput = false
}
} else {
continue
}
}
//4 获取一个输出缓冲区,开始解码
val outIndex = videoCodec!!.dequeueOutputBuffer(info, TIMEOUT)
if (outIndex >= 0) {
when (outIndex) {
MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
Log.d("Test", "INFO_OUTPUT_BUFFERS_CHANGED")
videoCodec!!.getOutputBuffer(outIndex)
}
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED ->
Log.d(
"Test",
"INFO_OUTPUT_FORMAT_CHANGED format : " + videoCodec!!.getOutputFormat()
)
MediaCodec.INFO_TRY_AGAIN_LATER -> {
}
else -> {
if (!first) {
startWhen = System.currentTimeMillis()
first = true
}
try {
val sleepTime: Long =
info.presentationTimeUs / 1000 - (System.currentTimeMillis() - startWhen)
if (sleepTime > 0) Thread.sleep(sleepTime)
} catch (e: InterruptedException) {
e.printStackTrace()
}
//对outputbuffer的处理完后,调用这个函数把buffer重新返回给codec类。
//调用这个api之后,SurfaceView才有图像
videoCodec!!.releaseOutputBuffer(outIndex, true)
}
}
}
}
//解码完毕,释放资源
videoCodec?.stop()
videoCodec?.release()
videoExtractor?.release()
} catch (e: Exception) {
Log.e("Test", "", e)
}
}
至此,视频播放就搞定了,下面讲一些音频播放,为了不冲突,这里使用了不同的MediaExtractor和MediaCodec
配置和启动过程其实跟视频解码差不多,不同点就是多了AudioTrack
//解码音频,进行播放
private val decodeAudioRunnable = Runnable {
try {
val inputBuffers: Array<ByteBuffer> = audioCodec!!.getInputBuffers()
var outputBuffers: Array<ByteBuffer> = audioCodec!!.getOutputBuffers()
val info = BufferInfo()
val buffsize = AudioTrack.getMinBufferSize(
sampleRate,
CHANNEL_OUT_STEREO,
ENCODING_PCM_16BIT
)
// 创建AudioTrack对象
var audioTrack: AudioTrack? = AudioTrack(
AudioManager.STREAM_MUSIC, sampleRate,
CHANNEL_OUT_STEREO,
ENCODING_PCM_16BIT,
buffsize,
MODE_STREAM
)
//启动AudioTrack
audioTrack!!.play()
while (isDecoding) {
val inIndex: Int = audioCodec!!.dequeueInputBuffer(TIMEOUT)
if (inIndex >= 0) {
val buffer = inputBuffers[inIndex]
//从MediaExtractor中读取待解数据
val sampleSize = audioExtractor!!.readSampleData(buffer, 0)
if (sampleSize < 0) {
audioCodec!!.queueInputBuffer(
inIndex, 0, 0, 0,
MediaCodec.BUFFER_FLAG_END_OF_STREAM
)
} else {
//向MediaDecoder输入待解码数据
audioCodec!!.queueInputBuffer(
inIndex,
0,
sampleSize,
videoExtractor!!.sampleTime,
0
)
audioExtractor!!.advance()
}
//从输出缓冲区队列取出解码后的数据
val outIndex: Int = audioCodec!!.dequeueOutputBuffer(info, TIMEOUT)
when (outIndex) {
MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED -> {
outputBuffers = audioCodec!!.getOutputBuffers()
}
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
val format: MediaFormat = audioCodec!!.getOutputFormat()
audioTrack.playbackRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE)
}
MediaCodec.INFO_TRY_AGAIN_LATER -> Log.d(
"Test",
"dequeueOutputBuffer timed out!"
)
else -> {
val outBuffer = outputBuffers[outIndex]
//Log.v(TAG, "outBuffer: " + outBuffer);
val chunk = ByteArray(info.size)
// Read the buffer all at once
outBuffer[chunk]
//清空buffer,否则下一次得到的还会得到同样的buffer
outBuffer.clear()
// AudioTrack write data
audioTrack.write(chunk, info.offset, info.offset + info.size)
audioCodec!!.releaseOutputBuffer(outIndex, false)
}
}
// 所有帧都解码、播放完之后退出循环
if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {
break
}
}
}
//释放MediaDecoder资源
audioCodec?.stop()
audioCodec?.release()
audioCodec = null
audioExtractor?.release()
audioExtractor = null
//释放AudioTrack资源
audioTrack.stop()
audioTrack.release()
audioTrack = null
} catch (e: Exception) {
Log.e("Test", "", e)
}
}
最后,说明一下一个关键点:视频宽高的确定。
我这里使用MediaExtractor获取到MediaFormat来获取视频宽高。实际开发中,一般是固定一个播放器的宽高,然后将SurfaceView进行缩放填充。
/**
* 根据视频大小改变SurfaceView大小
*/
private fun changeVideoSize(mediaFormat: MediaFormat) {
var videoWidth = mediaFormat.getInteger(MediaFormat.KEY_WIDTH) //获取高度
var videoHeight = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT) //获取高度
val surfaceWidth = sv_video.measuredWidth
val surfaceHeight = sv_video.measuredHeight
//根据视频尺寸去计算->视频可以在sufaceView中放大的最大倍数。
var maxSize: Double
maxSize =
if (resources.configuration.orientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
//竖屏模式下按视频宽度计算放大倍数值
max(videoWidth / surfaceWidth.toDouble(), videoHeight / surfaceHeight.toDouble())
} else {
//横屏模式下按视频高度计算放大倍数值
max(videoWidth / surfaceHeight.toDouble(), videoHeight / surfaceWidth.toDouble())
}
//视频宽高分别/最大倍数值 计算出放大后的视频尺寸
videoWidth = Math.ceil(videoWidth / maxSize).toInt();
videoHeight = Math.ceil(videoHeight / maxSize).toInt();
//将计算出的视频尺寸设置到surfaceView 让视频自动填充。
sv_video.layoutParams = ConstraintLayout.LayoutParams(videoWidth, videoHeight);
}
完整代码请看:VideoDemo