引言
在直播间中,大礼物动画时吸引用户注意、提升互动体验的重要环境之一。精美的动画不仅展示了用户的支持,还能营造出热烈的直播氛围。然而,背后看似简单的动画播放,却需要一套复杂的逻辑来支持:如何高效地管理礼物进入播放队列?如何确保动画播放的流程性?又该如何处理多个礼物同时到达,或者是优先级更高的礼物播放插队需求?
本篇博客将带你深入了解直播大礼物播放的核心逻辑,从礼物进入队列到实际播放的全过程,剖析插队机制如何在关键时刻保障重要礼物的优点呈现。希望本篇博客能够为你提供有价值的思考和启发。
大礼物动画播放的核心概念
在直播间中,大礼物动画是用户与主播互动的重要形式之一。它不仅体现了用户的支持力度,也为直播间营造了极具冲击力的视觉效果。为了保障大礼物的顺利呈现,背后需要一套完整的逻辑和机制来协调资源和时间。
1.队列机制
队列是大礼物动画播放的核心组件之一,用于按照一定的规则组织和管理礼物的播放顺序。常见的队列机制是FIFO(先进先出),确保礼物按照到达时间依次播放。然而,为了优化用户体验,某些情况下会引入优先级调整机制,使重要的礼物能够优先播放。
队列机制的核心作用:
- 保证礼物动画播放的有序性,避免同事触发多个动画导致的混乱。
- 提供一种礼物排队管理方式,为后续插队机制提供基础。
2.播放流程
大礼物动画的播放流程通常分为以下几个阶段:
- 大礼物时间触发:当用户赠送礼物时,客户端会接收到一个礼物事件,并将其添加到播放队列。
- 资源的加载与准备:在动画正式播放前,系统需要检查对应的资源是否已经加载到本地。若未加载,则触发异步下载。
- 动画播放:根据队列顺序,逐一取出礼物并触发播放动画,同时记录播放状态。
- 播放完成与清理:动画播放结束后,释放相关资源,并启动队列中的下一个礼物。
3.插队机制
在直播间中,某些特殊礼物(如高额礼物或SVIP礼物)往往需要优先展示,以突出其价值。这就需要插队机制的支持。插队机制的设计需要在以下两个方面做好平衡:
- 实时性:优先礼物需要尽快展示,而不应受当前队列中其它礼物的限制。
- 流畅性:插队不应该导致正在播放的动画出现明显的卡顿或中断,从而影响用户体验。
通过队列管理和插队机制的结合,大礼物动画播放得以兼顾有序性和灵活性,为直播间用户和主播提供了最佳的互动体验。
队列管理:从收到到播放的关键步骤
那么接下来我们就从礼物事件的触发到队列的管理开始着手,并结合代码来分析如何设计数据结构,如何保证并发情况下的队列安全。
队列
为了实现上述所说的效果,我们需要定义个队列,用来存放需要做动画的数据,并为其添加最基本的进队,出队,队首元素以及是否为空的方法。
class MWAnimationQueue: NSObject {
/// 数组
private var animationQueue: [Any] = []
/// 锁
private let lock = NSLock()
/// 进队
/// - Parameter animation: 需要做动画的模型
func enqueue(animation: Any) {
lock.lock()
animationQueue.append(animation)
lock.unlock()
}
/// 插入到队列头部
/// - Parameter animation: 需要做动画的模型
func insertToFirst(animation: Any) {
lock.lock()
animationQueue.insert(animation, at: 0)
lock.unlock()
}
/// 出队
/// - Returns: 动画模型
func dequeue() -> Any? {
if animationQueue.isEmpty {
return nil
}
lock.lock()
let animation = animationQueue.first
animationQueue.removeFirst()
lock.unlock()
return animation
}
/// 队列头部
/// - Returns: 队列头部
func peek() -> Any? {
lock.lock()
let animation = animationQueue.first
lock.unlock()
return animation
}
/// 是否为空
/// - Returns: 是否为空
func isEmpty() -> Bool {
lock.lock()
let isEmpty = animationQueue.isEmpty
lock.unlock()
return isEmpty
}
/// 队列长度
/// - Returns: 队列长度
func count() -> Int {
lock.lock()
let count = animationQueue.count
lock.unlock()
return count
}
/// 遍历队列
/// - Parameter block: 遍历回调
func enumerateObjectsUsingBlock(block: (Any) -> Void) {
lock.lock()
for animation in animationQueue {
block(animation)
}
lock.unlock()
}
/// 清空队列
func clear() {
lock.lock()
animationQueue.removeAll()
lock.unlock()
}
}
- 定义了一个元素为Any类型的数组,用来保存队里中的数据。
- 创建一个锁,保证数据进队和出队的线程安全。
- 进队操作。
- 插入队里头部操作。
- 出队操作。
- 队首元素。
- 队列是否为空。
- 队列长度。
- 遍历队列。
- 清空队列。
我们为队列定义了8个方法,但在大礼物动画播放的环境,我们只需要使用进队,出队,是否为空,以及清空队列的方法。插入队列的方法在后面插队时也需要用到。
进入队列
有了队列之后,我们在直播间的大动画播放模块来使用它。直播间内的每个礼物被送出之后,直播间的所有用户都会收到一条礼物消息,我们暂且不讨论管理礼物消息的解析分发和接收。当该礼物消息被直播间的大动画播放模块接收到之后,我们来判断该礼物是否是全屏的大动画礼物,如果是,那么意味着它需要进入大动画的播放队列,具体代码如下:
override func receiveIMMessage(_ message: MWIMMessage) {
// 收到礼物
if let giftMessage = message.data as? MWGiftMessage {
// 收到礼物消息
MWLogHelper.debug("收到礼物消息:\(giftMessage)",context: "MWAnimationShowModule")
// 获取礼物
guard let giftId = giftMessage.giftId else { return }
guard let gift = MWGiftPoolManager.shared.giftPoolMap[giftId] else {
return
}
if let screenurl = gift.screenUrl,screenurl.count > 0 {
// 添加到动画队列
animationQueue.enqueue(animation: giftMessage)
startAnimation()
}
}
....
}
从队列取出
当一个大礼物动画的数据进入队列后,我们调用了startAnimation()方法,表示需要执行动画,在该方法中会队列中取出队首的元素,并开始加载资源播放动画,具体代码如下:
/// 开始动画
private func startAnimation() {
if isPlaying {
return
}
// 从队列中取出动画
guard let animation = animationQueue.dequeue() else {
return
}
isPlaying = true
// 执行动画
MWLogHelper.debug("开始执行动画:\(animation)",context: "MWAnimationShowModule")
if let giftMessage = animation as? MWGiftMessage {
// 礼物动画
DispatchQueue.global().async {
self.showGiftAnimation(giftMessage: giftMessage)
}
}
....
}
- 在开始取出队列中元素时,首先判断是否有正在处于播放状态的动画,如果有则直接返回。
- 从队列中取出需要播放的数据,如果没有则直接返回。
- 从数据中加载资源。
动画播放:流程与实现细节
我们以webp动画为例。
当需要执行大动画的数据从出队之后,我们仍然有需要工作需要处理。数据出队,标记动画开始着手播放准备,设置isPlayeing状态为true。接着开始加载礼物的大动画,当礼物大动画加载成功之后开始正式展示动画。
礼物大图加载
当我们记载礼物的大动画数据时,在之前的博客中我们已经介绍了礼物大图资源的缓存,里面有一个方法专门用来加载大图礼物的资源,如果礼物资源没有加载到本地,我们会启动下载,下载完成之后再进行播放操作,所以我们注意到上面开始加载礼物数据的操作是在子线程中进行的。
/// 展示礼物动画
/// - Parameters:
/// - giftMessage: 礼物消息
private func showGiftAnimation(giftMessage: MWGiftMessage) {
guard let giftId = giftMessage.giftId else {
isPlaying = false
startAnimation()
return
}
/// 加载动图
MWLogHelper.debug("加载动图:\(giftId)",context: "MWAnimationShowModule")
MWGiftLoader.shared.loadBigGiftImage(giftId: giftId) { progress in
} callback: {[weak self] data in
guard let self = self else {
return
}
guard let data = data else {
self.isPlaying = false
self.startAnimation()
return
}
self.playAnimation(data: data)
}
}
- 加载礼物的大图资源。
- 如果加载失败,那么开始进入下一个动画的播放。
- 如果加载成功开始播放。
而关于加载资源的细节我们在之前的博客中已经讨论过了嗷。
播放和完成的处理
礼物大图资源获取完成之后,转换成动图资源开始进行播放,当礼物动画开始播放后,我们需要通过各种手段获取到礼物播放完成,礼物播放完成之后开始执行startAnimation()播放下一个动画。
播放
/// 播放动画
/// - Parameters:
/// - data: 数据
private func playAnimation(data: Data) {
guard let yyImage = YYImage(data: data) else {
MWLogHelper.error("解析动图失败",context: "MWAnimationShowModule")
self.isPlaying = false
self.startAnimation()
return
}
DispatchQueue.main.async {
MWLogHelper.debug("播放动画咯",context: "MWAnimationShowModule")
self.screenAnimationView.image = yyImage
self.screenAnimationView.startAnimating()
self.monitorAnimationCompletion()
}
}
- 加载动图资源,加载失败直接执行下一个动画。
- 加载成功之后切换到主线程开始播放动画,并检测动画完成。
结束
检测播放结束,如果可以获取到动画时长,也可以直接拿时长来移除当前动画,并开始下一个动画。
private func monitorAnimationCompletion() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
guard let self = self else { return }
if !self.screenAnimationView.isAnimating {
MWLogHelper.debug("动画播放完成", context: "MWAnimationShowModule")
self.screenAnimationView.image = nil
self.isPlaying = false
self.enterNameView.isHidden = true
self.startAnimation()
} else {
// 如果动画还在播放,继续检查
self.monitorAnimationCompletion()
}
}
}
另外需要注意的一点就是当模块销毁时,清空队列,停止动画。
override func moduleDestory() {
super.moduleDestory()
NSObject.cancelPreviousPerformRequests(withTarget: self)
enterNameView.isHidden = true
screenAnimationView.stopAnimating()
screenAnimationView.image = nil
animationQueue.clear()
}
插队机制:高优先级礼物的处理
关于插队机制我们使用入场动画来讲解,当有些高级用户入场后或者配搭大入场动画的用户入场后,也会显示全屏的动画,入场动画的优先级要高于礼物动画。所以需要进行插队操作。这个时候队列的插入到队首的方法有起到了作用,具体代码如下:
if let enterMessage = message.data as? MWEnterMessage {
// 收到入场消息
// 获取入场特效
guard let enterEffect = MWDressPoolManager.shared.enterEffectMap[enterMessage.eid] else {
return
}
if enterEffect.isFullScreen == 1 {
MWLogHelper.debug("收到入场消息全屏特效 :\(enterMessage)",context: "MWAnimationShowModule")
animationQueue.insertToFirst(animation: enterMessage)
enterNameView.loadEnterBannerImage(imageUrl: enterEffect.background ?? "")
enterNameView.setName(name: enterMessage.nick)
startAnimation()
}
}
那么当前的礼物动画完成之后,下一个播放的就应该是入场动画了。而队列中的其它动画将会依次往后排列。
结语
大礼物动画播放是直播间中提升用户体验和互动氛围的关键环节。通过队列机制确保播放的有序性,通过插队机制提升高优先级礼物的展示效率,我们能够为用户呈现流畅且极具吸引力的视觉效果。
然而,实现这一切的背后,需要在逻辑设计、资源管理和性能优化之间找到平衡。这不仅考验开发者对系统的把控能力,也需要对用户需求的深入理解。
希望通过本文的分享,能为你提供关于大礼物动画实现的一些启发。如果你在实际开发中有自己的经验或挑战,欢迎分享你的观点,让我们一起探索更优的解决方案!