Android MediaCodec 简明教程(五):使用 MediaCodec 编码 ByteBuffer 数据,并保存为 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 播放视频

文章目录

  • 系列文章目录
  • 前言
  • 编码流程概述
  • MediaCodec 异步模式编码
    • 创建编码器
    • 设置编码回调
    • 编码器 Configure
    • 创建 Muxer
    • 开始编码的工作
    • 循环地编码视频帧
    • 等待编码结束,释放资源
  • 总结
  • 参考


前言

前面我们了解了 MediaCodec 解码的具体使用流程,包括异步和同步模式、解码到 ByteBuffers 或者 Surface。本章开始,我们将开始学习如何使用 MediaCodec 进行编码。

与解码类似,MediaCodec 编码的输入支持 ByteBuffer 或者 Surface。 遵循循序渐进的原则,我们从最简单的一种情况开始讲起:MediaCodec 编码过程中,输入的图像数据存放在 ByteBuffer 中。

编码流程概述

首先,我们需要创建对应的 MediaCodec 编码器,并进行正确的 configure。这一步中,你要考虑一些编码的参数,包括视频的分辨率、帧率、比特率、color format 等。其中 color format 非常重要,它描述了送给编码器的数据是如何排列的,编码器根据这个属性来读取数据。

接着,为了将编码后的数据保存为 MP4 文件,我们创建 MediaMuxer 来进行封装的工作。

当 MediaCodec 编码器和 MediaMuxer 准备好后,就能够开始编码了:将视频数据送给 Codec,Codec 将编码后的数据吐给 MediaMuxer,Muxer 将这些压缩后的数据写入本地文件。一切都很简单。

接下来我将对具体的代码进行说明,本文完整代码你可以在 EncodeUsingBuffersActivity 找到,该代码使用异步模式进行编码,异步模式更加简洁,我更喜欢这种模式。如果你想看同步模式是如何实现的,可以参考 CTS - EncodeDecodeTest 中的 doEncodeDecodeVideoFromBuffer 函数。

MediaCodec 异步模式编码

创建编码器

val mimeType = MediaFormat.MIMETYPE_VIDEO_AVC
val format = MediaFormat.createVideoFormat(mimeType, videoWidth, videoHeight)
val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS)
val encodeCodecName = codecList.findEncoderForFormat(format)
val encoder = MediaCodec.createByCodecName(encodeCodecName)
  1. val mimeType = MediaFormat.MIMETYPE_VIDEO_AVC:定义了一个字符串常量mimeType,其值为MediaFormat.MIMETYPE_VIDEO_AVC,表示我们将使用的是AVC(即H.264)编码格式。
  2. val format = MediaFormat.createVideoFormat(mimeType, videoWidth, videoHeight):创建一个MediaFormat对象,该对象描述了我们想要的视频格式,包括编码格式、视频宽度和高度。
  3. val codecList = MediaCodecList(MediaCodecList.REGULAR_CODECS):获取系统中所有常规(非硬件加速)的编解码器列表。
  4. val encodeCodecName = codecList.findEncoderForFormat(format):在编解码器列表中查找能够处理我们指定格式的编码器。
  5. val encoder = MediaCodec.createByCodecName(encodeCodecName):通过编码器的名称创建一个MediaCodec对象,这个对象就是我们的视频编码器。

当然,也可以更简单:

val mimeType = MediaFormat.MIMETYPE_VIDEO_AVC
val encoder = MediaCodec.createEncoderByType(encodeCodecName)

设置编码回调

encoder.setCallback(object: MediaCodec.Callback(){override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {//}override fun onOutputBufferAvailable(codec: MediaCodec,index: Int,info: MediaCodec.BufferInfo) {//}override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {//}override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {//}
})

MediaCodec类中的setCallback()方法用于设置一个回调接口,这个接口将在编解码操作的各个阶段被调用。这个方法接收一个MediaCodec.Callback对象作为参数。

MediaCodec.Callback是一个抽象类,它定义了四个方法:

  1. onInputBufferAvailable(MediaCodec codec, int index):当输入缓冲区可用时,此方法被调用。参数index指示了哪个输入缓冲区已经变得可用。

  2. onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info):当输出缓冲区可用时,此方法被调用。参数index指示了哪个输出缓冲区已经变得可用,info包含了关于这个缓冲区的元数据,如其包含的数据的大小,时间戳等。

  3. onError(MediaCodec codec, MediaCodec.CodecException e):当编解码器发生错误时,此方法被调用。参数e是一个MediaCodec.CodecException对象,包含了关于错误的详细信息。

  4. onOutputFormatChanged(MediaCodec codec, MediaFormat format):当输出格式发生变化时,此方法被调用。参数format是一个MediaFormat对象,包含了新的输出格式。

回调中的代码是我们具体的编码逻辑,这个放后面详细讲。

编码器 Configure

val colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible
assert(encoder.codecInfo.getCapabilitiesForType(mimeType).colorFormats.contains(colorFormat))
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat)
format.setInteger(MediaFormat.KEY_BIT_RATE, videoBitrate)
format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE)
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL)
encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
  1. colorFormat 选择 COLOR_FormatYUV420Flexible 这是一种最常用的像素格式。
  2. 接下来这行代码是一个断言,它检查编码器是否支持上面定义的颜色格式。为了确保我们 Demo 的简洁,我假定你的机器是一定支持 COLOR_FormatYUV420Flexible 的,否则我需要写额外的代码来兼容,这会使得代码变得负责。
  3. 接着,设置了颜色格式、比特率、帧率等重要的编码信息。
  4. 最后调用 configure 函数,这行代码用上面设置的参数来配置编码器,最后一个参数指定了这是一个编码器,而不是解码器。

创建 Muxer

val outputDir = externalCacheDir
val outputName = "test.mp4"
val outputFile = File(outputDir, outputName)
muxer = MediaMuxer(outputFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)

开始编码的工作

现在我们有 encoder 和 muxer 组件,要开始编码视频的任务,需要启动这两个组件,但两者启动时机有差别。

首先,我们先启动 encoder

encoder.start()

那么 muxer 何时启动呢?在启动 muxer 之前我们需要明确知道 output format 的信息。

在使用MediaCodec进行编码时,onOutputFormatChanged 方法会在开始编码后首次调用。这是因为在开始编码后,MediaCodec 会根据你设置的参数(如分辨率、比特率等)来确定最终的输出格式。一旦输出格式确定,就会触发onOutputFormatChanged方法。

这个方法的调用表示编码器的输出格式已经准备好,你可以获取到这个新的输出格式,并用它来配置你的MediaMuxer。这是必要的,因为MediaMuxer需要知道它正在混合的音频和视频的具体格式。

基于上述原因,在异步模式下我们可以在 onOutputFormatChanged 回调函数中启动 muxer:

override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {videoTrackIndex = muxer.addTrack(format)muxer.start()
}

循环地编码视频帧

让我们来看回调函数中的具体逻辑,这些逻辑表明了我们是如何进行编码的

override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {val pts = computePresentationTime(generateIndex)// input eosif(generateIndex == NUM_FRAMES){codec.queueInputBuffer(index, 0, 0, pts, MediaCodec.BUFFER_FLAG_END_OF_STREAM)}else{val frameData = ByteArray(videoWidth * videoHeight * 3 / 2)generateFrame(generateIndex, codec.inputFormat.getInteger(MediaFormat.KEY_COLOR_FORMAT), frameData)val inputBuffer = codec.getInputBuffer(index)inputBuffer.put(frameData)codec.queueInputBuffer(index, 0, frameData.size, pts, 0)generateIndex++}
}
override fun onOutputBufferAvailable(codec: MediaCodec,index: Int,info: MediaCodec.BufferInfo
) {// output eosval isDone = (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0if(isDone){outputEnd.set(true)info.size = 0}if(info.size > 0){val encodedData = codec.getOutputBuffer(index)muxer.writeSampleData(videoTrackIndex, encodedData!!, info)codec.releaseOutputBuffer(index, false)}
}
override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {e.printStackTrace()
}
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {//...
}

首先看 onInputBufferAvailable 回调:

  1. val pts = computePresentationTime(generateIndex):这行代码计算了当前帧的显示时间,通常是根据帧率和当前帧的索引来计算的。
  2. if(generateIndex == NUM_FRAMES):这行代码检查是否已经处理完所有的帧。如果是,那么就需要向编码器发送一个表示输入结束的标志。
  3. codec.queueInputBuffer(index, 0, 0, pts, MediaCodec.BUFFER_FLAG_END_OF_STREAM):这行代码向编码器的输入队列中添加一个空的缓冲区,并设置了一个表示输入结束的标志。这告诉编码器不会有更多的数据输入了。
  4. val frameData = ByteArray(videoWidth * videoHeight * 3 / 2):这行代码创建了一个字节数组,用于存储一帧的数据。这里假设的是YUV420格式的数据,所以大小是宽度乘以高度的1.5倍。
  5. generateFrame(generateIndex, codec.inputFormat.getInteger(MediaFormat.KEY_COLOR_FORMAT), frameData):这行代码生成了一帧的数据。
  6. val inputBuffer = codec.getInputBuffer(index):这行代码获取了编码器的一个输入缓冲区。
  7. inputBuffer.put(frameData):这行代码将生成的帧数据放入输入缓冲区。
  8. codec.queueInputBuffer(index, 0, frameData.size, pts, 0):这行代码将填充了数据的输入缓冲区添加到编码器的输入队列中。
  9. generateIndex++:这行代码将帧的索引加一,准备处理下一帧的数据。

需要说明的是,我们使用 generateFrame 来生成 YUV 数据,而不是从某个图片或者视频读取,这是为了示例代码更简单。这部分代码参考了 CTS - EncodeDecodeTest 中的代码。生成的视频如下:

onOutputBufferAvailable 回调逻辑:
11. val isDone = (info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0:这行代码检查编码器是否已经处理完所有的输入数据并生成了所有的输出数据。如果是,那么isDone会被设置为true。
12. if(isDone) {…}:这个if语句检查是否已经完成了所有的编码工作。如果是,那么就设置outputEnd为true,表示输出结束,并将info.size设置为0,表示没有更多的输出数据。
13. if(info.size > 0){…}:这个if语句检查是否有输出数据。如果有,那么就处理这些数据。
14. val encodedData = codec.getOutputBuffer(index):这行代码获取了编码器的一个输出缓冲区,这个缓冲区包含了编码后的数据。
15. muxer.writeSampleData(videoTrackIndex, encodedData!!, info):这行代码将编码后的数据写入到媒体混合器中。这里的videoTrackIndex是视频轨道的索引,encodedData是编码后的数据,info包含了这些数据的元信息,如显示时间、大小等。
16. codec.releaseOutputBuffer(index, false):这行代码释放了编码器的输出缓冲区,让编码器可以继续使用这个缓冲区来存储新的输出数据。这里的false表示不需要将这个缓冲区的数据显示出来,因为我们是在编码数据,而不是播放数据。

等待编码结束,释放资源

while (!outputEnd.get())
{Thread.sleep(10)
}
encoder.stop()
muxer.stop()
encoder.release()
  1. 在编码线程中,我们等等编码结束,outputEnd 是退出的标志位
  2. 停止 encoder 和 muxer,接着调用 release 方法释放 encoder 资源

总结

本文介绍 MediaCodec 使用异步模式编码的各种细节,并提供了完整的示例代码,在示例中我们生成 YUV 数据,并配合 MediaMuxer 将编码后的数据保存到本地 MP4 文件。

参考

  • EncodeUsingBuffersActivity
  • CTS - EncodeDecodeTest

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

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

相关文章

每日一练:LeeCode-707. 设计链表 【链表+虚拟头结点+设计】

每日一练:LeeCode-707. 设计链表 【链表虚拟头结点设计】 思路设置虚拟头节点 本文是力扣 每日一练:LeeCode-707. 设计链表 【链表虚拟头结点设计】 学习与理解过程,本文仅做学习之用,对本题感兴趣的小伙伴可以出门左拐LeeCode-70…

0101二阶与三阶行列式-行列式-线性代数

一 引例 求解二元一次方程组 { a 11 x 1 a 12 x 2 b 1 a 21 x 1 a 22 x 2 b 2 \begin{cases} a_{11}x_1a_{12}x_2b_1\\ a_{21}x_1a_{22}x_2b_2\\ \end{cases} {a11​x1​a12​x2​b1​a21​x1​a22​x2​b2​​ 解: 1 a 21 − 2 a 11 ⇒ x 2 a 11 b 2 − a…

MQL5学习之简单移动平均线MA的编写

昨天还是有点高估自己了,MACD相对较难一点,改学MA的编写,首先明确MA的计算,假如有4个值,p[1,2, 3, 4], period3, 则v[0]p[0], v[1]p[1],v[2](p[0]p[1]p[2])/32, v[3](v[2]*3p[3]-p…

ChatGPT论文指南|ChatGPT如何助力论文中的数据分析!【建议收藏】

点击下方▼▼▼▼链接直达AIPaperPass ! AIPaperPass - AI论文写作指导平台 公众号原文▼▼▼▼: ChatGPT论文指南|ChatGPT如何助力论文中的数据分析!【建议收藏】 小编在之前的论文写作流程中,介绍了大量论文文字工作&#xff…

Effective objective-c-- 内存管理

Effective objective-c-- 内存管理 前言理解引用计数引用计数工作原理属性存取方法中的内存管理自动释放池保留环要点 以ARC简化引用计数使用ARC时必须遵循的方法和命名规则变量的内存管理语义ARC如何清理实例变量覆写内存管理方法要点 在dealloc方法中只释放引用并解除监听要点…

探索Linux世界:初次接触和基本指令(文件操作)

文章目录 1.基本介绍和准备2.基本指令和Linux的基本操作3.几个重要基本指令3.1 ls - 列出文件和目录3.1.1文件的知识3.1.2 .和..文件 3.2pwd - 显示当前工作目录3.2.1路径知识 3.3 cd - 切换目录3.4 touch - 创建文件或更新时间戳3.5mkdir - 创建新目录3.6rm - 删除文件或目录3…

深入了解 Android 中的 FrameLayout 布局

FrameLayout 是 Android 中常用的布局之一&#xff0c;它允许子视图堆叠在一起&#xff0c;可以在不同位置放置子视图。在这篇博客中&#xff0c;我们将详细介绍 FrameLayout 的属性及其作用。 <FrameLayout xmlns:android"http://schemas.android.com/apk/res/androi…

【数据结构和算法初阶(C语言)】带环链表问题详解(快慢指针的烧脑应用)

目录 1.铺垫-----带环链表基本了解 2. 题目&#xff1a;环形链表 3.环形链表|| ​编辑 3.1题解1 3.2 题解2 4.总结 1.铺垫-----带环链表基本了解 环形链表题目启迪&#xff1a; 环形链表特点&#xff1a;遍历链表会出现一模一样的地址 2. 题目&#xff1a;环形链表 给…

数字化转型导师鹏:政府数字化转型政务服务类案例研究

政府数字化转型政务服务类案例研究 课程背景&#xff1a; 很多地方政府存在以下问题&#xff1a; 不清楚标杆省政府数字化转型的政务服务类成功案例 不清楚地级市政府数字化转型的政务服务类成功案例 不清楚县区级政府数字化转型的政务服务类成功案例 课程特色&#x…

基于C语言实现内存型数据库(kv存储)

基于C语言实现内存型数据库(kv存储) 文章目录 基于C语言实现内存型数据库(kv存储)1. 项目背景1.1 Redis介绍1.2 项目预期及基本架构 2. 服务端原理及代码框架2.1 网络数据回环的实现2.2 array的实现2.3 rbtree的实现2.4 btree的实现2.5 hash的实现2.6 dhash的实现2.7 skiplist的…

XV4001KC数字输出 车载用(piezoman)

EPSON的XV4001KC角速度传感器是为满足汽车行业对高精度和高可靠性需求而设计的。它不仅提供了高级的运动监测特性&#xff0c;高精度的角速度测量和温度监测功能&#xff0c;而且其紧凑的设计6.04.83.3mm尺寸对于空间受限的车载环境来说&#xff0c;是一大优势&#xff0c;使得…

政务浏览器——打通信创闭环最后一公里

当前&#xff0c;信创建设工作主要集中在芯片、操作系统、数据库以及pc整机&#xff0c;这些领域基本可用&#xff0c;或者达到了市场主流水平。但是&#xff0c;政务办事场景下的信创落地仍然困难重重&#xff0c;很多地方不得不装双系统或买两台设备来来平衡日常业务和信创考…

STM32CubeIDE基础学习-软件安装,环境搭建

STM32CubeIDE基础学习-软件介绍及环境搭建步骤 文章目录 STM32CubeIDE基础学习-软件介绍及环境搭建步骤前言第1章 STM32CubeIDE 介绍1.1 软件描述1.2 软件支持的功能及特点 第2章 STM32CubeIDE 软件安装2.1 STM32CubeIDE 软件获取方法2.2 STM32CubeIDE 软件安装步骤2.2.1 错误安…

C++模板完整版

顾得泉&#xff1a;个人主页 个人专栏&#xff1a;《Linux操作系统》 《C从入门到精通》 《LeedCode刷题》 键盘敲烂&#xff0c;年薪百万&#xff01; 一、泛型编程 如何实现一个通用的交换函数呢&#xff1f; void Swap(int& left, int& right) {int temp left…

抖店入驻费用是多少?新手入驻都有哪些要求?2024费用明细!

我是电商珠珠 我做电商做了将近五年&#xff0c;做抖店做了三年多&#xff0c;期间还带着学员一起做店。 今天&#xff0c;就来给大家详细的讲一下在抖音开店&#xff0c;需要多少费用&#xff0c;最低需要投入多少。 1、营业执照200元左右 就拿个体店举例&#xff0c;在入…

hook函数——useReducer

目录 1.useReducer定义2.useReducer用法3.useState和useReducer区别 1.useReducer定义 const [state, dispatch] useReducer(reducer, initialArg, init?) reducer&#xff1a;用于更新 state 的纯函数。参数为 state 和 action&#xff0c;返回值是更新后的 state。state …

这波操作看麻了!十亿行数据,从71s到1.7s的优化之路。

节期间关注到了一个关于 Java 方面的比赛&#xff0c;很有意思。由于是开源的&#xff0c;我把项目拉下来试图学&#xff08;白&#xff09;习&#xff08;嫖&#xff09;别人的做题思路&#xff0c;在这期间一度让我产生了一个自我怀疑&#xff1a; 他们写的 Java 和我会的 Ja…

每日一题——LeetCode1576.替换所有的问号

方法一 3个字母原则 把&#xff1f;替换为和他左右都不相等的字符&#xff0c;那么找3个字符abc&#xff0c;&#xff1f;总能替换为abc中的一个字符&#xff0c;遍历字符串找到所有&#xff1f;&#xff0c;再遍历abc把&#xff1f;替换为abc中的一个字符 var modifyString …

解析 openGauss 的 AutoVacuum 机制及优化策略

前言 在 openGauss 数据库中&#xff0c;AutoVacuum 机制是一个关键的自动化功能&#xff0c;用于管理表的空间和性能。AutoVacuum 通过定期清理过时数据和更新统计信息&#xff0c;帮助数据库管理员维护数据库的性能和稳定性。 为什么需要 AutoVacuum&#xff1f; 了解AutoV…

JAVA内存模型与JVM内存结构

注意区分Java内存模型&#xff08;Java Memory Model&#xff0c;简称JMM&#xff09;与Jvm内存结构&#xff0c;前者与多线程相关&#xff0c;后者与JVM内部存储相关。本文会对两者进行简单介绍。 一、JAVA内存模型(JMM) 1. 概念 说来话长&#xff0c;由于在不同硬件厂商和…