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)?: continue@loop
               //用来保存解码后的数据
               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已经掌握的差不多了,接下来我们写几个小案例,进一步理解和加深他们的使用方法。

你可能感兴趣的:(Android音视频系列(五):使用MediaCodec播放视频文件)