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

相关推荐
sun0077003 小时前
android ndk编译valgrind
android
AI视觉网奇4 小时前
android studio 断点无效
android·ide·android studio
jiaxi的天空4 小时前
android studio gradle 访问不了
android·ide·android studio
No Silver Bullet5 小时前
android组包时会把从maven私服获取的包下载到本地吗
android
catchadmin5 小时前
PHP serialize 序列化完全指南
android·开发语言·php
tangweiguo030519877 小时前
Kable使用指南:Android BLE开发的现代化解决方案
android·kotlin
00后程序员张9 小时前
iOS App 混淆与资源保护:iOS配置文件加密、ipa文件安全、代码与多媒体资源防护全流程指南
android·安全·ios·小程序·uni-app·cocoa·iphone
柳岸风10 小时前
Android Studio Meerkat | 2024.3.1 Gradle Tasks不展示
android·ide·android studio
编程乐学11 小时前
安卓原创--基于 Android 开发的菜单管理系统
android
whatever who cares13 小时前
android中ViewModel 和 onSaveInstanceState 的最佳使用方法
android