安卓之美

Android我还可以相信你多少系列文章二之音视频播放

2017-05-21  本文已影响174人  想飞的小小小鱼

音频视频播放在现在的应用里面很常见,传统应用发展到一定阶段多少会引入音视频资源,特别是现在短视频被看作下一个增长爆发点,和之相关的创业层出不穷,作为开发者如何进行音视频技术选型非常关键

MediaPlayer和VideoView给我们提供了非常方便的播放音视频的能力,几乎不需要要写几行代码就可以完成。我们也可以使用MediaPlayer结合SurfaceView或者TextureView来实现视频播放,本质和VideoView是一样的,不过有更多的灵活性。

正因为封装性太强,意味着定制化变弱。MediaPlayer提供的setDataSource方法支持http,file,content等协议,但仍然无法应对复杂的需求。所以更灵活的AudioTrack的出现,可以让我们直接传送解码后的byte[]给他,带来的问题就是自己要做解码。解码不是件简单的事情,往往我们利用MediaCodec(Android4.1增加)或者外部解码库(比如ffmpeg)来实现。自己来实现解码要特别注意不要丢失了硬件加速,音频软解码还好,视频解码软解码对CPU压力会大很多。

在做音视频业务的时候,经常会遇到这样几个问题需要设置代理,或者边播边缓存,缓存加密,失败重试,网络优化等等

因为我们无法干涉MediaPlayer的网络请求部分,所以一般会将原始的播放地址http://xxx.com/playurl转换成本机代理地址http://127.0.0.1:port?url=htt...,这样MediaPlayer就会来请求本机port端口上面起的一个代理服务,在这个代理端可以做很多优化逻辑,比如给真正发往服务端的请求加上代理;将请求到的数据写入磁盘缓存,这个代理端可以根据磁盘缓存来按需请求服务端(使用http的Range参数);还有一些失败重试等网络优化手段。这个代理层还有个特别的意义甚至可以接管webview里面的audio和video标签请求。

这种实现方式在实际运行中偶尔会出现本机代理无法启动的情况,原因是Socket无法bind到指定端口,往往我们会在bind的时候指定让系统来分配一个可用端口,所以这种失败情况很有可能是root手机或者一些安全管理软件禁用了权限。

特别再说下边播边缓存的实现,缓存文件允许空洞,每个缓存文件配备另外一个内容索引文件,MediaPlayer本身会根据解码情况发出多个带Range的请求,根据内容索引文件来确定当前请求从文件哪个位置读,接下去多少字节从文件读,多少字节从网络读,网络读的部分同时写回文件以保证下次请求可以复用,这样就实现了一个边播边缓存的逻辑,甚至我们还可以给本地缓存文件进行加密。同时这个缓存文件的加载百分比可以用来做UI界面上面的缓冲进度,监控下载速度进行网络请求优化。

2.MediaPlayer的Looper。新手往往可能不关心MediaPlayer的实现,打开它的构造器前面几行代码我们就会看到他默认使用的是当前线程的Looper,如果当前线程不是个Looper线程则使用MainLooper。这一点比较重要,因为我们知道即使MediaPlayer运行在Service里面,实际上还在跑在主线程,这样的结果导致后续所有的MediaPlayer回调操作都跑在主线程,这可能是隐藏的一个定时炸弹。


更优雅的设计我们建议将MediaPlayer的回调和主动操作(stop,reset等操作)都放入work线程,操作的串行化是种最简单的设计,也是最有效的设计。大概的代码形式是这样的:

MediaPlayer在PlayHandlerThread里面初始化,就保证了他里面使用的Looper也是这个PlayHandlerThread的,这样回调就都会在这个线程触发,同时我们也在这个线程里面做setDataSource等主动操作。

3.视频播放本质上也是用MediaPlayer实现的,所以读取数据上面没有特别差异。现在比较热的小视频需要显示在列表页面支持滚动播放一个视频,点击在新页面继续观看,一般采用MediaPlayer+TextureView来实现,MediaPlayer可以采用全局定义唯一一个,只是不同时刻把内容绑定显示在不同的TextureView上而已。

4.MediaPlayer最大的问题还是在于其兼容性。从我们的经验来看可能会有这些问题:音频格式支持不全(ape,wma等原生系统不支持),未缓冲完不开始播放,播放过程中突然没有声音,播放存在跳帧,mediaserver

died;视频播放只有声音没有画面,视频格式兼容性差无法播放等。这些问题在系统基础上基本无法解决。

最头疼的问题是MediaPlayer返回的errorcode很多都是厂家扩展出来的,文档上面提供的几个值基本也是表意不清到底什么问题。这给排查问题带来很大麻烦。最最头疼的是MediaPlayer的EventHandler里面处理异常直接导致程序崩溃,比如像这样:

11-0413:43:08.966: E/AndroidRuntime(26482): java.lang.RuntimeException: failurecode: -3211-0413:43:08.966: E/AndroidRuntime(26482):    at android.media.MediaPlayer.invoke(MediaPlayer.java:664)11-0413:43:08.966: E/AndroidRuntime(26482):    at android.media.MediaPlayer.getInbandTrackInfo(MediaPlayer.java:1692)11-0413:43:08.966: E/AndroidRuntime(26482):    at android.media.MediaPlayer.scanInternalSubtitleTracks(MediaPlayer.java:1851)11-0413:43:08.966: E/AndroidRuntime(26482):    at android.media.MediaPlayer.access$600(MediaPlayer.java:529)11-0413:43:08.966: E/AndroidRuntime(26482):    at android.media.MediaPlayer$EventHandler.handleMessage(MediaPlayer.java:2198)11-0413:43:08.966: E/AndroidRuntime(26482):    at android.os.Handler.dispatchMessage(Handler.java:102)11-0413:43:08.966: E/AndroidRuntime(26482):    at android.os.Looper.loop(Looper.java:137)11-0413:43:08.966: E/AndroidRuntime(26482):    at android.app.ActivityThread.main(ActivityThread.java:4998)11-0413:43:08.966: E/AndroidRuntime(26482):    at java.lang.reflect.Method.invokeNative(Native Method)11-0413:43:08.966: E/AndroidRuntime(26482):    at java.lang.reflect.Method.invoke(Method.java:515)11-0413:43:08.966: E/AndroidRuntime(26482):    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:777)11-0413:43:08.966: E/AndroidRuntime(26482):    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:593)11-0413:43:08.966: E/AndroidRuntime(26482):    at dalvik.system.NativeStart.main(Native Method)

除了反射替换MediaPlayer里面的EventHandler来抓住异常,其他没啥特别好的办法。

遇到这么多问题开发者只能另投他路。市面上采用自解码的方案也很多,比较主流的是使用MediaCodec和ffmpeg,ffmpeg更是因为MediaCodec版本限制原因,加上本来就闻名遐迩,被很多开发者青睐。主流的音视频播放器大部分都是在这个上面进行改造的。

ExoPlayer:https://github.com/google/Exo...,作为google在MediaCodec的封装也是不错的推荐,相比自己要去抽取ffmpeg代码进行android适配编译来得容易得多

ffmepg:当然也有一些现成的实现:https://github.com/search?o=d...,最出名的当是ijkplayer,哔哩哔哩出品,跨平台还有弹幕。做视频弹幕真是开箱即用。ffmpeg功能强大,唯一的缺点就是软解码,这也是他兼容性好的原因,我们知道硬解码依赖各个厂家硬件实现兼容性自然就下降了。

在使用自解码的时候,我们建议将自己的MediaPlayer封装成Android高版本上面添加的接口一样:

/** * Sets the data source (MediaDataSource) to use. * *@paramdataSource the MediaDataSource for the media you want to play *@throwsIllegalStateException if it is called in an invalid state *@throwsIllegalArgumentException if dataSource is not a valid MediaDataSource */publicvoidsetDataSource(MediaDataSource dataSource)throwsIllegalArgumentException, IllegalStateException{    _setDataSource(dataSource);}

这样做的好处是所有实现都对MediaPlayer透明,我们只需要定义好MediaDataSource接口,后面只需要专注于实现就可以了,比如HttpDataSource,FileDataSource,MemoryDataSource等。

或许自解码会引入更多的不确定性,但是这一步迟早都要迈出去。推荐小型app或者需求不强的产品使用系统解码,在我上面提到的一些解决思路上进行改进应该能满足绝大部分场景。而那些音视频作为主业务的产品则不得不面对自解码来提高兼容性。

于是我们又在造轮子了;)

更多文章请关注微信公众号:anzhuozhimei

上一篇下一篇

猜你喜欢

热点阅读