音视频开发系列——全面了解Android MediaExtractor

Posted 伯努力不努力

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了音视频开发系列——全面了解Android MediaExtractor相关的知识,希望对你有一定的参考价值。

MediaExtractor的作用及使用场景

MediaExtractor是android系统中的媒体解封装库,它可以从视频或音频文件中提取媒体轨道(如视频、音频、字幕等)的数据,主要作用是将多媒体文件中的音频、视频等数据分离出来,并提供给应用程序进行解码、处理和播放。

更多音视频知识请关注公众号:进击的代码家

在实际项目中,MediaExtractor通常被用于以下场景:

1.播放本地媒体文件:通过MediaExtractor可以获取本地媒体文件中的音频、视频等数据流,然后通过MediaCodec等API进行解码和播放,从而实现本地媒体文件的播放功能。

2.媒体文件格式转换:通过MediaExtractor可以获取媒体文件中的音视频流数据,然后将其转换为其他格式的媒体文件。例如,可以将MP4文件中的音频流和视频流分别提取出来,并合成为一个MKV格式的媒体文件。

3。音视频编辑和处理:通过MediaExtractor可以获取媒体文件中的音视频流数据,并对其进行编辑和处理。例如,可以提取MP4文件中的音频流和视频流,然后对其进行剪切、合并、变速等操作,最终生成一个新的媒体文件。

需要注意的是,MediaExtractor仅仅是解封装数据,不会对数据进行解码。要对媒体数据进行解码,需要使用MediaCodec类。而且,MediaExtractor只能解封装媒体文件中的音视频等媒体轨道,而不能解析整个媒体文件的结构。如果需要解析整个媒体文件的结构,需要使用其他库或框架。

MediaExtractor支持哪些格式

MediaExtractor是Android系统中用于解封装媒体文件的类,支持多种常见的媒体格式,包括:

  1. MP4(MPEG-4 Part 14)文件格式,通常使用H.264或H.265(HEVC)编码的视频和AAC或MP3编码的音频。
  2. 3GP(3GPP文件格式)格式,通常使用H.263或H.264编码的视频和AMR或AAC编码的音频。
  3. MPEG-TS(MPEG传输流)格式,通常使用H.264编码的视频和AAC或MP3编码的音频。
  4. WebM格式,通常使用VP8或VP9编码的视频和Vorbis或Opus编码的音频。
  5. Matroska(MKV)格式,通常使用H.264或H.265编码的视频和AAC或MP3编码的音频。
  6. FLV(Flash Video)格式,通常使用VP6、Sorenson H.263或H.264编码的视频和MP3或AAC编码的音频。
    此外,MediaExtractor还支持解封装一些其他媒体格式,例如WAV(Waveform Audio File Format)格式、MP3(MPEG-1 Audio Layer III)格式等。但需要注意的是,MediaExtractor支持的媒体格式可能因不同设备而异,具体支持情况需参考官方文档和测试。

MediaExtractor Api介绍

MediaExtractor的主要API如下:

  1. setDataSource(String path): 设置媒体文件的路径。

  2. setDataSource(FileDescriptor fd): 设置媒体文件的FileDescriptor。

  3. setDataSource(Context context, Uri uri, Map<String, String> headers): 设置媒体文件的Uri和headers。

  4. getTrackCount(): 获取媒体文件中的音视频轨道数量。

  5. getTrackFormat(int index): 获取指定音视频轨道的格式。

  6. selectTrack(int index): 选择指定音视频轨道。

  7. readSampleData(ByteBuffer buffer, int offset): 读取一帧数据

public int readSampleData(ByteBuffer byteBuf, int offset)

其中,byteBuf 参数是用于接收数据的 ByteBuffer 对象,offset 参数表示该数据在 ByteBuffer 对象中的偏移量。

该方法的返回值为当前读取的数据大小,如果返回值为 -1,则表示已经读取到了文件的末尾。

在使用 MediaExtractor 读取音视频数据时,可以先调用 selectTrack 方法选择需要读取的轨道,然后通过 readSampleData 方法不断读取下一帧数据,读取完毕后需要通过 advance 方法前进到下一帧。

以下是一个使用 MediaExtractor 读取视频文件并输出每一帧时间戳的示例代码:

MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(filePath);
int trackIndex = selectTrack(extractor, "video/");
extractor.selectTrack(trackIndex);

MediaFormat format = extractor.getTrackFormat(trackIndex);
int frameRate = format.getInteger(MediaFormat.KEY_FRAME_RATE);
long frameDurationUs = 1000000L / frameRate;
long presentationTimeUs = 0L;

ByteBuffer byteBuf = ByteBuffer.allocate(format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE));
while (true) 
    int sampleSize = extractor.readSampleData(byteBuf, 0);
    if (sampleSize < 0) 
        break;
    

    long presentationTimeUs = extractor.getSampleTime();
    Log.d(TAG, "presentationTimeUs: " + presentationTimeUs);

    extractor.advance();

extractor.release();

在上述代码中,我们先调用 selectTrack 方法选择视频轨道,然后通过 getTrackFormat 方法获取该轨道的格式信息,并通过 getInteger(MediaFormat.KEY_FRAME_RATE) 获取视频帧率。然后,我们通过 readSampleData 方法不断读取下一帧视频数据,并通过 getSampleTime 获取当前数据的时间戳,即该帧数据的显示时间。最后,我们通过 advance 方法前进到下一帧数据,直到读取完毕。

readSampleData方法中offset有什么用?
offset参数用于指定读取数据的起始偏移量。这个参数通常用于在一个大的媒体文件中读取部分数据时使用。

举个例子,如果我们想要读取一个媒体文件的某一段,那么可以先使用seekTo方法定位到起始位置,然后使用readSampleData方法读取数据。这时,我们可以通过指定offset参数来告诉MediaExtractor从哪个位置开始读取数据。

如果我们不指定offset参数,默认会从当前sample的起始位置开始读取数据。如果我们在readSampleData方法之前没有调用advance方法来获取下一个sample的位置,那么默认情况下就是从上一次读取的位置开始读取数据。

因此,在使用MediaExtractor读取媒体数据时,需要注意offset参数的使用,确保读取的数据与我们想要的数据一致。

  1. seekTo(long timeUs, int mode): 定位到指定的时间点。

seekTo是MediaExtractor提供的一个方法,用于定位到指定时间点的位置,以便于读取对应时间点的音频或视频数据。

具体来说,seekTo方法有两个参数,分别是timeUs和mode。其中,timeUs是指定的时间点,单位是微秒,表示从媒体文件的起始位置开始计算的时间。

其中mode参数表示seek模式,具体包括以下三种:

**MediaExtractor.SEEK_TO_PREVIOUS_SYNC:**将时间戳定位到当前位置之前最近的一个关键帧位置,这样做可以提高解码器解码的效率,因为可以避免解码无用的数据。

**MediaExtractor.SEEK_TO_NEXT_SYNC:**将时间戳定位到当前位置之后最近的一个关键帧位置,和SEEK_TO_PREVIOUS_SYNC的效果相反。

**MediaExtractor.SEEK_TO_CLOSEST_SYNC:**将时间戳定位到离指定时间戳最近的一个关键帧位置,这样做可以提高解码器解码的效率。

一般情况下,如果需要在指定位置开始解码视频数据,推荐使用SEEK_TO_CLOSEST_SYNC模式,这样可以保证解码器的效率和准确性。如果需要跳转到某个特定关键帧之前或之后,可以分别使用SEEK_TO_PREVIOUS_SYNC和SEEK_TO_NEXT_SYNC模式。

在实际使用过程中,我们可以先通过getSampleTime方法获取当前sample的时间戳,然后和目标时间点进行比较,决定是否需要调用seekTo方法。具体流程如下:

MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(filePath);
int trackIndex = selectTrack(extractor, isAudio);
if (trackIndex < 0) 
    return;

extractor.selectTrack(trackIndex);

long targetTimeUs = ...; // 目标时间点
long sampleTimeUs = extractor.getSampleTime(); // 当前sample的时间戳
while (sampleTimeUs != -1 && sampleTimeUs < targetTimeUs) 
    extractor.advance();
    sampleTimeUs = extractor.getSampleTime();

extractor.seekTo(targetTimeUs, MediaExtractor.SEEK_TO_CLOSEST_SYNC);

在上面的代码中,我们首先使用getSampleTime方法获取当前sample的时间戳,然后和目标时间点进行比较。如果当前时间点小于目标时间点,那么就调用advance方法获取下一个sample,直到找到一个时间点大于等于目标时间点的sample,然后调用seekTo方法将定位到这个位置。最后,我们就可以通过readSampleData方法读取对应时间点的音频或视频数据了。

需要注意的是,seekTo方法可能会比较耗时,因此需要尽可能地减少调用次数。同时,由于MediaExtractor是单线程操作的,如果要多次调用seekTo方法,需要注意加锁,以免出现数据不一致的情况。

  1. getSampleFlags()
    用于获取当前采样的标志位信息,返回值为int类型的标志位值。常见的标志位值包括:

MediaCodec.BUFFER_FLAG_KEY_FRAME(0x0001):表示当前采样是关键帧。
MediaCodec.BUFFER_FLAG_END_OF_STREAM(0x0004):表示当前采样是流的结束帧。
MediaCodec.BUFFER_FLAG_CODEC_CONFIG(0x0002):表示当前采样是编解码器的配置信息。
MediaCodec.BUFFER_FLAG_PARTIAL_FRAME(0x0008):表示当前采样是部分帧。
MediaCodec.BUFFER_FLAG_SYNC_FRAME(0x0000):表示当前采样是同步帧(即关键帧)。
其中,MediaCodec.BUFFER_FLAG_KEY_FRAME和MediaCodec.BUFFER_FLAG_SYNC_FRAME都表示当前采样是关键帧,但是MediaCodec.BUFFER_FLAG_SYNC_FRAME的值为0,是因为它是MediaExtractor默认的标志位值。在读取音视频数据时,根据标志位可以判断当前采样是关键帧还是非关键帧,进而进行不同的处理,比如关键帧通常用于随机访问和解码,非关键帧用于加速解码速度等。

  1. getSampleTime()
    用于获取媒体文件中当前样本的时间戳。在读取音视频数据时,可以通过该方法获取到每个数据块的时间戳,从而实现音视频的同步播放。

该方法的返回值是一个 long 类型的时间戳,单位为微秒(1秒 = 1000毫秒 = 1000000微秒)。可以通过在 MediaExtractor 中设置的 selectTrack() 方法选择需要读取的音视频轨道,然后调用 getSampleTime() 方法逐个读取该轨道中的数据,并获取每个数据块的时间戳。

需要注意的是,getSampleTime() 方法的返回值是相对于当前轨道的开始时间的时间戳,而不是整个媒体文件的开始时间戳。因此,在获取完整个轨道的数据后,需要加上轨道的开始时间,才能得到该数据块在整个媒体文件中的时间戳。

另外,如果调用 getSampleTime() 方法时,当前轨道的数据已经读取完毕,该方法会返回 -1,表示读取操作已经结束。因此,需要在读取数据时不断地调用 getSampleTime() 方法,直到其返回 -1,才能确定当前轨道的数据已经全部读取完毕。

  1. advance
    advance()方法用于向后移动到下一个样本。它不返回任何值,只是更新MediaExtractor的内部指针以指向下一个样本。

该方法没有参数,每次调用都会将指针向前移动一个样本。通常,它与readSampleData()方法结合使用,以在提取媒体文件的数据时从一个样本移动到下一个样本。例如,在处理音频或视频时,可以使用advance()方法从一个样本移动到下一个样本,并使用getSampleTime()和getSampleTrackIndex()方法获取样本的时间戳和轨道索引。

下面是advance()方法的使用示例:

MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource("/path/to/media/file");
for (int i = 0; i < extractor.getTrackCount(); i++) 
    extractor.selectTrack(i);
    MediaFormat format = extractor.getTrackFormat(i);
    String mime = format.getString(MediaFormat.KEY_MIME);
    if (mime.startsWith("video/")) 
        // 如果是视频轨道,可以使用advance()方法逐帧处理
        while (extractor.advance()) 
            // 读取数据
            ByteBuffer buffer = ByteBuffer.allocate(format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE));
            int sampleSize = extractor.readSampleData(buffer, 0);
            if (sampleSize < 0) 
                // 如果读取到结尾,则退出循环
                break;
            
            // 处理数据
            // ...
        
    

extractor.release();

在这个示例中,我们遍历MediaExtractor中的所有轨道,选择视频轨道,并使用advance()方法逐帧处理视频数据。如果读取到了媒体文件的结尾,我们将退出循环。注意,在读取完数据后,我们需要调用advance()方法将指针移动到下一个样本,以便下一次读取。

以上是关于音视频开发系列——全面了解Android MediaExtractor的主要内容,如果未能解决你的问题,请参考以下文章

音视频开发系列——全面了解Android MediaExtractor

音视频开发系列——全面了解Android Surfaceview

云讲堂 | 5期视频带你全面了解滴滴Logi-KafkaManager

系列分享 |《最全面最细致的 VLC 教程》

开发框架模块视频系列-公用类库介绍

Path类的最全面详解 - 自定义View应用系列