AndroidVideoCache優化
本工程 forked fromdanikula/AndroidVideoCache ,版本2.7.1
前言
因為專案需要,在原 ijkplayer 播放器的基礎上要加入快取功能,在調研了一番發現目前比較好的方案就是本地代理方案,其中danikula/AndroidVideoCache 最為出名。但是AndroidVideoCache上面掛了2k+的issues,並且上一次的更新更是在半年前了。所以為了結合專案實際以及目前已知的問題,針對danikula/AndroidVideoCache 做了些定製化優化。
原danikula/AndroidVideoCache README 看這裡
正題
下面會分幾點說下自己的定製優化之處。
1.視訊拖動超過已快取部分則停止快取執行緒下載
AndroidVideoCache會一直連線網路下載資料,直到把資料下載完全,並且拖動要超過當前已部分快取的大於當前視訊已快取大小加上視訊檔案的20%,才會走不快取分支,並且原來的快取下載不會立即停止。這樣就造成一個問題,當前使用者如果網路環境不是足夠好或者當前視訊檔案本身比較大時,拖動到沒有快取的地方需要比較久才會播放。針對這一點所以做了自己的優化。sourceLength * NO_CACHE_BARRIER
用一個較小的常量值代替,並且使用者拖動超過已快取部分則停止快取下載執行緒,使得頻寬可以用於從拖動點開始播放,更快地加載出使用者所需要的部分。 主要改動ProxyCache以及HttpProxyCache兩個檔案
//HttpProxyCache.java 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 (!isForceCancel && isUseCache(request)) { Log.i(TAG, "processRequest: responseWithCache"); pauseCache(false); responseWithCache(out, offset); } else { Log.i(TAG, "processRequest: responseWithoutCache"); pauseCache(true); responseWithoutCache(out, offset); } } /** * 是否強制取消快取 */ public void cancelCache() { isForceCancel = true; } 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. long offset = request.rangeOffset; //如果seek只是超出少許(這裡設定為2M)仍然走快取 return !sourceLengthKnown || !request.partial || offset <= cacheAvailable + MINI_OFFSET_CACHE; } ... private void responseWithCache(OutputStream out, long offset) throws ProxyCacheException { byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; int readBytes; try { while ((readBytes = read(buffer, offset, buffer.length)) != -1 && !stopped) { out.write(buffer, 0, readBytes); offset += readBytes; } out.flush(); } catch (IOException e) { e.printStackTrace(); } finally { try { out.close(); } catch (IOException e) { e.printStackTrace(); } } } 複製程式碼
這裡對==isUseCache #800023==方法進行了修改,在只超出快取一點點(這裡設定成2M)就會停止快取,避免線上播放以及快取下載兩個執行緒同時搶佔頻寬,造成跳轉後需要比較長時間才會載入播放成功。 ==responseWithCache #801e00==方法中對while加入stopped標記位判斷,當進入responseWithoutCache
分支時則會呼叫父類中的pauseCache(true);
方法,將父類中stopped標記為true,停止從代理快取中返回資料給播放器。具體可以檢視HttpProxyCache
和ProxyCache
兩個類。
2.脫離播放器實現快取(離線快取)
AndroidVideoCache是依賴於播放器的,所以針對這個侷限進行了修改。離線快取說白了就是提前下載,無論視訊是否下載完成,都可以將這提前下載好的部分作為視訊快取使用。這裡對於下載不在具體展開,下載功能如何實現自行尋找合適的庫。下面對只下載了部分的視訊如何加入到本地代理中進行說明(全部已經下載好的視訊就不需要經過本地代理了) 這裡假設已部分下載的視訊檔案字尾為.download ;
2.1 修改FileCache.java
新增一個可傳入本地具體路徑FileCache建構函式
//FileCache.java public FileCache(String downloadFilePath) throws ProxyCacheException{ try { this.diskUsage = new UnlimitedDiskUsage(); this.file = new File(downloadFilePath); this.dataFile = new RandomAccessFile(this.file, "rw"); } catch (IOException e) { throw new ProxyCacheException("Error using file " + file + " as disc cache", e); } } 複製程式碼
加入了一種快取檔案格式,則判斷是否快取完成需要做相應的修改
@Override public synchronized void complete() throws ProxyCacheException { if (isCompleted()) { return; } close(); String fileName; if (file.getName().endsWith(DOWNLOAD_TEMP_POSTFIX)) { //臨時下載檔案 fileName = file.getName().substring(0, file.getName().length() - DOWNLOAD_TEMP_POSTFIX.length()); } else { fileName = file.getName().substring(0, file.getName().length() - TEMP_POSTFIX.length()); } File completedFile = new File(file.getParentFile(), fileName); boolean renamed = file.renameTo(completedFile); if (!renamed) { throw new ProxyCacheException("Error renaming file " + file + " to " + completedFile + " for completion!"); } file = completedFile; try { dataFile = new RandomAccessFile(file, "r"); diskUsage.touch(file); } catch (IOException e) { throw new ProxyCacheException("Error opening " + file + " as disc cache", e); } } ... private boolean isTempFile(File file) { return file.getName().endsWith(TEMP_POSTFIX) || file.getName().endsWith(DOWNLOAD_TEMP_POSTFIX); } 複製程式碼
2.2 修改HttpProxyCacheServerClients
新增一個可傳入本地視訊檔案的HttpProxyCacheServerClients建構函式,大部分修改都有註釋,所以不再作額外解釋了。
private FileCache mCache; private String downloadPath=null; public HttpProxyCacheServerClients(String url, Config config) { this.url = checkNotNull(url); this.config = checkNotNull(config); this.uiCacheListener = new UiListenerHandler(url, listeners); } public void processRequest(GetRequest request, Socket socket) { try { startProcessRequest(); clientsCount.incrementAndGet(); proxyCache.processRequest(request, socket); } catch (Exception e) { e.printStackTrace(); if (e instanceof ProxyCacheException){ uiCacheListener.onCacheError(e); } } finally { finishProcessRequest(); } } ... private synchronized void startProcessRequest() throws ProxyCacheException { if (proxyCache == null){ if (downloadPath==null){ //原proxyCache proxyCache=newHttpProxyCache(); }else{ //本地已部分下載的視訊檔案作為快取 newHttpProxyCacheForDownloadFile(downloadPath); } } if (isCancelCache){ proxyCache.cancelCache(); } } ...... public void shutdown() { listeners.clear(); if (proxyCache != null) { proxyCache.registerCacheListener(null); proxyCache.shutdown(); proxyCache = null; } clientsCount.set(0); //清除不必要的快取 if (mCache != null && isCancelCache && downloadPath == null) { mCache.file.delete(); } } /** * 生成以已部分下載的視訊為基礎的快取檔案 * @param downloadFilePath * @return * @throws ProxyCacheException */ private void newHttpProxyCacheForDownloadFile(String downloadFilePath) throws ProxyCacheException { HttpUrlSource source = new HttpUrlSource(url, config.sourceInfoStorage, config.headerInjector); mCache = new FileCache(downloadFilePath); HttpProxyCache httpProxyCache = new HttpProxyCache(source, mCache); httpProxyCache.registerCacheListener(uiCacheListener); proxyCache = httpProxyCache; } 複製程式碼
對,就是這麼簡單,本地部分下載的視訊檔案就可以作為視訊的快取了,並且在播放視訊的時候,視訊可以繼續快取,將資料續寫到本地部分下載的視訊檔案。
3.高位元速率快取,低位元速率不快取
這個是我們的專案需要,對高清以上的高位元速率視訊才去快取,低位元速率視訊則直接線上播放。這部分需要藉助播放器本身的能力。這裡以IjkPlayer為例,在onPrepare方法中呼叫HttpProxyCacheServer暴露出來的cancelCache(mVideoUrl)
,其實是將HttpProxyCache中isForceCancel屬性置為true,在seekTo之後重新發起代理請求,這時isForceCancel=true,將不會走快取分支,而是線上播放。具體過程看原始碼。
public void onPrepared(IMediaPlayer mp) { ... if ( !isLocalVideo && bitrate < MINI_BITRATE_USE_CACHE && mCacheManager.getDownloadTempPath(mVideoUrl)==null) { bufferPoint = -1; mOnBufferUpdateListener.update(this, -1); mCacheManager.cancelCache(mVideoUrl); //注意:seekTo會重新發起請求本地代理,cancelCache後將不會走快取分支 if (lastWatchPosition==-1){ seekTo(1); }else { seekTo(lastWatchPosition); } } if (mPreparedListener != null) { mPreparedListener.onPrepared(this); } ... } 複製程式碼
4.其餘小修改
其餘部分修改不多,也不重要,就不細說了。值得一提的是清除了slf4j依賴,所有日誌部分均使用Andrdoid自帶的Log來輸入日誌。