Last active
September 20, 2016 10:19
-
-
Save eygraber/8e935e19eedc70d2d8e3 to your computer and use it in GitHub Desktop.
Copied from DefaultHttpDataSource, but uses OkHttpClient instead of URLConnection
This file contains hidden or 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
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