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

news2024/12/23 0:26:01

系列文章目录

  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_DISPLAY
    private var mEGLSurface = EGL14.EGL_NO_SURFACE
    private var mEGLContext: EGLContext? = null

    fun 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 = null
            throw 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")
        }

        // 创建 EGLContext
        val 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")
        }

        // 创建 EGLSurface
        val 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 = null
        mEGLContext = null
        mEGLSurface = 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 = 720
    val height = 1280
    mEGLHelper.setupEGL(width, height)
    mEGLHelper.makeCurrent()
    count = 0
    // allocate texture id
    val numTexId = 1
    val textureHandles = IntArray(numTexId)
    GLES20.glGenTextures(numTexId, textureHandles, 0)
    checkGlError("glGenTextures")
    // bind texture id to oes and config it
    GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureHandles[0])
    // ....
    
    checkGlError("init oes texture")
    // create SurfaceTexture with texture id
    mSurfaceTexture = SurfaceTexture(textureHandles[0])
    thread = HandlerThread("FrameHandlerThread")
    thread!!.start()
    //mSurfaceTexture!!.setOnFrameAvailableListener(this)
    mSurfaceTexture!!.setOnFrameAvailableListener({
        synchronized(lock) {
            // New frame available before the last frame was proces
            if (frameAvailable)
                Log.d(TAG, "Frame available before the last frame w
            frameAvailable = true
            lock.notifyAll()
        }
    }, Handler(thread!!.looper))
    // create Surface With SurfaceTexture
    mOutputSurface = Surface(mSurfaceTexture)
    // create and configure media extractor
    val mediaExtractor = MediaExtractor()
    resources.openRawResourceFd(R.raw.h264_720p).use {
        mediaExtractor.setDataSource(it)
    }
    val videoTrackIndex = 0
    mediaExtractor.selectTrack(videoTrackIndex)
    val videoFormat = mediaExtractor.getTrackFormat(videoTrackIndex
    // create and configure media codec
    val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
    val codecName = codecList.findDecoderForFormat(videoFormat)
    val codec = MediaCodec.createByCodecName(codecName)
    val maxInputSize = videoFormat.getInteger(MediaFormat.KEY_MAX_I
    val inputBuffer = ByteBuffer.allocate(maxInputSize)
    val bufferInfo = MediaCodec.BufferInfo()
    val inputEnd = AtomicBoolean(false)
    val outputEnd = AtomicBoolean(false)
    // set codec callback in async mode
    codec.setCallback(object : MediaCodec.Callback() {
        override fun onInputBufferAvailable(codec: MediaCodec, inpu
            Log.d(TAG, "onInputBufferAvailable")
            val isExtractorReadEnd =
                getInputBufferFromExtractor(mediaExtractor, inputBu
            if (isExtractorReadEnd) {
                inputEnd.set(true)
                codec.queueInputBuffer(
                    inputBufferId, 0, 0, 0,
                    MediaCodec.BUFFER_FLAG_END_OF_STREAM
                )
            } else {
                val codecInputBuffer = codec.getInputBuffer(inputBu
                codecInputBuffer!!.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_STREAM
                outputEnd.set(true)
            }
            if (info.size > 0) {
                Log.i(TAG, "onOutputBufferAvailable")
                codec.releaseOutputBuffer(outputBufferId, true)
                waitTillFrameAvailable()
                mEGLHelper.makeCurrent()
                mSurfaceTexture!!.updateTexImage()
                saveTextureToImage(textureHandles[0], width, height
                count++
            }
        }
        override fun onError(codec: MediaCodec, e: MediaCodec.Codec
            e.printStackTrace()
        }
        override fun onOutputFormatChanged(codec: MediaCodec, forma
            Log.e(TAG, "onOutputFormatChanged")
        }
    })
    // configure with surface
    codec.configure(videoFormat, mOutputSurface, null, 0)
    // release EGL context in this thread
    mEGLHelper.releaseEGL()
    // start decoding
    codec.start()
    // wait for processing to complete
    while (!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.coloradmin.cn/o/1657679.html

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

相关文章

安装oh-my-zsh(命令行工具)

文章目录 一、安装zsh、git、wget二、安装运行脚本1、curl/wget下载2、手动下载 三、切换主题1、编辑配置文件2、切换主题 四、安装插件1、zsh-syntax-highlighting&#xff08;高亮语法错误&#xff09;2、zsh-autosuggestions&#xff08;自动补全&#xff09; 五、更多优化配…

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

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

【bug记录】清除僵尸进程,释放GPU显存

目录 1. 为什么会出现这种情况&#xff1f;2. 解决方案方法一&#xff1a;使用 fuser 命令方法二&#xff1a; 3. 小贴士 在进行深度学习或其他需要GPU支持的任务时&#xff0c;我们有时会发现虽然没有可见的进程在执行&#xff0c;但GPU资源却意外地被占用。这种情况往往会阻碍…

Redis(无中心化集群搭建)

文章目录 1.无中心化集群1.基本介绍2.集群说明 2.基本环境搭建1.部署规划&#xff08;6台服务器&#xff09;2.首先删除上次的rdb和aof文件&#xff08;对之前的三台服务器都操作&#xff09;1.首先分别登录命令行&#xff0c;关闭redis2.清除/root/下的rdb和aof文件3.把上次的…

(23)实时采集微信消息(基于主窗体)-微信UI自动化(.Net+C#)

整理 | 小耕家的喵大仙 出品 | CSDN&#xff08;ID&#xff1a;lichao19897314&#xff09; Q Q | 978124155 往期知识回顾 (1)开启探索微信自动化之路-微信UI自动化(.NetC#) (2)初始化微信窗体UI自动化实例-微信UI自动化(.NetC#) (3)采用热键终止微信采集任务-微信UI自动…

linux调试

文章目录 1. 使用打印来调试1.1 重定向1.2 标准预定义宏1.3 日志代码 2. 内核异常2.1 内核打印2.1.1 打印级别2.1.2 跟踪异常2.1.3 动态打印2.1.4 RAM console 2.2 OOPS2.2.1 有源代码的情况2.2.2 没有源代码的情况 3 查看日志4 工具调试 1. 使用打印来调试 1.1 重定向 2>…

HDLbits 刷题 -- Exams/m2014 q3

Consider the function f shown in the Karnaugh map below. Implement this function. d is dont-care, which means you may choose to output whatever value is convenient. 译&#xff1a;考虑下面卡诺图中显示的函数f。 实现这个函数。D是dont-care&#xff0c;这意味着…

【数据结构】栈(Stack)和队列(Queue)

文章目录 栈一、栈的概念及结构二、栈的特点三、栈的实现1.初始化栈2.判断栈空3.入栈4.出栈5.取栈顶元素6.栈的元素个数7.销毁 队列一、队列的概念及结构二、队列的特点三、队列的实现1.初始化2.入队3.出队4.判断队空5.取队头元素6.取队尾元素 总结 栈 一、栈的概念及结构 栈…

Python ValueError: bad transparency mask

修改前 修复后 运行正常 from PIL import Image# 读取图片 #报错信息解决ValueError: bad transparency mask--相关文档地址https://blog.csdn.net/kalath_aiur/article/details/103945309 #1. 检查 alpha 通道是否是一个有效的掩码。如果不是&#xff0c;则需要对 alpha 通道…

随机梯度下降SGD的理解和现象分析

提出问题&#xff1a;令人疑惑的损失值 在某次瞎炼丹的过程中&#xff0c;出现了如下令人疑惑的损失值变化图像&#xff1a; 嗯&#xff0c;看起来还挺工整&#xff0c;来看看前10轮打印的具体损失值变化&#xff1a; | epoch 1 | iter 5 / 10 | time 1[s] | loss 2.3137 |…

Hadoop3:HDFS的Shell操作(常用命令汇总)

一、简介 什么是HDFS的Shell操作&#xff1f; 很简单&#xff0c;就是在Linux的终端&#xff0c;通过命令来操作HDFS。 如果&#xff0c;你们学习过git、docker、k8s&#xff0c;应该会发现&#xff0c;这些命令的特点和shell命令非常相似 二、常用命令 1、准备工作相关命令…

【VTKExamples::Rendering】第一期 TestAmbientSpheres(环境照明系数)

很高兴在雪易的CSDN遇见你 VTK技术爱好者 QQ:870202403 公众号:VTK忠粉 前言 本文分享VTK样例TestAmbientShperes,介绍环境照明系数对Actor颜色的影响,希望对各位小伙伴有所帮助! 感谢各位小伙伴的点赞+关注,小易会继续努力分享,一起进步! 你的点赞就是我的动…

Qt模型视图代理之QListView使用的简单介绍

往期回顾&#xff1a; Qt绘图与图形视图之Graphics View坐标系的简单介绍_graphics view 坐标系-CSDN博客 Qt模型视图代理之MVD(模型-视图-代理)概念的简单介绍-CSDN博客 Qt模型视图代理之QTableView应用的简单介绍-CSDN博客 Qt模型视图代理之QListView使用的简单介绍 一、最…

视频监控平台:交通运输标准JTT808设备SDK接入源代码函数分享

目录 一、JT/T 808标准简介 &#xff08;一&#xff09;概述 &#xff08;二&#xff09;协议特点 1、通信方式 2、鉴权机制 3、消息分类 &#xff08;三&#xff09;协议主要内容 1、位置信息 2、报警信息 3、车辆控制 4、数据转发 二、代码和解释 &#xff08;一…

电商核心技术揭秘53:社群营销的策略与实施

相关系列文章 电商技术揭秘相关系列文章合集&#xff08;1&#xff09; 电商技术揭秘相关系列文章合集&#xff08;2&#xff09; 电商技术揭秘相关系列文章合集&#xff08;3&#xff09; 电商技术揭秘四十一&#xff1a;电商平台的营销系统浅析 电商技术揭秘四十二&#…

手机传输助手有哪些?如何快速互传文件?

手机已经成为我们生活和工作中不可或缺的一部分&#xff0c;而手机传输助手&#xff0c;作为一种帮助我们在不同设备之间快速、方便地共享文件的工具&#xff0c;其重要性不言而喻。无论是在工作中需要将文件从电脑传输到手机&#xff0c;还是在生活中想要与朋友分享美好的瞬间…

【智能算法】人工原生动物优化算法(APO)原理及实现

目录 1.背景2.算法原理2.1算法思想2.2算法过程 3.结果展示4.参考文献5.获取代码 1.背景 2024年&#xff0c;X Wang受到自然界原生动物启发&#xff0c;提出了人工原生动物优化算法&#xff08; Artificial Protozoa Optimizer, APO&#xff09;。 2.算法原理 2.1算法思想 AP…

一文玩转Vue3参数传递——全栈开发之路--前端篇(8)

全栈开发一条龙——前端篇 第一篇&#xff1a;框架确定、ide设置与项目创建 第二篇&#xff1a;介绍项目文件意义、组件结构与导入以及setup的引入。 第三篇&#xff1a;setup语法&#xff0c;设置响应式数据。 第四篇&#xff1a;数据绑定、计算属性和watch监视 第五篇 : 组件…

导弹追踪效果实现_unity基础开发教程

Unity开发中导弹追踪的原理与实现 前言原理逻辑实现导弹逻辑目标赋值 应用效果结语 前言 ⭕在之前的一个项目的开发中&#xff0c;需要加入一个导弹追踪的游戏功能&#xff0c;且还要实现不规则发射路径&#xff0c;但是这种功能是第一次做&#xff0c;经过查阅资料和询问做过的…

java io包

InputStream InputStream 是 Java I/O 中所有输入流的抽象基类&#xff0c;它定义了读取字节流的基本方法。InputStream 类提供了许多子类&#xff0c;用于从不同的数据源读取数据&#xff0c;如文件、网络连接、内存等。 InputStream 提供了以下常用的方法&#xff1a; int…