添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

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

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

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

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前缀的表中读写数据。

    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) {
    
    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()
    

    效果如图:

    无缓存边播边缓存
    无缓存 -middle-original.gif边播边缓存 -middle-original.gif

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

  • CacheController类
  • 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(),
                        ) { 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中获取。

    class Media3ExampleActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            // 设置逐步加载数据的缓存数据源
            CacheController.getMediaSourceFactory()?.let { exoPlayerBuilder.setMediaSourceFactory(it) }
    
  • Application类
  • 在Application类中初始化CacheController,并提前进行预加载。

    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

    全部评论
    空 还没有回复哦~

    相关推荐

    点赞 评论 收藏
    分享
    10-21 11:35
    已编辑
    门头沟学院 Java
    代码渣渣正在背八股: 不招35岁以上,你的简历已进入人才库。
    点赞 评论 收藏
    分享
    在秋招的香菇很中二: 把实践经历、校园经历删了,把课设包装成项目经历写上去。
    点赞 评论 收藏
    分享
    不愿透露姓名的神秘牛友
    昨天 19:32
    投递恒生电子股份有限公司等公司10个岗位 > 你都收到了哪些公司的感谢信?
    点赞 评论 收藏
    分享
    点赞 收藏 评论
    分享

    全站热榜