Android 视频缓存框架 AndroidVideoCache 用法
一、基本原理
AndroidVideoCache 通过代理的策略将我们的网络请求代理到本地服务,本地服务再决定是从本地缓存拿还是发起网络请求,如果需要发起网络请求就先向本地写入数据,再从本地提供数据给视频播放器。这样就做到了数据的复用。
图
在视频播放器,比如VideoView发起一个urlA,通过HttpProxyCacheServer转成一个本地host和端口的urlB,这样视频播放器发起请求就是向HttpProxyCacheServer请求,返回视频播放器的Socket,Server再建立一个HttpProxyCacheServerClients来发起网络请求处理缓存等工作,然后把数据通过前面的Socket返回给视频播放器。
二、用法
1、导入依赖包
dependencies {
   compile 'com.danikula:videocache:2.7.1'
}
2、初始化代理服务器
在全局初始化一个本地代理服务器,这里选择在 Application 的实现类中
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
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
   HttpProxyCacheServer proxy = getProxy();
   String proxyUrl = proxy.getProxyUrl(VIDEO_URL);
   videoView.setVideoPath(proxyUrl);
}
提供了更多的可以自定义的地方,比如缓存的文件最大大小,以及文件个数,缓存采取的是 LruCache 的方法,对于老文件在达到上限后会自动清理。
private HttpProxyCacheServer newProxy() {
    return new HttpProxyCacheServer.Builder(this)
            .maxCacheSize(1024 * 1024 * 1024)       // 1 Gb for cache
            .build();
}
private HttpProxyCacheServer newProxy() {
    return new HttpProxyCacheServer.Builder(this)
            .maxCacheFilesCount(20)
            .build();
}
除了这个,还有一个就是生成的文件名,默认是使用的 MD5 方式生成 key,考虑到一些业务逻辑,我们也可以继承一个 FileNameGenerator 来实现自己的策略
public class MyFileNameGenerator implements FileNameGenerator {
    // Urls contain mutable parts (parameter 'sessionToken') and stable video's id (parameter 'videoId').
    // e. g. http://example.com?videoId=abcqaz&sessionToken=xyz987
    public String generate(String url) {
        Uri uri = Uri.parse(url);
        String videoId = uri.getQueryParameter("videoId");
        return videoId + ".mp4";
    }
}
...
HttpProxyCacheServer proxy = HttpProxyCacheServer.Builder(context)
    .fileNameGenerator(new MyFileNameGenerator())
    .build()
很明显,构造Server是通过建造者的模式,看下Builder的代码就知道支持哪些配置和默认配置是什么了。
private static final long DEFAULT_MAX_SIZE = 512 * 1024 * 1024;
private File cacheRoot;
private FileNameGenerator fileNameGenerator;
private DiskUsage diskUsage;
private SourceInfoStorage sourceInfoStorage;
private HeaderInjector headerInjector;
public Builder(Context context) {
    this.sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(context);
    this.cacheRoot = StorageUtils.getIndividualCacheDirectory(context);
    this.diskUsage = new TotalSizeLruDiskUsage(DEFAULT_MAX_SIZE);
    this.fileNameGenerator = new Md5FileNameGenerator();
    this.headerInjector = new EmptyHeadersInjector();
}
cacheRoot就是缓存默认的文件夹,如果有sd卡并且申请了权限,会放到目录("/Android/data/[app_package_name]/cache")
否则放到手机的内部存储
cacheDirPath = "/data/data/" + context.getPackageName() + "/cache/";
FileNameGenerator用于生成文件名,默认是 Md5FileNameGenerator,生成MD5串作为文件名。
DiskUsage是用于管理本地缓存,默认是通过文件大小进行管理,大小默认是512M
private static final long DEFAULT_MAX_SIZE = 512 * 1024 * 1024;
this.diskUsage = new TotalSizeLruDiskUsage(DEFAULT_MAX_SIZE);
SourceInfoStorage是用于存储SourInfo,默认是数据库存储
this.sourceInfoStorage = SourceInfoStorageFactory.newSourceInfoStorage(context);
public static SourceInfoStorage newSourceInfoStorage(Context context) {
        return new DatabaseSourceInfoStorage(context);
}
那SourInfo是什么?主要用于存储http请求源的一些信息,比如url,数据长度length,请求资源的类型mime:
public final String url;
public final long length;
public final String mime;
HeaderInjector主要用于添加一些自定义的头部字段,默认是空
this.headerInjector = new EmptyHeadersInjector();
最后把这些字段构造成Config,构造HttpProxyCacheServer需要,后面会再传给HttpProxyCacheServerClients用于发起请求(url,length,mime)等,和本地缓存(DiskUsage,SourceInfoStorage,cacheRoot)等。
/**
 * Builds new instance of {@link HttpProxyCacheServer}.
 *
 * @return proxy cache. Only single instance should be used across whole app.
 */
public HttpProxyCacheServer build() {
    Config config = buildConfig();
    return new HttpProxyCacheServer(config);
}
private Config buildConfig() {
    return new Config(cacheRoot, fileNameGenerator, diskUsage, sourceInfoStorage, headerInjector);
}
三、AndroidVideoCache 的不足
1、Seek 的场景
播放器 Seek 后有可能就不缓存了。
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;
}
这个不符合我们的预期,seek后也应该进行缓存,这是缓存文件之间可能存在空洞,需要针对这种情况做些特殊处理。
2、预缓存(脱离播放器实现缓存)
提前下载,无论视频是否下载完成,都可以将这提前下载好的部分作为视频缓存使用,进行下扩展。根据url创建GetRequest,然后调用HttpProxyCacheServerClients#processRequest即可
HttpProxyCacheServerClients clients = getClients(url);
clients.processRequest(request);
3、线程管理
开启线程过多,过多线程的内存消耗以及状态同步是一个需要注意点。可以把线程改为线程池的方式实现。但是要特别并发和状态同步。
HttpProxyCacheServer.WaitRequestsRunnable—》等待socket连接
HttpProxyCacheServer.SocketProcessorRunnable—》处理单个socket连接
ProxyCache.SourceReaderRunnable —>分块(8192个字节)读取网络数据流写入到缓存文件并且返回给clientSocket 【这个线程要重点分析】
4、缓存是根据url来进行区分,对于大的视频,没有进行分片下载,节省流量
可以参考m3u8的方式,给一个视频进行分片。这个后面再分析另外一个开源项目是再来一些拆解。
