一些知名的視頻app客戶端(優酷,愛奇藝)播放視頻的時候都有一些緩存進度(二級進度緩存),還有一些短視頻app,都有邊播邊緩的處理。還有就是當文件緩存完畢了再次播放的話就不再請求網絡了直接播放本地文件了。既節省了流程又提高了加載速度。
今天我們就是來研究討論實現這個邊播邊緩存的框架,因為它不和任何的業務邏輯耦合。
開源的項目
目前比較好的開源項目是:https://github.com/danikula/AndroidVideoCache
代碼的架構寫的也很不錯,網絡用的httpurlconnect,文件緩存處理,文件最大限度策略,回調監聽處理,斷點續傳,代理服務等。很值得研究閱讀.
個人覺得項目中有幾個需要優化的點,今天就來處理這幾個并簡要分析下原理
優化點比如:
文件的緩存超過限制后沒有按照lru算法刪除,
處理返回給播放器的http響應頭消息,響應頭消息的獲取處理改為head請求(需服務器支持)
替換網絡庫為okhttp(因為大部分的項目都是以okhttp為網絡請求庫的)
該開源項目的原理分析-本地代理

采用了本地代理服務的方式,通過原始url給播放器返回一個本地代理的一個url ,代理URL類似::57430/xxxx;然后播放器播放的時候請求到了你本地的代理上了。
本地代理采用ServerSocket監聽127.0.0.1的有效端口,這個時候手機就是一個服務器了,客戶端就是socket,也就是播放器。
讀取客戶端就是socket來讀取數據(http協議請求)解析http協議。
根據url檢查視頻文件是否存在,讀取文件數據給播放器,也就是往socket里寫入數據。同時如果沒有下載完成會進行斷點下載,當然弱網的話數據需要生產消費同步處理。
優化點
1. 文件的緩存超過限制后沒有按照lru算法刪除.
Files類。
由于在移動設備上file.setLastModified() 方法不支持毫秒級的時間處理,導致超出限制大小后本應該刪除老的,卻沒有刪除拋出了異常。注釋掉主動拋出的異常即可。因為文件的修改時間就是對的。
static void setLastModifiedNow(File file) throws IOException { if (file.exists()) { long now = System.currentTimeMillis(); boolean modified = file.setLastModified(now/1000*1000); // on some devices (e.g. Nexus 5) doesn't work if (!modified) { modify(file); // if (file.lastModified() < now) { // VideoCacheLog.debug("LruDiskUsage", "modified not ok "); // throw new IOException("Error set last modified date to " + file); // }else{ // VideoCacheLog.debug("LruDiskUsage", "modified ok "); // } } } }
2. 處理返回給播放器的http響應頭消息,響應頭消息的獲取處理改為head請求(需要服務器支持)
HttpUrlSource類。fetchContentInfo方法是獲取視頻文件的Content-Type,Content-Length信息,是為了播放器播放的時候給播放器組裝http響應頭信息用的。所以這一塊需要用數據庫保存,這樣播放器每次播放的時候不要在此獲取了,減少了請求的次數,節省了流量。既然是只需要頭信息,不需要響應體,所以我們在獲取的時候可以直接采用HEAD方法。所以代碼增加了一個方法openConnectionForHeader如下:
private void fetchContentInfo() throws ProxyCacheException { VideoCacheLog.debug(TAG,"Read content info from " + sourceInfo.url); HttpURLConnection urlConnection = null; InputStream inputStream = null; try { urlConnection = openConnectionForHeader(20000); long length = getContentLength(urlConnection); String mime = urlConnection.getContentType(); inputStream = urlConnection.getInputStream(); this.sourceInfo = new SourceInfo(sourceInfo.url, length, mime); this.sourceInfoStorage.put(sourceInfo.url, sourceInfo); VideoCacheLog.debug(TAG,"Source info fetched: " + sourceInfo); } catch (IOException e) { VideoCacheLog.error(TAG,"Error fetching info from " + sourceInfo.url ,e); } finally { ProxyCacheUtils.close(inputStream); if (urlConnection != null) { urlConnection.disconnect(); } } } // for HEAD private HttpURLConnection openConnectionForHeader(int timeout) throws IOException, ProxyCacheException { HttpURLConnection connection; boolean redirected; int redirectCount = 0; String url = this.sourceInfo.url; do { VideoCacheLog.debug(TAG, "Open connection for header to " + url); connection = (HttpURLConnection) new URL(url).openConnection(); if (timeout > 0) { connection.setConnectTimeout(timeout); connection.setReadTimeout(timeout); } //只返回頭部,不需要BODY,既可以提高響應速度也可以減少網絡流量 connection.setRequestMethod("HEAD"); int code = connection.getResponseCode(); redirected = code == HTTP_MOVED_PERM || code == HTTP_MOVED_TEMP || code == HTTP_SEE_OTHER; if (redirected) { url = connection.getHeaderField("Location"); VideoCacheLog.debug(TAG,"Redirect to:" + url); redirectCount++; connection.disconnect(); VideoCacheLog.debug(TAG,"Redirect closed:" + url); } if (redirectCount > MAX_REDIRECTS) { throw new ProxyCacheException("Too many redirects: " + redirectCount); } } while (redirected); return connection; }
3.替換網絡庫為okhttp(因為大部分的項目都是以okhttp為網絡請求庫的)