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. Linux下对于makefile的理解

    什么是makefile呢?在Linux下makefile我们可以把理解为工程的编译规则.一个工程中源文件不计数,其按类型.功能.模块分别放在若干个目录中,makefile定义了一系列的规则来指定,那些 ...

  2. iis7下站点日志默认位置

    在iis6时,通过iis管理器的日志配置可以找到站点日志存储的位置. 但是在iis7下,iis管理器下的日志配置只能找到iis日志配置的主目录,但到底在哪个子目录,则无法直接获知.     后来在主日 ...

  3. [Swift实际操作]八、实用进阶-(4)通过protocol在两个对象中进行消息传递

    本文将演示如何借助协议,实现视图控制器对象和其内部的自定义视图对象之间的数据传递. 首先创建一个自定义视图对象.在项目名称文件夹点击鼠标右键New File ->Cocoa Touch Clas ...

  4. 发布 Android Library 到 JCenter 从入门到放弃

    最近想倒腾一个小小的 UIKit 到 JCenter,为开源社区贡献一点绵薄之力,于是就有了一系列惨无人道的踩坑史.好,接下来,直奔主题,以下是发布流程. 发布到 JCenter 发布到 JCente ...

  5. es查询,聚合、平均值、值范围、cardinality去重查询

    原文:https://blog.csdn.net/sxf_123456/article/details/78195829 普通查询 GET ana-apk/_search { "query& ...

  6. python学习笔记1.4

    注释不被程序执行的辅助性说明信息- 单行注释:以#开头,其后内容为注释# 这里是单行注释- 多行注释:以'''开头和结尾''' 这是多行注释第一行这是多行注释第二行 ''' 保留字and elif i ...

  7. 1.ajax简单实现异步交互

    效果:点击获取信息 testAjax.jsp: <%@ page language="java" contentType="text/html; charset=U ...

  8. 搭建USB摄像头转RTSP服务器的多种方法

    USB摄像头与网络摄像头相比,可选择范围广.种类多.成本低,但是实际使用时需要通过rtsp流来访问,起到直播的效果,因此在摄像头采集终端上构建rtsp流媒体服务器,将USB摄像头数据转化为rtsp,可 ...

  9. pip安装本地文件

    I do a lot of development without an internet connection1, so being able to install packages into a ...

  10. 128th LeetCode Weekly Contest Complement of Base 10 Integer

    Every non-negative integer N has a binary representation.  For example, 5 can be represented as &quo ...