-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
- Loading branch information
Showing
16 changed files
with
208 additions
and
46 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,13 +7,17 @@ | |
import java.io.OutputStream; | ||
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} | ||
* | ||
* @author Alexey Danilov ([email protected]). | ||
*/ | ||
class HttpProxyCache extends ProxyCache { | ||
|
||
private static final float NO_CACHE_BARRIER = .2f; | ||
|
||
private final HttpUrlSource source; | ||
private final FileCache cache; | ||
private CacheListener listener; | ||
|
@@ -30,27 +34,29 @@ public void registerCacheListener(CacheListener cacheListener) { | |
|
||
public void processRequest(GetRequest request, Socket socket) throws IOException, ProxyCacheException { | ||
OutputStream out = new BufferedOutputStream(socket.getOutputStream()); | ||
byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE]; | ||
int readBytes; | ||
boolean headersWrote = false; | ||
String responseHeaders = newResponseHeaders(request); | ||
out.write(responseHeaders.getBytes("UTF-8")); | ||
|
||
long offset = request.rangeOffset; | ||
while ((readBytes = read(buffer, offset, buffer.length)) != -1) { | ||
// tiny optimization: to prevent HEAD request in source for content-length. content-length 'll available after reading source | ||
if (!headersWrote) { | ||
String responseHeaders = newResponseHeaders(request); | ||
out.write(responseHeaders.getBytes("UTF-8")); | ||
headersWrote = true; | ||
} | ||
out.write(buffer, 0, readBytes); | ||
offset += readBytes; | ||
if (isUseCache(request)) { | ||
responseWithCache(out, offset); | ||
} else { | ||
responseWithoutCache(out, offset); | ||
} | ||
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 { | ||
String mime = source.getMime(); | ||
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; | ||
long contentLength = request.partial ? length - request.rangeOffset : length; | ||
boolean addRange = lengthKnown && request.partial; | ||
|
@@ -64,6 +70,32 @@ private String newResponseHeaders(GetRequest request) throws IOException, ProxyC | |
.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 | ||
protected void onCachePercentsAvailableChanged(int percents) { | ||
if (listener != null) { | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -79,7 +79,7 @@ public HttpProxyCacheServer(FileNameGenerator fileNameGenerator) { | |
|
||
private void makeSureServerWorks() { | ||
int maxPingAttempts = 3; | ||
int delay = 100; | ||
int delay = 200; | ||
int pingAttempts = 0; | ||
while (pingAttempts < maxPingAttempts) { | ||
try { | ||
|
@@ -92,13 +92,13 @@ private void makeSureServerWorks() { | |
SystemClock.sleep(delay); | ||
delay *= 2; | ||
} catch (InterruptedException | ExecutionException | TimeoutException e) { | ||
Log.e(LOG_TAG, "Error pinging server. Shutdown it... If you see this message, please, email me [email protected]", e); | ||
Log.e(LOG_TAG, "Error pinging server [attempt: " + pingAttempts + ", timeout: " + delay + "]. ", e); | ||
} | ||
} | ||
|
||
if (!pinged) { | ||
shutdown(); | ||
} | ||
Log.e(LOG_TAG, "Shutdown server… Error pinging server [attempt: " + pingAttempts + ", timeout: " + delay + "]. " + | ||
"If you see this message, please, email me [email protected]"); | ||
shutdown(); | ||
} | ||
|
||
private boolean pingServer() throws ProxyCacheException { | ||
|
@@ -212,7 +212,7 @@ private void processSocket(Socket socket) { | |
} catch (SocketException e) { | ||
// 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 | ||
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) { | ||
onError(new ProxyCacheException("Error processing request", e)); | ||
} finally { | ||
|
@@ -262,7 +262,7 @@ private void closeSocketInput(Socket socket) { | |
} catch (SocketException e) { | ||
// 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 | ||
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) { | ||
onError(new ProxyCacheException("Error closing socket input stream", e)); | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
75 changes: 75 additions & 0 deletions
75
test/src/test/java/com/danikula/videocache/HttpProxyCacheTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ([email protected]). | ||
*/ | ||
@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); | ||
} | ||
} |
Oops, something went wrong.