在 Android 上使用 MediaExtractor 和 MediaMuxer 提取视频\提取音频\转封装\添加音频等操作

文章目录

  • 前言
  • 一、MediaExtractor 基本介绍与使用
  • 二、MediaMuxer 基本介绍与使用
  • 示例
    • 提取视频
    • 提取音频
    • 混合视频与音频文件
  • 总结
  • 参考


前言

之前我们介绍了 FFmpeg 并利用它解封装、编解码的能力完成了一款简易的视频播放器。FFmpeg 是由 C 实现的,集成至 Android 等移动端平台需要一定的代价:

  1. 额外的 so 文件。你需要将多个 so 文件集成至你的 app 中,使得 app 整体体积增加。
  2. 额外的复杂性。这里的复杂性包括多个方面:
    • 集成的复杂性。为了引入 ffmpeg,你在编译脚本需要额外对这些库进行维护;此外,通常你不需要 FFmpeg 的全部能力,因此在编译 FFmpeg 库时你需要对其进行裁剪,这部分也需要额外的付出。
    • 编程的复杂性。由于 FFmpeg 全由 C 实现,为了从 Android 上层调用 FFmpeg 的能力你需要额外编写 JNI 代码。
    • GLP/LGPL 开源协议,也是你需要你额外的注意力,避免违反这些开源协议。

基于上述理由,有时候我们可以考虑使用 Android 原生接口来完成音视频处理相关的能力。今天,这里要介绍的是 MediaExtractor 和 MediaMuxer。我们将使用MediaExtractor 和 MediaMuxer 完成如下任务:

  1. 提取视频流
  2. 提取音频流
  3. 混合视频与音频文件

所有代码你可以在 MediaExtractor_MediaMuxer_Remux_Example 中找到


一、MediaExtractor 基本介绍与使用

首先解释 MediaExtractor 是什么?

Android MediaExtractor 是一个用于从多媒体文件中提取音频和视频数据的类。它可以从本地文件或网络流中读取音频和视频数据,并将其解码为原始的音频和视频帧。MediaExtractor 可以用于开发音视频播放器、视频编辑器、音频处理器等应用程序。它是 Android 系统中的一个标准 API,可以在 Android SDK 中找到。

在 基于 FFmpeg 的跨平台视频播放器简明教程(二):基础知识和解封装(demux) 一文中介绍了解封装的概念,而 MediaExtractor的主要功能就是解封装,也就是从媒体文件中提取出原始的音频和视频数据流。这些数据流可以被送入解码器进行解码,然后进行播放或者其他处理。

那么如何使用 MediaExtractor ?在 Android MediaExtractor 介绍中,给出了使用 MediaExtractor 的基本步骤,如下代码:

MediaExtractor extractor = new MediaExtractor();extractor.setDataSource(...);int numTracks = extractor.getTrackCount();for (int i = 0; i < numTracks; ++i) {MediaFormat format = extractor.getTrackFormat(i);String mime = format.getString(MediaFormat.KEY_MIME);if (weAreInterestedInThisTrack) {extractor.selectTrack(i);}}ByteBuffer inputBuffer = ByteBuffer.allocate(...)while (extractor.readSampleData(inputBuffer, ...) >= 0) {int trackIndex = extractor.getSampleTrackIndex();long presentationTimeUs = extractor.getSampleTime();...extractor.advance();}extractor.release();extractor = null;

总体上分为 步:

  1. 创建实例,并设置数据源。通过 setDataSource 设置数据源,如果数据源有问题则会抛出异常。
  2. 选择你需要的 Track。一个媒体文件中可能包含多个轨,你需要告诉 MediaExtractor 你对哪些 Track 感兴趣。使用 selectTrack 设置你需要的 Track。
  3. 读取轨道上的数据。调用 readSampleData 方法读取数据,读取成功后你可以调用 getSampleTrackIndexgetSampleTimegetSampleFlags 等接口查看当前 sample 的属性,这些属性很重要。
  4. 调用 advance,该方法用于将提取器的当前位置向前移动到下一个样本数据,也就是说,每次调用advance()方法后,MediaExtractor的当前位置都会指向下一个音频或视频样本。
    如果成功移动到下一个样本,该方法将返回true;如果已经没有更多的样本(也就是到达了文件的末尾),则返回false。在调用readSampleData()方法读取样本数据之后,通常都需要调用advance()方法来移动到下一个样本,以便继续读取数据。
  5. 调用 release 方法释放实例

MediaExtractor 的 API 使用方式还是比较简单易懂的,非常友好。在 MediaExtractorTest 对 MediaExtractor 的比较重要的函数进行测试与说明,可以参考参考。

二、MediaMuxer 基本介绍与使用

首先解释 MediaMuxer 是什么?

Android MediaMuxer是Android系统提供的一个用于混合音频和视频数据的API。它可以将音频和视频的原始数据流混合封装成媒体文件,例如MP4文件。MediaMuxer支持多种常见的媒体文件格式,如MP4、WebM等。
MediaMuxer常常和MediaExtractor一起使用,MediaExtractor用于从媒体文件中提取音频和视频数据,MediaMuxer用于将这些数据混合成新的媒体文件。

与 MediaExtractor 的解封装能力对应,MediaMuxer 则提供了视频封装能力。

那么如何使用 MediaMuxer ?在 Android MediaMuxer 中给了基本使用流程,如下代码:

MediaMuxer muxer = new MediaMuxer("temp.mp4", OutputFormat.MUXER_OUTPUT_MPEG_4);// More often, the MediaFormat will be retrieved from MediaCodec.getOutputFormat()// or MediaExtractor.getTrackFormat().MediaFormat audioFormat = new MediaFormat(...);MediaFormat videoFormat = new MediaFormat(...);int audioTrackIndex = muxer.addTrack(audioFormat);int videoTrackIndex = muxer.addTrack(videoFormat);ByteBuffer inputBuffer = ByteBuffer.allocate(bufferSize);boolean finished = false;BufferInfo bufferInfo = new BufferInfo();muxer.start();while(!finished) {// getInputBuffer() will fill the inputBuffer with one frame of encoded// sample from either MediaCodec or MediaExtractor, set isAudioSample to// true when the sample is audio data, set up all the fields of bufferInfo,// and return true if there are no more samples.finished = getInputBuffer(inputBuffer, isAudioSample, bufferInfo);if (!finished) {int currentTrackIndex = isAudioSample ? audioTrackIndex : videoTrackIndex;muxer.writeSampleData(currentTrackIndex, inputBuffer, bufferInfo);}};muxer.stop();muxer.release();
  1. 创建 MediaMuxer 对象,并指定输出文件的路径和格式。

  2. 添加音频或视频轨道,并获取它们的轨道索引。这些 Track Index 在后面写文件时需要被使用

  3. 从 MediaCodec 或 MediaExtractor 中获取编码后的音视频数据,并将其写入 ByteBuffer 中。

  4. 调用 writeSampleData 将音视频数据写入到 muxer 中,指定轨道索引和数据的 BufferInfo。

  5. 循环执行步骤 3 和步骤 4,直到所有的音视频数据都被写入到 muxer 中。

  6. 停止混合器,并释放资源。

官网给的使用流程比较含糊,其中有些细节并没有做说明,比如 BufferInfo 要如何设置并没有给出具体的答案。当然问题不大,本文通过一些具体的示例可以让你掌握这块知识。在 MediaMuxerTest 对 MediaMuxer 的重要函数进行测试与说明,可以参考参考。

示例

下面展示几个具体的示例来说明 MediaExtractor 和 MediaMuxer 是如何使用的。在展示代码前,先回答几个问题。

Q1: 如何找到自己想要的 Track,例如我想找到视频轨
A1: 调用 val trackFormat = mediaExtractor.getTrackFormat(i) 获取轨道的 format 信息,接着查询 format 信息来判断是音频轨道还是视频轨道,例如:

private fun isVideoTrack(mediaFormat: MediaFormat): Boolean{val mime = mediaFormat.getString(MediaFormat.KEY_MIME)return mime?.startsWith("video/") ?: false
}
private fun isAudioTrack(mediaFormat: MediaFormat): Boolean{val mime = mediaFormat.getString(MediaFormat.KEY_MIME)return mime?.startsWith("audio/") ?: false
}

MediaFormat 中还有很多其他信息,你可以直接打印它来查看全部信息,并通过具体的 getStringgetInteger 等方法来获取不同的属性的值。

Q2: 我应该申请多大的 ByteBuffer ?
A2: 如果 ByteBuffer 太小,readSampleData 会失败;如果 ByteBuffer 太大,内存有些浪费。可以调用 trackFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE) 来查询当前 Track 需要的最大大小。如果有多个轨道,那么可以取多个轨道的最大值。

Q3: 如何正确的设置 BufferInfo 中的属性?
A3: 如果 BufferInfo 属性设置不对,那么 MediaMuxer 写入文件会失败。BufferInfo 属性包括:

  1. size:缓冲区中有效数据的大小,单位为字节。
  2. offset:缓冲区中有效数据的偏移量,单位为字节。在我们的示例中都是 0。
  3. presentationTimeUs:缓冲区中数据的呈现时间,单位为微秒。
  4. flags:缓冲区的标志位,用于表示缓冲区的状态,例如是否为关键帧。
    由于我们给到 Muxer 的数据都是从 MediaExtractor 中读取的,因此直接使用 MediaExtractor 中 Sample 相关属性来填充 BufferInfo 即可:
private fun getInputBufferFromExtractor(mediaExtractor: MediaExtractor,inputBuffer: ByteBuffer,bufferInfo: BufferInfo
): Boolean {val sampleSize = mediaExtractor.readSampleData(inputBuffer, 0)if (sampleSize < 0) {return true}bufferInfo.size = sampleSizebufferInfo.presentationTimeUs = mediaExtractor.sampleTimebufferInfo.offset = 0bufferInfo.flags = mediaExtractor.sampleFlagsreturn false
}

OK,解释完上面三个问题后,接下来的示例你理解起来会很容易

提取视频

private fun extractVideo(outputFilePath: String){val mediaExtractor = MediaExtractor()try {resources.openRawResourceFd(R.raw.testfile).use { fd ->mediaExtractor.setDataSource(fd)}} catch (e: Exception) {e.printStackTrace()}textViewInput.text = buildFileInfo(mediaExtractor)val mediaMuxer = MediaMuxer(outputFilePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)val trackCount = mediaExtractor.trackCountvar maxInputSize = 0for (i in 0 until trackCount){val trackFormat = mediaExtractor.getTrackFormat(i)if(isVideoTrack(trackFormat)){val maxInputSizeFromThisTrack = trackFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)if (maxInputSizeFromThisTrack > maxInputSize) {maxInputSize = maxInputSizeFromThisTrack}mediaExtractor.selectTrack(i)mediaMuxer.addTrack(trackFormat)break}}val inputBuffer = ByteBuffer.allocate(maxInputSize)val bufferInfo = BufferInfo()mediaMuxer.start()while(true){val isInputBufferEnd = getInputBufferFromExtractor(mediaExtractor, inputBuffer, bufferInfo)if (isInputBufferEnd) {break}mediaMuxer.writeSampleData(0, inputBuffer, bufferInfo)mediaExtractor.advance()}mediaMuxer.stop()mediaMuxer.release()mediaExtractor.release()textViewOutput.text = buildFileInfo(outputFilePath)
}

对上述代码做一些解释:

  1. 创建 MediaExtractor ,并设置源
  2. 遍历源的所有 Track,找到视频 Track;mediaExtractor.selectTrack() 选择当前 Track;mediaMuxer.addTrack() 添加一个 Track;并纪录 KEY_MAX_INPUT_SIZE 以便后面申请 ByteBuffer
  3. 调用 mediaMuxer.start() 开始 muxer,接着一个大循环,持续地从 extractor 读取数据接着写入 muxer 中
  4. 完成工作后,记得释放 mediaExtractor 和 mediaMuxer

提取音频

提取音频的代码与提取视频代码几乎一致,唯一区别在与我们选择音频轨道:

// ...
for (i in 0 until trackCount){val trackFormat = mediaExtractor.getTrackFormat(i)if(isAudioTrack(trackFormat)){// ...}// ...
}
}
// ...

混合视频与音频文件

private fun mixAudioAndVideo(outputFilePath: String){val videoExtractor = MediaExtractor()val audioExtractor = MediaExtractor()try {resources.openRawResourceFd(R.raw.testfile).use { fd ->videoExtractor.setDataSource(fd)}resources.openRawResourceFd(R.raw.music).use { fd ->audioExtractor.setDataSource(fd)}} catch (e: Exception) {e.printStackTrace()}textViewInput.text = buildFileInfo(videoExtractor)val mediaMuxer = MediaMuxer(outputFilePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4)videoExtractor.selectTrack(0)val videoTrackFormat = videoExtractor.getTrackFormat(0)val muxerVideoTrackIndex = mediaMuxer.addTrack(videoTrackFormat)// audio track at 1 in this fileaudioExtractor.selectTrack(1)val audioTrackFormat = audioExtractor.getTrackFormat(1)val muxerAudioTrackIndex = mediaMuxer.addTrack(audioTrackFormat)val videoTrackDuration = videoTrackFormat.getLong(MediaFormat.KEY_DURATION)val videoMaxInputSize = videoTrackFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)val audioTrackDuration = audioTrackFormat.getLong(MediaFormat.KEY_DURATION)val audioMaxInputSize = audioTrackFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)val targetDuration = min(videoTrackDuration, audioTrackDuration)val targetMaxInputSize = max(videoMaxInputSize, audioMaxInputSize)val inputVideoBuffer = ByteBuffer.allocate(targetMaxInputSize)val bufferInfo = BufferInfo()mediaMuxer.start()while(true){val isVideoInputEnd = getInputBufferFromExtractor(videoExtractor, inputVideoBuffer, bufferInfo)if(isVideoInputEnd || bufferInfo.presentationTimeUs >= targetDuration){break}mediaMuxer.writeSampleData(muxerVideoTrackIndex, inputVideoBuffer, bufferInfo)videoExtractor.advance()}while(true){val isAudioInputEnd = getInputBufferFromExtractor(audioExtractor, inputVideoBuffer, bufferInfo)if(isAudioInputEnd || bufferInfo.presentationTimeUs >= targetDuration){break}mediaMuxer.writeSampleData(muxerAudioTrackIndex, inputVideoBuffer, bufferInfo)audioExtractor.advance()}mediaMuxer.stop()mediaMuxer.release()videoExtractor.release()audioExtractor.release()textViewOutput.text = buildFileInfo(outputFilePath)
}

总结

本文介绍了 Android MediaExtractor 和 MediaMuxer ,并通过 3 个具体的示例说明它们的使用方法,在某些情况下使用 MediaExtractor 和 MediaMuxer 就能够满足音视频处理的需求。所有代码你可以在 MediaExtractor_MediaMuxer_Remux_Example 中找到

参考

  • MediaExtractor
  • MediaMuxer
  • MediaExtractor_MediaMuxer_Remux_Example

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

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

相关文章

JVM中部分主要垃圾回收器的特点、使用的算法以及适用场景

JVM中部分主要垃圾回收器的特点、使用的算法以及适用场景&#xff1a; Serial GC&#xff08;串行收集器&#xff09; 特点&#xff1a;单线程执行&#xff0c;对新生代进行垃圾回收时采用复制算法&#xff08;Copying&#xff09;&#xff0c;在老年代可能使用标记-压缩或标记…

(学习打卡2)重学Java设计模式之六大设计原则

前言&#xff1a;听说有本很牛的关于Java设计模式的书——重学Java设计模式&#xff0c;然后买了(*^▽^*) 开始跟着小傅哥学Java设计模式吧&#xff0c;本文主要记录笔者的学习笔记和心得。 打卡&#xff01;打卡&#xff01; 六大设计原则 &#xff08;引读&#xff1a;这里…

K8S本地开发环境-minikube安装部署及实践

引言 在上一篇介绍了k8s的入门和实战&#xff0c;本章就来介绍一下在windows环境如何使用minikube搭建K8s集群&#xff0c;好了废话不多说&#xff0c;下面就和我一起了解Minikube吧。 什么是Minikube&#xff1f; Minikube 是一种轻量级的 Kubernetes 实现&#xff0c;可在本…

【docker】安装 Redis

查看可用的 redis版本 docker search redis拉取 redis最新镜像 docker pull redis:latest查看本地镜像 docker images创建挂在文件 mkdir -pv /test1/docker_volume/redis/datamkdir -pv /test1/docker_volume/redis/confcd /test1/docker_volume/redis/conf/touch redis.con…

IPC之十二:使用libdbus在D-Bus上异步发送/接收信号的实例

IPC 是 Linux 编程中一个重要的概念&#xff0c;IPC 有多种方式&#xff0c;本 IPC 系列文章的前十篇介绍了几乎所有的常用的 IPC 方法&#xff0c;每种方法都给出了具体实例&#xff0c;前面的文章里介绍了 D-Bus 的基本概念以及调用远程方法的实例&#xff0c;本文介绍 D-Bus…

[Angular] 笔记 23:Renderer2 - ElementRef 的生产版本

chatgpt: Renderer2 简介 在 Angular 中&#xff0c;Renderer2 是一个服务&#xff0c;用于处理 DOM 操作的抽象层。它提供了一种安全的方式来操作 DOM&#xff0c;同时与平台无关&#xff0c;有助于维护应用程序的跨浏览器兼容性和安全性。 Renderer2 的作用是在 Angular 组…

【C++】浅拷贝 / 深拷贝 / 写时拷贝

文章目录 1. 经典的string类问题2. 浅拷贝3. 深拷贝3.1 传统写法的String类3.2 现代写法的String类 4. 写时拷贝 1. 经典的string类问题 上一篇博客已经对string类进行了简单的介绍&#xff0c;大家只要能够正常使用即可。 链接&#xff1a;【C】string 在面试中&#xff0c;面…

20 太空漫游

效果演示 实现了一个太空漫游的动画效果&#xff0c;其中包括火箭、星星和月亮。当鼠标悬停在卡片上时&#xff0c;太阳和星星会变成黄色&#xff0c;火箭会变成飞机&#xff0c;月亮会变成小型的月亮。整个效果非常炫酷&#xff0c;可以让人想起科幻电影中的太空漫游。 Code &…

计算机网络(7):网络安全

网络安全问题 计算机网络上的通信面临以下的四种威胁: (1)截获(interception)攻击者从网络上窃听他人的通信内容。 (2)中断(interruption)攻击者有意中断他人在网络上的通信。 (3)篡改(modification)攻击者故意篡改网络上传送的报文。 (4)伪造(fabrication)攻击者伪造信息在网…

Python通过用户输入两个数字并计算两个

通过用户输入两个数字&#xff0c;并计算两个 以下实例为通过用户输入两个数字&#xff0c;并计算两个数字之和&#xff1a; 实例(Python 3.0) # -*- coding: UTF-8 -*-# Filename : test.py # author by : www.dida100.com# 用户输入数字 num1 input(输入第一个数字&#…

C# WinForm MessageBox自定义按键文本 COM组件版

c# 更改弹窗MessageBox按钮文字_c# messagebox.show 字体-CSDN博客 需要用到大佬上传到百度云盘的Hook类&#xff0c;在大佬给的例子的基础上改动了点。 加了ok按键&#xff0c;还原了最基础的messageBox。 为了适配多语言&#xff0c;增加ReadBtnLanguge方法。 应用时自己…

开发辅助三(缓存Redisson分布式锁+分页插件)

缓存 缓存穿透&#xff1a;查询一个不存在的数据&#xff0c;由于缓存不命中&#xff0c;将大量查询数据库&#xff0c;但是数据库也没有此记录。 没有将这次查询的null写入缓存&#xff0c;导致了这个不存在的数据每次请求都要到存储层查询&#xff0c;失去了缓存的意义。 解…

export .... 配置环境变量,环境变量都存去哪里了?

今天在配置镜像环境的时候&#xff0c;在纠结执行如下命令之后&#xff0c;这个环境变量会保存到哪里&#xff1f;然后找了.bash_profile和.zshrc文件都没有找到&#xff0c;在纠结乱存到哪里去了 export HOMEBREW_API_DOMAIN"https://mirrors.tuna.tsinghua.edu.cn/home…

复试 || 就业day02(2024.01.02)项目一

文章目录 前言最小二乘法的推导使用正规方程求解 y w x b ywxb ywxb多元线性回归 y w 1 x 1 w 2 x 2 b yw1x1w2x2b yw1x1w2x2b总结 前言 &#x1f4ab;你好&#xff0c;我是辰chen&#xff0c;本文旨在准备考研复试或就业 &#x1f4ab;本文内容来自某机构网课&#xff…

详解 docker 镜像制作的两种方式

概要 制作Docker镜像一般有2种方法&#xff1a; 通过Dockerfile&#xff0c;完成镜像的创建使用仓库中已有的镜像&#xff0c;安装自己使用的软件环境后完成新镜像创建 docker 常用命令 docker build: 用于构建 Docker 镜像。该命令可以从 Dockerfile 构建镜像&#xff0c;…

2022年12月10日-2023年12月28日

好久没写文章了,也就是好久没写流水账了.今天写的,到底是生日,还是记录这一年的工作,比较模糊,时间上来讲,两个用意合一起吧. 这一年,是极其艰苦的一年,也是不堪回首的一年.3月份的时候设宴欢送朋友离开深圳,回住处上楼时跌倒入院,之后开刀.家人们都从老家赶来,照顾了我好几天.…

Debezium发布历史43

原文地址&#xff1a; https://debezium.io/blog/2018/12/05/automating-cache-invalidation-with-change-data-capture/ 欢迎关注留言&#xff0c;我是收集整理小能手&#xff0c;工具翻译&#xff0c;仅供参考&#xff0c;笔芯笔芯. 通过更改数据捕获自动使缓存失效 2018 年…

文本表示模型简介

文本是一类非常重要的非结构化数据&#xff0c;如何表示文本数据一直是机器学习领域的一个重要研究方向。那么有哪些文本表示模型&#xff1f;以及它们各有什么优缺点&#xff1f; 1. 常见的文本表示模型 1.1. 词袋模型和N-gram模型 最基础的文本表示模型是词袋模型。顾名思…

QML —— RadioButton的两个经典示例(附完整源码)

示例1-效果 示例2-效果 实例1 - 源码 import QtQuick 2.12 import QtQuick.Window 2.12import QtQuick.Layouts 1.12 import QtQuick.Controls 2.5Window {visible: truewidth: 640height: 480title: qsTr("Hello World")Text{id: classNametext: qsTr("--&quo…

CSS-5

移动端适配 屏幕分辨率 屏幕分辨率&#xff1a;纵横向上的像素点数&#xff0c;单位是px电脑常见分辨率&#xff1a;1920 * 10801336 * 768...硬件分辨率 -> 物理分辨率&#xff08;出厂设置&#xff09; 缩放调节后的分辨率 -> 逻辑分辨率&#xff08;软件/驱动设置&a…