前言  
     之前做短视频项目,需求是需要视频缓存功能,我也觉得比较合理,毕竟一个视频看完之后重复观看的时候还需要从网上加载是很不友好的事情,一方面耗费用户的流量,另一方面直接从本地播放要更流畅,特别是在seek的时候。在github上看到了AndroidVideoCache,使用起来非常方便,大概知道它是用代理实现的,但是代理具体怎么做的一直没去深究,今天刚好得空,好好研究下,和大家分享。源码链接 https://github.com/danikula/AndroidVideoCache

AndroidVideoCache的用法

1.添加依赖  compile 'com.danikula:videocache:2.7.1'
    2.在Application里面创建全局单例 HttpProxyCacheServer,代码如下

public class App extends Application {private HttpProxyCacheServer proxy;public static HttpProxyCacheServer getProxy(Context context) {App app = (App) context.getApplicationContext();return app.proxy == null ? (app.proxy = app.newProxy()) : app.proxy;}private HttpProxyCacheServer newProxy() {return new HttpProxyCacheServer(this);}
}

3.在给播放器设置url的时候通过生成代理url来实现视频的缓存,示例代码如下

    private void startVideo() {//拿到全局的单例 HttpProxyCacheServerHttpProxyCacheServer proxy = App.getProxy(getActivity());//注册下载缓存监听proxy.registerCacheListener(this, url);//生成代理urlString proxyUrl = proxy.getProxyUrl(url);//给播放器设置播放路径的时候设置为上一步生成的proxyUrlvideoView.setVideoPath(proxyUrl);videoView.start();}

以上就是AndroidVideoCache的使用,是不是特简单!当然它还可以设置缓存的大小,缓存路径、缓存文件的数量等,在初始化的时候设置即可,代码如下

private HttpProxyCacheServer newProxy() {return new HttpProxyCacheServer.Builder(this).cacheDirectory(Utils.getVideoCacheDir(this))//缓存路径.maxCacheFilesCount(100)//最大缓存文件数量.maxCacheSize(500 * 1024 * 1024)//最大缓存大小.build();}

源码分析

分析源码之前,我们先大致了解一下这个框架的工作流程
        1.播放器播放视频的时候会使用我们给它设置的代理url,访问proxyUrl的时候自然走到了ProxyServer
        2.ProxyServer先判断是否有本地缓存,如果有本地缓存那么直接将本地缓存返回
        3.如果没有本地缓存,那么ProxyServer会使用原视频url去远程服务器RemoteServer请求视频数据,获取到RemoteServer返回的数据后再缓存到本地(需要缓存则缓存),然后再通知播放器进度更新同时将数据返回给播放器播放

分析源码当然要找一个入口,这里我们的入口当然是初始化HttpProxyCacheServer的地方,以上面的Builder方式初始化为例,我们看看HttpProxyCacheServer.Builder代码

        public Builder(Context context) {this.sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(context);//设置缓存路径this.cacheRoot = StorageUtils.getIndividualCacheDirectory(context);//设置缓存策略,采用限制大小的LRU策略this.diskUsage = new TotalSizeLruDiskUsage(DEFAULT_MAX_SIZE);//主要用来生成缓存文件名this.fileNameGenerator = new Md5FileNameGenerator();//默认不添加Http头信息this.headerInjector = new EmptyHeadersInjector();}

这里我们只分析this.sourceInfoStorage,其它的都已备注,没什么好解释的。我们看看SourceInfoStorageFactory
.newSourceInfoStorage做了什么,其实它只是new 了一个DatabaseSourceInfoStorage,源码如下

public class SourceInfoStorageFactory {public static SourceInfoStorage newSourceInfoStorage(Context context) {return new DatabaseSourceInfoStorage(context);}public static SourceInfoStorage newEmptySourceInfoStorage() {return new NoSourceInfoStorage();}
}

通过名字我们可以看出,这个用到了SQLite,我们再跟进去看看DatabaseSourceInfoStorage的构造,就会发现缓存的信息其实是存储在数据库里面的

class DatabaseSourceInfoStorage extends SQLiteOpenHelper implements SourceInfoStorage {private static final String TABLE = "SourceInfo";private static final String COLUMN_ID = "_id";private static final String COLUMN_URL = "url";private static final String COLUMN_LENGTH = "length";private static final String COLUMN_MIME = "mime";private static final String[] ALL_COLUMNS = new String[]{COLUMN_ID, COLUMN_URL, COLUMN_LENGTH, COLUMN_MIME};private static final String CREATE_SQL ="CREATE TABLE " + TABLE + " (" +COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +COLUMN_URL + " TEXT NOT NULL," +COLUMN_MIME + " TEXT," +COLUMN_LENGTH + " INTEGER" +");";DatabaseSourceInfoStorage(Context context) {super(context, "AndroidVideoCache.db", null, 1);checkNotNull(context);}@Overridepublic void onCreate(SQLiteDatabase db) {checkNotNull(db);db.execSQL(CREATE_SQL);}@Overridepublic void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {throw new IllegalStateException("Should not be called. There is no any migration");}@Overridepublic SourceInfo get(String url) {checkNotNull(url);Cursor cursor = null;try {cursor = getReadableDatabase().query(TABLE, ALL_COLUMNS, COLUMN_URL + "=?", new String[]{url}, null, null, null);return cursor == null || !cursor.moveToFirst() ? null : convert(cursor);} finally {if (cursor != null) {cursor.close();}}}@Overridepublic void put(String url, SourceInfo sourceInfo) {checkAllNotNull(url, sourceInfo);SourceInfo sourceInfoFromDb = get(url);boolean exist = sourceInfoFromDb != null;ContentValues contentValues = convert(sourceInfo);if (exist) {getWritableDatabase().update(TABLE, contentValues, COLUMN_URL + "=?", new String[]{url});} else {getWritableDatabase().insert(TABLE, null, contentValues);}}@Overridepublic void release() {close();}private SourceInfo convert(Cursor cursor) {return new SourceInfo(cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_URL)),cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_LENGTH)),cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_MIME)));}private ContentValues convert(SourceInfo sourceInfo) {ContentValues values = new ContentValues();values.put(COLUMN_URL, sourceInfo.url);values.put(COLUMN_LENGTH, sourceInfo.length);values.put(COLUMN_MIME, sourceInfo.mime);return values;}
}

这个代码非常明了,其实就是做的数据库的初始化工作,数据库里面存的字段主要是url、length、mime ,SourceInfo这个类也仅仅是对这3个字段的封装,get put都是基于SourceInfo的,仅仅是方便而已,无需解释,这个主要是用来查找和保存缓存的信息
    Builder走完后就到了build方法,build方法里面其实就是创建HttpProxyCacheServer的实例了,代码如下

private HttpProxyCacheServer(Config config) {this.config = checkNotNull(config);try {//PROXY_HOST为127.0.0.1其实就是拿的localhostInetAddress inetAddress = InetAddress.getByName(PROXY_HOST);//通过localhost生成一个ServerSocket,localPort传0的话系统会随机分配一个端口号this.serverSocket = new ServerSocket(0, 8, inetAddress);//拿到系统分配的端口号this.port = serverSocket.getLocalPort();//将localhost添加到IgnoreHostProxySelectorIgnoreHostProxySelector.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 startsthis.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);}}

这里我们主要分析this.waitConnectionThread = new Thread(new WaitRequestsRunnable(startSignal)); 其它的都已注释。我们跟进去看看 WaitRequestRunnalbe里面做了什么

private final class WaitRequestsRunnable implements Runnable {private final CountDownLatch startSignal;public WaitRequestsRunnable(CountDownLatch startSignal) {this.startSignal = startSignal;}@Overridepublic void run() {startSignal.countDown();waitForRequest();}}private void waitForRequest() {try {while (!Thread.currentThread().isInterrupted()) {Socket socket = serverSocket.accept();LOG.debug("Accept new socket " + socket);socketProcessor.submit(new SocketProcessorRunnable(socket));}} catch (IOException e) {onError(new ProxyCacheException("Error during waiting connection", e));}}

监听到请求后,就是处理请求了,我们可以看到waitForRequest 里面监听到请求后调用了
socketProcessor.submit(new SocketProcessorRunnable(socket)),我们跟进去看看这里面又做了些什么

    private final class SocketProcessorRunnable implements Runnable {private final Socket socket;public SocketProcessorRunnable(Socket socket) {this.socket = socket;}@Overridepublic void run() {//仅仅是调用processSocketprocessSocket(socket);}}private void processSocket(Socket socket) {try {//*** GetRequest.read(socket.getInputStream())会从proxyUrl里面提取视频原始url,即下面的request.uri是视频的原路径GetRequest request = GetRequest.read(socket.getInputStream());LOG.debug("Request to cache proxy:" + request);String url = ProxyCacheUtils.decode(request.uri);if (pinger.isPingRequest(url)) {pinger.responseToPing(socket);} else {HttpProxyCacheServerClients clients = getClients(url);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 stacktraceLOG.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());}}

这里就是处理网络请求了,那么里面的这个url是怎么来的呢,又是如何转换的,这个是关键,这里我整理了一下代码,大家看代码里的注释便一目了然

private void startVideo() {HttpProxyCacheServer proxy = App.getProxy(getActivity());//1.我们会将原始url注册进去proxy.registerCacheListener(this, url);//2.我们播放视频的时候会调用以下代码生成proxyUrlString proxyUrl = proxy.getProxyUrl(url);Log.d(LOG_TAG, "Use proxy url " + proxyUrl + " instead of original url " + url);videoView.setVideoPath(proxyUrl);videoView.start();}//3.步骤1中的url会传到这里
public void registerCacheListener(CacheListener cacheListener, String url) {checkAllNotNull(cacheListener, url);synchronized (clientsLock) {try {//4.原始视频url又被传递下去getClients(url).registerCacheListener(cacheListener);} catch (ProxyCacheException e) {LOG.warn("Error registering cache listener", e);}}}private HttpProxyCacheServerClients getClients(String url) throws ProxyCacheException {synchronized (clientsLock) {HttpProxyCacheServerClients clients = clientsMap.get(url);if (clients == null) {//5.url又被传递clients = new HttpProxyCacheServerClients(url, config);clientsMap.put(url, clients);}return clients;}}//6.到了这里就清楚了,其实HttpProxyCacheServerClients里面保存的是视频的原始url
public HttpProxyCacheServerClients(String url, Config config) {this.url = checkNotNull(url);this.config = checkNotNull(config);this.uiCacheListener = new UiListenerHandler(url, listeners);
}

接下来我们看看第2步里面生成的ProxyUrl具体对视频原始路径是如何进行操作的,其实就是将原始url经过URL编码后拼接在localhost后面的,不信我们看源码

public String getProxyUrl(String url) {return getProxyUrl(url, true);}public String getProxyUrl(String url, boolean allowCachedFileUri) {if (allowCachedFileUri && isCached(url)) {//如果视频已缓存,直接返回本地视频uriFile cacheFile = getCacheFile(url);//touchFileSafely只是更新视频的lastModifyTime,因为是LRUCachetouchFileSafely(cacheFile);return Uri.fromFile(cacheFile).toString();}return isAlive() ? appendToProxyUrl(url) : url;}private String appendToProxyUrl(String url) {return String.format(Locale.US, "http://%s:%d/%s", PROXY_HOST, port, ProxyCacheUtils.encode(url));}

既然给播放器的url是一个拼接过后的url,那么这个url怎么使用呢,按理它不是一个正常的url,因为前面是一个完成的链接,后面也是一个完整的链接,这里我猜它内部肯定用到了字符串拆分,查看源码果不其然,这里将代码整理了一下,大家一看就明白

private void processSocket(Socket socket) {try {//1.操作socket调用GetRequest.read()GetRequest request = GetRequest.read(socket.getInputStream());LOG.debug("Request to cache proxy:" + request);String url = ProxyCacheUtils.decode(request.uri);if (pinger.isPingRequest(url)) {pinger.responseToPing(socket);} else {HttpProxyCacheServerClients clients = getClients(url);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 stacktraceLOG.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());}}public static GetRequest read(InputStream inputStream) throws IOException {BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));StringBuilder stringRequest = new StringBuilder();String line;while (!TextUtils.isEmpty(line = reader.readLine())) { // until new line (headers ending)stringRequest.append(line).append('\n');}//2.此处又调用了GetRequest的构造return new GetRequest(stringRequest.toString());}public GetRequest(String request) {checkNotNull(request);long offset = findRangeOffset(request);this.rangeOffset = Math.max(0, offset);this.partial = offset >= 0;//3.构造里面对request.uri进行了处理this.uri = findUri(request);}private String findUri(String request) {//4.使用正则匹配链接,去第2个Matcher matcher = URL_PATTERN.matcher(request);if (matcher.find()) {return matcher.group(1);}throw new IllegalArgumentException("Invalid request `" + request + "`: url not found!");}private static final Pattern URL_PATTERN = Pattern.compile("GET /(.*) HTTP");

以上就是代理url的生成以及使用的时候转换的过程,那么具体的接收服务端返回的视频流以及缓存到本地是如何实现的呢,其实上面的processSocket方法里面调用了clients.processRequest(request, socket) 我们跟进去看看 HttpProxyCacheServerClient.
processRequest()方法又做了些什么,具体看我标的顺序就明了了

public void processRequest(GetRequest request, Socket socket) throws ProxyCacheException, IOException {//1.调用了startProcessRequest来处理requeststartProcessRequest();try {clientsCount.incrementAndGet();proxyCache.processRequest(request, socket);} finally {finishProcessRequest();}}private synchronized void startProcessRequest() throws ProxyCacheException {//2.获得一个HttpProxyCache proxyCache = proxyCache == null ? newHttpProxyCache() : proxyCache;}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;}//以上可以看出如果proxyCache为空的话则会创建一个新的HttpProxyCache,那么在步骤1执行完毕后就会执行proxyCache.processRequest(request,socket)
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"));long offset = request.rangeOffset;if (isUseCache(request)) {//3.处理带缓存的情况,这是我们要分析的地方responseWithCache(out, offset);} else {responseWithoutCache(out, offset);}}private void responseWithCache(OutputStream out, long offset) throws ProxyCacheException, IOException {byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];int readBytes;//4.注意此处调用了read(buffer,offset,buffer.length)while ((readBytes = read(buffer, offset, buffer.length)) != -1) {out.write(buffer, 0, readBytes);offset += readBytes;}out.flush();}public int read(byte[] buffer, long offset, int length) throws ProxyCacheException {ProxyCacheUtils.assertBuffer(buffer, offset, length);while (!cache.isCompleted() && cache.available() < (offset + length) && !stopped) {//5.这里就是缓存到本地的操作了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) {//6.具体的缓存操作在SourceReaderRunnable里面sourceReaderThread = new Thread(new SourceReaderRunnable(), "Source reader for " + source);sourceReaderThread.start();}}private class SourceReaderRunnable implements Runnable {@Overridepublic void run() {//7.到此,我们会发现所有的缓存操作都是方法readSource()做的了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);}}

注意:responseWithCache里面传的OutputStream   是processRequest里面传递进来的socket的输出流,即播放器请求视频的socket的输出流,所以最后通过该OutputStream写流的时候其实就是返给播放器

总的来说这个框架个人觉得搞清楚
        1.ProxyUrl怎么生成的,是什么形式,将localhost和视频源url编码后拼接
        2.ProxyUrl的使用,其实是使用的时候采用正则匹配然后取视频源url
        3.缓存和进度更新是怎么处理的,其实就是在接收远程服务器返回的视频流后缓存到本地然后通知本地下载进度更新,然后将该流返回给请求的播放器

AndroidVideoCache解析相关推荐

  1. AndroidVideoCache研究

    AndroidVideoCache研究 01.AndroidVideoCache + ijk 我们想让ijk支持边下边播的能力,通过AndroidVideoCache就可以实现,AndroidVide ...

  2. Android:Android9.0使用 AndroidVideoCache时不能缓存播放视频的解决

    一.问题现象: 项目中使用 https://github.com/danikula/AndroidVideoCache 作为视频缓存组件,但是在9.0手机上无法正常缓存,并且报错: 1.详细错误截图 ...

  3. android视频缓存框架 [AndroidVideoCache](https://github.com/danikula/AndroidVideoCache) 源码解析与评估

    文章目录 android视频缓存框架 [AndroidVideoCache](https://github.com/danikula/AndroidVideoCache) 源码解析与评估 引言 使用方 ...

  4. Android视频边播放边缓存的代理策略之——AndroidVideoCache

    简介 AndroidVideoCache是国外大神Alexey Danilov写的一个android开源库.一个视频/音频缓存库,利用本地代理实现了边下边播,支VideoView/MediaPlaye ...

  5. golang通过RSA算法生成token,go从配置文件中注入密钥文件,go从文件中读取密钥文件,go RSA算法下token生成与解析;go java token共用

    RSA算法 token生成与解析 本文演示两种方式,一种是把密钥文件放在配置文件中,一种是把密钥文件本身放入项目或者容器中. 下面两种的区别在于私钥公钥的初始化, init方法,需要哪种取哪种. 通过 ...

  6. List元素互换,List元素转换下标,Java Collections.swap()方法实例解析

    Java Collections.swap()方法解析 jdk源码: public static void swap(List<?> list, int i, int j) {// ins ...

  7. 条形码?二维码?生成、解析都在这里!

    二维码生成与解析 一.生成二维码 二.解析二维码 三.生成一维码 四.全部的代码 五.pom依赖 直接上代码: 一.生成二维码 public class demo {private static fi ...

  8. Go 学习笔记(82)— Go 第三方库之 viper(解析配置文件、热更新配置文件)

    1. viper 特点 viper 是一个完整的 Go应用程序的配置解决方案,它被设计为在应用程序中工作,并能处理所有类型的配置需求和格式.支持特性功能如下: 设置默认值 读取 JSON.TOML.Y ...

  9. Go 学习笔记(77)— Go 第三方库之 cronexpr(解析 crontab 表达式,定时任务)

    cronexpr 支持的比 Linux 自身的 crontab 更详细,可以精确到秒级别. ​ 1. 实现方式 cronexpr 表达式从前到后的顺序如下所示: 字段类型 是否为必须字段 允许的值 允 ...

最新文章

  1. GPT-2大战GPT-3:OpenAI内部的一场终极对决
  2. 支付宝公共服务窗开发总结
  3. MATLAB插值问题
  4. Java-类与对象的创建
  5. echart data放入数组_线性表(数组、链表、队列、栈)详细总结
  6. zabbix中文乱码设置
  7. 谷歌 analytics.js 简要分析
  8. R语言模拟:Cross Validation
  9. Linux scp连接很慢,ssh连接很慢问题分析
  10. Qos测试浅析 20090323
  11. 【图论】Bellman_Ford算法求有步数限制的最短路(图文详解)
  12. access2016访问mysql_关于VB连接access2016数据库
  13. 用mysql设计一个超市员工管理系统_数据库设计--小型超市管理系统
  14. 2014.12 总结
  15. web 前端面试题50道
  16. 投资人不投了、撤资了,创业者怎么办?
  17. Html中 发光字体 的CSS属性
  18. Py之shap:shap库的简介、安装、使用方法之详细攻略
  19. java 请假系统_JAVA 师生请假系统 课程设计
  20. 百度一下,你就知道.2

热门文章

  1. 三角定位matlab,matlab 在三维空间的三边定位算法模拟如何写?
  2. 100、基于51单片机数码管温控 温度控制风扇系统设计
  3. oracle如何查询记录生成时间戳,Oracle使用范围内的时间戳记记录历史记录
  4. Contract Coin (C-coin)4月12日全球正式上线
  5. 织梦可以不用mysql吗_织梦dedecms不用功能精简及安全设置
  6. 20种热带风景摄影调色luts预设
  7. 信号的调制与解调matlab仿真,基于MATLAB对信号调制与解调的仿真
  8. QQ2009 Preview deb包 更新下载地址
  9. DP4301—SUB-1G高集成度无线收发芯片
  10. fork() fork() || fork()