介绍

这是一篇关于多视频缓存的技术方案

诞生背景

希望能将视频缓存下来,然后在需要的时候立马播放。

需要的配件

1、AndroidVideoCache

2、项目上通用的播放器

源码

功能比较简单:直接上源码

...

import android.util.Log
import com.danikula.videocache.HttpProxyCacheServer
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield

/**
 * 多视频缓存帮助类
 * */
object MultiplayerHelper {
    private val TAG = MultiplayerHelper::class.java.simpleName

    var appScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

    var job: Job? = null

    val handler = CoroutineExceptionHandler { _, exception ->
        Log.d(TAG, "CoroutineExceptionHandler got $exception")
    }

    //缓存代理
    private val httpProxyCacheServer: HttpProxyCacheServer by lazy {
        App.get().appViewModelProvider.get(AppViewModel::class.java).httpProxyCacheServer
    }

    private val testUrlList = arrayListOf<String>(
        "http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4",
        "http://vjs.zencdn.net/v/oceans.mp4",
        "https://media.w3.org/2010/05/sintel/trailer.mp4",
        "http://mirror.aarnet.edu.au/pub/TED-talks/911Mothers_2010W-480p.mp4"
    )
    fun test() {
        cacheLinkListVideos(testUrlList)
    }

    private var videoLinkListSet = mutableSetOf<String>()
    private val videoPlayerList = mutableListOf<VideoPlayerEntity>()

    /**
     * 分批缓存视频
     * @param groupSize 每次可缓存多少视频
     * @param delayTime 缓存视频间隔
     * */
    @JvmStatic
    fun cacheLinkListVideos(
        videoString: List<String>?,
        groupSize: Int = 3,
        delayTime: Long = 500L
    ) {
        if (videoString == null) {
            Log.d(TAG, "cacheLinkListVideos: videoString == null")
            return
        }
        if (job != null && job?.isActive == true) {
            Log.d(TAG, "cacheLinkListVideos: cancel")
            job?.cancel()
            videoPlayerList.forEach {
                Log.d(
                    TAG, "cacheLinkListVideos: cancel 释放播放器 ${it.num}"
                )
                it.videoPlayer.release()
            }
            videoPlayerList.clear()
        }
        Log.d(TAG, "cacheLinkListVideos: start")
        videoLinkListSet.addAll(videoString)
        val splitList = splitList(videoLinkListSet.mapIndexed { i, it ->
            it to "视频序号 $i"
        }.filter {
            !httpProxyCacheServer.isCached(it.first)
        }, groupSize)
        Log.d(TAG, "cacheLinkListVideos: 拆解后的 $splitList")
        job = appScope.launch(handler) {
            for (i in 0 until groupSize) {
                val videoPlayer = VideoPlayer()
                videoPlayerList.add(VideoPlayerEntity(videoPlayer, i, true))
            }
            Log.d(TAG, "cacheLinkListVideos: 创建播放器数量${videoPlayerList.size}")
            splitList.forEachIndexed() { i, res ->
                /**
                 * 内部工作函数
                 * */
                suspend fun doWork(videoString: Pair<String, String>) {
                    val currentVideoPlayer = videoPlayerList.firstOrNull { it.isPlay }
                    if (currentVideoPlayer == null) {
                        Log.d(
                            TAG,
                            "当前没有播放器释放出来"
                        )
                        doWork(videoString)
                        return
                    }
                    Log.d(
                        TAG,
                        "doWork: currentVideoPlayer num = ${currentVideoPlayer.num}}"
                    )
                    currentVideoPlayer.isPlay = false
                    delay(500)
                    withContext(Dispatchers.Main) {
                        currentVideoPlayer.videoPlayer.startPlayVideo(
                            null,
                            httpProxyCacheServer.getProxyUrl(videoString.first)
                        )
                    }
                    runCatching {
                        httpProxyCacheServer.isCached(videoString.first)
                    }.onSuccess {
                        if (it) {
                            Log.d(
                                TAG,
                                "doWork: 播放器释放 ${
                                    httpProxyCacheServer.getProxyUrl(videoString.first)
                                }"
                            )
                                currentVideoPlayer.isPlay = true
                        } else {
                            Log.d(
                                TAG,
                                "doWork:当前没有成功缓存,继续等待 ${videoString.second}  播放器序号${currentVideoPlayer.num} ${Thread.currentThread().name} "
                            )
                            val jobRepeat = async {
                                repeat(Int.MAX_VALUE) {
                                    delay(500)
                                    Log.d(
                                        TAG,
                                        "doWork: repeat$it 播放器序号${currentVideoPlayer.num}"
                                    )
                                    if (httpProxyCacheServer.isCached(videoString.first)) {
                                        Log.d(
                                            TAG,
                                            "doWork: 播放器释放 repeat 播放器${currentVideoPlayer.num}${
                                                httpProxyCacheServer.getProxyUrl(videoString.first)
                                            }"
                                        )
                                        currentVideoPlayer.isPlay = true
                                        return@async true
                                    }
                                }
                            }
                            if (jobRepeat.await() == true) {
                                Log.d(TAG, "doWork: 计时结束")
                                jobRepeat.cancel()
                            }
                        }
                    }.onFailure {
                        Log.d(TAG, "doWork:error= ${it.message}")
                        job?.cancel()
                    }
                }
                res.map { ress ->
                    async {
                        doWork(ress)
                    }
                }.awaitAll()
                Log.d(
                    TAG,
                    "执行下次记录 当前$i"
                )
                delay(delayTime)
            }
            yield()
            Log.d(
                TAG, "功能结束 开始释放播放器"
            )
            withContext(Dispatchers.Main) {
                videoPlayerList.forEach {
                    Log.d(
                        TAG, "释放播放器 ${it.num}"
                    )
                    it.videoPlayer.release()
                }
            }
        }
    }

    /**
     * 拆分集合
     * @param messagesList 拆解前的集合
     * @param groupSize 拆解因子
     * */
    private fun <T> splitList(
        messagesList: List<T>,
        groupSize: Int
    ): List<List<T>> {
        val length = messagesList.size
        // 计算可以分成多少组
        val num = (length + groupSize - 1) / groupSize
        val newList: MutableList<List<T>> = ArrayList(num)
        for (i in 0 until num) {
            // 开始位置
            val fromIndex = i * groupSize
            // 结束位置
            val toIndex = if ((i + 1) * groupSize < length) (i + 1) * groupSize else length
            newList.add(messagesList.subList(fromIndex, toIndex))
        }
        return newList
    }

    /**
     * 多播放器实体类
     * */
    data class VideoPlayerEntity(val videoPlayer: VideoPlayer, val num: Int, var isPlay: Boolean)
}