1、背景
Android很坑,编解码大坑。最近遇到MediaExtractor的坑了:
坑1:读PCM音频巨慢,因为Android的实现是一个一个sample读的
坑2:某些手机只能读取到一路视频或音频track,如oppo Find X
个人觉得这不是个什么很难的事情,决定自己实现一下。
2、知识储备
Android上视频主要是MP4,所以目标是自己实现一个MediaExtractor完成Mp4音视频的数据提取。要完成这件事需要了解:
1、Android MediaExtractor的接口和使用方法,这个可以通过看源码和官方文档学习;
2、熟悉Mp4的封装;
下面重点介绍下2。
3、MP4封装简介
mp4是一种视频封装容器,他的所有内容都是放在box中,box又是一种嵌套结构。各种box的描述可以在《ISO/IEC 14496-12》标准文件中找到。
3.1、基本概念:
track 表示一些sample的集合,对于媒体数据来说,track表示一个视频或音频序列。
hint track 这个特殊的track并不包含媒体数据,而是包含了一些将其他数据track打包成流媒体的指示信息
sample 对于非hint track来说,video sample即为一帧视频,或一组连续视频帧,audio sample即为一段连续的压缩音频,它们统称sample。对于hint track,sample定义一个或多个流媒体包的格式。
sample table 指明sampe时序和物理布局的表。
chunk 一个track的几个sample组成的单元。
3.2、常见的MP4封装结构。
标红注释的box(除ftyp)是需要解析获取信息的内容。
ftyp(file type box)该box应该被放在文件的最开始,指示该MP4文件应用的相关信息
mdat(media data Box)所有的实际媒体数据
moov(movie box Box)包含了文件媒体的metadata信息
tkhd (track header Box)描述track头信息
stbl(Sample Table Box)“stbl”包含了关于track中sample所有时间和位置的信息,以及sample的编解码等信息。
stsd(Sample Description Box)这里面是解码器的配置信息,包含了vps sps pps。
stts(Time To Sample Box)“stts”存储了sample的duration,描述了sample时序的映射方法,我们通过它可以找到任何时间的sample。
stss(Sync Sample Box) “stss”确定media中的关键帧。
stsc(Sample To Chunk Box)描述了sample与chunk的映射关系,查看这张表就可以找到包含指定sample的thunk,从而找到这个sample。
stsz(Sample Size Box) “stsz” 定义了每个sample的大小,包含了媒体中全部sample的数目和一张给出每个sample大小的表。
stco(Chunk Offset Box)“stco”定义了每个thunk在媒体流中的位置。
4、具体实现
了解了MP4的结构,发现我们所需要的数据确实都可以找到,现在要做的就是怎么转化塞到MediaExtractor的接口里。要完成这个目标需要完成3个任务:
1、解析上面的box数据,这个工作量有点大,可以用第3方库解析'com.googlecode.mp4parser:isoparser:1.1.21'
2、获取视频的MediaFormat信息,这个主要难点在解析hvcc box获取vps sps pps
3、获取每一帧数据和帧时间戳,是否关键帧等信息。
这一块文字描述起来非常繁琐,下面是获取帧信息关键部分流程图,如果对代码感兴趣可以前往https://github.com/liyang-hello/Mp4Extractor查看。
代码实现在TrackParser中
public void prepareFramesInfo() {
if(mSTBLBoxParser != null) {
frames.clear();
for (int i = 1; i<mSTBLBoxParser.getChunkCount()+1; i++) {
//create a frame
MP4Frame frame = new MP4Frame();
frame.setKeyFrame(mSTBLBoxParser.isKeyFrame(i));
frame.setOffset(mSTBLBoxParser.getChunkOffset(i));
frame.setSize((int) mSTBLBoxParser.getChunkSize(i));
frame.setTime(getTimestamp(i-1));
if(getFormat().getString(MediaFormat.KEY_MIME).startsWith("video")) {
frame.setType(IoConstants.TYPE_VIDEO);
} else {
frame.setType(IoConstants.TYPE_AUDIO);
}
frames.add(frame);
// LogU.d("prepareFramesInfo i="+i+" frame "+ frame);
}
}
}
5、运行测试
下面是测试代码,用Mp4Extractor提取音视频保存成一个文件,然后在电脑上播放。
public void testMp4Extractor(final String path) {
MediaExtractor mediaExtractor = new MediaExtractor();
try {
mediaExtractor.setDataSource(path);
for(int i=0; i<mediaExtractor.getTrackCount(); i++) {
mediaExtractor.selectTrack(i);
MediaFormat mediaFormat = mediaExtractor.getTrackFormat(i);
LogU.d("mediaFormat "+ mediaFormat);
ByteBuffer csd_0 = mediaFormat.getByteBuffer("csd-0");
if(csd_0 != null) {
String buf = ConvertUtil.bytesToHex(csd_0.array(), csd_0.limit());
}
}
} catch (IOException e) {
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
IExtractor extractor = new Mp4Extractor();
try {
extractor.setDataSource(path);
for (int i=0; i<extractor.getTrackCount(); i++) {
MediaFormat format = extractor.getTrackFormat(i);
ByteBuffer csd_0 = format.getByteBuffer("csd-0");
if(csd_0 != null) {
String buf = ConvertUtil.bytesToHex(csd_0.array(), csd_0.limit());
}
extractor.selectTrack(i);
extractor.seekTo(0,0);
ByteBuffer byteBuffer = ByteBuffer.allocate(4*1024*1024);
byteBuffer.position(0);
int readSize = 1;
String path = "/sdcard/Test/track_"+i;
out = null;
if(csd_0 != null) {
path = path+ ".265";
saveFile(csd_0.array(), csd_0.limit(), false, path);
} else {
path = path+".wav";
}
while (true) {
byteBuffer.position(0);
readSize = extractor.readSampleData(byteBuffer,0);
//LogU.d("readSize "+ readSize);
if(readSize > 0) {
saveFile(byteBuffer.array(), readSize, false, path);
} else {
break;
}
extractor.advance();
}
saveFile(byteBuffer.array(), 0, true, path);
//LogU.d("save "+path + " successfully");
}
} catch (IOException e) {
e.printStackTrace();
}
}
private OutputStream out = null;
public void saveFile(byte[] byteBuffer, int size, boolean bEnd, String path){
if(byteBuffer!=null){
if(out == null){
try {
out = new FileOutputStream(path);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
try {
out.write(byteBuffer,0 ,size);
} catch (IOException e) {
e.printStackTrace();
}
if(bEnd){
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}).start();
}
对比发现mediaformat信息差不多;
用ffplay播放保存的视频文件可以播放。
6、总结
虽然功能实现,但是获取每一帧的速度较慢,还需要优化。