【问题标题】:Using cache in ExoPlayer在 ExoPlayer 中使用缓存
【发布时间】:2023-08-13 20:09:01
【问题描述】:

我正在寻找在 ExoPlayer 中实现缓存的任何示例。

ExoPlayer 在它的库中有关于缓存的不同类,Google 在这个video 中解释说我们可以使用 CacheDataSource 类来实现它,但 Google 没有提供任何关于它的演示。不幸的是,这似乎使用起来相当复杂,所以我目前正在寻找示例(在 Google 上没有成功)。

有没有人成功或有任何有用的信息?谢谢。

【问题讨论】:

标签: android caching media exoplayer


【解决方案1】:

这是 ExoPlayer 2.+ 的解决方案

创建自定义缓存数据源工厂

public class CacheDataSourceFactory implements DataSource.Factory {
    private final Context context;
    private final DefaultDataSourceFactory defaultDatasourceFactory;
    private final long maxFileSize, maxCacheSize;

    public CacheDataSourceFactory(Context context, long maxCacheSize, long maxFileSize) {
        super();
        this.context = context;
        this.maxCacheSize = maxCacheSize;
        this.maxFileSize = maxFileSize;
        String userAgent = Util.getUserAgent(context, context.getString(R.string.app_name));
        DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
        defaultDatasourceFactory = new DefaultDataSourceFactory(this.context,
                bandwidthMeter,
                new DefaultHttpDataSourceFactory(userAgent, bandwidthMeter));
    }

    @Override
    public DataSource createDataSource() {
        LeastRecentlyUsedCacheEvictor evictor = new LeastRecentlyUsedCacheEvictor(maxCacheSize);
        SimpleCache simpleCache = new SimpleCache(new File(context.getCacheDir(), "media"), evictor);
        return new CacheDataSource(simpleCache, defaultDatasourceFactory.createDataSource(),
                new FileDataSource(), new CacheDataSink(simpleCache, maxFileSize),
                CacheDataSource.FLAG_BLOCK_ON_CACHE | CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR, null);
    }
}

还有玩家

BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
TrackSelection.Factory videoTrackSelectionFactory =
        new AdaptiveTrackSelection.Factory(bandwidthMeter);
TrackSelector trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory);

SimpleExoPlayer exoPlayer = ExoPlayerFactory.newSimpleInstance(this, trackSelector);
MediaSource audioSource = new ExtractorMediaSource(Uri.parse(url),
            new CacheDataSourceFactory(context, 100 * 1024 * 1024, 5 * 1024 * 1024), new DefaultExtractorsFactory(), null, null);
exoPlayer.setPlayWhenReady(true); 
exoPlayer.prepare(audioSource);

效果很好。

【讨论】:

  • 我对这个解决方案的问题是,如果我使用它来缓存和播放多个视频,多个播放器可能会播放相同的数据(即,对于不同的 uri,似乎从缓存返回相同的数据流)。使用默认 ExtractorMediaSource 时不会发生此问题。视频的 uri 是独一无二的
  • 问题的解决方案:保留一个SimpleCache的共享实例,而不是在createDataSource中创建它。否则多个 Cache 对象将写入相同的文件导致麻烦
  • thnx.its 可以工作,但是加密缓存有什么解决方案吗?
  • 是否支持磁盘缓存??还是只在内存缓存中?
  • @Bao Le,这个实现的预期行为应该是缓存流的视频播放也应该离线发生,对吧?但是,尽管它是缓存流,但当网络断开连接时,我无法进行播放。视频播放显然只能在线播放吗?还是我在这里遗漏了什么?
【解决方案2】:

默认情况下,ExoPlayer 不缓存媒体(视频、音频等)。例如,如果你想播放一个在线视频文件,每次 ExoPlayer 都会打开一个连接,读取数据然后播放它。

幸运的是,它为我们提供了一些接口和实现类来支持在我们的应用中缓存媒体。

您可以编写自己的缓存来实现 ExoPlayer 的给定接口。为了简单起见,我将指导您如何使用实现类来启用缓存。

步骤 1: 指定一个包含您的媒体文件的文件夹,在 Android 中对于较小的缓存文件夹(小于 1MB),您应该使用getCacheDir,否则您可以指定您喜欢的缓存文件夹,以getFileDir 为例。

第 2 步:指定缓存文件夹的大小,以及达到该大小时的策略。有 2 个 API

  • NoOpCacheEvictor 永远不会驱逐/删除缓存文件。根据缓存文件夹的位置,如果它在内部存储中,当用户清除应用数据或卸载应用时,该文件夹将被删除。
  • LeastRecentlyUsedCacheEvictor 将首先驱逐/删除最近最少使用的缓存文件。例如,如果您的缓存大小为 10MB,当达到该大小时,它会自动查找并删除最近最少使用的文件。

把它放在一起

val renderersFactory = DefaultRenderersFactory(context.applicationContext)
val trackSelector = DefaultTrackSelector()
val loadControl = DefaultLoadControl()

val player = ExoPlayerFactory.newSimpleInstance(context, renderersFactory, trackSelector, loadControl)
player.addListener(this)

// Specify cache folder, my cache folder named media which is inside getCacheDir.
val cacheFolder = File(context.cacheDir, "media")

// Specify cache size and removing policies
val cacheEvictor = LeastRecentlyUsedCacheEvictor(1 * 1024 * 1024) // My cache size will be 1MB and it will automatically remove least recently used files if the size is reached out.

// Build cache
val cache = SimpleCache(cacheFolder, cacheEvictor)

// Build data source factory with cache enabled, if data is available in cache it will return immediately, otherwise it will open a new connection to get the data.
val cacheDataSourceFactory = CacheDataSourceFactory(cache, DefaultHttpDataSourceFactory("ExoplayerDemo"))

val uri = Uri.parse("Put your media url here")
val mediaSource = ExtractorMediaSource.Factory(cacheDataSourceFactory).createMediaSource(uri)

player.prepare(mediaSource)

【讨论】:

  • ExtractorMediaSource 的 CTOR 似乎已弃用。我认为它应该是这样的:val mediaSource = ExtractorMediaSource.Factory(cacheDataSourceFactory).createMediaSource(uri)。你能确认应该是这样吗?
  • 感谢您查看。您不必保留旧答案。人们通常不会故意使用旧版本的 SDK...
  • 在上述方法中加入这一行:playerView.setPlayer(player);
  • 我想补充一点,当我们创建 LeastRecentlyUsedCacheEvictor 时,我们传入的构造函数是内存缓存大小而不是磁盘缓存大小
  • 这个答案帮助我缓存 HLS 视频。谢谢@Son Truong
【解决方案3】:

我在这里回答了类似的问题:https://*.com/a/58678192/2029134

基本上,我使用这个库:https://github.com/danikula/AndroidVideoCache 从 URL 缓存文件 然后放到ExoPlayer中。

这里是示例代码:

String mediaURL = "https://my_cool_vid.com/vi.mp4";
SimpleExoPlayer exoPlayer = ExoPlayerFactory.newSimpleInstance(getContext());
HttpProxyCacheServer proxyServer = HttpProxyCacheServer.Builder(getContext()).maxCacheSize(1024 * 1024 * 1024).build();

String proxyURL = proxyServer.getProxyUrl(mediaURL);


DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(getContext(),
                Util.getUserAgent(getContext(), getActivity().getApplicationContext().getPackageName()));


exoPlayer.prepare(new ProgressiveMediaSource.Factory(dataSourceFactory)
                .createMediaSource(Uri.parse(proxyURL)););

希望对您有所帮助。

【讨论】:

    【解决方案4】:

    要解决多个视频或进程尝试访问同一个缓存的问题,您需要一个真正的 Singleton。一种可靠的方法是这样做:

    object VideoCache {
        private var sDownloadCache: SimpleCache? = null
        private const val maxCacheSize: Long = 100 * 1024 * 1024
    
        fun getInstance(context: Context): SimpleCache {
            val evictor = LeastRecentlyUsedCacheEvictor(maxCacheSize)
            if (sDownloadCache == null) sDownloadCache = SimpleCache(File(context.cacheDir, "koko-media"), evictor)
            return sDownloadCache as SimpleCache
        }
    }
    

    您现在可以使用:

    private val simpleCache: SimpleCache by lazy {
            VideoCache.getInstance(context)
        }
    

    【讨论】:

      【解决方案5】:

      这里是一个用 OkHttp 替换 demo 数据源的例子,默认是没有缓存 https://github.com/b95505017/ExoPlayer/commit/ebfdda8e7848a2e2e275f5c0525f614b56ef43a6 https://github.com/b95505017/ExoPlayer/tree/okhttp_http_data_source 所以,你只需要正确配置 OkHttp 缓存,请求就会被缓存。

      【讨论】:

      • 我从 exoplayer 2.2.0 演示应用中获得了 OkHttpDataSource。您能否也分享一些配置 OkHttp 缓存的链接。
      【解决方案6】:

      我已经在渲染器构建器中这样实现了

      private static final int BUFFER_SEGMENT_SIZE = 64 * 1024;
      private static final int BUFFER_SEGMENT_COUNT = 160;
      
      final String userAgent = Util.getUserAgent(mContext, appName);
      final DefaultBandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
      final Allocator allocator = new DefaultAllocator(BUFFER_SEGMENT_SIZE);*
      
      Cache cache = new SimpleCache(context.getCacheDir(), new LeastRecentlyUsedCacheEvictor(1024 * 1024 * 10));
      DataSource dataSource = new DefaultUriDataSource(context, bandwidthMeter, userAgent);
      CacheDataSource cacheDataSource = new CacheDataSource(cache, dataSource, false, false);
      ExtractorSampleSource sampleSource = new ExtractorSampleSource(uri
                      , cacheDataSource
                      , allocator
                      , BUFFER_SEGMENT_COUNT * BUFFER_SEGMENT_SIZE
                      , new Mp4Extractor());
      

      【讨论】:

      • 这段代码编译运行,但似乎没有在指定的缓存文件夹中写入任何视频。它对你有用吗?它是否在没有互联网连接的情况下从缓存播放?更深入的信息将不胜感激。谢谢
      • 也添加了这段代码,但是和上面一样,它看起来并没有缓存任何东西。我们这里有什么遗漏吗?
      • 根据github.com/google/ExoPlayer/issues/420,此答案仅对 DASH 流有效。对于 MP4 文件,OkHttpDataSource 似乎产生了很好的结果(根据该线程上的人)。
      【解决方案7】:

      除了宝乐的回答,这里准备使用CacheDataSourceFactory的Kotlin版本,保留一个SimpleCache的实例来解决多个Cache对象写入同一个目录的问题。

      class CacheDataSourceFactory(private val context: Context,
                                            private val maxCacheSize: Long,
                                            private val maxFileSize: Long) : DataSource.Factory {
      
          private val defaultDatasourceFactory: DefaultDataSourceFactory
          private val simpleCache: SimpleCache by lazy {
              val evictor = LeastRecentlyUsedCacheEvictor(maxCacheSize)
              SimpleCache(File(context.cacheDir, "media"), evictor)
          }
      
          init {
              val userAgent = Util.getUserAgent(context, context.packageName)
              val bandwidthMeter = DefaultBandwidthMeter()
              defaultDatasourceFactory = DefaultDataSourceFactory(context,
                      bandwidthMeter,
                      DefaultHttpDataSourceFactory(userAgent, bandwidthMeter))
          }
      
          override fun createDataSource(): DataSource {
              return CacheDataSource(simpleCache,
                      defaultDatasourceFactory.createDataSource(),
                      FileDataSource(),
                      CacheDataSink(simpleCache, maxFileSize),
                      CacheDataSource.FLAG_BLOCK_ON_CACHE or CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR,
                      null)
          }
      }
      

      【讨论】:

        【解决方案8】:

        这是我在 Kotlin 中的示例(项目可用 here):

        class MainActivity : AppCompatActivity() {
            private var player: SimpleExoPlayer? = null
            override fun onCreate(savedInstanceState: Bundle?) {
                super.onCreate(savedInstanceState)
                if (cache == null) {
                    cache = SimpleCache(File(cacheDir, "media"), LeastRecentlyUsedCacheEvictor(MAX_PREVIEW_CACHE_SIZE_IN_BYTES))
                }
                setContentView(R.layout.activity_main)
            }
        
            override fun onStart() {
                super.onStart()
                playVideo()
            }
        
            private fun playVideo() {
                player = ExoPlayerFactory.newSimpleInstance(this@MainActivity, DefaultTrackSelector())
                playerView.player = player
                player!!.volume = 1f
                player!!.playWhenReady = true
                player!!.repeatMode = Player.REPEAT_MODE_ALL
                player!!.playVideoFromUrl(this, "https://sample-videos.com/video123/mkv/240/big_buck_bunny_240p_20mb.mkv", cache!!)
        //        player!!.playVideoFromUrl(this, "https://sample-videos.com/video123/mkv/720/big_buck_bunny_720p_1mb.mkv", cache!!)
        //        player!!.playVideoFromUrl(this, "https://sample-videos.com/video123/mkv/720/big_buck_bunny_720p_1mb.mkv")
        //        player!!.playRawVideo(this,R.raw.videoplayback)
            }
        
            override fun onStop() {
                super.onStop()
                playerView.player = null
                player!!.release()
                player = null
            }
        
            companion object {
                const val MAX_PREVIEW_CACHE_SIZE_IN_BYTES = 20L * 1024L * 1024L
                var cache: com.google.android.exoplayer2.upstream.cache.Cache? = null
        
                @JvmStatic
                fun getUserAgent(context: Context): String {
                    val packageManager = context.packageManager
                    val info = packageManager.getPackageInfo(context.packageName, 0)
                    val appName = info.applicationInfo.loadLabel(packageManager).toString()
                    return Util.getUserAgent(context, appName)
                }
            }
        
            fun SimpleExoPlayer.playRawVideo(context: Context, @RawRes rawVideoRes: Int) {
                val dataSpec = DataSpec(RawResourceDataSource.buildRawResourceUri(rawVideoRes))
                val rawResourceDataSource = RawResourceDataSource(context)
                rawResourceDataSource.open(dataSpec)
                val factory: DataSource.Factory = DataSource.Factory { rawResourceDataSource }
                prepare(LoopingMediaSource(ExtractorMediaSource.Factory(factory).createMediaSource(rawResourceDataSource.uri)))
            }
        
            fun SimpleExoPlayer.playVideoFromUrl(context: Context, url: String, cache: Cache? = null) = playVideoFromUri(context, Uri.parse(url), cache)
        
            fun SimpleExoPlayer.playVideoFile(context: Context, file: File) = playVideoFromUri(context, Uri.fromFile(file))
        
            fun SimpleExoPlayer.playVideoFromUri(context: Context, uri: Uri, cache: Cache? = null) {
                val factory = if (cache != null)
                    CacheDataSourceFactory(cache, DefaultHttpDataSourceFactory(getUserAgent(context)))
                else
                    DefaultDataSourceFactory(context, MainActivity.getUserAgent(context))
                val mediaSource = ExtractorMediaSource.Factory(factory).createMediaSource(uri)
                prepare(mediaSource)
            }
        }
        

        【讨论】:

          【解决方案9】:

          Exoplayer 的文档列表是class DashDownloader,并有一些用于该类型源的示例代码。 (单击 [Frames] 以返回文档导航。我必须将其删除才能获得深层链接。)

          【讨论】:

          • 这篇文章看起来不像是试图回答这个问题。这里的每一篇文章都应该明确地尝试回答这个问题;如果您有批评或需要澄清问题或其他答案,您可以在其下方直接post a comment(如这个)。请删除此答案并创建评论或新问题。见:Ask questions, get answers, no distractions
          • 我当然想帮助回答最初的问题。 AFAIR 提到的类 DashDownloader 是我对缓存问题的解决方案,因为我需要完全缓存一组媒体文件。由于有些人可能出于同样的原因来到这里,您可能想收回反对票;谢谢。
          【解决方案10】:
          SimpleCache simpleCache = new SimpleCache(new File(context.getCacheDir(), "media/"+id), evictor);
          

          这里,id 必须是唯一的。

          【讨论】: