知道bilibili发送弹幕的原理后,觉得钱花的有点不值

2,196 阅读5分钟

前言

今年小编喜欢上了在bilibili上看游戏直播,也不是因为有多爱打游戏,主要是up有些帅气,当看到观众们在直播中发送五彩斑斓的弹幕时,我也跃跃欲试,加入了发弹幕的行列。然而,我很快发现,我的普通弹幕在屏幕上迅速划过,而一些观众的弹幕却以其独特的颜色和字体脱颖而出。

经过一番探索,我了解到要获得这种更加丰富的弹幕效果,需要加入up主的粉丝团并提升等级,这也就打开了小编的花点小钱打赏之路。当然作为一个技术人员,我也对发送弹幕背后的技术实现产生了兴趣,所以接下我将讲解如何给实现发送弹幕功能。

原理

发送弹幕通常在视频播放器中应用,它的基本原理是利用 HTML5 的 Canvas 技术,在视频播放的同时,在视频上方绘制一个透明的画布(Canvas),然后在该画布上绘制弹幕文本,并通过动画效果实现文本的移动、显示和隐藏。

下面是发送弹幕使用 Canvas 的基本原理:

  1. 创建 Canvas 元素:首先,在视频播放器上方创建一个透明的 Canvas 元素,并设置其使其位于视频上方,这样就可以在视频上方绘制弹幕。

  2. 绘制弹幕文本:当用户发送弹幕时,将用户输入的文本绘制到 Canvas 上。可以通过 JavaScript 获取用户输入的弹幕文本,并使用 Canvas API 在 Canvas 上绘制文本。可以设置文本的样式、颜色、字体等。

  3. 动画效果:绘制文本后,通过动画效果实现弹幕的移动。通常情况下,弹幕会从屏幕右侧(或其他指定位置)出现,然后向左移动至屏幕边缘,最终消失。可以使用 JavaScript 的定时器或 requestAnimationFrame() 函数来实现动画效果,不断地更新文本的位置,从而实现文本的移动。

  4. 控制弹幕显示时间:可以设置每条弹幕的显示时间,一般情况下,弹幕显示一段时间后会自动隐藏。可以在发送弹幕时记录发送时间,并在绘制弹幕时检查当前时间,若超过了显示时间,则停止绘制该弹幕。

界面

为了更好的讲解我们先写一个简单的界面!

html和css代码

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    * {
      padding: 0;
      margin: 0;
    }

    .wrap {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      height: 100vh;
      /* 让wrap占据整个视口的高度 */
    }

    #canvas {
      position: absolute;
    }

    .main {
      text-align: center;
    }

    .content {
      margin: 20px;
      text-align: center;
    }

    #text {
      padding: 10px;
      margin-right: 10px;
      border: 2px solid #ccc;
      border-radius: 5px;
      font-size: 16px;
    }

    #btn {
      padding: 10px 20px;
      background-color: #4CAF50;
      color: white;
      border: none;
      border-radius: 5px;
      cursor: pointer;
      font-size: 16px;
    }

    #color {
      margin-left: 10px;
      vertical-align: middle;
    }

    #range {
      margin-left: 10px;
    }
  </style>
</head>

<body>
  <div class="wrap">
    <h1>蒲猫猫直播录屏</h1>
    <div class="main">
      <canvas id="canvas"></canvas>
      <video src="./mv.MP4" controls width="720" height="480" id="video"></video>
    </div>
    <div class="content">
      <input type="text" id="text">
      <input type="button" id="btn" value="发弹幕">
      <input type="color" id="color">
      <input type="range" id="range" min="20" max="40">
    </div>
  </div>

  <script src="./index.js"></script>
</body>

</html>

效果:

image.png

JS逻辑

1.数据定义

let data = [
  {
    value: '我生于南方,长在北方',
    time: 5,
    color: 'blue',
    speed: 1,
    fontSize: 30
  },
  // 其他弹幕数据
];

这里我们自定义了一些弹幕数据,包括弹幕内容、出现时间、颜色、速度和字体大小等信息。

2. 获取页面元素

let canvas = document.getElementById('canvas');
let video = document.getElementById('video');
let $text = document.getElementById('text');
let $btn = document.getElementById('btn');
let $color = document.getElementById('color');
let $range = document.getElementById('range');

我们通过getElementById方法获取了页面上对应的canvas元素、video元素、文本输入框、发送按钮、颜色选择器和字体大小调节器。

3.创建弹幕绘制类

class CanvasBarrage {
  constructor(canvas, video, opts = {}) {
    if (!canvas || !video) {
      throw new Error('canvas and video is required');
    }

    this.video = video;
    this.canvas = canvas;
    this.canvas.width = video.width;
    this.canvas.height = video.height;

    // 创建画布上下文
    this.ctx = canvas.getContext('2d');

    // 弹幕默认值
    let defOpts = {
      color: '#e91e63',
      speed: 1,
      fontSize: 20,
      opacity: 0.5,
      data: []
    };

    // 合并默认值和传入的选项
    Object.assign(this, defOpts, opts);
    this.isPaused = true;

    // 初始化所有弹幕
    this.barrages = this.data.map(item => new Barrage(item, this));
    this.render();
  }

  render() {
    // 清除画布
    this.clear();
    // 渲染弹幕
    this.renderBarrages();
    // 如果视频未暂停,继续渲染下一帧
    if (!this.isPaused) {
      requestAnimationFrame(this.render.bind(this))
    }
  }

  clear() {
    // 清除画布上的内容
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
  }

  renderBarrages() {
    // 获取视频当前播放时间
    let time = this.video.currentTime;
    this.barrages.forEach(barrage => {
      // 判断弹幕是否应该出现
      if (barrage.time <= time && !barrage.flag) {
      
        if (!barrage.isInit) {
          barrage.init();
          barrage.isInit = true;
        }
        
        // 更新弹幕位置
        barrage.x -= barrage.speed;
        barrage.render();
        // 如果弹幕移出画布,将其标记为已处理
        if (barrage.x < -barrage.width) {
          barrage.flag = true;
        }
      }
    });
  }

  add(obj) {
    // 添加新的弹幕
    this.barrages.push(new Barrage(obj, this));
  }
}
  1. 构造函数 constructor(canvas, video, opts = {})
  • 初始化画布和视频元素,设置画布尺寸,创建画布上下文。

  • 定义弹幕的默认属性(颜色、速度、字体大小、不透明度、数据),并将传入的选项与默认值合并。

  • 将传入的数据初始化为 Barrage 实例,并开始渲染。

渲染方法 render()

  • 清除画布内容。

  • 渲染所有弹幕。

  • 通过递归调用 requestAnimationFrame 实现动画效果,除非视频暂停。

清除方法 clear()

  • 清除画布上的所有内容,以便重新绘制。

渲染弹幕方法 renderBarrages()

  • 获取视频的当前播放时间。

  • 遍历所有弹幕,判断是否应该显示,并更新其位置和渲染。

  • 如果弹幕移出画布,标记为已处理。

添加弹幕方法 add(obj)

  • 接收新的弹幕对象,并将其转换为 Barrage 实例添加到 barrages 数组中。

4.创建弹幕类

class Barrage {
  constructor(obj, context) {
    this.value = obj.value; // 弹幕的文本内容
    this.time = obj.time; // 弹幕出现的时间
    this.obj = obj;
    this.context = context; // CanvasBarrage 实例的引用||存储上下文对象
    this.flag = false; // 标记弹幕是否已经移出屏幕
  }

  init() {
    this.color = this.obj.color || this.context.color; // 设置弹幕颜色
    this.speed = this.obj.speed || this.context.speed; // 设置弹幕速度
    this.opacity = this.obj.opacity || this.context.opacity; // 设置弹幕不透明度
    this.fontSize = this.obj.fontSize || this.context.fontSize; // 设置弹幕字体大小

    // 计算每一条弹幕的宽度
    let p = document.createElement('p');
    p.style.fontSize = this.fontSize + 'px';
    p.innerHTML = this.value;
    document.body.appendChild(p);
    this.width = p.clientWidth;
    document.body.removeChild(p);

    // 设置弹幕初始位置
    this.x = this.context.canvas.width;
    this.y = this.context.canvas.height * Math.random();
    // 限制弹幕的横纵向显示范围
    if (this.y < this.fontSize) {
      this.y = this.fontSize;
    } else if (this.y > this.context.canvas.height - this.fontSize) {
      this.y = this.context.canvas.height - this.fontSize;
    }
  }

  render() {
    this.context.ctx.font = `${this.fontSize}px Arial`; // 设置字体
    this.context.ctx.fillStyle = this.color; // 设置颜色
    this.context.ctx.fillText(this.value, this.x, this.y); // 绘制文本
  }
}

构造函数 constructor(obj, context)

  • 初始化弹幕的基本属性,包括内容、出现时间、引用的上下文(CanvasBarrage 实例)和标记是否已经移出屏幕。

初始化方法 init()

  • 设置弹幕的颜色、速度、不透明度和字体大小,优先使用传入的值,如果没有则使用默认值。

  • 通过创建一个临时的 DOM 元素来计算弹幕文本的宽度。

  • 设置弹幕的初始位置(在画布右边缘,垂直位置随机但不超出画布范围)。

渲染方法 render()

  • 设置画布的字体和颜色,然后在画布上绘制弹幕的文本内容。

5.事件监听与交互


//初始化CanvasBarrage实例
let canvasBarrage = new CanvasBarrage(canvas, video, { data });

video.addEventListener('play', () => {
  canvasBarrage.isPaused = false;// 视频播放时取消暂停状态
  canvasBarrage.render();// 视频播放时取消暂停状态
})

$btn.addEventListener('click', () => {
  let value = $text.value;
  let time = video.currentTime;
  let color = $color.value;
  let fontSize = $range.value;
  let obj = { value, time, color, fontSize };// 创建一个新的弹幕对象
  canvasBarrage.add(obj);// 创建一个新的弹幕对象
  $text.value = '';
})

这部分代码添加了视频播放事件监听和发送弹幕的点击事件监听。

当视频播放时,启动弹幕渲染;点击发送按钮时,获取输入的弹幕内容、当前视频时间以及颜色和字体大小等信息,并添加到弹幕列表中。

最后

结果截图

image.png

这只是一个简单的demo,主要让大家理解这样一个原理和逻辑,各种细节还需要我们去考量修改!

(文章中用的视频因为是录屏,所以也会录到当时的一些弹幕,有些干扰,多多包涵)

image.png