实现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)
}
}
注意事项
-
性能优化:
- 使用异步绘制(CoreText)
- 限制同时显示的弹幕数量
- 使用对象池复用弹幕视图
-
内存管理:
- 及时移除不可见的弹幕
- 避免强引用循环
-
用户体验:
- 提供弹幕透明度调节
- 支持弹幕显示区域设置
- 实现弹幕屏蔽功能
-
弹幕防挡:
- 重要内容区域(如主播面部)自动避开弹幕
- 提供手动设置防挡区域功能
通过以上方案,你可以实现一个高性能、功能丰富的iOS直播弹幕系统。根据实际需求,你可以进一步扩展功能,如弹幕礼物、高级弹幕效果等。