之前我们介绍了 FFmpeg 并利用它解封装、编解码的能力完成了一款简易的视频播放器。FFmpeg 是由 C 实现的,集成至 Android 等移动端平台需要一定的代价:
基于上述理由,有时候我们可以考虑使用 Android 原生接口来完成音视频处理相关的能力。今天,这里要介绍的是 MediaExtractor 和 MediaMuxer。我们将使用MediaExtractor 和 MediaMuxer 完成如下任务:
所有代码你可以在 MediaExtractor_MediaMuxer_Remux_Example 中找到
首先解释 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;
总体上分为 步:
setDataSource
设置数据源,如果数据源有问题则会抛出异常。selectTrack
设置你需要的 Track。readSampleData
方法读取数据,读取成功后你可以调用 getSampleTrackIndex
、getSampleTime
、getSampleFlags
等接口查看当前 sample 的属性,这些属性很重要。release
方法释放实例MediaExtractor 的 API 使用方式还是比较简单易懂的,非常友好。在 MediaExtractorTest 对 MediaExtractor 的比较重要的函数进行测试与说明,可以参考参考。
首先解释 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();
创建 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 信息来判断是音频轨道还是视频轨道,例如:
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 属性包括:
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,解释完上面三个问题后,接下来的示例你理解起来会很容易
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)
}
对上述代码做一些解释:
提取音频的代码与提取视频代码几乎一致,唯一区别在与我们选择音频轨道:
// ...
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 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 中找到