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

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

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

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

原文地址

提要

最近在整理硬编码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硬解码视频,并将视频帧存储为图片文件,这一篇是解码指定帧。音视频相关的知识体系还很大,会继续学习下去。

结语

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

相关文章:

threejs 绘制球体_ThreeJs 绘制点、线、面

所有的三位物体都是由点构成&#xff0c;两点构成线&#xff0c;三点构成面&#xff0c;ThreeJs又如何绘制出点、线、面呢 &#xff1f;在ThreeJs中&#xff1a;模型由几何体和材质构成模型以何种形式(点、线、面)展示取决于渲染方式1. 几何体首先我们来创建一个自定义的几何体…

model多表操作

一. 创建模型 from django.db import models# Create your models here.#比较常用的作者信息放到这个表里面 class Author(models.Model): id models.AutoField(primary_keyTrue)namemodels.CharField(max_length32)agemodels.IntegerField()authorDetailmodels.OneToOneField…

cocos2d-x注意事项(十)Lua发展飞机战争-4-创建主角

二战中被称为二战飞机飞机&#xff0c;当然&#xff0c;以飞机作业。这是一个游戏&#xff0c;我们必须加入一个飞机——这是我们的英雄。 首先创建一个层&#xff08;PlaneLayer&#xff09;要显示飞机。然后&#xff0c;create飞机初始化方法 module("PlaneLayer",…

.PHONY makefile中的伪目标

拿clean举例&#xff0c;如果make完成后&#xff0c;自己另外定义一个名叫clean的文件&#xff0c;再执行make clean时&#xff0c;将不会执行rm命令。 为了避免出现这个问题&#xff0c;需要.PHONY: clean所谓伪目标就是这样一个目标&#xff0c;它不代表一个真正的文件名&…

jq父级绑定事件的意义_jq——事件

$(document),$(body)加载事件&#xff1a;$(document).ready(fn)$(document).ready(function(){ alert("这是加载事件"); });简写&#xff1a;$(function(){});工作中简写比较多js:window.οnlοadfn 把页面上的DOM和资源加载完成之后执行jq:$(document).ready(fn)…

美妆彩妆宣传PPT模板

还有四天就是三八妇女节了&#xff0c;相信有不少从事美妆美容行业的工作者也要开始对产品的一个宣传简介了&#xff0c;对外进行一番交流配上一份优秀的PPT模板是再好不过的了。今天办公资源要和大家分享的PPT模板是可用于美妆美容美发行业的工作策划&#xff0c;总结汇报等。…

报表性能优化方案之报表服务器优化基础讲解

内存 JVM堆栈内存是决定应用服务器性能的关键指标&#xff0c;一般服务器默认的内存配置都比较小&#xff0c;在较大型的应用项目中&#xff0c;这点内存是不够的&#xff0c;因此需要进行查看与修改Web服务器内存大小&#xff0c;接下来就介绍服务器内存查看的方法以及不同服务…

windows下git bash乱码问题

网上有很多相关资料&#xff0c;备份一下&#xff1a; 1,/etc/gitconfig&#xff1a;[gui] encoding utf-8 #代码库统一用urf-8,在git gui中可以正常显示中文[i18n] commitencoding GB2312 #log编码&#xff0c;window下默认gb2312,声明后发到服务器才不会乱码[svn] …

python tcp server分包_如何创建线程池来监听tcpserver包python

我试图创建线程池来同时对传入的tcp包执行一些操作。在我在python3中找不到任何内置线程池。我也读了一些关于multiprocessing.Pool的文章&#xff0c;但是它不支持内存共享。所以我使用Queue来模拟线程池。在为每个线程创建一个队列是否更好&#xff1f;有没有人有建议或更好的…

R语言 股价分析

首先判断股价的分布是不是正态分布: #获取3m公司收盘价 mmmdata read.csv("E:\\kuaipan\\A Introduction to Analysis of Financial Data with R\\chapter 1\\ch1data\\d-mmm-0111.txt",header T) mmmprice as.numeric(sapply(mmmdata, function(l){substring(l,1…

JavaのFile类基础操作之问题

在上一章节&#xff0c;介绍了File类的基础操作&#xff0c;比如文件的创建&#xff0c;文件的删除等等。这一章节&#xff0c;将介绍在File类基础操作中遇到的问题。 1.路径分割符问题 在实际的软件开发与运行过程中&#xff0c;往往都会在Windows环境下进行项目的开发&#x…

把时间当作朋友(第一版)笔记

平生第一次下载并阅读了电子版的关于学习类型的书籍&#xff0c;并且用软件&#xff08;MindManager)记录了读书摘要&#xff08;其实最多能叫摘录&#xff09;&#xff0c;不过也不妨碍我发篇随笔记录下。 平常&#xff0c;我更喜欢读纸质的书籍&#xff0c;用日志本记录笔记&…

python中执行linux命令(调用linux命令)_Python调用Linux bash命令

import subprocess as sup # 以下注释很多(为了自己以后不忘), 如果只是想在python中执行Linux命令, 看前5行就够了# 3.5版本之后官方推荐使用sup.run()sup.run("ls -l", shellTrue) # 如果参数args是字符串, shell要设置为Truesup.run(("ls", "-l&…

IOS中CoreLocation框架地理定位

1.CoreLocation框架使用前提&#xff1a; #import <CoreLocation/CoreLocation.h> CoreLocation框架中所有数据类型的前缀都是CL &#xff0c;CoreLocation中使用CLLocationManager对象来做用户定位 2.CLLocationManager的常用操作&#xff1a; 开始用户定位 - (void)sta…

Your First Concordion.Net Project (Part 5)-Running Specs with Gallio

http://living-in-concordion.blogspot.com/2009/05/your-first-concordionnet-project-part-5.html 需要一些自动运行Spec的Runner&#xff0c;这里采用的为Gallio写的一个插件。 Gallio是一个开源的测试框架&#xff0c;能够运行各种.net的测试框架&#xff0c;具有较高的扩展…

Vue学习的路径

接下来我将正式学习Vue,根据Vue作者所给的学习路径进行学习。每天做好学习笔记。​ vue学习路径和建议----尤雨溪 ​ vue官网

ssh mysql环境搭建 myeclipse_MyEclipse整合ssh三大框架环境搭载用户注册源码下载

前言SSH不是一个框架&#xff0c;而是多个框架(strutsspringhibernate)的集成&#xff0c;是目前较流行的一种Web应用程序开源集成框架&#xff0c;用于构建灵活、易于扩展的多层Web应用程序。集成SSH框架的系统从职责上分为四层&#xff1a;表示层、业务逻辑层、数据持久层和域…

IIS7.5 HTTP 错误 500 调用loadlibraryex失败的解决方法

在IIS7.5打开网页的时候&#xff0c;提示&#xff1a; HTTP 错误 500.0 - Internal Server Error 调用 LoadLibraryEx 失败&#xff0c;在 ISAPI 筛选器 C:\Windows\Microsoft.NET\Framework\v4.0.30319\\aspnet_filter.dll,经过排除发现原来是两个斜杠导致在IIS7.5打开网页的时…

Android学习笔记之progressBar(进度条)

一、说明 <1>在某项延续性工作的进展过程中为了不让用户觉得程序死掉了&#xff0c;需要有个活动的进度条&#xff0c;表示此过程正在进行中。 <2>在某些操作的进度中的可视指示器&#xff0c;为用户呈现操作的进度&#xff0c;还它有一个次要的进度条&#xff0c;…

在hadoop上运行python_hadoop上运行python程序

数据来源&#xff1a;http://www.nber.org/patents/acite75_99.zip首先上传测试数据到hdfs&#xff1a;[rootlocalhost:/usr/local/hadoop/hadoop-0.19.2]#bin/hadoopfs -ls /user/root/test-inFound 5 items-rw-r--r-- 1root supergroup101 2010-10-24 14:39 /user/root/test-…

leetcode 237: Delete Node in a Linked List

题目&#xff1a; Write a function to delete a node (except the tail) in a singly linked list, given only access to that node. Supposed the linked list is 1 -> 2 -> 3 -> 4 and you are given the third node with value 3, the linked list should become…

一次奇怪的AP注册异常问题处理

很久没遇到值得分享的经验&#xff0c;今天遇到一个问题&#xff0c;处理过程很有意思&#xff0c;和大家一起分享一下。有一个客户部署华三的一套无线&#xff0c;采用控制器瘦AP的方式。部署过程都是轻车熟路&#xff0c;配置全部完成&#xff0c;静等AP上线&#xff0c;这时…

今天是第一次开博客,for--futurechild!!!

今天刚刚开了一个博客&#xff0c;或许觉得每一个比较牛叉的人都会经历一个不平凡的过程。通往高手之路是一个及其不平凡的路途。学校教育不能培养出真正高手。学习的最高境界是---悟。自己一步一个脚印最终能到达高手的殿堂。当然&#xff0c;我也希望能记录下自己的这个过程。…

memcache php mysql_PHP中的数据库二、memcache

在一个高并发的web应用中&#xff0c;数据库存取瓶颈一直是个大问题&#xff0c;一旦达到某个极限&#xff0c;数据库很容易崩溃&#xff0c;但是如果我们把常用的数据放到内存中&#xff0c;在需要的时候从内存中取&#xff0c;不光读取速度快&#xff0c;而且节约数据库IO。m…

JavaScript sync and async(同步和异步)

推荐四篇文章&#xff1a; JavaScript 是单线程的深入分析JavaScript 运行机制详解&#xff1a;再谈 Event LoopJavaScript 异步编程的4种方法JavaScript 既是单线程又是异步的&#xff0c;请问这二者是否冲突&#xff0c;以及有什么区别&#xff1f;一个重要前提&#xff1a;J…

掌握 MySQL 这 19 个骚操作,效率至少提高3倍

本文我们来谈谈项目中常用的MySQL优化方法&#xff0c;共19条&#xff0c;利用好这19条方法&#xff0c;会让你的效率提升至少3倍。1、EXPLAIN做MySQL优化&#xff0c;我们要善用EXPLAIN查看SQL执行计划。下面来个简单的示例&#xff0c;标注&#xff08;1、2、3、4、5&#xf…

对于PS和flash协作做过光字的一点心得

上次一个朋友要求要帮忙帮他网站BANNER上面做个透明的过光字FLASH&#xff0c;要求必须是PS处理的字的效果再用过光字做成FLASH。。。 导入PS生成的文字效果PNG透明图片&#xff0c;直接新建图层&#xff0c;放下面&#xff0c;画个过光的效果&#xff0c;结果没有过光效果&…

mysql 5.6 binlog_format_ROW 格式binlog 在MySQL5.6上的数据恢复实验

5.6和5.7版本的MySQL&#xff0c;有个参数binlog_row_image&#xff0c;默认值为FULL&#xff0c;表示记录的是全部的binlog操作日志(仅在binlog_formatROW时候生效)。此外binlog_row_image还可以是minimal&#xff0c;表示binlog记录的就只是影响后的行。如此一来使用ROW格式就…

Appium学习笔记2_Android获取元素篇

在利用Appium做自动化测试时&#xff0c;最重要的一步就是获取对应的元素值&#xff0c;根据元素来对对象进行对应的操作&#xff0c;如果获得对象元素呢&#xff1f; Appium Server Console其实提供了一个界面对话框"Inspector",但是一般情况下&#xff0c;它无法获…

C#制作安装包

软件项目编码完工后,接下来就是制作安装包了.有一些人对制作安装包不屑一顾,但我认为这是软件工程中必不可少的环节,就如何包装商品一样.我曾经经过一个星期的研究,学会了如何制作安装包. 我做的一个安装包是可以操作配置文件的,具体步骤如下: 1)在我已经开发好的解决方案项目…