AndroidVideoCache简单使用及源码分析

严曜文
2023-12-01

        对于视频播放,如果需要用到缓存,AndroidVideoCach是一个不错的选择,该项目地址:

        https://github.com/danikula/AndroidVideoCache 

优缺点:

        优点:1、使用简单,支持设置缓存视频的大小或个数;

                  2、支持断点缓存(一段视频缓存一部分后,退出关闭视频后,下次再看时会从上次缓存到的部分开始继续缓存);

        缺点:1、当快进视频时,如果视频没缓存到该位置,需要等视频缓存到这个点才会播放,不会直接跳到快进点开始缓存;

使用:

        使用还是很简单的,首先在Application中进行初始化:

 

public static HttpProxyCacheServer getProxy(Context context) {
    AppApplication app = (AppApplication) context.getApplicationContext();
    return app.proxy == null ? (app.proxy = app.newProxy()) : app.proxy;
}


private HttpProxyCacheServer newProxy() {
    return new HttpProxyCacheServer.Builder(this).cacheDirectory(new File(FileUtils.getCachedDirs(this)))
            //最大缓存200M
            .maxCacheSize(200 * 1024 * 1024)
            .build();
}

这里其实就是为了创建一个HttpProxyCacheServer的单例对象。你也可以根据自己的需求来创建。创建完这个对象后,接下来就是使用这个对象生成一个代理的url路径了:

 

String proxyUrl = getProxy.getProxyUrl("urlPath");

urlPath指的是网络上的视频路径,返回的proxyUrl是一个代理路径,得到这个代理路径后,接下来就只需要将这个路径设置给播放器就完成了。

        简单的使用说完了,接下来就去看看HttpProxyCacheServer是如何构建的,从它的构建方法中我们才能知道他可以设置哪些参数以及有什么作用:

 

public static final class Builder {

    private static final long DEFAULT_MAX_SIZE = 512 * 1024 * 1024;

    private File cacheRoot;
    private FileNameGenerator fileNameGenerator;
    private DiskUsage diskUsage;
    private SourceInfoStorage sourceInfoStorage;

    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();
    }

    //视频文件的缓存路径
    public Builder cacheDirectory(File file) {
        this.cacheRoot = checkNotNull(file);
        return this;
    }

    //生成的缓存视频文件的名字,传进来的对象实现FileNameGenerator这个类就可以了
    public Builder fileNameGenerator(FileNameGenerator fileNameGenerator) {
        this.fileNameGenerator = checkNotNull(fileNameGenerator);
        return this;
    }
    
    //设置缓存文件的大小,单位是bytes,默认是512M
    public Builder maxCacheSize(long maxSize) {
        this.diskUsage = new TotalSizeLruDiskUsage(maxSize);
        return this;
    }

    //设置缓存文件的个数,缓存的策略只能是大小和个数中的一个
    public Builder maxCacheFilesCount(int count) {
        this.diskUsage = new TotalCountLruDiskUsage(count);
        return this;
    }

    //缓存策略也可以自己定义,实现DiskUsage就可以了,看自己需要
    public Builder diskUsage(DiskUsage diskUsage) {
        this.diskUsage = checkNotNull(diskUsage);
        return this;
    }
    
    public HttpProxyCacheServer build() {
        Config config = buildConfig();
        return new HttpProxyCacheServer(config);
    }

    private Config buildConfig() {
        return new Config(cacheRoot, fileNameGenerator, diskUsage, sourceInfoStorage);
    }
}

从上面可以看出,构建HttpProxyCacheServer提供给我们可以设置的可以分为三类:

        1、设置缓存文件得名字,默认使用的是url的MD5码;

        2、设置缓存文件的路径,默认在路径:Android/data/包名/cache;

        3、设置缓存文件的大小,默认是512M;

到这里,使用就不成问题了,那如果你想知道它是如何实现的,那就继续往下看。

源码分析:

        在看代码前,先说下它的整个设计思路,大体可以理解成两部分,这也是我看完后自己的理解:

        1、开启一个线程池去给定的路径下下载文件,将下载的文件保存到本地;

        2、视频播放的时候读保存到本地的文件,如果播放的地方还没保存到本地,那就需要等待视频下载到这个地方才能播放;

        上面这种情况是边播边缓存,如果在已经缓存好去播视频时,这时执行的逻辑就是直接播放本地视频了;

        接下来就看看具体是如何实现的,首先要看的就是HttpProxyCacheServer的构造方法了:

 

private HttpProxyCacheServer(Config config) {
    this.config = checkNotNull(config);
    try {
        //使用Socket将本地的一个端口作为服务器,这个端口号自动生成
        InetAddress inetAddress = InetAddress.getByName(PROXY_HOST);
        this.serverSocket = new ServerSocket(0, 8, inetAddress);
        //本地服务器的端口号
        this.port = serverSocket.getLocalPort();
        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
        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);
    }
}

上面创建了一个线程,然后就开启了,要看就应该是这个线程到底做了什么,跟着代码走下去,最后执行的方法是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));
    }
}

代码还是挺少的,就是一个while循环,这个循环的条件就是当前线程没有被中断,这里还有一个需要注意的地方就是ServerSocket的accept()方法,这是一个阻塞方法,对Socket不太了解的可以先去了解下,当有访问这个ServerSocket的端口时,这是就会返回一个Socket对象,通过这个对象就可以与客户端进行通信了,这个Socket可以理解为就是视频播放器那边传过来的,我们把视频数据从这个Socket中返回,那视频就可以播放了。socketProcess是一个线程池,把这个socket对象放进了线程中,在线程中有做了些什么呢?那就看下SocketProcessRunnable这个对象的run()方法执行了什么,它里面调用的是processSocket()方法,那就看下这个方法:

 

private void processSocket(Socket socket) {
    try {
        //这里就是获取socket流中请求头的信息,然后创建了一个GetRequest对象并返回
        GetRequest request = GetRequest.read(socket.getInputStream());
        LOG.debug("Request to cache proxy:" + request);
        //request.uri是我们传给播放器的代理url,这里获取的是真正的url
        String url = ProxyCacheUtils.decode(request.uri);
        //这个url是分为两种情况的,一种是ping的时候传的url,另一种就是真正请
        // 求资源的url路径了,获取代理路径的时候就会去ping,感兴趣的可以从获
        // 取代理路径那里跟下去看
        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 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这个类中,这里调用的是processRequest(),看来这里可以算作是一个请求资源的入口了:

 

public void processRequest(GetRequest request, Socket socket) throws ProxyCacheException, IOException {
    startProcessRequest();
    try {
        clientsCount.incrementAndGet();
        proxyCache.processRequest(request, socket);
    } finally {
        finishProcessRequest();
    }
}

首先来看的是startProcessRequest()这个方法,这里主要还是实例化HttpProxyCache对象:

 

private synchronized void startProcessRequest() throws ProxyCacheException {
    proxyCache = proxyCache == null ? newHttpProxyCache() : proxyCache;
}
private HttpProxyCache newHttpProxyCache() throws ProxyCacheException {
    //获取远程资源就是通过这个对象
    HttpUrlSource source = new HttpUrlSource(url, config.sourceInfoStorage);
    //远程资源缓存在本地的文件
    FileCache cache = new FileCache(config.generateCacheFile(url), config.diskUsage);
    HttpProxyCache httpProxyCache = new HttpProxyCache(source, cache);
    httpProxyCache.registerCacheListener(uiCacheListener);
    return httpProxyCache;
}

接下来就是去看看HttpProxyCache的processRequest()方法了:

 

public void processRequest(GetRequest request, Socket socket) throws IOException, ProxyCacheException {
    //拿到socket的输出流
    OutputStream out = new BufferedOutputStream(socket.getOutputStream());
    //组装响应头信息
    String responseHeaders = newResponseHeaders(request);
    //将组装的头信息输出到socket的输出流中
    out.write(responseHeaders.getBytes("UTF-8"));

    long offset = request.rangeOffset;
    //是否使用缓存,这里分析使用缓存的情况
    if (isUseCache(request)) {
        responseWithCache(out, offset);
    } else {
        responseWithoutCache(out, offset);
    }
}

继续往下就是responseWithCache()方法了:

 

private void responseWithCache(OutputStream out, long offset) throws ProxyCacheException, IOException {
    byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
    int readBytes;
    //read()方法是重点,这个方法是读取数据,知道读取数据完毕才会停止
    while ((readBytes = read(buffer, offset, buffer.length)) != -1) {
        //out是socket的输出流,这里就是将读取的数据输出给视频播放
        out.write(buffer, 0, readBytes);
        offset += readBytes;
    }
    out.flush();
}

现在要看的就是read()这个方法是从哪里读取到的数据:

 

public int read(byte[] buffer, long offset, int length) throws ProxyCacheException {
    ProxyCacheUtils.assertBuffer(buffer, offset, length);
    //cache就是我们前面说的缓存在本地的文件
    //cache.isCompleted()本地是否已经全部缓存完,没有返回false
    //cache.available() < (offset + length)这个条件成立是:当前缓存文件的长度还没缓存到需要读取数据的长度
    //stoped标记的是当前是否已经停止了
    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;
}

这里就是主要的逻辑所在,前半部分是从服务器读取数据缓存在本地,后半部分就是从本地缓存区中读取数据了,这里我们先看简单的,那就是cache的read()方法,这里的cache是FileCache对象:

 

@Override
public synchronized int read(byte[] buffer, long offset, int length) throws ProxyCacheException {
    try {
        dataFile.seek(offset);
        return dataFile.read(buffer, 0, length);
    } catch (IOException e) {
        String format = "Error reading %d bytes with offset %d from file[%d bytes] to buffer[%d bytes]";
        throw new ProxyCacheException(String.format(format, length, offset, available(), buffer.length), e);
    }
}

还是很简单的,这里的dataFile是一个RandomAccessFile对象,就是从这个文件对象中去获取资源,然后将获取到的资源输出到socket的输出流中,也就是视频播放了。

        接下来就是看去网络上获取资源了,这里看到方法是readSourceAsync():

 

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();
    }
}

这里又是开启了一个线程,那自然还是看这个线程中的run()了,run()方法中执行的是readSource()方法:

 

private void readSource() {
    long sourceAvailable = -1;
    long offset = 0;
    try {
        //这个cache是FileCache对象,返回的就是已缓存文件的长度,
        // 这也从侧面说明是支持断点续传的
        offset = cache.available();
        //这个source是一个HttpUrlSource对象,这里面封装的就
        // 是HttpUrlConnection对资源的获取
        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);
    }
}

到这里就全部都连接上了,通过HttpUrlConnection从服务器进行数据的读取,将读取的数据缓存在本地,然后播放的视频数据就从缓存中读取的。今天就到这了,如果有什么疑问,欢迎留言。

        

 

 类似资料: