Android多类型礼物特效播放队列

模型类

enum class EffectType {
    SVGA,
    VAP
}

data class EffectTask(
    val id: String,          // 特效唯一标识
    val url: String,         // 下载链接
    val type: EffectType,    // 特效类型
    val priority: Int = 0    // 可选:如果后续需要插队(如全服广播),可以预留优先级字段
)

接口

interface IEffectPlayer {
    /**
     * 准备就绪,开始播放
     * @param task 当前的特效任务
     * @param localFile 已下载好的本地文件
     * @param onComplete 播放结束(或播放失败)时必须回调此方法,通知队列继续
     */
    fun playEffect(task: EffectTask, localFile: File, onComplete: () -> Unit)
}

核心队列管理器设计

import com.liulishuo.okdownload.DownloadTask
import com.liulishuo.okdownload.core.cause.EndCause
import com.liulishuo.okdownload.core.listener.DownloadListener1
import com.liulishuo.okdownload.core.listener.assist.Listener1Assist
import java.io.File
import java.util.concurrent.ConcurrentLinkedQueue

class EffectQueueManager(
    private val cacheDir: File,
    private val player: IEffectPlayer
) {
    private val queue = ConcurrentLinkedQueue<EffectTask>()
    private var isPlaying = false
    private var currentDownloadTask: DownloadTask? = null

    /**
     * 将特效加入队列
     */
    fun addEffect(task: EffectTask) {
        queue.offer(task)
        checkAndPlayNext()
    }

    /**
     * 检查并播放下一个
     */
    private fun checkAndPlayNext() {
        if (isPlaying || queue.isEmpty()) {
            return
        }
        
        isPlaying = true
        val nextTask = queue.poll()
        
        if (nextTask != null) {
            processTask(nextTask)
        } else {
            isPlaying = false
        }
    }

    /**
     * 处理任务:检查缓存 -> 下载 -> 播放
     */
    private fun processTask(task: EffectTask) {
        // 使用 URL 的 Hash 或 MD5 作为文件名,避免重复下载
        val fileName = task.url.hashCode().toString() + getExtension(task.type)
        val targetFile = File(cacheDir, fileName)

        if (targetFile.exists() && targetFile.length() > 0) {
            // 命中本地缓存,直接播放
            performPlay(task, targetFile)
        } else {
            // 启动 okdownload 下载
            startDownload(task, targetFile)
        }
    }

    private fun startDownload(task: EffectTask, targetFile: File) {
        currentDownloadTask = DownloadTask.Builder(task.url, cacheDir)
            .setFilename(targetFile.name)
            // 礼物特效通常较小,为了队列速度,可以根据业务需求调整并发数,这里保持默认
            .setMinIntervalMillisCallbackProcess(30)
            .setPassIfAlreadyCompleted(true)
            .build()

        currentDownloadTask?.enqueue(object : DownloadListener1() {
            override fun taskStart(task: DownloadTask, model: Listener1Assist.Listener1Model) {}
            override fun retry(task: DownloadTask, cause: EndCause) {}
            override fun connected(task: DownloadTask, blockCount: Int, currentOffset: Long, totalLength: Long) {}
            override fun progress(task: DownloadTask, currentOffset: Long, totalLength: Long) {}

            override fun taskEnd(downloadTask: DownloadTask, cause: EndCause, realCause: Exception?, model: Listener1Assist.Listener1Model) {
                if (cause == EndCause.COMPLETED) {
                    // 下载成功,去播放
                    performPlay(task, targetFile)
                } else {
                    // 下载失败:记录日志,并跳过当前任务,继续播放下一个
                    // Log.e("EffectQueue", "Download failed: ${realCause?.message}")
                    finishCurrentAndPlayNext()
                }
            }
        })
    }

    private fun performPlay(task: EffectTask, localFile: File) {
        // 交给 UI 层播放器播放
        player.playEffect(task, localFile) {
            // UI 层通知播放完毕,触发下一个
            finishCurrentAndPlayNext()
        }
    }

    private fun finishCurrentAndPlayNext() {
        isPlaying = false
        currentDownloadTask = null
        checkAndPlayNext()
    }

    /**
     * 清理队列并停止当前操作(例如退出直播间/语音房时调用)
     */
    fun release() {
        queue.clear()
        currentDownloadTask?.cancel()
        currentDownloadTask = null
        isPlaying = false
    }

    private fun getExtension(type: EffectType): String {
        return when (type) {
            EffectType.SVGA -> ".svga"
            EffectType.VAP -> ".mp4"
        }
    }
}

调用

class RoomActivity : AppCompatActivity() {

    private lateinit var queueManager: EffectQueueManager
    private lateinit var svgaPlayer: SVGAImageView // 你的 SVGA 控件
    private lateinit var vapPlayer: AnimView       // 你的 VAP 控件

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // ... 初始化视图
        
        val effectCacheDir = File(cacheDir, "effect_cache").apply { mkdirs() }

        queueManager = EffectQueueManager(effectCacheDir, object : IEffectPlayer {
            override fun playEffect(task: EffectTask, localFile: File, onComplete: () -> Unit) {
                when (task.type) {
                    EffectType.SVGA -> playSVGA(localFile, onComplete)
                    EffectType.VAP -> playVAP(localFile, onComplete)
                }
            }
        })
    }

    private fun playSVGA(file: File, onComplete: () -> Unit) {
        svgaPlayer.visibility = View.VISIBLE
        val parser = SVGAParser(this)
        
        // 记得设置回调
        svgaPlayer.callback = object : SVGACallback {
            override fun onFinished() {
                svgaPlayer.visibility = View.GONE
                svgaPlayer.clear()
                onComplete() // 必须调用,触发下一个礼物
            }
            // ... 其他回调实现
        }

        parser.decodeFromInputStream(FileInputStream(file), file.absolutePath, object : SVGAParser.ParseCompletion {
            override fun onComplete(videoItem: SVGAVideoEntity) {
                svgaPlayer.setVideoItem(videoItem)
                svgaPlayer.startAnimation()
            }
            override fun onError() {
                onComplete() // 解析失败也要触发下一个,防止队列卡死
            }
        }, true)
    }

    private fun playVAP(file: File, onComplete: () -> Unit) {
        vapPlayer.visibility = View.VISIBLE
        
        vapPlayer.setAnimListener(object : IAnimListener {
            override fun onVideoComplete() {
                vapPlayer.visibility = View.GONE
                onComplete() // 必须调用
            }
            override fun onFailed(errorType: Int, errorMsg: String?) {
                vapPlayer.visibility = View.GONE
                onComplete() // 失败也要放行
            }
            // ... 其他回调实现
        })

        vapPlayer.startPlay(file)
    }

    // 接收到新礼物消息时调用
    fun onReceiveNewGift(giftUrl: String, isSvga: Boolean) {
        val type = if (isSvga) EffectType.SVGA else EffectType.VAP
        queueManager.addEffect(EffectTask(UUID.randomUUID().toString(), giftUrl, type))
    }

    override fun onDestroy() {
        super.onDestroy()
        queueManager.release()
    }
}