记录一下第一次啃源码的过程
对于一直以来都是面向百度,使用Crlt A C V编程的我来说,读源码的过程就好像在做阅读理解。然而这次的问题有些特殊,网上不可能有答案。好在,纵使英文-1级,靠着谷歌翻译还有不停下断点,最后还是成功猜出作者当时的想法。整个过程耗时一星期。花了这么多时间,就想着把中间学到的东西总结一下(没有有价值的内容,大概也就是一些被说烂了的东西),没钱租服务器搭博客,就先把这个过程写成专栏吧。
首先,我是想要开发一个音乐app。那么问题来了,安卓自带的播放音视频的API有MediaPlayer和SoundPool,因为涉及到在线播放的功能,这两个肯定是不够用的。
然后是第三方的库,找到最后,决定B站的IjkPlayer和谷歌的ExoPlayer二选一。IjkPlayer没提供Java层的调用,要用这个就要手动编译so文件,另外这玩意也没个使用文档啥的。看到C的代码就头晕,又瞅了一眼隔壁ExoPlayer的文档,好像三行Java代码就能搞定,果断投靠谷歌。
OK,然后问题就来了。因为某些原因,存在服务器上的视频是分片的文件。比如一个300M的MP4格式视频,被切成了三个100M的文件。这个切割是针对文件而言的,和把文件剪辑成三份不一样,这样直接切割导致的结果就是只有第一个文件能够被播放器识别。
但是很神奇的一点是,同样的切割方式,把一个MP3格式的文件切成三份就能正常播放。
那么,接下来要做的就是让播放器能够正常播放这些被切割的文件。
通过谷歌知道了,MP4把所有的信息(第几个字节到第几个字节对应视频的第几帧到第几帧之类的吧)都储存在头部,切割以后第一个以外的文件都不包含那部分信息所以无法播放。这样很容易想到,只要把后面那些文件缺失的头部给加上就可以正常播放了。于是又花了好长时间看分析MP4编码的文章,最后实在看不懂放弃了。
ExoPlayer的官方文档说支持HLS和DASH两种流媒体格式,说实话不用这个还真不知道有流媒体格式这种东西存在,一直以为网站上的视频就是MP4格式。既然有Web专用的视频格式,那么直接把视频转成这两个格式就行了......手动转换肯定是不可能的,然而好像有个叫ffmpeg的工具可以做到。用ffmpeg转格式算是最后的杀手锏了,在那之前先试着不改变原来的文件让播放器正常播放:
// Create a data source factory.
DataSource.Factory dataSourceFactory = new DefaultHttpDataSourceFactory();
// Create a DASH media source pointing to a DASH manifest uri.
MediaSource mediaSource =
new DashMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri(dashUri));
// Create a player instance.
SimpleExoPlayer player = new SimpleExoPlayer.Builder(context).build();
// Set the media source to be played.
player.setMediaSource(mediaSource);
// Prepare the player.
player.prepare();
继续看官方文档,有一个叫DefaultHttpDataSourceFactory的东西。啊,这个我知道,工厂模式,虽然根本不知道为什么非要把对象的创建搞得这么麻烦(解耦?我所有功能写在一个类里面的时候也没感觉耦合啊)。那肯定就有DefaultHttpDataSource,根据名字来看,这个类应该是处理来自Http请求的数据源的。既然有HttpDataSource,那肯定也有其他的DataSource,说不定有能满足需求的,这样就不用多写代码了。

好吧,因为英文太差,看不太懂注释,只好每个都用一下。一轮测试下来还是没一个能用的。
没办法,只能对这个类动手脚,先让它播放第一个分片。在它读取数据的时候做判断,如果需要的数据不在第一个分片,就去后面的分片里读取。
很幸运,找到成员变量里面有一个HttpUrlConnection。这个类是用来处理网络请求的,只要对这个对象动些手脚,应该就能实现想要的效果了。
看一下哪个方法用到了httpUrlConnection,最终找到了一个open方法。懒得看内容,直接下个断点运行……拖动进度条的时候open方法被调用了,open内部又调用了makeConnection,在这个方法内部会给http请求添加一个Range头,作用是指定需要获取的文件的字节范围。
好吧,现在明白为什么mp4格式也能在线播放了,其实就是添加了一个Range请求头。
虽然文件被分割了,但播放器显示的视频长度仍然是分割之前的。此时如果拖动进度条到末尾,会抛出一个异常,说服务器的返回码是416(正常情况下应该是206)。
下断点,发现把进度条拖到末尾时,makeConnection方法依旧很耿直地添加了一个超出文件大小的Range。也就是说就算文件不完整,播放器依旧知道拖动进度条时应该去读哪一部分的内容。
到这里事情就简单了,直接在添加Range时判断,如果大小超过当前文件,就给httpUrlConnection重新赋值为后面的文件的链接,然后设置相应的Range。
好吧,当时也不知道可不可行,不过这样做以后居然奇迹般的正常运行了。再仔细想想好像也没什么奇怪的,DataSource类的职责就只是提供数据供播放器解析,至于这些数据从哪来,上层并不关心。
于是写了一个类继承DefaultHttpDataSource。然后问题来了,我要改动的属性是父类的私有属性,方法也是私有方法……试了一下反射,可能是用的kotlin的缘故,this.getClass().getSuperClass得到的不是DefaultHttpDataSource而是Object。搞不懂,网上也没有解决方案……最后只好直接把源码copy过来了。
虽然现在说出来很轻松,但当时真的脑子一片浆糊。无头苍蝇一样,好多类的源码都翻了一遍看得脑淤血,最后也就改动了一个地方的代码。
总结:
看不懂源码就别先别看,先下断点跑两步把整个结构捋清楚。最重要的是,别急,别看不懂就觉得“我是不是弱智啊?”然后就放弃了。第一次就能看懂别人的代码是不可能的(虽然花了一个星期也就搞明白MP4的在线播放是利用Range这种理所当然的事)。
解决同一个问题的方法多种多样,譬如如果我对视频编码足够了解,就可以在读取数据时给后面没有头的分片加上头部了。而能够想到多少种方法,取决于知识的广度,譬如这次我能解决这个问题,是因为我之前对http请求有了解,否则根本无从下手。
MP4只是一种容器格式,和具体的视频编码无关(之前一直有误会)。MP3格式的文件,就算被“分尸”也依旧可以正常播放。
英语很重要,能决定一个程序员的上限。