模型类
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()
}
}