当前位置:首页>编程日记>正文

MediaCodeC解码视频指定帧,迅捷、精确

本站寻求有缘人接手,详细了解请联系站长QQ1493399855

原创文章,转载请联系作者

若待明朝风雨过,人在天涯!春在天涯

原文地址

提要

最近在整理硬编码MediaCodec相关的学习笔记,以及代码文档,分享出来以供参考。本人水平有限,项目难免有思虑不当之处,若有问题可以提Issues。项目地址传送门
此篇文章,主要是分享如何用MediaCodeC解码视频指定时间的一帧,回调Bitmap对象。之前还有一篇MediaCodeC硬解码视频,并将视频帧存储为图片文件,主要内容是将视频完整解码,并存储为JPEG文件,大家感兴趣可以去看一看。

如何使用

VideoDecoder2上手简单直接,首先需要创建一个解码器对象:

val videoDecoder2 = VideoDecoder2(dataSource)
复制代码

dataSoure就是视频文件地址

解码器会在对象创建的时候,对视频文件进行分析,得出时长、帧率等信息。有了解码器对象后,在需要解码帧的地方,直接调用函数:

videoDecoder2.getFrame(time, { it->//成功回调,it为对应帧Bitmap对象}, {//失败回调})复制代码

time 接受一个Float数值,级别为秒

getFrame函数式一个异步回调,会自动回调到主线程里来。同时这个函数也没有过度调用限制。也就是说——,你可以频繁调用而不用担心出现其他问题。

代码结构、实现过程

代码结构

VideoDecoder2目前只支持硬编码解码,在某些机型或者版本下,可能会出现兼容问题。后续会继续补上软解码的功能模块。
先来看一下VideoDecoder2的代码框架,有哪些类构成,以及这些类起到的作用。

VideoDecoder2中,DecodeFrame承担着核心任务,由它发起这一帧的解码工作。获取了目标帧的YUV数据后;由GLCore来将这一帧转为Bitmap对象,它内部封装了OpenGL环境的搭建,以及配置了Surface供给MediaCodeC使用。
FrameCache主要是做着缓存的工作,内部有内存缓存LruCache以及磁盘缓存DiskLruCache,因为缓存的存在,很大程度上提高了二次读取的效率。

工作流程

VideoDecoder2的工作流程,是一个线性任务队列串行的方式。其工作流程图如下:

具体流程:

  • 1.当执行getFrame函数时,首先从缓存从获取这一帧的图片缓存。
  • 2.如果缓存中没有这一帧的缓存,那么首先判断任务队列中正在执行的任务是否和此时需要的任务重复,如果不重复,则创建一个DecodeFrame任务加入队列。
  • 3.任务队列的任务是在一个特定的子线程内,线性执行。新的任务会被加入队列尾端,而已有任务则会被提高优先级,移到队列中index为1的位置。
  • 4、DecodeFrame获取到这一帧的Bitmap后,会将这一帧缓存为内存缓存,并在会在缓存线程内作磁盘缓存,方便二次读取。

接下来分析一下,实现过程中的几个重要的点。

实现过程

  • 如何定位和目标时间戳相近的采样点
  • 如何使用MediaCodeC获取视频特定时间帧
  • 缓存是如何工作,起到的作用有哪些
定位精确帧

精确其实是一个相对而言的概念,MediaExtractorseekTo函数,有三个可供选择的标记: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: Intget() = 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 = 0var sampleTime = checkExtractor.sampleTimewhile (count < maxRange) {checkExtractor.advance()val s = checkExtractor.sampleTimeif (s != -1L) {count++// 选取和目标时间差值最小的那个sampleTime = time.minDifferenceValue(sampleTime, s)if (Math.abs(sampleTime - time) <= perFrameTime) {//如果这个差值在 一帧间隔 内,即为成功return sampleTime}} else {count = maxRange}}return sampleTime}
复制代码

帧间隔其实就是:1s/帧率

使用MediaCodeC解码指定帧

获取到相对精确的采样点(帧)后,接下来就是使用MediaCodeC解码了。首先,使用MediaExtractorseekTo函数定位到目标采样点。

mediaExtractor.seekTo(time, MediaExtractor.SEEK_TO_PREVIOUS_SYNC)
复制代码

然后MediaCodeCMediaExtractor读取的数据压入输入队列,不断循环,直到拿到想要的目标帧的数据。

/*
* 持续压入数据,直到拿到目标帧
* */
private fun handleFrame(time: Long, info: MediaCodec.BufferInfo, emitter: ObservableEmitter<Bitmap>? = null) {var outputDone = falsevar inputDone = falsevideoAnalyze.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.sampleTimeLog.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) {// 遇到目标时间帧,才生产BitmapoutputDone = trueval bitmap = decodeCore.generateFrame()frameCache.cacheFrame(time, bitmap)emitter?.onNext(bitmap)}}})}decoder.flush()
}
复制代码

需要注意的是,解码的时候,并不是压入一帧数据,就能得到一帧输出数据的。
常规的做法是,持续不断向输入队列填充帧数据,直到拿到想要的目标帧数据。
原因还是因为视频帧的编码,并不是每一帧都是关键帧,有些帧的解码必须依靠前后帧的信息。

缓存
  • LruCache,内存缓存
  • DiskLruCache

LruCache自不用多说,磁盘缓存使用的是著名的DiskLruCache。缓存在VideoDecoder2中占有很重要的位置,它有效的提高了解码器二次读取的效率,从而不用多次解码以及使用OpenGL绘制。

之前在Oppo R15的测试机型上,进行了一轮解码测试。
使用MediaCodeC解码一帧到到的Bitmap,大概需要100~200ms的时间。
而使用磁盘缓存的话,读取时间大概在50~60ms徘徊,效率增加了一倍。

在磁盘缓存使用的过程中,有对DiskLruCache进行二次封装,内部使用单线程队列形式。进行磁盘缓存,对外提供了异步和同步两种方式获取缓存。可以直接搭配DiskLruCache使用——DiskCacheAssist.kt

总结

到目前为止,视频解码的部分已经完成。上一篇是对视频完整解码并存储为图片文件,MediaCodeC硬解码视频,并将视频帧存储为图片文件,这一篇是解码指定帧。音视频相关的知识体系还很大,会继续学习下去。

结语

此处有项目地址,点击传送


http://www.coolblog.cn/news/6ade1f5b0daa0de7.html

相关文章:

  • asp多表查询并显示_SpringBoot系列(五):SpringBoot整合Mybatis实现多表关联查询
  • s7day2学习记录
  • 【求锤得锤的故事】Redis锁从面试连环炮聊到神仙打架。
  • 矿Spring入门Demo
  • 拼音怎么写_老师:不会写的字用圈代替,看到孩子试卷,网友:人才
  • Linux 实时流量监测(iptraf中文图解)
  • Win10 + Python + GPU版MXNet + VS2015 + RTools + R配置
  • 美颜
  • shell访问php文件夹,Shell获取某目录下所有文件夹的名称
  • 如何优雅的实现 Spring Boot 接口参数加密解密?
  • LeCun亲授的深度学习入门课:从飞行器的发明到卷积神经网络
  • Mac原生Terminal快速登录ssh
  • 法拉利虚拟学院2010 服务器,法拉利虚拟学院2010
  • 支撑微博千亿调用的轻量级RPC框架:Motan
  • mysql commit 机制_1024MySQL事物提交机制
  • java受保护的数据与_Javascript类定义语法,私有成员、受保护成员、静态成员等介绍...
  • 2019-9
  • jquery 使用小技巧
  • 科学计算工具NumPy(3):ndarray的元素处理
  • vscode pylint 错误_将实际未错误的py库添加到pylint白名单
  • 工程师在工作电脑存 64G 不雅文件,被公司开除后索赔 41 万,结果…
  • linux批量创建用户和密码
  • js常用阻止冒泡事件
  • 气泡图在开源监控工具中的应用效果
  • newinsets用法java_Java XYPlot.setInsets方法代碼示例
  • 各类型土地利用图例_划重点!国土空间总体规划——土地利用
  • php 启动服务器监听
  • dubbo简单示例
  • Ubuntu13.10:[3]如何开启SSH SERVER服务
  • [iptables]Redhat 7.2下使用iptables实现NAT
  • Django View(视图系统)
  • 【设计模式】 模式PK:策略模式VS状态模式
  • CSS小技巧——CSS滚动条美化
  • JS实现-页面数据无限加载
  • 最新DOS大全
  • 阿里巴巴分布式服务框架 Dubbo
  • 阿里大鱼.net core 发送短信
  • Sorenson Capital:值得投资的 5 种 AI 技术
  • 程序员入错行怎么办?
  • Arm芯片的新革命在缓缓上演
  • 两张超级大表join优化
  • 第九天函数
  • Linux软件安装-----apache安装
  • HDU 5988 最小费用流
  • 《看透springmvc源码分析与实践》读书笔记一
  • 通过Spark进行ALS离线和Stream实时推荐
  • nagios自写插件—check_file
  • python3 错误 Max retries exceeded with url 解决方法
  • 正式开课!如何学习相机模型与标定?(单目+双目+鱼眼+深度相机)
  • 行为模式之Template Method模式