原创文章,转载请联系做者java
若待明朝风雨过,人在天涯!春在天涯git
原文地址github
最近在整理硬编码MediaCodec相关的学习笔记,以及代码文档,分享出来以供参考。本人水平有限,项目不免有思虑不当之处,如有问题能够提Issues
。项目地址传送门
此篇文章,主要是分享如何用MediaCodeC
解码视频指定时间的一帧,回调Bitmap对象。以前还有一篇MediaCodeC硬解码视频,并将视频帧存储为图片文件,主要内容是将视频完整解码,并存储为JPEG文件,你们感兴趣能够去看一看。缓存
VideoDecoder2
上手简单直接,首先须要建立一个解码器对象:bash
val videoDecoder2 = VideoDecoder2(dataSource)
复制代码
dataSoure就是视频文件地址框架
解码器会在对象建立的时候,对视频文件进行分析,得出时长、帧率等信息。有了解码器对象后,在须要解码帧的地方,直接调用函数:异步
videoDecoder2.getFrame(time, { it->
//成功回调,it为对应帧Bitmap对象
}, {
//失败回调
})
复制代码
time 接受一个Float数值,级别为秒ide
getFrame
函数式一个异步回调,会自动回调到主线程里来。同时这个函数也没有过分调用限制。也就是说——,你能够频繁调用而不用担忧出现其余问题。函数
VideoDecoder2
目前只支持硬编码解码,在某些机型或者版本下,可能会出现兼容问题。后续会继续补上软解码的功能模块。
先来看一下VideoDecoder2
的代码框架,有哪些类构成,以及这些类起到的做用。 学习
VideoDecoder2
中,
DecodeFrame
承担着核心任务,由它发起这一帧的解码工做。获取了目标帧的YUV数据后;由
GLCore
来将这一帧转为Bitmap对象,它内部封装了
OpenGL环境的搭建,以及配置了
Surface
供给
MediaCodeC
使用。
FrameCache
主要是作着缓存的工做,内部有内存缓存
LruCache以及磁盘缓存
DiskLruCache,由于缓存的存在,很大程度上提升了二次读取的效率。
VideoDecoder2
的工做流程,是一个线性任务队列串行的方式。其工做流程图以下:
getFrame
函数时,首先从缓存从获取这一帧的图片缓存。DecodeFrame
任务加入队列。index为1
的位置。DecodeFrame
获取到这一帧的Bitmap后,会将这一帧缓存为内存缓存,并在会在缓存线程内做磁盘缓存,方便二次读取。接下来分析一下,实现过程当中的几个重要的点。
MediaCodeC
获取视频特定时间帧精确实际上是一个相对而言的概念,MediaExtractor
的seekTo
函数,有三个可供选择的标记:SEEK_TO_PREVIOUS_SYNC, SEEK_TO_CLOSEST_SYNC, SEEK_TO_NEXT_SYNC,分别是seek指定帧的上一帧,最近帧和下一帧。
其实,seekTo
并没有法每次都准确的跳到指定帧,这个函数只会seek到目标时间的最接近的(CLOSEST)、上一帧(PREVIOUS)和下一帧(NEXT)。由于视频编码的关系,解码器只会从关键帧开始解码,也就是I帧。由于只有I帧才包含完整的信息。而P帧和B帧包含的信息并不彻底,只有依靠先后帧的信息才能解码。因此这里的解决办法是:先定位到目标时间的上一帧,而后advance
,直到读取的时间和目标时间的差值最小,或者读取的时间和目标时间的差值小于帧间隔。
val MediaFormat.fps: Int
get() = try {
getInteger(MediaFormat.KEY_FRAME_RATE)
} catch (e: Exception) {
0
}
/*
*
* return : 每一帧持续时间,微秒
* */
val perFrameTime by lazy {
1000000L / mediaFormat.fps
}
/*
*
* 查找这个时间点对应的最接近的一帧。
* 这一帧的时间点若是和目标时间相差不到 一帧间隔 就算相近
*
* maxRange:查找范围
* */
fun getValidSampleTime(time: Long, @IntRange(from = 2) maxRange: Int = 5): Long {
checkExtractor.seekTo(time, MediaExtractor.SEEK_TO_PREVIOUS_SYNC)
var count = 0
var sampleTime = checkExtractor.sampleTime
while (count < maxRange) {
checkExtractor.advance()
val s = checkExtractor.sampleTime
if (s != -1L) {
count++
// 选取和目标时间差值最小的那个
sampleTime = time.minDifferenceValue(sampleTime, s)
if (Math.abs(sampleTime - time) <= perFrameTime) {
//若是这个差值在 一帧间隔 内,即为成功
return sampleTime
}
} else {
count = maxRange
}
}
return sampleTime
}
复制代码
帧间隔其实就是:1s/帧率
MediaCodeC
解码指定帧获取到相对精确的采样点(帧)后,接下来就是使用MediaCodeC
解码了。首先,使用MediaExtractor
的seekTo
函数定位到目标采样点。
mediaExtractor.seekTo(time, MediaExtractor.SEEK_TO_PREVIOUS_SYNC)
复制代码
而后MediaCodeC
将MediaExtractor
读取的数据压入输入队列,不断循环,直到拿到想要的目标帧的数据。
/*
* 持续压入数据,直到拿到目标帧
* */
private fun handleFrame(time: Long, info: MediaCodec.BufferInfo, emitter: ObservableEmitter<Bitmap>? = null) {
var outputDone = false
var inputDone = false
videoAnalyze.mediaExtractor.seekTo(time, MediaExtractor.SEEK_TO_PREVIOUS_SYNC)
while (!outputDone) {
if (!inputDone) {
decoder.dequeueValidInputBuffer(DEF_TIME_OUT) { inputBufferId, inputBuffer ->
val sampleSize = videoAnalyze.mediaExtractor.readSampleData(inputBuffer, 0)
if (sampleSize < 0) {
decoder.queueInputBuffer(inputBufferId, 0, 0, 0L,
MediaCodec.BUFFER_FLAG_END_OF_STREAM)
inputDone = true
} else {
// 将数据压入到输入队列
val presentationTimeUs = videoAnalyze.mediaExtractor.sampleTime
Log.d(TAG, "${if (emitter != null) "main time" else "fuck time"} dequeue time is $presentationTimeUs ")
decoder.queueInputBuffer(inputBufferId, 0,
sampleSize, presentationTimeUs, 0)
videoAnalyze.mediaExtractor.advance()
}
}
decoder.disposeOutput(info, DEF_TIME_OUT, {
outputDone = true
}, { id ->
Log.d(TAG, "out time ${info.presentationTimeUs} ")
if (decodeCore.updateTexture(info, id, decoder)) {
if (info.presentationTimeUs == time) {
// 遇到目标时间帧,才生产Bitmap
outputDone = true
val bitmap = decodeCore.generateFrame()
frameCache.cacheFrame(time, bitmap)
emitter?.onNext(bitmap)
}
}
})
}
decoder.flush()
}
复制代码
须要注意的是,解码的时候,并非压入一帧数据,就能获得一帧输出数据的。
常规的作法是,持续不断向输入队列填充帧数据,直到拿到想要的目标帧数据。
缘由仍是由于视频帧的编码,并非每一帧都是关键帧,有些帧的解码必须依靠先后帧的信息。
LruCache自不用多说,磁盘缓存使用的是著名的DiskLruCache。缓存在VideoDecoder2
中占有很重要的位置,它有效的提升了解码器二次读取的效率,从而不用屡次解码以及使用OpenGL
绘制。
以前在
Oppo R15
的测试机型上,进行了一轮解码测试。
使用MediaCodeC
解码一帧到到的Bitmap,大概须要100~200ms的时间。
而使用磁盘缓存的话,读取时间大概在50~60ms徘徊,效率增长了一倍。
在磁盘缓存使用的过程当中,有对DiskLruCache进行二次封装,内部使用单线程队列形式。进行磁盘缓存,对外提供了异步和同步两种方式获取缓存。能够直接搭配DiskLruCache
使用——DiskCacheAssist.kt
到目前为止,视频解码的部分已经完成。上一篇是对视频完整解码并存储为图片文件,MediaCodeC硬解码视频,并将视频帧存储为图片文件,这一篇是解码指定帧。音视频相关的知识体系还很大,会继续学习下去。