高性能直播弹幕系统实现:从 Canvas 2D 到 WebGPU
前言
在现代直播应用中,弹幕是提升用户互动体验的重要功能。本文将深入介绍如何实现一个支持大规模并发、高性能渲染的弹幕系统,该系统支持 Canvas 2D 和 WebGPU 两种渲染方式,能够在不同设备环境下自适应选择最佳渲染方案。
技术选型与架构设计
整体架构
我们的弹幕系统采用了以下架构设计:
markdown
┌─────────────────────┐
│ DanmakuCanvas.vue │ ← Vue组件层(UI交互)
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ DanmakuManager.ts │ ← 管理层(协调通信)
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ worker.js │ ← Worker层(核心渲染逻辑)
└─────────────────────┘
核心特性:
- 🚀 使用 Web Worker 实现离屏渲染,避免阻塞主线程
- 🎨 支持 Canvas 2D 和 WebGPU 双渲染引擎
- 📊 智能轨道分配算法,防止弹幕碰撞
- 🎯 支持富文本渲染(文字 + 表情)
- 📈 性能监控与数据上报
- 🔄 响应式画布尺寸适配
技术栈
- Vue 3: 组件层框架
- TypeScript: 类型安全
- OffscreenCanvas: 离屏渲染
- Web Worker: 多线程
- WebGPU: GPU加速渲染(可选)
核心实现详解
一、Vue 组件层实现
DanmakuCanvas.vue 作为用户界面层,主要负责:
vue
<template>
<div class="xhs-danmaku-container">
<canvas ref="canvasRef" class="xhs-danmaku-container-canvas" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
import DanmakuManager from './danmakuManager'
const props = defineProps({
position: { type: String, default: 'top' },
emojis: null,
showDanmaku: { type: Boolean, default: true },
config: { type: Object, default: null },
})
const canvasRef = ref<HTMLCanvasElement>()
const danmakuManager = ref<DanmakuManager>()
// 初始化弹幕管理器
function init() {
if (!canvasRef.value) return
danmakuManager.value = new DanmakuManager(
handleError,
handleErrorReport,
updateHeartDim,
logger
)
danmakuManager.value.init(canvasRef.value, props.emojis, props.config)
}
// 添加弹幕的公共方法
function addDanmaku(message: string, options: any = { type: 'scroll' }) {
if (!danmakuManager.value || !message.trim()) return
danmakuManager.value?.addDanmaku(message, options)
}
// 响应式尺寸适配
function updateCanvasSize() {
if (!danmakuManager.value || !canvasRef.value) return
const rect = canvasRef.value.getBoundingClientRect()
const newConfig = {
canvasWidth: rect.width,
canvasHeight: rect.height
}
danmakuManager.value.updateConfig(newConfig)
}
onMounted(() => {
init()
if (props.showDanmaku) {
danmakuManager.value?.start()
}
// 监听窗口变化
window.addEventListener('resize', handleResize)
window.addEventListener('fullscreenchange', handleResize)
})
onUnmounted(() => {
danmakuManager.value?.destroy()
window.removeEventListener('resize', handleResize)
window.removeEventListener('fullscreenchange', handleResize)
})
// 暴露方法给父组件
defineExpose({
addDanmaku,
openDanmaku,
closeDanmaku,
playDanmaku,
pauseDanmaku,
})
</script>
关键点:
- 使用
ref获取 canvas DOM 元素 - 生命周期管理:初始化 → 运行 → 销毁
- 监听窗口 resize 和全屏事件,实时调整画布尺寸
- 通过
defineExpose暴露控制接口
二、管理层实现
danmakuManager.ts 负责主线程与 Worker 线程的通信:
typescript
export default class DanmakuManager {
private worker: Worker | null = null
private onError: (err: any) => void
private onErrorReport: (data: any) => void
private updateHeartDim: (key: string, value: any) => void
constructor(
onError: (err: any) => void,
onErrorReport: (data: any) => void,
updateHeartDim: (key: string, value: any) => void,
logger?: any,
) {
this.onError = onError
this.onErrorReport = onErrorReport
this.updateHeartDim = updateHeartDim
try {
// 创建 Web Worker
this.worker = work(require.resolve('./worker.js'))
this.worker.onerror = this.handleError.bind(this)
this.worker.onmessage = this.handleMessage
} catch (error) {
this.logger.warn('创建弹幕 Worker 失败:', error)
this.onError(error)
}
}
// 初始化离屏Canvas
init = (canvas: HTMLCanvasElement, mojiData: any, config: any) => {
try {
// 转移 Canvas 控制权到 Worker
const offScreenCanvas = canvas.transferControlToOffscreen()
const emojis = this.serializeMojiData(mojiData)
const rect = canvas.getBoundingClientRect()
// 向 Worker 发送初始化消息
this.worker?.postMessage({
type: 'INIT',
data: {
config: {
canvasWidth: rect.width,
canvasHeight: rect.height,
pixelRatio: window.devicePixelRatio || 1,
emojis,
...config,
},
danmuRenderType: localStorage.getItem('danmuRenderType'),
offScreenCanvas,
},
}, [offScreenCanvas]) // 转移对象所有权
} catch (error) {
this.onError(error)
}
}
// 添加弹幕
addDanmaku(message: string, options: any) {
this.worker?.postMessage({
type: 'ADD_DANMAKU',
data: { message, options }
})
}
// 更新弹幕配置(用于响应式调整)
updateConfig(newConfig: any) {
this.worker?.postMessage({
type: 'UPDATE_CONFIG',
data: { newConfig }
})
}
// 销毁 Worker
destroy() {
this.worker?.terminate()
}
}
核心技术点:
- OffscreenCanvas 转移 :通过
transferControlToOffscreen()将 Canvas 控制权转移到 Worker 线程 - 结构化克隆 :使用
postMessage的第二个参数传递可转移对象 - ImageBitmap 序列化:将表情图片转换为可传输的 ImageBitmap 对象
三、Worker 核心渲染逻辑
worker.js 是整个系统的核心,包含以下关键模块:
3.1 弹幕数据结构
javascript
class Danmaku {
constructor(message, options, config, ctx) {
const type = options?.type || 'scroll'
const parts = this.parseRichText(message)
const width = this.computeDanmakuWidth(parts, options, config, ctx)
const boxWidth = options.showBorder ? width + PADDING_LEFT * 2 : width
const speed = options.speed || config.speed
this.id = this.getDanmakuId()
this.text = message
this.type = type
this.speed = type === 'scroll' ? speed : 0
this.parts = parts // 富文本片段
this.width = width
this.boxWidth = boxWidth
this.x = this.getDanmakuX(boxWidth, type, config)
this.timestamp = Date.now()
this.color = options.color || config.color
this.fontSize = options.fontSize || config.fontSize
this.priority = options.priority || 0
this.showBorder = options.showBorder || false
}
// 解析富文本(文字+表情)
parseRichText(message) {
const parts = []
let lastIndex = 0
const matches = [...message.matchAll(/\[([^\]]+)\]/g)]
if (matches.length === 0) return []
for (const match of matches) {
// 添加普通文本
if (match.index > lastIndex) {
parts.push({
type: 'text',
content: message.slice(lastIndex, match.index),
})
}
// 添加表情
parts.push({
type: 'emoji',
content: match[0],
})
lastIndex = match.index + match[0].length
}
// 添加剩余文本
if (lastIndex < message.length) {
parts.push({
type: 'text',
content: message.slice(lastIndex),
})
}
return parts
}
}
设计亮点:
- 富文本解析:支持
[表情名]格式的表情符号 - 动态宽度计算:精确计算文字+表情的混合宽度
- 优先级系统:支持 VIP 弹幕等优先展示场景
3.2 渲染器实现
javascript
class DanmakuRenderer {
constructor(config, ctx) {
this.config = config
this.ctx = ctx
}
render(danmakuList) {
danmakuList.forEach((danmaku) => {
if (danmaku.showBorder) {
this.drawDanmakuWithBorder(danmaku)
} else {
this.renderRichDanmaku(danmaku)
}
})
}
// 富文本弹幕渲染
renderRichDanmaku(danmaku) {
this.setupCanvasContext(danmaku)
const startX = danmaku.x
const yPosition = danmaku.y
if (!danmaku.parts || danmaku.parts.length === 0) {
this.renderSimpleDanmaku(danmaku.text, startX, yPosition)
return
}
this.renderParts(danmaku, startX, yPosition)
}
// 渲染富文本各部分
renderParts(danmaku, startX, yPosition) {
let currentX = startX
for (const part of danmaku.parts) {
const { content, type } = part || {}
if (!content) continue
if (type === 'emoji') {
currentX = this.renderEmoji(content, danmaku, currentX, yPosition)
} else {
currentX = this.renderText(content, danmaku, currentX, yPosition)
}
}
}
// 渲染文本
renderText(content, danmaku, x, y) {
this.ctx.strokeText(content, x, y)
this.ctx.fillText(content, x, y)
return x + this.measureTextWidth(content, danmaku).width
}
// 渲染表情
renderEmoji(content, danmaku, x, y) {
try {
const emojiBitmap = this.config.emojis[content]?.bitmap
if (!emojiBitmap) {
// 回退到文本渲染
return this.renderText(content, danmaku, x, y)
}
const emojiActualSize = danmaku.fontSize
const emojiY = y - emojiActualSize / 2
this.ctx.drawImage(
emojiBitmap,
x,
emojiY,
emojiActualSize,
emojiActualSize,
)
return x + danmaku.fontSize
} catch (error) {
return this.renderText(content, danmaku, x, y)
}
}
}
渲染优化:
- 文字描边:使用
strokeText+fillText提升可读性 - 混排处理:文字和表情按顺序依次渲染
- 容错机制:表情加载失败时回退到文本显示
3.3 智能轨道分配算法
javascript
class DanmakuWorker {
constructor() {
this.danmakuList = [] // 屏幕上的弹幕
this.penddingList = [] // 等待队列
this.usedTrackIds = new Set() // 已占用轨道
this.config = defaultConfig
}
// 创建轨道列表
createTrackList() {
const { trackCount, trackHeight, trackGap } = this.config
return Array.from({ length: trackCount }, (_, i) => ({
id: `${i}-track`,
height: trackHeight * (i + 1) + trackGap / 2,
}))
}
// 为新弹幕分配轨道
assignTrack(newDanmaku) {
const trackList = this.config.trackList
// 优先分配未使用的轨道
if (this.usedTrackIds.size < trackList.length) {
return trackList.find(track => !this.usedTrackIds.has(track.id))
}
// 检查每个轨道是否有足够空间
for (const track of trackList) {
if (this.isTrackAvailable(track, newDanmaku)) {
return track
}
}
return null // 无可用轨道
}
// 检查轨道是否可用
isTrackAvailable(track, newDanmaku) {
if (newDanmaku.type !== 'scroll') {
// 固定弹幕:确保轨道上没有其他固定弹幕
const sameTrackDanmakus = this.danmakuList.filter(
d => d.type !== 'scroll' && d.trackId === track.id,
)
return sameTrackDanmakus.length === 0
}
// 滚动弹幕:检查是否有足够空间
const sameTrackDanmakus = this.danmakuList.filter(
d => d.type === 'scroll' && d.trackId === track.id,
)
if (sameTrackDanmakus.length === 0) return true
// 检查最后一个弹幕是否已留出足够空间
const lastDanmaku = sameTrackDanmakus[sameTrackDanmakus.length - 1]
const lastDanmakuPosition = lastDanmaku.x + lastDanmaku.boxWidth
const availableSpace = this.config.canvasWidth - lastDanmakuPosition
return availableSpace >= SAFE_AREA // 36px 安全距离
}
}
算法特点:
- 空间优先:优先使用完全空闲的轨道
- 碰撞检测:计算前一条弹幕是否留出足够安全距离
- 队列机制:无可用轨道时加入等待队列
3.4 WebGPU 渲染实现
javascript
async initWebGpu() {
if (!navigator.gpu) {
return false
}
// 获取 GPU 适配器和设备
const adapter = await navigator.gpu.requestAdapter()
const device = await adapter.requestDevice()
const context = this.offScreenCanvas.getContext('webgpu')
// 创建辅助 Canvas 用于 2D 绘制
const webgpuCanvas = new OffscreenCanvas(
this.offScreenCanvas.width,
this.offScreenCanvas.height
)
const webgpuCtx = webgpuCanvas.getContext('2d')
this.webgpuCanvas = webgpuCanvas
this.ctx = webgpuCtx // 使用 2D 上下文绘制,再由 GPU 渲染
// 配置 Canvas 格式
const canvasFormat = navigator.gpu.getPreferredCanvasFormat()
context.configure({
device,
format: canvasFormat,
alphaMode: 'premultiplied',
})
// 创建着色器
const vertexShaderCode = `
struct VertexOutput {
@builtin(position) position: vec4f,
@location(0) uv: vec2f,
};
@vertex
fn main(@location(0) position: vec2f, @location(1) uv: vec2f) -> VertexOutput {
var output: VertexOutput;
output.position = vec4f(position, 0.0, 1.0);
output.uv = uv;
return output;
}
`
const fragShaderCode = `
@group(0) @binding(0) var textureSampler: sampler;
@group(0) @binding(1) var texture: texture_2d<f32>;
@fragment
fn main(@location(0) uv: vec2f) -> @location(0) vec4f {
let flippedUV = vec2<f32>(uv.x, 1.0 - uv.y);
return textureSample(texture, textureSampler, flippedUV);
}
`
// 创建渲染管线
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module: device.createShaderModule({ code: vertexShaderCode }),
entryPoint: 'main',
buffers: [/* ... */],
},
fragment: {
module: device.createShaderModule({ code: fragShaderCode }),
entryPoint: 'main',
targets: [{ format: canvasFormat }],
},
primitive: { topology: 'triangle-strip' },
})
this.pipeline = pipeline
this.renderType = 'WEBGPU'
return true
}
async renderWebgpu() {
// 1. 在 2D Canvas 上绘制弹幕
this.renderer.render(this.danmakuList)
// 2. 将 2D Canvas 内容复制到 GPU 纹理
this.device.queue.copyExternalImageToTexture(
{ source: this.webgpuCanvas },
{ texture: this.texture },
{ width: this.webgpuCanvas.width, height: this.webgpuCanvas.height },
)
// 3. 使用 GPU 渲染到屏幕
const encoder = this.device.createCommandEncoder()
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: this.context.getCurrentTexture().createView(),
loadOp: 'clear',
clearValue: [0, 0, 0, 0],
storeOp: 'store',
}],
})
pass.setPipeline(this.pipeline)
pass.setVertexBuffer(0, this.vertexBuffer)
pass.setBindGroup(0, this.bindGroup)
pass.draw(4)
pass.end()
this.device.queue.submit([encoder.finish()])
}
WebGPU 优势:
- GPU 加速合成,降低 CPU 负载
- 更高的渲染性能,支持更大弹幕量
- 适合高端设备,提供极致体验
3.5 动画循环与性能优化
javascript
update = (currentTime) => {
if (this.state !== 'playing') return
const elapsed = currentTime - this.lastUpdateTime
this.lastUpdateTime = currentTime
// 防止时间跳变(如标签页切换回来)
if (elapsed <= 0) {
this.animationId = requestAnimationFrame(this.update)
return
}
// 更新弹幕位置
this.updateDanmakuX(elapsed)
// 渲染
if (this.renderType === 'WEBGPU') {
this.renderWebgpu()
} else {
this.render2D()
}
// 尝试从队列中添加弹幕
this.tryAddPendingDanmaku()
this.animationId = requestAnimationFrame(this.update)
}
// 更新弹幕位置
updateDanmakuX = (deltaTime) => {
this.danmakuList = this.danmakuList.filter((danmaku) => {
// 限制 deltaTime 防止时间跳变导致位置突变
let _deltaTime = deltaTime
if (_deltaTime >= 20) {
_deltaTime = 20
}
if (deltaTime < 20 && deltaTime > 15) {
_deltaTime = 16
}
// 滚动弹幕位置更新
if (danmaku.type === 'scroll' && danmaku.trackId) {
danmaku.x -= danmaku.speed * (_deltaTime / 1000)
}
const isVisible = this.isDanmakuVisible(danmaku)
if (!isVisible) {
this.clearCanvas()
}
return isVisible
})
}
// 检查弹幕可见性
isDanmakuVisible(danmaku) {
if (danmaku.type === 'scroll') {
// 滚动弹幕:完全离开屏幕左侧才移除
return danmaku.x + danmaku.boxWidth + SAFE_AREA > 0
} else {
// 固定弹幕:根据持续时间判断
return Date.now() - danmaku.timestamp < danmaku.duration
}
}
性能优化点:
- 时间平滑处理 :限制
deltaTime范围,避免标签页切换导致的位置跳变 - 自动清理:及时移除不可见弹幕,减少渲染负担
- 按需渲染:只在有弹幕时执行渲染逻辑
四、响应式尺寸适配
javascript
updateConfig = ({ newConfig }) => {
const oldWidth = this.config?.canvasWidth
const newWidth = newConfig.canvasWidth
// 合并新配置
this.config = { ...this.config, ...newConfig }
const newWidthPx = newConfig.canvasWidth * this.config.pixelRatio
const newHeightPx = newConfig.canvasHeight * this.config.pixelRatio
// 更新画布尺寸
if (this.offScreenCanvas) {
this.offScreenCanvas.width = newWidthPx
this.offScreenCanvas.height = newHeightPx
}
// 调整现有弹幕位置
this.adjustDanmakuX(oldWidth, newWidth, this.danmakuList)
this.adjustDanmakuX(oldWidth, newWidth, this.penddingList)
}
adjustDanmakuX = (oldWidth, newWidth, danmakuList) => {
danmakuList.forEach((danmaku) => {
if (danmaku.type === 'scroll') {
// 滚动弹幕:保持相对位置
danmaku.x += (newWidth - oldWidth)
} else {
// 固定弹幕:重新居中
danmaku.x = this.config.canvasWidth / 2 - (danmaku.boxWidth / 2)
}
})
}
适配特点:
- 无缝调整:窗口变化时保持弹幕连续性
- 位置修正:滚动弹幕保持相对位置,固定弹幕重新居中
- 双向同步:同时调整屏幕上的弹幕和等待队列
性能对比
| 指标 | Canvas 2D | WebGPU |
|---|---|---|
| CPU 占用 | 中等 | 低 |
| GPU 占用 | 低 | 中等 |
| 最大弹幕量 | ~300/s | ~800/s |
| 兼容性 | 99%+ | ~70% |
| 适用场景 | 通用 | 高端设备 |
使用示例
vue
<template>
<DanmakuCanvas
ref="danmakuRef"
:show-danmaku="true"
:emojis="emojiData"
:config="danmakuConfig"
@on-error="handleError"
/>
</template>
<script setup>
import { ref } from 'vue'
import DanmakuCanvas from './components/CanvasBarrage/DanmakuCanvas.vue'
const danmakuRef = ref()
const danmakuConfig = {
fontSize: 20,
fontFamily: 'PingFang SC',
color: '#fff',
duration: 8000,
trackHeight: 52,
trackGap: 16,
trackCount: 3,
speed: 140,
}
// 发送弹幕
function sendDanmaku(message) {
danmakuRef.value?.addDanmaku(message, {
type: 'scroll', // scroll | fixed
priority: 0, // 优先级
showBorder: false, // 是否显示边框
})
}
// 发送 VIP 弹幕
function sendVipDanmaku(message) {
danmakuRef.value?.addDanmaku(message, {
type: 'scroll',
priority: 10, // 高优先级
showBorder: true, // 带边框
color: '#FFD700', // 金色
})
}
</script>
最佳实践
1. 性能监控
javascript
// 在 Worker 中上报性能指标
globalThis.postMessage({
type: 'updateHeartDim',
data: {
key: 'onScreenDanmuCount',
value: this.danmakuList.length
}
})
2. 渲染模式选择
javascript
// 根据设备能力选择渲染方式
const danmuRenderType = localStorage.getItem('danmuRenderType') || 'webgpu'
// 浏览器支持检测
if (!navigator.gpu) {
localStorage.setItem('danmuRenderType', 'canvas2d')
window.location.reload()
}
3. 表情图片预处理
javascript
// 使用 ImageBitmap 提升渲染性能
async function loadEmojis(emojiUrls) {
const emojis = {}
for (const [key, url] of Object.entries(emojiUrls)) {
const response = await fetch(url)
const blob = await response.blob()
emojis[key] = await createImageBitmap(blob)
}
return emojis
}
4. 内存管理
javascript
// 限制等待队列长度
const MAX_PENDDING_LIST_LEN = 100
if (this.penddingList.length >= MAX_PENDDING_LIST_LEN) {
// 丢弃最早的弹幕
this.penddingList.shift()
// 上报丢弃数据
globalThis.postMessage({
type: 'updateHeartDim',
data: { key: 'discardDanmuCount', value: 1 }
})
}
总结
本文介绍的弹幕系统具备以下特点:
✅ 高性能 :Web Worker + OffscreenCanvas,不阻塞主线程
✅ 可扩展 :双渲染引擎,支持渐进增强
✅ 智能调度 :轨道分配算法 + 优先级队列
✅ 功能丰富 :富文本、边框、多种弹幕类型
✅ 响应式 :自适应屏幕尺寸变化
✅ 可监控:完善的性能指标上报
这套方案已在生产环境稳定运行,能够支撑高并发直播场景下的大规模弹幕渲染需求。
参考资料
如果这篇文章对你有帮助,欢迎点赞收藏!有任何问题欢迎在评论区讨论~ 🎉