iOS 直播弹幕功能的实现

实现iOS直播弹幕功能需要考虑多个方面,包括弹幕的显示、管理、动画效果以及与直播流的同步。

核心实现方案

1. 弹幕显示视图

swift 复制代码
class BarrageView: UIView {
    // 弹道(轨道)数组
    private var tracks: [CALayer] = []
    // 正在显示的弹幕数组 
    private var displayingBarrages: [BarrageLabel] = []
    // 等待显示的弹幕队列 
    private var waitingBarrages: [BarrageModel] = []
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupTracks()
    }
    
    // 初始化弹道
    private func setupTracks() {
        let trackCount = 5 // 根据需求调整弹道数量
        let trackHeight: CGFloat = 30 // 每条弹道高度
        
        for i in 0..<trackCount {
            let track = CALayer()
            track.frame = CGRect(x: 0, 
                                y: CGFloat(i) * trackHeight, 
                                width: bounds.width, 
                                height: trackHeight)
            tracks.append(track)
        }
    }
    
    // 添加新弹幕 
    func addBarrage(_ barrage: BarrageModel) {
        waitingBarrages.append(barrage)
        tryDisplayNextBarrage()
    }
    
    // 尝试显示下一条弹幕 
    private func tryDisplayNextBarrage() {
        guard !waitingBarrages.isEmpty else { return }
        
        // 找到空闲的弹道 
        if let freeTrackIndex = findFreeTrack() {
            let barrage = waitingBarrages.removeFirst()
            displayBarrage(barrage, on: freeTrackIndex)
        }
    }
    
    // 在指定弹道上显示弹幕
    private func displayBarrage(_ barrage: BarrageModel, on trackIndex: Int) {
        let track = tracks[trackIndex]
        let barrageLabel = BarrageLabel(barrage: barrage)
        
        // 设置初始位置(右侧屏幕外)
        barrageLabel.frame = CGRect(x: bounds.width, 
                                   y: track.frame.origin.y, 
                                   width: barrageLabel.intrinsicContentSize.width, 
                                   height: track.frame.height)
        
        addSubview(barrageLabel)
        displayingBarrages.append(barrageLabel)
        
        // 动画
        UIView.animate(withDuration: 8.0, // 根据弹幕长度调整时间 
                       delay: 0,
                       options: [.curveLinear],
                       animations: {
            barrageLabel.frame.origin.x = -barrageLabel.bounds.width 
        }, completion: { _ in 
            barrageLabel.removeFromSuperview()
            if let index = self.displayingBarrages.firstIndex(of: barrageLabel) {
                self.displayingBarrages.remove(at: index)
            }
            self.tryDisplayNextBarrage()
        })
    }
    
    // 查找空闲弹道
    private func findFreeTrack() -> Int? {
        for (index, track) in tracks.enumerated() {
            var isOccupied = false 
            for barrage in displayingBarrages {
                if barrage.frame.origin.y == track.frame.origin.y {
                    isOccupied = true
                    break 
                }
            }
            if !isOccupied {
                return index
            }
        }
        return nil
    }
}

2. 弹幕数据模型

swift 复制代码
struct BarrageModel {
    let text: String 
    let color: UIColor 
    let fontSize: CGFloat 
    let timestamp: TimeInterval // 相对于直播开始的时间戳 
    let type: BarrageType // 滚动、顶部、底部等类型 
    
    enum BarrageType {
        case scroll 
        case top 
        case bottom 
    }
}
 
class BarrageLabel: UILabel {
    init(barrage: BarrageModel) {
        super.init(frame: .zero)
        text = barrage.text
        textColor = barrage.color 
        font = UIFont.systemFont(ofSize: barrage.fontSize)
        backgroundColor = UIColor.black.withAlphaComponent(0.3)
        layer.cornerRadius = 4
        clipsToBounds = true 
    }
}

3. 弹幕管理器

swift 复制代码
class BarrageManager {
    private let barrageView: BarrageView
    private var timer: Timer?
    private var currentPlayTime: TimeInterval = 0
    
    init(barrageView: BarrageView) {
        self.barrageView = barrageView 
    }
    
    // 开始弹幕播放 
    func start(with barrages: [BarrageModel]) {
        stop()
        currentPlayTime = 0
        timer = Timer.scheduledTimer(timeInterval: 0.1, 
                                    target: self, 
                                    selector: #selector(updateBarrages), 
                                    userInfo: nil, 
                                    repeats: true)
    }
    
    // 停止弹幕播放
    func stop() {
        timer?.invalidate()
        timer = nil
    }
    
    // 更新当前播放时间 
    func updatePlayTime(_ time: TimeInterval) {
        currentPlayTime = time
    }
    
    @objc private func updateBarrages() {
        // 在实际应用中,这里应该从服务器获取当前时间点的弹幕 
        // 这里简化为随机生成一些弹幕 
        if Int.random(in: 0...10) > 7 { // 30%概率生成新弹幕 
            let randomTexts = ["666", "主播好帅", "哈哈哈", "这是什么?", "太精彩了!", "爱了爱了"]
            let randomColors: [UIColor] = [.white, .red, .yellow, .green, .cyan]
            
            let barrage = BarrageModel(
                text: randomTexts.randomElement()!,
                color: randomColors.randomElement()!,
                fontSize: CGFloat.random(in: 14...18),
                timestamp: currentPlayTime,
                type: .scroll
            )
            
            DispatchQueue.main.async {
                self.barrageView.addBarrage(barrage)
            }
        }
    }
}

高级优化方案

1. 弹幕渲染性能优化

swift 复制代码
// 使用CoreText自定义绘制
class BarrageLabel: UIView {
    private var attributedText: NSAttributedString!
    
    init(barrage: BarrageModel) {
        super.init(frame: .zero)
        
        let attributes: [NSAttributedString.Key: Any] = [
            .font: UIFont.systemFont(ofSize: barrage.fontSize),
            .foregroundColor: barrage.color,
            .strokeWidth: -2,
            .strokeColor: UIColor.black
        ]
        
        attributedText = NSAttributedString(string: barrage.text, attributes: attributes)
        backgroundColor = UIColor.black.withAlphaComponent(0.3)
        layer.cornerRadius = 4
        clipsToBounds = true 
    }
    
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        guard let context = UIGraphicsGetCurrentContext() else { return }
        
        context.textMatrix = .identity 
        context.translateBy(x: 0, y: bounds.size.height)
        context.scaleBy(x: 1.0, y: -1.0)
        
        let path = CGMutablePath()
        path.addRect(bounds)
        
        let framesetter = CTFramesetterCreateWithAttributedString(attributedText)
        let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attributedText.length), path, nil)
        
        CTFrameDraw(frame, context)
    }
}

2. 弹幕与直播同步

swift 复制代码
// 在播放器回调中更新弹幕时间
func playerTimeUpdate(_ time: TimeInterval) {
    barrageManager.updatePlayTime(time)
}
 
// 弹幕预加载
func preloadBarrages(for timeRange: ClosedRange<TimeInterval>) {
    // 从服务器获取指定时间范围内的弹幕 
    APIManager.fetchBarrages(from: timeRange.lowerBound, to: timeRange.upperBound) { [weak self] barrages in 
        self?.barrageCache.addBarrages(barrages)
    }
}

3. 弹幕交互功能

swift 复制代码
// 点击弹幕处理 
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let touch = touches.first else { return }
    let point = touch.location(in: self)
    
    for barrage in displayingBarrages.reversed() {
        if barrage.frame.contains(point) {
            handleBarrageTap(barrage)
            break
        }
    }
}
 
private func handleBarrageTap(_ barrage: BarrageLabel) {
    // 显示弹幕操作菜单
    let alert = UIAlertController(title: "弹幕操作", message: nil, preferredStyle: .actionSheet)
    
    alert.addAction(UIAlertAction(title: "回复", style: .default) { _ in
        // 回复弹幕 
    })
    
    alert.addAction(UIAlertAction(title: "举报", style: .destructive) { _ in 
        // 举报弹幕
    })
    
    alert.addAction(UIAlertAction(title: "取消", style: .cancel))
    
    // 显示alert 
    // 需要获取当前视图控制器
}

服务器端实现要点

1. 弹幕存储结构:

swift 复制代码
struct ServerBarrage {
    let id: String 
    let content: String 
    let color: String // "#FFFFFF"
    let size: Int // 字体大小
    let timestamp: TimeInterval // 相对于直播开始的时间 
    let userId: String 
    let type: Int // 0:滚动 1:顶部 2:底部
}

2. 弹幕分发:

  • 使用WebSocket实时推送新弹幕
  • 提供按时间范围查询历史弹幕的API

完整集成示例

swift 复制代码
class LiveViewController: UIViewController {
    private let barrageView = BarrageView()
    private let barrageManager = BarrageManager(barrageView: barrageView)
    private var player: AVPlayer!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        setupPlayer()
        setupBarrageView()
        loadInitialBarrages()
    }
    
    private func setupPlayer() {
        // 设置播放器 
        player = AVPlayer(url: liveURL)
        let playerLayer = AVPlayerLayer(player: player)
        playerLayer.frame = view.bounds
        view.layer.addSublayer(playerLayer)
        player.play()
        
        // 添加时间观察者
        player.addPeriodicTimeObserver(forInterval: CMTime(seconds: 0.1, preferredTimescale: CMTimeScale(NSEC_PER_SEC)), 
                                      queue: .main) { [weak self] time in 
            self?.barrageManager.updatePlayTime(time.seconds)
        }
    }
    
    private func setupBarrageView() {
        barrageView.frame = view.bounds
        view.addSubview(barrageView)
        view.bringSubviewToFront(barrageView)
    }
    
    private func loadInitialBarrages() {
        // 加载初始弹幕
        APIManager.fetchInitialBarrages { [weak self] barrages in
            self?.barrageManager.start(with: barrages)
        }
    }
    
    @IBAction func sendBarrage(_ sender: Any) {
        let alert = UIAlertController(title: "发送弹幕", message: nil, preferredStyle: .alert)
        alert.addTextField { textField in 
            textField.placeholder = "输入弹幕内容"
        }
        
        alert.addAction(UIAlertAction(title: "发送", style: .default) { [weak self] _ in
            guard let text = alert.textFields?.first?.text, !text.isEmpty else { return }
            
            let newBarrage = BarrageModel(
                text: text,
                color: .white,
                fontSize: 16,
                timestamp: self?.player.currentTime().seconds ?? 0,
                type: .scroll
            )
            
            // 发送到服务器
            APIManager.sendBarrage(newBarrage) { success in
                if success {
                    DispatchQueue.main.async {
                        self?.barrageView.addBarrage(newBarrage)
                    }
                }
            }
        })
        
        present(alert, animated: true)
    }
}

注意事项

  1. 性能优化:

    • 使用异步绘制(CoreText)
    • 限制同时显示的弹幕数量
    • 使用对象池复用弹幕视图
  2. 内存管理:

    • 及时移除不可见的弹幕
    • 避免强引用循环
  3. 用户体验:

    • 提供弹幕透明度调节
    • 支持弹幕显示区域设置
    • 实现弹幕屏蔽功能
  4. 弹幕防挡:

    • 重要内容区域(如主播面部)自动避开弹幕
    • 提供手动设置防挡区域功能

通过以上方案,你可以实现一个高性能、功能丰富的iOS直播弹幕系统。根据实际需求,你可以进一步扩展功能,如弹幕礼物、高级弹幕效果等。

相关推荐
安和昂1 天前
iOS 初识RunLoop
macos·ios·cocoa
I烟雨云渊T1 天前
iOS Runtime与RunLoop的对比和使用
ios
YJlio1 天前
iOS 15.4.1 TrollStore(巨魔商店)安装教程详解:第二篇
macos·ios·cocoa
ii_best2 天前
按键精灵ios/安卓辅助工具高级函数OcrEx文字识别(增强版)脚本开发介绍
android·ios
Digitally2 天前
如何从不同位置将联系人导入 iPhone(完整指南)
ios·iphone
咕噜企业签名分发-淼淼2 天前
iOS苹果和Android安卓测试APP应用程序的区别差异
android·ios·cocoa
I烟雨云渊T3 天前
iOS APP启动页及广告页的实现
ios
鸿蒙布道师3 天前
鸿蒙NEXT开发动画案例9
android·ios·华为·harmonyos·鸿蒙系统·arkui·huawei
羑悻的小杀马特3 天前
iOS:重新定义移动交互,引领智能生活新潮流
macos·ios·objective-c·cocoa·mac