文章目录
android视频缓存框架 AndroidVideoCache 源码解析与评估
引言
android中许多视频播放框架都会有切换清晰度的选项, 而最佳的播放清晰度和流畅度无非是本地播放视频了; AndroidVideoCache 允许添加缓存支持 VideoView/MediaPlayer,ExoPlayer,或其他单行播放器
;
基本原理为: 通过在本地构建一个服务器,再使用socket连接,通过socket读取流数据;
特征:
- 在加载流时缓存至本地中;
- 缓存资源离线工作;
- 部分加载;
- 自定义缓存限制;
- 同一个url多客户端支持;
该项目仅支持 直接url
媒体文件,并不支持如 DASH, SmoothStreaming, HLS
等流媒体;
本次 代码解析版本为 com.danikula:videocache:2.7.1
使用方式
其中的一个使用方式
然后通过 String proxyUrl = ApplicationDemo.getProxy(mContext).getProxyUrl(VIDEO_URL);
获取代理后url用于视频播放;
关键类解析
HttpProxyCacheServer 代理缓存服务类
提供配置构造者,系统入口及功能整合;
private static final Logger LOG = LoggerFactory.getLogger("HttpProxyCacheServer");
//本地ip地址,用于构建本地socket;
private static final String PROXY_HOST = "127.0.0.1";
//client 的锁对象;
private final Object clientsLock = new Object();
//固定线程数线程池;
private final ExecutorService socketProcessor = Executors.newFixedThreadPool(8);
//client 的 线程安全容器,key 为 url;
private final Map<String, HttpProxyCacheServerClients> clientsMap = new ConcurrentHashMap<>();
//服务端socket,用于阻塞等待socket连入;
private final ServerSocket serverSocket;
//端口
private final int port;
//等待socket连接子线程;
private final Thread waitConnectionThread;
//server 构建配置;
private final Config config;
//ping 系统,用于判断是否连接;
private final Pinger pinger;
//>>>>>>>> 这里是初始化的入口:
public HttpProxyCacheServer(Context context) {
//使用默认的配置构建server;
this(new Builder(context).buildConfig());
}
private HttpProxyCacheServer(Config config) {
this.config = checkNotNull(config);
try {
InetAddress inetAddress = InetAddress.getByName(PROXY_HOST);
//todo 使用本地ip地址建立服务端socket;
this.serverSocket = new ServerSocket(0, 8, inetAddress);
//服务端端口;
this.port = serverSocket.getLocalPort();
//ProxySelector 关键类:为当前的socket的host和端口忽略默认代理;
IgnoreHostProxySelector.install(PROXY_HOST, port);
//信号量 (门闩),阻塞当前线程,收到通知后继续执行;
CountDownLatch startSignal = new CountDownLatch(1);
this.waitConnectionThread = new Thread(new WaitRequestsRunnable(startSignal));
this.waitConnectionThread.start();
startSignal.await(); // freeze thread, wait for server starts
//Pinger 关键类:
this.pinger = new Pinger(PROXY_HOST, port);
//使用pinger 去判断ServerSocket是否存活;
LOG.info("Proxy cache server started. Is it alive? " + isAlive());
} catch (IOException | InterruptedException e) {
//中断时直接shutdown线程池;
socketProcessor.shutdown();
throw new IllegalStateException("Error starting local proxy server", e);
}
}
//子线程运行
private final class WaitRequestsRunnable implements Runnable {
private final CountDownLatch startSignal;
public WaitRequestsRunnable(CountDownLatch startSignal) {
this.startSignal = startSignal;
}
//线程运行时,countDownLatch打开,死循环等待外部socket接入;
@Override
public void run() {
//notify freezed thread;
startSignal.countDown();
waitForRequest();
}
}
private void waitForRequest() {
try {
//中断时结束循环
while (!Thread.currentThread().isInterrupted()) {
//阻塞当前子线程(waitConnectionThread)
Socket socket = serverSocket.accept();
LOG.debug("Accept new socket " + socket);
//已接入一个外部socket,线程池运行runnable,调用`processSocket(socket);`
socketProcessor.submit(new SocketProcessorRunnable(socket));
}
} catch (IOException e) {
onError(new ProxyCacheException("Error during waiting connection", e));
}
}
//线程池运行
private void processSocket(Socket socket) {
try {
//读取socket中输入流; 记录range 和 url 等请求数据;
GetRequest request = GetRequest.read(socket.getInputStream());
LOG.debug("Request to cache proxy:" + request);
//url Decode, 此url 为 URL中定位的资源,ping或者videoUrl;
String url = ProxyCacheUtils.decode(request.uri);
//如果输入流中url 为`ping`,则返回连接状态ok;
if (pinger.isPingRequest(url)) {
pinger.responseToPing(socket);
} else {
//建立client,响应请求;
HttpProxyCacheServerClients clients = getClients(url);
//使用与url绑定的client处理socket输入流; 此处获取真实加载的videoUrl处理;
clients.processRequest(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());
}
}
//获取HttpProxyCacheServerClients 对象;
private HttpProxyCacheServerClients getClients(String url) throws ProxyCacheException {
synchronized (clientsLock) {
HttpProxyCacheServerClients clients = clientsMap.get(url);
if (clients == null) {
clients = new HttpProxyCacheServerClients(url, config);
clientsMap.put(url, clients);
}
return clients;
}
}
// >>>>>>>> 2.代理videoUrl的方法入口;
public String getProxyUrl(String url) {
return getProxyUrl(url, true);
}
public String getProxyUrl(String url, boolean allowCachedFileUri) {
//isCached 使用url 和命名生成器 判断本地是否存在缓存文件;
if (allowCachedFileUri && isCached(url)) {
File cacheFile = getCacheFile(url);
//如果存在,尝试用diskUsage 的lru算法保存文件;
touchFileSafely(cacheFile);
//此处意为,如果已经下载完成后,直接用本地缓存文件路径播放;
return Uri.fromFile(cacheFile).toString();
}
//如果serverSocket存活状态, 拼接代理VideoUrl; 加载时触发 `processSocket `方法
return isAlive() ? appendToProxyUrl(url) : url;
}
//使用ping-ping ok 系统判断本地ip是否能成功连通;
private boolean isAlive() {
//最大尝试数3次,每次重新尝试会翻倍timeout时间;
return pinger.ping(3, 70); // 70+140+280=max~500ms
}
//>>>>>>>>> 2. 核心处理videourl,使用本地代理ip; 请求时,获取GET 的包头信息 即videoUrl或者ping;
private String appendToProxyUrl(String url) {
return String.format(Locale.US, "http://%s:%d/%s", PROXY_HOST, port, ProxyCacheUtils.encode(url));
}
**java.net.ProxySelector ** 代理选择
{@link ProxySelector} that ignore system default proxies for concrete host.
ProxySelector 用于为具体的host忽略系统默认的代理;
IgnoreHostProxySelector extends ProxySelector 修改系统默认proxySelector 忽略本地ip;
//ProxySelector.java 静态代码块中会进行初始化
public abstract class ProxySelector {
...
static {
try {
Class var0 = Class.forName("sun.net.spi.DefaultProxySelector");
if (var0 != null && ProxySelector.class.isAssignableFrom(var0)) {
theProxySelector = (ProxySelector)var0.newInstance();
}
} catch (Exception var1) {
theProxySelector = null;
}
}
public static ProxySelector getDefault() {
SecurityManager var0 = System.getSecurityManager();
if (var0 != null) {
var0.checkPermission(SecurityConstants.GET_PROXYSELECTOR_PERMISSION);
}
return theProxySelector;
}
public static void setDefault(ProxySelector var0) {
SecurityManager var1 = System.getSecurityManager();
if (var1 != null) {
var1.checkPermission(SecurityConstants.SET_PROXYSELECTOR_PERMISSION);
}
theProxySelector = var0;
}
}
class IgnoreHostProxySelector extends ProxySelector {
private static final List<Proxy> NO_PROXY_LIST = Arrays.asList(Proxy.NO_PROXY);
private final ProxySelector defaultProxySelector;
private final String hostToIgnore;
private final int portToIgnore;
IgnoreHostProxySelector(ProxySelector defaultProxySelecto