1. 边播放边缓存
视频播放时边播放边缓存,这样用户再次播放时可以节省流量,提高用户体验,这是视频播放很常见的需求。但是,Android的VideoView是没有提供这样的功能的。
有个开源库比较好用,github地址:https://github.com/danikula/AndroidVideoCache
2. 简述一下AndroidVideoCache的大体实现原理
大家都知道,VideoView.setVideoPath(proxyUrl);走的也是http请求,而http底层是走tcp协议的socket实现的。AndroidVideoCache的实现就是在tcp层架一个SocketServer的代理服务器,也就是说VideoView.setVideoPath(proxyUrl);的请求是先请求到代理服务器SocketServer,然后代理服务器SocketServer再去请求真正的服务器。这样,这个代理服务器SocketServer就可以去请求真正的服务器去下载视频(AndroidVideoCache是分段下载,也就是断点续传,每次8 * 1024大小),然后将视频响应给请求。当然代理服务器SocketServer还会先判断该视频是否已经下载缓存完,如果已经下载缓存完就直接使用本地的缓存视频。
3. AndroidVideoCache源代码简述
1)App全局架设一个本地Socket代理服务器
InetAddress inetAddress = InetAddress.getByName(PROXY_HOST);
this.serverSocket = new ServerSocket(0, 8, inetAddress);
2)getProxyUrl方法,先判断是否已经缓存过了,如果是直接使用本地缓存。如果否,就判断代理服务器SocketServer是否可用
public String getProxyUrl(String url, boolean allowCachedFileUri) {
if (allowCachedFileUri && isCached(url)) { //缓存ok
File cacheFile = getCacheFile(url);
touchFileSafely(cacheFile); //touch一下文件,让该文件时间最新,用于LRUcache缓存,LRUcache缓存是按照时间排序的。
return Uri.fromFile(cacheFile).toString();
}
return isAlive() ? appendToProxyUrl(url) : url; //isAlive()方法里就是ping了一下代理服务器SocketServer,看看是否可用?是,包装成proxyURL,否,使用来源的url,不缓存
}
3)processSocket处理所有的请求进来的Socket,包括ping的和VideoView.setVideoPath(proxyUrl)的Socket
private void processSocket(Socket socket) {
GetRequest request = GetRequest.read(socket.getInputStream());
LOG.debug("Request to cache proxy:" + request);
String url = ProxyCacheUtils.decode(request.uri);
if (pinger.isPingRequest(url)) { //如果是ping的,返回ping响应
pinger.responseToPing(socket);
} else { //视频的Socket,启动对应的Client去处理
HttpProxyCacheServerClients clients = getClients(url);
clients.processRequest(request, socket);
}
}
4)clients.processRequest(request, socket);方法的实现
public void processRequest(GetRequest request, Socket socket) throws ProxyCacheException, IOException {
startProcessRequest(); //这个方法其实就是获取一个proxyCache对应
try {
clientsCount.incrementAndGet();
proxyCache.processRequest(request, socket); //交给proxyCache处理
} finally {
finishProcessRequest();
}
}
private synchronized void startProcessRequest() throws ProxyCacheException {
proxyCache = proxyCache == null ? newHttpProxyCache() : proxyCache; //newHttpProxyCache() 这个方法准备一下url,缓存file路径,监听器等等。。。
}
5)proxyCache.processRequest(request, socket);方法的实现
public void processRequest(GetRequest request, Socket socket) throws IOException, ProxyCacheException {
OutputStream out = new BufferedOutputStream(socket.getOutputStream()); //创建一个Socket的响应流
String responseHeaders = newResponseHeaders(request);
out.write(responseHeaders.getBytes("UTF-8"));
long offset = request.rangeOffset;
if (isUseCache(request)) { //判断是否使用缓存?是
responseWithCache(out, offset); //缓存并响应
} else { //不使用缓存
responseWithoutCache(out, offset); //不缓存并响应
}
}
6)responseWithCache(out, offset);方法的实现
while (!cache.isCompleted() && cache.available() < (offset + length) && !stopped) {
readSourceAsync(); //异步请求真正的服务器读取该段数据,读取完会通知
waitForSourceData(); //加锁等待readSourceAsync()的通知
checkReadSourceErrorsCount(); //校验错误
}
6-1)readSourceAsync();方法最后回调用readSource()方法
private void readSource() {
long sourceAvailable = -1;
long offset = 0;
try {
offset = cache.available();
source.open(offset);
sourceAvailable = source.length();
byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE]; //每次读取这么多数据ProxyCacheUtils.DEFAULT_BUFFER_SIZE
int readBytes;
while ((readBytes = source.read(buffer)) != -1) {
synchronized (stopLock) {
if (isStopped()) {
return;
}
cache.append(buffer, readBytes); //追加到cache
}
offset += readBytes;
notifyNewCacheDataAvailable(offset, sourceAvailable); //通知有数据可用,也就是唤醒waitForSourceData()方法,让while (!cache.isCompleted() && cache.available() < (offset + length) && !stopped)继续判断执行下去
}
tryComplete(); //这个方法判断文件是否已经下载完成了。cache的文件是一个临时的.download为后缀的文件,分段缓存完成整个视频文件后,修改文件名为与请求url关联的文件名
onSourceRead();
} catch (Throwable e) {
readSourceErrorsCount.incrementAndGet();
onError(e);
} finally {
closeSource();
notifyNewCacheDataAvailable(offset, sourceAvailable);
}
}
7)responseWithoutCache(out, offset);方法的实现
private void responseWithoutCache(OutputStream out, long offset) throws ProxyCacheException, IOException {
HttpUrlSource newSourceNoCache = new HttpUrlSource(this.source);
try {
newSourceNoCache.open((int) offset); //这个方法里面打开HttpURLConnection
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
int readBytes;
while ((readBytes = newSourceNoCache.read(buffer)) != -1) { //read读取HttpURLConnection的getInputStream()的数据
out.write(buffer, 0, readBytes);
offset += readBytes;
}
out.flush();
} finally {
newSourceNoCache.close();
}
}