1.简析:

在客户端播放视频的使用,容易出现这样的一个问题。在网络状况不好的情况下,视频流很容易卡顿或者中断,即使播放软件本身有一点的缓存能力,但是这个往往不够,造成播放失败,卡顿。

AndroidVideoCache框架就是为了解决这问题创建的。

它的本质是一个通过代理的策略实现了一个中间层。

AndroidVideoCache 通过代理的策略实现一个中间层将我们的网络请求转移到本地实现的代理服务器上,这样我们真正请求的数据就会被代理拿到,这样代理一边向本地写入数据,一边根据我们需要的数据看是读网络数据还是读本地缓存数据再提供给我们,真正做到了数据的复用。

2.源码跟踪

其实AndroidVideoCache 看起来很高大上,很复杂,但是其实看代码很清晰,很简洁。

2.1HttpProxyCacheServer

这个类是整个框架的入口,它负责实现了一个运行在客户端的代理服务器,接收视频播放的Http请求,然后判断本地是否有缓存,有则读取缓存,然后写入流,返回数据给视频播放器,如果没有缓存,就把Http的请求转发出去。

2.1.1创建一个运行在手机的代理服务器:

 private HttpProxyCacheServer(Config config) {
this.config = checkNotNull(config);
try {
InetAddress inetAddress = InetAddress.getByName(PROXY_HOST);
this.serverSocket = new ServerSocket(0, 8, inetAddress);//建立一个端口号随机的ServerSocket,用于接收视频播放器的http请求
this.port = serverSocket.getLocalPort();//保存Server代理服务器端口号
IgnoreHostProxySelector.install(PROXY_HOST, port);//确保所有这类型的请求都不会走系统代理
CountDownLatch startSignal = new CountDownLatch(1);
this.waitConnectionThread = new Thread(new WaitRequestsRunnable(startSignal));//新建一个死循环线程用于处理Socket连接
this.waitConnectionThread.start();
startSignal.await(); // freeze thread, wait for server starts 用于等待Sever线程完成
this.pinger = new Pinger(PROXY_HOST, port);
LOG.info("Proxy cache server started. Is it alive? " + isAlive());
} catch (IOException | InterruptedException e) {
socketProcessor.shutdown();
throw new IllegalStateException("Error starting local proxy server", e);
}
}

2.1.2 代理服务器接收视频播放器的Http请求(本质上是一个Socket连接)

 private final class WaitRequestsRunnable implements Runnable {

        private final CountDownLatch startSignal;

        public WaitRequestsRunnable(CountDownLatch startSignal) {
this.startSignal = startSignal;
} @Override
public void run() {
startSignal.countDown();
waitForRequest();
}
}
//死循环
private void waitForRequest() {
try {
while (!Thread.currentThread().isInterrupted()) {
Socket socket = serverSocket.accept();//阻塞等待客户的Socket连接
LOG.debug("Accept new socket " + socket);
socketProcessor.submit(new SocketProcessorRunnable(socket));//通过线程池处理每一个Socket连接
}
} catch (IOException e) {
onError(new ProxyCacheException("Error during waiting connection", e));
}
}

2.1.3 处理客户端的Socket连接

   private final class SocketProcessorRunnable implements Runnable {

        private final Socket socket;

        public SocketProcessorRunnable(Socket socket) {
this.socket = socket;
} @Override
public void run() {
processSocket(socket);
}
} private void processSocket(Socket socket) {
try {
GetRequest request = GetRequest.read(socket.getInputStream());//从客户端的Socket获取输入流,然后创建一个GetRequest
LOG.debug("Request to cache proxy:" + request);
String url = ProxyCacheUtils.decode(request.uri);
if (pinger.isPingRequest(url)) {
pinger.responseToPing(socket);//如果是视频播放器发出了一个ping请求,直接返回 200 ok
} else {
HttpProxyCacheServerClients clients = getClients(url);//通过url获取一个处理的Client
clients.processRequest(request, socket);//通过Client处理请求request,socket
}
} catch (SocketException e) {
// There is no way to determine that client closed connection http://stackoverflow.com/a/10241044/999458
// So just to prevent log flooding don't log stacktrace
LOG.debug("Closing socket… Socket is closed by client.");
} catch (ProxyCacheException | IOException e) {
onError(new ProxyCacheException("Error processing request", e));
} finally {
releaseSocket(socket);
LOG.debug("Opened connections: " + getClientsCount());
}
}

2.1.4处理每一个视频播放器url的请求交给HttpProxyCacheServerClients

clients = getClients(url);//通过url获取(如果没有就 new 一个client)
clients.processRequest(request, socket);//(request 交给这个client处理) //HttpProxyCacheServerClients
public void processRequest(GetRequest request, Socket socket) throws ProxyCacheException, IOException {
startProcessRequest();
try {
clientsCount.incrementAndGet();
proxyCache.processRequest(request, socket);//真正处理请求的逻辑交给HttpProxyCache
} finally {
finishProcessRequest();
}
} //在处理Request之前,判断有没有生成HttpProxyCache实例
private synchronized void startProcessRequest() throws ProxyCacheException {
proxyCache = proxyCache == null ? newHttpProxyCache() : proxyCache;
} //new 一个输入当前的Client 处理当前请求的HttpProxyCache
private HttpProxyCache newHttpProxyCache() throws ProxyCacheException {
HttpUrlSource source = new HttpUrlSource(url, config.sourceInfoStorage, config.headerInjector);
FileCache cache = new FileCache(config.generateCacheFile(url), config.diskUsage);
HttpProxyCache httpProxyCache = new HttpProxyCache(source, cache);
httpProxyCache.registerCacheListener(uiCacheListener);
return httpProxyCache;
}

2.1.5 url的请求交给HttpProxyCache 处理

  public void processRequest(GetRequest request, Socket socket) throws IOException, ProxyCacheException {
OutputStream out = new BufferedOutputStream(socket.getOutputStream());
String responseHeaders = newResponseHeaders(request);
out.write(responseHeaders.getBytes("UTF-8"));//先写入Http响应头 long offset = request.rangeOffset;
if (isUseCache(request)) {
responseWithCache(out, offset);
} else {
responseWithoutCache(out, offset);
}
} //判断是否使用文件缓存
private boolean isUseCache(GetRequest request) throws ProxyCacheException {
long sourceLength = source.length();
boolean sourceLengthKnown = sourceLength > 0;
long cacheAvailable = cache.available();
// do not use cache for partial requests which too far from available cache. It seems user seek video.
return !sourceLengthKnown || !request.partial || request.rangeOffset <= cacheAvailable + sourceLength * NO_CACHE_BARRIER;
} //从offset位置开始不断读取缓存文件的数据到buffer然后写入OutputStream
private void responseWithCache(OutputStream out, long offset) throws ProxyCacheException, IOException {
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
int readBytes;
while ((readBytes = read(buffer, offset, buffer.length)) != -1) {
out.write(buffer, 0, readBytes);
offset += readBytes;
}
out.flush();
} //从offset位置开始不断读取网络的数据到buffer然后写入OutputStream
private void responseWithoutCache(OutputStream out, long offset) throws ProxyCacheException, IOException {
HttpUrlSource newSourceNoCache = new HttpUrlSource(this.source);
try {
newSourceNoCache.open((int) offset);
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
int readBytes;
//这里不断的从网络的Http连接的InputStream里面读取数据到buffer,然后在写入OutputStream
while ((readBytes = newSourceNoCache.read(buffer)) != -1) {
out.write(buffer, 0, readBytes);
offset += readBytes;
}
out.flush();
} finally {
newSourceNoCache.close();
}
}

2.1.6 HttpUrlSource 处理网络请求

//打开一个Http请求连接
@Override
public void open(long offset) throws ProxyCacheException {
try {
connection = openConnection(offset, -1);
String mime = connection.getContentType();
inputStream = new BufferedInputStream(connection.getInputStream(), DEFAULT_BUFFER_SIZE);
long length = readSourceAvailableBytes(connection, offset, connection.getResponseCode());
this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime);
this.sourceInfoStorage.put(sourceInfo.url, sourceInfo);
} catch (IOException e) {
throw new ProxyCacheException("Error opening connection for " + sourceInfo.url + " with offset " + offset, e);
}
} //这里是通过HttpURLConnection实现了Http连接,注意这里处理了重定向的可能
private HttpURLConnection openConnection(long offset, int timeout) throws IOException, ProxyCacheException {
HttpURLConnection connection;
boolean redirected;
int redirectCount = 0;
String url = this.sourceInfo.url;
do {
LOG.debug("Open connection " + (offset > 0 ? " with offset " + offset : "") + " to " + url);
connection = (HttpURLConnection) new URL(url).openConnection();
injectCustomHeaders(connection, url);
if (offset > 0) {
connection.setRequestProperty("Range", "bytes=" + offset + "-");
}
if (timeout > 0) {
connection.setConnectTimeout(timeout);
connection.setReadTimeout(timeout);
}
int code = connection.getResponseCode();
redirected = code == HTTP_MOVED_PERM || code == HTTP_MOVED_TEMP || code == HTTP_SEE_OTHER;
if (redirected) {
url = connection.getHeaderField("Location");
redirectCount++;
connection.disconnect();
}
if (redirectCount > MAX_REDIRECTS) {
throw new ProxyCacheException("Too many redirects: " + redirectCount);
}
} while (redirected);
return connection;
} //从Http连接的的InputStream里面读取数据到buffer里面
@Override
public int read(byte[] buffer) throws ProxyCacheException {
if (inputStream == null) {
throw new ProxyCacheException("Error reading data from " + sourceInfo.url + ": connection is absent!");
}
try {
return inputStream.read(buffer, 0, buffer.length);
} catch (InterruptedIOException e) {
throw new InterruptedProxyCacheException("Reading source " + sourceInfo.url + " is interrupted", e);
} catch (IOException e) {
throw new ProxyCacheException("Error reading data from " + sourceInfo.url, e);
}
}

3.具体实现思路分析:

3.1 从实际的Url到代理Url的转换:

public String getProxyUrl(String url, boolean allowCachedFileUri) {
if (allowCachedFileUri && isCached(url)) {
File cacheFile = getCacheFile(url);
touchFileSafely(cacheFile);//更新一下文件最后的修改时间,这是为了防止时间太久被Lru缓存清除
return Uri.fromFile(cacheFile).toString();//如果url对应的媒体文件已经全部被缓存,则返回这个文件的Uri地址给播放器播放即可
}
return isAlive() ? appendToProxyUrl(url) : url;//如果代理服务器在运行,就返回一个ProxyUrl,否则还是返回真实的Url给播放器播放
} //ProxyUrl 生成逻辑非常简单,将原Url拼接到一个 http://127.0.0.1:xxx/Url 即可
private String appendToProxyUrl(String url) {
return String.format(Locale.US, "http://%s:%d/%s", PROXY_HOST, port, ProxyCacheUtils.encode(url));
}

3.2如何实现边缓存边播放

//HttpProxyCache:
private void responseWithCache(OutputStream out, long offset) throws ProxyCacheException, IOException {
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
int readBytes;
while ((readBytes = read(buffer, offset, buffer.length)) != -1) {
out.write(buffer, 0, readBytes);
offset += readBytes;
}
out.flush();
} //ProxyCache:
public int read(byte[] buffer, long offset, int length) throws ProxyCacheException {
ProxyCacheUtils.assertBuffer(buffer, offset, length); while (!cache.isCompleted() && cache.available() < (offset + length) && !stopped) {
readSourceAsync();//关键是这一步,异步去读取网络数据
waitForSourceData();
checkReadSourceErrorsCount();
}
int read = cache.read(buffer, offset, length);
if (cache.isCompleted() && percentsAvailable != 100) {
percentsAvailable = 100;
onCachePercentsAvailableChanged(100);
}
return read;
} /可以看到这一个个同步方法,然后开启一个异步线程去读取网络资源
private synchronized void readSourceAsync() throws ProxyCacheException {
boolean readingInProgress = sourceReaderThread != null && sourceReaderThread.getState() != Thread.State.TERMINATED;
if (!stopped && !cache.isCompleted() && !readingInProgress) {
sourceReaderThread = new Thread(new SourceReaderRunnable(), "Source reader for " + source);
sourceReaderThread.start();
}
} private class SourceReaderRunnable implements Runnable { @Override
public void run() {
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];
int readBytes;
while ((readBytes = source.read(buffer)) != -1) {//抽象类
synchronized (stopLock) {
if (isStopped()) {
return;
}
cache.append(buffer, readBytes);//抽象逻辑,交给具体子类实现
}
offset += readBytes;
notifyNewCacheDataAvailable(offset, sourceAvailable);
}
tryComplete();
onSourceRead();
} catch (Throwable e) {
readSourceErrorsCount.incrementAndGet();
onError(e);
} finally {
closeSource();
notifyNewCacheDataAvailable(offset, sourceAvailable);
}
}

总结:

AndroidVideoCache是一个很简洁的框架,看似很复杂高大上的功能被作者完成的很好,值得看一看,作者的思路值得参考。

AndroidVideoCache 框架源码分析的更多相关文章

  1. 介绍开源的.net通信框架NetworkComms框架 源码分析

    原文网址: http://www.cnblogs.com/csdev Networkcomms 是一款C# 语言编写的TCP/UDP通信框架  作者是英国人  以前是收费的 售价249英镑 我曾经花了 ...

  2. Android Small插件化框架源码分析

    Android Small插件化框架源码分析 目录 概述 Small如何使用 插件加载流程 待改进的地方 一.概述 Small是一个写得非常简洁的插件化框架,工程源码位置:https://github ...

  3. YII框架源码分析(百度PHP大牛创作-原版-无广告无水印)

           YII 框架源码分析    百度联盟事业部——黄银锋 目 录 1. 引言 3 1.1.Yii 简介 3 1.2.本文内容与结构 3 2.组件化与模块化 4 2.1.框架加载和运行流程 4 ...

  4. Spark RPC框架源码分析(一)简述

    Spark RPC系列: Spark RPC框架源码分析(一)运行时序 Spark RPC框架源码分析(二)运行时序 Spark RPC框架源码分析(三)运行时序 一. Spark rpc框架概述 S ...

  5. Spark RPC框架源码分析(二)RPC运行时序

    前情提要: Spark RPC框架源码分析(一)简述 一. Spark RPC概述 上一篇我们已经说明了Spark RPC框架的一个简单例子,Spark RPC相关的两个编程模型,Actor模型和Re ...

  6. Spark RPC框架源码分析(三)Spark心跳机制分析

    一.Spark心跳概述 前面两节中介绍了Spark RPC的基本知识,以及深入剖析了Spark RPC中一些源码的实现流程. 具体可以看这里: Spark RPC框架源码分析(二)运行时序 Spark ...

  7. nodejs的Express框架源码分析、工作流程分析

    nodejs的Express框架源码分析.工作流程分析 1.Express的编写流程 2.Express关键api的使用及其作用分析 app.use(middleware); connect pack ...

  8. laravel框架源码分析(一)自动加载

    一.前言 使用php已有好几年,laravel的使用也是有好长时间,但是一直对于框架源码的理解不深,原因很多,归根到底还是php基础不扎实,所以源码看起来也比较吃力.最近有时间,所以开启第5.6遍的框 ...

  9. iOS常用框架源码分析

    SDWebImage NSCache 类似可变字典,线程安全,使用可变字典自定义实现缓存时需要考虑加锁和释放锁 在内存不足时NSCache会自动释放存储的对象,不需要手动干预 NSCache的key不 ...

随机推荐

  1. Python2和Python3共存,pip共存

    使用python开发,环境有Python2和 python3 两种,有时候需要两种环境切换使用,下面提供详细教程一份. 1.下载python3和python2 进入python官网,链接https:/ ...

  2. 2019.2.15 t2

    考虑倒过来计算最短路径长度,设dis[u]表示在最坏情况下,点u到最近的一 个出口的最短路,则p个出口的dis值都是0,答案即为dis[0]. #include <cstdio> #inc ...

  3. win7 设置docker加速器

    本来专门已经有了一个源于docker加速器的了,公司的电脑是mac,配置很简单,但是我自己的电脑是win7,在实际操作的时候还真是累啊,官网的教程不知道为什么没起效果,所以最终还是找了其他人发的帖,可 ...

  4. MySQL中optimize的作用

    MySQL执行命令delete语句时,如果包括where条件,并不会真正的把数据从表中删除,而是将数据转换成了碎片,通过下面的命令可以查看表中的碎片数量和索引等信息: mysql > show ...

  5. EndNote同步功能<Sync>

    EndNote的同步功能Sync可以把本地文献和网络云文献进行同步,如何进行同步,同步过程中需要注意什么,本文就EndNote的同步功能Sync作一图文说明. 一直对EndNote的同步功能Sync不 ...

  6. POJ 2253-Frogger(最小生成树的最大权)

    原题链接:点击此处 题意: 一只叫Freddy的青蛙蹲坐在湖中的一块石头上.突然他发现一只叫Fiona的青蛙在湖中的另一块石头上.Freddy想要跟Fiona约会,但由于湖水太脏,他不想游泳过去而是跳 ...

  7. 【算法笔记】B1019 数字黑洞

    1019 数字黑洞 (20 分)   给定任一个各位数字不完全相同的 4 位正整数,如果我们先把 4 个数字按非递增排序,再按非递减排序,然后用第 1 个数字减第 2 个数字,将得到一个新的数字.一直 ...

  8. shevle模块

    什么是shevle模块 该模块用于序列化python中的数据,但是序列化已经有pickle了为什么出现了shevle? 因为shevle更加简单,封装了文件的读写操作.load和dump操作, 只有一 ...

  9. 浅谈C#.NET防止SQL注入式攻击

    1#region 防止sql注入式攻击(可用于UI层控制)  2  3///   4/// 判断字符串中是否有SQL攻击代码  5///   6/// 传入用户提交数据  7/// true-安全:f ...

  10. JS框架设计之主流框架的引入机制DomeReady一种子模块

    DomReady其实是一种名为"DomContentLoaded"事件的名称,不过由于框架的需要,它与真正的DomContentLoaded有区别,在旧的JS书籍中m都会让我们把J ...