Android Media3(三)— 提前缓存视频

在App的开发中偶尔会需要播放网络视频,播放网络视频肯定就绕不开提前缓存的功能。本文简单介绍下Media3库怎么实现提前缓存视频的功能。

添加依赖

在app module下的build.gradle中添加代码,如下:

scss 复制代码
dependencies {
    implementation("androidx.media3:media3-ui:1.1.0")
    implementation("androidx.media3:media3-session:1.1.0")
    implementation("androidx.media3:media3-exoplayer:1.1.0")
}

实现缓存视频

播放时缓存

ExoPlayerMediaSourceFactory设置为CacheDataSource.Factory,就可以在播放过程中缓存视频,之后再播放同个网络视频时就无需等待太久,代码如下:

  • DatabaseProvider

为媒体库提供数据库实例,向带有ExoPlayer前缀的表中读写数据。

kotlin 复制代码
class ExampleDatabaseProvider(
    context: Context,
    databaseName: String = "example_exoplayer_internal.db",
    version: Int = 1
) : SQLiteOpenHelper(context.applicationContext, databaseName, null, version), DatabaseProvider {

    override fun onCreate(sqLiteDatabase: SQLiteDatabase?) {
    }

    override fun onUpgrade(sqLiteDatabase: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {

    }
}
  • 播放页面
kotlin 复制代码
class Media3ExampleActivity : AppCompatActivity() {

    private lateinit var binding: LayoutMedia3ExampleActivityBinding
    private lateinit var cache: Cache

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

        val cacheParentDirectory = if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()) {
            File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), packageName)
        } else {
            File(filesDir, packageName)
        }

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

        // 创建ExoPlayer,配置到PlayerView中
        val exoPlayerBuilder = ExoPlayer.Builder(this)
        // 设置逐步加载数据的缓存数据源
        exoPlayerBuilder.setMediaSourceFactory(ProgressiveMediaSource.Factory(cacheDataSourceFactory))
        binding.playView.player = exoPlayerBuilder.build()
        binding.playView.player?.run {
            // 设置播放监听
            addListener(object : Player.Listener {
                override fun onIsPlayingChanged(isPlaying: Boolean) {
                    super.onIsPlayingChanged(isPlaying)
                    // 播放状态变化回调
                }

                override fun onPlaybackStateChanged(playbackState: Int) {
                    super.onPlaybackStateChanged(playbackState)
                    when (playbackState) {
                        Player.STATE_IDLE -> {
                            //播放器停止时的状态
                        }

                        Player.STATE_BUFFERING -> {
                            // 正在缓冲数据
                        }

                        Player.STATE_READY -> {
                            // 可以开始播放
                        }

                        Player.STATE_ENDED -> {
                            // 播放结束
                        }
                    }

                }

                override fun onPlayerError(error: PlaybackException) {
                    super.onPlayerError(error)
                    // 获取播放错误信息
                }
            })
            // 设置重复模式
            // Player.REPEAT_MODE_ALL 无限重复
            // Player.REPEAT_MODE_ONE 重复一次
            // Player.REPEAT_MODE_OFF 不重复
            repeatMode = Player.REPEAT_MODE_ALL
            // 设置当缓冲完毕后直接播放视频
            playWhenReady = true
        }
        binding.btnPlaySingleVideo.setOnClickListener {
            binding.playView.player?.run {
                // 停止之前播放的视频
                stop()
                //设置单个资源
                setMediaItem(MediaItem.fromUri("https://minigame.vip/Uploads/images/2021/09/18/1631951892_page_img.mp4"))
                // 开始缓冲
                prepare()
            }
        }
        binding.btnPlayMultiVideo.setOnClickListener {
            binding.playView.player?.run {
                // 停止之前播放的视频
                stop()
                // 设置多个资源,当一个视频播完后自动播放下一个
                setMediaItems(arrayListOf(
                    MediaItem.fromUri("https://minigame.vip/Uploads/images/2021/09/18/1631951892_page_img.mp4"),
                    MediaItem.fromUri("https://storage.googleapis.com/exoplayer-test-media-1/mp4/dizzy-with-tx3g.mp4")
                ))
                // 开始缓冲
                prepare()
            }
        }
    }

    override fun onResume() {
        super.onResume()
        // 恢复播放
        binding.playView.onResume()
    }

    override fun onPause() {
        super.onPause()
        // 暂停播放
        binding.playView.onPause()
    }

    override fun onDestroy() {
        super.onDestroy()
        // 释放播放器资源
        binding.playView.player?.release()
        binding.playView.player = null
        cache.release()
    }
}

效果如图:

无缓存 边播边缓存

提前缓存

Media3库提供了CacheWriter类,可用于提前缓存视频。CacheWriter需要用到CacheDataSource.Factory生成的CacheDataSource,提前加载又是发生在播放视频之前,因此把CacheDataSource.Factory的配置提取到一个公共类CacheController中。示例中在Application中提前调用缓存方法,代码如下:

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

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

    private val cacheTask: ConcurrentHashMap<String, CacheWriter> = ConcurrentHashMap()

    init {
        val cacheParentDirectory = if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()) {
            File(context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), context.packageName)
        } else {
            File(context.filesDir, context.packageName)
        }

        // 设置缓存目录和缓存机制,如果不需要清除缓存可以使用NoOpCacheEvictor
        cache = SimpleCache(File(cacheParentDirectory, "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()
                        cacheTask[mediaUrl] = cacheWriter
                    }
                }
            }
        }

        fun cancelCache(mediaUrl: String) {
            // 取消缓存
            cacheController?.cacheTask?.get(mediaUrl)?.cancel()
        }
        
        fun getMediaSourceFactory(): MediaSource.Factory? {
            var mediaSourceFactory: MediaSource.Factory? = null
            cacheController?.run {
                // 创建逐步加载数据的数据源
                mediaSourceFactory = ProgressiveMediaSource.Factory(cacheDataSourceFactory)
            }
            return mediaSourceFactory
        }

        fun release() {
            cacheController?.cacheTask?.values?.forEach { it.cancel() }
            cacheController?.cache?.release()
        }
    }

    // 获取媒体资源的大小
    private fun getMediaResourceSize(mediaUrl: String): Long {
        try {
            val connection = URL(mediaUrl).openConnection() as HttpURLConnection
            // 请求方法设置为HEAD,只获取请求头
            connection.requestMethod = "HEAD"
            connection.connect()
            if (connection.responseCode == HttpURLConnection.HTTP_OK) {
                return connection.getHeaderField("Content-Length").toLong()
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return 0L
    }
}
  • 播放页面

播放页主要的变化就是ExoPlayerMediaSourceFactoryCacheController中获取。

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

    ...

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        ...
        
        // 设置逐步加载数据的缓存数据源
        CacheController.getMediaSourceFactory()?.let { exoPlayerBuilder.setMediaSourceFactory(it) }
        
        ...
    }

    ...
}
  • Application类

在Application类中初始化CacheController,并提前进行预加载。

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"))
        }
    }
}

效果如图:

需要注意的是,在测试过程中发现,使用CacheWriter缓存资源时,需要等DataSpec设置的整个资源都缓存完成,ExoPlayer播放时才会直接从缓存数据源中获取数据,否则仍然会直接从上游数据源中获取数据,所以视频资源比较大的情况下,提前缓存最好只缓存一部分。

示例

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

ExampleDemo github

ExampleDemo gitee

相关推荐
AmazingMQ9 分钟前
Android 13----在framworks层映射一个物理按键
android
小李飞飞砖21 分钟前
Android 插件化实现原理详解
android
m0_5973453129 分钟前
【Android】安卓四大组件之广播接收器(Broadcast Receiver):从基础到进阶
android·java·boradcast·安卓四大组件
whysqwhw1 小时前
OkHttp PublicSuffix包的后缀列表处理
android
yeziyfx1 小时前
kotlin中集合的用法
android·开发语言·kotlin
EngZegNgi3 小时前
安卓应用启动崩溃的问题排查记录
android·crash·启动崩溃
火柴就是我4 小时前
每日见闻之Container Decoration
android·flutter
天枢破军4 小时前
【AOSP】解决repo拉取提示无法连接android.googlesource.com
android
whysqwhw4 小时前
OkHttp之AndroidPlatform类分析
android
XiaolongTu4 小时前
Kotlin Flow详述:从一个“卡顿”问题到线程切换的本质
android·面试