Appearance
配方 · 直播间弹幕
参考实现:
examples/team-video-vue/composables/useDanmakuStream.ts
需求:WebSocket 收到弹幕 → 显示在视频上;用户能发弹幕;能开关、能清屏。
分工(ADR-028)
你负责: WebSocket 连接 · 拉历史 · 发送到服务端 · 敏感词过滤 · 限流
SDK 负责: 渲染(xgplayer Danmu 引擎)· 轨道排布 · 碰撞避让SDK 不拉弹幕,也不发弹幕。它只有一个入口:你把一条 DanmakuItem 推进来, 它把这条渲染出去。
理由和其他所有数据一样 —— SDK 不发业务 HTTP。你的弹幕服务鉴权怎么做、 协议是 WebSocket 还是 SSE、要不要合并帧,SDK 都不该有意见。
开启
ts
const danmakuConfig: DanmakuConfig = {
enabled: true,
mode: 'streaming',
display: { area: 'full', opacity: 0.85 },
}vue
<TeamVideoPlayer ref="teamPlayerRef" :danmaku="danmakuConfig" />推送
ts
useDanmakuStream({
intervalMs: 800,
onDanmaku: (item) => teamPlayerRef.value?.pushDanmaku(item),
}).start()参考实现里 useDanmakuStream 是个假数据源(定时器随机吐), 真实业务把它换成 WebSocket 订阅:
ts
ws.onmessage = (ev) => {
const msg = JSON.parse(ev.data)
teamPlayerRef.value?.pushDanmaku({
id: msg.id,
text: msg.text,
color: msg.color,
})
}pushDanmaku 是逐条推,不是批量。历史弹幕补进来的时候注意别一次灌几千条。
发送
SDK 不管发送。你自己发到服务端,同时本地推一条,让用户立刻看到自己的弹幕:
ts
let danmakuSeq = 0
async function handleDanmakuSend(text: string) {
danmakuSeq += 1
await playerFrameRef.value?.pushDanmaku({
id: `team-${danmakuSeq}`,
text,
color: '#ffcc00', // 自己发的高亮(业务策略)
})
}本地回显要小心重复
如果你本地推了一条,服务端广播回来又推一条,同一句会显示两次。
常见做法:发送时带一个本地 id,收到广播时按 id 去重;或者干脆不本地回显, 等服务端回声(代价是有延迟感)。
SDK 不做去重 —— 它不知道哪两条是"同一条"。
开关与清屏
ts
ref.setDanmakuEnabled(false) // 关闭渲染
ref.clearDanmaku() // 清空当前屏上的setDanmakuEnabled(false) 之后再 pushDanmaku,推进去的不会显示 —— 但你的 WebSocket 该断还得自己断,SDK 关的只是渲染。
失败了会怎样
推送失败拿 E_DANMAKU_SEND_FAILED,retryable: true。
这个错误码存在的原因是 xgplayer 的弹幕引擎在某些状态下(比如还没初始化完) 会抛异常,而 pushDanmaku 如果不接住,异常会冒到你的 WebSocket 回调里 把整个消息处理打断。
js
onError(({ code }) => {
if (code === 'E_DANMAKU_SEND_FAILED') {
// 单条失败通常可以直接丢弃 —— 弹幕是"流",丢一条不影响
}
})直播和点播的弹幕不一样
| 直播 | 点播 | |
|---|---|---|
| 数据来源 | WebSocket 实时流 | 按时间轴预加载 |
| 推送时机 | 收到就推 | 跟着 timeupdate 推 |
| seek 之后 | 不存在 seek | 要清屏 + 重新按新位置推 |
点播场景 seek 之后没清屏,会看到上一个位置的弹幕残留在屏幕上:
js
onSeeked(({ time }) => {
ref.clearDanmaku()
pushDanmakuAround(time) // 按新位置重新投
})参考实现是直播场景,没有这段 —— 做点播弹幕要自己补。