🎉 VideoCache 1.0 released

This commit is contained in:
Alexey Danilov
2015-04-01 14:24:35 +03:00
commit 70baa564c0
37 changed files with 2042 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
package com.danikula.videocache;
import java.io.ByteArrayInputStream;
import java.util.Arrays;
import static com.danikula.videocache.Preconditions.checkArgument;
import static com.danikula.videocache.Preconditions.checkNotNull;
/**
* Simple memory based {@link Cache} implementation.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class ByteArrayCache implements Cache {
private volatile byte[] data;
private volatile boolean completed;
public ByteArrayCache() {
this(new byte[0]);
}
public ByteArrayCache(byte[] data) {
this.data = Preconditions.checkNotNull(data);
}
@Override
public int read(byte[] buffer, long offset, int length) throws ProxyCacheException {
if (offset >= data.length) {
return -1;
}
if (offset > Integer.MAX_VALUE) {
throw new IllegalArgumentException("Too long offset for memory cache " + offset);
}
return new ByteArrayInputStream(data).read(buffer, (int) offset, length);
}
@Override
public int available() throws ProxyCacheException {
return data.length;
}
@Override
public void append(byte[] newData, int length) throws ProxyCacheException {
Preconditions.checkNotNull(data);
Preconditions.checkArgument(length >= 0 && length <= newData.length);
byte[] appendedData = Arrays.copyOf(data, data.length + length);
System.arraycopy(newData, 0, appendedData, data.length, length);
data = appendedData;
}
@Override
public void close() throws ProxyCacheException {
}
@Override
public void complete() {
completed = true;
}
@Override
public boolean isCompleted() {
return completed;
}
}

View File

@@ -0,0 +1,39 @@
package com.danikula.videocache;
import java.io.ByteArrayInputStream;
/**
* Simple memory based {@link Source} implementation.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class ByteArraySource implements Source {
private final byte[] data;
private ByteArrayInputStream arrayInputStream;
public ByteArraySource(byte[] data) {
this.data = data;
}
@Override
public int read(byte[] buffer) throws ProxyCacheException {
return arrayInputStream.read(buffer, 0, buffer.length);
}
@Override
public int available() throws ProxyCacheException {
return data.length;
}
@Override
public void open(int offset) throws ProxyCacheException {
arrayInputStream = new ByteArrayInputStream(data);
arrayInputStream.skip(offset);
}
@Override
public void close() throws ProxyCacheException {
}
}

View File

@@ -0,0 +1,21 @@
package com.danikula.videocache;
/**
* Cache for proxy.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public interface Cache {
int available() throws ProxyCacheException;
int read(byte[] buffer, long offset, int length) throws ProxyCacheException;
void append(byte[] data, int length) throws ProxyCacheException;
void close() throws ProxyCacheException;
void complete() throws ProxyCacheException;
boolean isCompleted();
}

View File

@@ -0,0 +1,10 @@
package com.danikula.videocache;
/**
* @author Egor Makovsky (yahor.makouski@gmail.com).
*/
public interface CacheListener {
void onError(ProxyCacheException e);
void onCacheDataAvailable(int cachePercentage);
}

View File

@@ -0,0 +1,111 @@
package com.danikula.videocache;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import static com.danikula.videocache.Preconditions.checkNotNull;
/**
* {@link Cache} that uses file for storing data.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class FileCache implements Cache {
private static final String TEMP_POSTFIX = ".download";
private RandomAccessFile dataFile;
private File file;
public FileCache(File file) throws ProxyCacheException {
try {
Preconditions.checkNotNull(file);
boolean partialFile = isTempFile(file);
boolean completed = file.exists() && !partialFile;
if (completed) {
this.dataFile = new RandomAccessFile(file, "r");
this.file = file;
} else {
ProxyCacheUtils.createDirectory(file.getParentFile());
this.file = partialFile ? file : new File(file.getParentFile(), file.getName() + TEMP_POSTFIX);
this.dataFile = new RandomAccessFile(this.file, "rw");
}
} catch (IOException e) {
throw new ProxyCacheException("Error using file " + file + " as disc cache", e);
}
}
@Override
public synchronized int available() throws ProxyCacheException {
try {
return (int) dataFile.length();
} catch (IOException e) {
throw new ProxyCacheException("Error reading length of file " + dataFile, e);
}
}
@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);
}
}
@Override
public synchronized void append(byte[] data, int length) throws ProxyCacheException {
try {
if (isCompleted()) {
throw new ProxyCacheException("Error append cache: cache file " + file + " is completed!");
}
dataFile.seek(available());
dataFile.write(data, 0, length);
} catch (IOException e) {
String format = "Error writing %d bytes to %s from buffer with size %d";
throw new ProxyCacheException(String.format(format, length, dataFile, data.length), e);
}
}
@Override
public synchronized void close() throws ProxyCacheException {
try {
dataFile.close();
} catch (IOException e) {
throw new ProxyCacheException("Error closing file " + file, e);
}
}
@Override
public synchronized void complete() throws ProxyCacheException {
if (isCompleted()) {
return;
}
close();
String fileName = file.getName().substring(0, file.getName().length() - TEMP_POSTFIX.length());
File completedFile = new File(file.getParentFile(), fileName);
boolean renamed = file.renameTo(completedFile);
if (!renamed) {
throw new ProxyCacheException("Error renaming file " + file + " to " + completedFile + " for completion!");
}
file = completedFile;
try {
dataFile = new RandomAccessFile(file, "r");
} catch (IOException e) {
throw new ProxyCacheException("Error opening " + file + " as disc cache", e);
}
}
@Override
public synchronized boolean isCompleted() {
return !isTempFile(file);
}
private boolean isTempFile(File file) {
return file.getName().endsWith(TEMP_POSTFIX);
}
}

View File

@@ -0,0 +1,240 @@
package com.danikula.videocache;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Log;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* {@link ProxyCache} that uses local server to handle requests and cache data.
* Typical usage:
* <pre><code>
* HttpProxyCache proxy;
* public onCreate(Bundle state) {
* super.onCreate(state);
* ...
* try{
* HttpUrlSource source = new HttpUrlSource(YOUR_VIDEO_URI);
* Cache cache = new FileCache(new File(context.getCacheDir(), "video.mp4"));
* proxy = new HttpProxyCache(source, cache);
* videoView.setVideoPath(proxy.getUrl());
* } catch(ProxyCacheException e) {
* Log.e(LOG_TAG, "Error playing video", e);
* }
* }
* public onDestroy(){
* super.onDestroy();
*
* if (proxy != null) {
* proxy.shutdown();
* }
* }
* <code/></pre>
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class HttpProxyCache extends ProxyCache {
private static final int CLIENT_COUNT = 3;
private static final Pattern RANGE_HEADER_PATTERN = Pattern.compile("[R,r]ange:[ ]?bytes=(\\d*)-");
private static final String PROXY_HOST = "127.0.0.1";
private final HttpUrlSource httpUrlSource;
private final Cache cache;
private final ServerSocket serverSocket;
private final int port;
private final Thread waitConnectionThread;
private final ExecutorService executorService;
public HttpProxyCache(HttpUrlSource source, Cache cache, boolean logEnabled) throws ProxyCacheException {
super(source, cache, logEnabled);
this.httpUrlSource = source;
this.cache = cache;
this.executorService = Executors.newFixedThreadPool(CLIENT_COUNT);
try {
InetAddress inetAddress = InetAddress.getByName(PROXY_HOST);
this.serverSocket = new ServerSocket(0, CLIENT_COUNT, inetAddress);
this.port = serverSocket.getLocalPort();
CountDownLatch startSignal = new CountDownLatch(1);
this.waitConnectionThread = new Thread(new WaitRequestsRunnable(startSignal));
this.waitConnectionThread.start();
startSignal.await(); // freeze thread, wait for server starts
} catch (IOException | InterruptedException e) {
executorService.shutdown();
throw new ProxyCacheException("Error starting local server", e);
}
}
public HttpProxyCache(HttpUrlSource source, Cache cache) throws ProxyCacheException {
this(source, cache, false);
}
public String getUrl() {
return "http://" + PROXY_HOST + ":" + port + Uri.parse(httpUrlSource.url).getPath();
}
@Override
public void shutdown() {
super.shutdown();
Log.i(ProxyCacheUtils.LOG_TAG, "Shutdown proxy");
waitConnectionThread.interrupt();
try {
if (!serverSocket.isClosed()) {
serverSocket.close();
}
} catch (IOException e) {
onError(new ProxyCacheException("Error shutting down local server", e));
}
}
private void waitForRequest() {
try {
while (!Thread.currentThread().isInterrupted()) {
Socket socket = serverSocket.accept();
Log.d(ProxyCacheUtils.LOG_TAG, "Accept new socket " + socket);
processSocketInBackground(socket);
}
} catch (IOException e) {
onError(new ProxyCacheException("Error during waiting connection", e));
}
}
private void processSocketInBackground(final Socket socket) throws IOException {
executorService.submit(new Runnable() {
@Override
public void run() {
try {
processSocket(socket);
} catch (Throwable e) {
onError(e);
}
}
});
}
private void processSocket(Socket socket) {
try {
InputStream inputStream = socket.getInputStream();
String request = readRequest(inputStream);
Log.i(ProxyCacheUtils.LOG_TAG, "Request to cache proxy:\n" + request);
long rangeOffset = getRangeOffset(request);
writeResponse(socket, rangeOffset);
} catch (ProxyCacheException | IOException e) {
onError(new ProxyCacheException("Error processing request", e));
} finally {
releaseSocket(socket);
}
}
private void writeResponse(Socket socket, long rangeOffset) throws ProxyCacheException, IOException {
OutputStream out = new BufferedOutputStream(socket.getOutputStream());
byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE];
int readBytes;
long offset = Math.max(rangeOffset, 0);
boolean headersWrote = false;
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) {
writeResponseHeaders(out, rangeOffset);
headersWrote = true;
}
out.write(buffer, 0, readBytes);
if (isLogEnabled()) {
Log.d(ProxyCacheUtils.LOG_TAG, "Write data[" + readBytes + " bytes] to socket " + socket + " with offset " + offset + ": " + ProxyCacheUtils.preview(buffer, readBytes));
}
offset += readBytes;
}
out.flush();
}
private void writeResponseHeaders(OutputStream out, long rangeOffset) throws IOException, ProxyCacheException {
String responseHeaders = newResponseHeaders(rangeOffset);
out.write(responseHeaders.getBytes("UTF-8"));
Log.i(ProxyCacheUtils.LOG_TAG, "Response headers:\n" + responseHeaders);
}
private String newResponseHeaders(long offset) throws IOException, ProxyCacheException {
boolean partial = offset >= 0;
String mime = httpUrlSource.getMime();
boolean mimeKnown = !TextUtils.isEmpty(mime);
int length = cache.isCompleted() ? cache.available() : httpUrlSource.available();
boolean lengthKnown = length >= 0;
long contentLength = partial ? length - offset : length;
return new StringBuilder()
.append(partial ? "HTTP/1.1 206 PARTIAL CONTENT\n" : "HTTP/1.1 200 OK\n")
.append("Accept-Ranges: bytes\n")
.append(lengthKnown ? String.format("Content-Length: %d\n", contentLength) : "")
.append(lengthKnown && partial ? String.format("Content-Range: bytes %d-%d/%d\n", offset, length, length) : "")
.append(mimeKnown ? String.format("Content-Type: %s\n", mime) : "")
.append("\n") // headers end
.toString();
}
private String readRequest(InputStream inputStream) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
StringBuilder str = new StringBuilder();
String line;
while (!TextUtils.isEmpty(line = reader.readLine())) { // until new line (headers ending)
str.append(line).append('\n');
}
return str.toString();
}
private long getRangeOffset(String request) {
Matcher matcher = RANGE_HEADER_PATTERN.matcher(request);
if (matcher.find()) {
String rangeValue = matcher.group(1);
return Long.parseLong(rangeValue);
}
return -1;
}
private void releaseSocket(Socket socket) {
try {
socket.shutdownInput();
} catch (IOException e) {
onError(new ProxyCacheException("Error closing socket input stream", e));
}
try {
socket.shutdownOutput();
} catch (IOException e) {
onError(new ProxyCacheException("Error closing socket output stream", e));
}
try {
socket.close();
} catch (IOException e) {
onError(new ProxyCacheException("Error closing socket", e));
}
}
private final class WaitRequestsRunnable implements Runnable {
private final CountDownLatch startSignal;
public WaitRequestsRunnable(CountDownLatch startSignal) {
this.startSignal = startSignal;
}
@Override
public void run() {
startSignal.countDown();
waitForRequest();
}
}
}

View File

@@ -0,0 +1,112 @@
package com.danikula.videocache;
import android.text.TextUtils;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import static com.danikula.videocache.Preconditions.checkNotNull;
import static java.net.HttpURLConnection.HTTP_OK;
import static java.net.HttpURLConnection.HTTP_PARTIAL;
/**
* {@link Source} that uses http resource as source for {@link ProxyCache}.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class HttpUrlSource implements Source {
final String url;
private HttpURLConnection connection;
private InputStream inputStream;
private volatile int available = Integer.MIN_VALUE;
private volatile String mime;
public HttpUrlSource(String url) {
this(url, ProxyCacheUtils.getSupposablyMime(url));
}
public HttpUrlSource(String url, String mime) {
this.url = Preconditions.checkNotNull(url);
this.mime = mime;
}
@Override
public int available() throws ProxyCacheException {
if (available == Integer.MIN_VALUE) {
fetchContentInfo();
}
return available;
}
@Override
public void open(int offset) throws ProxyCacheException {
try {
Log.d(ProxyCacheUtils.LOG_TAG, "Open connection " + (offset > 0 ? " with offset " + offset : "") + " to " + url);
connection = (HttpURLConnection) new URL(url).openConnection();
if (offset > 0) {
connection.setRequestProperty("Range", "bytes=" + offset + "-");
}
mime = connection.getContentType();
inputStream = connection.getInputStream();
readSourceAvailableBytes(connection, offset);
} catch (IOException e) {
throw new ProxyCacheException("Error opening connection for " + url + " with offset " + offset, e);
}
}
private void readSourceAvailableBytes(HttpURLConnection connection, int offset) throws IOException {
int contentLength = connection.getContentLength();
int responseCode = connection.getResponseCode();
available = responseCode == HTTP_OK ? contentLength :
responseCode == HTTP_PARTIAL ? contentLength + offset :
available;
}
@Override
public void close() throws ProxyCacheException {
if (connection != null) {
connection.disconnect();
}
}
@Override
public int read(byte[] buffer) throws ProxyCacheException {
if (inputStream == null) {
throw new ProxyCacheException("Error reading data from " + url + ": connection is absent!");
}
try {
return inputStream.read(buffer, 0, buffer.length);
} catch (IOException e) {
throw new ProxyCacheException("Error reading data from " + url, e);
}
}
private void fetchContentInfo() throws ProxyCacheException {
Log.d(ProxyCacheUtils.LOG_TAG, "Read content info from " + url);
HttpURLConnection urlConnection = null;
try {
urlConnection = (HttpURLConnection) new URL(url).openConnection();
urlConnection.setRequestMethod("HEAD");
available = urlConnection.getContentLength();
mime = urlConnection.getContentType();
Log.d(ProxyCacheUtils.LOG_TAG, "Content-Length of " + url + " is " + available + " bytes, mime is " + mime);
} catch (IOException e) {
throw new ProxyCacheException("Error fetching Content-Length from " + url);
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
}
}
public String getMime() throws ProxyCacheException {
if (TextUtils.isEmpty(mime)) {
fetchContentInfo();
}
return mime;
}
}

View File

@@ -0,0 +1,30 @@
package com.danikula.videocache;
final class Preconditions {
static <T> T checkNotNull(T reference) {
if (reference == null) {
throw new NullPointerException();
}
return reference;
}
static <T> T checkNotNull(T reference, String errorMessage) {
if (reference == null) {
throw new NullPointerException(errorMessage);
}
return reference;
}
static void checkArgument(boolean expression) {
if (!expression) {
throw new IllegalArgumentException();
}
}
static void checkArgument(boolean expression, String errorMessage) {
if (!expression) {
throw new IllegalArgumentException(errorMessage);
}
}
}

View File

@@ -0,0 +1,202 @@
package com.danikula.videocache;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import java.util.concurrent.atomic.AtomicInteger;
import static com.danikula.videocache.Preconditions.checkNotNull;
import static com.danikula.videocache.ProxyCacheUtils.LOG_TAG;
/**
* Proxy for {@link Source} with caching support ({@link Cache}).
* <p/>
* Can be used only for sources with persistent data (that doesn't change with time).
* Method {@link #read(byte[], long, int)} will be blocked while fetching data from source.
* Useful for streaming something with caching e.g. streaming video/audio etc.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public class ProxyCache {
private static final int MAX_READ_SOURCE_ATTEMPTS = 1;
private final Source source;
private final Cache cache;
private final Object wc;
private final Handler handler;
private volatile Thread sourceReaderThread;
private volatile boolean stopped;
private final AtomicInteger readSourceErrorsCount;
private CacheListener cacheListener;
private final boolean logEnabled;
public ProxyCache(Source source, Cache cache, boolean logEnabled) {
this.source = checkNotNull(source);
this.cache = checkNotNull(cache);
this.logEnabled = logEnabled;
this.wc = new Object();
this.handler = new Handler(Looper.getMainLooper());
this.readSourceErrorsCount = new AtomicInteger();
}
public ProxyCache(Source source, Cache cache) {
this(source, cache, false);
}
public void setCacheListener(CacheListener cacheListener) {
this.cacheListener = cacheListener;
}
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();
checkIsCacheValid();
checkReadSourceErrorsCount();
}
int read = cache.read(buffer, offset, length);
if (isLogEnabled()) {
Log.d(LOG_TAG, "Read data[" + read + " bytes] from cache with offset " + offset + ": " + ProxyCacheUtils.preview(buffer, read));
}
return read;
}
private void checkIsCacheValid() throws ProxyCacheException {
int sourceAvailable = source.available();
if (sourceAvailable > 0 && cache.available() > sourceAvailable) {
throw new ProxyCacheException("Unexpected cache: cache [" + cache.available() + " bytes] > source[" + sourceAvailable + " bytes]");
}
}
private void checkReadSourceErrorsCount() throws ProxyCacheException {
int errorsCount = readSourceErrorsCount.get();
if (errorsCount >= MAX_READ_SOURCE_ATTEMPTS) {
readSourceErrorsCount.set(0);
throw new ProxyCacheException("Error reading source " + errorsCount + " times");
}
}
public void shutdown() {
try {
stopped = true;
if (sourceReaderThread != null) {
sourceReaderThread.interrupt();
}
cache.close();
} catch (ProxyCacheException e) {
onError(e);
}
}
private 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 ProxyCache");
sourceReaderThread.start();
}
}
private void waitForSourceData() throws ProxyCacheException {
synchronized (wc) {
try {
wc.wait(1000);
} catch (InterruptedException e) {
throw new ProxyCacheException("Waiting source data is interrupted!", e);
}
}
}
private void notifyNewCacheDataAvailable(final int cachePercentage) {
handler.post(new Runnable() {
@Override
public void run() {
if (cacheListener != null) {
cacheListener.onCacheDataAvailable(cachePercentage);
}
}
});
synchronized (wc) {
wc.notifyAll();
}
}
private void readSource() {
int cachePercentage = 0;
try {
int offset = cache.available();
source.open(offset);
byte[] buffer = new byte[ProxyCacheUtils.DEFAULT_BUFFER_SIZE];
int readBytes;
while ((readBytes = source.read(buffer)) != -1 && !Thread.currentThread().isInterrupted() && !stopped) {
if (isLogEnabled()) {
Log.d(LOG_TAG, "Write data[" + readBytes + " bytes] to cache from source with offset " + offset + ": " + ProxyCacheUtils.preview(buffer, readBytes));
}
cache.append(buffer, readBytes);
offset += readBytes;
cachePercentage = offset * 100 / source.available();
notifyNewCacheDataAvailable(cachePercentage);
}
if (cache.available() == source.available()) {
cache.complete();
}
} catch (Throwable e) {
readSourceErrorsCount.incrementAndGet();
onError(e);
} finally {
closeSource();
notifyNewCacheDataAvailable(cachePercentage);
}
}
private void closeSource() {
try {
source.close();
} catch (ProxyCacheException e) {
onError(new ProxyCacheException("Error closing source " + source, e));
}
}
protected final void onError(final Throwable e) {
Log.e(LOG_TAG, "ProxyCache error", e);
handler.post(new ErrorDeliverer(e));
}
protected boolean isLogEnabled() {
return logEnabled;
}
private class SourceReaderRunnable implements Runnable {
@Override
public void run() {
readSource();
}
}
private class ErrorDeliverer implements Runnable {
private final Throwable error;
public ErrorDeliverer(Throwable error) {
this.error = error;
}
@Override
public void run() {
if (error instanceof ProxyCacheException) {
if (cacheListener != null) {
cacheListener.onError((ProxyCacheException) error);
}
} else {
throw new RuntimeException("Unexpected error!", error);
}
}
}
}

View File

@@ -0,0 +1,21 @@
package com.danikula.videocache;
/**
* Indicates any error in work of {@link ProxyCache}.
*
* @author Alexey Danilov
*/
public class ProxyCacheException extends Exception {
public ProxyCacheException(String message) {
super(message);
}
public ProxyCacheException(String message, Throwable cause) {
super(message, cause);
}
public ProxyCacheException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,60 @@
package com.danikula.videocache;
import android.text.TextUtils;
import android.webkit.MimeTypeMap;
import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import static com.danikula.videocache.Preconditions.checkArgument;
import static com.danikula.videocache.Preconditions.checkNotNull;
/**
* Just simple utils.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
class ProxyCacheUtils {
static final String LOG_TAG = "ProxyCache";
static final int DEFAULT_BUFFER_SIZE = 8 * 1024;
static final int MAX_ARRAY_PREVIEW = 16;
static String getSupposablyMime(String url) {
MimeTypeMap mimes = MimeTypeMap.getSingleton();
String extension = MimeTypeMap.getFileExtensionFromUrl(url);
return TextUtils.isEmpty(extension) ? null : mimes.getMimeTypeFromExtension(extension);
}
static void assertBuffer(byte[] buffer, long offset, int length) {
checkNotNull(buffer, "Buffer must be not null!");
checkArgument(offset >= 0, "Data offset must be positive!");
checkArgument(length >= 0 && length <= buffer.length, "Length must be in range [0..buffer.length]");
}
static String preview(byte[] data, int length) {
int previewLength = Math.min(MAX_ARRAY_PREVIEW, Math.max(length, 0));
byte[] dataRange = Arrays.copyOfRange(data, 0, previewLength);
String preview = Arrays.toString(dataRange);
if (previewLength < length) {
preview = preview.substring(0, preview.length() - 1) + ", ...]";
}
return preview;
}
static void createDirectory(File directory) throws IOException {
checkNotNull(directory, "File must be not null!");
if (directory.exists()) {
checkArgument(directory.isDirectory(), "File is not directory!");
} else {
boolean isCreated = directory.mkdirs();
if (!isCreated) {
String error = String.format("Directory %s can't be created", directory.getAbsolutePath());
throw new IOException(error);
}
}
}
}

View File

@@ -0,0 +1,17 @@
package com.danikula.videocache;
/**
* Source for proxy.
*
* @author Alexey Danilov (danikula@gmail.com).
*/
public interface Source {
int available() throws ProxyCacheException;
void open(int offset) throws ProxyCacheException;
void close() throws ProxyCacheException;
int read(byte[] buffer) throws ProxyCacheException;
}