/* * 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.cache; import com.google.android.exoplayer.C; import com.google.android.exoplayer.upstream.DataSink; import com.google.android.exoplayer.upstream.DataSource; import com.google.android.exoplayer.upstream.DataSpec; import com.google.android.exoplayer.upstream.FileDataSource; import com.google.android.exoplayer.upstream.TeeDataSource; import com.google.android.exoplayer.upstream.cache.CacheDataSink.CacheDataSinkException; import com.google.android.exoplayer.util.Assertions; import android.net.Uri; import java.io.IOException; /** * A {@link DataSource} that reads and writes a {@link Cache}. Requests are fulfilled from the cache * when possible. When data is not cached it is requested from an upstream {@link DataSource} and * written into the cache. */ public final class CacheDataSource implements DataSource { /** * Interface definition for a callback to be notified of {@link CacheDataSource} events. */ public interface EventListener { /** * Invoked when bytes have been read from the cache. * * @param cacheSizeBytes Current cache size in bytes. * @param cachedBytesRead Total bytes read from the cache since this method was last invoked. */ void onCachedBytesRead(long cacheSizeBytes, long cachedBytesRead); } private final Cache cache; private final DataSource cacheReadDataSource; private final DataSource cacheWriteDataSource; private final DataSource upstreamDataSource; private final EventListener eventListener; private final boolean blockOnCache; private final boolean ignoreCacheOnError; private DataSource currentDataSource; private Uri uri; private String key; private long readPosition; private long bytesRemaining; private CacheSpan lockedSpan; private boolean ignoreCache; private long totalCachedBytesRead; /** * Constructs an instance with default {@link DataSource} and {@link DataSink} instances for * reading and writing the cache. */ public CacheDataSource(Cache cache, DataSource upstream, boolean blockOnCache, boolean ignoreCacheOnError) { this(cache, upstream, blockOnCache, ignoreCacheOnError, Long.MAX_VALUE); } /** * Constructs an instance with default {@link DataSource} and {@link DataSink} instances for * reading and writing the cache. The sink is configured to fragment data such that no single * cache file is greater than maxCacheFileSize bytes. */ public CacheDataSource(Cache cache, DataSource upstream, boolean blockOnCache, boolean ignoreCacheOnError, long maxCacheFileSize) { this(cache, upstream, new FileDataSource(), new CacheDataSink(cache, maxCacheFileSize), blockOnCache, ignoreCacheOnError, null); } /** * Constructs an instance with arbitrary {@link DataSource} and {@link DataSink} instances for * reading and writing the cache. One use of this constructor is to allow data to be transformed * before it is written to disk. * * @param cache The cache. * @param upstream A {@link DataSource} for reading data not in the cache. * @param cacheReadDataSource A {@link DataSource} for reading data from the cache. * @param cacheWriteDataSink A {@link DataSink} for writing data to the cache. * @param blockOnCache A flag indicating whether we will block reads if the cache key is locked. * If this flag is false, then we will read from upstream if the cache key is locked. * @param ignoreCacheOnError Whether the cache is bypassed following any cache related error. If * true, then cache related exceptions may be thrown for one cycle of open, read and close * calls. Subsequent cycles of these calls will then bypass the cache. * @param eventListener An optional {@link EventListener} to receive events. */ public CacheDataSource(Cache cache, DataSource upstream, DataSource cacheReadDataSource, DataSink cacheWriteDataSink, boolean blockOnCache, boolean ignoreCacheOnError, EventListener eventListener) { this.cache = cache; this.cacheReadDataSource = cacheReadDataSource; this.blockOnCache = blockOnCache; this.ignoreCacheOnError = ignoreCacheOnError; this.upstreamDataSource = upstream; if (cacheWriteDataSink != null) { this.cacheWriteDataSource = new TeeDataSource(upstream, cacheWriteDataSink); } else { this.cacheWriteDataSource = null; } this.eventListener = eventListener; } @Override public long open(DataSpec dataSpec) throws IOException { Assertions.checkState(dataSpec.uriIsFullStream); // TODO: Support caching for unbounded requests. This requires storing the source length // into the cache (the simplest approach is to incorporate it into each cache file's name). Assertions.checkState(dataSpec.length != C.LENGTH_UNBOUNDED); try { uri = dataSpec.uri; key = dataSpec.key; readPosition = dataSpec.position; bytesRemaining = dataSpec.length; openNextSource(); return dataSpec.length; } catch (IOException e) { handleBeforeThrow(e); throw e; } } @Override public int read(byte[] buffer, int offset, int max) throws IOException { try { int bytesRead = currentDataSource.read(buffer, offset, max); if (bytesRead >= 0) { if (currentDataSource == cacheReadDataSource) { totalCachedBytesRead += bytesRead; } readPosition += bytesRead; bytesRemaining -= bytesRead; } else { closeCurrentSource(); if (bytesRemaining > 0) { openNextSource(); return read(buffer, offset, max); } } return bytesRead; } catch (IOException e) { handleBeforeThrow(e); throw e; } } @Override public void close() throws IOException { notifyBytesRead(); try { closeCurrentSource(); } catch (IOException e) { handleBeforeThrow(e); throw e; } } /** * Opens the next source. If the cache contains data spanning the current read position then * {@link #cacheReadDataSource} is opened to read from it. Else {@link #upstreamDataSource} is * opened to read from the upstream source and write into the cache. */ private void openNextSource() throws IOException { try { DataSpec dataSpec; CacheSpan span; if (ignoreCache) { span = null; } else if (blockOnCache) { span = cache.startReadWrite(key, readPosition); } else { span = cache.startReadWriteNonBlocking(key, readPosition); } if (span == null) { // The data is locked in the cache, or we're ignoring the cache. Bypass the cache and read // from upstream. currentDataSource = upstreamDataSource; dataSpec = new DataSpec(uri, readPosition, bytesRemaining, key); } else if (span.isCached) { // Data is cached, read from cache. Uri fileUri = Uri.fromFile(span.file); long filePosition = readPosition - span.position; long length = Math.min(span.length - filePosition, bytesRemaining); dataSpec = new DataSpec(fileUri, readPosition, length, key, filePosition); currentDataSource = cacheReadDataSource; } else { // Data is not cached, and data is not locked, read from upstream with cache backing. lockedSpan = span; long length = span.isOpenEnded() ? bytesRemaining : Math.min(span.length, bytesRemaining); dataSpec = new DataSpec(uri, readPosition, length, key); currentDataSource = cacheWriteDataSource != null ? cacheWriteDataSource : upstreamDataSource; } currentDataSource.open(dataSpec); } catch (InterruptedException e) { // Should never happen. throw new RuntimeException(e); } } private void closeCurrentSource() throws IOException { if (currentDataSource == null) { return; } try { currentDataSource.close(); currentDataSource = null; } finally { if (lockedSpan != null) { cache.releaseHoleSpan(lockedSpan); lockedSpan = null; } } } private void handleBeforeThrow(IOException exception) { if (ignoreCacheOnError && (currentDataSource == cacheReadDataSource || exception instanceof CacheDataSinkException)) { // Ignore the cache from now on. ignoreCache = true; } } private void notifyBytesRead() { if (eventListener != null && totalCachedBytesRead > 0) { eventListener.onCachedBytesRead(cache.getCacheSpace(), totalCachedBytesRead); totalCachedBytesRead = 0; } } }