Android MediaCodec 简明教程(九):使用 MediaCodec 解码到纹理,使用 OpenGL ES 进行处理,并编码为 MP4 文件

系列文章目录

  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 文件
  7. Android MediaCodec 简明教程(七):使用 MediaCodec 解码到 OES 纹理上
  8. Android MediaCodec 简明教程(八):使用 MediaCodec 解码到纹理,使用 OpenGL ES 进行处理并显示在 GLSurfaceView 上

前言

在上一章节,我们已经探讨了如何使用 OpenGL ES 处理解码后的纹理,将彩色画面转换为灰色画面,并在 GLSurfaceView 上展示。在本章节,我们将研究如何将处理后的视频帧保存为本地的 MP4 文件。
本文所有代码可以在 DecodeEditEncodeActivity.kt 找到

数据流

在这里插入图片描述
整体流程可以大致描述为: Demuxer -> MediaCodec Decoder -> Edit -> MediaCodec Encoder -> Muxer

我们选择 Surface 作为视频数据传递的介质,其中 Surface 中的 Buffer Queue 起着关键作用。在这个流程中,我们需要关注每个 Surface 的生产者和消费者,以便清晰地理解数据的流向。

  1. Demuxer 负责解封装,将压缩数据传递给 MediaCodec 解码器。
  2. MediaCodec 解码器负责解码,将解码后的数据写入 Surface 的 Buffer Queue 中。
  3. SurfaceTexture 作为消费者获取到 Buffer 后,将视频数据绘制到 OES 纹理上。
  4. 使用 OpenGL ES API 将 OES 纹理绘制到编码器的 Surface 上,绘制过程中可以进行图像处理工作。此时,OpenGL 是该 Surface Buffer Queue 的生产者。
  5. MediaCodec 编码器收到 Buffer 后负责将其编码压缩。
  6. 编码压缩后的数据由 Muxer 进行封装,最终写入 MP4 文件中。

通过以上流程,视频数据经过解封装、解码、编辑、编码和封装等步骤,最终生成了一个完整的视频文件。

发生了编码卡死的问题

我在编写本章代码时遇到了卡死的问题,线程卡在 glColor 或者 glDrawElements 等 OpenGL 绘制 API 上,并且在华为手机上是必现的,但在小米手机上却没能复现。经过排查,我找到了原因:编码器的 Surface Buffer Queue 满了,导致在调用绘制 api 时,阻塞了当前线程。

那么,问题一:为什么编码器的 Surface 满了?这是因为我们使用的是 MediaCodec 的异步模式,无论是编码还是解码;并且通过 Debug 你就会知道,编码器和解码器虽然是两个 MediaCodec 实例,但它们的回调函数却在同一个线程中执行。于是乎,当出现解码器任务比较多的时候,编码器的 Surface 就可能满,导致卡死。如下图。
在这里插入图片描述

问题二,为什么华为手机上必现,小米手机却是正常的。通过日志我发现华为手机上 Surface Buffer Queue 大小为 5,而小米手机是 15,这就导致了小米手机上比较难出现 Buffer Quque 满了导致卡死的问题,但实际上也只是概率比较小,在极限情况仍然可能出现卡死的问题。

知道卡死的原因后如何修复?其实也很简单,我们让编解码器的回调函数执行在不同线程下即可,这部分在代码中会有说明。

Show me the code

先看下整体流程的代码:

private fun decodeASync() {var done = AtomicBoolean(false)// setup extractorval mediaExtractor = MediaExtractor()resources.openRawResourceFd(R.raw.h264_720p).use {mediaExtractor.setDataSource(it)}val videoTrackIndex = 0mediaExtractor.selectTrack(videoTrackIndex)val inputVideoFormat = mediaExtractor.getTrackFormat(videoTrackIndex)val videoWidth = inputVideoFormat.getInteger(MediaFormat.KEY_WIDTH)val videoHeight = inputVideoFormat.getInteger(MediaFormat.KEY_HEIGHT)Log.i(TAG, "get video width: $videoWidth, height: $videoHeight")// setup muxerval outputDir = externalCacheDirval outputName = "decode_edit_encode_test.mp4"val outputFile = File(outputDir, outputName)val muxer = MediaMuxer(outputFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)var muxerSelectVideoTrackIndex = 0// create encoderval mimeType = MediaFormat.MIMETYPE_VIDEO_AVCval outputFormat = MediaFormat.createVideoFormat(mimeType, videoWidth, videoHeight)val colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatSurfaceval videoBitrate = 2000000val frameRate = 30val iFrameInterval = 60outputFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat)outputFormat.setInteger(MediaFormat.KEY_BIT_RATE, videoBitrate)outputFormat.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate)outputFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval)val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)val encodeCodecName = codecList.findEncoderForFormat(outputFormat)val encoder = MediaCodec.createByCodecName(encodeCodecName)Log.i(TAG, "create encoder with format: $outputFormat")// set encoder callbackencoder.setCallback(...)encoder.configure(outputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)// create input surface and egl context for opengl renderingval inputSurface = InputSurface(encoder.createInputSurface())inputSurface.makeCurrent()// create decoderval decodeCodecName = codecList.findDecoderForFormat(inputVideoFormat)val decoder = MediaCodec.createByCodecName(decodeCodecName)// create output surface textureval textureRenderer = TextureRenderer2()val surfaceTexture = SurfaceTexture(textureRenderer.texId)val outputSurface = Surface(surfaceTexture)inputSurface.releaseEGLContext()val thread = HandlerThread("FrameHandlerThread")thread.start()surfaceTexture.setOnFrameAvailableListener({Log.d(TAG, "setOnFrameAvailableListener")synchronized(lock) {if (frameAvailable)Log.d(TAG,"Frame available before the last frame was process...we dropped some frames")frameAvailable = truelock.notifyAll()}}, Handler(thread.looper))val texMatrix = FloatArray(16)// set callbackval maxInputSize = inputVideoFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)val inputBuffer = ByteBuffer.allocate(maxInputSize)val bufferInfo = MediaCodec.BufferInfo()val videoDecoderHandlerThread = HandlerThread("DecoderThread")videoDecoderHandlerThread.start()decoder.setCallback(..., Handler(videoDecoderHandlerThread.looper))// config decoderdecoder.configure(inputVideoFormat, outputSurface, null, 0)decoder.start()encoder.start()// wait for donewhile(!done.get()){Thread.sleep(10)}Log.d(TAG, "finished")// release resourcesLog.d(TAG, "release resources...")mediaExtractor.release()decoder.stop()decoder.release()surfaceTexture.release()outputSurface.release()encoder.stop()encoder.release()muxer.stop()muxer.release()Log.d(TAG, "release resources end...")
}
  1. 创建一个MediaExtractor实例,用于从原始资源文件中提取视频轨道。
  2. 选择要处理的视频轨道,并获取其格式、宽度和高度。
  3. 创建一个 MediaMuxer 实例,用于将编码后的视频数据写入到输出文件。
  4. 创建一个 MediaCodec 实例,用于编码视频数据。编码器的配置包括视频格式、颜色格式、比特率、帧率和关键帧间隔。
  5. 利用 MediaCodec Encoder 创建一个输入 Surface 和一个 EGL Context,用于 OpenGL 渲染。注意这里,我们创建了一个 EGL Context,也就意味着可以在当前线程调用 OpenGL 相关的 API。
  6. 创建一个 MediaCodec 解码器,用于解码输入视频数据。
  7. 创建一个 SurfaceTexture,并通过它创建一个解码输出的 Surface。注意,创建 SurfaceTexture 前我们创建了 TextureRenderer2,而 TextureRenderer2.texId 是通过 OpenGL API 来创建的,我们要确保当前线程有 EGL Context 才能够调用 GL API;此外,我们还创建了一个线程,用来setOnFrameAvailableListener 回调函数,原因在上一章中我已经解释过了,不再赘述。
  8. 设置解码器的回调函数,用于处理解码后的视频帧。注意,我们创建了一个解码线程用来处理解码器的回调函数,原因正如我在分析卡死问题时提到的那样。
  9. 配置解码器,并启动解码器和编码器。
  10. 在一个循环中等待解码和编码过程完成。
  11. 释放所有使用的资源,包括MediaExtractor、解码器、表面纹理、输出表面、编码器和MediaMuxer。

上面的过程除了一些 GL Context、线程等细节外,整体上还是比较容易理解的。接下来,我们看解码器和编码器的回调函数,这才是真正干活的地方。

encoder.setCallback(object : MediaCodec.Callback() {override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {}override fun onOutputBufferAvailable(codec: MediaCodec,index: Int,info: MediaCodec.BufferInfo) {val isEncodeDone = (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0if (isEncodeDone) {info.size = 0done.set(true)}// got encoded frame, write it to muxerif (info.size > 0) {val encodedData = codec.getOutputBuffer(index)muxer.writeSampleData(muxerSelectVideoTrackIndex, encodedData!!, info)codec.releaseOutputBuffer(index, info.presentationTimeUs * 1000)}}override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {}override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {muxerSelectVideoTrackIndex = muxer.addTrack(format)muxer.start()}});

编码器的回调函数逻辑比较简单:

  1. onOutputBufferAvailable ,当编码器的输出缓冲区有数据可用时,此函数会被调用。在这个函数中,你可以从输出缓冲区获取编码后的数据。在这段代码中,首先检查是否已经到达流的结束,如果是,则设置done标志为true。然后,如果输出缓冲区的数据大小大于0,就将编码后的数据写入到muxer,然后释放输出缓冲区。
  2. onOutputFormatChanged,当编码器的输出格式发生改变时,此函数会被调用。在这段代码中,当输出格式改变时,将新的格式添加到muxer,然后启动muxer。
decoder.setCallback(object : MediaCodec.Callback() {override fun onInputBufferAvailable(codec: MediaCodec, inputBufferId: Int) {val isExtractorReadEnd =getInputBufferFromExtractor(mediaExtractor, inputBuffer, bufferInfo)if (isExtractorReadEnd) {codec.queueInputBuffer(inputBufferId, 0, 0, 0,MediaCodec.BUFFER_FLAG_END_OF_STREAM)} else {val codecInputBuffer = codec.getInputBuffer(inputBufferId)codecInputBuffer!!.put(inputBuffer)codec.queueInputBuffer(inputBufferId,0,bufferInfo.size,bufferInfo.presentationTimeUs,bufferInfo.flags)mediaExtractor.advance()}}override fun onOutputBufferAvailable(codec: MediaCodec,index: Int,info: MediaCodec.BufferInfo) {if (info.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG != 0) {codec.releaseOutputBuffer(index, false)return}val render = info.size > 0codec.releaseOutputBuffer(index, render)if (render) {waitTillFrameAvailable()val ptsNs = info.presentationTimeUs * 1000inputSurface.makeCurrent()surfaceTexture.updateTexImage()surfaceTexture.getTransformMatrix(texMatrix)// draw oes text to input surfacetextureRenderer.draw(videoWidth, videoWidth, texMatrix, getMvp())inputSurface.setPresentationTime(ptsNs)inputSurface.swapBuffers()inputSurface.releaseEGLContext()}if (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) {encoder.signalEndOfInputStream()}}override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {}override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {}}, Handler(videoDecoderHandlerThread.looper))
  1. onInputBufferAvailable,当解码器需要输入数据时调用。在该回调函数中,首先通过调用getInputBufferFromExtractor()方法从MediaExtractor中获取输入数据,并将数据放入解码器的输入缓冲区中。如果已经读取到了Extractor的末尾,则向解码器的输入缓冲区发送结束标志。否则,将输入数据放入解码器的输入缓冲区,并调用advance()方法继续读取下一帧数据。
  2. onOutputBufferAvailable,当解码器的输出缓冲区有数据可用时,此函数会被调用。在这个函数中,你可以从输出缓冲区获取解码后的数据。在这段代码中,首先检查输出缓冲区的数据是否是编解码器配置数据,如果是,则释放输出缓冲区并返回。然后,如果输出缓冲区的数据大小大于0,就将解码后的数据渲染到 Surface。最后,如果已经到达流的结束,就向编码器发送流结束的信号。注意,为了绘制数据到 Surface 上,我们要确保当前线程有 EGL Context 环境,因此调用了 inputSurface.makeCurrent();接着,inputSurface.setPresentationTime 设置 PTS,然后使用 inputSurface.swapBuffers() 来交换 Buffer,告诉编码器来了一帧数据;最后 inputSurface.releaseEGLContext 来解除当前的 EGL 环境。

参考

  • DecodeEditEncodeActivity.kt
  • android-decodeencodetest

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

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

相关文章

可变参数

自学python如何成为大佬(目录):https://blog.csdn.net/weixin_67859959/article/details/139049996?spm1001.2014.3001.5501 在Python中,还可以定义可变参数。可变参数也称不定长参数,即传入函数中的实际参数可以是任意多个。 定义可变参数时&#xf…

【Linux】Linux环境基础开发工具_3

文章目录 四、Linux环境基础开发工具2. vim3. gcc和g动静态库的理解 未完待续 四、Linux环境基础开发工具 2. vim vim 怎么批量化注释呢?最简单的方法就是在注释开头和结尾输入 /* 或 */ 。当然也可以使用快捷键: Ctrl v 按 hjkl 光标移动进行区域选择…

【LeetCode】38.外观数列

外观数列 题目描述: 「外观数列」是一个数位字符串序列,由递归公式定义: countAndSay(1) "1"countAndSay(n) 是 countAndSay(n-1) 的行程长度编码。 行程长度编码(RLE)是一种字符串压缩方法&#xff0c…

LeetCode374猜数字大小

题目描述 我们正在玩猜数字游戏。猜数字游戏的规则如下:我会从 1 到 n 随机选择一个数字。 请你猜选出的是哪个数字。如果你猜错了,我会告诉你,我选出的数字比你猜测的数字大了还是小了。你可以通过调用一个预先定义好的接口 int guess(int n…

民国漫画杂志《时代漫画》第33期.PDF

时代漫画33.PDF: https://url03.ctfile.com/f/1779803-1248635648-d8235b?p9586 (访问密码: 9586) 《时代漫画》的杂志在1934年诞生了,截止1937年6月战争来临被迫停刊共发行了39期。 ps: 资源来源网络!

JVM优化之垃圾收集器

JVM优化之垃圾收集器 Serial收集器Parallel Scavenge收集器ParNew收集器 如果说垃圾收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。 没有最好的垃圾收集器,只有根据具体应用场景选择适合自己的垃圾收集器。 Serial收集器 #使用方…

apache大数据各组件部署搭建(超级详细)

apache大数据数仓各组件部署搭建 第一章 环境准备 1. 机器规划 准备3台服务器用于集群部署,系统建议CentOS7+,2核8G内存 172.19.195.228 hadoop101 172.19.195.229 hadoop102 172.19.195.230 hadoop103 [root@hadoop101 ~]# cat /etc/redhat-release CentOS Linux rele…

飞腾+FPGA多U多串全国产工控主机

飞腾多U多串工控主机基于国产化飞腾高性能8核D2000处理器平台的国产自主可控解决方案,搭载国产化固件,支持UOS、银河麒麟等国产操作系统,满足金融系统安全运算需求,实现从硬件、操作系统到应用的完全国产、自主、可控,是国产金融信…

基于51单片机多功能防盗报警proteus仿真( proteus仿真+程序+设计报告+原理图+讲解视频)

基于51单片机多功能防盗报警系统 1. 主要功能:2. 讲解视频:3. 仿真4. 程序代码5. 设计报告6. 原理图7. 设计资料内容清单&&下载链接 基于51单片机多功能防盗报警系统( proteus仿真程序设计报告原理图讲解视频) 仿真图proteus8.9及以上…

Spring boot 随笔 1 DatasourceInitializer

0. 为啥感觉升级了 win11 之后,电脑像是刚买回来的,很快 这篇加餐完全是一个意外:时隔两年半,再看 Springboot-quartz-starter 集成实现的时候,不知道为啥我的h2 在应用启动的时候,不能自动创建quartz相关…

设计师竞品分析全攻略:高效指南!

在我们的设计工作中,竞争产品的分析非常重要。通过对市场上竞争产品的分析,设计师可以在短时间内快速了解其产品在整个市场中的定位。 同时,竞争产品分析也为设计师提供了可量化的评价标准,避免了过于主观的观点,帮助…

管道液位传感器怎么接线

如今,随着科技智能化的发展,检测液位的方法也越来越多,管道液位传感器是检测水管缺水的传感器,利用光学原理,通过液体在水和空气中的折射不同来判断传感器位置液位的变化,管道液位传感器怎么接线&#xff1…

计算机组成原理-----实验1

实 验 报 告 实验一 基本运算器实验 1、实验目的 (一)了解运算器的组成结构; (二) 掌握运算器的工作原理; (三)熟悉运算器的数据传送通路; (四)按…

如何搭建B2B2C商城系统?开发语言、功能扩展、优势分析

如今,越来越多的企业意识到单靠第三方电商平台不足以快速实现品牌曝光和销售增加,相反还有诸多限制。 因此,搭建一个B2B2C商城也就成为企业发展业务的首选,既可以满足自营和商家入驻的需求,功能操作又灵活&#xff0c…

IDEA 安装BPMN插件-Activiti BPMN visualizer

IDEA安装BPMN插件 idea 18版本之前idea 18版本之后安装插件 推荐使用 Activiti BPMN visualizer插件注意 创建bpmn文件使用可视化面板 在可视化面板中右键可创建各种节点每个节点上都有连线 删除 设置的按钮 保存图片 idea 18版本之前 可以通过搜索插件actiBPMN直接安装 idea…

【LeetCode算法】第101题:对称二叉树

目录 一、题目描述 二、初次解答 三、官方解法 四、总结 一、题目描述 二、初次解答 1. 思路:递归判定左子树和右子树是否对称。用一个新函数sym来递归判定左子树和右子树是否对称。该函数细节:判定当前传入的两个根节点是否为空,若均为空…

C/S模型测试

1 1.1代码示例 #include<stdio.h> #include<stdio.h>#include <sys/types.h> /* See NOTES */ #include <sys/socket.h>#include <netinet/in.h> #include <netinet/ip.h> /* superset of previous */ #include <arpa/inet.…

推荐系统三十六式学习笔记:02|个性化推荐系统有哪些绕不开的经典问题?

目录 推荐系统的问题模式评分预测行为预测 几个常见顽疾1、冷启动问题2、探索与利用问题安全问题 总结 推荐系统的问题模式 推荐系统的使命是为用户和物品建立连接&#xff0c;建立的方式是提前找出哪些隐藏的连接呈现给用户&#xff0c;这是一个预测问题&#xff1b;所以推荐…

IMU状态预积分代码实现 —— IMU状态预积分类

IMU状态预积分代码实现 —— IMU状态预积分类 实现IMU状态预积分类 实现IMU状态预积分类 首先&#xff0c;实现预积分自身的结构。一个预积分类应该存储一下数据&#xff1a; 预积分的观测量 △ R ~ i j , △ v ~ i j , △ p ~ i j \bigtriangleup \tilde{R} _{ij},\bigtrian…

2020 6.s081——Lab2:system calls

左岸的一座白色环形阶梯 浪人正在用和弦练习忧郁 晨曦下的少女听着吉他旋律 在许愿池边巴洛克式的叹息 ——许愿池的希腊少女 完整代码见&#xff1a;SnowLegend-star/6.s081 at syscall (github.com) System call tracing (moderate) 这个实验要求我们跟踪系统调用。 感觉实…