All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.android.exoplayer.upstream.HttpDataSource Maven / Gradle / Ivy

The newest version!
/*
 * 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.upstream;

import android.text.TextUtils;
import android.util.Log;

import com.google.android.exoplayer.C;
import com.google.android.exoplayer.util.Assertions;
import com.google.android.exoplayer.util.Predicate;
import com.google.android.exoplayer.util.Util;

import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * An http {@link DataSource}.
 */
public class HttpDataSource implements DataSource {

  /**
   * A {@link Predicate} that rejects content types often used for pay-walls.
   */
  public static final Predicate REJECT_PAYWALL_TYPES = new Predicate() {

    @Override
    public boolean evaluate(String contentType) {
      contentType = Util.toLowerInvariant(contentType);
      return !TextUtils.isEmpty(contentType)
          && (!contentType.contains("text") || contentType.contains("text/vtt"))
          && !contentType.contains("html") && !contentType.contains("xml");
    }

  };

  /**
   * Thrown when an error is encountered when trying to read from HTTP data source.
   */
  public static class HttpDataSourceException extends IOException {

    /*
     * The {@link DataSpec} associated with the current connection.
     */
    public final DataSpec dataSpec;

    public HttpDataSourceException(DataSpec dataSpec) {
      super();
      this.dataSpec = dataSpec;
    }

    public HttpDataSourceException(String message, DataSpec dataSpec) {
      super(message);
      this.dataSpec = dataSpec;
    }

    public HttpDataSourceException(IOException cause, DataSpec dataSpec) {
      super(cause);
      this.dataSpec = dataSpec;
    }

    public HttpDataSourceException(String message, IOException cause, DataSpec dataSpec) {
      super(message, cause);
      this.dataSpec = dataSpec;
    }

  }

  /**
   * Thrown when the content type is invalid.
   */
  public static final class InvalidContentTypeException extends HttpDataSourceException {

    public final String contentType;

    public InvalidContentTypeException(String contentType, DataSpec dataSpec) {
      super("Invalid content type: " + contentType, dataSpec);
      this.contentType = contentType;
    }

  }

  /**
   * Thrown when an attempt to open a connection results in a response code not in the 2xx range.
   */
  public static final class InvalidResponseCodeException extends HttpDataSourceException {

    /**
     * The response code that was outside of the 2xx range.
     */
    public final int responseCode;

    /**
     * An unmodifiable map of the response header fields and values.
     */
    public final Map> headerFields;

    public InvalidResponseCodeException(int responseCode, Map> headerFields,
        DataSpec dataSpec) {
      super("Response code: " + responseCode, dataSpec);
      this.responseCode = responseCode;
      this.headerFields = headerFields;
    }

  }

  public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 8 * 1000;
  public static final int DEFAULT_READ_TIMEOUT_MILLIS = 8 * 1000;

  private static final String TAG = "HttpDataSource";
  private static final Pattern CONTENT_RANGE_HEADER =
      Pattern.compile("^bytes (\\d+)-(\\d+)/(\\d+)$");

  private final int connectTimeoutMillis;
  private final int readTimeoutMillis;
  private final String userAgent;
  private final Predicate contentTypePredicate;
  private final HashMap requestProperties;
  private final TransferListener listener;

  private DataSpec dataSpec;
  private HttpURLConnection connection;
  private InputStream inputStream;
  private boolean opened;

  private long dataLength;
  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 HttpDataSource(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 HttpDataSource(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 HttpDataSource(String userAgent, Predicate contentTypePredicate,
      TransferListener listener, int connectTimeoutMillis, int readTimeoutMillis) {
    this.userAgent = Assertions.checkNotEmpty(userAgent);
    this.contentTypePredicate = contentTypePredicate;
    this.listener = listener;
    this.requestProperties = new HashMap();
    this.connectTimeoutMillis = connectTimeoutMillis;
    this.readTimeoutMillis = readTimeoutMillis;
  }

  /**
   * Sets the value of a request header field. The value will be used for subsequent connections
   * established by the source.
   *
   * @param name The name of the header field.
   * @param value The value of the field.
   */
  public void setRequestProperty(String name, String value) {
    Assertions.checkNotNull(name);
    Assertions.checkNotNull(value);
    synchronized (requestProperties) {
      requestProperties.put(name, value);
    }
  }

  /**
   * Clears the value of a request header field. The change will apply to subsequent connections
   * established by the source.
   *
   * @param name The name of the header field.
   */
  public void clearRequestProperty(String name) {
    Assertions.checkNotNull(name);
    synchronized (requestProperties) {
      requestProperties.remove(name);
    }
  }

  /**
   * Clears all request header fields that were set by {@link #setRequestProperty(String, String)}.
   */
  public void clearAllRequestProperties() {
    synchronized (requestProperties) {
      requestProperties.clear();
    }
  }

  @Override
  public long open(DataSpec dataSpec) throws HttpDataSourceException {
    this.dataSpec = dataSpec;
    this.bytesRead = 0;
    try {
      connection = 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;
    try {
      responseCode = connection.getResponseCode();
    } catch (IOException e) {
      throw new HttpDataSourceException("Unable to connect to " + dataSpec.uri.toString(), e,
          dataSpec);
    }
    if (responseCode < 200 || responseCode > 299) {
      Map> headers = connection.getHeaderFields();
      closeConnection();
      throw new InvalidResponseCodeException(responseCode, headers, dataSpec);
    }

    // Check for a valid content type.
    String contentType = connection.getContentType();
    if (contentTypePredicate != null && !contentTypePredicate.evaluate(contentType)) {
      closeConnection();
      throw new InvalidContentTypeException(contentType, dataSpec);
    }

    long contentLength = getContentLength(connection);
    dataLength = dataSpec.length == C.LENGTH_UNBOUNDED ? contentLength : dataSpec.length;

    if (dataSpec.length != C.LENGTH_UNBOUNDED && contentLength != C.LENGTH_UNBOUNDED
        && contentLength != dataSpec.length) {
      // The DataSpec specified a length and we resolved a length from the response headers, but
      // the two lengths do not match.
      closeConnection();
      throw new HttpDataSourceException(
          new UnexpectedLengthException(dataSpec.length, contentLength), dataSpec);
    }

    try {
      inputStream = connection.getInputStream();
    } catch (IOException e) {
      closeConnection();
      throw new HttpDataSourceException(e, dataSpec);
    }

    opened = true;
    if (listener != null) {
      listener.onTransferStart();
    }

    return dataLength;
  }

  @Override
  public int read(byte[] buffer, int offset, int readLength) throws HttpDataSourceException {
    int read = 0;
    try {
      read = inputStream.read(buffer, offset, readLength);
    } catch (IOException e) {
      throw new HttpDataSourceException(e, dataSpec);
    }

    if (read > 0) {
      bytesRead += read;
      if (listener != null) {
        listener.onBytesTransferred(read);
      }
    } else if (dataLength != C.LENGTH_UNBOUNDED && dataLength != bytesRead) {
      // Check for cases where the server closed the connection having not sent the correct amount
      // of data. We can only do this if we know the length of the data we were expecting.
      throw new HttpDataSourceException(new UnexpectedLengthException(dataLength, bytesRead),
          dataSpec);
    }

    return read;
  }

  @Override
  public void close() throws HttpDataSourceException {
    try {
      if (inputStream != null) {
        try {
          inputStream.close();
        } catch (IOException e) {
          throw new HttpDataSourceException(e, dataSpec);
        }
        inputStream = null;
      }
    } finally {
      if (opened) {
        opened = false;
        if (listener != null) {
          listener.onTransferEnd();
        }
        closeConnection();
      }
    }
  }

  private void closeConnection() {
    if (connection != null) {
      connection.disconnect();
      connection = null;
    }
  }

  /**
   * Returns the current connection, or null if the source is not currently opened.
   *
   * @return The current open connection, or null.
   */
  protected final HttpURLConnection getConnection() {
    return connection;
  }

  /**
   * 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 dataLength == C.LENGTH_UNBOUNDED ? dataLength : dataLength - bytesRead; } private HttpURLConnection makeConnection(DataSpec dataSpec) throws IOException { URL url = new URL(dataSpec.uri.toString()); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setConnectTimeout(connectTimeoutMillis); connection.setReadTimeout(readTimeoutMillis); connection.setDoOutput(false); synchronized (requestProperties) { //FIXME: NOT compiled by maven! //for (HashMap.Entry property : requestProperties.entrySet()) { //connection.setRequestProperty(property.getKey(), property.getValue()); //} } connection.setRequestProperty("Accept-Encoding", "deflate"); connection.setRequestProperty("User-Agent", userAgent); connection.setRequestProperty("Range", buildRangeHeader(dataSpec)); connection.connect(); return connection; } private String buildRangeHeader(DataSpec dataSpec) { String rangeRequest = "bytes=" + dataSpec.position + "-"; if (dataSpec.length != C.LENGTH_UNBOUNDED) { rangeRequest += (dataSpec.position + dataSpec.length - 1); } return rangeRequest; } private long getContentLength(HttpURLConnection connection) { long contentLength = C.LENGTH_UNBOUNDED; String contentLengthHeader = connection.getHeaderField("Content-Length"); if (!TextUtils.isEmpty(contentLengthHeader)) { try { contentLength = Long.parseLong(contentLengthHeader); } catch (NumberFormatException e) { Log.e(TAG, "Unexpected Content-Length [" + contentLengthHeader + "]"); } } String contentRangeHeader = connection.getHeaderField("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; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy