自己实现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、总结

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容