详解Android短视频同时下载和播放的方法
短视频作为一种常见的富媒体信息载体已经在移动互联网上得到非常普遍的应用,比如Snapchat、微信、手Q日迹等。由于手机网络流量珍贵且带宽有限,应用通常不会直接在线播放视频,而是把视频完整下载到本地后再进行播放,但是下载完整视频需要时间,尤其是视频较大或在网络较差的情况下等待下载的时间就会更长,容易影响用户体验。于是我们想到了边下载边播放,既不浪费流量,也不占用等待时间。下面我们将分别对它的几个实现要点进行详细讲述。
【视频格式】
想要实现边下边播,首先我们需要了解一下视频文件格式。一般情况下,视频文件结构如下所示:
内容元素主要包括:
- 图像(image)
- 音频(audio)
- 元信息(metadata) 编码格式(codec)主要包括:
- video:H.264、H.265、…
- audio:AAC、HE-AAC、… 容器封装(container)主要包括:
- MP4、FLV、AVI、MOV、RMVB、…
播放器在播放视频文件时,之所以知道该怎么去解码,以什么样的时间间隔去显示每一帧,是因为metadata记录了当前视频文件的图像尺寸、编码格式、帧率、码率等等信息,播放器通过解析metadata得到了这些信息,才能控制视频的显示,也就是说播放器要先解析完metadata才会开始播放。
我们拿MP4作为例子来说明,不同容器的封装在数据存储上会存在一些差异,MP4视频文件结构如下所示:
它对应的metadata信息称为moov,mdat包含了音频和视频数据。MP4在实际制作中,moov有可能被放到了mdat后面,所以我们要保证制作出来的MP4的moov是放置在mdat前面的,这样才可以实现边下边播功能。如果不是这样,可以用FFmpeg的faststart命令(ffmpeg -i input.mp4 -movflags +faststart output.mp4)处理一下。 另外值得一提的是,如果moov比较大,播放器需要较多的时间去解析,所以在播放之前可能会出现较长的缓冲时间,特别是视频文件较大的情况下,所以现在有些点播网站会采用每段mdat都有自己独立的metadata的封装方式,这样就可实现渐进式下载和快速缓冲的效果。
【本地代理】
在确保视频文件的metadata在头部后,我们只要完整下载metadata,再加上少许音视频数据,就可以开始播放视频了,那么如何实现“边下”呢?我们都知道,Android平台上要播放视频,最基本的方式就是实例化一个MediaPlayer, 将视频的URL通过setDataSource()设置给播放器,之后调用prepare()或prepareAsync()和start()就可以开始播放视频了。于是我们很容易想到将MediaPlayer的视频源设置为本地文件,然后通过子线程不断将下载数据追加到该文件,但笔者经过验证,这种做法会经常导致MediaPlayer各种报错,无法顺利播放。
经过深入调研,很遗憾MediaPlayer并没有提供类似可以拦截URL或文件流的API可以让我们将视频文件保存到本地(然而,iOS视频播放器有提供了类似接口)。所以我们换了一个思路,就是当播放器请求播放远程视频文件时,我们将远程URL篡改成本地URL,播放器播放视频时不再是直接访问远程视频文件,而是先访问本地代理,本地代理再去下载远程视频,下载多少就给播放器输送多少,这样就实现了边下边播,我们将这种做法称之为本地代理服务器。
【数据流程】 播放器请求本地代理服务器的数据流程如下图:
1、播放器播放之前,先把网络视频的远程url替换成本地的url(类似http://127.0.0.1/xxx);
2、播放器开始播放时将本地url请求发给proxy server;
3、proxy server根据本地url在本地缓存中查找是否存在该视频,如果存在则直接跳到步骤7,如果不存在,则进入步骤4;
4、proxy server根据视频远程url向视频server请求下载视频数据;
5、视频server返回给proxy server视频数据;
6、proxy server将返回的视频数据缓存到本地,并且处理其他业务逻辑;
7、proxy server将视频数据返回给播放器,播放器开始播放。 比起播放器直接播放网络视频,Proxy的做法使得视频的播放和下载在一定程度上变得可控,除了能够提供边下边播能力以外,还可以增加额外的视频相关业务逻辑,比如缓存、预下载、防盗链等等。
【技术架构】
Proxy Server的http服务器实现可以参考一些开源项目如NanoHttpd,但如果想自己实现也不会很难,我们一起来看下它的技术架构,如下所示:
- proxy server为播放器提供http服务,是一个本地http服务器,内部通过线程轮询监听播放器请求,可以支持get和range header的请求和响应,range 主要用来支持视频断点续传或播放拖拽功能;
- 由于播放器可能会有多个请求或多个播放器同时请求,所以需要线程池来支持并发请求;
- 当播放器发起视频下载请求,proxy首先会根据url在本地缓存查找对应的视频文件,如果找到就直接返回数据给播放器,如果没有找到,proxy会向视频server发起http请求;
- 由于SD卡空间有限,下载后的视频采用LRU算法进行淘汰。
【缓存淘汰】
关于Proxy Server下载的视频缓存路径,由于手机内部存储空间有限,视频又比较大,不建议内部存储,所以可以放到SD卡上的路径/sdcard/Android/data//cache下,并且为了唯一标识文件,可以使用MD5(url)作为文件名。选择在这个位置有两点好处:第一,这是存储在SD卡上的,因此即使缓存再多的数据也不会对手机的内置存储空间有任何影响,只要SD卡空间足够。第二,这个路径被Android系统认定为应用程序的缓存路径,当程序被卸载的时候,这里的数据也会一起被清除掉,这样就不会出现卸载应用之后手机上还有残留数据的问题。
由于SD卡存储空间有限,下载的视频如果不清除很快就会爆满。或许我们可以在达到爆满之前给用户提醒要手动清除,但用户自己可能也很难做出正确的淘汰判断,而且经常提醒会让用户容易厌烦,所以我们可以使用LRU(Least Recently Used,近期最少使用算法)来实现视频的自动清除,它是一种较为常见的缓存淘汰算法,其核心思想是“如果数据最近经常被访问过,那么将来被访问的几率也更高,反之就应该被淘汰”。缓存淘汰的逻辑流程如下所示:
- 触发时机:因缓存淘汰需要遍历所有视频并排序,视频数量多会比 较耗IO和CPU,所以可以选择应用进入后台再异步进行;
- 淘汰条件:视频总数超过300个,视频总大小超过500M,视频过期 (未使用超过1周),具体数值可根据需求动态配置。
【安全设计】
手机连接网络,实际上是通过运营商网关(SGSN和GGSN)做一个网络地址转换(Network Address Translation,NAT),实现内网IP和外网IP的映射,从而连接上了Internet,所以手机移动网络实际上是一个大型的“局域网”。
由于Proxy Server本质是一个http服务器,启动时会随机开放一个端口,如此一来,就相当于将本地服务端口暴露给整个手机网络,在这种情况下,黑客可以在这个大“局域网”内扫描出开放的端口,然后再伪造非法url模拟请求。臭名昭著的“WormHole虫洞漏洞”就是该漏洞的典型,其根本原因就是没有对请求进行限制和验证,而本身又提供了敏感服务,让黑客有机可乘。
明白了攻击原理,我们就知道如何预防,措施如下:
1、对请求url进行规则限制,只接受特定的url请求;
2、对请求者进行身份验证,只接受播放器发起的请求,这里使用了消息摘要算法HMAC-MD5或HMAC-SHA1,并对其稍作改造:
1).播放器请求时,生成一个随机数random_key;
2).将random_key作为密钥,url和timestamp作为输入,使用HMAC-MD5/SHA1生成一个hash值sign,然后将该字符串追加到url后面,向proxy发起请求,如下图所示:
3).proxy收到请求后,先验证timestamp是否超过时间限制,防止重放攻击,接着根据random_key(本地获取)、url、timestamp使用同样的签名算法也生成一个签名字符串sign,然后和请求的sign比对,如果一致,则认为是授权的,否则就拒绝请求,如下图所示:
综上所述,为了实现短视频的边下边播功能,本文首先分析了实现“边播”的要点在于视频的Metadata要在头部,然后分析了“边下”的实现方案,提出本地代理并对其架构进行说明,接着介绍如何使用LRU对缓存视频进行淘汰,最后针对本地代理可能存在的安全性问题进行了思考并给出解决方案。通过文章全篇分析可以看出,边下边播其实是“短视频类应用”播放短视频的较好的通用解决方案,因此笔者后续会将本文描述的解决方案的代码封装成组件提供出来,供类似场景快速复用。
推荐阅读
-
腾讯视频直播 02-推流-美颜滤镜 同样,腾讯云提供了 setBeautyFilter 方法来设置美颜风格、磨皮程度、美白程度和泛红程度 //style 磨皮风格:0:平滑 1:自然 2:朦胧 //美容级别:0-9。值为 0 时关闭美颜效果。默认值:0,关闭美颜效果。 //美白级别:取值 0-9。值为 0 时,将关闭美白效果。默认值:0,关闭美白效果。 //ruddyLevel:取值范围为 0-9。值为 0 时关闭美白效果。默认值:0,关闭美白效果。 public boolean setBeautyFilter(int style, int beautyLevel, int whiteningLevel, int ruddyLevel);; public boolean setBeautyFilter(int style, int beautyLevel, int whiteningLevel, int ruddyLevel) 滤镜 setFilter 方法可以设置滤镜效果,滤镜本身是一个直方图文件。setSpecialRatio 方法可以设置滤镜的程度,从 0 到 1,越大滤镜效果越明显,默认值为 0.5。 Bitmap bitmap = BitmapUtils.decodeResource(getResources, R.drawable.langman); if (mLivePusher) if (mLivePusher ! = null) { mLivePusher.setFilter(bmp); } 控制摄像头 腾讯云 sdk 默认为前置摄像头(可以通过修改 TXLivePushConfig 的配置函数 setFrontCamera 来修改默认值),调用一次 switchCamera 就切换一次,注意切换摄像头前要确保 TXLivePushConfig 和 TXLivePusher 对象已经初始化。 mLivePushConfig.setFrontCamera(true); // 默认前置摄像头。 mLivePusher.switchCamera; //切换摄像头。 ⑦ 设置徽标水印 腾讯视频云目前支持两种设置水印的方式:一种是在流媒体 SDK 中设置水印,原理是在 SDK 中对视频进行编码前在画面中设置水印。另一种方式是在云端设置水印,即由云端解析视频并添加水印标识。 建议使用 SDK 添加水印,因为在云端添加水印会有问题。下面是添加水印的 SDK 介绍: //设置视频水印 mLivePushConfig.setWatermark(BitmapFactory.decodeResource(getResources,R.drawable.watermark), 10, 10); // 最后两个参数是视频的水印。 //最后两个参数是水印位置的 X 轴和 Y 轴坐标。 mLivePusher.setConfig(mLivePushConfig); 如果需要对水印图像的位置进行模型适配,则需要调用水印规范化接口。 /设置视频水印 mLivePushConfig.setWatermark(mBitmap, 0.02f, 0.05f, 0.2f); //参数为水印图像。 //参数包括水印图像的位图、水印位置的 X 轴坐标、水印位置的 Y 轴坐标和水印宽度。后三个参数的范围是 [0,1]。 // 最后两个参数是水印位置的 X 轴坐标和 Y 轴坐标。 mLivePusher.setConfig(mLivePushConfig); TXLivePushConfig 中的 setHardwareAcceleration 方法可以启用或禁用硬件编码。 if (mHWVideoEncode){ if (mLivePushConfig ! = null) { if (Build.VERSION.SDK_INT < 18){ Toast.makeText(getApplicationContext, "Hardware acceleration failed, current phone API level is too low (min 18)"、 Toast.LENGTH_SHORT).show; mHWVideoEncode = false; } } } } mLivePushConfig.setHardwareAcceleration(mHWVideoEncode ? TXLiveConstants.ENCODE_VIDEO_HARDWARE : TXLiveConstants.ENCODE_VIDEO_SOFTWARE); mLivePusher.setConfig(mLivePushConfig); // 如果您不确定何时启用硬件加速,建议将其设置为 ENCODE_VIDEO_AUTO。 // 默认情况下启用软件编码,但如果手机的 CPU 使用率超过 80% 或帧速率为 10,SDK 将自动切换到硬件编码。 ⑨ 后台推流 在常规模式下,一旦应用程序进入后台,摄像头捕捉数据的能力就会被 Android 禁用,这意味着 SDK 无法继续捕捉和编码音频和视频数据。如果我们什么都不做,故事就会按照下面的脚本发展: 阶段 1(背景剪切后 10 秒 ->)- CDN 无法将视频流传输给观众,因为没有数据,观众看到的是主帧。 阶段 2(10 秒-> 70 秒)--观众一方的播放器因无法接收到直播流而退出,房间里空无一人。 第 3 阶段(70 秒后)--服务器直接断开了推送流媒体的 RTMP 链接,主播需要重新打开直播才能继续。 主播可能只是短暂地接了一个紧急电话,但各云提供商的安全措施会迫使主播的直播提前结束。 1) 设置 setPauseFlag 在开始推流之前,使用 TXLivePushConfig 的 setPauseImg 接口设置一个等待图像,其含义建议为 "主播将暂时离开,稍后再回来"。
-
详解Android短视频同时下载和播放的方法
-
在 Android 平台上实现同时下载和播放视频的技术!