自己实现MediaExtractor(一)

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封装结构。

图3-1 MP4 box 树
标红注释的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查看。

图4-1获取帧信息流程图
代码实现在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、总结

虽然功能实现,但是获取每一帧的速度较慢,还需要优化。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值