Android Media3(六)— 缓存相关操作

之前的文章Android Media3(三)--- 提前缓存视频介绍了如何使用Media3库实现缓存视频,有掘友问怎么删除视频的缓存以及怎么获取已缓存视频的文件。本文在使用Media3缓存视频的基础上,进一步介绍如何通过Cache类删除视频的缓存以及获取已缓存视频。

删除缓存

Media3库通过Cache类来实现缓存视频,也可以调用Cache.removeResource方法传入String类型的参数key来删除指定的缓存。

通过链接删除缓存

默认情况下,通过缓存视频的链接删除对应的缓存,示例代码如下:

  • CacheController

用于初始化缓存配置,添加、删除缓存。

kotlin 复制代码
class CacheController(context: Context) {

    private val cache: Cache
    private val cacheDataSourceFactory: CacheDataSource.Factory
    private val cacheDataSource: CacheDataSource

    init {
        // 设置缓存目录和缓存机制,如果不需要清除缓存可以使用NoOpCacheEvictor
        cache = SimpleCache(File(if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()) context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) else context.filesDir, "example_media_cache"),
            LeastRecentlyUsedCacheEvictor(100 * 1024 * 1024),
            ExampleDatabaseProvider(context)
        )
        // 根据缓存目录创建缓存数据源
        cacheDataSourceFactory = CacheDataSource.Factory()
            .setCache(cache)
            // 设置上游数据源,缓存未命中时通过此获取数据
            .setUpstreamDataSourceFactory(DefaultHttpDataSource.Factory().setAllowCrossProtocolRedirects(true))
        cacheDataSource = cacheDataSourceFactory.createDataSource()
    }

    companion object {

        @Volatile
        private var cacheController: CacheController? = null

        fun init(context: Context) {
            if (cacheController == null) {
                synchronized(CacheController::class.java) {
                    if (cacheController == null) {
                        cacheController = CacheController(context)
                    }
                }
            }
        }

        fun cacheMedia(mediaSources: ArrayList<String>) {
            cacheController?.run {
                mediaSources.forEach { mediaUrl ->
                    // 创建CacheWriter缓存数据
                    CacheWriter(
                        cacheDataSource,
                        DataSpec.Builder()
                            // 设置资源链接
                            .setUri(mediaUrl)
                            // 设置需要缓存的大小(可以只缓存一部分)
                            .setLength((getMediaResourceSize(mediaUrl) * 0.1).toLong())
                            .build(),
                        null
                    ) { requestLength, bytesCached, newBytesCached ->
                        // 缓冲进度变化时回调
                        // requestLength 请求总大小
                        // bytesCached 已缓冲的字节数
                        // newBytesCached 新缓冲的字节数
                    }.let { cacheWriter ->
                        cacheWriter.cache()
                    }
                }
            }
        }

        fun removeCache(key: String) {
            cacheController?.cache?.removeResource(key)
        }
    }

    private fun getMediaResourceSize(mediaUrl: String): Long {
        try {
            val connection = URL(mediaUrl).openConnection() as HttpURLConnection
            connection.requestMethod = "HEAD"
            connection.connect()
            if (connection.responseCode == HttpURLConnection.HTTP_OK) {
                return connection.getHeaderField("Content-Length").toLong()
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return 0L
    }
}
  • Applicaiton

提前缓存视频。

kotlin 复制代码
class ExampleApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        CacheController.init(this)
        // 网络请求需要在子线程中进行
        GlobalScope.launch(Dispatchers.IO) {
            CacheController.cacheMedia(arrayListOf("https://minigame.vip/Uploads/images/2021/09/18/1631951892_page_img.mp4"))
        }
    }
}
  • 示例页面

演示添加播放视频资源,删除缓存。

kotlin 复制代码
class Media3ExampleActivity : AppCompatActivity() {

    private lateinit var binding: LayoutMedia3ExampleActivityBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = LayoutMedia3ExampleActivityBinding.inflate(layoutInflater).also {
            setContentView(it.root)
        }
        binding.includeTitle.tvTitle.text = "Media3 Example"

        val exoPlayerBuilder = ExoPlayer.Builder(this)
        CacheController.getMediaSourceFactory()?.let { exoPlayerBuilder.setMediaSourceFactory(it) }
        binding.playView.player = exoPlayerBuilder.build()
        binding.playView.player?.run {
            repeatMode = Player.REPEAT_MODE_ALL
            playWhenReady = true
        }

        binding.btnPlaySingleVideo.setOnClickListener {
            binding.playView.player?.run {
                stop()
                setMediaItem(MediaItem.Builder()
                    .setUri("https://minigame.vip/Uploads/images/2021/09/18/1631951892_page_img.mp4")
                    .build())
                prepare()
            }
        }

        binding.btnRemoveCache.setOnClickListener {
            lifecycleScope.launch(Dispatchers.IO) {
                // 建议在子线程中调用
                CacheController.removeCache("https://minigame.vip/Uploads/images/2021/09/18/1631951892_page_img.mp4")
            }
        }
    }
}

效果如下:

删除前 删除后

通过自定义Key删除缓存

如果不想使用链接作为key,可以使用自定义的字符串作为key,调整后示例代码(重复部分省略)如下:

  • CacheController
kotlin 复制代码
class CacheController(context: Context) {
    
    ......

    companion object {

        ......

        fun cacheMedia(mediaUrl: String, key: String = "") {
            cacheController?.run {
                val dataSpecBuilder = DataSpec.Builder()
                    .setUri(mediaUrl)
                    .setLength((getMediaResourceSize(mediaUrl) * 0.1).toLong())
                if (key.isNotEmpty()) {
                    dataSpecBuilder.setKey(key)
                }
                CacheWriter(cacheDataSource, dataSpecBuilder.build(), null) { requestLength, bytesCached, newBytesCached ->
                    ......
                }.let { cacheWriter ->
                    ......
                }
            }
        }
        
        ......
    }
}
  • Application类
kotlin 复制代码
class ExampleApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        CacheController.init(this)
        GlobalScope.launch(Dispatchers.IO) {
            CacheController.cacheMedia("https://minigame.vip/Uploads/images/2021/09/18/1631951892_page_img.mp4", "testVideo")
        }
    }
}
  • 示例页面
scss 复制代码
class Media3ExampleActivity : AppCompatActivity() {

    ......

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ......

        binding.btnPlaySingleVideo.setOnClickListener {
            binding.playView.player?.run {
                stop()
                setMediaItem(MediaItem.Builder()
                    .setUri("https://minigame.vip/Uploads/images/2021/09/18/1631951892_page_img.mp4")
                    .setCustomCacheKey("testVideo")
                    .build())
                prepare()
            }
        }

        binding.btnRemoveCache.setOnClickListener {
            lifecycleScope.launch(Dispatchers.IO) {
                CacheController.removeCache("testVideo")
            }
        }
    }
}

PS: MediaItem.Builder.setCustomCacheKey传入的值要与DataSpec.Builder.setKey传入的值保持一致。

获取缓存的视频

Media3库会通过CacheSpan拆分要缓存的视频并以.v3.exo格式存储,很明显这不是常见的视频格式。

Cache类提供了getCachedSpans方法,可以传入key获取已缓存的CacheSpanCacheSpan则提供了获取存储文件的方法,试试看直接将所有CacheSpan文件合并为一个mp4文件是否可行,代码(重复部分省略)如下:

  • CacheController
kotlin 复制代码
class CacheController(context: Context) {

    ......

    companion object {

        ......

        fun transformCacheToVideo(context: Context, key: String = ""): File? {
            var transformFile: File? = null
            cacheController?.cache?.run {
                key.ifEmpty {
                    keys.firstOrNull()
                }?.let {
                    transformFile = cacheController?.transformCacheSpanToMp4(context, getCachedSpans(it))
                }
            }
            return transformFile
        }
        
        ......
    }

    ......

    private fun transformCacheSpanToMp4(context: Context, cacheSpans: NavigableSet<CacheSpan>): File {
        val targetMp4File = File(if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()) {
            context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
        } else {
            context.filesDir
        }, "testVideo.mp4")
        if (!targetMp4File.exists()) {
            targetMp4File.createNewFile()
            FileOutputStream(targetMp4File, true).use { output ->
                cacheSpans.forEach { cacheSpan ->
                    FileInputStream(cacheSpan.file).let { input ->
                        val buffer = ByteArray(1024)
                        var length: Int
                        while (input.read(buffer).also { length = it } > 0) {
                            output.write(buffer, 0, length)
                        }
                        input.close()
                    }
                }
            }
        }
        return targetMp4File
    }
}
  • 示例页面
scss 复制代码
class Media3ExampleActivity : AppCompatActivity() {
    
    ......
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        ......
        
        binding.btnTransformCacheSpan.setOnClickListener {
            // 原有exoPlayer设置了数据源,播放本地文件会失败,重新创建一个不设置数据源的播放器
            binding.playView.player?.stop()
            binding.playView.player?.release()
            binding.playView.player = ExoPlayer.Builder(this).build()
            binding.playView.player?.run {
                addListener(playerListener)
                repeatMode = Player.REPEAT_MODE_ALL
                playWhenReady = true
            }

            lifecycleScope.launch(Dispatchers.IO) {
                CacheController.transformCacheToVideo(this@Media3ExampleActivity, "testVideo")?.let {
                    withContext(Dispatchers.Main) {
                        binding.playView.player?.run {
                            setMediaItem(MediaItem.Builder()
                                .setUri(it.toUri())
                                .build())
                            prepare()
                        }
                    }
                }
            }
        }
    }
    
    ......
}

效果如图:

PS: 如果初始只缓存了视频的一部分,要等视频完整缓存完之后再合并。

示例

演示代码已在示例Demo中添加。

ExampleDemo github

ExampleDemo gitee

相关推荐
枯骨成佛43 分钟前
Android中Crash Debug技巧
android
kim56596 小时前
android studio 更改gradle版本方法(备忘)
android·ide·gradle·android studio
咸芝麻鱼6 小时前
Android Studio | 最新版本配置要求高,JDK运行环境不适配,导致无法启动App
android·ide·android studio
无所谓จุ๊บ6 小时前
Android Studio使用c++编写
android·c++
csucoderlee7 小时前
Android Studio的新界面New UI,怎么切换回老界面
android·ui·android studio
kim56597 小时前
各版本android studio下载地址
android·ide·android studio
饮啦冰美式7 小时前
Android Studio 将项目打包成apk文件
android·ide·android studio
夜色。7 小时前
Unity6 + Android Studio 开发环境搭建【备忘】
android·unity·android studio
ROCKY_8178 小时前
AndroidStudio-滚动视图ScrollView
android
趴菜小玩家9 小时前
使用 Gradle 插件优化 Flutter Android 插件开发中的 Flutter 依赖缺失问题
android·flutter·gradle