当前位置:   article > 正文

ExoPlayer 如何实现持续缓存以及缓存进度监听_exoplayer缓存

exoplayer缓存

音频 APP 的一个必备功能就是在播放的时候会持续缓存完整个音频,同时进度条会更新缓存进度。但是 ExoPlayer 本身并没有提供什么方便的接口去实现这个功能,使用 ExoPlayer 的大多数 APP 应该还是使用 AndroidVideoCache 这个开源库,AndroidVideoCache 的原理是通过代理的策略实现一个中间层,将网络视频请求转移到本地实现的代理服务器上,这样真正请求的数据就会被代理拿到,然后代理一边向本地写入数据,一边根据需要的数据看是读网络数据还是读本地缓存数据,从而实现数据的复用。

其实 ExoPlayer 本身就有完善的缓存逻辑,为了实现上述功能就引入 AndroidVideoCache 虽然可以更轻松地实现,但是不够优雅。下面我们来改造下 ExoPlayer 的代码,来实现缓存进度监听的功能。

首先设置下 ExoPlayer,让它能在播放音频的时候持续缓存完整个音频文件。

ExoPlayer.Builder(...).setLoadControl(
    object : DefaultLoadControl() {
        override fun shouldContinueLoading(playbackPositionUs: Long, bufferedDurationUs: Long, playbackSpeed: Float): Boolean {
            val shouldContinueLoading = if (urlCacheable && isNetworkConnected) {
                true
            } else {
                super.shouldContinueLoading(playbackPositionUs, bufferedDurationUs, playbackSpeed)
            }
            return shouldContinueLoading
        }
    })
}...

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

然后来实现进度监听,需要修改 CacheDataSource,直接改 ExoPlayer 源码重新打包不利于后期升级维护,所以这里是拷贝了一个 CacheDataSource 类的实现,增加缓存进度回调。这里直接给出我改完的源码,大家复制到自己的工程里直接就可以用了。在复制之前,需要先把 CacheDataSource 用到的依赖引入一下。

// 引入依赖
implementation("org.checkerframework:checker-qual:3.13.0")

// 这个注解也要复制放到工程里
/**
 * Indicates that the return value of the annotated API(s) can be safely ignored.
 *
 * This is the opposite of [CheckReturnValue]. It can be used inside classes or packages
 * annotated with `@CheckReturnValue` to exempt specific APIs from the default.
 */
@MustBeDocumented
@Target(
    AnnotationTarget.FUNCTION,
    AnnotationTarget.PROPERTY_GETTER,
    AnnotationTarget.PROPERTY_SETTER,
    AnnotationTarget.CONSTRUCTOR,
    AnnotationTarget.CLASS
)
@Retention(
    AnnotationRetention.BINARY
)
annotation class CanIgnoreReturnValue

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

下面是 CacheDataSource 改之后的代码,这个是我们内部播放框架的一部分(没有什么保密内容。。),框架暂时没有开源计划,下面代码已上传到 github,地址:CacheDataSource。其中CanIgnoreReturnValue 提示找不到引用,需要自己导入下。本文也放一份备用,可直接点复制代码按钮复制。

import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Util.castNonNull;
import static java.lang.Math.min;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;

import android.net.Uri;
import android.os.SystemClock;
import android.text.TextUtils;

import androidx.annotation.IntDef;
import androidx.annotation.Nullable;

import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.PlaybackException;
import com.google.android.exoplayer2.upstream.DataSink;
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.FileDataSource;
import com.google.android.exoplayer2.upstream.PlaceholderDataSource;
import com.google.android.exoplayer2.upstream.PriorityDataSource;
import com.google.android.exoplayer2.upstream.TeeDataSource;
import com.google.android.exoplayer2.upstream.TransferListener;
import com.google.android.exoplayer2.upstream.cache.Cache;
import com.google.android.exoplayer2.upstream.cache.Cache.CacheException;
import com.google.android.exoplayer2.upstream.cache.CacheDataSink;
import com.google.android.exoplayer2.upstream.cache.CacheKeyFactory;
import com.google.android.exoplayer2.upstream.cache.CacheSpan;
import com.google.android.exoplayer2.upstream.cache.ContentMetadata;
import com.google.android.exoplayer2.upstream.cache.ContentMetadataMutations;
import com.google.android.exoplayer2.util.Assertions;
import com.google.android.exoplayer2.util.PriorityTaskManager;

import org.checkerframework.checker.nullness.qual.MonotonicNonNull;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import kotlin.ranges.RangesKt;

/**
 * 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 {
    public static final String TAG = "CacheDataSource";

    /**
     * {@link DataSource.Factory} for {@link CacheDataSource} instances.
     */
    public static final class Factory implements DataSource.Factory {

        private @MonotonicNonNull Cache cache;
        private DataSource.Factory cacheReadDataSourceFactory;
        @Nullable
        private DataSink.Factory cacheWriteDataSinkFactory;
        private CacheKeyFactory cacheKeyFactory;
        private boolean cacheIsReadOnly;
        @Nullable
        private DataSource.Factory upstreamDataSourceFactory;
        @Nullable
        private PriorityTaskManager upstreamPriorityTaskManager;
        private int upstreamPriority;
        private @Flags int flags;
        @Nullable
        private EventListener eventListener;

        public Factory() {
            cacheReadDataSourceFactory = new FileDataSource.Factory();
            cacheKeyFactory = CacheKeyFactory.DEFAULT;
        }

        /**
         * Sets the cache that will be used.
         *
         * <p>Must be called before the factory is used.
         *
         * @param cache The cache that will be used.
         * @return This factory.
         */
        @CanIgnoreReturnValue
        public Factory setCache(Cache cache) {
            this.cache = cache;
            return this;
        }

        /**
         * Returns the cache that will be used, or {@code null} if {@link #setCache} has yet to be
         * called.
         */
        @Nullable
        public Cache getCache() {
            return cache;
        }

        /**
         * Sets the {@link DataSource.Factory} for {@link DataSource DataSources} for reading from the
         * cache.
         *
         * <p>The default is a {@link FileDataSource.Factory} in its default configuration.
         *
         * @param cacheReadDataSourceFactory The {@link DataSource.Factory} for reading from the cache.
         * @return This factory.
         */
        @CanIgnoreReturnValue
        public Factory setCacheReadDataSourceFactory(DataSource.Factory cacheReadDataSourceFactory) {
            this.cacheReadDataSourceFactory = cacheReadDataSourceFactory;
            return this;
        }

        /**
         * Sets the {@link DataSink.Factory} for generating {@link DataSink DataSinks} for writing data
         * to the cache. Passing {@code null} causes the cache to be read-only.
         *
         * <p>The default is a {@link CacheDataSink.Factory} in its default configuration.
         *
         * @param cacheWriteDataSinkFactory The {@link DataSink.Factory} for generating {@link DataSink
         *                                  DataSinks} for writing data to the cache, or {@code null} to disable writing.
         * @return This factory.
         */
        @CanIgnoreReturnValue
        public Factory setCacheWriteDataSinkFactory(
            @Nullable DataSink.Factory cacheWriteDataSinkFactory) {
            this.cacheWriteDataSinkFactory = cacheWriteDataSinkFactory;
            this.cacheIsReadOnly = cacheWriteDataSinkFactory == null;
            return this;
        }

        /**
         * Sets the {@link CacheKeyFactory}.
         *
         * <p>The default is {@link CacheKeyFactory#DEFAULT}.
         *
         * @param cacheKeyFactory The {@link CacheKeyFactory}.
         * @return This factory.
         */
        @CanIgnoreReturnValue
        public Factory setCacheKeyFactory(CacheKeyFactory cacheKeyFactory) {
            this.cacheKeyFactory = cacheKeyFactory;
            return this;
        }

        /**
         * Returns the {@link CacheKeyFactory} that will be used.
         */
        public CacheKeyFactory getCacheKeyFactory() {
            return cacheKeyFactory;
        }

        /**
         * Sets the {@link DataSource.Factory} for upstream {@link DataSource DataSources}, which are
         * used to read data in the case of a cache miss.
         *
         * <p>The default is {@code null}, and so this method must be called before the factory is used
         * in order for data to be read from upstream in the case of a cache miss.
         *
         * @param upstreamDataSourceFactory The upstream {@link DataSource} for reading data not in the
         *                                  cache, or {@code null} to cause failure in the case of a cache miss.
         * @return This factory.
         */
        @CanIgnoreReturnValue
        public Factory setUpstreamDataSourceFactory(
            @Nullable DataSource.Factory upstreamDataSourceFactory) {
            this.upstreamDataSourceFactory = upstreamDataSourceFactory;
            return this;
        }

        /**
         * Sets an optional {@link PriorityTaskManager} to use when requesting data from upstream.
         *
         * <p>If set, reads from the upstream {@link DataSource} will only be allowed to proceed if
         * there are no higher priority tasks registered to the {@link PriorityTaskManager}. If there
         * exists a higher priority task then {@link PriorityTaskManager.PriorityTooLowException} will
         * be thrown instead.
         *
         * <p>Note that requests to {@link CacheDataSource} instances are intended to be used as parts
         * of (possibly larger) tasks that are registered with the {@link PriorityTaskManager}, and
         * hence {@link CacheDataSource} does <em>not</em> register a task by itself. This must be done
         * by the surrounding code that uses the {@link CacheDataSource} instances.
         *
         * <p>The default is {@code null}.
         *
         * @param upstreamPriorityTaskManager The upstream {@link PriorityTaskManager}.
         * @return This factory.
         */
        @CanIgnoreReturnValue
        public Factory setUpstreamPriorityTaskManager(
            @Nullable PriorityTaskManager upstreamPriorityTaskManager) {
            this.upstreamPriorityTaskManager = upstreamPriorityTaskManager;
            return this;
        }

        /**
         * Returns the {@link PriorityTaskManager} that will bs used when requesting data from upstream,
         * or {@code null} if there is none.
         */
        @Nullable
        public PriorityTaskManager getUpstreamPriorityTaskManager() {
            return upstreamPriorityTaskManager;
        }

        /**
         * Sets the priority to use when requesting data from upstream. The priority is only used if a
         * {@link PriorityTaskManager} is set by calling {@link #setUpstreamPriorityTaskManager}.
         *
         * <p>The default is {@link C#PRIORITY_PLAYBACK}.
         *
         * @param upstreamPriority The priority to use when requesting data from upstream.
         * @return This factory.
         */
        @CanIgnoreReturnValue
        public Factory setUpstreamPriority(int upstreamPriority) {
            this.upstreamPriority = upstreamPriority;
            return this;
        }

        /**
         * Sets the {@link Flags}.
         *
         * <p>The default is {@code 0}.
         *
         * @param flags The {@link Flags}.
         * @return This factory.
         */
        @CanIgnoreReturnValue
        public Factory setFlags(@Flags int flags) {
            this.flags = flags;
            return this;
        }

        /**
         * Sets the {link EventListener} to which events are delivered.
         *
         * <p>The default is {@code null}.
         *
         * @param eventListener The {@link EventListener}.
         * @return This factory.
         */
        @CanIgnoreReturnValue
        public Factory setEventListener(@Nullable EventListener eventListener) {
            this.eventListener = eventListener;
            return this;
        }

        @Override
        public CacheDataSource createDataSource() {
            return createDataSourceInternal(
                upstreamDataSourceFactory != null ? upstreamDataSourceFactory.createDataSource() : null,
                flags,
                upstreamPriority);
        }

        /**
         * Returns an instance suitable for downloading content. The created instance is equivalent to
         * one that would be created by {@link #createDataSource()}, except:
         *
         * <ul>
         *   <li>The {@link #FLAG_BLOCK_ON_CACHE} is always set.
         *   <li>The task priority is overridden to be {@link C#PRIORITY_DOWNLOAD}.
         * </ul>
         *
         * @return An instance suitable for downloading content.
         */
        public CacheDataSource createDataSourceForDownloading() {
            return createDataSourceInternal(
                upstreamDataSourceFactory != null ? upstreamDataSourceFactory.createDataSource() : null,
                flags | FLAG_BLOCK_ON_CACHE,
                C.PRIORITY_DOWNLOAD);
        }

        /**
         * Returns an instance suitable for reading cached content as part of removing a download. The
         * created instance is equivalent to one that would be created by {@link #createDataSource()},
         * except:
         *
         * <ul>
         *   <li>The upstream is overridden to be {@code null}, since when removing content we don't
         *       want to request anything that's not already cached.
         *   <li>The {@link #FLAG_BLOCK_ON_CACHE} is always set.
         *   <li>The task priority is overridden to be {@link C#PRIORITY_DOWNLOAD}.
         * </ul>
         *
         * @return An instance suitable for reading cached content as part of removing a download.
         */
        public CacheDataSource createDataSourceForRemovingDownload() {
            return createDataSourceInternal(
                /* upstreamDataSource= */ null, flags | FLAG_BLOCK_ON_CACHE, C.PRIORITY_DOWNLOAD);
        }

        private CacheDataSource createDataSourceInternal(
            @Nullable DataSource upstreamDataSource, @Flags int flags, int upstreamPriority) {
            Cache cache = checkNotNull(this.cache);
            @Nullable DataSink cacheWriteDataSink;
            if (cacheIsReadOnly || upstreamDataSource == null) {
                cacheWriteDataSink = null;
            } else if (cacheWriteDataSinkFactory != null) {
                cacheWriteDataSink = cacheWriteDataSinkFactory.createDataSink();
            } else {
                cacheWriteDataSink = new CacheDataSink.Factory().setCache(cache).createDataSink();
            }
            return new CacheDataSource(
                cache,
                upstreamDataSource,
                cacheReadDataSourceFactory.createDataSource(),
                cacheWriteDataSink,
                cacheKeyFactory,
                flags,
                upstreamPriorityTaskManager,
                upstreamPriority,
                eventListener);
        }
    }

    /**
     * Listener of {@link CacheDataSource} events.
     */
    public interface EventListener {

        /**
         * Called 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 called.
         */
        void onCachedBytesRead(long cacheSizeBytes, long cachedBytesRead);

        void onCachedProgress(long contentLength, long bytesCached, float percent);

        /**
         * Called when the current request ignores cache.
         *
         * @param reason Reason cache is bypassed.
         */
        void onCacheIgnored(@CacheIgnoredReason int reason);
    }

    /**
     * Flags controlling the CacheDataSource's behavior. Possible flag values are {@link
     * #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR} and {@link
     * #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}.
     */
    @Documented
    @Retention(RetentionPolicy.SOURCE)
    @Target(TYPE_USE)
    @IntDef(
        flag = true,
        value = {
            FLAG_BLOCK_ON_CACHE,
            FLAG_IGNORE_CACHE_ON_ERROR,
            FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS
        })
    public @interface Flags {
    }

    /**
     * A flag indicating whether we will block reads if the cache key is locked. If unset then data is
     * read from upstream if the cache key is locked, regardless of whether the data is cached.
     */
    public static final int FLAG_BLOCK_ON_CACHE = 1;

    /**
     * A flag indicating whether the cache is bypassed following any cache related error. If set 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.
     */
    public static final int FLAG_IGNORE_CACHE_ON_ERROR = 1 << 1; // 2

    /**
     * A flag indicating that the cache should be bypassed for requests whose lengths are unset. This
     * flag is provided for legacy reasons only.
     */
    public static final int FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS = 1 << 2; // 4

    /**
     * Reasons the cache may be ignored. One of {@link #CACHE_IGNORED_REASON_ERROR} or {@link
     * #CACHE_IGNORED_REASON_UNSET_LENGTH}.
     */
    // @Target list includes both 'default' targets and TYPE_USE, to ensure backwards compatibility
    // with Kotlin usages from before TYPE_USE was added.
    @Documented
    @Retention(RetentionPolicy.SOURCE)
    @Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE})
    @IntDef({CACHE_IGNORED_REASON_ERROR, CACHE_IGNORED_REASON_UNSET_LENGTH})
    public @interface CacheIgnoredReason {
    }

    /**
     * Cache not ignored.
     */
    private static final int CACHE_NOT_IGNORED = -1;

    /**
     * Cache ignored due to a cache related error.
     */
    public static final int CACHE_IGNORED_REASON_ERROR = 0;

    /**
     * Cache ignored due to a request with an unset length.
     */
    public static final int CACHE_IGNORED_REASON_UNSET_LENGTH = 1;

    /**
     * Minimum number of bytes to read before checking cache for availability.
     */
    private static final long MIN_READ_BEFORE_CHECKING_CACHE = 100 * 1024;

    private final Cache cache;
    private final DataSource cacheReadDataSource;
    @Nullable
    private final DataSource cacheWriteDataSource;
    private final DataSource upstreamDataSource;
    private final CacheKeyFactory cacheKeyFactory;
    @Nullable
    private final EventListener eventListener;

    private final boolean blockOnCache;
    private final boolean ignoreCacheOnError;
    private final boolean ignoreCacheForUnsetLengthRequests;

    @Nullable
    private Uri actualUri;
    @Nullable
    private DataSpec requestDataSpec;
    @Nullable
    private DataSpec currentDataSpec;
    @Nullable
    private DataSource currentDataSource;
    private long currentDataSourceBytesRead;
    private long readPosition;
    private long bytesRemaining;
    @Nullable
    private CacheSpan currentHoleSpan;
    private boolean seenCacheError;
    private boolean currentRequestIgnoresCache;
    private long totalCachedBytesRead;
    private long checkCachePosition;
    private long contentLength;
    private long mBytesCached;
    private long mLastBytesCached = 0L;
    private long mLastNotifyTime = SystemClock.elapsedRealtime();

    /**
     * Constructs an instance with default {@link DataSource} and {@link DataSink} instances for
     * reading and writing the cache.
     *
     * @param cache              The cache.
     * @param upstreamDataSource A {@link DataSource} for reading data not in the cache. If null,
     *                           reading will fail if a cache miss occurs.
     */
    public CacheDataSource(Cache cache, @Nullable DataSource upstreamDataSource) {
        this(cache, upstreamDataSource, /* flags= */ 0);
    }

    /**
     * Constructs an instance with default {@link DataSource} and {@link DataSink} instances for
     * reading and writing the cache.
     *
     * @param cache              The cache.
     * @param upstreamDataSource A {@link DataSource} for reading data not in the cache. If null,
     *                           reading will fail if a cache miss occurs.
     * @param flags              A combination of {@link #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR}
     *                           and {@link #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}, or 0.
     */
    public CacheDataSource(Cache cache, @Nullable DataSource upstreamDataSource, @Flags int flags) {
        this(
            cache,
            upstreamDataSource,
            new FileDataSource(),
            new CacheDataSink(cache, CacheDataSink.DEFAULT_FRAGMENT_SIZE),
            flags,
            /* eventListener= */ 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 upstreamDataSource  A {@link DataSource} for reading data not in the cache. If null,
     *                            reading will fail if a cache miss occurs.
     * @param cacheReadDataSource A {@link DataSource} for reading data from the cache.
     * @param cacheWriteDataSink  A {@link DataSink} for writing data to the cache. If null, cache is
     *                            accessed read-only.
     * @param flags               A combination of {@link #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR}
     *                            and {@link #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}, or 0.
     * @param eventListener       An optional {@link EventListener} to receive events.
     */
    public CacheDataSource(
        Cache cache,
        @Nullable DataSource upstreamDataSource,
        DataSource cacheReadDataSource,
        @Nullable DataSink cacheWriteDataSink,
        @Flags int flags,
        @Nullable EventListener eventListener) {
        this(
            cache,
            upstreamDataSource,
            cacheReadDataSource,
            cacheWriteDataSink,
            flags,
            eventListener,
            /* cacheKeyFactory= */ 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 upstreamDataSource  A {@link DataSource} for reading data not in the cache. If null,
     *                            reading will fail if a cache miss occurs.
     * @param cacheReadDataSource A {@link DataSource} for reading data from the cache.
     * @param cacheWriteDataSink  A {@link DataSink} for writing data to the cache. If null, cache is
     *                            accessed read-only.
     * @param flags               A combination of {@link #FLAG_BLOCK_ON_CACHE}, {@link #FLAG_IGNORE_CACHE_ON_ERROR}
     *                            and {@link #FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS}, or 0.
     * @param eventListener       An optional {@link EventListener} to receive events.
     * @param cacheKeyFactory     An optional factory for cache keys.
     */
    public CacheDataSource(
        Cache cache,
        @Nullable DataSource upstreamDataSource,
        DataSource cacheReadDataSource,
        @Nullable DataSink cacheWriteDataSink,
        @Flags int flags,
        @Nullable EventListener eventListener,
        @Nullable CacheKeyFactory cacheKeyFactory) {
        this(
            cache,
            upstreamDataSource,
            cacheReadDataSource,
            cacheWriteDataSink,
            cacheKeyFactory,
            flags,
            /* upstreamPriorityTaskManager= */ null,
            /* upstreamPriority= */ C.PRIORITY_PLAYBACK,
            eventListener);
    }

    private CacheDataSource(
        Cache cache,
        @Nullable DataSource upstreamDataSource,
        DataSource cacheReadDataSource,
        @Nullable DataSink cacheWriteDataSink,
        @Nullable CacheKeyFactory cacheKeyFactory,
        @Flags int flags,
        @Nullable PriorityTaskManager upstreamPriorityTaskManager,
        int upstreamPriority,
        @Nullable EventListener eventListener) {
        this.cache = cache;
        this.cacheReadDataSource = cacheReadDataSource;
        this.cacheKeyFactory = cacheKeyFactory != null ? cacheKeyFactory : CacheKeyFactory.DEFAULT;
        this.blockOnCache = (flags & FLAG_BLOCK_ON_CACHE) != 0;
        this.ignoreCacheOnError = (flags & FLAG_IGNORE_CACHE_ON_ERROR) != 0;
        this.ignoreCacheForUnsetLengthRequests =
            (flags & FLAG_IGNORE_CACHE_FOR_UNSET_LENGTH_REQUESTS) != 0;
        if (upstreamDataSource != null) {
            if (upstreamPriorityTaskManager != null) {
                upstreamDataSource =
                    new PriorityDataSource(
                        upstreamDataSource, upstreamPriorityTaskManager, upstreamPriority);
            }
            this.upstreamDataSource = upstreamDataSource;
            this.cacheWriteDataSource =
                cacheWriteDataSink != null
                    ? new TeeDataSource(upstreamDataSource, cacheWriteDataSink)
                    : null;
        } else {
            this.upstreamDataSource = PlaceholderDataSource.INSTANCE;
            this.cacheWriteDataSource = null;
        }
        this.eventListener = eventListener;
    }

    /**
     * Returns the {@link Cache} used by this instance.
     */
    public Cache getCache() {
        return cache;
    }

    /**
     * Returns the {@link CacheKeyFactory} used by this instance.
     */
    public CacheKeyFactory getCacheKeyFactory() {
        return cacheKeyFactory;
    }

    @Override
    public void addTransferListener(TransferListener transferListener) {
        checkNotNull(transferListener);
        cacheReadDataSource.addTransferListener(transferListener);
        upstreamDataSource.addTransferListener(transferListener);
    }

    @Override
    public long open(DataSpec dataSpec) throws IOException {
        try {
            String key = cacheKeyFactory.buildCacheKey(dataSpec);
            DataSpec requestDataSpec = dataSpec.buildUpon().setKey(key).build();
            this.requestDataSpec = requestDataSpec;
            actualUri = getRedirectedUriOrDefault(cache, key, /* defaultUri= */ requestDataSpec.uri);
            readPosition = dataSpec.position;

            int reason = shouldIgnoreCacheForRequest(dataSpec);
            currentRequestIgnoresCache = reason != CACHE_NOT_IGNORED;
            if (currentRequestIgnoresCache) {
                notifyCacheIgnored(reason);
            }

            if (currentRequestIgnoresCache) {
                bytesRemaining = C.LENGTH_UNSET;
            } else {
                bytesRemaining = ContentMetadata.getContentLength(cache.getContentMetadata(key));
                if (bytesRemaining != C.LENGTH_UNSET) {
                    bytesRemaining -= dataSpec.position;
                    if (bytesRemaining < 0) {
                        throw new DataSourceException(
                            PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE);
                    }
                }
            }
            if (dataSpec.length != C.LENGTH_UNSET) {
                bytesRemaining =
                    bytesRemaining == C.LENGTH_UNSET
                        ? dataSpec.length
                        : min(bytesRemaining, dataSpec.length);
            }
            if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) {
                openNextSource(requestDataSpec, false);
            }
            long length = dataSpec.length != C.LENGTH_UNSET ? dataSpec.length : bytesRemaining;
            return length;
        } catch (Throwable e) {
            handleBeforeThrow(e);
            throw e;
        }
    }

    @Override
    public int read(byte[] buffer, int offset, int length) throws IOException {
        if (length == 0) {
            return 0;
        }
        if (bytesRemaining == 0) {
            return C.RESULT_END_OF_INPUT;
        }
        DataSpec requestDataSpec = checkNotNull(this.requestDataSpec);
        DataSpec currentDataSpec = checkNotNull(this.currentDataSpec);
        try {
            if (readPosition >= checkCachePosition) {
                openNextSource(requestDataSpec, true);
            }
            int bytesRead = checkNotNull(currentDataSource).read(buffer, offset, length);

            if (isWritingToCache()) {
                mBytesCached += bytesRead;
                notifyProgressUpdate(0.001f);
            }
            if (bytesRead != C.RESULT_END_OF_INPUT) {
                if (isReadingFromCache()) {
                    totalCachedBytesRead += bytesRead;
                }
                readPosition += bytesRead;
                currentDataSourceBytesRead += bytesRead;
                if (bytesRemaining != C.LENGTH_UNSET) {
                    bytesRemaining -= bytesRead;
                }
            } else if (isReadingFromUpstream()
                && (currentDataSpec.length == C.LENGTH_UNSET
                || currentDataSourceBytesRead < currentDataSpec.length)) {
                // We've encountered RESULT_END_OF_INPUT from the upstream DataSource at a position not
                // imposed by the current DataSpec. This must mean that we've reached the end of the
                // resource.
                setNoBytesRemainingAndMaybeStoreLength(castNonNull(requestDataSpec.key));
            } else if (bytesRemaining > 0 || bytesRemaining == C.LENGTH_UNSET) {
                closeCurrentSource();
                openNextSource(requestDataSpec, false);
                return read(buffer, offset, length);
            }
            return bytesRead;
        } catch (Throwable e) {
            handleBeforeThrow(e);
            throw e;
        }
    }

    @Override
    @Nullable
    public Uri getUri() {
        return actualUri;
    }

    @Override
    public Map<String, List<String>> getResponseHeaders() {
        // TODO: Implement.
        return isReadingFromUpstream()
            ? upstreamDataSource.getResponseHeaders()
            : Collections.emptyMap();
    }

    @Override
    public void close() throws IOException {
        actualUri = null;
        readPosition = 0;
        notifyBytesRead();
        if (isWritingToCache()) {
            if (requestDataSpec != null) {
                String cacheKey = requestDataSpec.key;
                if (!TextUtils.isEmpty(cacheKey)) {
                    long cachedBytes = cache.getCachedBytes(cacheKey, 0L, C.LENGTH_UNSET);
                    if (cachedBytes > mBytesCached) {
                        mBytesCached = cachedBytes;
                    }
                }
            }
            notifyProgressUpdate(1f);
        }
        requestDataSpec = null;
        try {
            closeCurrentSource();
        } catch (Throwable 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.
     *
     * <p>There must not be a currently open source when this method is called, except in the case
     * that {@code checkCache} is true. If {@code checkCache} is true then there must be a currently
     * open source, and it must be {@link #upstreamDataSource}. It will be closed and a new source
     * opened if it's possible to switch to reading from or writing to the cache. If a switch isn't
     * possible then the current source is left unchanged.
     *
     * @param requestDataSpec The original {@link DataSpec} to build upon for the next source.
     * @param checkCache      If true tries to switch to reading from or writing to cache instead of
     *                        reading from {@link #upstreamDataSource}, which is the currently open source.
     */
    private void openNextSource(DataSpec requestDataSpec, boolean checkCache) throws IOException {
        @Nullable CacheSpan nextSpan;
        String key = castNonNull(requestDataSpec.key);
        if (currentRequestIgnoresCache) {
            nextSpan = null;
        } else if (blockOnCache) {
            try {
                nextSpan = cache.startReadWrite(key, readPosition, bytesRemaining);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new InterruptedIOException();
            }
        } else {
            nextSpan = cache.startReadWriteNonBlocking(key, readPosition, bytesRemaining);
        }

        DataSpec nextDataSpec;
        DataSource nextDataSource;
        if (nextSpan == null) {
            // The data is locked in the cache, or we're ignoring the cache. Bypass the cache and read
            // from upstream.
            nextDataSource = upstreamDataSource;
            nextDataSpec =
                requestDataSpec.buildUpon().setPosition(readPosition).setLength(bytesRemaining).build();
        } else if (nextSpan.isCached) {
            // Data is cached in a span file starting at nextSpan.position.
            Uri fileUri = Uri.fromFile(castNonNull(nextSpan.file));
            long filePositionOffset = nextSpan.position;
            long positionInFile = readPosition - filePositionOffset;
            long length = nextSpan.length - positionInFile;
            if (bytesRemaining != C.LENGTH_UNSET) {
                length = min(length, bytesRemaining);
            }
            nextDataSpec =
                requestDataSpec
                    .buildUpon()
                    .setUri(fileUri)
                    .setUriPositionOffset(filePositionOffset)
                    .setPosition(positionInFile)
                    .setLength(length)
                    .build();
            nextDataSource = cacheReadDataSource;
        } else {
            // Data is not cached, and data is not locked, read from upstream with cache backing.
            long length;
            if (nextSpan.isOpenEnded()) {
                length = bytesRemaining;
            } else {
                length = nextSpan.length;
                if (bytesRemaining != C.LENGTH_UNSET) {
                    length = min(length, bytesRemaining);
                }
            }
            nextDataSpec =
                requestDataSpec.buildUpon().setPosition(readPosition).setLength(length).build();
            if (cacheWriteDataSource != null) {
                nextDataSource = cacheWriteDataSource;
            } else {
                nextDataSource = upstreamDataSource;
                cache.releaseHoleSpan(nextSpan);
                nextSpan = null;
            }
        }
        checkCachePosition =
            !currentRequestIgnoresCache && nextDataSource == upstreamDataSource
                ? readPosition + MIN_READ_BEFORE_CHECKING_CACHE
                : Long.MAX_VALUE;
        if (checkCache) {
            Assertions.checkState(isBypassingCache());
            if (nextDataSource == upstreamDataSource) {
                // Continue reading from upstream.
                return;
            }
            // We're switching to reading from or writing to the cache.
            try {
                closeCurrentSource();
            } catch (Throwable e) {
                if (castNonNull(nextSpan).isHoleSpan()) {
                    // Release the hole span before throwing, else we'll hold it forever.
                    cache.releaseHoleSpan(nextSpan);
                }
                throw e;
            }
        }

        if (nextSpan != null && nextSpan.isHoleSpan()) {
            currentHoleSpan = nextSpan;
        }
        currentDataSource = nextDataSource;
        currentDataSpec = nextDataSpec;
        currentDataSourceBytesRead = 0;
        long resolvedLength = nextDataSource.open(nextDataSpec);

        // Update bytesRemaining, actualUri and (if writing to cache) the cache metadata.
        mLastBytesCached = 0L;
        contentLength = 0L;
        ContentMetadataMutations mutations = new ContentMetadataMutations();
        if (nextDataSpec.length == C.LENGTH_UNSET && resolvedLength != C.LENGTH_UNSET) {
            bytesRemaining = resolvedLength;
            contentLength = readPosition + bytesRemaining;
            ContentMetadataMutations.setContentLength(mutations, contentLength);
        }
        if (isReadingFromUpstream()) {
            actualUri = nextDataSource.getUri();
            boolean isRedirected = !requestDataSpec.uri.equals(actualUri);
            ContentMetadataMutations.setRedirectedUri(mutations, isRedirected ? actualUri : null);
        }
        if (isWritingToCache()) {
            cache.applyContentMetadataMutations(key, mutations);
        }
        CacheSpan finalNextSpan = nextSpan;
        String cacheKey = nextDataSpec.key;
        if (!TextUtils.isEmpty(cacheKey)) {
            mBytesCached = cache.getCachedBytes(cacheKey, 0L, C.LENGTH_UNSET);
            if (contentLength <= 0L) {
                if (bytesRemaining >= 0) {
                    contentLength = readPosition + bytesRemaining;
                } else {
                    contentLength = ContentMetadata.getContentLength(cache.getContentMetadata(cacheKey));
                }
            }
            notifyProgressUpdate(1f);
        } else {
            mBytesCached = 0L;
        }
    }

    private void notifyProgressUpdate(float sampleRate) {
        long elapsedRealtime = SystemClock.elapsedRealtime();
        if (mLastBytesCached != mBytesCached && contentLength > 0L && eventListener != null && (sampleRate == 1f || elapsedRealtime - mLastNotifyTime > 100L)) {
            mLastBytesCached = mBytesCached;
            mLastNotifyTime = elapsedRealtime;
            eventListener.onCachedProgress(
                contentLength,
                mBytesCached,
                RangesKt.coerceIn(mBytesCached / (float) contentLength, 0f, 1f)
            );
        }
    }

    private void setNoBytesRemainingAndMaybeStoreLength(String key) throws IOException {
        bytesRemaining = 0;
        if (isWritingToCache()) {
            notifyProgressUpdate(1f);

            ContentMetadataMutations mutations = new ContentMetadataMutations();
            ContentMetadataMutations.setContentLength(mutations, readPosition);
            cache.applyContentMetadataMutations(key, mutations);
        }
    }

    private static Uri getRedirectedUriOrDefault(Cache cache, String key, Uri defaultUri) {
        @Nullable Uri redirectedUri = ContentMetadata.getRedirectedUri(cache.getContentMetadata(key));
        return redirectedUri != null ? redirectedUri : defaultUri;
    }

    private boolean isReadingFromUpstream() {
        return !isReadingFromCache();
    }

    private boolean isBypassingCache() {
        return currentDataSource == upstreamDataSource;
    }

    private boolean isReadingFromCache() {
        return currentDataSource == cacheReadDataSource;
    }

    private boolean isWritingToCache() {
        return currentDataSource == cacheWriteDataSource;
    }

    private void closeCurrentSource() throws IOException {
        if (currentDataSource == null) {
            return;
        }
        try {
            currentDataSource.close();
        } finally {
            currentDataSpec = null;
            currentDataSource = null;
            if (currentHoleSpan != null) {
                cache.releaseHoleSpan(currentHoleSpan);
                currentHoleSpan = null;
            }
        }
    }

    private void handleBeforeThrow(Throwable exception) {
        if (isReadingFromCache() || exception instanceof CacheException) {
            seenCacheError = true;
        }
    }

    private int shouldIgnoreCacheForRequest(DataSpec dataSpec) {
        if (ignoreCacheOnError && seenCacheError) {
            return CACHE_IGNORED_REASON_ERROR;
        } else if (ignoreCacheForUnsetLengthRequests && dataSpec.length == C.LENGTH_UNSET) {
            return CACHE_IGNORED_REASON_UNSET_LENGTH;
        } else {
            return CACHE_NOT_IGNORED;
        }
    }

    private void notifyCacheIgnored(@CacheIgnoredReason int reason) {
        if (eventListener != null) {
            eventListener.onCacheIgnored(reason);
        }
    }

    private void notifyBytesRead() {
        if (eventListener != null && totalCachedBytesRead > 0) {
            eventListener.onCachedBytesRead(cache.getCacheSpace(), totalCachedBytesRead);
            totalCachedBytesRead = 0;
        }
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306
  • 307
  • 308
  • 309
  • 310
  • 311
  • 312
  • 313
  • 314
  • 315
  • 316
  • 317
  • 318
  • 319
  • 320
  • 321
  • 322
  • 323
  • 324
  • 325
  • 326
  • 327
  • 328
  • 329
  • 330
  • 331
  • 332
  • 333
  • 334
  • 335
  • 336
  • 337
  • 338
  • 339
  • 340
  • 341
  • 342
  • 343
  • 344
  • 345
  • 346
  • 347
  • 348
  • 349
  • 350
  • 351
  • 352
  • 353
  • 354
  • 355
  • 356
  • 357
  • 358
  • 359
  • 360
  • 361
  • 362
  • 363
  • 364
  • 365
  • 366
  • 367
  • 368
  • 369
  • 370
  • 371
  • 372
  • 373
  • 374
  • 375
  • 376
  • 377
  • 378
  • 379
  • 380
  • 381
  • 382
  • 383
  • 384
  • 385
  • 386
  • 387
  • 388
  • 389
  • 390
  • 391
  • 392
  • 393
  • 394
  • 395
  • 396
  • 397
  • 398
  • 399
  • 400
  • 401
  • 402
  • 403
  • 404
  • 405
  • 406
  • 407
  • 408
  • 409
  • 410
  • 411
  • 412
  • 413
  • 414
  • 415
  • 416
  • 417
  • 418
  • 419
  • 420
  • 421
  • 422
  • 423
  • 424
  • 425
  • 426
  • 427
  • 428
  • 429
  • 430
  • 431
  • 432
  • 433
  • 434
  • 435
  • 436
  • 437
  • 438
  • 439
  • 440
  • 441
  • 442
  • 443
  • 444
  • 445
  • 446
  • 447
  • 448
  • 449
  • 450
  • 451
  • 452
  • 453
  • 454
  • 455
  • 456
  • 457
  • 458
  • 459
  • 460
  • 461
  • 462
  • 463
  • 464
  • 465
  • 466
  • 467
  • 468
  • 469
  • 470
  • 471
  • 472
  • 473
  • 474
  • 475
  • 476
  • 477
  • 478
  • 479
  • 480
  • 481
  • 482
  • 483
  • 484
  • 485
  • 486
  • 487
  • 488
  • 489
  • 490
  • 491
  • 492
  • 493
  • 494
  • 495
  • 496
  • 497
  • 498
  • 499
  • 500
  • 501
  • 502
  • 503
  • 504
  • 505
  • 506
  • 507
  • 508
  • 509
  • 510
  • 511
  • 512
  • 513
  • 514
  • 515
  • 516
  • 517
  • 518
  • 519
  • 520
  • 521
  • 522
  • 523
  • 524
  • 525
  • 526
  • 527
  • 528
  • 529
  • 530
  • 531
  • 532
  • 533
  • 534
  • 535
  • 536
  • 537
  • 538
  • 539
  • 540
  • 541
  • 542
  • 543
  • 544
  • 545
  • 546
  • 547
  • 548
  • 549
  • 550
  • 551
  • 552
  • 553
  • 554
  • 555
  • 556
  • 557
  • 558
  • 559
  • 560
  • 561
  • 562
  • 563
  • 564
  • 565
  • 566
  • 567
  • 568
  • 569
  • 570
  • 571
  • 572
  • 573
  • 574
  • 575
  • 576
  • 577
  • 578
  • 579
  • 580
  • 581
  • 582
  • 583
  • 584
  • 585
  • 586
  • 587
  • 588
  • 589
  • 590
  • 591
  • 592
  • 593
  • 594
  • 595
  • 596
  • 597
  • 598
  • 599
  • 600
  • 601
  • 602
  • 603
  • 604
  • 605
  • 606
  • 607
  • 608
  • 609
  • 610
  • 611
  • 612
  • 613
  • 614
  • 615
  • 616
  • 617
  • 618
  • 619
  • 620
  • 621
  • 622
  • 623
  • 624
  • 625
  • 626
  • 627
  • 628
  • 629
  • 630
  • 631
  • 632
  • 633
  • 634
  • 635
  • 636
  • 637
  • 638
  • 639
  • 640
  • 641
  • 642
  • 643
  • 644
  • 645
  • 646
  • 647
  • 648
  • 649
  • 650
  • 651
  • 652
  • 653
  • 654
  • 655
  • 656
  • 657
  • 658
  • 659
  • 660
  • 661
  • 662
  • 663
  • 664
  • 665
  • 666
  • 667
  • 668
  • 669
  • 670
  • 671
  • 672
  • 673
  • 674
  • 675
  • 676
  • 677
  • 678
  • 679
  • 680
  • 681
  • 682
  • 683
  • 684
  • 685
  • 686
  • 687
  • 688
  • 689
  • 690
  • 691
  • 692
  • 693
  • 694
  • 695
  • 696
  • 697
  • 698
  • 699
  • 700
  • 701
  • 702
  • 703
  • 704
  • 705
  • 706
  • 707
  • 708
  • 709
  • 710
  • 711
  • 712
  • 713
  • 714
  • 715
  • 716
  • 717
  • 718
  • 719
  • 720
  • 721
  • 722
  • 723
  • 724
  • 725
  • 726
  • 727
  • 728
  • 729
  • 730
  • 731
  • 732
  • 733
  • 734
  • 735
  • 736
  • 737
  • 738
  • 739
  • 740
  • 741
  • 742
  • 743
  • 744
  • 745
  • 746
  • 747
  • 748
  • 749
  • 750
  • 751
  • 752
  • 753
  • 754
  • 755
  • 756
  • 757
  • 758
  • 759
  • 760
  • 761
  • 762
  • 763
  • 764
  • 765
  • 766
  • 767
  • 768
  • 769
  • 770
  • 771
  • 772
  • 773
  • 774
  • 775
  • 776
  • 777
  • 778
  • 779
  • 780
  • 781
  • 782
  • 783
  • 784
  • 785
  • 786
  • 787
  • 788
  • 789
  • 790
  • 791
  • 792
  • 793
  • 794
  • 795
  • 796
  • 797
  • 798
  • 799
  • 800
  • 801
  • 802
  • 803
  • 804
  • 805
  • 806
  • 807
  • 808
  • 809
  • 810
  • 811
  • 812
  • 813
  • 814
  • 815
  • 816
  • 817
  • 818
  • 819
  • 820
  • 821
  • 822
  • 823
  • 824
  • 825
  • 826
  • 827
  • 828
  • 829
  • 830
  • 831
  • 832
  • 833
  • 834
  • 835
  • 836
  • 837
  • 838
  • 839
  • 840
  • 841
  • 842
  • 843
  • 844
  • 845
  • 846
  • 847
  • 848
  • 849
  • 850
  • 851
  • 852
  • 853
  • 854
  • 855
  • 856
  • 857
  • 858
  • 859
  • 860
  • 861
  • 862
  • 863
  • 864
  • 865
  • 866
  • 867
  • 868
  • 869
  • 870
  • 871
  • 872
  • 873
  • 874
  • 875
  • 876
  • 877
  • 878
  • 879
  • 880
  • 881
  • 882
  • 883
  • 884
  • 885
  • 886
  • 887
  • 888
  • 889
  • 890
  • 891
  • 892
  • 893
  • 894
  • 895
  • 896
  • 897
  • 898
  • 899
  • 900
  • 901
  • 902
  • 903
  • 904
  • 905
  • 906
  • 907
  • 908
  • 909
  • 910
  • 911
  • 912
  • 913
  • 914
  • 915
  • 916
  • 917
  • 918
  • 919
  • 920
  • 921
  • 922
  • 923
  • 924
  • 925
  • 926
  • 927
  • 928
  • 929
  • 930
  • 931
  • 932
  • 933
  • 934
  • 935
  • 936
  • 937
  • 938
  • 939
  • 940
  • 941
  • 942
  • 943
  • 944
  • 945
  • 946
  • 947
  • 948
  • 949
  • 950
  • 951
  • 952
  • 953
  • 954
  • 955
  • 956
  • 957
  • 958
  • 959
  • 960
  • 961
  • 962
  • 963
  • 964
  • 965
  • 966
  • 967
  • 968
  • 969
  • 970
  • 971
  • 972
  • 973
  • 974

假设你有个 DataSourceManager,在里面复制如下代码,用于生成 DataSourceFactory 和设置缓存进度监听。


private var mOnProgress: ((contentLength: Long, cachedBytes: Long, percent: Float) -> Unit)? = null
private val mCacheEventListener by lazy {
    object: CacheDataSource.EventListener {
        override fun onCachedBytesRead(cacheSizeBytes: Long, cachedBytesRead: Long) {
        }

        override fun onCachedProgress(contentLength: Long, bytesCached: Long, percent: Float) {
            mOnProgress?.invoke(contentLength, bytesCached, percent)
        }

        override fun onCacheIgnored(reason: Int) {
        }
    }
}

// 这里我有其他 factory,所以是方法的形式,如果你只需要这一个 cacheFactory,可以用字段形式
fun getDataSourceFactory(urlCacheable: Boolean) = mCacheDataSourceFactory

fun setupCacheDataSourceFactory(cacheDirPath: String?) {
    val cache = 自己生成一个 SimpleCache
    mCacheDataSourceFactory = CacheDataSource.Factory().apply {
        setUpstreamDataSourceFactory(mUpstreamFactory)
        setCache(cache)
        setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
        setCacheWriteDataSinkFactory {
            // fragmentSize 设为 C.LENGTH_UNSET,文件不分片,想分片的话可以自己改这里
            CacheDataSink.Factory().setCache(cache).setFragmentSize(C.LENGTH_UNSET.toLong()).createDataSink()
        }
        setEventListener(mCacheEventListener)
    }
}

fun setCacheProcessListener(onProgress: (contentLength: Long, cachedBytes: Long, percent: Float) -> Unit) {
    mOnProgress = onProgress
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

然后在设置 setMediaSource 的时候设置上面的 CacheDataSourceFactory 就好了。

mDataSourceManager.setCacheProcessListener { _, _, percent ->
    // 通知 UI 更新
    onCacheProgress(percent)
}

...
val mediaSource = ProgressiveMediaSource.Factory(cacheDataSourceFactory, DefaultExtractorsFactory())
    .createMediaSource(mediaItem)
exoPlayer.setMediaSource(mediaSource, position)

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

最后

如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
在这里插入图片描述
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

全套视频资料:

一、面试合集

在这里插入图片描述
二、源码解析合集
在这里插入图片描述

三、开源框架合集
在这里插入图片描述
欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取↓↓↓

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/299249
推荐阅读
相关标签
  

闽ICP备14008679号