之前的文章Android Media3(三)--- 提前缓存视频
介绍了如何使用Media3库实现缓存视频,有掘友问怎么删除视频的缓存以及怎么获取已缓存视频的文件。本文在使用Media3缓存视频的基础上,进一步介绍如何通过Cache
类删除视频的缓存以及获取已缓存视频。
删除缓存
Media3库通过Cache
类来实现缓存视频,也可以调用Cache.removeResource
方法传入String
类型的参数key来删除指定的缓存。
通过链接删除缓存
默认情况下,通过缓存视频的链接删除对应的缓存,示例代码如下:
CacheController
类
用于初始化缓存配置,添加、删除缓存。
kotlin
class CacheController(context: Context) {
private val cache: Cache
private val cacheDataSourceFactory: CacheDataSource.Factory
private val cacheDataSource: CacheDataSource
init {
// 设置缓存目录和缓存机制,如果不需要清除缓存可以使用NoOpCacheEvictor
cache = SimpleCache(File(if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()) context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) else context.filesDir, "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()
}
}
}
}
fun removeCache(key: String) {
cacheController?.cache?.removeResource(key)
}
}
private fun getMediaResourceSize(mediaUrl: String): Long {
try {
val connection = URL(mediaUrl).openConnection() as HttpURLConnection
connection.requestMethod = "HEAD"
connection.connect()
if (connection.responseCode == HttpURLConnection.HTTP_OK) {
return connection.getHeaderField("Content-Length").toLong()
}
} catch (e: Exception) {
e.printStackTrace()
}
return 0L
}
}
Applicaiton
类
提前缓存视频。
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"))
}
}
}
- 示例页面
演示添加播放视频资源,删除缓存。
kotlin
class Media3ExampleActivity : AppCompatActivity() {
private lateinit var binding: LayoutMedia3ExampleActivityBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = LayoutMedia3ExampleActivityBinding.inflate(layoutInflater).also {
setContentView(it.root)
}
binding.includeTitle.tvTitle.text = "Media3 Example"
val exoPlayerBuilder = ExoPlayer.Builder(this)
CacheController.getMediaSourceFactory()?.let { exoPlayerBuilder.setMediaSourceFactory(it) }
binding.playView.player = exoPlayerBuilder.build()
binding.playView.player?.run {
repeatMode = Player.REPEAT_MODE_ALL
playWhenReady = true
}
binding.btnPlaySingleVideo.setOnClickListener {
binding.playView.player?.run {
stop()
setMediaItem(MediaItem.Builder()
.setUri("https://minigame.vip/Uploads/images/2021/09/18/1631951892_page_img.mp4")
.build())
prepare()
}
}
binding.btnRemoveCache.setOnClickListener {
lifecycleScope.launch(Dispatchers.IO) {
// 建议在子线程中调用
CacheController.removeCache("https://minigame.vip/Uploads/images/2021/09/18/1631951892_page_img.mp4")
}
}
}
}
效果如下:
删除前 | 删除后 |
---|---|
通过自定义Key删除缓存
如果不想使用链接作为key,可以使用自定义的字符串作为key,调整后示例代码(重复部分省略)如下:
CacheController
类
kotlin
class CacheController(context: Context) {
......
companion object {
......
fun cacheMedia(mediaUrl: String, key: String = "") {
cacheController?.run {
val dataSpecBuilder = DataSpec.Builder()
.setUri(mediaUrl)
.setLength((getMediaResourceSize(mediaUrl) * 0.1).toLong())
if (key.isNotEmpty()) {
dataSpecBuilder.setKey(key)
}
CacheWriter(cacheDataSource, dataSpecBuilder.build(), null) { requestLength, bytesCached, newBytesCached ->
......
}.let { cacheWriter ->
......
}
}
}
......
}
}
- Application类
kotlin
class ExampleApplication : Application() {
override fun onCreate() {
super.onCreate()
CacheController.init(this)
GlobalScope.launch(Dispatchers.IO) {
CacheController.cacheMedia("https://minigame.vip/Uploads/images/2021/09/18/1631951892_page_img.mp4", "testVideo")
}
}
}
- 示例页面
scss
class Media3ExampleActivity : AppCompatActivity() {
......
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
......
binding.btnPlaySingleVideo.setOnClickListener {
binding.playView.player?.run {
stop()
setMediaItem(MediaItem.Builder()
.setUri("https://minigame.vip/Uploads/images/2021/09/18/1631951892_page_img.mp4")
.setCustomCacheKey("testVideo")
.build())
prepare()
}
}
binding.btnRemoveCache.setOnClickListener {
lifecycleScope.launch(Dispatchers.IO) {
CacheController.removeCache("testVideo")
}
}
}
}
PS: MediaItem.Builder.setCustomCacheKey
传入的值要与DataSpec.Builder.setKey
传入的值保持一致。
获取缓存的视频
Media3库会通过CacheSpan
拆分要缓存的视频并以.v3.exo格式存储,很明显这不是常见的视频格式。
Cache
类提供了getCachedSpans
方法,可以传入key获取已缓存的CacheSpan
,CacheSpan
则提供了获取存储文件的方法,试试看直接将所有CacheSpan
文件合并为一个mp4文件是否可行,代码(重复部分省略)如下:
CacheController
类
kotlin
class CacheController(context: Context) {
......
companion object {
......
fun transformCacheToVideo(context: Context, key: String = ""): File? {
var transformFile: File? = null
cacheController?.cache?.run {
key.ifEmpty {
keys.firstOrNull()
}?.let {
transformFile = cacheController?.transformCacheSpanToMp4(context, getCachedSpans(it))
}
}
return transformFile
}
......
}
......
private fun transformCacheSpanToMp4(context: Context, cacheSpans: NavigableSet<CacheSpan>): File {
val targetMp4File = File(if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()) {
context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
} else {
context.filesDir
}, "testVideo.mp4")
if (!targetMp4File.exists()) {
targetMp4File.createNewFile()
FileOutputStream(targetMp4File, true).use { output ->
cacheSpans.forEach { cacheSpan ->
FileInputStream(cacheSpan.file).let { input ->
val buffer = ByteArray(1024)
var length: Int
while (input.read(buffer).also { length = it } > 0) {
output.write(buffer, 0, length)
}
input.close()
}
}
}
}
return targetMp4File
}
}
- 示例页面
scss
class Media3ExampleActivity : AppCompatActivity() {
......
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
......
binding.btnTransformCacheSpan.setOnClickListener {
// 原有exoPlayer设置了数据源,播放本地文件会失败,重新创建一个不设置数据源的播放器
binding.playView.player?.stop()
binding.playView.player?.release()
binding.playView.player = ExoPlayer.Builder(this).build()
binding.playView.player?.run {
addListener(playerListener)
repeatMode = Player.REPEAT_MODE_ALL
playWhenReady = true
}
lifecycleScope.launch(Dispatchers.IO) {
CacheController.transformCacheToVideo(this@Media3ExampleActivity, "testVideo")?.let {
withContext(Dispatchers.Main) {
binding.playView.player?.run {
setMediaItem(MediaItem.Builder()
.setUri(it.toUri())
.build())
prepare()
}
}
}
}
}
}
......
}
效果如图:
PS: 如果初始只缓存了视频的一部分,要等视频完整缓存完之后再合并。
示例
演示代码已在示例Demo中添加。