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

实现缓存视频

播放时缓存

将ExoPlayer的MediaSourceFactory设置为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
    }
}
  • 播放页面

播放页主要的变化就是ExoPlayer的MediaSourceFactory从CacheController中获取。

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

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