在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中添加。