直播间互动升级:大礼物动画播放逻辑与插队机制揭秘

引言

在直播间中,大礼物动画时吸引用户注意、提升互动体验的重要环境之一。精美的动画不仅展示了用户的支持,还能营造出热烈的直播氛围。然而,背后看似简单的动画播放,却需要一套复杂的逻辑来支持:如何高效地管理礼物进入播放队列?如何确保动画播放的流程性?又该如何处理多个礼物同时到达,或者是优先级更高的礼物播放插队需求?

本篇博客将带你深入了解直播大礼物播放的核心逻辑,从礼物进入队列到实际播放的全过程,剖析插队机制如何在关键时刻保障重要礼物的优点呈现。希望本篇博客能够为你提供有价值的思考和启发。

大礼物动画播放的核心概念

在直播间中,大礼物动画是用户与主播互动的重要形式之一。它不仅体现了用户的支持力度,也为直播间营造了极具冲击力的视觉效果。为了保障大礼物的顺利呈现,背后需要一套完整的逻辑和机制来协调资源和时间。

1.队列机制

队列是大礼物动画播放的核心组件之一,用于按照一定的规则组织和管理礼物的播放顺序。常见的队列机制是FIFO(先进先出),确保礼物按照到达时间依次播放。然而,为了优化用户体验,某些情况下会引入优先级调整机制,使重要的礼物能够优先播放。

队列机制的核心作用:

  • 保证礼物动画播放的有序性,避免同事触发多个动画导致的混乱。
  • 提供一种礼物排队管理方式,为后续插队机制提供基础。

2.播放流程

大礼物动画的播放流程通常分为以下几个阶段:

  1. 大礼物时间触发:当用户赠送礼物时,客户端会接收到一个礼物事件,并将其添加到播放队列。
  2. 资源的加载与准备:在动画正式播放前,系统需要检查对应的资源是否已经加载到本地。若未加载,则触发异步下载。
  3. 动画播放:根据队列顺序,逐一取出礼物并触发播放动画,同时记录播放状态。
  4. 播放完成与清理:动画播放结束后,释放相关资源,并启动队列中的下一个礼物。

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()
    }
    
}
  1. 定义了一个元素为Any类型的数组,用来保存队里中的数据。
  2. 创建一个锁,保证数据进队和出队的线程安全。
  3. 进队操作。
  4. 插入队里头部操作。
  5. 出队操作。
  6. 队首元素。
  7. 队列是否为空。
  8. 队列长度。
  9. 遍历队列。
  10. 清空队列。

我们为队列定义了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)
            }
        } 
        ....
    }
  1. 在开始取出队列中元素时,首先判断是否有正在处于播放状态的动画,如果有则直接返回。
  2. 从队列中取出需要播放的数据,如果没有则直接返回。
  3. 从数据中加载资源。

动画播放:流程与实现细节

我们以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)
        }
    }
  1. 加载礼物的大图资源。
  2. 如果加载失败,那么开始进入下一个动画的播放。
  3. 如果加载成功开始播放。

而关于加载资源的细节我们在之前的博客中已经讨论过了嗷。

播放和完成的处理

礼物大图资源获取完成之后,转换成动图资源开始进行播放,当礼物动画开始播放后,我们需要通过各种手段获取到礼物播放完成,礼物播放完成之后开始执行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()
        }
    }
  1. 加载动图资源,加载失败直接执行下一个动画。
  2. 加载成功之后切换到主线程开始播放动画,并检测动画完成。

结束

检测播放结束,如果可以获取到动画时长,也可以直接拿时长来移除当前动画,并开始下一个动画。

    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()
            }
        }

那么当前的礼物动画完成之后,下一个播放的就应该是入场动画了。而队列中的其它动画将会依次往后排列。

结语

大礼物动画播放是直播间中提升用户体验和互动氛围的关键环节。通过队列机制确保播放的有序性,通过插队机制提升高优先级礼物的展示效率,我们能够为用户呈现流畅且极具吸引力的视觉效果。

然而,实现这一切的背后,需要在逻辑设计、资源管理和性能优化之间找到平衡。这不仅考验开发者对系统的把控能力,也需要对用户需求的深入理解。

希望通过本文的分享,能为你提供关于大礼物动画实现的一些启发。如果你在实际开发中有自己的经验或挑战,欢迎分享你的观点,让我们一起探索更优的解决方案!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值