0. 前言
在音视频开发过程中,一个最基本的操作就是需要从多媒体文件(如MP4、MP3、AAC等)中分离出相应的视频或音频流数据,并从中提取音频、视频流的不同属性(如fps、码率等),这个过程被称为“解封装”。
1. 介绍
1.1 MediaExtractor
MediaExtractor是Android系统提供的一套解封装组件,是Android的多媒体处理框架中的一环,使用提取出的音视频原始数据,可以很方便地交给后续MediaCodec进行编解码处理。
1.2 MediaFormat
MediaFormat是Android多媒体框架中用来描述音视频流格式的类,可以是视频/音频/字幕/图片流。它记录了多媒体文件中每一条流的元数据信息(metadata),如视频流的码率、帧率、颜色格式、对应的编解码器名称,音频流的采样率、声道数量等等信息。它通过键值对的形式来组织和维护,下面简单罗列了一些MediaFormat常用的数据内容。
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信息内容说明的一个表格:
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;
}