在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()
    }
}

效果如图:

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

提早缓存

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(),
                        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中获取。

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

效果如图:

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

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

示例

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

ExampleDemo github

ExampleDemo gitee