在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")
}
完成缓存视频
播映时缓存
将ExoPlayer
的MediaSourceFactory
设置为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()
}
}
效果如图:
无缓存 | 边播边缓存 |
---|---|
提早缓存
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
}
}
- 播映页面
播映页主要的改变就是ExoPlayer
的MediaSourceFactory
从CacheController
中获取。
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