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

文章目录


前言

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

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

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

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

所有代码你可以在 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;

总体上分为 步:

  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 中给了基本使用流程,如下代码:

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();
  
  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 信息来判断是音频轨道还是视频轨道,例如:

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 中还有很多其他信息,你可以直接打印它来查看全部信息,并通过具体的 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 即可:
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)
}

对上述代码做一些解释:

  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

提取音频

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

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 中找到

参考

相关推荐
氤氲息43 分钟前
Android 底部tab,使用recycleview实现
android
Clockwiseee1 小时前
PHP之伪协议
android·开发语言·php
小林爱1 小时前
【Compose multiplatform教程08】【组件】Text组件
android·java·前端·ui·前端框架·kotlin·android studio
Java搬砖组长2 小时前
抖音视频下载去水印工具推荐
音视频
小何开发2 小时前
Android Studio 安装教程
android·ide·android studio
开发者阿伟3 小时前
Android Jetpack LiveData源码解析
android·android jetpack
weixin_438150993 小时前
广州大彩串口屏安卓/linux触摸屏四路CVBS输入实现同时显示!
android·单片机
CheungChunChiu4 小时前
Android10 rk3399 以太网接入流程分析
android·framework·以太网·eth·net·netd
木头没有瓜4 小时前
ruoyi 请求参数类型不匹配,参数[giftId]要求类型为:‘java.lang.Long‘,但输入值为:‘orderGiftUnionList
android·java·okhttp
键盘侠0074 小时前
springboot 上传图片 转存成webp
android·spring boot·okhttp