blob: 5d99335e7709f6f3dea12a3e72b58f4864584bd8 [file] [log] [blame]
/*
* Copyright (C) 2017 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.upstream.cache;
import android.net.Uri;
import android.util.Pair;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.google.android.exoplayer2.C;
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.util.Assertions;
import com.google.android.exoplayer2.util.PriorityTaskManager;
import com.google.android.exoplayer2.util.Util;
import java.io.EOFException;
import java.io.IOException;
import java.util.NavigableSet;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Caching related utility methods.
*/
public final class CacheUtil {
/** Receives progress updates during cache operations. */
public interface ProgressListener {
/**
* Called when progress is made during a cache operation.
*
* @param requestLength The length of the content being cached in bytes, or {@link
* C#LENGTH_UNSET} if unknown.
* @param bytesCached The number of bytes that are cached.
* @param newBytesCached The number of bytes that have been newly cached since the last progress
* update.
*/
void onProgress(long requestLength, long bytesCached, long newBytesCached);
}
/** Default buffer size to be used while caching. */
public static final int DEFAULT_BUFFER_SIZE_BYTES = 128 * 1024;
/** Default {@link CacheKeyFactory}. */
public static final CacheKeyFactory DEFAULT_CACHE_KEY_FACTORY =
(dataSpec) -> dataSpec.key != null ? dataSpec.key : generateKey(dataSpec.uri);
/**
* Generates a cache key out of the given {@link Uri}.
*
* @param uri Uri of a content which the requested key is for.
*/
public static String generateKey(Uri uri) {
return uri.toString();
}
/**
* Queries the cache to obtain the request length and the number of bytes already cached for a
* given {@link DataSpec}.
*
* @param dataSpec Defines the data to be checked.
* @param cache A {@link Cache} which has the data.
* @param cacheKeyFactory An optional factory for cache keys.
* @return A pair containing the request length and the number of bytes that are already cached.
*/
public static Pair<Long, Long> getCached(
DataSpec dataSpec, Cache cache, @Nullable CacheKeyFactory cacheKeyFactory) {
String key = buildCacheKey(dataSpec, cacheKeyFactory);
long position = dataSpec.position;
long requestLength = getRequestLength(dataSpec, cache, key);
long bytesAlreadyCached = 0;
long bytesLeft = requestLength;
while (bytesLeft != 0) {
long blockLength =
cache.getCachedLength(
key, position, bytesLeft != C.LENGTH_UNSET ? bytesLeft : Long.MAX_VALUE);
if (blockLength > 0) {
bytesAlreadyCached += blockLength;
} else {
blockLength = -blockLength;
if (blockLength == Long.MAX_VALUE) {
break;
}
}
position += blockLength;
bytesLeft -= bytesLeft == C.LENGTH_UNSET ? 0 : blockLength;
}
return Pair.create(requestLength, bytesAlreadyCached);
}
/**
* Caches the data defined by {@code dataSpec}, skipping already cached data. Caching stops early
* if the end of the input is reached.
*
* <p>This method may be slow and shouldn't normally be called on the main thread.
*
* @param cache A {@link Cache} to store the data.
* @param dataSpec Defines the data to be cached.
* @param upstreamDataSource A {@link DataSource} for reading data not in the cache.
* @param progressListener A listener to receive progress updates, or {@code null}.
* @param isCanceled An optional flag that will interrupt caching if set to true.
* @throws IOException If an error occurs reading from the source.
* @throws InterruptedException If the thread was interrupted directly or via {@code isCanceled}.
*/
@WorkerThread
public static void cache(
Cache cache,
DataSpec dataSpec,
DataSource upstreamDataSource,
@Nullable ProgressListener progressListener,
@Nullable AtomicBoolean isCanceled)
throws IOException, InterruptedException {
cache(
new CacheDataSource(cache, upstreamDataSource),
dataSpec,
progressListener,
isCanceled,
/* enableEOFException= */ false,
new byte[DEFAULT_BUFFER_SIZE_BYTES]);
}
/**
* Caches the data defined by {@code dataSpec}, skipping already cached data. Caching stops early
* if end of input is reached and {@code enableEOFException} is false.
*
* <p>If {@code dataSource} has a {@link PriorityTaskManager}, then it's the responsibility of the
* calling code to call {@link PriorityTaskManager#add} to register with the manager before
* calling this method, and to call {@link PriorityTaskManager#remove} afterwards to unregister.
*
* <p>This method may be slow and shouldn't normally be called on the main thread.
*
* @param dataSource A {@link CacheDataSource} to be used for caching the data.
* @param dataSpec Defines the data to be cached.
* @param progressListener A listener to receive progress updates, or {@code null}.
* @param isCanceled An optional flag that will interrupt caching if set to true.
* @param enableEOFException Whether to throw an {@link EOFException} if end of input has been
* reached unexpectedly.
* @param temporaryBuffer A temporary buffer to be used during caching.
* @throws IOException If an error occurs reading from the source.
* @throws InterruptedException If the thread was interrupted directly or via {@code isCanceled}.
*/
@WorkerThread
public static void cache(
CacheDataSource dataSource,
DataSpec dataSpec,
@Nullable ProgressListener progressListener,
@Nullable AtomicBoolean isCanceled,
boolean enableEOFException,
byte[] temporaryBuffer)
throws IOException, InterruptedException {
Assertions.checkNotNull(dataSource);
Assertions.checkNotNull(temporaryBuffer);
Cache cache = dataSource.getCache();
CacheKeyFactory cacheKeyFactory = dataSource.getCacheKeyFactory();
String key = buildCacheKey(dataSpec, cacheKeyFactory);
long bytesLeft;
@Nullable ProgressNotifier progressNotifier = null;
if (progressListener != null) {
progressNotifier = new ProgressNotifier(progressListener);
Pair<Long, Long> lengthAndBytesAlreadyCached = getCached(dataSpec, cache, cacheKeyFactory);
progressNotifier.init(lengthAndBytesAlreadyCached.first, lengthAndBytesAlreadyCached.second);
bytesLeft = lengthAndBytesAlreadyCached.first;
} else {
bytesLeft = getRequestLength(dataSpec, cache, key);
}
long position = dataSpec.position;
boolean lengthUnset = bytesLeft == C.LENGTH_UNSET;
while (bytesLeft != 0) {
throwExceptionIfInterruptedOrCancelled(isCanceled);
long blockLength =
cache.getCachedLength(key, position, lengthUnset ? Long.MAX_VALUE : bytesLeft);
if (blockLength > 0) {
// Skip already cached data.
} else {
// There is a hole in the cache which is at least "-blockLength" long.
blockLength = -blockLength;
long length = blockLength == Long.MAX_VALUE ? C.LENGTH_UNSET : blockLength;
boolean isLastBlock = length == bytesLeft;
long read =
readAndDiscard(
dataSpec,
position,
length,
dataSource,
isCanceled,
progressNotifier,
isLastBlock,
temporaryBuffer);
if (read < blockLength) {
// Reached to the end of the data.
if (enableEOFException && !lengthUnset) {
throw new EOFException();
}
break;
}
}
position += blockLength;
if (!lengthUnset) {
bytesLeft -= blockLength;
}
}
}
private static long getRequestLength(DataSpec dataSpec, Cache cache, String key) {
if (dataSpec.length != C.LENGTH_UNSET) {
return dataSpec.length;
} else {
long contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(key));
return contentLength == C.LENGTH_UNSET ? C.LENGTH_UNSET : contentLength - dataSpec.position;
}
}
/**
* Reads and discards all data specified by the {@code dataSpec}.
*
* @param dataSpec Defines the data to be read. The {@code position} and {@code length} fields are
* overwritten by the following parameters.
* @param position The position of the data to be read.
* @param length Length of the data to be read, or {@link C#LENGTH_UNSET} if it is unknown.
* @param dataSource The {@link CacheDataSource} to read the data from.
* @param isCanceled An optional flag that will interrupt caching if set to true.
* @param progressNotifier A notifier through which to report progress updates, or {@code null}.
* @param isLastBlock Whether this read block is the last block of the content.
* @param temporaryBuffer A temporary buffer to be used during caching.
* @return Number of read bytes, or 0 if no data is available because the end of the opened range
* has been reached.
*/
private static long readAndDiscard(
DataSpec dataSpec,
long position,
long length,
CacheDataSource dataSource,
@Nullable AtomicBoolean isCanceled,
@Nullable ProgressNotifier progressNotifier,
boolean isLastBlock,
byte[] temporaryBuffer)
throws IOException, InterruptedException {
long positionOffset = position - dataSpec.position;
long initialPositionOffset = positionOffset;
long endOffset = length != C.LENGTH_UNSET ? positionOffset + length : C.POSITION_UNSET;
@Nullable PriorityTaskManager priorityTaskManager = dataSource.getUpstreamPriorityTaskManager();
while (true) {
if (priorityTaskManager != null) {
// Wait for any other thread with higher priority to finish its job.
priorityTaskManager.proceed(dataSource.getUpstreamPriority());
}
throwExceptionIfInterruptedOrCancelled(isCanceled);
try {
long resolvedLength = C.LENGTH_UNSET;
boolean isDataSourceOpen = false;
if (endOffset != C.POSITION_UNSET) {
// If a specific length is given, first try to open the data source for that length to
// avoid more data then required to be requested. If the given length exceeds the end of
// input we will get a "position out of range" error. In that case try to open the source
// again with unset length.
try {
resolvedLength =
dataSource.open(dataSpec.subrange(positionOffset, endOffset - positionOffset));
isDataSourceOpen = true;
} catch (IOException exception) {
if (!isLastBlock || !isCausedByPositionOutOfRange(exception)) {
throw exception;
}
Util.closeQuietly(dataSource);
}
}
if (!isDataSourceOpen) {
resolvedLength = dataSource.open(dataSpec.subrange(positionOffset, C.LENGTH_UNSET));
}
if (isLastBlock && progressNotifier != null && resolvedLength != C.LENGTH_UNSET) {
progressNotifier.onRequestLengthResolved(positionOffset + resolvedLength);
}
while (positionOffset != endOffset) {
throwExceptionIfInterruptedOrCancelled(isCanceled);
int bytesRead =
dataSource.read(
temporaryBuffer,
0,
endOffset != C.POSITION_UNSET
? (int) Math.min(temporaryBuffer.length, endOffset - positionOffset)
: temporaryBuffer.length);
if (bytesRead == C.RESULT_END_OF_INPUT) {
if (progressNotifier != null) {
progressNotifier.onRequestLengthResolved(positionOffset);
}
break;
}
positionOffset += bytesRead;
if (progressNotifier != null) {
progressNotifier.onBytesCached(bytesRead);
}
}
return positionOffset - initialPositionOffset;
} catch (PriorityTaskManager.PriorityTooLowException exception) {
// catch and try again
} finally {
Util.closeQuietly(dataSource);
}
}
}
/**
* Removes all of the data specified by the {@code dataSpec}.
*
* <p>This methods blocks until the operation is complete.
*
* @param dataSpec Defines the data to be removed.
* @param cache A {@link Cache} to store the data.
* @param cacheKeyFactory An optional factory for cache keys.
*/
@WorkerThread
public static void remove(
DataSpec dataSpec, Cache cache, @Nullable CacheKeyFactory cacheKeyFactory) {
remove(cache, buildCacheKey(dataSpec, cacheKeyFactory));
}
/**
* Removes all of the data specified by the {@code key}.
*
* <p>This methods blocks until the operation is complete.
*
* @param cache A {@link Cache} to store the data.
* @param key The key whose data should be removed.
*/
@WorkerThread
public static void remove(Cache cache, String key) {
NavigableSet<CacheSpan> cachedSpans = cache.getCachedSpans(key);
for (CacheSpan cachedSpan : cachedSpans) {
try {
cache.removeSpan(cachedSpan);
} catch (Cache.CacheException e) {
// Do nothing.
}
}
}
/* package */ static boolean isCausedByPositionOutOfRange(IOException e) {
@Nullable Throwable cause = e;
while (cause != null) {
if (cause instanceof DataSourceException) {
int reason = ((DataSourceException) cause).reason;
if (reason == DataSourceException.POSITION_OUT_OF_RANGE) {
return true;
}
}
cause = cause.getCause();
}
return false;
}
private static String buildCacheKey(
DataSpec dataSpec, @Nullable CacheKeyFactory cacheKeyFactory) {
return (cacheKeyFactory != null ? cacheKeyFactory : DEFAULT_CACHE_KEY_FACTORY)
.buildCacheKey(dataSpec);
}
private static void throwExceptionIfInterruptedOrCancelled(@Nullable AtomicBoolean isCanceled)
throws InterruptedException {
if (Thread.interrupted() || (isCanceled != null && isCanceled.get())) {
throw new InterruptedException();
}
}
private CacheUtil() {}
private static final class ProgressNotifier {
/** The listener to notify when progress is made. */
private final ProgressListener listener;
/** The length of the content being cached in bytes, or {@link C#LENGTH_UNSET} if unknown. */
private long requestLength;
/** The number of bytes that are cached. */
private long bytesCached;
public ProgressNotifier(ProgressListener listener) {
this.listener = listener;
}
public void init(long requestLength, long bytesCached) {
this.requestLength = requestLength;
this.bytesCached = bytesCached;
listener.onProgress(requestLength, bytesCached, /* newBytesCached= */ 0);
}
public void onRequestLengthResolved(long requestLength) {
if (this.requestLength == C.LENGTH_UNSET && requestLength != C.LENGTH_UNSET) {
this.requestLength = requestLength;
listener.onProgress(requestLength, bytesCached, /* newBytesCached= */ 0);
}
}
public void onBytesCached(long newBytesCached) {
bytesCached += newBytesCached;
listener.onProgress(requestLength, bytesCached, newBytesCached);
}
}
}