Android MediaCodec 简明教程(七):使用 MediaCodec 解码到 OES 纹理上

系列文章目录

  1. Android MediaCodec 简明教程(一):使用 MediaCodecList 查询 Codec 信息,并创建 MediaCodec 编解码器
  2. Android MediaCodec 简明教程(二):使用 MediaCodecInfo.CodecCapabilities 查询 Codec 支持的宽高,颜色空间等能力
  3. Android MediaCodec 简明教程(三):详解如何在同步与异步模式下,使用MediaCodec将视频解码到ByteBuffers,并在ImageView上展示
  4. Android MediaCodec 简明教程(四):使用 MediaCodec 将视频解码到 Surface,并使用 SurfaceView 播放视频
  5. Android MediaCodec 简明教程(五):使用 MediaCodec 编码 ByteBuffer 数据,并保存为 MP4 文件
  6. Android MediaCodec 简明教程(六):使用 EGL 和 OpenGL 绘制图像到 Surface 上,并通过 MediaCodec 编码 Surface 数据,并保存到 MP4 文件

前言

在这个系列的第七章中,我们将深入探讨一些更复杂的知识点:如何将视频帧解码到OES纹理上。在前几章中,我们已经学习了如何查询MediaCodec信息,以及如何使用MediaCodec进行解码和编码。

首先,我们需要理解为什么我们需要将视频帧解码到纹理上。Android MediaCodec 简明教程(四):使用 MediaCodec 将视频解码到 Surface,并使用 SurfaceView 播放视频,我们直接将视频帧解码到SurfaceView上进行播放,这种方法并没有对视频帧进行任何处理。但是,如果我们想要给视频添加滤镜,这种方法就无法实现。

那么,我们应该如何操作呢?有两种可能的解决方案:

  1. 我们可以不再将视频帧解码到 Surface 上,而是解码为ByteBuffers。然后,我们可以对ByteBuffers中的图片像素数据进行处理,最后再将这些数据显示到窗口上。
  2. 我们可以将视频帧解码到纹理上,然后使用OpenGL对纹理内容进行变换,并将其绘制到窗口上。

虽然第一种方法是可行的,但是其性能会非常差。因为大部分任务都是在CPU上完成的,所以处理速度会非常慢,无法满足正常视频播放的需求。因此,我们应该采用第二种方法。

为了降低每一章节的难度,将第二种方法拆为两个章节,本章将说明如何解码到 OES 纹理上,下一章则说明如何添加滤镜。本章代码你可以在 LearnMediaCodec-DecodeToTextureOESActivity 中找到。

Surface、SurfaceTexture 以及 OES 纹理

MediaCodec 仅接受 Surface 作为解码的输出目标。那么问题来了,我们如何将纹理与Surface进行关联呢?幸运的是,Android为我们提供了 SurfaceTexture 和 OES 纹理这两个工具。关于OES纹理和SurfaceTexture的详细信息,你可以参考相关资料,这里就不再详细展开了。

  • SurfaceTexture
  • 谈一谈Android上的SurfaceTexture
  • Android Opengl OES 纹理渲染到 GL_TEXTURE_2D

下面的代码显示了如何更具一个 OES 纹理来创建 Surface

val numTexId = 1
val textureHandles = IntArray(numTexId)
GLES20.glGenTextures(numTexId, textureHandles, 0)
val surfaceTexture = SurfaceTexture(textureHandles[0])
val surface = Surface(surfaceTexture)

在这里插入图片描述
创建好 Surface 后,我们用这个 Surface 来构建 codec 即可:

codec.configure(videoFormat, surface, null, 0)

如此一来,MediaCodec 就能够将视频帧解码至 OES 纹理上了。

建立 EGL 环境

注意,我们调用了以 GLES20 开头的 OpenGL ES API,你需要保证调用 OpenGL API 前当前线程的 EGL 环境是正确的。这部分比较难理解,但目前你只需要记住就行了。
在代码中,我们构建了一个 EGLHelper 的类来帮助我们正确的建立 EGL 环境,代码如下:

class EGLHelper {private val TAG = "EGLHelper"private var mEGLDisplay = EGL14.EGL_NO_DISPLAYprivate var mEGLSurface = EGL14.EGL_NO_SURFACEprivate var mEGLContext: EGLContext? = nullfun setupEGL(width: Int, height: Int) {mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)if (mEGLDisplay === EGL14.EGL_NO_DISPLAY) {getError()throw java.lang.RuntimeException("unable to get EGL14 display")}val version = IntArray(2)if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) {mEGLDisplay = nullthrow java.lang.RuntimeException("unable to initialize EGL14")}// Configure EGL for pbuffer and OpenGL ES 2.0.  We want enough RGB bits// to be able to tell if the frame is reasonable.val attribList = intArrayOf(EGL14.EGL_RED_SIZE, 8,EGL14.EGL_GREEN_SIZE, 8,EGL14.EGL_BLUE_SIZE, 8,EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,EGLExt.EGL_RECORDABLE_ANDROID, 1,EGL14.EGL_NONE)val configs = arrayOfNulls<EGLConfig>(1)val numConfigs = IntArray(1)if (!EGL14.eglChooseConfig(mEGLDisplay,attribList,0,configs,0,configs.size,numConfigs,0)) {throw RuntimeException("eglChooseConfig failed")}// 创建 EGLContextval contextAttrs = intArrayOf(EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,EGL14.EGL_NONE)mEGLContext =EGL14.eglCreateContext(mEGLDisplay, configs[0], EGL14.EGL_NO_CONTEXT, contextAttrs, 0)if (mEGLContext == EGL14.EGL_NO_CONTEXT) {getError()throw RuntimeException("eglCreateContext failed")}// 创建 EGLSurfaceval surfaceAttrib = intArrayOf(EGL14.EGL_WIDTH, width,EGL14.EGL_HEIGHT, height,EGL14.EGL_NONE)mEGLSurface = EGL14.eglCreatePbufferSurface(mEGLDisplay, configs[0], surfaceAttrib, 0)if (mEGLSurface == EGL14.EGL_NO_SURFACE) {getError()throw RuntimeException("eglCreateWindowSurface failed")}}fun makeCurrent() {if (!EGL14.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext)) {getError()throw RuntimeException("eglMakeCurrent failed")}}fun releaseEGL() {if (!EGL14.eglMakeCurrent(mEGLDisplay,EGL14.EGL_NO_SURFACE,EGL14.EGL_NO_SURFACE,EGL14.EGL_NO_CONTEXT)) {throw java.lang.RuntimeException("eglMakeCurrent failed")}}fun release() {if (EGL14.eglGetCurrentContext() == mEGLContext) {// Clear the current context and surface to ensure they are discarded immediately.EGL14.eglMakeCurrent(mEGLDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,EGL14.EGL_NO_CONTEXT)}EGL14.eglDestroySurface(mEGLDisplay, mEGLSurface)EGL14.eglDestroyContext(mEGLDisplay, mEGLContext)EGL14.eglTerminate(mEGLDisplay)mEGLDisplay = nullmEGLContext = nullmEGLSurface = null}
}

上面代码看起来确实有够复杂的,但我并不想一一进行解释,关于 Android EGL 内容你可以参考 Android OpenGL 开发—EGL 的使用 。

  1. setupEGL 中调用 EGL14.eglCreatePbufferSurface 创建了一个 EGLSurface,width 和 height 作为属性一同作为输入。
  2. makeCurrent 函数将 GL Context 切换到当前线程中,而 releaseEGL 解绑当前的 GL Context。再重申一遍,在调用任何 OpenGL API 前应该确保当前线程中有 GL Context。
  3. release 释放 EGL 相关的资源,在退出时应该调用

Show me the code

基础知识已经铺垫完毕,接下来看具体的代码,并对代码做详细的解释

private fun decodeToSurfaceAsync() {val width = 720val height = 1280mEGLHelper.setupEGL(width, height)mEGLHelper.makeCurrent()count = 0// allocate texture idval numTexId = 1val textureHandles = IntArray(numTexId)GLES20.glGenTextures(numTexId, textureHandles, 0)checkGlError("glGenTextures")// bind texture id to oes and config itGLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureHandles[0])// ....checkGlError("init oes texture")// create SurfaceTexture with texture idmSurfaceTexture = SurfaceTexture(textureHandles[0])thread = HandlerThread("FrameHandlerThread")thread!!.start()//mSurfaceTexture!!.setOnFrameAvailableListener(this)mSurfaceTexture!!.setOnFrameAvailableListener({synchronized(lock) {// New frame available before the last frame was procesif (frameAvailable)Log.d(TAG, "Frame available before the last frame wframeAvailable = truelock.notifyAll()}}, Handler(thread!!.looper))// create Surface With SurfaceTexturemOutputSurface = Surface(mSurfaceTexture)// create and configure media extractorval mediaExtractor = MediaExtractor()resources.openRawResourceFd(R.raw.h264_720p).use {mediaExtractor.setDataSource(it)}val videoTrackIndex = 0mediaExtractor.selectTrack(videoTrackIndex)val videoFormat = mediaExtractor.getTrackFormat(videoTrackIndex// create and configure media codecval codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)val codecName = codecList.findDecoderForFormat(videoFormat)val codec = MediaCodec.createByCodecName(codecName)val maxInputSize = videoFormat.getInteger(MediaFormat.KEY_MAX_Ival inputBuffer = ByteBuffer.allocate(maxInputSize)val bufferInfo = MediaCodec.BufferInfo()val inputEnd = AtomicBoolean(false)val outputEnd = AtomicBoolean(false)// set codec callback in async modecodec.setCallback(object : MediaCodec.Callback() {override fun onInputBufferAvailable(codec: MediaCodec, inpuLog.d(TAG, "onInputBufferAvailable")val isExtractorReadEnd =getInputBufferFromExtractor(mediaExtractor, inputBuif (isExtractorReadEnd) {inputEnd.set(true)codec.queueInputBuffer(inputBufferId, 0, 0, 0,MediaCodec.BUFFER_FLAG_END_OF_STREAM)} else {val codecInputBuffer = codec.getInputBuffer(inputBucodecInputBuffer!!.put(inputBuffer)codec.queueInputBuffer(inputBufferId,0,bufferInfo.size,bufferInfo.presentationTimeUs,bufferInfo.flags)mediaExtractor.advance()}}override fun onOutputBufferAvailable(codec: MediaCodec,outputBufferId: Int,info: MediaCodec.BufferInfo) {if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAMoutputEnd.set(true)}if (info.size > 0) {Log.i(TAG, "onOutputBufferAvailable")codec.releaseOutputBuffer(outputBufferId, true)waitTillFrameAvailable()mEGLHelper.makeCurrent()mSurfaceTexture!!.updateTexImage()saveTextureToImage(textureHandles[0], width, heightcount++}}override fun onError(codec: MediaCodec, e: MediaCodec.Codece.printStackTrace()}override fun onOutputFormatChanged(codec: MediaCodec, formaLog.e(TAG, "onOutputFormatChanged")}})// configure with surfacecodec.configure(videoFormat, mOutputSurface, null, 0)// release EGL context in this threadmEGLHelper.releaseEGL()// start decodingcodec.start()// wait for processing to completewhile (!outputEnd.get() && count < 10) {Log.i(TAG, "count: $count")Thread.sleep(10)}mediaExtractor.release()codec.stop()codec.release()
}

代码大致说明:

  1. mEGLHelper.setupEGL(width, height) 建立 EGL 环境,接着使用 makeCurrent 方法切换当前线程的 GL Context,因为我们要开始调用 GL API 了
  2. GLES20.glGenTextures 它生成一个OpenGL纹理;GLES20.glBindTexture 将其绑定到OES(OpenGL ES)纹理上,然后设置纹理的各种参数。
  3. 创建一个SurfaceTexture,它可以从图像流(如摄像头)接收图像帧,并将其转换为OpenGL ES可用的纹理。
  4. 创建一个HandlerThread,这是一个带有Looper的线程,可以处理消息队列中的任务。
  5. 设置SurfaceTexture的帧可用监听器,当有新的帧可用时,它会被调用。
  6. 创建一个Surface,它是一个抽象的绘图表面,可以从SurfaceTexture接收图像帧。
  7. 创建并配置MediaExtractor,它可以从媒体文件中提取轨道和元数据。
  8. 创建并配置MediaCodec,它是一个用于编解码音频和视频数据的类。
  9. 设置MediaCodec的回调,这些回调在输入缓冲区可用、输出缓冲区可用、发生错误和输出格式改变时被调用。
  10. 配置MediaCodec,使其使用Surface作为输出表面。
  11. 启动MediaCodec,开始解码过程。
  12. 在解码过程结束后,释放资源。

现在讨论几个细节的问题。首先让我们看 onOutputBufferAvailable 中的逻辑

if (info.size > 0) {Log.i(TAG, "onOutputBufferAvailable")codec.releaseOutputBuffer(outputBufferId, true)waitTillFrameAvailable()mEGLHelper.makeCurrent()mSurfaceTexture!!.updateTexImage()saveTextureToImage(textureHandles[0], width, height, count)count++
}

调用 releaseOutputBuffer 后,MediaCodec 将渲染好这一帧,然后通过 onFrameAvailable 回调告知使用者帧已经画好了,可以被消费了。因此我们在这里调用了 waitTillFrameAvailable 来等待回调函数被调用。
当我们等到了一帧后,makeCurrent 切换 GL Context 到当前线程,然后调用 updateTexImage 将视频帧更新到 OES 纹理上。注意,调用updateTexImage 前必须得用 makeCurrent 绑定 GL Context。接着,我们将 OES 纹理上的内容保存到了本地,以便验证效果。

第二个问题,为什么要额外创建一个 HandlerThread 用来处理 setOnFrameAvailableListener 回调事件?如果不这么进行设置,在当前这个 case 下是会卡死的,我猜测是因为 waitTillFrameAvailable 阻塞了 MediaCodec 的回调线程,而 MediaCodec 也用这个线程来执行 onFrameAvailable,所以导致卡死。

总结

本文介绍了如何使用 MediaCodec 解码视频帧到 OES 纹理上,对 OES 纹理、SurfaceTexture 等概念做了介绍和说明。本文所有代码在 LearnMediaCodec-DecodeToTextureOESActivity

参考

  • SurfaceTexture
  • 谈一谈Android上的SurfaceTexture
  • Android Opengl OES 纹理渲染到 GL_TEXTURE_2D

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/9138.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【qt】QString字符串

前言&#xff1a; 这节很轻松&#xff0c;大家可以放心食用 ♪(&#xff65;ω&#xff65;)&#xff89; QString目录 一.与cString的区别二.隐式共享三.初始化四.判断是否为空串五.字符串的长度六.添加字符串1.尾加2.任意位置加 七.替换字符串八.修改字符串九.删除字符串1.清…

【C语言项目】贪吃蛇(上)

个人主页 ~ gitee仓库~ 欢迎大家来到C语言系列的最后一个篇章–贪吃蛇游戏的实现&#xff0c;当我们实现了贪吃蛇之后&#xff0c;我们的C语言就算是登堂入室了&#xff0c;基本会使用了&#xff0c;当然&#xff0c;想要更加熟练地使用还需要多多练习 贪吃蛇 一、目标二、需要…

(八)JSP教程——application对象

application对象是一个比较重要的对象&#xff0c;服务器在启动后就会产生这个application对象&#xff0c;所有连接到服务器的客户端application对象都是相同的&#xff0c;所有的客户端共享这个内置的application对象&#xff0c;直到服务器关闭为止。 可以使用application对…

MGRE 实验

需求&#xff1a;1、R2为ISP&#xff0c;其上只能配置IP地址。 2、R1-R2之间为HDLC封装 3、R2-R3之间为ppp封装&#xff0c;pap认证&#xff0c;R2为主认证方。 4、R2-R4之间为ppp封装&#xff0c;chap认证&#xff0c;R2为主认证方。 5、R1、R2、R3构建MGRE环境&#xff0…

sourceTree push失败

新电脑选择commit and push&#xff0c;报错了&#xff0c;不过commit成功&#xff0c;只不过push失败了。 原因是这个&#xff0c;PuTTYs cache and carry on connecting. 这里的ssh选择的是 PuTTY/Plink&#xff0c;本地没有这个ssh密钥&#xff0c;改换成openSSH&#xff…

Windows电脑搭建HarmonyOS NEXTDeveloper Preview2环境详解

Windows电脑搭建HarmonyOS NEXTDeveloper Preview2环境详解&#xff1a; HarmonyOS NEXT Preview系列教程基于Api11讲解-IT营大地老师 1 、电脑要求以及注意事项 操作系统 &#xff1a; Windows10 64 位、 Windows11 64 位 内存 &#xff1a; 8GB 及以上&#xff0c;推荐 16G…

新火种AI|AI让大家都变“土”了!

作者&#xff1a;一号 编辑&#xff1a;美美 AI不仅要把人变“土”&#xff0c;还要把人变多样。 这个世界&#xff0c;终究是变“土”了。 今年五一假期&#xff0c;一个名为“Remini”的AI修图APP火遍了全网。注意&#xff0c;是Remini&#xff0c;而不是Redmi&#xff0…

Redis学习汇总

目录 1.Linux环境下安装redis 2.redis的数据结构及命令 3.redis.conf配置文件常用配置 3.redis的事务操作 4.redis实现乐观锁 5.通过jedis操作redis 6.Springboot集成redis 7.自定义一个RedisTemplate 8.持久化策略 RDB和AOF 9.redis集群环境搭建 10.哨兵模式 11.缓…

解锁自动化办公新技能:Python实战应用-自动转发邮件到企业微信

&#x1f4e7; 【高效联络&#xff0c;信息不落空】 &#x1f4e7; 在这个信息爆炸的时代&#xff0c;作为企业的一员&#xff0c;我们如何从繁杂的邮件中迅速筛选出客户的心声、展会的新动向&#xff0c;同时又不遗漏每一份重要的咨询呢&#xff1f;秘诀就在我们的新策略里&a…

Day3 | Java基础 | 4常见类

Day3 | Java基础 | 4 常见类 基础版Object类equalshashCode&#xff08;散列码&#xff09;hashCode和equals clone方法String类 问题回答版Object类Object类的常见方法有哪些&#xff1f;和equals()的区别是什么&#xff1f;为什么要有hashCode&#xff1f;hashCode和equals的…

FFmpeg常用命令详解与实战指南

下载地址&#xff1a;Releases BtbN/FFmpeg-Builds (github.com) 1. 获取视频信息 使用FFmpeg获取视频信息是最基本的操作之一。你可以使用-i选项指定输入文件&#xff0c;然后使用FFmpeg内置的分析器来获取视频的各种信息&#xff0c;包括视频编解码器、音频编解码器、分辨…

JAVA文件的简单操作

文件IO&#xff08;Input和Output&#xff09; 文件的输入和输出是人为规定的&#xff0c;那么什么是输入&#xff1f;什么是输出捏&#xff1f;在这里统一已CPU为基准 例如&#xff1a;将文件由内存写入硬盘就是输出&#xff0c;有硬盘写入内存就是输入。可以总结为&#xff…

OpenHarmony 3.2 Release版本实战开发——Codec HDI适配过程

简介 OpenHarmony Codec HDI&#xff08;Hardware Device Interface&#xff09;驱动框架基于 OpenMax 实现了视屏硬件编解码驱动&#xff0c;提供 Codec 基础能力接口供上层媒体服务调用&#xff0c;包括获取组件编解码能力、创建组件、参数设置、数据的轮转和控制、以及销毁…

C++反汇编,指针和内存分配细节,面试题05

文章目录 20. 指针 vs 引用21. new vs malloc 20. 指针 vs 引用 指针是实体&#xff0c;占用内存空间&#xff0c;逻辑上独立&#xff1b;引用是别名&#xff0c;与变量共享内存空间&#xff0c;逻辑上不独立。指针定义时可以不初始化&#xff1b;引用定义时必须初始化。指针的…

Cmake编译源代码生成库文件以及使用

在项目实战中&#xff0c;通过模块化设计能够使整个工程更加简洁明了。简单的示例如下&#xff1a; 1、项目结构 project_folder/├── CMakeLists.txt├── src/│ ├── my_library.cpp│ └── my_library.h└── app/└── main.cpp2、CMakeList文件 # CMake …

Python sqlite3库 实现 数据库基础及应用 输入地点,可输出该地点的爱国主义教育基地名称和批次的查询结果。

目录 【第11次课】实验十数据库基础及应用1-查询 要求: 提示: 运行结果&#xff1a; 【第11次课】实验十数据库基础及应用1-查询 声明&#xff1a;著作权归作者所有。商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处。 1.简答题 数据库文件Edu_Base.db&#…

国内唯一!阿里云荣膺MongoDB“2024年度DBaaS认证合作伙伴奖”

近日&#xff0c;在MongoDB用户大会纽约站上&#xff0c;阿里云荣膺MongoDB“2024年度DBaaS认证合作伙伴奖”。这是阿里云连续第五年斩获MongoDB合作伙伴奖项&#xff0c;也是唯一获此殊荣的中国云厂商。 MongoDB是当今全球最受欢迎的非关系型数据库之一。凭借灵活的模式和丰富…

Unity 修复Sentinel key not found (h0007)错误

这个问题是第二次遇到了&#xff0c;上次稀里糊涂的解决了&#xff0c;也没当回事&#xff0c;这次又跑出来了&#xff0c;网上找的教程大部分都是出自一个人。 1.删除这个路径下的文件 C:\ProgramData\SafeNet Sentinel&#xff0c;注意ProgramData好像是隐藏文件 2.在Windows…

Redis(安装及配置)

1.什么是redis Redis 全称 Remote Dictionary Server&#xff08;即远程字典服务&#xff09;&#xff0c;它是一个基于内存实现的键值型非关系&#xff08;NoSQL&#xff09;数据库&#xff0c;由意大利人 Salvatore Sanfilippo 使用 C 语言编写。 2.优势 性能极高&#xff…

如何进行资产梳理

前言 为什么要进行资产梳理&#xff1f; 资产梳理方式一: 一、安全防护设备资产 二、对外开放服务项目资产 三、项目外包业务流程资产 资产梳理方式二: 一、业务资源梳理 二、设备资产梳理 三、第三方的服务信息梳理 风险梳理 风险有哪些&#xff1f; 一,账号权限风…