@TOC
前言
之前我们介绍了 FFmpeg 并利用它解封装、编解码的能力完成了一款简易的视频播放器。FFmpeg 是由 C 实现的,集成至 Android 等移动端平台需要一定的代价:
- 额外的 so 文件。你需要将多个 so 文件集成至你的 app 中,使得 app 整体体积增加。
- 额外的复杂性。这里的复杂性包括多个方面:
- 集成的复杂性。为了引入 ffmpeg,你在编译脚本需要额外对这些库进行维护;此外,通常你不需要 FFmpeg 的全部能力,因此在编译 FFmpeg 库时你需要对其进行裁剪,这部分也需要额外的付出。
- 编程的复杂性。由于 FFmpeg 全由 C 实现,为了从 Android 上层调用 FFmpeg 的能力你需要额外编写 JNI 代码。
- GLP/LGPL 开源协议,也是你需要你额外的注意力,避免违反这些开源协议。
基于上述理由,有时候我们可以考虑使用 Android 原生接口来完成音视频处理相关的能力。今天,这里要介绍的是 MediaExtractor 和 MediaMuxer。我们将使用MediaExtractor 和 MediaMuxer 完成如下任务:
- 提取视频流
- 提取音频流
- 混合视频与音频文件
所有代码你可以在 MediaExtractor_MediaMuxer_Remux_Example 中找到
一、MediaExtractor 基本介绍与使用
首先解释 MediaExtractor 是什么?
Android MediaExtractor 是一个用于从多媒体文件中提取音频和视频数据的类。它可以从本地文件或网络流中读取音频和视频数据,并将其解码为原始的音频和视频帧。MediaExtractor 可以用于开发音视频播放器、视频编辑器、音频处理器等应用程序。它是 Android 系统中的一个标准 API,可以在 Android SDK 中找到。
在 基于 FFmpeg 的跨平台视频播放器简明教程(二):基础知识和解封装(demux) 一文中介绍了解封装的概念,而 MediaExtractor的主要功能就是解封装,也就是从媒体文件中提取出原始的音频和视频数据流。这些数据流可以被送入解码器进行解码,然后进行播放或者其他处理。
那么如何使用 MediaExtractor ?在 Android MediaExtractor 介绍中,给出了使用 MediaExtractor 的基本步骤,如下代码:
kotlin
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;
总体上分为 步:
- 创建实例,并设置数据源。通过
setDataSource
设置数据源,如果数据源有问题则会抛出异常。 - 选择你需要的 Track。一个媒体文件中可能包含多个轨,你需要告诉 MediaExtractor 你对哪些 Track 感兴趣。使用
selectTrack
设置你需要的 Track。 - 读取轨道上的数据。调用
readSampleData
方法读取数据,读取成功后你可以调用getSampleTrackIndex
、getSampleTime
、getSampleFlags
等接口查看当前 sample 的属性,这些属性很重要。 - 调用 advance,该方法用于将提取器的当前位置向前移动到下一个样本数据,也就是说,每次调用advance()方法后,MediaExtractor的当前位置都会指向下一个音频或视频样本。 如果成功移动到下一个样本,该方法将返回true;如果已经没有更多的样本(也就是到达了文件的末尾),则返回false。在调用readSampleData()方法读取样本数据之后,通常都需要调用advance()方法来移动到下一个样本,以便继续读取数据。
- 调用
release
方法释放实例
MediaExtractor 的 API 使用方式还是比较简单易懂的,非常友好。在 MediaExtractorTest 对 MediaExtractor 的比较重要的函数进行测试与说明,可以参考参考。
二、MediaMuxer 基本介绍与使用
首先解释 MediaMuxer 是什么?
Android MediaMuxer是Android系统提供的一个用于混合音频和视频数据的API。它可以将音频和视频的原始数据流混合封装成媒体文件,例如MP4文件。MediaMuxer支持多种常见的媒体文件格式,如MP4、WebM等。 MediaMuxer常常和MediaExtractor一起使用,MediaExtractor用于从媒体文件中提取音频和视频数据,MediaMuxer用于将这些数据混合成新的媒体文件。
与 MediaExtractor 的解封装能力对应,MediaMuxer 则提供了视频封装能力。
那么如何使用 MediaMuxer ?在 Android MediaMuxer 中给了基本使用流程,如下代码:
kotlin
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();
-
创建 MediaMuxer 对象,并指定输出文件的路径和格式。
-
添加音频或视频轨道,并获取它们的轨道索引。这些 Track Index 在后面写文件时需要被使用
-
从 MediaCodec 或 MediaExtractor 中获取编码后的音视频数据,并将其写入 ByteBuffer 中。
-
调用
writeSampleData
将音视频数据写入到 muxer 中,指定轨道索引和数据的 BufferInfo。 -
循环执行步骤 3 和步骤 4,直到所有的音视频数据都被写入到 muxer 中。
-
停止混合器,并释放资源。
官网给的使用流程比较含糊,其中有些细节并没有做说明,比如 BufferInfo 要如何设置并没有给出具体的答案。当然问题不大,本文通过一些具体的示例可以让你掌握这块知识。在 MediaMuxerTest 对 MediaMuxer 的重要函数进行测试与说明,可以参考参考。
示例
下面展示几个具体的示例来说明 MediaExtractor 和 MediaMuxer 是如何使用的。在展示代码前,先回答几个问题。
Q1: 如何找到自己想要的 Track,例如我想找到视频轨 A1: 调用 val trackFormat = mediaExtractor.getTrackFormat(i)
获取轨道的 format 信息,接着查询 format 信息来判断是音频轨道还是视频轨道,例如:
kotlin
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 中还有很多其他信息,你可以直接打印它来查看全部信息,并通过具体的 getString
、getInteger
等方法来获取不同的属性的值。
Q2: 我应该申请多大的 ByteBuffer ? A2: 如果 ByteBuffer 太小,readSampleData 会失败;如果 ByteBuffer 太大,内存有些浪费。可以调用 trackFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE)
来查询当前 Track 需要的最大大小。如果有多个轨道,那么可以取多个轨道的最大值。
Q3: 如何正确的设置 BufferInfo 中的属性? A3: 如果 BufferInfo 属性设置不对,那么 MediaMuxer 写入文件会失败。BufferInfo 属性包括:
- size:缓冲区中有效数据的大小,单位为字节。
- offset:缓冲区中有效数据的偏移量,单位为字节。在我们的示例中都是 0。
- presentationTimeUs:缓冲区中数据的呈现时间,单位为微秒。
- flags:缓冲区的标志位,用于表示缓冲区的状态,例如是否为关键帧。 由于我们给到 Muxer 的数据都是从 MediaExtractor 中读取的,因此直接使用 MediaExtractor 中 Sample 相关属性来填充 BufferInfo 即可:
kotlin
private fun getInputBufferFromExtractor(
mediaExtractor: MediaExtractor,
inputBuffer: ByteBuffer,
bufferInfo: BufferInfo
): Boolean {
val sampleSize = mediaExtractor.readSampleData(inputBuffer, 0)
if (sampleSize < 0) {
return true
}
bufferInfo.size = sampleSize
bufferInfo.presentationTimeUs = mediaExtractor.sampleTime
bufferInfo.offset = 0
bufferInfo.flags = mediaExtractor.sampleFlags
return false
}
OK,解释完上面三个问题后,接下来的示例你理解起来会很容易
提取视频
kotlin
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.trackCount
var maxInputSize = 0
for (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)
}
对上述代码做一些解释:
- 创建 MediaExtractor ,并设置源
- 遍历源的所有 Track,找到视频 Track;mediaExtractor.selectTrack() 选择当前 Track;mediaMuxer.addTrack() 添加一个 Track;并纪录 KEY_MAX_INPUT_SIZE 以便后面申请 ByteBuffer
- 调用 mediaMuxer.start() 开始 muxer,接着一个大循环,持续地从 extractor 读取数据接着写入 muxer 中
- 完成工作后,记得释放 mediaExtractor 和 mediaMuxer
提取音频
提取音频的代码与提取视频代码几乎一致,唯一区别在与我们选择音频轨道:
kotlin
// ...
for (i in 0 until trackCount){
val trackFormat = mediaExtractor.getTrackFormat(i)
if(isAudioTrack(trackFormat)){
// ...
}
// ...
}
}
// ...
混合视频与音频文件
kotlin
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 file
audioExtractor.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 中找到