直播需求:
流媒体:流式传输方式在Internet上播放的媒体格式,音视频等多媒体文件。
流式传输:将多媒体文件经过特殊压缩后分成一个个压缩包,再由服务器向客户端连续,实时传送。用户不用等整个文件全部下载完毕后才能播放,而是只需要经过几秒或几十秒的启动延时即可边下载边播放。
常用的流媒体传输协议:HTTP渐进式下载和实时流媒体协议。
常用的直播流协议:
直播原理:
视频采集处理后,编码 然后流媒体封装 推流至流媒体服务器,然后用户端从服务器拉视频流过来解码播放。
客户端直播插件
hls.js:可以实现HTTP实时流媒体客户端的js库。包小,灵活 专业直播hls协议流,但是对于常规的通用性播放器没有封装好的UI,功能上需要自己调API,协议单一,只支持HLS。
video.js:基于H5的网络视频播放器,支持多种格式的流媒体播放,浏览器不支持时可以实现优雅降级。插件机制强大,兼容性好。但是包大,修改UI时需要通过插件实现。
vue-video-player:其实就是将video.js集成到了Vue中,在Vue项目中使用起来会更方便。
弹幕实现:
思路:横向分为11个channel,每一个channel有一个值控制当前channel是否可以往里面添加弹幕dom。循环这个channel数组,如果当前这个channel是不能添加的话,看看下一个channel是否能添加。所有的弹幕循环一次,如果有弹幕没有坐上车,则建一个新的数组,按顺序推入,下一趟车再坐。直到所有的弹幕都已经坐上车为止。
每一个弹幕dom创建的时候随机一个step值,在window.requestAnimationFrame这个异步方法里来改变该弹幕的right值。
step大小不一样,移动的速度也不一样。
当前channel如何判断是否可以添加下一个弹幕?
假设下一个弹幕的step是最大值,此处最大是5,计算这个弹幕和当前的弹幕从右边移动到左边需要的时间差。然后用这个时间乘以当前弹幕的step,那就是这个弹幕先领先这么多时间移动,下一条弹幕就在从右到左的过程中是不会追上当前弹幕。
当前弹幕领先移动这么多时间移动到计算的距离时,再把当前这个channel标识置为可添加。
<template>
<div ref="barrageContainer" class="barrage-content tw-absolute tw-top-0 tw-left-0 tw-w-full tw-h-77 tw-overflow-hidden">
<div v-for="(item, index) in channel" :key="index" class="barrage-channel tw-h-7 tw-flex tw-items-center tw-relative"></div>
</div>
</template>
<script lang="ts">
import {Component, Watch, Ref} from 'vue-property-decorator';
import Vue from '@ncfe/nc.vue';
@Component({
name: 'BarrageRoll'
})
export default class BarrageRoll extends Vue {
@Ref('barrageContainer') barrageContainer!: HTMLDivElement;
private channel: any = new Array(11).fill(false);
private barragePool: any = [];
random(min, max) {
return parseInt(Math.random() * (max - min) + min);
}
mounted() {
this.barragePool = ['前方大量弹幕来袭,请做好准备!', '2333333', '主人下马客在船, 举酒欲饮无管弦。', '醉不成欢惨将别, 别时茫茫江浸月', '忽闻水上琵琶声, 主人忘归客不发。', '寻声暗问弹者谁? 琵琶声停欲语迟。', '移船相近邀相见, 添酒回灯重开宴。', '千呼万唤始出来, 犹抱琵琶半遮面。', '转轴拨弦三两声, 未成曲调先有情。', '弦弦掩抑声声思, 似诉平生不得志。', '低眉信手续续弹, 说尽心中无限事。', '轻拢慢捻抹复挑, 初为霓裳后六幺。', '大弦嘈嘈如急雨, 小弦切切如私语。', '嘈嘈切切错杂弹, 大珠小珠落玉盘。', '间关莺语花底滑, 幽咽泉流冰下难。', '冰泉冷涩弦凝绝, 凝绝不通声暂歇。', '别有幽愁暗恨生, 此时无声胜有声。', '银瓶乍破水浆迸, 铁骑突出刀枪鸣。', '曲终收拨当心画, 四弦一声如裂帛。', '东船西舫悄无言, 唯见江心秋月白。', '沉吟放拨插弦中, 整顿衣裳起敛容。', '自言本是京城女, 家在虾蟆陵下住。', '十三学得琵琶成, 名属教坊第一部。', '曲罢曾教善才服, 妆成每被秋娘妒。', '五陵年少争缠头, 一曲红绡不知数。', '钿头银篦击节碎, 血色罗裙翻酒污。', '今年欢笑复明年, 秋月春风等闲度。', '弟走从军阿姨死, 暮去朝来颜色故。', '门前冷落鞍马稀, 老大嫁作商人妇。', '商人重利轻别离, 前月浮梁买茶去。', '去来江口守空船, 绕船月明江水寒。', '夜深忽梦少年事, 梦啼妆泪红阑干。', '我闻琵琶已叹息, 又闻此语重唧唧。', '同是天涯沦落人, 相逢何必曾相识!', '我从去年辞帝京, 谪居卧病浔阳城。', '浔阳地僻无音乐, 终岁不闻丝竹声。', '住近湓江地低湿, 黄芦苦竹绕宅生。', '其间旦暮闻何物? 杜鹃啼血猿哀鸣。', '春江花朝秋月夜, 往往取酒还独倾。', '岂无山歌与村笛? 呕哑嘲哳难为听。', '今夜闻君琵琶语, 如听仙乐耳暂明。', '莫辞更坐弹一曲, 为君翻作《琵琶行》。', '感我此言良久立, 却坐促弦弦转急。', '凄凄不似向前声, 满座重闻皆掩泣。', '座中泣下谁最多? 江州司马青衫湿。', '2333333', '2333333', '2333333', '2333333', '浔阳江头夜送客, 枫叶荻花秋瑟瑟', '2333333', '2333333', '2333333', '2333333', '2333333', '2333333'];
}
@Watch('barragePool', {deep: true})
barragePoolAction(list) {
if (list.length) {
this.checkList(list);
}
}
checkList(list) {
let remind: any = [];
list.forEach(barrage => {
let isShoot: boolean = false;
if (barrage) {
for (let index in this.channel) {
if (!this.channel[index]) {
this.channel[index] = true;
this.shoot(barrage, index);
isShoot = true;
break;
}
}
if (!isShoot) {
remind.push(barrage);
}
}
});
if (remind.length) {
let timeout = setTimeout(() => {
this.checkList(remind);
clearTimeout(timeout);
}, 500);
}
}
shoot(barrage, index) {
const oSpan = document.createElement('span');
oSpan.innerHTML = barrage;
oSpan.className = 'barrage-span';
oSpan.dataset.begin = 'limit';
const containerW = this.barrageContainer.getBoundingClientRect().width;
const channel = document.querySelectorAll('.barrage-channel')[index];
channel.appendChild(oSpan);
const step = this.random(2, 5);
const spanW = oSpan.getBoundingClientRect().width;
let right = -spanW;
window.requestAnimationFrame(this.move(oSpan, spanW, right, index, step, channel, containerW));
}
move(span, width, right, index, step, channel, containerW) {
return () => {
right += step;
span.style.right = right + 'px';
if (right < containerW) {
window.requestAnimationFrame(this.move(span, width, right, index, step, channel, containerW));
if (span.dataset.begin === 'limit' && right > this.diffDistance(step, containerW) && this.channel[index]) {
span.dataset.begin = 'notLimit';
this.channel[index] = false;
}
} else {
channel.removeChild(span);
}
};
}
diffDistance(step, containerW) {
return (containerW / step - containerW / 5) * step + 20;
}
}
</script>
<style lang="scss">
.barrage-span {
font-size: 20px;
line-height: 28px;
position: absolute;
top: 0px;
color: white;
white-space: nowrap;
}
</style>
效果:
window.requestAnimationFrame(()=>{}); 16ms多执行一次
requestAnimationFrame相对比setTimeout和setInterval的优势:
requestAnimationFrame会把每一帧的所有dom操作集中起来,在一次重绘或回流中完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说 这个频率为每秒60帧。
在隐藏或不可见的元素中,requestAnimationFrame将不会进行重绘或回流,更少的cpu,gpu和内存使用量。
停止requestAnimationFrame:
cancelAnimationFrame() 接收一个参数,requestAnimationFrame默认会返回一个id,cancelAnimationFrame只需要传入这个id就可以停止了。