让 Glide 能够加载任意音视频文件帧
我在前面的文章中有解读过 Glide
的源码,没有看过的可以翻翻我前面的文章,Glide
的缓存(包括磁盘缓存和内存缓存)管理可以说是一个标杆的代码,但是 Glide
默认是无法加载音乐文件的封面图和视频文件的任意帧,如果说到谁解码厉害,那不得不说 FFmpeg
,如果能够让 Glide
和 FFmpeg
他们能够合作一起工作那不是太酷辣?结合优秀的 Glide
缓存管理和 FFmpeg
强悍的解码能力。
回忆一下 Glide 加载一帧网络图片
假设缓存不可用的条件下哈。
首先我们输入的是一个 http
协议的链接字符串作为加载图片的地址,加载的地址被称为 model
,Glide
会去找到一个能够处理 http
协议的 ModelLoader
,我们可以将这个默认 ModelLoader
的替换成 OkHttp
的 ModelLoader
(如果不会的可以看看我前面的文章),当 ModelLoader
当处理了 model
后,就会将加载后的数据(这里的数据类型默认是 InputStream
)交给下一站来处理;
处理的下一站就是 Decoder
,因为从网络上加载的数据是经过编码的,常见的编码就是 PNG
和 JPEG
,Decoder
就需要将编码后的数据解码后就成了 Bitmap
,然后后续又经过一堆操作后就显示到 ImageView
上了,这里就不再讲这个过程了。
还有一个比较关键的步骤就是 Encoder
,Encoder
当 Bitmap
需要缓存到本地文件时,需要通过 Encoder
编码后写入到文件中,编码方式是 PNG
或者是 JPEG
。
Glide 加载音视频文件
首先这里要说明一下,我使用 FFmpeg
加载帧的结果直接是 Bitmap
,如果想要处理得简单不用自定义 Decoder
和 Encoder
,将 FFmpeg
在 ModelLoader
加载的 Bitmap
成功后,将 Bitmap
再次编码成 PNG
或者 JPEG
然后将数据格式转换成 InputStream
就好了,然后就可以了将处理后的数据丢给系统的 Decoder
就好了。不过这里有两个问题,因为这里会多一次编码过程,首先编码成 PNG
和 JPEG
是有损的,同时编码也消耗系统资源。所以我们放弃了这种方案。
前面说到我们使用 FFmpeg
中加载的结果是 Bitmap
,但是默认的 Decoder
中没有能够处理 Bitmap
这种输入格式的,所以我们需要自定义一种 Decoder
将 Bitmap
解码成 Bitmap
(其实也就是解码了个寂寞)。同样默认的 Encoder
中也没有处理 Bitmap
输入格式的,为了磁盘缓存能够正常工作,我们也需要自定义一个 Encoder
。
到这里我相信你大概知道要做些什么事情了,然后我们就来实际操作一下了。
自定义 Model
data class MediaImageModel(
val mediaFilePath: String,
val targetPosition: Long,
val keyId: Long
) : Key {
private val keyBytes: ByteArray by lazy {
"$targetPosition$keyId".toByteArray(Charset.defaultCharset())
}
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
messageDigest.update(keyBytes)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as MediaImageModel
if (targetPosition != other.targetPosition) return false
if (keyId != other.keyId) return false
return true
}
override fun hashCode(): Int {
var result = keyId.hashCode()
result = 31 * result + targetPosition.hashCode()
return result
}
}
首先定义了一个 mediaFilePath
参数用来描述加载的多媒体文件;还定义了 targetPosition
参数来描述帧的位置;这里还实现了 Glide
内部的 Key
接口,这个接口是用来确定资源的唯一性,缓存相关的等功能会用到。
自定义 ModelLoader
class MediaImageModelLoader : ModelLoader<MediaImageModel, Bitmap> {
override fun buildLoadData(
model: MediaImageModel,
width: Int,
height: Int,
options: Options
): ModelLoader.LoadData<Bitmap> {
return ModelLoader.LoadData(model, MediaImageDataFetcher(model))
}
override fun handles(model: MediaImageModel): Boolean {
return true
}
class MediaImageDataFetcher(private val model: MediaImageModel) : DataFetcher<Bitmap> {
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in Bitmap>) {
loadExecutor.execute {
loadSemaphore.acquire()
if (!loadFailHistory.containsKey(model)) {
val bitmap = tMediaFrameLoader.loadMediaFileFrame(model.mediaFilePath, model.targetPosition)
if (bitmap != null) {
callback.onDataReady(bitmap)
} else {
loadFailHistory[model] = Unit
callback.onLoadFailed(Exception("tMediaFrameLoader load $model fail."))
}
} else {
callback.onLoadFailed(Exception("tMediaFrameLoader load $model fail."))
}
loadSemaphore.release()
}
}
override fun cleanup() { }
override fun cancel() { }
override fun getDataClass(): Class<Bitmap> = Bitmap::class.java
override fun getDataSource(): DataSource = DataSource.REMOTE
}
companion object {
class Factory : ModelLoaderFactory<MediaImageModel, Bitmap> {
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<MediaImageModel, Bitmap> {
return MediaImageModelLoader()
}
override fun teardown() {}
}
private const val MAX_CONCURRENT_JOBS = 5
// Max 5 load jobs.
private val loadSemaphore: Semaphore by lazy {
Semaphore(MAX_CONCURRENT_JOBS)
}
private val loadFailHistory: ConcurrentHashMap<MediaImageModel, Unit> by lazy {
ConcurrentHashMap()
}
private val loadExecutor: Executor by lazy {
ThreadPoolExecutor(
0, MAX_CONCURRENT_JOBS,
60L, TimeUnit.SECONDS,
SynchronousQueue(),
{
Thread(it, "MediaImageModelLoader")
},
CallerRunsPolicy()
)
}
}
}
我们需要自定义一个 ModelLoader
来加载我们上面自定义的 MediaImageModel
类型的 model
,我们自定义的 ModelLoader
处理的数据类型是 MediaImageModel
-> Bitmap
。其中通过 FFmpeg
加载 Bitmap
的方法是 tMediaFrameLoader#loadMediaFileFrame()
,具体的实现这里就不多说了。
自定义 Decoder
class BitmapResourceDecoder : ResourceDecoder<Bitmap, Bitmap> {
private val bitmapPool by lazy {
BitmapPoolAdapter()
}
override fun handles(source: Bitmap, options: Options): Boolean = true
override fun decode(
source: Bitmap,
width: Int,
height: Int,
options: Options
): Resource<Bitmap> {
return BitmapResource(source, bitmapPool)
}
}
这个 Decoder
可以说是朴实无华,因为什么都没有处理,直接就是 Bitmap
-> Bitmap
。
自定义 Encoder
虽然 Glide
没有默认处理 Bitmap
类型的 Encoder
,但是它有编码 Bitmap
的代码,我直接拷贝过来了。
class BitmapEncoder(private val arrayPool: ArrayPool) : Encoder<Bitmap> {
override fun encode(bitmap: Bitmap, file: File, options: Options): Boolean {
val format: CompressFormat = getFormat(bitmap, options)
val quality: Int = options.get(BitmapEncoder.COMPRESSION_QUALITY)!!
var success = false
var os: OutputStream? = null
try {
os = FileOutputStream(file)
os = BufferedOutputStream(os, arrayPool)
bitmap.compress(format, quality, os)
os.close()
success = true
} catch (e: IOException) {
e.printStackTrace()
} finally {
if (os != null) {
try {
os.close()
} catch (e: IOException) {
// Do nothing.
}
}
}
return success
}
private fun getFormat(bitmap: Bitmap, options: Options): CompressFormat {
val format = options.get(BitmapEncoder.COMPRESSION_FORMAT)
return format
?: if (bitmap.hasAlpha()) {
CompressFormat.PNG
} else {
CompressFormat.JPEG
}
}
}
上面的代码也没啥好说的,非常简单。
在 Glide 中注册我们自定义的组件
@GlideModule
class MediaImageModule : AppGlideModule() {
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
super.registerComponents(context, glide, registry)
// ModelLoader
registry.replace(
MediaImageModel::class.java,
Bitmap::class.java,
MediaImageModelLoader.Companion.Factory()
)
// ResourceDecoder
registry.append(
Registry.BUCKET_BITMAP,
Bitmap::class.java,
Bitmap::class.java,
BitmapResourceDecoder()
)
// Encoder
registry.append(Bitmap::class.java, BitmapEncoder(glide.arrayPool))
}
}
注册完了后我们就可以使用 Glide
加载我们自定义的 model
辣,代码如下:
// ...
Glide.with(requireActivity())
.load(loadModel)
.error(R.drawable.icon_movie)
.into(itemViewBinding.videoIv)
// ...
哈哈哈哈,这样使用 Glide
加载音视频的帧是不是很优雅呢?
最后
Glide
和 使用 FFmpeg
加载多媒体文件帧的源码都在这里:tMediaPlayer,如果代码对你有帮助欢迎 Star。