学而实习之 不亦乐乎

Android 视频缓存框架 AndroidVideoCache 用法

2023-08-18 07:44:19

一、基本原理

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的方式,给一个视频进行分片。这个后面再分析另外一个开源项目是再来一些拆解。

5、AndroidVideoCache采用数据库进行存储缓存的信息,可以不使用,减少IO操作

6、如果我们的有其他代理,那么这个socket方式拿url就会出问题,因为我们拿到的也是一个代理url,所以在开发时需要考虑代理用户提供兼容性处理。