Skip to content

Instantly share code, notes, and snippets.

@eygraber
Last active September 20, 2016 10:19
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eygraber/8e935e19eedc70d2d8e3 to your computer and use it in GitHub Desktop.
Save eygraber/8e935e19eedc70d2d8e3 to your computer and use it in GitHub Desktop.
Copied from DefaultHttpDataSource, but uses OkHttpClient instead of URLConnection
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;
import com.google.android.exoplayer.C;
import com.google.android.exoplayer.upstream.DataSpec;
import com.google.android.exoplayer.upstream.HttpDataSource;
import com.google.android.exoplayer.upstream.TransferListener;
import com.google.android.exoplayer.util.Assertions;
import okhttp3.Headers;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import java.io.EOFException;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class OkHttpDataSource implements HttpDataSource {
private static final String TAG = "OkHttpDataSource";
private static final Pattern CONTENT_RANGE_HEADER = Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$");
private static final AtomicReference<byte[]> skipBufferReference = new AtomicReference<>();
private final OkHttpClient okHttpClient;
private final HashMap<String, String> requestProperties;
private final TransferListener listener;
private DataSpec dataSpec;
private Request request;
private Response response;
private boolean opened;
private long bytesToSkip;
private long bytesToRead;
private long bytesSkipped;
private long bytesRead;
public OkHttpDataSource(OkHttpClient okHttpClient, @Nullable TransferListener listener) {
this.okHttpClient = okHttpClient;
this.requestProperties = new HashMap<>();
this.listener = listener;
}
@Override
public String getUri() {
return request == null ? null : request.url().toString();
}
@Override
public Map<String, List<String>> getResponseHeaders() {
return request == null ? null : request.headers().toMultimap();
}
@Override
public void setRequestProperty(String name, String value) {
Assertions.checkNotNull(name);
Assertions.checkNotNull(value);
synchronized (requestProperties) {
requestProperties.put(name, value);
}
}
@Override
public void clearRequestProperty(String name) {
Assertions.checkNotNull(name);
synchronized (requestProperties) {
requestProperties.remove(name);
}
}
@Override
public void clearAllRequestProperties() {
synchronized (requestProperties) {
requestProperties.clear();
}
}
@Override
public long open(DataSpec dataSpec) throws HttpDataSourceException {
this.dataSpec = dataSpec;
this.bytesRead = 0;
this.bytesSkipped = 0;
try {
response = makeConnection(dataSpec);
}
catch (IOException e) {
throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e, dataSpec);
}
// Check for a valid response code.
int responseCode = response.code();
if (responseCode < 200 || responseCode > 299) {
Map<String, List<String>> headers = response.headers().toMultimap();
closeQuietly();
throw new InvalidResponseCodeException(responseCode, headers, dataSpec);
}
// If we requested a range starting from a non-zero position and received a 200 rather than a
// 206, then the server does not support partial requests. We'll need to manually skip to the
// requested position.
bytesToSkip = responseCode == 200 && dataSpec.position != 0 ? dataSpec.position : 0;
// Determine the length of the data to be read, after skipping.
if ((dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) == 0) {
long contentLength = getContentLength(response);
bytesToRead = dataSpec.length != C.LENGTH_UNBOUNDED ? dataSpec.length
: contentLength != C.LENGTH_UNBOUNDED ? contentLength - bytesToSkip
: C.LENGTH_UNBOUNDED;
}
else {
// Gzip is enabled. If the server opts to use gzip then the content length in the response
// will be that of the compressed data, which isn't what we want. Furthermore, there isn't a
// reliable way to determine whether the gzip was used or not. Always use the dataSpec length
// in this case.
bytesToRead = dataSpec.length;
}
opened = true;
if (listener != null) {
listener.onTransferStart();
}
return bytesToRead;
}
@Override
public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException {
try {
skipInternal();
return readInternal(buffer, offset, readLength);
}
catch (IOException e) {
throw new HttpDataSourceException(e, dataSpec);
}
}
@Override
public void close() throws HttpDataSourceException {
try {
if (response != null) {
ResponseBody responseBody = response.body();
if(responseBody != null) {
responseBody.close();
}
}
}
finally {
response = null;
request = null;
if (opened) {
opened = false;
if (listener != null) {
listener.onTransferEnd();
}
}
}
}
/**
* Establishes a connection, following redirects to do so where permitted.
*/
private Response makeConnection(DataSpec dataSpec) throws IOException {
URL url = new URL(dataSpec.uri.toString());
byte[] postBody = dataSpec.postBody;
long position = dataSpec.position;
long length = dataSpec.length;
boolean allowGzip = (dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) != 0;
Map<String, String> headersMap;
synchronized (requestProperties) {
headersMap = new HashMap<>(requestProperties);
}
if (!(position == 0 && length == C.LENGTH_UNBOUNDED)) {
String rangeRequest = "bytes=" + position + "-";
if (length != C.LENGTH_UNBOUNDED) {
rangeRequest += (position + length - 1);
}
headersMap.put("Range", rangeRequest);
}
if (!allowGzip) {
headersMap.put("Accept-Encoding", "identity");
}
Headers headers = Headers.of(headersMap);
Request.Builder builder = new Request.Builder()
.url(url)
.headers(headers);
if (postBody != null) {
builder.post(RequestBody.create(null, postBody));
}
else {
builder.get();
}
request = builder.build();
return okHttpClient.newCall(request).execute();
}
/**
* Attempts to extract the length of the content from the response headers of an open connection.
*
* @param response The response.
* @return The extracted length, or {@link C#LENGTH_UNBOUNDED}.
*/
private static long getContentLength(Response response) {
long contentLength = C.LENGTH_UNBOUNDED;
String contentLengthHeader = response.header("Content-Length");
if (!TextUtils.isEmpty(contentLengthHeader)) {
try {
contentLength = Long.parseLong(contentLengthHeader);
}
catch (NumberFormatException e) {
Log.e(TAG, "Unexpected Content-Length [" + contentLengthHeader + "]");
}
}
String contentRangeHeader = response.header("Content-Range");
if (!TextUtils.isEmpty(contentRangeHeader)) {
Matcher matcher = CONTENT_RANGE_HEADER.matcher(contentRangeHeader);
if (matcher.find()) {
try {
long contentLengthFromRange =
Long.parseLong(matcher.group(2)) - Long.parseLong(matcher.group(1)) + 1;
if (contentLength < 0) {
// Some proxy servers strip the Content-Length header. Fall back to the length
// calculated here in this case.
contentLength = contentLengthFromRange;
}
else if (contentLength != contentLengthFromRange) {
// If there is a discrepancy between the Content-Length and Content-Range headers,
// assume the one with the larger value is correct. We have seen cases where carrier
// change one of them to reduce the size of a request, but it is unlikely anybody would
// increase it.
Log.w(TAG, "Inconsistent headers [" + contentLengthHeader + "] [" + contentRangeHeader
+ "]");
contentLength = Math.max(contentLength, contentLengthFromRange);
}
}
catch (NumberFormatException e) {
Log.e(TAG, "Unexpected Content-Range [" + contentRangeHeader + "]");
}
}
}
return contentLength;
}
/**
* Skips any bytes that need skipping. Else does nothing.
* <p>
* This implementation is based roughly on {@code libcore.io.Streams.skipByReading()}.
*
* @throws InterruptedIOException If the thread is interrupted during the operation.
* @throws EOFException If the end of the input stream is reached before the bytes are skipped.
*/
private void skipInternal() throws IOException {
if (bytesSkipped == bytesToSkip) {
return;
}
// Acquire the shared skip buffer.
byte[] skipBuffer = skipBufferReference.getAndSet(null);
if (skipBuffer == null) {
skipBuffer = new byte[4096];
}
while (bytesSkipped != bytesToSkip) {
int readLength = (int) Math.min(bytesToSkip - bytesSkipped, skipBuffer.length);
int read = response.body().source().read(skipBuffer, 0, readLength);
if (Thread.interrupted()) {
throw new InterruptedIOException();
}
if (read == -1) {
throw new EOFException();
}
bytesSkipped += read;
if (listener != null) {
listener.onBytesTransferred(read);
}
}
// Release the shared skip buffer.
skipBufferReference.set(skipBuffer);
}
/**
* Reads up to {@code length} bytes of data and stores them into {@code buffer}, starting at
* index {@code offset}.
* <p>
* This method blocks until at least one byte of data can be read, the end of the opened range is
* detected, or an exception is thrown.
*
* @param buffer The buffer into which the read data should be stored.
* @param offset The start offset into {@code buffer} at which data should be written.
* @param readLength The maximum number of bytes to read.
* @return The number of bytes read, or {@link C#RESULT_END_OF_INPUT} if the end of the opened
* range is reached.
* @throws IOException If an error occurs reading from the source.
*/
private int readInternal(byte[] buffer, int offset, int readLength) throws IOException {
readLength = bytesToRead == C.LENGTH_UNBOUNDED ? readLength : (int) Math.min(readLength, bytesToRead - bytesRead);
if (readLength == 0) {
// We've read all of the requested data.
return C.RESULT_END_OF_INPUT;
}
int read = response.body().source().read(buffer, offset, readLength);
if (read == -1) {
if (bytesToRead != C.LENGTH_UNBOUNDED && bytesToRead != bytesRead) {
// The server closed the connection having not sent sufficient data.
throw new EOFException();
}
return C.RESULT_END_OF_INPUT;
}
bytesRead += read;
if (listener != null) {
listener.onBytesTransferred(read);
}
return read;
}
/**
* Closes the current connection quietly, if there is one.
*/
private void closeQuietly() {
if(response != null) {
ResponseBody responseBody = response.body();
if(responseBody != null) {
responseBody.close();
}
response = null;
}
request = null;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment