diff --git a/extensions/okhttp/README.md b/extensions/okhttp/README.md
new file mode 100644
index 00000000000..54fd5dfaddc
--- /dev/null
+++ b/extensions/okhttp/README.md
@@ -0,0 +1,9 @@
+# ExoPlayer OkHttp Extension #
+
+## Description ##
+
+The OkHttp Extension is a [HTTP Data Source][] implementation using Square's [OkHttp][].
+
+[HTTP Data Source]: http://google.github.io/ExoPlayer/doc/reference/com/google/android/exoplayer/upstream/HttpDataSource.html
+
+[OkHttp]: http://square.github.io/okhttp/
\ No newline at end of file
diff --git a/extensions/okhttp/build.gradle b/extensions/okhttp/build.gradle
new file mode 100644
index 00000000000..6566214f910
--- /dev/null
+++ b/extensions/okhttp/build.gradle
@@ -0,0 +1,35 @@
+apply plugin: 'com.android.library'
+
+android{
+ compileSdkVersion 22
+ buildToolsVersion "22.0.1"
+
+ defaultConfig {
+ minSdkVersion 9
+ targetSdkVersion 22
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
+ }
+ }
+
+ lintOptions {
+ abortOnError false
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_7
+ targetCompatibility JavaVersion.VERSION_1_7
+ }
+}
+
+dependencies {
+ compile project(':library')
+ compile('com.squareup.okhttp:okhttp:2.4.0') {
+ exclude group: 'org.json'
+ }
+ compile 'com.android.support:support-annotations:23.0.0'
+}
\ No newline at end of file
diff --git a/extensions/okhttp/src/main/AndroidManifest.xml b/extensions/okhttp/src/main/AndroidManifest.xml
new file mode 100644
index 00000000000..d47c4c210be
--- /dev/null
+++ b/extensions/okhttp/src/main/AndroidManifest.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
diff --git a/extensions/okhttp/src/main/java/com/google/android/exoplayer/ext/datasource/okhttp/OkHttpDataSource.java b/extensions/okhttp/src/main/java/com/google/android/exoplayer/ext/datasource/okhttp/OkHttpDataSource.java
new file mode 100644
index 00000000000..9ba349445e6
--- /dev/null
+++ b/extensions/okhttp/src/main/java/com/google/android/exoplayer/ext/datasource/okhttp/OkHttpDataSource.java
@@ -0,0 +1,468 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.android.exoplayer.ext.datasource.okhttp;
+
+import com.google.android.exoplayer.C;
+import com.google.android.exoplayer.upstream.DataSpec;
+import com.google.android.exoplayer.upstream.DefaultHttpDataSource;
+import com.google.android.exoplayer.upstream.HttpDataSource;
+import com.google.android.exoplayer.upstream.TransferListener;
+import com.google.android.exoplayer.util.Assertions;
+import com.google.android.exoplayer.util.Predicate;
+import com.squareup.okhttp.CacheControl;
+import com.squareup.okhttp.HttpUrl;
+import com.squareup.okhttp.Interceptor;
+import com.squareup.okhttp.OkHttpClient;
+import com.squareup.okhttp.Request;
+import com.squareup.okhttp.RequestBody;
+import com.squareup.okhttp.Response;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.ProtocolException;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static com.squareup.okhttp.internal.Util.closeQuietly;
+
+/**
+ * A {@link HttpDataSource} that uses Square's {@link OkHttpClient}.
+ *
+ * By default this implementation will follow cross-protocol redirects (i.e. redirects from
+ * HTTP to HTTPS or vice versa). Cross-protocol redirects can be disabled by using the
+ * {@link #OkHttpDataSource(String, Predicate, TransferListener, int, int, boolean, OkHttpClient, CacheControl)}
+ * constructor and passing {@code false} as the sixth argument.
+ */
+public class OkHttpDataSource implements HttpDataSource {
+
+ /**
+ * The default connection timeout, in milliseconds.
+ */
+ private static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS;
+ /**
+ * The default read timeout, in milliseconds.
+ */
+ private static final int DEFAULT_READ_TIMEOUT_MILLIS = DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS;
+
+ private static final String TAG = "OkHttpDataSource";
+ private static final AtomicReference skipBufferReference = new AtomicReference<>();
+ private final String userAgent;
+
+ private final Predicate contentTypePredicate;
+ private final HashMap requestProperties;
+ private final CacheControl cacheControl;
+ private final TransferListener listener;
+
+ private DataSpec dataSpec;
+ private static OkHttpClient okHttpClient;
+ private Response response;
+ private boolean opened;
+
+ private long bytesToSkip;
+ private long bytesToRead;
+
+ private long bytesSkipped;
+ private long bytesRead;
+
+ /**
+ * @param userAgent The User-Agent string that should be used.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is
+ * rejected by the predicate then a {@link InvalidContentTypeException} is
+ * thrown from {@link #open(DataSpec)}.
+ */
+ public OkHttpDataSource(String userAgent, Predicate contentTypePredicate) {
+ this(userAgent, contentTypePredicate, null);
+ }
+
+ /**
+ * @param userAgent The User-Agent string that should be used.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is
+ * rejected by the predicate then a {@link InvalidContentTypeException} is
+ * thrown from {@link #open(DataSpec)}.
+ * @param listener An optional listener.
+ */
+ public OkHttpDataSource(String userAgent, Predicate contentTypePredicate,
+ TransferListener listener) {
+ this(userAgent, contentTypePredicate, listener, DEFAULT_CONNECT_TIMEOUT_MILLIS,
+ DEFAULT_READ_TIMEOUT_MILLIS);
+ }
+
+ /**
+ * @param userAgent The User-Agent string that should be used.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is
+ * rejected by the predicate then a {@link InvalidContentTypeException} is
+ * thrown from {@link #open(DataSpec)}.
+ * @param listener An optional listener.
+ * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is
+ * interpreted as an infinite timeout.
+ * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted
+ * as an infinite timeout.
+ */
+ public OkHttpDataSource(String userAgent, Predicate contentTypePredicate,
+ TransferListener listener, int connectTimeoutMillis, int readTimeoutMillis) {
+ this(userAgent, contentTypePredicate, listener, connectTimeoutMillis, readTimeoutMillis, true, null, null);
+ }
+
+ /**
+ * @param userAgent The User-Agent string that should be used.
+ * @param contentTypePredicate An optional {@link Predicate}. If a content type is
+ * rejected by the predicate then a {@link InvalidContentTypeException} is
+ * thrown from {@link #open(DataSpec)}.
+ * @param listener An optional listener.
+ * @param connectTimeoutMillis The connection timeout, in milliseconds. A timeout of zero is
+ * interpreted as an infinite timeout. Pass {@link #DEFAULT_CONNECT_TIMEOUT_MILLIS} to use
+ * the default value.
+ * @param readTimeoutMillis The read timeout, in milliseconds. A timeout of zero is interpreted
+ * as an infinite timeout. Pass {@link #DEFAULT_READ_TIMEOUT_MILLIS} to use the default value.
+ * @param allowCrossProtocolRedirects Whether cross-protocol redirects (i.e. redirects from HTTP
+ * to HTTPS and vice versa) are enabled.
+ * @param httpClient An optional {@link OkHttpClient}. Most applications can use a single OkHttpClient for all of
+ * their HTTP requests. Pass an {@link OkHttpClient} if you already have an
+ * {@link OkHttpClient} in your application, or you want some customized feature, such as
+ * monitor calls using {@link Interceptor}.
+ * @param cacheControl An optional {@link CacheControl} which sets all requests' Cache-Control header. For example,
+ * you could force the network response for all requests.
+ */
+ public OkHttpDataSource(String userAgent, Predicate contentTypePredicate,
+ TransferListener listener, int connectTimeoutMillis, int readTimeoutMillis,
+ boolean allowCrossProtocolRedirects, OkHttpClient httpClient, CacheControl cacheControl) {
+ this.userAgent = Assertions.checkNotEmpty(userAgent);
+ this.contentTypePredicate = contentTypePredicate;
+ this.listener = listener;
+ this.requestProperties = new HashMap<>();
+
+ if (httpClient != null) {
+ okHttpClient = httpClient;
+ } else if (okHttpClient == null) {
+ okHttpClient = new OkHttpClient();
+ }
+ okHttpClient.setConnectTimeout(connectTimeoutMillis, TimeUnit.MILLISECONDS);
+ okHttpClient.setReadTimeout(readTimeoutMillis, TimeUnit.MILLISECONDS);
+
+ if (!allowCrossProtocolRedirects) {
+ okHttpClient.setFollowSslRedirects(allowCrossProtocolRedirects);
+ }
+ this.cacheControl = cacheControl;
+ }
+
+ @Override
+ public String getUri() {
+ return response == null ? null : response.request().urlString();
+ }
+
+ @Override
+ public Map> getResponseHeaders() {
+ return response == null ? null : response.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;
+ Request request = makeRequest(dataSpec);
+ try {
+ response = okHttpClient.newCall(request).execute();
+ } catch (IOException e) {
+ throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e,
+ dataSpec);
+ }
+
+ int responseCode = response.code();
+
+ // Check for a valid response code.
+ if (!response.isSuccessful()) {
+ Map> headers = request.headers().toMultimap();
+ closeConnection();
+ throw new InvalidResponseCodeException(responseCode, headers, dataSpec);
+ }
+
+ // Check for a valid content type.
+ String contentType = response.body().contentType().toString();
+ if (contentTypePredicate != null && !contentTypePredicate.evaluate(contentType)) {
+ closeConnection();
+ throw new InvalidContentTypeException(contentType, 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 = 0;
+ try {
+ contentLength = response.body().contentLength();
+ } catch (IOException e) {
+ closeConnection();
+ throw new HttpDataSourceException(e, dataSpec);
+ }
+ 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 {
+ if (opened) {
+ opened = false;
+ if (listener != null) {
+ listener.onTransferEnd();
+ }
+ closeConnection();
+ }
+ }
+
+ /**
+ * Returns the current connection, or null if the source is not currently opened.
+ *
+ * @return The current open connection, or null.
+ */
+ protected final OkHttpClient getOkHttpClient() {
+ return okHttpClient;
+ }
+
+ /**
+ * Returns the number of bytes that have been skipped since the most recent call to
+ * {@link #open(DataSpec)}.
+ *
+ * @return The number of bytes skipped.
+ */
+ protected final long bytesSkipped() {
+ return bytesSkipped;
+ }
+
+ /**
+ * Returns the number of bytes that have been read since the most recent call to
+ * {@link #open(DataSpec)}.
+ *
+ * @return The number of bytes read.
+ */
+ protected final long bytesRead() {
+ return bytesRead;
+ }
+
+ /**
+ * Returns the number of bytes that are still to be read for the current {@link DataSpec}.
+ *
+ * If the total length of the data being read is known, then this length minus {@code bytesRead()}
+ * is returned. If the total length is unknown, {@link C#LENGTH_UNBOUNDED} is returned.
+ *
+ * @return The remaining length, or {@link C#LENGTH_UNBOUNDED}.
+ */
+ protected final long bytesRemaining() {
+ return bytesToRead == C.LENGTH_UNBOUNDED ? bytesToRead : bytesToRead - bytesRead;
+ }
+
+ private Request makeRequest(DataSpec dataSpec) {
+ long position = dataSpec.position;
+ long length = dataSpec.length;
+ boolean allowGzip = (dataSpec.flags & DataSpec.FLAG_ALLOW_GZIP) != 0;
+ HttpUrl url = HttpUrl.parse(dataSpec.uri.toString());
+ Request.Builder builder = new Request.Builder()
+ .url(url);
+ if (cacheControl != null) {
+ builder.cacheControl(cacheControl);
+ }
+ synchronized (requestProperties) {
+ for (Map.Entry property : requestProperties.entrySet()) {
+ builder.addHeader(property.getKey(), property.getValue());
+ }
+ }
+ if (!(position == 0 && length == C.LENGTH_UNBOUNDED)) {
+ String rangeRequest = "bytes=" + position + "-";
+ if (length != C.LENGTH_UNBOUNDED) {
+ rangeRequest += (position + length - 1);
+ }
+ builder.addHeader("Range", rangeRequest);
+ }
+ builder.addHeader("User-Agent", userAgent);
+ if (!allowGzip) {
+ builder.addHeader("Accept-Encoding", "identity");
+ }
+ if (dataSpec.postBody != null) {
+ builder.post(RequestBody.create(null, dataSpec.postBody));
+ }
+ return builder.build();
+ }
+
+ /**
+ * Handles a redirect.
+ *
+ * @param originalUrl The original URL.
+ * @param location The Location header in the response.
+ * @return The next URL.
+ * @throws IOException If redirection isn't possible.
+ */
+ private static URL handleRedirect(URL originalUrl, String location) throws IOException {
+ if (location == null) {
+ throw new ProtocolException("Null location redirect");
+ }
+ // Form the new url.
+ URL url = new URL(originalUrl, location);
+ // Check that the protocol of the new url is supported.
+ String protocol = url.getProtocol();
+ if (!"https".equals(protocol) && !"http".equals(protocol)) {
+ throw new ProtocolException("Unsupported protocol redirect: " + protocol);
+ }
+ // Currently this method is only called if allowCrossProtocolRedirects is true, and so the code
+ // below isn't required. If we ever decide to handle redirects ourselves when cross-protocol
+ // redirects are disabled, we'll need to uncomment this block of code.
+ // if (!allowCrossProtocolRedirects && !protocol.equals(originalUrl.getProtocol())) {
+ // throw new ProtocolException("Disallowed cross-protocol redirect ("
+ // + originalUrl.getProtocol() + " to " + protocol + ")");
+ // }
+ return url;
+ }
+
+ /**
+ * Skips any bytes that need skipping. Else does nothing.
+ *
+ * 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().byteStream().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}.
+ *
+ * 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().byteStream().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, if there is one.
+ */
+ private void closeConnection() {
+ closeQuietly(response.body());
+ response = null;
+ }
+}
diff --git a/extensions/okhttp/src/main/project.properties b/extensions/okhttp/src/main/project.properties
new file mode 100644
index 00000000000..2ed62fbfcf4
--- /dev/null
+++ b/extensions/okhttp/src/main/project.properties
@@ -0,0 +1,16 @@
+# This file is automatically generated by Android Tools.
+# Do not modify this file -- YOUR CHANGES WILL BE ERASED!
+#
+# This file must be checked in Version Control Systems.
+#
+# To customize properties used by the Ant build system edit
+# "ant.properties", and override values to adapt the script to your
+# project structure.
+#
+# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home):
+#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
+
+# Project target.
+target=android-22
+android.library=true
+android.library.reference.1=../../../../library/src/main
diff --git a/settings.gradle b/settings.gradle
index 70fb45ca11d..78266291f6c 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -17,7 +17,9 @@ include ':playbacktests'
include ':opus-extension'
include ':vp9-extension'
include ':webm-sw-demo'
+include ':okhttp-extension'
project(':opus-extension').projectDir = new File(settingsDir, 'extensions/opus')
project(':vp9-extension').projectDir = new File(settingsDir, 'extensions/vp9')
project(':webm-sw-demo').projectDir = new File(settingsDir, 'demo_misc/webm_sw_decoder')
+project(':okhttp-extension').projectDir = new File(settingsDir, 'extensions/okhttp')