mirror of
https://github.com/zhigang1992/AndroidVideoCache.git
synced 2026-04-29 12:25:34 +08:00
@@ -12,7 +12,7 @@ repositories {
|
|||||||
maven { url 'https://dl.bintray.com/alexeydanilov/maven' }
|
maven { url 'https://dl.bintray.com/alexeydanilov/maven' }
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
compile 'com.danikula:videocache:2.1.4'
|
compile 'com.danikula:videocache:2.2.0'
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ private HttpProxyCacheServer getProxy() {
|
|||||||
```
|
```
|
||||||
|
|
||||||
To guarantee normal work you should use **single** instance of `HttpProxyCacheServer` for whole app.
|
To guarantee normal work you should use **single** instance of `HttpProxyCacheServer` for whole app.
|
||||||
For example you can store shared proxy on your `Application`:
|
For example you can store shared proxy in your `Application`:
|
||||||
|
|
||||||
```java
|
```java
|
||||||
public class App extends Application {
|
public class App extends Application {
|
||||||
@@ -59,6 +59,9 @@ More preferable way is use some dependency injector like [Dagger](http://square.
|
|||||||
See `sample` app for details.
|
See `sample` app for details.
|
||||||
|
|
||||||
## Whats new
|
## Whats new
|
||||||
|
### 2.2.0
|
||||||
|
- allow to [seek video](https://github.com/danikula/AndroidVideoCache/issues/21) in any position and [fix](https://github.com/danikula/AndroidVideoCache/issues/17) streaming while caching
|
||||||
|
|
||||||
### 2.1.4
|
### 2.1.4
|
||||||
- [fix](https://github.com/danikula/AndroidVideoCache/issues/18) available cache percents callback
|
- [fix](https://github.com/danikula/AndroidVideoCache/issues/18) available cache percents callback
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ publish {
|
|||||||
userOrg = 'alexeydanilov'
|
userOrg = 'alexeydanilov'
|
||||||
groupId = 'com.danikula'
|
groupId = 'com.danikula'
|
||||||
artifactId = 'videocache'
|
artifactId = 'videocache'
|
||||||
publishVersion = '2.1.4'
|
publishVersion = '2.2.0'
|
||||||
description = 'Cache support for android VideoView'
|
description = 'Cache support for android VideoView'
|
||||||
website = 'https://github.com/danikula/AndroidVideoCache'
|
website = 'https://github.com/danikula/AndroidVideoCache'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public class ByteArraySource implements Source {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int available() throws ProxyCacheException {
|
public int length() throws ProxyCacheException {
|
||||||
return data.length;
|
return data.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,9 +63,9 @@ class GetRequest {
|
|||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "GetRequest{" +
|
return "GetRequest{" +
|
||||||
"uri='" + uri + '\'' +
|
"rangeOffset=" + rangeOffset +
|
||||||
", rangeOffset=" + rangeOffset +
|
|
||||||
", partial=" + partial +
|
", partial=" + partial +
|
||||||
|
", uri='" + uri + '\'' +
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import java.io.IOException;
|
|||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
|
|
||||||
|
import static com.danikula.videocache.ProxyCacheUtils.DEFAULT_BUFFER_SIZE;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link ProxyCache} that read http url and writes data to {@link Socket}
|
* {@link ProxyCache} that read http url and writes data to {@link Socket}
|
||||||
*
|
*
|
||||||
@@ -14,6 +16,8 @@ import java.net.Socket;
|
|||||||
*/
|
*/
|
||||||
class HttpProxyCache extends ProxyCache {
|
class HttpProxyCache extends ProxyCache {
|
||||||
|
|
||||||
|
private static final float NO_CACHE_BARRIER = .2f;
|
||||||
|
|
||||||
private final HttpUrlSource source;
|
private final HttpUrlSource source;
|
||||||
private final FileCache cache;
|
private final FileCache cache;
|
||||||
private CacheListener listener;
|
private CacheListener listener;
|
||||||
@@ -30,27 +34,29 @@ class HttpProxyCache extends ProxyCache {
|
|||||||
|
|
||||||
public void processRequest(GetRequest request, Socket socket) throws IOException, ProxyCacheException {
|
public void processRequest(GetRequest request, Socket socket) throws IOException, ProxyCacheException {
|
||||||
OutputStream out = new BufferedOutputStream(socket.getOutputStream());
|
OutputStream out = new BufferedOutputStream(socket.getOutputStream());
|
||||||
byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE];
|
String responseHeaders = newResponseHeaders(request);
|
||||||
int readBytes;
|
out.write(responseHeaders.getBytes("UTF-8"));
|
||||||
boolean headersWrote = false;
|
|
||||||
long offset = request.rangeOffset;
|
long offset = request.rangeOffset;
|
||||||
while ((readBytes = read(buffer, offset, buffer.length)) != -1) {
|
if (isUseCache(request)) {
|
||||||
// tiny optimization: to prevent HEAD request in source for content-length. content-length 'll available after reading source
|
responseWithCache(out, offset);
|
||||||
if (!headersWrote) {
|
} else {
|
||||||
String responseHeaders = newResponseHeaders(request);
|
responseWithoutCache(out, offset);
|
||||||
out.write(responseHeaders.getBytes("UTF-8"));
|
|
||||||
headersWrote = true;
|
|
||||||
}
|
|
||||||
out.write(buffer, 0, readBytes);
|
|
||||||
offset += readBytes;
|
|
||||||
}
|
}
|
||||||
out.flush();
|
}
|
||||||
|
|
||||||
|
private boolean isUseCache(GetRequest request) throws ProxyCacheException {
|
||||||
|
int sourceLength = source.length();
|
||||||
|
boolean sourceLengthKnown = sourceLength > 0;
|
||||||
|
int cacheAvailable = cache.available();
|
||||||
|
// do not use cache for partial requests which too far from available cache. It seems user seek video.
|
||||||
|
return !sourceLengthKnown || !request.partial || request.rangeOffset <= cacheAvailable + sourceLength * NO_CACHE_BARRIER;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String newResponseHeaders(GetRequest request) throws IOException, ProxyCacheException {
|
private String newResponseHeaders(GetRequest request) throws IOException, ProxyCacheException {
|
||||||
String mime = source.getMime();
|
String mime = source.getMime();
|
||||||
boolean mimeKnown = !TextUtils.isEmpty(mime);
|
boolean mimeKnown = !TextUtils.isEmpty(mime);
|
||||||
int length = cache.isCompleted() ? cache.available() : source.available();
|
int length = cache.isCompleted() ? cache.available() : source.length();
|
||||||
boolean lengthKnown = length >= 0;
|
boolean lengthKnown = length >= 0;
|
||||||
long contentLength = request.partial ? length - request.rangeOffset : length;
|
long contentLength = request.partial ? length - request.rangeOffset : length;
|
||||||
boolean addRange = lengthKnown && request.partial;
|
boolean addRange = lengthKnown && request.partial;
|
||||||
@@ -64,6 +70,32 @@ class HttpProxyCache extends ProxyCache {
|
|||||||
.toString();
|
.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void responseWithoutCache(OutputStream out, long offset) throws ProxyCacheException, IOException {
|
||||||
|
try {
|
||||||
|
HttpUrlSource source = new HttpUrlSource(this.source);
|
||||||
|
source.open((int) offset);
|
||||||
|
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
|
||||||
|
int readBytes;
|
||||||
|
while ((readBytes = source.read(buffer)) != -1) {
|
||||||
|
out.write(buffer, 0, readBytes);
|
||||||
|
offset += readBytes;
|
||||||
|
}
|
||||||
|
out.flush();
|
||||||
|
} finally {
|
||||||
|
source.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCachePercentsAvailableChanged(int percents) {
|
protected void onCachePercentsAvailableChanged(int percents) {
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ public class HttpProxyCacheServer {
|
|||||||
|
|
||||||
private void makeSureServerWorks() {
|
private void makeSureServerWorks() {
|
||||||
int maxPingAttempts = 3;
|
int maxPingAttempts = 3;
|
||||||
int delay = 100;
|
int delay = 200;
|
||||||
int pingAttempts = 0;
|
int pingAttempts = 0;
|
||||||
while (pingAttempts < maxPingAttempts) {
|
while (pingAttempts < maxPingAttempts) {
|
||||||
try {
|
try {
|
||||||
@@ -92,13 +92,13 @@ public class HttpProxyCacheServer {
|
|||||||
SystemClock.sleep(delay);
|
SystemClock.sleep(delay);
|
||||||
delay *= 2;
|
delay *= 2;
|
||||||
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
} catch (InterruptedException | ExecutionException | TimeoutException e) {
|
||||||
Log.e(LOG_TAG, "Error pinging server. Shutdown it... If you see this message, please, email me danikula@gmail.com", e);
|
Log.e(LOG_TAG, "Error pinging server [attempt: " + pingAttempts + ", timeout: " + delay + "]. ", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pinged) {
|
Log.e(LOG_TAG, "Shutdown server… Error pinging server [attempt: " + pingAttempts + ", timeout: " + delay + "]. " +
|
||||||
shutdown();
|
"If you see this message, please, email me danikula@gmail.com");
|
||||||
}
|
shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean pingServer() throws ProxyCacheException {
|
private boolean pingServer() throws ProxyCacheException {
|
||||||
@@ -212,7 +212,7 @@ public class HttpProxyCacheServer {
|
|||||||
} catch (SocketException e) {
|
} catch (SocketException e) {
|
||||||
// There is no way to determine that client closed connection http://stackoverflow.com/a/10241044/999458
|
// There is no way to determine that client closed connection http://stackoverflow.com/a/10241044/999458
|
||||||
// So just to prevent log flooding don't log stacktrace
|
// So just to prevent log flooding don't log stacktrace
|
||||||
Log.d(LOG_TAG, "Client communication problem. It seems client closed connection");
|
Log.d(LOG_TAG, "Closing socket… Socket is closed by client.");
|
||||||
} catch (ProxyCacheException | IOException e) {
|
} catch (ProxyCacheException | IOException e) {
|
||||||
onError(new ProxyCacheException("Error processing request", e));
|
onError(new ProxyCacheException("Error processing request", e));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -262,7 +262,7 @@ public class HttpProxyCacheServer {
|
|||||||
} catch (SocketException e) {
|
} catch (SocketException e) {
|
||||||
// There is no way to determine that client closed connection http://stackoverflow.com/a/10241044/999458
|
// There is no way to determine that client closed connection http://stackoverflow.com/a/10241044/999458
|
||||||
// So just to prevent log flooding don't log stacktrace
|
// So just to prevent log flooding don't log stacktrace
|
||||||
Log.d(LOG_TAG, "Error closing client's input stream: it seems client closed connection");
|
Log.d(LOG_TAG, "Releasing input stream… Socket is closed by client.");
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
onError(new ProxyCacheException("Error closing socket input stream", e));
|
onError(new ProxyCacheException("Error closing socket input stream", e));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ public class HttpUrlSource implements Source {
|
|||||||
public final String url;
|
public final String url;
|
||||||
private HttpURLConnection connection;
|
private HttpURLConnection connection;
|
||||||
private InputStream inputStream;
|
private InputStream inputStream;
|
||||||
private volatile int available = Integer.MIN_VALUE;
|
private volatile int length = Integer.MIN_VALUE;
|
||||||
private volatile String mime;
|
private volatile String mime;
|
||||||
|
|
||||||
public HttpUrlSource(String url) {
|
public HttpUrlSource(String url) {
|
||||||
@@ -41,12 +41,18 @@ public class HttpUrlSource implements Source {
|
|||||||
this.mime = mime;
|
this.mime = mime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public HttpUrlSource(HttpUrlSource source) {
|
||||||
|
this.url = source.url;
|
||||||
|
this.mime = source.mime;
|
||||||
|
this.length = source.length;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public synchronized int available() throws ProxyCacheException {
|
public synchronized int length() throws ProxyCacheException {
|
||||||
if (available == Integer.MIN_VALUE) {
|
if (length == Integer.MIN_VALUE) {
|
||||||
fetchContentInfo();
|
fetchContentInfo();
|
||||||
}
|
}
|
||||||
return available;
|
return length;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -55,7 +61,7 @@ public class HttpUrlSource implements Source {
|
|||||||
connection = openConnection(offset, "GET", -1);
|
connection = openConnection(offset, "GET", -1);
|
||||||
mime = connection.getContentType();
|
mime = connection.getContentType();
|
||||||
inputStream = new BufferedInputStream(connection.getInputStream(), DEFAULT_BUFFER_SIZE);
|
inputStream = new BufferedInputStream(connection.getInputStream(), DEFAULT_BUFFER_SIZE);
|
||||||
available = readSourceAvailableBytes(connection, offset, connection.getResponseCode());
|
length = readSourceAvailableBytes(connection, offset, connection.getResponseCode());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new ProxyCacheException("Error opening connection for " + url + " with offset " + offset, e);
|
throw new ProxyCacheException("Error opening connection for " + url + " with offset " + offset, e);
|
||||||
}
|
}
|
||||||
@@ -64,7 +70,7 @@ public class HttpUrlSource implements Source {
|
|||||||
private int readSourceAvailableBytes(HttpURLConnection connection, int offset, int responseCode) throws IOException {
|
private int readSourceAvailableBytes(HttpURLConnection connection, int offset, int responseCode) throws IOException {
|
||||||
int contentLength = connection.getContentLength();
|
int contentLength = connection.getContentLength();
|
||||||
return responseCode == HTTP_OK ? contentLength
|
return responseCode == HTTP_OK ? contentLength
|
||||||
: responseCode == HTTP_PARTIAL ? contentLength + offset : available;
|
: responseCode == HTTP_PARTIAL ? contentLength + offset : length;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -94,10 +100,10 @@ public class HttpUrlSource implements Source {
|
|||||||
InputStream inputStream = null;
|
InputStream inputStream = null;
|
||||||
try {
|
try {
|
||||||
urlConnection = openConnection(0, "HEAD", 10000);
|
urlConnection = openConnection(0, "HEAD", 10000);
|
||||||
available = urlConnection.getContentLength();
|
length = urlConnection.getContentLength();
|
||||||
mime = urlConnection.getContentType();
|
mime = urlConnection.getContentType();
|
||||||
inputStream = urlConnection.getInputStream();
|
inputStream = urlConnection.getInputStream();
|
||||||
Log.i(LOG_TAG, "Content info for `" + url + "`: mime: " + mime + ", content-length: " + available);
|
Log.i(LOG_TAG, "Content info for `" + url + "`: mime: " + mime + ", content-length: " + length);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.e(LOG_TAG, "Error fetching info from " + url, e);
|
Log.e(LOG_TAG, "Error fetching info from " + url, e);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ class ProxyCache {
|
|||||||
try {
|
try {
|
||||||
offset = cache.available();
|
offset = cache.available();
|
||||||
source.open(offset);
|
source.open(offset);
|
||||||
sourceAvailable = source.available();
|
sourceAvailable = source.length();
|
||||||
byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE];
|
byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE];
|
||||||
int readBytes;
|
int readBytes;
|
||||||
while ((readBytes = source.read(buffer)) != -1) {
|
while ((readBytes = source.read(buffer)) != -1) {
|
||||||
@@ -144,7 +144,7 @@ class ProxyCache {
|
|||||||
|
|
||||||
private void tryComplete() throws ProxyCacheException {
|
private void tryComplete() throws ProxyCacheException {
|
||||||
synchronized (stopLock) {
|
synchronized (stopLock) {
|
||||||
if (!isStopped() && cache.available() == source.available()) {
|
if (!isStopped() && cache.available() == source.length()) {
|
||||||
cache.complete();
|
cache.complete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ public interface Source {
|
|||||||
void open(int offset) throws ProxyCacheException;
|
void open(int offset) throws ProxyCacheException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns available bytes or <b>negative value</b> if available bytes count is unknown.
|
* Returns length bytes or <b>negative value</b> if length is unknown.
|
||||||
*
|
*
|
||||||
* @return bytes available
|
* @return bytes length
|
||||||
* @throws ProxyCacheException if error occur while fetching source data.
|
* @throws ProxyCacheException if error occur while fetching source data.
|
||||||
*/
|
*/
|
||||||
int available() throws ProxyCacheException;
|
int length() throws ProxyCacheException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read data to byte buffer from source with current offset.
|
* Read data to byte buffer from source with current offset.
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ dependencies {
|
|||||||
// compile project(':library')
|
// compile project(':library')
|
||||||
compile 'com.android.support:support-v4:23.0.1'
|
compile 'com.android.support:support-v4:23.0.1'
|
||||||
compile 'org.androidannotations:androidannotations-api:3.3.2'
|
compile 'org.androidannotations:androidannotations-api:3.3.2'
|
||||||
compile 'com.danikula:videocache:2.1.4'
|
compile 'com.danikula:videocache:2.2.0'
|
||||||
compile 'com.viewpagerindicator:library:2.4.2-SNAPSHOT@aar'
|
compile 'com.viewpagerindicator:library:2.4.2-SNAPSHOT@aar'
|
||||||
apt 'org.androidannotations:androidannotations:3.3.2'
|
apt 'org.androidannotations:androidannotations:3.3.2'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ public class HttpProxyCacheServerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testProxyPartialResponse() throws Exception {
|
public void testProxyPartialResponse() throws Exception {
|
||||||
int offset = 42000;
|
int offset = 18000;
|
||||||
Pair<File, Response> response = readProxyData(HTTP_DATA_BIG_URL, offset);
|
Pair<File, Response> response = readProxyData(HTTP_DATA_BIG_URL, offset);
|
||||||
|
|
||||||
assertThat(response.second.code).isEqualTo(206);
|
assertThat(response.second.code).isEqualTo(206);
|
||||||
@@ -116,7 +116,7 @@ public class HttpProxyCacheServerTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testProxyPartialResponseWithRedirect() throws Exception {
|
public void testProxyPartialResponseWithRedirect() throws Exception {
|
||||||
int offset = 42000;
|
int offset = 18000;
|
||||||
Pair<File, Response> response = readProxyData(HTTP_DATA_BIG_URL_ONE_REDIRECT, offset);
|
Pair<File, Response> response = readProxyData(HTTP_DATA_BIG_URL_ONE_REDIRECT, offset);
|
||||||
|
|
||||||
assertThat(response.second.code).isEqualTo(206);
|
assertThat(response.second.code).isEqualTo(206);
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package com.danikula.videocache;
|
||||||
|
|
||||||
|
import com.danikula.videocache.support.ProxyCacheTestUtils;
|
||||||
|
import com.danikula.videocache.support.Response;
|
||||||
|
import com.danikula.videocache.test.BuildConfig;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
import org.robolectric.RobolectricGradleTestRunner;
|
||||||
|
import org.robolectric.annotation.Config;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.net.Socket;
|
||||||
|
|
||||||
|
import static com.danikula.videocache.support.ProxyCacheTestUtils.HTTP_DATA_URL;
|
||||||
|
import static com.danikula.videocache.support.ProxyCacheTestUtils.loadTestData;
|
||||||
|
import static org.fest.assertions.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Matchers.any;
|
||||||
|
import static org.mockito.Matchers.anyInt;
|
||||||
|
import static org.mockito.Matchers.anyLong;
|
||||||
|
import static org.mockito.Mockito.doThrow;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test {@link HttpProxyCache}.
|
||||||
|
*
|
||||||
|
* @author Alexey Danilov (danikula@gmail.com).
|
||||||
|
*/
|
||||||
|
@RunWith(RobolectricGradleTestRunner.class)
|
||||||
|
@Config(constants = BuildConfig.class, emulateSdk = BuildConfig.MIN_SDK_VERSION)
|
||||||
|
public class HttpProxyCacheTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessRequestNoCache() throws Exception {
|
||||||
|
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL);
|
||||||
|
FileCache cache = new FileCache(ProxyCacheTestUtils.newCacheFile());
|
||||||
|
HttpProxyCache proxyCache = new HttpProxyCache(source, cache);
|
||||||
|
GetRequest request = new GetRequest("GET /" + HTTP_DATA_URL + " HTTP/1.1");
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
Socket socket = mock(Socket.class);
|
||||||
|
when(socket.getOutputStream()).thenReturn(out);
|
||||||
|
|
||||||
|
proxyCache.processRequest(request, socket);
|
||||||
|
Response response = new Response(out.toByteArray());
|
||||||
|
|
||||||
|
assertThat(response.data).isEqualTo(loadTestData());
|
||||||
|
assertThat(response.code).isEqualTo(200);
|
||||||
|
assertThat(response.contentLength).isEqualTo(ProxyCacheTestUtils.HTTP_DATA_SIZE);
|
||||||
|
assertThat(response.contentType).isEqualTo("image/jpeg");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessPartialRequestWithoutCache() throws Exception {
|
||||||
|
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL);
|
||||||
|
FileCache fileCache = new FileCache(ProxyCacheTestUtils.newCacheFile());
|
||||||
|
FileCache spyFileCache = Mockito.spy(fileCache);
|
||||||
|
doThrow(new RuntimeException()).when(spyFileCache).read(any(byte[].class), anyLong(), anyInt());
|
||||||
|
HttpProxyCache proxyCache = new HttpProxyCache(source, spyFileCache);
|
||||||
|
GetRequest request = new GetRequest("GET /" + HTTP_DATA_URL + " HTTP/1.1\nRange: bytes=2000-");
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
Socket socket = mock(Socket.class);
|
||||||
|
when(socket.getOutputStream()).thenReturn(out);
|
||||||
|
|
||||||
|
proxyCache.processRequest(request, socket);
|
||||||
|
Response response = new Response(out.toByteArray());
|
||||||
|
|
||||||
|
byte[] fullData = loadTestData();
|
||||||
|
byte[] partialData = new byte[fullData.length - 2000];
|
||||||
|
System.arraycopy(fullData, 2000, partialData, 0, partialData.length);
|
||||||
|
assertThat(response.data).isEqualTo(partialData);
|
||||||
|
assertThat(response.code).isEqualTo(206);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,14 +63,14 @@ public class HttpUrlSourceTest {
|
|||||||
@Test
|
@Test
|
||||||
public void testFetchContentLength() throws Exception {
|
public void testFetchContentLength() throws Exception {
|
||||||
Source source = new HttpUrlSource(HTTP_DATA_URL);
|
Source source = new HttpUrlSource(HTTP_DATA_URL);
|
||||||
assertThat(source.available()).isEqualTo(loadAssetFile(ASSETS_DATA_NAME).length);
|
assertThat(source.length()).isEqualTo(loadAssetFile(ASSETS_DATA_NAME).length);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testFetchInfoWithRedirect() throws Exception {
|
public void testFetchInfoWithRedirect() throws Exception {
|
||||||
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL_ONE_REDIRECT);
|
HttpUrlSource source = new HttpUrlSource(HTTP_DATA_URL_ONE_REDIRECT);
|
||||||
source.open(0);
|
source.open(0);
|
||||||
int available = source.available();
|
int available = source.length();
|
||||||
String mime = source.getMime();
|
String mime = source.getMime();
|
||||||
source.close();
|
source.close();
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import com.danikula.videocache.Source;
|
|||||||
public class AngryHttpUrlSource implements Source {
|
public class AngryHttpUrlSource implements Source {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int available() throws ProxyCacheException {
|
public int length() throws ProxyCacheException {
|
||||||
throw new IllegalStateException();
|
throw new IllegalStateException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,10 @@ public class ProxyCacheTestUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static byte[] loadTestData() throws IOException {
|
||||||
|
return loadAssetFile(ASSETS_DATA_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
public static byte[] loadAssetFile(String name) throws IOException {
|
public static byte[] loadAssetFile(String name) throws IOException {
|
||||||
InputStream in = RuntimeEnvironment.application.getResources().getAssets().open(name);
|
InputStream in = RuntimeEnvironment.application.getResources().getAssets().open(name);
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
|||||||
@@ -1,14 +1,27 @@
|
|||||||
package com.danikula.videocache.support;
|
package com.danikula.videocache.support;
|
||||||
|
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import com.google.common.base.Preconditions;
|
||||||
import com.google.common.io.ByteStreams;
|
import com.google.common.io.ByteStreams;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.StringReader;
|
||||||
import java.net.HttpURLConnection;
|
import java.net.HttpURLConnection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class Response {
|
public class Response {
|
||||||
|
|
||||||
|
private static final String CONTENT_TYPE_HEADER = "Content-Type";
|
||||||
|
private static final String CONTENT_LENGTH_HEADER = "Content-Length";
|
||||||
|
private static final Pattern STATUS_CODE_PATTERN = Pattern.compile("HTTP/1.1 (\\d{3}) ");
|
||||||
|
|
||||||
public final int code;
|
public final int code;
|
||||||
public final byte[] data;
|
public final byte[] data;
|
||||||
public final int contentLength;
|
public final int contentLength;
|
||||||
@@ -22,4 +35,33 @@ public class Response {
|
|||||||
this.headers = connection.getHeaderFields();
|
this.headers = connection.getHeaderFields();
|
||||||
this.data = ByteStreams.toByteArray(connection.getInputStream());
|
this.data = ByteStreams.toByteArray(connection.getInputStream());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Response(byte[] responseData) throws IOException {
|
||||||
|
int read = 0;
|
||||||
|
BufferedReader reader = new BufferedReader(new StringReader(new String(responseData, "ascii")));
|
||||||
|
String statusLine = reader.readLine();
|
||||||
|
read += statusLine.length() + 1;
|
||||||
|
Matcher matcher = STATUS_CODE_PATTERN.matcher(statusLine);
|
||||||
|
boolean hasCode = matcher.find();
|
||||||
|
Preconditions.checkArgument(hasCode, "Status code not found in `" + statusLine + "`");
|
||||||
|
this.code = Integer.parseInt(matcher.group(1));
|
||||||
|
|
||||||
|
String header;
|
||||||
|
this.headers = new HashMap<>();
|
||||||
|
while (!TextUtils.isEmpty(header = reader.readLine())) {
|
||||||
|
read += header.length() + 1;
|
||||||
|
String[] keyValue = header.split(":");
|
||||||
|
String headerName = keyValue[0].trim();
|
||||||
|
String headerValue = keyValue[1].trim();
|
||||||
|
headers.put(headerName, Collections.singletonList(headerValue));
|
||||||
|
}
|
||||||
|
read++;
|
||||||
|
|
||||||
|
this.contentType = headers.containsKey(CONTENT_TYPE_HEADER) ? headers.get(CONTENT_TYPE_HEADER).get(0) : null;
|
||||||
|
this.contentLength = headers.containsKey(CONTENT_LENGTH_HEADER) ? Integer.parseInt(headers.get(CONTENT_LENGTH_HEADER).get(0)) : -1;
|
||||||
|
|
||||||
|
int bodySize = responseData.length - read;
|
||||||
|
this.data = new byte[bodySize];
|
||||||
|
System.arraycopy(responseData, read, data, 0, bodySize);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user