NewPipe/app/src/main/java/org/schabi/newpipe/player/datasource/YoutubeHttpDataSource.java

1015 lines
42 KiB
Java
Raw Normal View History

Add support of other delivery methods than progressive HTTP (in the player only) Detailed changes: - External players: - Add a message instruction about stream selection; - Add a message when there is no stream available for external players; - Return now HLS, DASH and SmoothStreaming URL contents, in addition to progressive HTTP ones. - Player: - Support DASH, HLS and SmoothStreaming streams for videos, whether they are content URLs or the manifests themselves, in addition to progressive HTTP ones; - Use a custom HttpDataSource to play YouTube contents, based of ExoPlayer's default one, which allows better spoofing of official clients (custom user-agent and headers (depending of the client used), use of range and rn (set dynamically by the DataSource) parameters); - Fetch YouTube progressive contents as DASH streams, like official clients, support fully playback of livestreams which have ended recently and OTF streams; - Use ExoPlayer's default retries count for contents on non-fatal errors (instead of Integer.MAX_VALUE for non-live contents and 5 for live contents). - Download dialog: - Add message about support of progressive HTTP streams only for downloading; - Remove several duplicated code and update relevant usages; - Support downloading of contents with an unknown media format. - ListHelper: - Catch NumberFormatException when trying to compare two video streams between them. - Tests: - Update ListHelperTest and StreamItemAdapterTest to fix breaking changes in the extractor. - Other places: - Fixes deprecation of changes made in the extractor; - Improve some code related to the files changed. - Issues fixed and/or improved with the changes: - Seeking of PeerTube HLS streams (the duration shown was the one from the stream duration and not the one parsed, incomplete because HLS streams are fragmented MP4s with multiple sidx boxes, for which seeking is not supported by ExoPlayer) (the app now uses the HLS manifest returned for each quality, in the master playlist (not fetched and computed by the extractor)); - Crash when loading PeerTube streams with a separated audio; - Lack of some streams on some YouTube videos (OTF streams); - Loading times of YouTube streams, after a quality change or a playback start; - View count of YouTube ended livestreams interpreted as watching count (this type of streams is not interpreted anymore as livestreams); - Watchable time of YouTube ended livestreams; - Playback of SoundCloud HLS-only tracks (which cannot be downloaded anymore because the workaround which was used is being removed by SoundCloud, so it has been removed from the extractor).
2022-06-16 11:13:19 +02:00
/*
2022-07-24 20:11:31 +02:00
* Based on ExoPlayer's DefaultHttpDataSource, version 2.18.1.
Add support of other delivery methods than progressive HTTP (in the player only) Detailed changes: - External players: - Add a message instruction about stream selection; - Add a message when there is no stream available for external players; - Return now HLS, DASH and SmoothStreaming URL contents, in addition to progressive HTTP ones. - Player: - Support DASH, HLS and SmoothStreaming streams for videos, whether they are content URLs or the manifests themselves, in addition to progressive HTTP ones; - Use a custom HttpDataSource to play YouTube contents, based of ExoPlayer's default one, which allows better spoofing of official clients (custom user-agent and headers (depending of the client used), use of range and rn (set dynamically by the DataSource) parameters); - Fetch YouTube progressive contents as DASH streams, like official clients, support fully playback of livestreams which have ended recently and OTF streams; - Use ExoPlayer's default retries count for contents on non-fatal errors (instead of Integer.MAX_VALUE for non-live contents and 5 for live contents). - Download dialog: - Add message about support of progressive HTTP streams only for downloading; - Remove several duplicated code and update relevant usages; - Support downloading of contents with an unknown media format. - ListHelper: - Catch NumberFormatException when trying to compare two video streams between them. - Tests: - Update ListHelperTest and StreamItemAdapterTest to fix breaking changes in the extractor. - Other places: - Fixes deprecation of changes made in the extractor; - Improve some code related to the files changed. - Issues fixed and/or improved with the changes: - Seeking of PeerTube HLS streams (the duration shown was the one from the stream duration and not the one parsed, incomplete because HLS streams are fragmented MP4s with multiple sidx boxes, for which seeking is not supported by ExoPlayer) (the app now uses the HLS manifest returned for each quality, in the master playlist (not fetched and computed by the extractor)); - Crash when loading PeerTube streams with a separated audio; - Lack of some streams on some YouTube videos (OTF streams); - Loading times of YouTube streams, after a quality change or a playback start; - View count of YouTube ended livestreams interpreted as watching count (this type of streams is not interpreted anymore as livestreams); - Watchable time of YouTube ended livestreams; - Playback of SoundCloud HLS-only tracks (which cannot be downloaded anymore because the workaround which was used is being removed by SoundCloud, so it has been removed from the extractor).
2022-06-16 11:13:19 +02:00
*
* Original source code copyright (C) 2016 The Android Open Source Project, licensed under the
* Apache License, Version 2.0.
*/
package org.schabi.newpipe.player.datasource;
import static com.google.android.exoplayer2.upstream.DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS;
import static com.google.android.exoplayer2.upstream.DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS;
import static com.google.android.exoplayer2.upstream.HttpUtil.buildRangeRequestHeader;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getAndroidUserAgent;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.getIosUserAgent;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isAndroidStreamingUrl;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isIosStreamingUrl;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isWebStreamingUrl;
import static org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper.isTvHtml5SimplyEmbeddedPlayerStreamingUrl;
import static java.lang.Math.min;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.upstream.BaseDataSource;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DataSourceException;
import com.google.android.exoplayer2.upstream.DataSpec;
import com.google.android.exoplayer2.upstream.DataSpec.HttpMethod;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource;
import com.google.android.exoplayer2.upstream.HttpDataSource;
import com.google.android.exoplayer2.upstream.HttpUtil;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.Util;
import com.google.common.base.Predicate;
import com.google.common.collect.ForwardingMap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import com.google.common.net.HttpHeaders;
import org.schabi.newpipe.DownloaderImpl;
Add support of other delivery methods than progressive HTTP (in the player only) Detailed changes: - External players: - Add a message instruction about stream selection; - Add a message when there is no stream available for external players; - Return now HLS, DASH and SmoothStreaming URL contents, in addition to progressive HTTP ones. - Player: - Support DASH, HLS and SmoothStreaming streams for videos, whether they are content URLs or the manifests themselves, in addition to progressive HTTP ones; - Use a custom HttpDataSource to play YouTube contents, based of ExoPlayer's default one, which allows better spoofing of official clients (custom user-agent and headers (depending of the client used), use of range and rn (set dynamically by the DataSource) parameters); - Fetch YouTube progressive contents as DASH streams, like official clients, support fully playback of livestreams which have ended recently and OTF streams; - Use ExoPlayer's default retries count for contents on non-fatal errors (instead of Integer.MAX_VALUE for non-live contents and 5 for live contents). - Download dialog: - Add message about support of progressive HTTP streams only for downloading; - Remove several duplicated code and update relevant usages; - Support downloading of contents with an unknown media format. - ListHelper: - Catch NumberFormatException when trying to compare two video streams between them. - Tests: - Update ListHelperTest and StreamItemAdapterTest to fix breaking changes in the extractor. - Other places: - Fixes deprecation of changes made in the extractor; - Improve some code related to the files changed. - Issues fixed and/or improved with the changes: - Seeking of PeerTube HLS streams (the duration shown was the one from the stream duration and not the one parsed, incomplete because HLS streams are fragmented MP4s with multiple sidx boxes, for which seeking is not supported by ExoPlayer) (the app now uses the HLS manifest returned for each quality, in the master playlist (not fetched and computed by the extractor)); - Crash when loading PeerTube streams with a separated audio; - Lack of some streams on some YouTube videos (OTF streams); - Loading times of YouTube streams, after a quality change or a playback start; - View count of YouTube ended livestreams interpreted as watching count (this type of streams is not interpreted anymore as livestreams); - Watchable time of YouTube ended livestreams; - Playback of SoundCloud HLS-only tracks (which cannot be downloaded anymore because the workaround which was used is being removed by SoundCloud, so it has been removed from the extractor).
2022-06-16 11:13:19 +02:00
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.NoRouteToHostException;
import java.net.URL;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.zip.GZIPInputStream;
/**
* An {@link HttpDataSource} that uses Android's {@link HttpURLConnection}, based on
* {@link com.google.android.exoplayer2.upstream.DefaultHttpDataSource}, for YouTube streams.
*
* <p>
* It adds more headers to {@code videoplayback} URLs, such as {@code Origin}, {@code Referer}
* (only where it's relevant) and also more parameters, such as {@code rn} and replaces the use of
* the {@code Range} header by the corresponding parameter ({@code range}), if enabled.
* </p>
*
* There are many unused methods in this class because everything was copied from {@link
* com.google.android.exoplayer2.upstream.DefaultHttpDataSource} with as little changes as possible.
* SonarQube warnings were also suppressed for the same reason.
Add support of other delivery methods than progressive HTTP (in the player only) Detailed changes: - External players: - Add a message instruction about stream selection; - Add a message when there is no stream available for external players; - Return now HLS, DASH and SmoothStreaming URL contents, in addition to progressive HTTP ones. - Player: - Support DASH, HLS and SmoothStreaming streams for videos, whether they are content URLs or the manifests themselves, in addition to progressive HTTP ones; - Use a custom HttpDataSource to play YouTube contents, based of ExoPlayer's default one, which allows better spoofing of official clients (custom user-agent and headers (depending of the client used), use of range and rn (set dynamically by the DataSource) parameters); - Fetch YouTube progressive contents as DASH streams, like official clients, support fully playback of livestreams which have ended recently and OTF streams; - Use ExoPlayer's default retries count for contents on non-fatal errors (instead of Integer.MAX_VALUE for non-live contents and 5 for live contents). - Download dialog: - Add message about support of progressive HTTP streams only for downloading; - Remove several duplicated code and update relevant usages; - Support downloading of contents with an unknown media format. - ListHelper: - Catch NumberFormatException when trying to compare two video streams between them. - Tests: - Update ListHelperTest and StreamItemAdapterTest to fix breaking changes in the extractor. - Other places: - Fixes deprecation of changes made in the extractor; - Improve some code related to the files changed. - Issues fixed and/or improved with the changes: - Seeking of PeerTube HLS streams (the duration shown was the one from the stream duration and not the one parsed, incomplete because HLS streams are fragmented MP4s with multiple sidx boxes, for which seeking is not supported by ExoPlayer) (the app now uses the HLS manifest returned for each quality, in the master playlist (not fetched and computed by the extractor)); - Crash when loading PeerTube streams with a separated audio; - Lack of some streams on some YouTube videos (OTF streams); - Loading times of YouTube streams, after a quality change or a playback start; - View count of YouTube ended livestreams interpreted as watching count (this type of streams is not interpreted anymore as livestreams); - Watchable time of YouTube ended livestreams; - Playback of SoundCloud HLS-only tracks (which cannot be downloaded anymore because the workaround which was used is being removed by SoundCloud, so it has been removed from the extractor).
2022-06-16 11:13:19 +02:00
*/
@SuppressWarnings({"squid:S3011", "squid:S4738"})
public final class YoutubeHttpDataSource extends BaseDataSource implements HttpDataSource {
/**
* {@link DataSource.Factory} for {@link YoutubeHttpDataSource} instances.
*/
public static final class Factory implements HttpDataSource.Factory {
private final RequestProperties defaultRequestProperties;
@Nullable
private TransferListener transferListener;
@Nullable
private Predicate<String> contentTypePredicate;
private int connectTimeoutMs;
private int readTimeoutMs;
private boolean allowCrossProtocolRedirects;
private boolean keepPostFor302Redirects;
private boolean rangeParameterEnabled;
private boolean rnParameterEnabled;
/**
* Creates an instance.
*/
public Factory() {
defaultRequestProperties = new RequestProperties();
connectTimeoutMs = DEFAULT_CONNECT_TIMEOUT_MILLIS;
readTimeoutMs = DEFAULT_READ_TIMEOUT_MILLIS;
}
@NonNull
@Override
public Factory setDefaultRequestProperties(
@NonNull final Map<String, String> defaultRequestPropertiesMap) {
defaultRequestProperties.clearAndSet(defaultRequestPropertiesMap);
return this;
}
/**
* Sets the connect timeout, in milliseconds.
*
* <p>
* The default is {@link DefaultHttpDataSource#DEFAULT_CONNECT_TIMEOUT_MILLIS}.
* </p>
*
* @param connectTimeoutMsValue The connect timeout, in milliseconds, that will be used.
* @return This factory.
*/
public Factory setConnectTimeoutMs(final int connectTimeoutMsValue) {
connectTimeoutMs = connectTimeoutMsValue;
return this;
}
/**
* Sets the read timeout, in milliseconds.
*
* <p>The default is {@link DefaultHttpDataSource#DEFAULT_READ_TIMEOUT_MILLIS}.
*
* @param readTimeoutMsValue The connect timeout, in milliseconds, that will be used.
* @return This factory.
*/
public Factory setReadTimeoutMs(final int readTimeoutMsValue) {
readTimeoutMs = readTimeoutMsValue;
return this;
}
/**
* Sets whether to allow cross protocol redirects.
*
* <p>The default is {@code false}.
*
* @param allowCrossProtocolRedirectsValue Whether to allow cross protocol redirects.
* @return This factory.
*/
public Factory setAllowCrossProtocolRedirects(
final boolean allowCrossProtocolRedirectsValue) {
allowCrossProtocolRedirects = allowCrossProtocolRedirectsValue;
return this;
}
/**
* Sets whether the use of the {@code range} parameter instead of the {@code Range} header
* to request ranges of streams is enabled.
*
* <p>
* Note that it must be not enabled on streams which are using a {@link
* com.google.android.exoplayer2.source.ProgressiveMediaSource}, as it will break playback
* for them (some exceptions may be thrown).
* </p>
*
* @param rangeParameterEnabledValue whether the use of the {@code range} parameter instead
* of the {@code Range} header (must be only enabled when
* non-{@code ProgressiveMediaSource}s)
* @return This factory.
*/
public Factory setRangeParameterEnabled(final boolean rangeParameterEnabledValue) {
rangeParameterEnabled = rangeParameterEnabledValue;
return this;
}
/**
* Sets whether the use of the {@code rn}, which stands for request number, parameter is
* enabled.
*
* <p>
* Note that it should be not enabled on streams which are using {@code /} to delimit URLs
* parameters, such as the streams of HLS manifests.
* </p>
*
* @param rnParameterEnabledValue whether the appending the {@code rn} parameter to
* {@code videoplayback} URLs
* @return This factory.
*/
public Factory setRnParameterEnabled(final boolean rnParameterEnabledValue) {
rnParameterEnabled = rnParameterEnabledValue;
return this;
}
/**
* Sets a content type {@link Predicate}. If a content type is rejected by the predicate
* then a {@link HttpDataSource.InvalidContentTypeException} is thrown from
* {@link YoutubeHttpDataSource#open(DataSpec)}.
*
* <p>
* The default is {@code null}.
* </p>
*
* @param contentTypePredicateToSet The content type {@link Predicate}, or {@code null} to
* clear a predicate that was previously set.
* @return This factory.
*/
public Factory setContentTypePredicate(
@Nullable final Predicate<String> contentTypePredicateToSet) {
this.contentTypePredicate = contentTypePredicateToSet;
return this;
}
/**
* Sets the {@link TransferListener} that will be used.
*
* <p>The default is {@code null}.
*
* <p>See {@link DataSource#addTransferListener(TransferListener)}.
*
* @param transferListenerToUse The listener that will be used.
* @return This factory.
*/
public Factory setTransferListener(
@Nullable final TransferListener transferListenerToUse) {
this.transferListener = transferListenerToUse;
return this;
}
/**
* Sets whether we should keep the POST method and body when we have HTTP 302 redirects for
* a POST request.
*
* @param keepPostFor302RedirectsValue Whether we should keep the POST method and body when
* we have HTTP 302 redirects for a POST request.
* @return This factory.
*/
public Factory setKeepPostFor302Redirects(final boolean keepPostFor302RedirectsValue) {
this.keepPostFor302Redirects = keepPostFor302RedirectsValue;
return this;
}
@NonNull
@Override
public YoutubeHttpDataSource createDataSource() {
final YoutubeHttpDataSource dataSource = new YoutubeHttpDataSource(
connectTimeoutMs,
readTimeoutMs,
allowCrossProtocolRedirects,
rangeParameterEnabled,
rnParameterEnabled,
defaultRequestProperties,
contentTypePredicate,
keepPostFor302Redirects);
if (transferListener != null) {
dataSource.addTransferListener(transferListener);
}
return dataSource;
}
}
private static final String TAG = YoutubeHttpDataSource.class.getSimpleName();
private static final int MAX_REDIRECTS = 20; // Same limit as okhttp.
private static final int HTTP_STATUS_TEMPORARY_REDIRECT = 307;
private static final int HTTP_STATUS_PERMANENT_REDIRECT = 308;
private static final long MAX_BYTES_TO_DRAIN = 2048;
private static final String RN_PARAMETER = "&rn=";
private static final String YOUTUBE_BASE_URL = "https://www.youtube.com";
private final boolean allowCrossProtocolRedirects;
private final boolean rangeParameterEnabled;
private final boolean rnParameterEnabled;
private final int connectTimeoutMillis;
private final int readTimeoutMillis;
@Nullable
private final RequestProperties defaultRequestProperties;
private final RequestProperties requestProperties;
private final boolean keepPostFor302Redirects;
@Nullable
private final Predicate<String> contentTypePredicate;
@Nullable
private DataSpec dataSpec;
@Nullable
private HttpURLConnection connection;
@Nullable
private InputStream inputStream;
private boolean opened;
private int responseCode;
private long bytesToRead;
private long bytesRead;
private long requestNumber;
@SuppressWarnings("checkstyle:ParameterNumber")
private YoutubeHttpDataSource(final int connectTimeoutMillis,
Add support of other delivery methods than progressive HTTP (in the player only) Detailed changes: - External players: - Add a message instruction about stream selection; - Add a message when there is no stream available for external players; - Return now HLS, DASH and SmoothStreaming URL contents, in addition to progressive HTTP ones. - Player: - Support DASH, HLS and SmoothStreaming streams for videos, whether they are content URLs or the manifests themselves, in addition to progressive HTTP ones; - Use a custom HttpDataSource to play YouTube contents, based of ExoPlayer's default one, which allows better spoofing of official clients (custom user-agent and headers (depending of the client used), use of range and rn (set dynamically by the DataSource) parameters); - Fetch YouTube progressive contents as DASH streams, like official clients, support fully playback of livestreams which have ended recently and OTF streams; - Use ExoPlayer's default retries count for contents on non-fatal errors (instead of Integer.MAX_VALUE for non-live contents and 5 for live contents). - Download dialog: - Add message about support of progressive HTTP streams only for downloading; - Remove several duplicated code and update relevant usages; - Support downloading of contents with an unknown media format. - ListHelper: - Catch NumberFormatException when trying to compare two video streams between them. - Tests: - Update ListHelperTest and StreamItemAdapterTest to fix breaking changes in the extractor. - Other places: - Fixes deprecation of changes made in the extractor; - Improve some code related to the files changed. - Issues fixed and/or improved with the changes: - Seeking of PeerTube HLS streams (the duration shown was the one from the stream duration and not the one parsed, incomplete because HLS streams are fragmented MP4s with multiple sidx boxes, for which seeking is not supported by ExoPlayer) (the app now uses the HLS manifest returned for each quality, in the master playlist (not fetched and computed by the extractor)); - Crash when loading PeerTube streams with a separated audio; - Lack of some streams on some YouTube videos (OTF streams); - Loading times of YouTube streams, after a quality change or a playback start; - View count of YouTube ended livestreams interpreted as watching count (this type of streams is not interpreted anymore as livestreams); - Watchable time of YouTube ended livestreams; - Playback of SoundCloud HLS-only tracks (which cannot be downloaded anymore because the workaround which was used is being removed by SoundCloud, so it has been removed from the extractor).
2022-06-16 11:13:19 +02:00
final int readTimeoutMillis,
final boolean allowCrossProtocolRedirects,
final boolean rangeParameterEnabled,
final boolean rnParameterEnabled,
@Nullable final RequestProperties defaultRequestProperties,
@Nullable final Predicate<String> contentTypePredicate,
final boolean keepPostFor302Redirects) {
super(true);
this.connectTimeoutMillis = connectTimeoutMillis;
this.readTimeoutMillis = readTimeoutMillis;
this.allowCrossProtocolRedirects = allowCrossProtocolRedirects;
this.rangeParameterEnabled = rangeParameterEnabled;
this.rnParameterEnabled = rnParameterEnabled;
this.defaultRequestProperties = defaultRequestProperties;
this.contentTypePredicate = contentTypePredicate;
this.requestProperties = new RequestProperties();
this.keepPostFor302Redirects = keepPostFor302Redirects;
this.requestNumber = 0;
}
@Override
@Nullable
public Uri getUri() {
return connection == null ? null : Uri.parse(connection.getURL().toString());
}
@Override
public int getResponseCode() {
return connection == null || responseCode <= 0 ? -1 : responseCode;
}
@NonNull
@Override
public Map<String, List<String>> getResponseHeaders() {
if (connection == null) {
return ImmutableMap.of();
}
// connection.getHeaderFields() always contains a null key with a value like
// ["HTTP/1.1 200 OK"]. The response code is available from
// HttpURLConnection#getResponseCode() and the HTTP version is fixed when establishing the
// connection.
// DataSource#getResponseHeaders() doesn't allow null keys in the returned map, so we need
// to remove it.
// connection.getHeaderFields() returns a special unmodifiable case-insensitive Map
// so we can't just remove the null key or make a copy without the null key. Instead we
// wrap it in a ForwardingMap subclass that ignores and filters out null keys in the read
// methods.
return new NullFilteringHeadersMap(connection.getHeaderFields());
}
@Override
public void setRequestProperty(@NonNull final String name, @NonNull final String value) {
checkNotNull(name);
checkNotNull(value);
requestProperties.set(name, value);
}
@Override
public void clearRequestProperty(@NonNull final String name) {
checkNotNull(name);
requestProperties.remove(name);
}
@Override
public void clearAllRequestProperties() {
requestProperties.clear();
}
/**
* Opens the source to read the specified data.
*/
@Override
public long open(@NonNull final DataSpec dataSpecParameter) throws HttpDataSourceException {
this.dataSpec = dataSpecParameter;
bytesRead = 0;
bytesToRead = 0;
transferInitializing(dataSpecParameter);
final HttpURLConnection httpURLConnection;
final String responseMessage;
try {
this.connection = makeConnection(dataSpec);
httpURLConnection = this.connection;
responseCode = httpURLConnection.getResponseCode();
responseMessage = httpURLConnection.getResponseMessage();
} catch (final IOException e) {
closeConnectionQuietly();
throw HttpDataSourceException.createForIOException(e, dataSpec,
HttpDataSourceException.TYPE_OPEN);
}
// Check for a valid response code.
if (responseCode < 200 || responseCode > 299) {
final Map<String, List<String>> headers = httpURLConnection.getHeaderFields();
if (responseCode == 416) {
final long documentSize = HttpUtil.getDocumentSize(
httpURLConnection.getHeaderField(HttpHeaders.CONTENT_RANGE));
if (dataSpecParameter.position == documentSize) {
opened = true;
transferStarted(dataSpecParameter);
return dataSpecParameter.length != C.LENGTH_UNSET
? dataSpecParameter.length
: 0;
}
}
final InputStream errorStream = httpURLConnection.getErrorStream();
byte[] errorResponseBody;
try {
errorResponseBody = errorStream != null
? Util.toByteArray(errorStream)
: Util.EMPTY_BYTE_ARRAY;
} catch (final IOException e) {
errorResponseBody = Util.EMPTY_BYTE_ARRAY;
}
closeConnectionQuietly();
final IOException cause = responseCode == 416 ? new DataSourceException(
PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE)
: null;
throw new InvalidResponseCodeException(responseCode, responseMessage, cause, headers,
dataSpec, errorResponseBody);
}
// Check for a valid content type.
final String contentType = httpURLConnection.getContentType();
if (contentTypePredicate != null && !contentTypePredicate.apply(contentType)) {
closeConnectionQuietly();
throw new InvalidContentTypeException(contentType, dataSpecParameter);
}
final long bytesToSkip;
if (!rangeParameterEnabled) {
// 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 && dataSpecParameter.position != 0
? dataSpecParameter.position
: 0;
} else {
bytesToSkip = 0;
}
// Determine the length of the data to be read, after skipping.
final boolean isCompressed = isCompressed(httpURLConnection);
if (!isCompressed) {
if (dataSpecParameter.length != C.LENGTH_UNSET) {
bytesToRead = dataSpecParameter.length;
} else {
final long contentLength = HttpUtil.getContentLength(
httpURLConnection.getHeaderField(HttpHeaders.CONTENT_LENGTH),
httpURLConnection.getHeaderField(HttpHeaders.CONTENT_RANGE));
bytesToRead = contentLength != C.LENGTH_UNSET
? (contentLength - bytesToSkip)
: C.LENGTH_UNSET;
}
} 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. Always use
// the dataSpec length in this case.
bytesToRead = dataSpecParameter.length;
}
try {
inputStream = httpURLConnection.getInputStream();
if (isCompressed) {
inputStream = new GZIPInputStream(inputStream);
}
} catch (final IOException e) {
closeConnectionQuietly();
throw new HttpDataSourceException(e, dataSpec,
PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
HttpDataSourceException.TYPE_OPEN);
}
opened = true;
transferStarted(dataSpecParameter);
try {
skipFully(bytesToSkip, dataSpec);
} catch (final IOException e) {
closeConnectionQuietly();
if (e instanceof HttpDataSourceException) {
throw (HttpDataSourceException) e;
}
throw new HttpDataSourceException(e, dataSpec,
PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
HttpDataSourceException.TYPE_OPEN);
}
return bytesToRead;
}
@Override
public int read(@NonNull final byte[] buffer, final int offset, final int length)
throws HttpDataSourceException {
try {
return readInternal(buffer, offset, length);
} catch (final IOException e) {
throw HttpDataSourceException.createForIOException(e, castNonNull(dataSpec),
HttpDataSourceException.TYPE_READ);
}
}
@Override
public void close() throws HttpDataSourceException {
try {
final InputStream connectionInputStream = this.inputStream;
if (connectionInputStream != null) {
final long bytesRemaining = bytesToRead == C.LENGTH_UNSET
? C.LENGTH_UNSET
: bytesToRead - bytesRead;
maybeTerminateInputStream(connection, bytesRemaining);
try {
connectionInputStream.close();
} catch (final IOException e) {
throw new HttpDataSourceException(e, castNonNull(dataSpec),
PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
HttpDataSourceException.TYPE_CLOSE);
}
}
} finally {
inputStream = null;
closeConnectionQuietly();
if (opened) {
opened = false;
transferEnded();
}
}
}
@NonNull
private HttpURLConnection makeConnection(@NonNull final DataSpec dataSpecToUse)
throws IOException {
URL url = new URL(dataSpecToUse.uri.toString());
@HttpMethod int httpMethod = dataSpecToUse.httpMethod;
@Nullable byte[] httpBody = dataSpecToUse.httpBody;
final long position = dataSpecToUse.position;
final long length = dataSpecToUse.length;
final boolean allowGzip = dataSpecToUse.isFlagSet(DataSpec.FLAG_ALLOW_GZIP);
if (!allowCrossProtocolRedirects && !keepPostFor302Redirects) {
// HttpURLConnection disallows cross-protocol redirects, but otherwise performs
// redirection automatically. This is the behavior we want, so use it.
return makeConnection(url, httpMethod, httpBody, position, length, allowGzip, true,
dataSpecToUse.httpRequestHeaders);
}
// We need to handle redirects ourselves to allow cross-protocol redirects or to keep the
// POST request method for 302.
int redirectCount = 0;
while (redirectCount++ <= MAX_REDIRECTS) {
final HttpURLConnection httpURLConnection = makeConnection(url, httpMethod, httpBody,
position, length, allowGzip, false, dataSpecToUse.httpRequestHeaders);
final int httpURLConnectionResponseCode = httpURLConnection.getResponseCode();
final String location = httpURLConnection.getHeaderField("Location");
if ((httpMethod == DataSpec.HTTP_METHOD_GET || httpMethod == DataSpec.HTTP_METHOD_HEAD)
&& (httpURLConnectionResponseCode == HttpURLConnection.HTTP_MULT_CHOICE
|| httpURLConnectionResponseCode == HttpURLConnection.HTTP_MOVED_PERM
|| httpURLConnectionResponseCode == HttpURLConnection.HTTP_MOVED_TEMP
|| httpURLConnectionResponseCode == HttpURLConnection.HTTP_SEE_OTHER
|| httpURLConnectionResponseCode == HTTP_STATUS_TEMPORARY_REDIRECT
|| httpURLConnectionResponseCode == HTTP_STATUS_PERMANENT_REDIRECT)) {
httpURLConnection.disconnect();
url = handleRedirect(url, location, dataSpecToUse);
} else if (httpMethod == DataSpec.HTTP_METHOD_POST
&& (httpURLConnectionResponseCode == HttpURLConnection.HTTP_MULT_CHOICE
|| httpURLConnectionResponseCode == HttpURLConnection.HTTP_MOVED_PERM
|| httpURLConnectionResponseCode == HttpURLConnection.HTTP_MOVED_TEMP
|| httpURLConnectionResponseCode == HttpURLConnection.HTTP_SEE_OTHER)) {
httpURLConnection.disconnect();
final boolean shouldKeepPost = keepPostFor302Redirects
&& responseCode == HttpURLConnection.HTTP_MOVED_TEMP;
if (!shouldKeepPost) {
// POST request follows the redirect and is transformed into a GET request.
httpMethod = DataSpec.HTTP_METHOD_GET;
httpBody = null;
}
url = handleRedirect(url, location, dataSpecToUse);
} else {
return httpURLConnection;
}
}
// If we get here we've been redirected more times than are permitted.
throw new HttpDataSourceException(
new NoRouteToHostException("Too many redirects: " + redirectCount),
dataSpecToUse,
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
HttpDataSourceException.TYPE_OPEN);
}
/**
* Configures a connection and opens it.
*
* @param url The url to connect to.
* @param httpMethod The http method.
* @param httpBody The body data, or {@code null} if not required.
* @param position The byte offset of the requested data.
* @param length The length of the requested data, or {@link C#LENGTH_UNSET}.
* @param allowGzip Whether to allow the use of gzip.
* @param followRedirects Whether to follow redirects.
* @param requestParameters parameters (HTTP headers) to include in request.
* @return the connection opened
*/
@SuppressWarnings("checkstyle:ParameterNumber")
@NonNull
private HttpURLConnection makeConnection(
@NonNull final URL url,
@HttpMethod final int httpMethod,
@Nullable final byte[] httpBody,
final long position,
final long length,
final boolean allowGzip,
final boolean followRedirects,
final Map<String, String> requestParameters) throws IOException {
// This is the method that contains breaking changes with respect to DefaultHttpDataSource!
Add support of other delivery methods than progressive HTTP (in the player only) Detailed changes: - External players: - Add a message instruction about stream selection; - Add a message when there is no stream available for external players; - Return now HLS, DASH and SmoothStreaming URL contents, in addition to progressive HTTP ones. - Player: - Support DASH, HLS and SmoothStreaming streams for videos, whether they are content URLs or the manifests themselves, in addition to progressive HTTP ones; - Use a custom HttpDataSource to play YouTube contents, based of ExoPlayer's default one, which allows better spoofing of official clients (custom user-agent and headers (depending of the client used), use of range and rn (set dynamically by the DataSource) parameters); - Fetch YouTube progressive contents as DASH streams, like official clients, support fully playback of livestreams which have ended recently and OTF streams; - Use ExoPlayer's default retries count for contents on non-fatal errors (instead of Integer.MAX_VALUE for non-live contents and 5 for live contents). - Download dialog: - Add message about support of progressive HTTP streams only for downloading; - Remove several duplicated code and update relevant usages; - Support downloading of contents with an unknown media format. - ListHelper: - Catch NumberFormatException when trying to compare two video streams between them. - Tests: - Update ListHelperTest and StreamItemAdapterTest to fix breaking changes in the extractor. - Other places: - Fixes deprecation of changes made in the extractor; - Improve some code related to the files changed. - Issues fixed and/or improved with the changes: - Seeking of PeerTube HLS streams (the duration shown was the one from the stream duration and not the one parsed, incomplete because HLS streams are fragmented MP4s with multiple sidx boxes, for which seeking is not supported by ExoPlayer) (the app now uses the HLS manifest returned for each quality, in the master playlist (not fetched and computed by the extractor)); - Crash when loading PeerTube streams with a separated audio; - Lack of some streams on some YouTube videos (OTF streams); - Loading times of YouTube streams, after a quality change or a playback start; - View count of YouTube ended livestreams interpreted as watching count (this type of streams is not interpreted anymore as livestreams); - Watchable time of YouTube ended livestreams; - Playback of SoundCloud HLS-only tracks (which cannot be downloaded anymore because the workaround which was used is being removed by SoundCloud, so it has been removed from the extractor).
2022-06-16 11:13:19 +02:00
String requestUrl = url.toString();
// Don't add the request number parameter if it has been already added (for instance in
// DASH manifests) or if that's not a videoplayback URL
final boolean isVideoPlaybackUrl = url.getPath().startsWith("/videoplayback");
if (isVideoPlaybackUrl && rnParameterEnabled && !requestUrl.contains(RN_PARAMETER)) {
requestUrl += RN_PARAMETER + requestNumber;
++requestNumber;
}
if (rangeParameterEnabled && isVideoPlaybackUrl) {
final String rangeParameterBuilt = buildRangeParameter(position, length);
if (rangeParameterBuilt != null) {
requestUrl += rangeParameterBuilt;
}
}
final HttpURLConnection httpURLConnection = openConnection(new URL(requestUrl));
httpURLConnection.setConnectTimeout(connectTimeoutMillis);
httpURLConnection.setReadTimeout(readTimeoutMillis);
final Map<String, String> requestHeaders = new HashMap<>();
if (defaultRequestProperties != null) {
requestHeaders.putAll(defaultRequestProperties.getSnapshot());
}
requestHeaders.putAll(requestProperties.getSnapshot());
requestHeaders.putAll(requestParameters);
for (final Map.Entry<String, String> property : requestHeaders.entrySet()) {
httpURLConnection.setRequestProperty(property.getKey(), property.getValue());
}
if (!rangeParameterEnabled) {
final String rangeHeader = buildRangeRequestHeader(position, length);
if (rangeHeader != null) {
httpURLConnection.setRequestProperty(HttpHeaders.RANGE, rangeHeader);
}
}
if (isWebStreamingUrl(requestUrl)
|| isTvHtml5SimplyEmbeddedPlayerStreamingUrl(requestUrl)) {
httpURLConnection.setRequestProperty(HttpHeaders.ORIGIN, YOUTUBE_BASE_URL);
httpURLConnection.setRequestProperty(HttpHeaders.REFERER, YOUTUBE_BASE_URL);
httpURLConnection.setRequestProperty(HttpHeaders.SEC_FETCH_DEST, "empty");
httpURLConnection.setRequestProperty(HttpHeaders.SEC_FETCH_MODE, "cors");
httpURLConnection.setRequestProperty(HttpHeaders.SEC_FETCH_SITE, "cross-site");
}
httpURLConnection.setRequestProperty(HttpHeaders.TE, "trailers");
final boolean isAndroidStreamingUrl = isAndroidStreamingUrl(requestUrl);
final boolean isIosStreamingUrl = isIosStreamingUrl(requestUrl);
if (isAndroidStreamingUrl) {
Add support of other delivery methods than progressive HTTP (in the player only) Detailed changes: - External players: - Add a message instruction about stream selection; - Add a message when there is no stream available for external players; - Return now HLS, DASH and SmoothStreaming URL contents, in addition to progressive HTTP ones. - Player: - Support DASH, HLS and SmoothStreaming streams for videos, whether they are content URLs or the manifests themselves, in addition to progressive HTTP ones; - Use a custom HttpDataSource to play YouTube contents, based of ExoPlayer's default one, which allows better spoofing of official clients (custom user-agent and headers (depending of the client used), use of range and rn (set dynamically by the DataSource) parameters); - Fetch YouTube progressive contents as DASH streams, like official clients, support fully playback of livestreams which have ended recently and OTF streams; - Use ExoPlayer's default retries count for contents on non-fatal errors (instead of Integer.MAX_VALUE for non-live contents and 5 for live contents). - Download dialog: - Add message about support of progressive HTTP streams only for downloading; - Remove several duplicated code and update relevant usages; - Support downloading of contents with an unknown media format. - ListHelper: - Catch NumberFormatException when trying to compare two video streams between them. - Tests: - Update ListHelperTest and StreamItemAdapterTest to fix breaking changes in the extractor. - Other places: - Fixes deprecation of changes made in the extractor; - Improve some code related to the files changed. - Issues fixed and/or improved with the changes: - Seeking of PeerTube HLS streams (the duration shown was the one from the stream duration and not the one parsed, incomplete because HLS streams are fragmented MP4s with multiple sidx boxes, for which seeking is not supported by ExoPlayer) (the app now uses the HLS manifest returned for each quality, in the master playlist (not fetched and computed by the extractor)); - Crash when loading PeerTube streams with a separated audio; - Lack of some streams on some YouTube videos (OTF streams); - Loading times of YouTube streams, after a quality change or a playback start; - View count of YouTube ended livestreams interpreted as watching count (this type of streams is not interpreted anymore as livestreams); - Watchable time of YouTube ended livestreams; - Playback of SoundCloud HLS-only tracks (which cannot be downloaded anymore because the workaround which was used is being removed by SoundCloud, so it has been removed from the extractor).
2022-06-16 11:13:19 +02:00
// Improvement which may be done: find the content country used to request YouTube
// contents to add it in the user agent instead of using the default
httpURLConnection.setRequestProperty(HttpHeaders.USER_AGENT,
getAndroidUserAgent(null));
} else if (isIosStreamingUrl) {
Add support of other delivery methods than progressive HTTP (in the player only) Detailed changes: - External players: - Add a message instruction about stream selection; - Add a message when there is no stream available for external players; - Return now HLS, DASH and SmoothStreaming URL contents, in addition to progressive HTTP ones. - Player: - Support DASH, HLS and SmoothStreaming streams for videos, whether they are content URLs or the manifests themselves, in addition to progressive HTTP ones; - Use a custom HttpDataSource to play YouTube contents, based of ExoPlayer's default one, which allows better spoofing of official clients (custom user-agent and headers (depending of the client used), use of range and rn (set dynamically by the DataSource) parameters); - Fetch YouTube progressive contents as DASH streams, like official clients, support fully playback of livestreams which have ended recently and OTF streams; - Use ExoPlayer's default retries count for contents on non-fatal errors (instead of Integer.MAX_VALUE for non-live contents and 5 for live contents). - Download dialog: - Add message about support of progressive HTTP streams only for downloading; - Remove several duplicated code and update relevant usages; - Support downloading of contents with an unknown media format. - ListHelper: - Catch NumberFormatException when trying to compare two video streams between them. - Tests: - Update ListHelperTest and StreamItemAdapterTest to fix breaking changes in the extractor. - Other places: - Fixes deprecation of changes made in the extractor; - Improve some code related to the files changed. - Issues fixed and/or improved with the changes: - Seeking of PeerTube HLS streams (the duration shown was the one from the stream duration and not the one parsed, incomplete because HLS streams are fragmented MP4s with multiple sidx boxes, for which seeking is not supported by ExoPlayer) (the app now uses the HLS manifest returned for each quality, in the master playlist (not fetched and computed by the extractor)); - Crash when loading PeerTube streams with a separated audio; - Lack of some streams on some YouTube videos (OTF streams); - Loading times of YouTube streams, after a quality change or a playback start; - View count of YouTube ended livestreams interpreted as watching count (this type of streams is not interpreted anymore as livestreams); - Watchable time of YouTube ended livestreams; - Playback of SoundCloud HLS-only tracks (which cannot be downloaded anymore because the workaround which was used is being removed by SoundCloud, so it has been removed from the extractor).
2022-06-16 11:13:19 +02:00
httpURLConnection.setRequestProperty(HttpHeaders.USER_AGENT,
getIosUserAgent(null));
} else {
// non-mobile user agent
httpURLConnection.setRequestProperty(HttpHeaders.USER_AGENT, DownloaderImpl.USER_AGENT);
Add support of other delivery methods than progressive HTTP (in the player only) Detailed changes: - External players: - Add a message instruction about stream selection; - Add a message when there is no stream available for external players; - Return now HLS, DASH and SmoothStreaming URL contents, in addition to progressive HTTP ones. - Player: - Support DASH, HLS and SmoothStreaming streams for videos, whether they are content URLs or the manifests themselves, in addition to progressive HTTP ones; - Use a custom HttpDataSource to play YouTube contents, based of ExoPlayer's default one, which allows better spoofing of official clients (custom user-agent and headers (depending of the client used), use of range and rn (set dynamically by the DataSource) parameters); - Fetch YouTube progressive contents as DASH streams, like official clients, support fully playback of livestreams which have ended recently and OTF streams; - Use ExoPlayer's default retries count for contents on non-fatal errors (instead of Integer.MAX_VALUE for non-live contents and 5 for live contents). - Download dialog: - Add message about support of progressive HTTP streams only for downloading; - Remove several duplicated code and update relevant usages; - Support downloading of contents with an unknown media format. - ListHelper: - Catch NumberFormatException when trying to compare two video streams between them. - Tests: - Update ListHelperTest and StreamItemAdapterTest to fix breaking changes in the extractor. - Other places: - Fixes deprecation of changes made in the extractor; - Improve some code related to the files changed. - Issues fixed and/or improved with the changes: - Seeking of PeerTube HLS streams (the duration shown was the one from the stream duration and not the one parsed, incomplete because HLS streams are fragmented MP4s with multiple sidx boxes, for which seeking is not supported by ExoPlayer) (the app now uses the HLS manifest returned for each quality, in the master playlist (not fetched and computed by the extractor)); - Crash when loading PeerTube streams with a separated audio; - Lack of some streams on some YouTube videos (OTF streams); - Loading times of YouTube streams, after a quality change or a playback start; - View count of YouTube ended livestreams interpreted as watching count (this type of streams is not interpreted anymore as livestreams); - Watchable time of YouTube ended livestreams; - Playback of SoundCloud HLS-only tracks (which cannot be downloaded anymore because the workaround which was used is being removed by SoundCloud, so it has been removed from the extractor).
2022-06-16 11:13:19 +02:00
}
httpURLConnection.setRequestProperty(HttpHeaders.ACCEPT_ENCODING,
allowGzip ? "gzip" : "identity");
httpURLConnection.setInstanceFollowRedirects(followRedirects);
httpURLConnection.setDoOutput(httpBody != null);
// Mobile clients uses POST requests to fetch contents
httpURLConnection.setRequestMethod(isAndroidStreamingUrl || isIosStreamingUrl
Add support of other delivery methods than progressive HTTP (in the player only) Detailed changes: - External players: - Add a message instruction about stream selection; - Add a message when there is no stream available for external players; - Return now HLS, DASH and SmoothStreaming URL contents, in addition to progressive HTTP ones. - Player: - Support DASH, HLS and SmoothStreaming streams for videos, whether they are content URLs or the manifests themselves, in addition to progressive HTTP ones; - Use a custom HttpDataSource to play YouTube contents, based of ExoPlayer's default one, which allows better spoofing of official clients (custom user-agent and headers (depending of the client used), use of range and rn (set dynamically by the DataSource) parameters); - Fetch YouTube progressive contents as DASH streams, like official clients, support fully playback of livestreams which have ended recently and OTF streams; - Use ExoPlayer's default retries count for contents on non-fatal errors (instead of Integer.MAX_VALUE for non-live contents and 5 for live contents). - Download dialog: - Add message about support of progressive HTTP streams only for downloading; - Remove several duplicated code and update relevant usages; - Support downloading of contents with an unknown media format. - ListHelper: - Catch NumberFormatException when trying to compare two video streams between them. - Tests: - Update ListHelperTest and StreamItemAdapterTest to fix breaking changes in the extractor. - Other places: - Fixes deprecation of changes made in the extractor; - Improve some code related to the files changed. - Issues fixed and/or improved with the changes: - Seeking of PeerTube HLS streams (the duration shown was the one from the stream duration and not the one parsed, incomplete because HLS streams are fragmented MP4s with multiple sidx boxes, for which seeking is not supported by ExoPlayer) (the app now uses the HLS manifest returned for each quality, in the master playlist (not fetched and computed by the extractor)); - Crash when loading PeerTube streams with a separated audio; - Lack of some streams on some YouTube videos (OTF streams); - Loading times of YouTube streams, after a quality change or a playback start; - View count of YouTube ended livestreams interpreted as watching count (this type of streams is not interpreted anymore as livestreams); - Watchable time of YouTube ended livestreams; - Playback of SoundCloud HLS-only tracks (which cannot be downloaded anymore because the workaround which was used is being removed by SoundCloud, so it has been removed from the extractor).
2022-06-16 11:13:19 +02:00
? "POST"
: DataSpec.getStringForHttpMethod(httpMethod));
if (httpBody != null) {
httpURLConnection.setFixedLengthStreamingMode(httpBody.length);
httpURLConnection.connect();
final OutputStream os = httpURLConnection.getOutputStream();
os.write(httpBody);
os.close();
} else {
httpURLConnection.connect();
}
return httpURLConnection;
}
/**
* Creates an {@link HttpURLConnection} that is connected with the {@code url}.
*
* @param url the {@link URL} to create an {@link HttpURLConnection}
* @return an {@link HttpURLConnection} created with the {@code url}
*/
private HttpURLConnection openConnection(@NonNull final URL url) throws IOException {
return (HttpURLConnection) url.openConnection();
}
/**
* Handles a redirect.
*
* @param originalUrl The original URL.
* @param location The Location header in the response. May be {@code null}.
* @param dataSpecToHandleRedirect The {@link DataSpec}.
* @return The next URL.
* @throws HttpDataSourceException If redirection isn't possible.
*/
@NonNull
private URL handleRedirect(final URL originalUrl,
@Nullable final String location,
final DataSpec dataSpecToHandleRedirect)
throws HttpDataSourceException {
if (location == null) {
throw new HttpDataSourceException("Null location redirect", dataSpecToHandleRedirect,
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
HttpDataSourceException.TYPE_OPEN);
}
// Form the new url.
final URL url;
try {
url = new URL(originalUrl, location);
} catch (final MalformedURLException e) {
throw new HttpDataSourceException(e, dataSpecToHandleRedirect,
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
HttpDataSourceException.TYPE_OPEN);
}
// Check that the protocol of the new url is supported.
final String protocol = url.getProtocol();
if (!"https".equals(protocol) && !"http".equals(protocol)) {
throw new HttpDataSourceException("Unsupported protocol redirect: " + protocol,
dataSpecToHandleRedirect,
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
HttpDataSourceException.TYPE_OPEN);
}
if (!allowCrossProtocolRedirects && !protocol.equals(originalUrl.getProtocol())) {
throw new HttpDataSourceException(
"Disallowed cross-protocol redirect ("
+ originalUrl.getProtocol()
+ " to "
+ protocol
+ ")",
dataSpecToHandleRedirect,
PlaybackException.ERROR_CODE_IO_NETWORK_CONNECTION_FAILED,
HttpDataSourceException.TYPE_OPEN);
}
return url;
}
/**
* Attempts to skip the specified number of bytes in full.
*
* @param bytesToSkip The number of bytes to skip.
* @param dataSpecToUse The {@link DataSpec}.
* @throws IOException If the thread is interrupted during the operation, or if the data ended
* before skipping the specified number of bytes.
*/
@SuppressWarnings("checkstyle:FinalParameters")
private void skipFully(long bytesToSkip, final DataSpec dataSpecToUse) throws IOException {
if (bytesToSkip == 0) {
return;
}
final byte[] skipBuffer = new byte[4096];
while (bytesToSkip > 0) {
final int readLength = (int) min(bytesToSkip, skipBuffer.length);
final int read = castNonNull(inputStream).read(skipBuffer, 0, readLength);
if (Thread.currentThread().isInterrupted()) {
throw new HttpDataSourceException(
new InterruptedIOException(),
dataSpecToUse,
PlaybackException.ERROR_CODE_IO_UNSPECIFIED,
HttpDataSourceException.TYPE_OPEN);
}
if (read == -1) {
throw new HttpDataSourceException(
dataSpecToUse,
PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE,
HttpDataSourceException.TYPE_OPEN);
}
bytesToSkip -= 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.
* </p>
*
* @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.
*/
@SuppressWarnings("checkstyle:FinalParameters")
private int readInternal(final byte[] buffer, final int offset, int readLength)
throws IOException {
if (readLength == 0) {
return 0;
}
if (bytesToRead != C.LENGTH_UNSET) {
final long bytesRemaining = bytesToRead - bytesRead;
if (bytesRemaining == 0) {
return C.RESULT_END_OF_INPUT;
}
readLength = (int) min(readLength, bytesRemaining);
}
final int read = castNonNull(inputStream).read(buffer, offset, readLength);
if (read == -1) {
return C.RESULT_END_OF_INPUT;
}
bytesRead += read;
bytesTransferred(read);
return read;
}
/**
* On platform API levels 19 and 20, okhttp's implementation of {@link InputStream#close} can
* block for a long time if the stream has a lot of data remaining. Call this method before
* closing the input stream to make a best effort to cause the input stream to encounter an
* unexpected end of input, working around this issue. On other platform API levels, the method
* does nothing.
*
* @param connection The connection whose {@link InputStream} should be terminated.
* @param bytesRemaining The number of bytes remaining to be read from the input stream if its
* length is known. {@link C#LENGTH_UNSET} otherwise.
*/
private static void maybeTerminateInputStream(@Nullable final HttpURLConnection connection,
final long bytesRemaining) {
if (connection == null || Util.SDK_INT < 19 || Util.SDK_INT > 20) {
return;
}
try {
final InputStream inputStream = connection.getInputStream();
if (bytesRemaining == C.LENGTH_UNSET) {
// If the input stream has already ended, do nothing. The socket may be re-used.
if (inputStream.read() == -1) {
return;
}
} else if (bytesRemaining <= MAX_BYTES_TO_DRAIN) {
// There isn't much data left. Prefer to allow it to drain, which may allow the
// socket to be re-used.
return;
}
final String className = inputStream.getClass().getName();
if ("com.android.okhttp.internal.http.HttpTransport$ChunkedInputStream"
.equals(className)
|| "com.android.okhttp.internal.http.HttpTransport$FixedLengthInputStream"
.equals(className)) {
final Class<?> superclass = inputStream.getClass().getSuperclass();
final Method unexpectedEndOfInput = checkNotNull(superclass).getDeclaredMethod(
"unexpectedEndOfInput");
unexpectedEndOfInput.setAccessible(true);
unexpectedEndOfInput.invoke(inputStream);
}
} catch (final Exception e) {
// If an IOException then the connection didn't ever have an input stream, or it was
// closed already. If another type of exception then something went wrong, most likely
// the device isn't using okhttp.
}
}
/**
* Closes the current connection quietly, if there is one.
*/
private void closeConnectionQuietly() {
if (connection != null) {
try {
connection.disconnect();
} catch (final Exception e) {
Log.e(TAG, "Unexpected error while disconnecting", e);
}
connection = null;
}
}
private static boolean isCompressed(@NonNull final HttpURLConnection connection) {
final String contentEncoding = connection.getHeaderField("Content-Encoding");
return "gzip".equalsIgnoreCase(contentEncoding);
}
/**
* Builds a {@code range} parameter for the given position and length.
*
* <p>
* To fetch its contents, YouTube use range requests which append a {@code range} parameter
* to videoplayback URLs instead of the {@code Range} header (even if the server respond
* correctly when requesting a range of a ressouce with it).
* </p>
*
* <p>
* The parameter works in the same way as the header.
* </p>
*
* @param position The request position.
* @param length The request length, or {@link C#LENGTH_UNSET} if the request is unbounded.
* @return The corresponding {@code range} parameter, or {@code null} if this parameter is
* unnecessary because the whole resource is being requested.
*/
@Nullable
private static String buildRangeParameter(final long position, final long length) {
if (position == 0 && length == C.LENGTH_UNSET) {
return null;
}
final StringBuilder rangeParameter = new StringBuilder();
rangeParameter.append("&range=");
rangeParameter.append(position);
rangeParameter.append("-");
if (length != C.LENGTH_UNSET) {
rangeParameter.append(position + length - 1);
}
return rangeParameter.toString();
}
private static final class NullFilteringHeadersMap
extends ForwardingMap<String, List<String>> {
private final Map<String, List<String>> headers;
NullFilteringHeadersMap(final Map<String, List<String>> headers) {
this.headers = headers;
}
@NonNull
@Override
protected Map<String, List<String>> delegate() {
return headers;
}
@Override
public boolean containsKey(@Nullable final Object key) {
return key != null && super.containsKey(key);
}
@Nullable
@Override
public List<String> get(@Nullable final Object key) {
return key == null ? null : super.get(key);
}
@NonNull
@Override
public Set<String> keySet() {
return Sets.filter(super.keySet(), Objects::nonNull);
}
@NonNull
@Override
public Set<Entry<String, List<String>>> entrySet() {
return Sets.filter(super.entrySet(), entry -> entry.getKey() != null);
}
@Override
public int size() {
return super.size() - (super.containsKey(null) ? 1 : 0);
}
@Override
public boolean isEmpty() {
return super.isEmpty() || (super.size() == 1 && super.containsKey(null));
}
@Override
public boolean containsValue(@Nullable final Object value) {
return super.standardContainsValue(value);
}
@Override
public boolean equals(@Nullable final Object object) {
return object != null && super.standardEquals(object);
}
@Override
public int hashCode() {
return super.standardHashCode();
}
}
}