让 Glide 能够加载任意音视频文件帧

1,014 阅读5分钟

让 Glide 能够加载任意音视频文件帧

我在前面的文章中有解读过 Glide 的源码,没有看过的可以翻翻我前面的文章,Glide 的缓存(包括磁盘缓存和内存缓存)管理可以说是一个标杆的代码,但是 Glide 默认是无法加载音乐文件的封面图和视频文件的任意帧,如果说到谁解码厉害,那不得不说 FFmpeg,如果能够让 GlideFFmpeg 他们能够合作一起工作那不是太酷辣?结合优秀的 Glide 缓存管理和 FFmpeg 强悍的解码能力。

回忆一下 Glide 加载一帧网络图片

假设缓存不可用的条件下哈。

首先我们输入的是一个 http 协议的链接字符串作为加载图片的地址,加载的地址被称为 modelGlide 会去找到一个能够处理 http 协议的 ModelLoader,我们可以将这个默认 ModelLoader 的替换成 OkHttpModelLoader (如果不会的可以看看我前面的文章),当 ModelLoader 当处理了 model 后,就会将加载后的数据(这里的数据类型默认是 InputStream)交给下一站来处理;

处理的下一站就是 Decoder,因为从网络上加载的数据是经过编码的,常见的编码就是 PNGJPEGDecoder 就需要将编码后的数据解码后就成了 Bitmap,然后后续又经过一堆操作后就显示到 ImageView 上了,这里就不再讲这个过程了。

还有一个比较关键的步骤就是 EncoderEncoderBitmap 需要缓存到本地文件时,需要通过 Encoder 编码后写入到文件中,编码方式是 PNG 或者是 JPEG

Glide 加载音视频文件

首先这里要说明一下,我使用 FFmpeg 加载帧的结果直接是 Bitmap,如果想要处理得简单不用自定义 DecoderEncoder,将 FFmpegModelLoader 加载的 Bitmap 成功后,将 Bitmap 再次编码成 PNG 或者 JPEG 然后将数据格式转换成 InputStream 就好了,然后就可以了将处理后的数据丢给系统的 Decoder 就好了。不过这里有两个问题,因为这里会多一次编码过程,首先编码成 PNGJPEG 是有损的,同时编码也消耗系统资源。所以我们放弃了这种方案。

前面说到我们使用 FFmpeg 中加载的结果是 Bitmap,但是默认的 Decoder 中没有能够处理 Bitmap 这种输入格式的,所以我们需要自定义一种 DecoderBitmap 解码成 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。