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获取已缓存的CacheSpan,CacheSpan则提供了获取存储文件的方法,试试看直接将所有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

相关推荐
千里马学框架4 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台4 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone4 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc4 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo4 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077004 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼4 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone4 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen4 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone4 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui