〇、背景
最近有做需求關於視頻緩存,瞭解到相關的開源庫AndroidVideoCache,一款市面上相對比較流行的視頻緩存框架,而我想利用該框架進行視頻緩存的處理,並且希望能夠支持預加載。然而該框架作者在18年就已經停止了維護,所以留下了無限的編程空間給其他程序員,對於視頻預加載,只搜到一篇《AndroidVideoCache源碼詳解以及改造系列-源碼篇》,然而點進該作者的博客列表,說好的預加載呢???後面也沒有了下文,搜遍全網好像沒有做AndroidVideoCache的預加載相關的事情,那麼這樣子的話……自己幹吧。
首先需要明白AndroidVideoCache的實現原理,推薦查看《AndroidVideoCache-視頻邊播放邊緩存的代理策略》這裏不再贅述。
其實預加載的思路很簡單,在進行一個播放視頻後,再返回接下來需要預加載的視頻url,啓用後臺線程去請求下載數據,不過中間涉及的細節邏輯比較多。
一、實現方案
主要邏輯爲:
1、後臺開啓一個線程去請求並預加載一部分的數據
2、可能需要預加載的數據大於>1,利用隊列先進入的先進行加載,加上前面的條件 使用HandlerThread再適合不過了。
我們首先定義好需要去處理的任務情況:
private void preload( String method,Call call) {
switch (method) {
case "addPreloadURL":
addPreloadURL(call); //添加url到預加載隊列
break;
case "cancelPreloadURLIfNeeded":
cancelPreloadURLIfNeeded(call); //取消對應的url預加載(因爲可能是立馬需要播放這個視頻,那麼就不需要預加載了)
break;
case "cancelAnyPreloads":
cancelAnyPreLoads();//取消所有的預加載,主要是方便管理任務
break;
default:
}
}
那麼對於每次的預加載邏輯基本上是這樣的方法執行順序:
cancelPreloadURLIfNeeded()->addPreloadURL(); //取消對應url加載的任務,因爲有可能該url不需要再進行預加載了(參考抖音,當用戶瞬間下滑幾個視頻,那麼很多視頻就需要跳過了不需要再進行預加載)
cancelAnyPreLoads()->addPreloadURL(); //取消對應url加載的任務(這時候需要立馬播放最新的視頻,那麼就應該讓出網速給該視頻),之後再添加新一輪的預加載url。
接下來具體的處理邏輯VideoPreLoader類,我直接放上所有的代碼邏輯吧,爲方便觀察刪除了一部分不太重要的邏輯,其實總體流程也比較簡單。
public class VideoPreLoader {
private Handler handler;
private HandlerThread handlerThread;
private List<String> cancelList = new ArrayList<>();
private VideoPreLoader() {
handlerThread = new HandlerThread("VideoPreLoaderThread");
handlerThread.start();
handler = new Handler(handlerThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
}
void addPreloadURL(final VideoPreLoadModel data) {
handler.post(new Runnable() {
@Override
public void run() {
realPreload(data);
}
});
}
void cancelPreloadURLIfNeeded(String url) {
cancelList.add(url);
}
void cancelAnyPreLoads() {
handler.removeCallbacksAndMessages(null);
cancelList.clear();
}
private void realPreload(VideoPreLoadModel data) {
if (data == null || isCancel(data.originalUrl)) {
return;
}
HttpURLConnection conn = null;
try {
URL myURL = new URL(data.proxyUrl);
conn = (HttpURLConnection) myURL.openConnection();
conn.connect();
InputStream is = conn.getInputStream();
byte[] buf = new byte[1024];
int downLoadedSize = 0;
do {
int numRead = is.read(buf);
downLoadedSize += numRead;
if (downLoadedSize >= data.preLoadBytes || numRead == -1) { //Reached preload range or end of Input stream.
break;
}
} while (true);
is.close();
}
....
}
private boolean isCancel(String url) {
if (TextUtils.isEmpty(url)) {
return true;
}
for (String cancelUrl : cancelList) {
if (cancelUrl.equals(url)) {
return true;
}
}
return false;
}
}
對於這段代碼中其實有“兩個”隊列,一個是HandlerThread中的隊列,熟悉消息機制的同學應該都能明白,內部是一個looper在不斷地循環獲取消息,當一個消息處理完畢之後纔會處理下一個消息。我還定義了一個就是取消隊列,因爲HandlerThread中的任務我們不太好控制取消具體的任務,所以設置了一個取消隊列,當之後的消息再需要執行的時候會首先判斷是否是在取消隊列裏面,這樣子就能做到對預加載隊列邏輯的控制。
二、關於一些細節問題
這樣子我們在播放一個視頻的時候,只需要傳給我們接下來將會播放的視頻的URL,我們就能對其預加載並緩存下來,但是會存在其他條件:
預加載的長度?
對於視頻加載長度,我們很容易想到在視頻url請求加入Range在header上面,比如
conn.addRequestProperty("Range", "0-102400");
我們只獲取前102400 bytes,不用將整個視頻全部進行預加載,我有進行這樣的嘗試,但是實際發現是有坑的。我做了很多嘗試,發現不論怎麼請求,拿到的 responseCode 雖然是206,但是 還是把數據給全部下載完了,這就有點不科學了!!
最終去源碼中才發現:源碼有對range做正則匹配
private static final Pattern RANGE_HEADER_PATTERN = Pattern.compile("[R,r]ange:[ ]?bytes=(\\d*)-");
private long findRangeOffset(String request) {
Matcher matcher = RANGE_HEADER_PATTERN.matcher(request);
if (matcher.find()) {
String rangeValue = matcher.group(1);
return Long.parseLong(rangeValue);
}
return -1;
}
看清楚了 "[R,r]ange:[ ]?bytes=(\d)-"* 它只去匹配了前面的的,也就是說 我傳入了 0-102400 它最終只當作是:Range:0- 來處理,導致addRequestProperty設置的range實現。坑!不過能理解作者爲什麼這麼做,後面總結會講到。沒有辦法只有使用最原始的方法進行判斷了:在每次獲取inputStream的時候進行判斷是否達到預加載的大小,雖然有一定的性能開銷,但是不去改源碼的話也沒有 辦法了。
do {
int numRead = is.read(buf);
downLoadedSize += numRead;
if (downLoadedSize >= data.preLoadBytes || numRead == -1) { //Reached preload range or end of Input stream.
break;
}
} while (true);
is.close();
三、總結
本文主要講了基於AndroidVideoCache的預加載具體實現原理,以及其中遇到的坑
1、預加載主要通過HandlerThread去實現後臺網絡的訪問以及緩存的處理邏輯
2、加入取消隊列去控制對應需要取消的任務
3、對於預加載的size只能通過讀取的時候進行判斷,沒有辦法使用range去判斷。其實很容易理解作者爲什麼正則要這樣寫,因爲它只是一個視頻緩存框架,主要是用來做“邊播邊存”,所以每次去進行請求的時候應該都是在原有的緩存之上去進行緩存數據處理,而緩存最終需要處理完的就是 content-size,不需要再去管Range中的結束範圍了。