欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android音视频系列(五):使用MediaCodec播放视频文件

程序员文章站 2022-07-05 16:19:40
...

前言

本片博客我们一起来研究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已经掌握的差不多了,接下来我们写几个小案例,进一步理解和加深他们的使用方法。