Android音视频学习(二):MediaExtractor

567 阅读6分钟

0. 前言

在音视频开发过程中,一个最基本的操作就是需要从多媒体文件(如MP4、MP3、AAC等)中分离出相应的视频或音频流数据,并从中提取音频、视频流的不同属性(如fps、码率等),这个过程被称为“解封装”。

1. 介绍

1.1 MediaExtractor

MediaExtractor是Android系统提供的一套解封装组件,是Android的多媒体处理框架中的一环,使用提取出的音视频原始数据,可以很方便地交给后续MediaCodec进行编解码处理。

1.2 MediaFormat

MediaFormat是Android多媒体框架中用来描述音视频流格式的类,可以是视频/音频/字幕/图片流。它记录了多媒体文件中每一条流的元数据信息(metadata),如视频流的码率、帧率、颜色格式、对应的编解码器名称,音频流的采样率、声道数量等等信息。它通过键值对的形式来组织和维护,下面简单罗列了一些MediaFormat常用的数据内容。

MediaFormat.png

1.2.1 Codec-specific Data

对于很多数据编码格式,需要在具体的帧数据之前,设置一些前缀参数。如H264的SPS和PPS,AAC的adts/adif头部等,这部分在MediaFormat中称为Codec-specific Data(编解码器特定数据),简称“csd”。这部分数据能够以ByteBuffer的形式通过对应的key “csd-0”, "csd-1", "csd-2",从MediaFormat获取或对其进行设置。对于使用MediaMuxer进行编码的场景,这部分数据是必须的,且要符合编码器要求。

以下是摘自Google官方文档的对于不同编码器的csd信息内容说明的一个表格: image.png

2. 主要使用流程

2.1 获取视频属性

使用MediaExtractor来获取音视频流属性信息并不复杂,主要流程如下:

graph TD
创建MediaExtractor --> setDataSource
setDataSource --> getTrackFormat
getTrackFormat --> 使用format获取不同流属性
使用format获取不同流属性 --> release释放

2.2 解封装

graph TD
创建MediaExtractor --> setDataSource设置输入源
setDataSource设置输入源 --> selectTrack选择一路流
selectTrack选择一路流 --> seekTo到指定的起始时间
seekTo到指定的起始时间 --> readSampleData读取已经编码的packet
readSampleData读取已经编码的packet --> advance读下一帧
advance读下一帧 --> release释放

3. 常用API介绍

3.1 setDataSource()

setDataSource主要是设置输入的数据源,有多个不同版本参数输入的重载方法,如输入路径名、输入文件描述符等。

3.2 getTrackCount()

返回当前媒体文件中的音频、视频、字幕流数量。

3.3 getTrackFormat()

getTrackFormat()返回流对应的format,可以看到返回值打印如下所示,包含流的主要信息。

public MediaFormat getTrackFormat(int index);

// MediaFormat.toString()
// {track-id=1, file-format=video/mp4, level=1024, mime=video/avc, frame-count=1737, profile=8, language=und, color-standard=1, display-width=1440, csd-1=java.nio.HeapByteBuffer[pos=0 lim=9 cap=9], color-transfer=3, durationUs=57957900, display-height=720, width=1440, color-range=2, rotation-degrees=0, max-input-size=777601, frame-rate=30, height=720, csd-0=java.nio.HeapByteBuffer[pos=0 lim=30 cap=30]}

// {max-bitrate=128782, sample-rate=48000, track-id=2, file-format=video/mp4, mime=audio/mp4a-latm, profile=2, bitrate=128782, language=und, aac-profile=2, encoder-delay=1024, durationUs=57962666, aac-format-adif=0, channel-count=2, max-input-size=65541, csd-0=java.nio.HeapByteBuffer[pos=0 lim=5 cap=5]}

3.3.1 MediaFormat解析

MediaFormat的数据主要由键值对的形式组织,下面罗列一些读取时常用到的一些方法。

boolean containsKey(String name);            // format中是否包含该key
int getValueTypeForKey(String name)          // 获取key对应的value的类型,如返TYPE_INTEGER=1
int getInteger(String name);                 // 获取其中整型数据
float getFloat(String name);                 // 获取Float类型数据
String getString(String name);               // 获取字符串
ByteBuffer getByteBuffer(String name);       // 获取字符ByteBuffer类型数据

3.4 selectTrack()

选择指定流。

3.5 seekTo()

// 根据输入时间和mode,seek到指定时间的帧
public void seekTo (long timeUs, int mode)

3.6 readSampleData()

// 读取所指定流中的一帧packet, offset用于指定输出存放在ByteBuffer中的偏移位置
int readSampleData(ByteBuffer byteBuf, int offset);

3.6 advance()

到下一帧。

4. 示例

4.1 获取文件信息

protected void extracteMedia() {
    String filename = Environment.getExternalStorageDirectory().getPath() + "/" + getString(R.string.test_file);
    Log.i(TAG, filename);
    try {
        extractor = new MediaExtractor();
        extractor.setDataSource(filename);
        // 获取轨道数量
        int trackCount = extractor.getTrackCount();
        for(int i = 0; i < trackCount; ++i) {
            MediaFormat mediaFormat = extractor.getTrackFormat(i);
            Log.i(TAG, "track " + i + " : " + mediaFormat.toString());

            // Integer -------------------------------------------------------------------------
            // 码率,单位bit/s
            if(mediaFormat.containsKey(MediaFormat.KEY_BIT_RATE)) {
                int bitrate = mediaFormat.getInteger(MediaFormat.KEY_BIT_RATE);
                Log.i(TAG, "bitrate : " + bitrate);
            }
            // 采样率, Hz
            if(mediaFormat.containsKey(MediaFormat.KEY_SAMPLE_RATE)) {
                int sample_rate = mediaFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
                Log.i(TAG, "sample rate : " + sample_rate);
            }
            // 声道数
            if(mediaFormat.containsKey(MediaFormat.KEY_CHANNEL_COUNT)) {
                int channel_count = mediaFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
                Log.i(TAG, "channel count : " + channel_count);
            }
            // 视频宽度
            if(mediaFormat.containsKey(MediaFormat.KEY_WIDTH)) {
                int width = mediaFormat.getInteger(MediaFormat.KEY_WIDTH);
                Log.i(TAG, "width : " + width);
            }
            // 视频高度
            if(mediaFormat.containsKey(MediaFormat.KEY_HEIGHT)) {
                int height = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT);
                Log.i(TAG, "height : " + height);
            }
            // fps
            if(mediaFormat.containsKey(MediaFormat.KEY_FRAME_RATE)) {
                int fps = mediaFormat.getInteger(MediaFormat.KEY_FRAME_RATE);
                Log.i(TAG, "fps : " + fps);
            }
            // gop size
            if(mediaFormat.containsKey(MediaFormat.KEY_I_FRAME_INTERVAL)) {
                int gopSize = mediaFormat.getInteger(MediaFormat.KEY_I_FRAME_INTERVAL);
                Log.i(TAG, "gop size : " + gopSize);
            }
            // String --------------------------------------------------------------------------
            // mime类型
            if(mediaFormat.containsKey(MediaFormat.KEY_MIME)) {
                String mime = mediaFormat.getString(MediaFormat.KEY_MIME);
                Log.i(TAG, "mime : " + mime);
            }
            // 语言
            if(mediaFormat.containsKey(MediaFormat.KEY_LANGUAGE)) {
                String language = mediaFormat.getString(MediaFormat.KEY_LANGUAGE);
                Log.i(TAG, "language : " + language);
            }
            // 编码器信息
            if(mediaFormat.containsKey(MediaFormat.KEY_CODECS_STRING)) {
                String codecStr = mediaFormat.getString(MediaFormat.KEY_CODECS_STRING);
                Log.i(TAG, "codec string : " + codecStr);
            }
        }
    } catch(Exception e) {
        Log.e(TAG, e.toString());
    }
}

4.2 使用MediaExtracter来分离mp4的H264流和aac流

下面再举一个解复用的例子,来分离出原始mp4文件的h264流和aac流,并使用文件写入保存到本地。

4.2.1 保存视频流

需要注意的是,为了使得保存的视频流可以播放,需要写入SPS和PPS数据,这部分内容保存在mediaFormat的csd-0和csd-1字段中,在最开始写入。

// 提取视频流
protected void extracteVideo() {
    // 设置输入文件
    Uri file = Utils.GetTestFile(getApplicationContext());
    try {
        MediaFormat mediaFormat = null;
        extractor = new MediaExtractor();
        extractor.setDataSource(getApplicationContext(), file, null);
        // 获取轨道数量,选中视频流
        int trackCount = extractor.getTrackCount();
        int i = 0;
        for(; i < trackCount; ++i) {
            mediaFormat = extractor.getTrackFormat(i);
            String mime = mediaFormat.getString(MediaFormat.KEY_MIME);
            if(mime.startsWith("video")) {
                break;
            }
        }

        extractor.selectTrack(i);
        // 获取packet最大的size,用于后续创建Buffer
        int maxBufferSize = mediaFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);

        File outputFile = new File(getExternalFilesDir(null).getPath() + "/out_" + i +".h264");
        FileOutputStream outputStream = new FileOutputStream(outputFile);

        // "csd-0" 和 "csd-1" 是什么,对于 H264 视频的话,它对应的是 sps 和 pps它一般存在于编码器生成的第一帧之前。
        // 如果需要h264可以播放,需要先写入这两个字段
        byte[] csd0 = mediaFormat.getByteBuffer("csd-0").array();
        outputStream.write(csd0);
        byte[] csd1 = mediaFormat.getByteBuffer("csd-1").array();
        outputStream.write(csd1);

        // 读取用的buffer
        ByteBuffer buffer = ByteBuffer.allocate(maxBufferSize);
        int sampleSize = 0;

        // 持续读取并写入packet
        while((sampleSize = extractor.readSampleData(buffer, 0)) > 0) {
            outputStream.write(buffer.array(), 0, sampleSize);

            // 下一帧
            extractor.advance();
        }

        outputStream.flush();
        outputStream.close();

        extractor.release();

    } catch(Exception e) {
        Log.e(TAG, e.toString());
    }
}

4.2.2 保存音频流

保存音频流的主要流程和视频流类似,最大的不同在于aac流的mediaFormat只有“csd-0”字段,没有“csd-1”字段,对应的是“压缩过后”的ADTS头部,这部分数据,可以转换成标准的ADTS头部。
ADTS头部和csd-0的对应关系可以参见Android 笔记: AAC ADTS笔记

protected void extracteAudio() {
    // 设置输入文件
    Uri file = Utils.GetTestFile(getApplicationContext());
    try {
        MediaFormat mediaFormat = null;
        extractor = new MediaExtractor();
        extractor.setDataSource(getApplicationContext(), file, null);
        // 获取轨道数量
        int trackCount = extractor.getTrackCount();
        // 选中音频流
        int i = 0;
        for(; i < trackCount; ++i) {
            mediaFormat = extractor.getTrackFormat(i);
            String mime = mediaFormat.getString(MediaFormat.KEY_MIME);
            if(mime.startsWith("audio")) {
                break;
            }
        }

        extractor.selectTrack(i);
        // 获取packet最大的size,用于后续创建Buffer
        int maxBufferSize = mediaFormat.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE);

        File outputFile = new File(getExternalFilesDir(null).getPath() + "/out_" + i +".aac");

        FileOutputStream outputStream = new FileOutputStream(outputFile);

        // 对于音频流,csd-0对应的是压缩过的ADTS,如果需要使得音频能够正常播放,需要在每一帧前面加入adts,7个字节
        byte[] csd0 = mediaFormat.getByteBuffer("csd-0").array();
        outputStream.write(csd0);

        ByteBuffer buffer = ByteBuffer.allocate(maxBufferSize);
        int sampleSize = 0;

        // 持续读取packet
        while((sampleSize = extractor.readSampleData(buffer, 0)) > 0) {
            // 添加ADTS头部
            byte[] bytes = new byte[7];
            addADTStoPacket(bytes, sampleSize + 7, csd0);
            // 写入adts
            outputStream.write(bytes);
            // 写入packet
            outputStream.write(buffer.array(), 0, sampleSize);

            // 下一帧
            extractor.advance();
        }

        outputStream.flush();
        outputStream.close();

        extractor.release();

    } catch(Exception e) {
        Log.e(TAG, e.toString());
    }
}

private static void addADTStoPacket(byte[] packet, int packetLen, byte[] csd0) {
    /*
    标识使用AAC级别 当前选择的是LC
    一共有1: AAC Main 2:AAC LC (Low Complexity) 3:AAC SSR (Scalable Sample Rate) 4:AAC LTP (Long Term Prediction)
    */
    int profile = csd0[0] & 0xf8;
    int frequencyIndex = (((csd0[0] & 0x07) << 1) | (csd0[1] & 0x80)); //设置采样率
    int channelConfiguration = ((csd0[1] & 0x78) >> 3); //设置频道,其实就是声道

    // fill in ADTS data
    packet[0] = (byte) 0xFF;
    packet[1] = (byte) 0xF9;
    packet[2] = (byte) (((profile - 1) << 6) + (frequencyIndex << 2) + (channelConfiguration >> 2));
    packet[3] = (byte) (((channelConfiguration & 3) << 6) + (packetLen >> 11));
    packet[4] = (byte) ((packetLen & 0x7FF) >> 3);
    packet[5] = (byte) (((packetLen & 7) << 5) + 0x1F);
    packet[6] = (byte) 0xFC;
}

5. 参考资料

  1. Android官方文档:Extractor
  2. Android开发 多媒体提取器MediaExtractor详解_将一个视频文件分离视频与音频
  3. 【多媒体封装格式详解】--- AAC ADTS格式分析 
  4. Android 笔记: AAC ADTS笔记
  5. Android开发 多媒体提取器MediaExtractor详解_入门篇