一、概述
AndroidVideoCache是一個視頻緩存框架,支持邊下載邊播放。
基本原理:使用本地代理代替直接根據url請求網絡服務。
1.首先在本地新建一個服務(ServerSocket),監聽客戶端的接入,一旦有客戶端接入就新建一個Socket來維持客戶端和服務端之間的通訊。
2.轉換url,在客戶端請求的時候使用proxy.getProxyUrl(url)把網絡url轉為本地url,調用這個url的時候會和本地的代理服務ServerSocket連接(也就是第一步)
3.根據第二步,連接成功后創建一個HttpProxyCacheServerClients客戶端,並執行request方法。
4.創建HttpProxyCache類,並執行其processRequest方法回復數據。
5.判斷文件是否下載完成,如果沒有下載完成,則單獨開啟一個線程進行異步下載(readSourceAsync)下載的數據會存入緩存。之后從緩存中讀取數據(cache.read),也就是是播放器只讀緩存數據

二、代碼示例分析
入口是HttpProxyCacheServer.java類。
1.在Application中初始化HttpProxyCacheServer類,並建立本地代理服務
public static HttpProxyCacheServer getProxy(Context context) { App app = (App) context.getApplicationContext(); return app.proxy == null ? (app.proxy = app.newProxy()) : app.proxy; } private HttpProxyCacheServer newProxy() { return new HttpProxyCacheServer.Builder(this) .cacheDirectory(Utils.getVideoCacheDir(this)) .build(); }
在HttpProxyCacheServer的構造方法中會做一系列的初始化動作及新建一個代理服務,代碼如下
private HttpProxyCacheServer(Config config) { this.config = checkNotNull(config); try { InetAddress inetAddress = InetAddress.getByName(PROXY_HOST); /**ServerSocket(port,backlog,inetAddress)參數說明: * port:第一個參數為端口號,如果填寫0,則系統自動會分配一個端口號 * backlog:第二個參數:最多同時可支持多少個鏈接 * inetAddress:主機地址 * */ this.serverSocket = new ServerSocket(0, 8, inetAddress); //獲取隨機分配的端口號 this.port = serverSocket.getLocalPort(); IgnoreHostProxySelector.install(PROXY_HOST, port); /**信號量:其目的是為了讓waitConnectionThread的run方法先執行*/ CountDownLatch startSignal = new CountDownLatch(1); /**創建並開啟一個一直等待客戶端連接的線程*/ this.waitConnectionThread = new Thread(new WaitRequestsRunnable(startSignal)); this.waitConnectionThread.start(); startSignal.await(); // freeze thread, wait for server starts //創建一個ping實例 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); } }
其中上述代碼:startSignal信號量是用來確保WaitRequestRunnable類的run方法先執行,WaitRequestsRunnable線程使用來開啟代理服務,並等待接受客戶端的鏈接
/**
* 一直等待客戶端連接的線程
*/
private final class WaitRequestsRunnable implements Runnable {
private final CountDownLatch startSignal;
public WaitRequestsRunnable(CountDownLatch startSignal) {
this.startSignal = startSignal;
}
@Override
public void run() {
startSignal.countDown();//確保run方法先執行
waitForRequest();
}
}
通過waitForRequest()等待客戶端的接入,並將創建好的客戶端Socket放入socketProcessor線程池中執行
/**
* 死循環通過serverSocket.accept()一直阻塞等待客戶端連接
*/
private void waitForRequest() {
try {
while (!Thread.currentThread().isInterrupted()) {
//一直等待有客戶端連接進來
Socket socket = serverSocket.accept();
LOG.info("Accept new socket " + socket);
//連接進來之后立馬提交給線程池執行
socketProcessor.submit(new SocketProcessorRunnable(socket));
}
} catch (IOException e) {
onError(new ProxyCacheException("Error during waiting connection", e));
}
}
到這里初始化這塊已經結束了。首先ServerSocket服務建立好了,其次接受客戶端連接的線程也創建好了,最后接收客戶端Socket后會放入線程池中執行。
2.接下來就是客戶端請求服務端,代碼如下:
private void startVideo() { HttpProxyCacheServer proxy = App.getProxy(getActivity()); proxy.registerCacheListener(this, url); //將url轉換為代理url String proxyUrl = proxy.getProxyUrl(url); Log.d(LOG_TAG, "Use proxy url " + proxyUrl + " instead of original url " + url); videoView.setVideoPath(proxyUrl); videoView.start(); }
在上述代碼中最主要的是proxy.getProxyUrl(url)方法,此方法是把網絡url編碼為本地的url,然后videoView.setVideoPath()設置本地請求路徑,之后通過videoView.start()方法通過本地路徑請求服務,此請求會到本地代理服務ServerSocket中。即會放到SocketProcessorRunnable類中執行。
請求到這里算是結束了,接下來會看一下請求后的操作(這塊是最主要的)
3.客戶端和服務端連通成功后的操作。其實這里會分成兩塊,一塊是播放器和本地代理服務,另一塊是本地代理服務和真正的雲端服務。
調用客戶端HttpProxyCacheServerClients的processRequest方法創建一個HttpProxyCache類,利用該類的processRequest方法發起(本地)網絡請求
public void processRequest(GetRequest request, Socket socket) throws ProxyCacheException, IOException {
startProcessRequest();
try {
clientsCount.incrementAndGet();
proxyCache.processRequest(request, socket);
} finally {
finishProcessRequest();
}
}
封裝響應頭並給客戶端回復消息
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); } }
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方法。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);
if (cache.isCompleted() && percentsAvailable != 100) {
percentsAvailable = 100;
onCachePercentsAvailableChanged(100);
}
return read;
}
此方法做了兩件主要的事情:1.檢測文件是否已經下載完成(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();
}
}
在SourceReaderRunnable中會調用run方法執行readSource方法
private void readSource() {
long sourceAvailable = -1;
long offset = 0;
try {
offset = cache.available();
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);
}
}
此方法的作用是接着上次沒下載完的文件繼續下載,下載后寫入緩存文件,並實時通知客戶端下載進度
read方法做的第二件事情就是從緩存中讀取數據給客戶端
@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);
}
}
總結:到此AndroidVideoCache的源代碼已經分析完畢了。其最主要的流程便是:代理服務器連接真是的網絡url並把文件下載到本地cache,客戶端連接本地代理服務器,並從本地cache中拿到數據並播放。
