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

@TOC


前言

之前我们介绍了 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 中找到

参考

相关推荐
lxkj_20247 小时前
使用线程局部存储解决ffmpeg中多实例调用下自定义日志回调问题
ffmpeg
runing_an_min11 小时前
ffmpeg视频滤镜:替换部分帧-freezeframes
ffmpeg·音视频·freezeframes
ruizhenggang11 小时前
ffmpeg本地编译不容易发现的问题 — Error:xxxxx not found!
ffmpeg
runing_an_min13 小时前
ffmpeg视频滤镜:提取缩略图-framestep
ffmpeg·音视频·framestep
韩曙亮1 天前
【FFmpeg】FFmpeg 内存结构 ③ ( AVPacket 函数简介 | av_packet_ref 函数 | av_packet_clone 函数 )
ffmpeg·音视频·avpacket·av_packet_clone·av_packet_ref·ffmpeg内存结构
WilliamLuo1 天前
MP4结构初识-第一篇
前端·javascript·音视频开发
oushaojun21 天前
ubuntu中使用ffmpeg和nginx推流rtmp视频
nginx·ubuntu·ffmpeg·rtmp
莫固执,朋友1 天前
网络抓包工具tcpdump 在海思平台上的编译使用
网络·ffmpeg·音视频·tcpdump
lxkj_20241 天前
修改ffmpeg实现https-flv内容加密
网络协议·https·ffmpeg
cuijiecheng20181 天前
音视频入门基础:MPEG2-TS专题(6)——FFmpeg源码中,获取MPEG2-TS传输流每个transport packet长度的实现
ffmpeg·音视频