一种视频预加载的方案
前言:视频的预加载是提高用户体验的重要因素。预加载成为网络视频播放不可或缺的一个技术环节。
预加载的形式:
1.边存边播:下载多少播放多少。
优点:快速加载播放,实现简单;缺点:不能拖动未存区域;适合音频媒体
2.代理服务器:预先下载媒体的头部(头部Size为 s1 byte)->监听播放器的请求,当Request的是预加载的URL->代理把媒体头部作为Response返回给播放器,并改Ranage 为 s1 byte 发送Request->代理服务器纯粹作为透传。
优点:快速加载播放,支持拖动;缺点:实现非常复杂;适合视频媒体
影响预加载的因素:
-
网络状态
-
缓冲文件大小
-
视频码率
码率低、网速快的情况没必要使用预加载,码率中等、网速一般的情况合适使用。另外,缓冲文件也不能设置太大:过大的缓冲区会刷爆MediaPlayer内置的缓冲区,影响正常播放;再者,读取缓冲文件也耗时。
预加载场景:
电视剧播放第1集时,在快要结束前5分钟,开始加载第2集。播放时就会连贯起来。缩短加载时间,如当系统检测您的网络环境较差时,会提示您开启预加载模式,开启预加载模式后,系统将为您缓冲整部影片。为了您流畅观看视频,建议您在开启预加载模式5分钟后,再次点击播放按钮,开始播放。这时就不会卡在缓冲中的状态。就是在视频播放的时候,在播放器通过本地URL,请求视频数据时,本地代理截取这次请求,经过本地代理逻辑,向服务器或本地缓存请求数据。本地代理在获得视频数据后,将数据转给播放器,实现播放。相比直接请求视频数据,本地代理的优势在于,buffer由本地控制,提大提升视频播放速度。提高用户体验。
一张图看清本地代理:
数据预加载:
不同rom上的MediaPlayer是不同的,会有一些差异,取决rom多媒体团队对MediaPlayer的定制程度,例如:有些MediaPlayer首次播放从头buffer,有些MdiaPlayer首次播放会多次Request,Range到网络媒体文件的头部、中间和文件尾,再从指定位置buffer。系统播放器需要下载5s的数据才开始把buffer进行播放出来。5s的数据,如果网络差的话,就处于是buffering中,一旦有5s的数据,就先播起来,而后,再在背后预加载,如一般播放panel的seekbar,有两种颜色,一种颜色(下图蓝色)是当前正在播的位置,还是一种颜色(灰色)走在前面,就是加载好的数据。
效果图1:
效果图2:
实现思路及部分代码:
MainActivity.用VideoView进行视频播放,url的视频为【鬼吹灯之精绝古城第1集】,会有一个开关,决定是否预加载。默认打开。
HttpProxy,就是启动一个本地代理,用127.0.0.1来替换视频源的服务器地址。
packagecom.hejunlin.videopreloaded;
importandroid.util.Log;
publicclassHttpProxy{
publicstaticfinalintSIZE = (int) (5*1024*1024);
publicstaticfinalStringTAG =HttpProxy.class.getSimpleName();
privateintremotePort = -1;
privateStringremoteHost;
privateintlocalPort;
privateStringlocalHost;
privateServerSocketlocalServer =null;
privateSocketsckPlayer =null;
privateSocketsckServer =null;
privateSocketAddressserverAddress;
privateHandleDownLoaddownload =null;
/**
* 初始化代理服务器
*
* @param localport
* 代理服务器监听的端口
*/
publicHttpProxy(intlocalport) {
try{
localPort = localport;
localHost =Contants.LOCAL_IP_ADDRESS;
localServer =newServerSocket(localport,1,
InetAddress.getByName(localHost));
}catch(Exceptione) {
System.exit(0);
}
URI tmpURI =newURI(urlString);
StringfileName =Utils.urlToFileName(tmpURI.getPath());
StringfilePath =Contants.getBufferDir() +"/"+ fileName;
download =newHandleDownLoad(urlString, filePath, size);
download.startThread();
returnfilePath;
}
/**
* 把网络URL转为本地URL,127.0.0.1替换网络域名
*
* @param url网络URL
* @return [0]:重定向后MP4真正URL,[1]:本地URL
*/
publicString[] getLocalURL(StringurlString) {
StringtargetUrl =Utils.getRedirectUrl(urlString);
// ----获取对应本地代理服务器的链接----//
StringlocalUrl =null;
URI originalURI = URI.create(targetUrl);
remoteHost = originalURI.getHost();
if(originalURI.getPort() != -1) {// URL带Port
serverAddress =newInetSocketAddress(remoteHost,
originalURI.getPort());// 使用默认端口
remotePort = originalURI.getPort();// 保存端口,中转时替换
localUrl = targetUrl.replace(
remoteHost +":"+ originalURI.getPort(), localHost +":"
+ localPort);
}else{// URL不带Port
serverAddress =newInetSocketAddress(remoteHost,Contants.HTTP_PORT);// 使用80端口
remotePort = -1;
localUrl = targetUrl.replace(remoteHost, localHost +":"
+ localPort);
}
String[] result =newString[] { targetUrl, localUrl };
returnresult;
}
/**
* 异步启动代理服务器
*
* @throws IOException
*/
publicvoidasynStartProxy() {
newThread() {
publicvoidrun() {
startProxy();
}
}.start();
}
privatevoidstartProxy() {
HttpParserhttpParser =null;
HttpProxyUtilsutils =null;
intbytes_read;
byte[] local_request =newbyte[1024];
byte[] remote_reply =newbyte[1024];
while(true) {
booleansentResponseHeader =false;
try{// 开始新的request之前关闭过去的Socket
if(sckPlayer !=null)
sckPlayer.close();
if(sckServer !=null)
sckServer.close();
}catch(IOExceptione1) {
}
try{
// --------------------------------------
// 监听MediaPlayer的请求,MediaPlayer->代理服务器
// --------------------------------------
sckPlayer = localServer.accept();
Log.e(TAG,
"------------------------------------------------------------------");
if(download !=null&& download.isDownloading())
download.stopThread(false);
httpParser =newHttpParser(remoteHost, remotePort, localHost,
localPort);
utils =newHttpProxyUtils(sckPlayer, sckServer,
serverAddress);
HttpParser.ProxyRequestrequest =null;
while((bytes_read = sckPlayer.getInputStream().read(
local_request)) != -1) {
byte[] buffer = httpParser.getRequestBody(local_request,
bytes_read);
if(buffer !=null) {
request = httpParser.getProxyRequest(buffer);
break;
}
}
booleanisExists =newFile(request._prebufferFilePath)
.exists();
if(isExists)
Log.e(TAG,">> prebuffer size:"+ download.getDownloadedSize());
sckServer = utils.sentToServer(request._body);
// ------------------------------------------------------
// 把网络服务器的反馈发到MediaPlayer,网络服务器->代理服务器->MediaPlayer
// ------------------------------------------------------
while((bytes_read = sckServer.getInputStream().read(
remote_reply)) != -1) {
if(sentResponseHeader) {
try{// 拖动进度条时,容易在此异常,断开重连
utils.sendToMP(remote_reply, bytes_read);
}catch(Exceptione) {
break;// 发送异常直接退出while
}
continue;// 退出本次while
}
List<byte[]> httpResponse = httpParser.getResponseBody(
remote_reply, bytes_read);
if(httpResponse.size() ==0)
continue;// 没Header则退出本次循环
sentResponseHeader =true;
StringresponseStr =newString(httpResponse.get(0));
Log.e(TAG,">> responseStr "+ responseStr);
// send http header to mediaplayer
utils.sendToMP(httpResponse.get(0));
if(isExists) {// 需要发送预加载到MediaPlayer
isExists =false;
intsentBufferSize =0;
try{
sentBufferSize = utils.sendPrebufferToMP(
request._prebufferFilePath,
request._rangePosition);
}catch(Exceptionex) {
break;
}
if(sentBufferSize >0) {// 成功发送预加载,重新发送请求到服务器
intnewRange = (int) (sentBufferSize + request._rangePosition);
StringnewRequestStr = httpParser
.modifyRequestRange(request._body, newRange);
Log.e(TAG +"-pre->", newRequestStr);
// 修改Range后的Request发送给服务器
sckServer = utils.sentToServer(newRequestStr);
// 把服务器的Response的Header去掉
utils.removeResponseHeader(httpParser);
continue;
}
}
// 发送剩余数据
if(httpResponse.size() ==2) {
utils.sendToMP(httpResponse.get(1));
}
}
Log.e(TAG,">> preloaded over");
// 关闭 2个SOCKET
sckPlayer.close();
sckServer.close();
}catch(Exceptione) {
Log.e(TAG, e.toString());
Log.e(TAG,Utils.getExceptionMessage(e));
}
}
}
}
HttpPaser就是解析真实地址的类,这里就不再给出。
第一时间获得博客更新提醒,以及更多 android干货,源码分析,欢迎关注我的微信公众号,扫一扫下方二维码或者长按识别二维码,即可关注。