如何开发一个自己的弹幕库

4,004 阅读4分钟

前言

之前做项目的时候需要一个弹幕效果,但是当时没找到合适的弹幕库,就自己写了一个 vue-custom-barrage,下面说一下弹幕库的实现原理,大家也可以后面开发自己的弹幕库。

开发功能

  1. 无碰撞弹幕
  2. 暂停/开始
  3. 自定义弹幕展示区域
  4. 自定义弹幕风格且不需要 innerHTML,防止 xss 攻击
  5. 弹幕量拉满 fps 也要接近 60

基本思路

大家可以把弹幕想象成一个个运动员,然后把屏幕想成运动场,运动员都在不同的跑道上跑步,互不干扰。

image.png

在同一条跑道上可以存在多个运动员,后一个运动员起跑的时机就是前一个运动员身体完全进入跑道,两个运动员贴着跑不好看,可以让后面的运动员稍等下再进入跑道。

image.png

无碰撞弹幕

大家看视频的时候会发现弹幕长度肯定是不一样的,而且速度也是不一样的,假设现在跑道上有一个运动员 A,我们已知下面的数据:

  1. 跑道总长:l
  2. 自身宽度:l1
  3. 运动总时长(指跑完 l + l1 的时长):t
  4. 自身起跑时间:t1
  5. 当前时间:t2
  6. 已运动时长:t3 = t2 - t1
  7. 剩余运动时长:t4 = t - t3
  8. 速度:v1 = (l + l1) / t

image.png

现在要上场的运动员 B 与 A 的极限碰撞位置就是在跑道的最左侧:

image.png

我们就可以知道运动员 B 的数据:

  1. 自身宽度:l2
  2. 最大速度:v2 = l / t4
  3. 运动总时长:t5 = (l + l2) / v2

我们按照这样就可以依次计算出下个弹幕的最大速度,就可以达到所有弹幕不产生碰撞。

暂停/开始

对于我们的运动动画,我们使用 animation 来实现,因为 css 变量会有继承的效果,所以我们只要设置容器的样式,所有弹幕都会停止或者继续:

// 设置容器样式
container.style.setProperty('--playState', 暂停 || 播放)
// 弹幕样式
.barrage {
    animation: xxx;
    animation-play-state: var(--playState);
}

但是注意每次暂停和播放的时候得记录下来当前的时间用以计算每个弹幕的运动时长,要不后面的弹幕就会发生碰撞

自定义弹幕展示区域

这个我是去创建了一个单独的跑道类,每个跑道会去观察是否有新的弹幕,如果有的话就加入自己的跑道,所以我们知道去关闭某些跑道的观察,就可以做到展示特定的区域。

自定义弹幕风格

因为弹幕不一定是只有文字,可能有很多用户自定义的东西,我之前看的有些弹幕库用的 innerHTML,但是这样就可能会引来 xss 攻击,我们只要站在巨人的肩膀上就可以轻松搞定,下面以 Vue 举例:

// 我们封装的 Vue 弹幕组件
<Barrage>
    <template #default='data'>
        <div>自定义弹幕:{{ data }}</div>
    </template>
</Barrage>
// 在弹幕组件中

// 拿到插槽的内容, data 是用户传入的数据
const slots = this.$scopedSlots.default(data)

// 拿到一个 Vue 子类
const BarrageVNode = vue.extend({
  render(h) {
    return h('div', slots)
  }
})

// 拿到对应的 vm
const barrageInstance = new BarrageVNode().$mount()

// 追加到容器中
container.append(barrageDom.$el)

这样不论用户想搞成什么样子都可以了。

fps

我们使用 animation 结合 transform 来生成动画,因为 transform 动画会提升为 合成层,会触发 GPU 加速,且在动画过程中是不会触发 layout 和 paint 的,性能是很高的:

GIF 2021-11-9 15-18-11.gif

延伸

大家看 B 站的时候会发现弹幕不会挡住视频中的人物,下面说下他是怎么实现的。

效果

image.png

抠图

前端会不停的请求当前视频的人物抠图,视频每帧的人物抠图应该是用机器学习去做的。

image.png

css 实现

他给弹幕容器增加了一个 mask-image 属性,url 对应的就是刚才抠出来的人物图,就这么简单的一个属性,就实现了弹幕的不遮挡效果

image.png

image.png

本地测试

我去把刚才的人物抠图放到本地试了下效果,一模一样。

image.png

.container {
  position: relative;
  border: 1px solid;
  width: 600px;
  height: 600px;
}
.masked {
  position: absolute;
  z-index: 2;
  width: 100%;
  height: 100%;
  -webkit-mask-image: url("data:image/svg+xml;base64,PHN2ZyB2ZXJzaW9uPSIxLjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKIHdpZHRoPSIyOTUuNDg4ODYyODM3MDQ1N3B4IiBoZWlnaHQ9IjUyMnB4IiB2aWV3Qm94PSIwIDAgMTgwLjAwMDAwMCAzMjAuMDAwMDAwIgogcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQgbWVldCI+CjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAuMDAwMDAwLDMyMC4wMDAwMDApIHNjYWxlKDAuMTAwMDAwLC0wLjEwMDAwMCkiCmZpbGw9IiMwMDAwMDAiIHN0cm9rZT0ibm9uZSI+CjxwYXRoIGQ9Ik0wIDE2MDUgbDAgLTE1OTUgMjQxIDAgMjQxIDAgLTcgMTEzIGMtMyA2MSAtMTEgMTQxIC0xOCAxNzYgLTE0IDc3IDYgMjE0IDUyCjM1NiA2MCAxODIgNjIgMTk0IDY2IDMzOCA1IDEzMyAtNiAyNDcgLTI0IDI0NyAtOSAwIC00MSAtNTUgLTQxIC03MSAwIC0yNQotNTQgLTgyIC05NiAtMTAxIC02MyAtMjcgLTg4IC0xMyAtMTI3IDc1IC0zMCA2OCAtMzAgNjkgLTE4IDE1MCAyMCAxMjkgNTgKMjY4IDg4IDMyMyAzNiA2NSA5OCAxMjAgMTYwIDE0MiA0NyAxNyA3NyAzMyAxNzYgOTUgNjQgNDEgNzcgODUgNTAgMTcyIC0xMwo0NCAtMTggOTEgLTE4IDE3NSAwIDEwNSAzIDEyMCAyOCAxNzIgMzIgNjQgNzIgOTkgMTM4IDExNyA0MiAxMiA1MCAxMSA5NCAtMTEKNDQgLTIxIDUwIC0yOSA3MiAtODggMjQgLTYzIDQ5IC0xNTIgNTkgLTIxNSA0IC0yMiAtMiAtNDIgLTI0IC03NSAtNjQgLTk2Ci00OCAtMTgyIDQzIC0yMzIgNzggLTQyIDE4OCAtOTUgMjEyIC0xMDIgMzggLTEyIDkyIC04OSAxMDggLTE1OCA5IC0zNSAyNwotOTkgNDAgLTE0MyAxMyAtNDQgMzggLTE0NyA1NSAtMjMwIDE2IC04MiAzNSAtMTcyIDQwIC0xOTggMTEgLTU1IDEzIC0xMTUgNAotMTcwIC02IC0zMiAtMTAgLTM3IC0zMyAtMzcgLTI4IDAgLTYxIC0zMCAtNjEgLTU1IDAgLTkgMTcgLTIwIDQ1IC0yNyA0OSAtMTMKNTAgLTE1IDMzIC03MyAtNiAtMjIgLTExIC00MyAtMTAgLTQ3IDMgLTEzIC0yNiAzIC0zMiAxOCAtNCAxMCAtMTQgMTIgLTMwIDgKLTMzIC04IC00MCAxNiAtMzkgMTM0IDEgMTA5IC0xNSAxNzMgLTQ0IDE4NiAtMjQgMTEgLTU3IDc5IC03MyAxNTAgLTIwIDkwCi00MyAxMTEgLTU1IDQ5IC01IC0yNiAtOSAtMzMgLTE1IC0yMyAtMTYgMjcgLTIwIC0xMCAtMTYgLTE1MCAzIC0xMTggOCAtMTQ4CjM2IC0yMzUgMzcgLTExNCA3MSAtMjQxIDkyIC0zMzUgMTIgLTUzIDE5IC02NyA0MSAtNzggMTYgLTggMjcgLTIxIDI3IC0zMyAwCi0xMSA3IC0yMiAxNSAtMjUgMTggLTcgMTkgLTUxIDIgLTExNyBsLTEyIC00OCAtMzkgNDcgYy0zOCA0NSAtMzkgNDYgLTU4IDI3Ci0xNSAtMTUgLTIwIC0zNyAtMjIgLTEwNiBsLTQgLTg3IDIyOSAwIDIyOSAwIDAgMTU5NSAwIDE1OTUgLTkwMCAwIC05MDAgMCAwCi0xNTk1eiIvPgo8L2c+Cjwvc3ZnPgo=");
}
.val {
  position: absolute;
  left: 20px;
  top: 130px;
}



<div class="container">
  <div class="masked">
      <div class="val">我是内容我是内容我是内容我是内容</div>
  </div>
</div>

最后

大家可以借鉴或使用我的 vue-custom-barrage,一个基本弹幕的功能就上面我说的那些,如有疑问欢迎在评论区咨询我。

051A36A0.jpg