一、关于AndroidVideoCache
AndroidVideoCache是一个音视频缓存库,用于支持VideoView/MediaPlayer, ExoPlayer ,IJK等播放器的边下载边播放,按照github列出支持的特性如下:
1、音视频播放的时候会将多媒体数据存储于磁盘上面
2、如果播放的数据已经缓存,支持离线播放
3、支持部分加载(该特性未知)
4、可以设置缓存配置,如缓存的大小,允许最大的缓存文件数量
5、对于同一个url地址请求源,允许有多个请求客户端链接(见下文代码中红色标注)
6、不支持DASH, SmoothStreaming, HLS之类的流媒体协议
二、AndroidVideoCache的使用
1、AndroidVideoCache只在jcenter中存在,所以添加如下库依赖
repositories { jcenter() } dependencies { compile 'com.danikula:videocache:2.6.3' } |
2、在代码中使用
if (mProxy == null) { mProxy = new HttpProxyCacheServer(mContext.getApplicationContext()); } String proxyUrl = mProxy.getProxyUrl(mUri.toString()); mMediaPlayer.setDataSource(mContext, Uri.parse(proxyUrl)); |
其中,HttpProxyCacheServer保持单例。
三、源码解读
1、实例化HttpProxyCacheServer代码如下:
private HttpProxyCacheServer(Config config) { InetAddress inetAddress = InetAddress.getByName(PROXY_HOST); this.serverSocket = new ServerSocket(0, 8, inetAddress); this.port = serverSocket.getLocalPort(); 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); } |
首先设置PROXY_HOST为127.0.0.1的本地代理服务socket用于响应播放器的多媒体数据请求服务。WaitRequestsRunnable内部创建线程,进行ServerSocket监听:
while (!Thread.currentThread().isInterrupted()) { Socket socket = serverSocket.accept();//该步逻辑对于同一个url地址请求源,允许有多个请求客户端链接 socketProcessor.submit(new SocketProcessorRunnable(socket)); } |
该线程执行serverSocket.accept()挂起,直到播放器进行播放多媒体数据请求,serverSocket.accept()返回Socket和播放器交互。
2、HttpProxyCacheServer的getProxyUrl将流数据源url地址转化为本地的代理服务器url,传递给播放器使用
public String getProxyUrl(String url, boolean allowCachedFileUri) { if (allowCachedFileUri && isCached(url)) { File cacheFile = getCacheFile(url); touchFileSafely(cacheFile); return Uri.fromFile(cacheFile).toString(); } return isAlive() ? appendToProxyUrl(url) : url; } |
3、使用第二部生成的代理url地址进行MediaPlayer.setDataSource(mContext, Uri.parse(proxyUrl));该播放器使用代理url播放数据源,会建立到本地代理服务的socket连接。1步骤WaitRequestsRunnable的serverSocket.accept()返回Socket,执行
socketProcessor.submit(new SocketProcessorRunnable(socket)); |
SocketProcessorRunnable执行线程方法:
private void processSocket(Socket socket) { GetRequest request = GetRequest.read(socket.getInputStream()); String url = ProxyCacheUtils.decode(request.uri); if (pinger.isPingRequest(url)) { pinger.responseToPing(socket); } else { HttpProxyCacheServerClients clients = getClients(url); clients.processRequest(request, socket); } } |
processSocket方法解析出url地址,如果是ping包返回ping包数据响应,否则根据url生成HttpProxyCacheServerClients处理请求
public void processRequest(GetRequest request, Socket socket) throws ProxyCacheException, IOException { startProcessRequest(); try { clientsCount.incrementAndGet(); proxyCache.processRequest(request, socket); } finally { finishProcessRequest(); } } |
startProcessRequest内部实例化对象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 { OutputStream out = new BufferedOutputStream(socket.getOutputStream()); String responseHeaders = newResponseHeaders(request); out.write(responseHeaders.getBytes("UTF-8")); long offset = request.rangeOffset; if (isUseCache(request)) { responseWithCache(out, offset); } else { responseWithoutCache(out, offset); } } |
processRequest内部首先根据播放器的请求实例化ResponseHeaders返回200ok成功请求
private String newResponseHeaders(GetRequest request) throws IOException, ProxyCacheException { String mime = source.getMime(); boolean mimeKnown = !TextUtils.isEmpty(mime); int length = cache.isCompleted() ? cache.available() : source.length(); boolean lengthKnown = length >= 0; long contentLength = request.partial ? length - request.rangeOffset : length; boolean addRange = lengthKnown && request.partial; return new StringBuilder() .append(request.partial ? "HTTP/1.1 206 PARTIAL CONTENT\n" : "HTTP/1.1 200 OK\n") .append("Accept-Ranges: bytes\n") .append(lengthKnown ? String.format("Content-Length: %d\n", contentLength) : "") .append(addRange ? String.format("Content-Range: bytes %d-%d/%d\n", request.rangeOffset, length - 1, length) : "") .append(mimeKnown ? String.format("Content-Type: %s\n", mime) : "") .append("\n") // headers end .toString(); } |
然后计算获取的媒体偏移量同时判断是否设置以及允许使用本地缓存,如果设置直接从服务端获取,则responseWithoutCache,否则responseWithCache,此处分析responseWithCache
private void responseWithCache(OutputStream out, long offset) throws ProxyCacheException, IOException { byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; int readBytes; while ((readBytes = read(buffer, offset, buffer.length)) != -1) { 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); while (!cache.isCompleted() && cache.available() < (offset + length) && !stopped) {//循环读取数据,直到获取的数据达到一定的量才终止循环 readSourceAsync(); waitForSourceData(); checkReadSourceErrorsCount(); } int read = cache.read(buffer, offset, length); return read; } readSourceAsync函数: private synchronized void readSourceAsync() throws ProxyCacheException { sourceReaderThread = new Thread(new SourceReaderRunnable(), "Source reader for " + source); sourceReaderThread.start(); } 开取线程,执行线程方法readSource: private void readSource() { int sourceAvailable = -1; int offset = 0; try { offset = cache.available(); source.open(offset);(source对象在newHttpProxyCache中初始化为HttpUrlSource) sourceAvailable = source.length(); byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE]; int readBytes; while ((readBytes = source.read(buffer)) != -1) {//一直从媒体数据服务器获取数据 cache.append(buffer, readBytes); (cache对象在newHttpProxyCache中初始化为FileCache) } offset += readBytes; notifyNewCacheDataAvailable(offset, sourceAvailable); } tryComplete(); onSourceRead(); } } |
总结responseWithCache的步骤为:
1) 调用HttpUrlSource的openConnection函数,说明AndroidVideoCache从源获取多媒体数据基于http
private HttpURLConnection openConnection(int offset, int timeout) throws IOException, ProxyCacheException { HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); if (offset > 0) { connection.setRequestProperty("Range", "bytes=" + offset + "-"); } if (timeout > 0) { connection.setConnectTimeout(timeout); connection.setReadTimeout(timeout); } int code = connection.getResponseCode(); } |
2) 调用HttpUrlSource的open函数
public void open(int offset) throws ProxyCacheException { connection = openConnection(offset, -1); String mime = connection.getContentType(); inputStream = new BufferedInputStream(connection.getInputStream(), DEFAULT_BUFFER_SIZE); int length = readSourceAvailableBytes(connection, offset, connection.getResponseCode()); this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime); this.sourceInfoStorage.put(sourceInfo.url, sourceInfo); } |
3) 调用HttpUrlSource的read函数读数据到buffer当中
public int read(byte[] buffer)throws ProxyCacheException { try { return inputStream.read(buffer,0, buffer.length); } catch (InterruptedIOException e) { throw new InterruptedProxyCacheException("Reading source "+ sourceInfo.url+ " is interrupted", e); } catch (IOException e) { throw new ProxyCacheException("Error reading data from "+ sourceInfo.url, e); } } |
4) 调用FileCache中的将数据写入到磁盘当中
public synchronized void append(byte[] data,int length) throwsProxyCacheException { try { if (isCompleted()) { throw new ProxyCacheException("Error append cache: cache file "+ file +" is completed!"); } dataFile.seek(available()); dataFile.write(data,0, length); } catch (IOException e) { String format = "Error writing %d bytes to %s from buffer with size %d"; throw new ProxyCacheException(String.format(format, length,dataFile, data.length), e); } } |
5) 通知源数据已经获取成功,调用ProxyCache的read函数开始从磁盘当中读取数据并返回给播放器
private void responseWithCache(OutputStream out,long offset) throwsProxyCacheException, IOException { byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; int readBytes; while ((readBytes = read(buffer, offset, buffer.length)) != -1) { out.write(buffer, 0, readBytes); offset += readBytes; } out.flush(); } |