blob: fe2bdd672b0e4486d9bdbfd50dbdb38d09dc9684 [file] [log] [blame]
/*
* Copyright (C) 2016 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.exoplayer2.ext.okhttp;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import android.net.Uri;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.ExoPlayerLibraryInfo;
import com.google.android.exoplayer2.upstream.BaseDataSource;
import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.Predicate;
import com.google.android.exoplayer2.util.Util;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import okhttp3.CacheControl;
import okhttp3.Call;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
/**
* An {@link HttpDataSource} that delegates to Square's {@link Call.Factory}.
*
* <p>Note: HTTP request headers will be set using all parameters passed via (in order of decreasing
* priority) the {@code dataSpec}, {@link #setRequestProperty} and the default parameters used to
* construct the instance.
*/
public class OkHttpDataSource extends BaseDataSource implements HttpDataSource {
static {
ExoPlayerLibraryInfo.registerModule("goog.exo.okhttp");
}
private static final byte[] SKIP_BUFFER = new byte[4096];
private final Call.Factory callFactory;
private final RequestProperties requestProperties;
@Nullable private final String userAgent;
@Nullable private final CacheControl cacheControl;
@Nullable private final RequestProperties defaultRequestProperties;
@Nullable private Predicate<String> contentTypePredicate;
@Nullable private DataSpec dataSpec;
@Nullable private Response response;
@Nullable private InputStream responseByteStream;
private boolean opened;
private long bytesToSkip;
private long bytesToRead;
private long bytesSkipped;
private long bytesRead;
/**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the source.
* @param userAgent An optional User-Agent string.
*/
public OkHttpDataSource(Call.Factory callFactory, @Nullable String userAgent) {
this(callFactory, userAgent, /* cacheControl= */ null, /* defaultRequestProperties= */ null);
}
/**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the source.
* @param userAgent An optional User-Agent string.
* @param cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
* @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
* server as HTTP headers on every request.
*/
public OkHttpDataSource(
Call.Factory callFactory,
@Nullable String userAgent,
@Nullable CacheControl cacheControl,
@Nullable RequestProperties defaultRequestProperties) {
super(/* isNetwork= */ true);
this.callFactory = Assertions.checkNotNull(callFactory);
this.userAgent = userAgent;
this.cacheControl = cacheControl;
this.defaultRequestProperties = defaultRequestProperties;
this.requestProperties = new RequestProperties();
}
/**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the source.
* @param userAgent An optional User-Agent string.
* @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)}.
* @deprecated Use {@link #OkHttpDataSource(Call.Factory, String)} and {@link
* #setContentTypePredicate(Predicate)}.
*/
@Deprecated
public OkHttpDataSource(
Call.Factory callFactory,
@Nullable String userAgent,
@Nullable Predicate<String> contentTypePredicate) {
this(
callFactory,
userAgent,
contentTypePredicate,
/* cacheControl= */ null,
/* defaultRequestProperties= */ null);
}
/**
* @param callFactory A {@link Call.Factory} (typically an {@link okhttp3.OkHttpClient}) for use
* by the source.
* @param userAgent An optional User-Agent string.
* @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 cacheControl An optional {@link CacheControl} for setting the Cache-Control header.
* @param defaultRequestProperties Optional default {@link RequestProperties} to be sent to the
* server as HTTP headers on every request.
* @deprecated Use {@link #OkHttpDataSource(Call.Factory, String, CacheControl,
* RequestProperties)} and {@link #setContentTypePredicate(Predicate)}.
*/
@Deprecated
public OkHttpDataSource(
Call.Factory callFactory,
@Nullable String userAgent,
@Nullable Predicate<String> contentTypePredicate,
@Nullable CacheControl cacheControl,
@Nullable RequestProperties defaultRequestProperties) {
super(/* isNetwork= */ true);
this.callFactory = Assertions.checkNotNull(callFactory);
this.userAgent = userAgent;
this.contentTypePredicate = contentTypePredicate;
this.cacheControl = cacheControl;
this.defaultRequestProperties = defaultRequestProperties;
this.requestProperties = new RequestProperties();
}
/**
* Sets a content type {@link Predicate}. If a content type is rejected by the predicate then a
* {@link HttpDataSource.InvalidContentTypeException} is thrown from {@link #open(DataSpec)}.
*
* @param contentTypePredicate The content type {@link Predicate}, or {@code null} to clear a
* predicate that was previously set.
*/
public void setContentTypePredicate(@Nullable Predicate<String> contentTypePredicate) {
this.contentTypePredicate = contentTypePredicate;
}
@Override
@Nullable
public Uri getUri() {
return response == null ? null : Uri.parse(response.request().url().toString());
}
@Override
public int getResponseCode() {
return response == null ? -1 : response.code();
}
@Override
public Map<String, List<String>> getResponseHeaders() {
return response == null ? Collections.emptyMap() : response.headers().toMultimap();
}
@Override
public void setRequestProperty(String name, String value) {
Assertions.checkNotNull(name);
Assertions.checkNotNull(value);
requestProperties.set(name, value);
}
@Override
public void clearRequestProperty(String name) {
Assertions.checkNotNull(name);
requestProperties.remove(name);
}
@Override
public void clearAllRequestProperties() {
requestProperties.clear();
}
@Override
public long open(DataSpec dataSpec) throws HttpDataSourceException {
this.dataSpec = dataSpec;
this.bytesRead = 0;
this.bytesSkipped = 0;
transferInitializing(dataSpec);
Request request = makeRequest(dataSpec);
Response response;
ResponseBody responseBody;
try {
this.response = callFactory.newCall(request).execute();
response = this.response;
responseBody = Assertions.checkNotNull(response.body());
responseByteStream = responseBody.byteStream();
} catch (IOException e) {
throw new HttpDataSourceException(
"Unable to connect", e, dataSpec, HttpDataSourceException.TYPE_OPEN);
}
int responseCode = response.code();
// Check for a valid response code.
if (!response.isSuccessful()) {
Map<String, List<String>> headers = response.headers().toMultimap();
closeConnectionQuietly();
InvalidResponseCodeException exception =
new InvalidResponseCodeException(responseCode, response.message(), headers, dataSpec);
if (responseCode == 416) {
exception.initCause(new DataSourceException(DataSourceException.POSITION_OUT_OF_RANGE));
}
throw exception;
}
// Check for a valid content type.
MediaType mediaType = responseBody.contentType();
String contentType = mediaType != null ? mediaType.toString() : "";
if (contentTypePredicate != null && !contentTypePredicate.evaluate(contentType)) {
closeConnectionQuietly();
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.length != C.LENGTH_UNSET) {
bytesToRead = dataSpec.length;
} else {
long contentLength = responseBody.contentLength();
bytesToRead = contentLength != -1 ? (contentLength - bytesToSkip) : C.LENGTH_UNSET;
}
opened = true;
transferStarted(dataSpec);
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, Assertions.checkNotNull(dataSpec), HttpDataSourceException.TYPE_READ);
}
}
@Override
public void close() throws HttpDataSourceException {
if (opened) {
opened = false;
transferEnded();
closeConnectionQuietly();
}
}
/**
* 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}.
* <p>
* 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_UNSET} is returned.
*
* @return The remaining length, or {@link C#LENGTH_UNSET}.
*/
protected final long bytesRemaining() {
return bytesToRead == C.LENGTH_UNSET ? bytesToRead : bytesToRead - bytesRead;
}
/** Establishes a connection. */
private Request makeRequest(DataSpec dataSpec) throws HttpDataSourceException {
long position = dataSpec.position;
long length = dataSpec.length;
HttpUrl url = HttpUrl.parse(dataSpec.uri.toString());
if (url == null) {
throw new HttpDataSourceException(
"Malformed URL", dataSpec, HttpDataSourceException.TYPE_OPEN);
}
Request.Builder builder = new Request.Builder().url(url);
if (cacheControl != null) {
builder.cacheControl(cacheControl);
}
Map<String, String> headers = new HashMap<>();
if (defaultRequestProperties != null) {
headers.putAll(defaultRequestProperties.getSnapshot());
}
headers.putAll(requestProperties.getSnapshot());
headers.putAll(dataSpec.httpRequestHeaders);
for (Map.Entry<String, String> header : headers.entrySet()) {
builder.header(header.getKey(), header.getValue());
}
if (!(position == 0 && length == C.LENGTH_UNSET)) {
String rangeRequest = "bytes=" + position + "-";
if (length != C.LENGTH_UNSET) {
rangeRequest += (position + length - 1);
}
builder.addHeader("Range", rangeRequest);
}
if (userAgent != null) {
builder.addHeader("User-Agent", userAgent);
}
if (!dataSpec.isFlagSet(DataSpec.FLAG_ALLOW_GZIP)) {
builder.addHeader("Accept-Encoding", "identity");
}
RequestBody requestBody = null;
if (dataSpec.httpBody != null) {
requestBody = RequestBody.create(null, dataSpec.httpBody);
} else if (dataSpec.httpMethod == DataSpec.HTTP_METHOD_POST) {
// OkHttp requires a non-null body for POST requests.
requestBody = RequestBody.create(null, Util.EMPTY_BYTE_ARRAY);
}
builder.method(dataSpec.getHttpMethodString(), requestBody);
return builder.build();
}
/**
* 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;
}
while (bytesSkipped != bytesToSkip) {
int readLength = (int) Math.min(bytesToSkip - bytesSkipped, SKIP_BUFFER.length);
int read = castNonNull(responseByteStream).read(SKIP_BUFFER, 0, readLength);
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedIOException();
}
if (read == -1) {
throw new EOFException();
}
bytesSkipped += read;
bytesTransferred(read);
}
}
/**
* 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 {
if (readLength == 0) {
return 0;
}
if (bytesToRead != C.LENGTH_UNSET) {
long bytesRemaining = bytesToRead - bytesRead;
if (bytesRemaining == 0) {
return C.RESULT_END_OF_INPUT;
}
readLength = (int) Math.min(readLength, bytesRemaining);
}
int read = castNonNull(responseByteStream).read(buffer, offset, readLength);
if (read == -1) {
if (bytesToRead != C.LENGTH_UNSET) {
// End of stream reached having not read sufficient data.
throw new EOFException();
}
return C.RESULT_END_OF_INPUT;
}
bytesRead += read;
bytesTransferred(read);
return read;
}
/**
* Closes the current connection quietly, if there is one.
*/
private void closeConnectionQuietly() {
if (response != null) {
Assertions.checkNotNull(response.body()).close();
response = null;
}
responseByteStream = null;
}
}