iOS 直播弹幕功能的实现
实现iOS直播弹幕功能需要考虑多个方面,包括弹幕的显示、管理、动画效果以及与直播流的同步。
核心实现方案
1. 弹幕显示视图
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 = truebreak }}if !isOccupied {return index}}return nil}
}
2. 弹幕数据模型
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.texttextColor = barrage.color font = UIFont.systemFont(ofSize: barrage.fontSize)backgroundColor = UIColor.black.withAlphaComponent(0.3)layer.cornerRadius = 4clipsToBounds = true }
}
3. 弹幕管理器
class BarrageManager {private let barrageView: BarrageViewprivate var timer: Timer?private var currentPlayTime: TimeInterval = 0init(barrageView: BarrageView) {self.barrageView = barrageView }// 开始弹幕播放 func start(with barrages: [BarrageModel]) {stop()currentPlayTime = 0timer = 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. 弹幕渲染性能优化
// 使用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 = 4clipsToBounds = 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. 弹幕与直播同步
// 在播放器回调中更新弹幕时间
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. 弹幕交互功能
// 点击弹幕处理
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. 弹幕存储结构:
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
完整集成示例
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.boundsview.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.boundsview.addSubview(barrageView)view.bringSubviewToFront(barrageView)}private func loadInitialBarrages() {// 加载初始弹幕APIManager.fetchInitialBarrages { [weak self] barrages inself?.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] _ inguard 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 inif success {DispatchQueue.main.async {self?.barrageView.addBarrage(newBarrage)}}}})present(alert, animated: true)}
}
注意事项
-
性能优化:
- 使用异步绘制(CoreText)
- 限制同时显示的弹幕数量
- 使用对象池复用弹幕视图
-
内存管理:
- 及时移除不可见的弹幕
- 避免强引用循环
-
用户体验:
- 提供弹幕透明度调节
- 支持弹幕显示区域设置
- 实现弹幕屏蔽功能
-
弹幕防挡:
- 重要内容区域(如主播面部)自动避开弹幕
- 提供手动设置防挡区域功能
通过以上方案,你可以实现一个高性能、功能丰富的iOS直播弹幕系统。根据实际需求,你可以进一步扩展功能,如弹幕礼物、高级弹幕效果等。