爱丫爱丫影院电视剧

Heero.Luo发表于(yu)3年前,已被查(cha)看2384次

直播是(shi)眼下最为火(huo)爆的行业,而(er)弹幕无疑是(shi)直播平台中最流行(xing)、最重要的功(gong)能之一。本文(wen)将讲述如何(he)实现兼容 PC 浏(liu)览器和移动(dong)浏览器的弹幕(mu)。

无法满足1980意大利呼叫合

大地资源在线资源免费观看

一般来说,弹(tan)幕数据会通(tong)过异步请求(qiu)或 socket 消息传到(dao)前端,这里会(hui)存在一个隐(yin)患——数据(ju)量可能非常(chang)大。如果一收(shou)到弹幕数据(ju)就马上渲染(ran)出来,在量大(da)的时候:

  • 显示(shi)区域不足以(yi)放置这(zhe)么(me)多的弹幕,弹(tan)幕会堆叠在(zai)一起;
  • 渲染过(guo)程会占用大(da)量 CPU 资源,导致页(ye)面卡顿。

所以(yi)在接收(shou)和渲染数据(ju)之间,要引入队列(lie)做缓(huan)冲。把(ba)收到的弹幕(mu)数据都存入(ru)数组(即下文(wen)代码中的 this._queue),再(zai)通过轮询该数组,把(ba)弹幕逐条渲染(ran)出来:

class Danmaku {
  // 省略 N 行(xing)代码...

  add(data) {
    this._queue.push(this._parseData(data));
    if (!this._renderTimer) { this._render(); }
  }

  _render() {
    try {
      this._renderToDOM();
    } finally {
      this._renderEnd();
    }
  }

  _renderEnd() {
    if (this._queue.length > 0) {
      this._renderTimer = setTimeout(() => {
        this._render();
      }, this._renderInterval);
    } else {
      this._renderTimer = null;
    }
  }

  // 省略 N 行(xing)代码...
}

弹幕的(de)滚动

弹幕的(de)滚动本质上是位移(yi)动画,从显示(shi)区域的右侧(ce)移动到左侧(ce)。前端实现位(wei)移动画有两(liang)种方案——DOM 和 canvas。

  • DOM 方(fang)案实现的动(dong)画较为(wei)流畅,且一些(xie)特殊效果(如(ru)文字阴影)较容(rong)易实现(只(zhi)要在 CSS 中设置(zhi)对应的属性(xing)即可)。
  • Canvas 方(fang)案的动画流(liu)畅度要差一(yi)些,要做特殊(shu)效果也不那(na)么容易,但是(shi)它在 CPU 占用上(shang)有优势。

本文(wen)将以 DOM 方(fang)案实现弹幕(mu)的滚动,并通过(guo) CSS 的 transition 和 transform 来(lai)实(shi)现动画,这样(yang)可以利用浏(liu)览器渲染过(guo)程中的(de)「合成层」机制(zhi)(有兴趣可以(yi)查阅这篇文(wen)章),提高性能(neng)。弹幕滚动(dong)的示例代码(ma)如下:

var $item = $('.danmaku-item').css({
  left: '100%',
  'transition-duration': '2s',
  'transition-property': 'transform',
  'will-change': 'transform' 
});
setTimeout(function() {
  $item.css(
    'transform',
    `translateX(${-$item.width() + container.offsetWidth})`
  );
}, 1000);

弹幕的渲染

在(zai) DOM 方案下,每条(tiao)弹幕对应一(yi)个 HTML 元素,把元(yuan)素的样式都(dou)设定好之后,就(jiu)可以添加到(dao) HTML 文档里面:

class Danmaku {
  // 省略 N 行代(dai)码...

  _renderToDOM() {
    const data = this._queue[0];
    let node = data.node;
    if (!node) {
      data.node = node = document.createElement('div');
      node.innerText = data.msg;
      node.style.position = 'absolute';
      node.style.left = '100%';
      node.style.whiteSpace = 'nowrap';
      node.style.color = data.fontColor;
      node.style.fontSize = data.fontSize + 'px';
      node.style.willChange = 'transform';
      this._container.appendChild(node);

      // 占用轨道(dao)数
      data.useTracks = Math.ceil(node.offsetHeight / this._trackSize);
      // 宽度
      data.width = node.offsetWidth;
      // 总位(wei)移(yi)(弹(tan)幕宽度(du)+显示区域宽(kuan)度)
      data.totalDistance = data.width + this._totalWidth;
      // 位移时间(jian)(如果数据(ju)里面没有指(zhi)定,就按照默(mo)认方式计算(suan))
      data.rollTime = data.rollTime ||
        Math.floor(data.totalDistance * 0.0058 * (Math.random() * 0.3 + 0.7));
      // 位移速度
      data.rollSpeed = data.totalDistance / data.rollTime;

      // To be continued ...
    }   
  }

  // 省(sheng)略 N 行代码...
}

由(you)于元素的 left 样(yang)式值设置为 100%,所以它在(zai)显示区域之(zhi)外。这样可(ke)以在用户看(kan)到这条弹幕(mu)之前,做一些(xie)“暗箱操作”,包(bao)括获取(qu)弹幕的尺寸(cun)、占用的轨道(dao)数、总位移、位(wei)移时间、位移(yi)速度。接下来的(de)问题是,要把弹幕(mu)显示在哪个位置呢(ne)?

首先,弹幕的(de)文字大小不(bu)一定一致,从(cong)而占用的高(gao)度也不尽相(xiang)同。为了能充(chong)分利用(yong)显示区域的(de)空间,我们可(ke)以把显示(shi)区域划分为(wei)多行,一行即(ji)为一条轨道(dao)。一条弹幕至少占用(yong)一条轨道。而(er)存(cun)储结构方面(mian),可以用二维(wei)数组记录每(mei)条轨道中存(cun)在的弹幕。下(xia)图是弹幕占用轨道(dao)及其对应存(cun)储结构的一(yi)个例子:

弹幕(mu)占用轨道及(ji)其对应存储(chu)结构

其次,要防止弹(tan)幕重叠。原理(li)其实非常简(jian)单,请看下面这(zhe)题数学题。假设有起点站(zhan)、终点站(zhan)和一条轨道(dao),列车都(dou)以匀速运动(dong)方式从起点(dian)开到终点。列(lie)车 A 先发车,请(qing)问:如果在某(mou)个时刻,列车(che) B 发车(che)的话,会不会(hui)在列车 A 完全(quan)进站之前撞(zhuang)上列车 A?

列车(che)碰撞数学题

聪(cong)明的你可能(neng)已经发现,这(zhe)里的轨(gui)道所对应的(de)就是弹幕显(xian)示区域里面(mian)的一行,列(lie)车对应的就(jiu)是弹幕。解题之(zhi)前(qian),先(xian)过一下已知(zhi)量:

  • 路程 S,对应(ying)显示区域的(de)宽度;
  • 两车长(chang)度 la 和 lb,对应弹幕(mu)的(de)宽度;
  • 两车速(su)度 va 和 vb,已(yi)经计算出来(lai)了;
  • 前车已行(xing)走距离 sa,即弹(tan)幕元素当前(qian)的位置,可以通过读(du)取样式值获取。

那在(zai)什么情况下(xia),两车不会相(xiang)撞呢?

  • 其一,如果列车(che) A 没有(you)完全出站(已行(xing)走距离小于车长),则列(lie)车 B 不具备发(fa)车条件;
  • 其二(er),如果列车 B 的(de)速度小于等(deng)于列车 A 的速(su)度,由于 A 先发(fa)车,这是(shi)肯定撞不上(shang)的;
  • 其三,如果(guo)列车 B 的速(su)度大于列车(che) A 的速度,那就(jiu)要看两者的(de)速度差(cha)了:
    • 列车 A 追上列(lie)车 B 所需时间(jian) tba = (sa - la) / (vb - va);
    • 列车 A 完全到(dao)站所需时间(jian) tad =  (s + la - sa) / va;
    • tba > tad 时,两车不会(hui)撞上。

有了理(li)论支撑(cheng),就可以编写(xie)对应的代码(ma)了。

class Danmaku {
  // 省(sheng)略 N 行代码...
  
  // 把弹幕数(shu)据放(fang)置到合适的轨(gui)道
  _addToTrack(data) {
    // 单条轨道(dao)
    let track;
    // 轨道的最后一项弹(tan)幕数据
    let lastItem;
    // 弹幕(mu)已经走的路程(cheng)
    let distance;
    // 弹幕数(shu)据最终坐落(luo)的轨道索引(yin)
    // 有些弹幕会(hui)占多条轨道,所以 y 是(shi)个数组
    let y = [];

    for (let i = 0; i < this._tracks.length; i++) {
      track = this._tracks[i];

      if (track.length) {
        // 轨道(dao)被占用,要计(ji)算是否会重(zhong)叠
        // 只需要跟(gen)轨道最后一(yi)条弹幕比较即可
        lastItem = track[track.length - 1];

        // 获(huo)取已滚动距(ju)离(即当前的(de) translateX)
        distance = -getTranslateX(lastItem.node);

        // 计算最后一(yi)条(tiao)弹幕全部消(xiao)失前,是否会与新增弹幕(mu)重叠
        // (对(dui)应数学题分(fen)析中的三种(zhong)情况)
        // 如果不(bu)会重叠,则可(ke)以使用当前(qian)轨道
        if (
          (distance > lastItem.width) &&
          (
            (data.rollSpeed <= lastItem.rollSpeed) ||
            ((distance - lastItem.width) / (data.rollSpeed - lastItem.rollSpeed) >
              (this._totalWidth + lastItem.width - distance) / lastItem.rollSpeed)
          )
        ) {
          y.push(i);
        } else {
          y = [];
        }

      } else {
        // 轨道未(wei)被占用
        y.push(i);
      }

      // 有足(zu)够的轨(gui)道可以用时(shi),就可以(yi)新增弹幕了(le),否则等下一次(ci)轮询
      if (y.length >= data.useTracks) {
        data.y = y;
        y.forEach((i) => {
          this._tracks[i].push(data);
        });
        break;
      }
    }
  }

  // 省略 N 行(xing)代码...
}

只要弹(tan)幕成功(gong)入轨(data.y 存在),就可(ke)以显示在对(dui)应的位置并(bing)执行动画了(le):

class Danmaku {
  // 省略 N 行代码(ma)...

  _renderToDOM {
    const data = this._queue[0];
    let node = data.node;
    
    if (!data.node) {
      // 省略 N 行代码(ma)...
    }

    this._addToTrack();

    if (data.y) {
      this._queue.shift();

      // 轨道对应(ying)的 top 值
      node.style.top = data.y[0] * this._trackSize + 'px';
      // 动画参(can)数
      node.style.transition = `transform ${data.rollTime}s linear`;
      node.style.transform = `translateX(-${data.totalDistance}px)`;
      // 动画结束(shu)后移除
      node.addEventListener('transitionend', () => {
        this._removeFromTrack(data.y, data.autoId);
        this._container.removeChild(node);
      }, false);
    }
  }

  // 省略(lue) N 行代(dai)码...
}

至此,渲染流程结束(shu),此时的弹幕(mu)效果见此 demo 页(ye)。为(wei)了能够让大(da)家看清楚渲(xuan)染过程中的(de)“暗箱操作”,demo 页(ye)中会把显示(shi)区域以外的部分也(ye)展示出来。

完(wan)善

上一节已(yi)经实现了弹(tan)幕的基本功(gong)能,但仍有一些(xie)细节需要完善。

跳过弹(tan)幕

仔(zi)细观察上文的弹幕(mu) demo 可以发现,同一(yi)条轨道内,弹(tan)幕之间的距(ju)离偏大。而该(gai) demo 中,队列(lie)轮询的间隔(ge)为 150ms,理应不(bu)会有这么大(da)的间距。

回顾(gu)渲染的代码(ma)可以发现,该(gai)流程总(zong)是先检查第(di)一条弹幕能不(bu)能入轨,倘若(ruo)不(bu)能(neng),那后续的(de)弹幕都会被(bei)堵塞,从而导(dao)致弹幕(mu)密集度不足(zu)。然而,每条弹(tan)幕的长度、速度等参数(shu)不尽相同,第(di)一条弹幕不(bu)具备入(ru)轨条件不代(dai)表后续的弹(tan)幕都不具备。所(suo)以,在单次渲(xuan)染过程中,如(ru)果第一条弹(tan)幕还不能入(ru)轨,可(ke)以(yi)往后多尝试几(ji)条。

相关(guan)的代码改动(dong)也不大,只要(yao)加个循环就(jiu)行了:

_renderToDOM() {
  // 根据轨(gui)道数量每次(ci)处理一定数量的弹(tan)幕数据。数量越(yue)大,弹幕越密(mi)集,CPU 占用越高
  let count = Math.floor(totalTracks / 3), i;
  while (count && i < this._queue.length) {
    const data = this._queue[i];
    // 省(sheng)略 N 行代码...
    if (data.y) {
      this._queue.splice(i, 1);
      // 省略(lue) N 行代码...
    } else {
      i++;
    }
    count--;
  }
}

改动后的效(xiao)果见此 demo 页,可以(yi)看到弹幕密(mi)集程度有明(ming)显改善。

弹幕(mu)已滚动路程(cheng)的获取

防重(zhong)叠检测(ce)是弹幕渲染(ran)过程中执行(xing)得最为频繁(fan)的部分,因此(ci)其优化显得(de)特别重要。JavaScript 性能优(you)化的关(guan)键是:尽可能(neng)避免 DOM 操作。而整个防(fang)重叠检测算法中涉(she)及的唯一(yi)一处 DOM 操作,就是弹(tan)幕已滚(gun)动路程的获(huo)取:

distance = -getTranslateX(data.node);

而实际上(shang),这个路程不(bu)一定要通过(guo)读取当前样(yang)式值来获取(qu)。因为在匀速运(yun)动的情(qing)况下,路程(cheng)=速度×时间,速度是已知的(de),而(er)时间嘛,只需(xu)要用当前时(shi)间减去开始时间就(jiu)可以得出。先(xian)记录开始时(shi)间:

_renderToDOM() {
  // 根据轨道(dao)数量每次处(chu)理一定数量(liang)的弹幕数据(ju)。数量越大,弹幕越密(mi)集,CPU 占用越高(gao)
  let count = Math.floor(totalTracks / 3), i;
  while (count && i < this._queue.length) {
    const data = this._queue[i];
    // 省略 N 行代码(ma)...
    if (data.y) {
      this._queue.splice(i, 1);
      // 省略 N 行代(dai)码...
      node.addEventListener('transitionstart', () => {
        data.startTime = Date.now();
      }, false);
      // 从设置动(dong)画样式到动(dong)画开始有一(yi)定的时间差,所以加(jia)上 80 毫秒
      data.startTime = Date.now() + 80;

    } else {
      i++;
    }

    count--;
  }
}

注(zhu)意,这里设置(zhi)了两次开始(shi)时间,一次是(shi)在设置动画(hua)样式、绑定事件(jian)之后,另(ling)一(yi)次(ci)是在(zai) transitionstart 事件中。理(li)论上只需(xu)要后者即可(ke)。之所以加上(shang)前者,还是因(yin)为兼容性问(wen)题——并不是所(suo)有浏览(lan)器都支持 transitionstart 事(shi)件

然后,获取(qu)弹幕已滚动(dong)路程的代码(ma)就可以优化(hua)成:

distance = data.rollSpeed * (Date.now() - data.startTime) / 1000;

别看这个改动很小,前(qian)后只涉(she)及 5 行代码,但(dan)效果是立竿(gan)见影的(见此(ci) demo 页):

浏览器getTranslateX 匀速(su)公式计算
ChromeCPU 16%~20%CPU 13%~16%
Firefox能(neng)耗影响 3能耗(hao)影(ying)响(xiang) 0.75
SafariCPU 8%~10%CPU 3%~5%
IECPU 7%~10%CPU 4%~7%

暂停和(he)恢复

首(shou)先要解释一(yi)下为什么要(yao)做暂停和恢(hui)复,主要是两(liang)个方面的考(kao)虑。

第一个考(kao)虑是浏览器(qi)的兼容问题。弹幕渲(xuan)染流程会频(pin)繁调用到 JS 的(de) setTimeout 以及 CSS 的 transition,如果把当前标(biao)签页切到后台(浏(liu)览器最小化(hua)或切换到其他标签(qian)页),两者会有(you)什么变化呢(ne)?请看测试结(jie)果:

浏览器setTimeouttransition
Chrome/Edge延(yan)迟加大如果(guo)动画未开始(shi),则等待标签页切到(dao)前台后才开(kai)始
Safari/IE 11正常如果动画未开始(shi),则等待标签(qian)页切到前台(tai)后才开始
Firefox正常(chang)正常

可见,不(bu)同浏览(lan)器的处理方(fang)式不尽相同(tong)。而(er)从(cong)实际场景上考(kao)虑,标签(qian)页切到后台(tai)之后,即使渲(xuan)染弹幕用户(hu)也看不(bu)见,白白消耗(hao)硬件资源。索(suo)性引入一个(ge)机制:标签页(ye)切到后台,则(ze)弹幕暂停,切(qie)到前台再恢(hui)复

let hiddenProp, visibilityChangeEvent;
if (typeof document.hidden !== 'undefined') {
  hiddenProp = 'hidden';
  visibilityChangeEvent = 'visibilitychange';
} else if (typeof document.msHidden !== 'undefined') {
  hiddenProp = 'msHidden';
  visibilityChangeEvent = 'msvisibilitychange';
} else if (typeof document.webkitHidden !== 'undefined') {
  hiddenProp = 'webkitHidden';
  visibilityChangeEvent = 'webkitvisibilitychange';
}

document.addEventListener(visibilityChangeEvent, () => {
  if (document[hiddenProp]) {
    this.pause();
  } else {
    // 必须异步执行,否则(ze)恢复后动画(hua)速度可能会(hui)加快,从而导(dao)致弹幕消失(shi)或重叠,原因(yin)不明
    this._resumeTimer = setTimeout(() => { this.resume(); }, 200);
  }
}, false);

先看下(xia)暂停滚(gun)动的主要代(dai)码(注意已滚(gun)动路程 rolledDistance,将用(yong)于恢复播放(fang)和防重叠):

this._eachDanmakuNode((node, y, id) => {
  const data = this._findData(y, id);
  if (data) {
    // 获取(qu)已滚动距(ju)离
    data.rolledDistance = -getTranslateX(node);
    // 移除动画(hua),计算出弹幕所在的(de)位置,固定样式(shi)
    node.style.transition = '';
    node.style.transform = `translateX(-${data.rolledDistance}px)`;
  }
});

接下来是恢(hui)复滚动的主(zhu)要代码:

this._eachDanmakuNode((node, y, id) => {
  const data = this._findData(y, id);
  if (data) {
    // 重新(xin)计算滚完剩(sheng)余距离需要(yao)多少时间
    data.rollTime = (data.totalDistance - data.rolledDistance) / data.rollSpeed;
    data.startTime = Date.now();
    node.style.transition = `transform ${data.rollTime}s linear`;
    node.style.transform = `translateX(-${data.totalDistance}px)`;
  }
});

this._render();

防重叠的(de)计算公式也(ye)需要修改:

// 新增了 lastItem.rolledDistance
distance = lastItem.rolledDistance + lastItem.rollSpeed * (now - lastItem.startTime) / 1000;

修(xiu)改后(hou)效果见此 demo 页,可(ke)以留意切换(huan)浏览器标签(qian)页后的效果(guo)并与前(qian)面几个 demo 对比(bi)。

丢弃排队时(shi)间(jian)过长的弹幕(mu)

弹幕并发量(liang)大时,队列中(zhong)的弹幕数据(ju)会非常多,而在防重(zhong)叠机制下,一(yi)屏能显示的弹(tan)幕是有限的(de)。这就会出现(xian)“供过于求”,导致弹(tan)幕“滞销”,用户(hu)看(kan)到的(de)弹幕将不再(zai)“新鲜”(比如视(shi)频已经播到第 10 分钟,但(dan)还在显示第(di) 3 分(fen)钟时发的弹(tan)幕)。

为了(le)应对这种情(qing)况,要引(yin)入丢弃机制(zhi),如果(guo)弹幕的库存(cun)比(bi)较多,而且这批(pi)库存已经放(fang)了很久,就扔掉它。相关(guan)代码改动如(ru)下:

while (count && i < this._queue.length) {
  const data = this._queue[i];
  let node = data.node;

  if (!node) {
    if (this._queue.length > this._tracks.length * 2 &&
      Date.now() - data.timestamp > 5000
    ) {
      this._queue.splice(i, 1);
      continue;
    }
  }

  // ...
}

修(xiu)改后效果见(jian)此 demo 页

最后

DOM 的(de)渲染完全是(shi)由浏览器控(kong)制的,也就是说实(shi)际渲染情况(kuang)与 JavaScript 算(suan)出来的存在偏(pian)差,一般情况下(xia)偏差不大,渲染(ran)效果就是正(zheng)常(chang)的。但(dan)是在极端情(qing)况下,偏(pian)差较大时,弹幕(mu)就可能会出(chu)现轻微重叠(die)。这一(yi)点也是 DOM 不如 canvas 的一(yi)个方面(mian),canvas 的每一(yi)帧都是(shi)可以控制的(de)。

最后(hou)附(fu)上(shang) demo 的(de) Github 仓库(ku):http://www.ued163.com/

评论 (2条)

发表评(ping)论

(必填(tian))

(选填(tian),不公(gong)开)

(选填,不公开(kai))

(必(bi)填)

久久香综合精品久久伊人欧美日韩一区二区综合高清在线观看 网站地图
四虎国产精品永久在线观看高清视频,白领人妻系列视频在线观看免费高清视频,性直播视频在线观看免费 久久香综合精品久久伊人欧美日韩一区二区综合高清在线观看每日更新国产精品白丝AV网站观看免费高清版,欧美日韩一区二区综合观看在线下载播放等成年人看的在线视频