【小案例】基于色键技术的纯客户端实时蒙版弹幕

导读:本文内容是笔者最近实现的 web 端弹幕组件—— Barrage UI 的一个延伸。在阅读本文的实例和相关代码之前,不妨先浏览项目文档,对组件的使用方式和相关接口进行了解。各位童鞋如果经常上 B 站(bilibili.com) ,应该对 蒙版弹幕 这个概念并不陌生。蒙版弹幕 是由知名弹幕视频网站 bilibili 于 2018 年中推出的一种弹幕渲染效果,可以有效减少弹幕文字对视频主体信息的干扰。关于 B 站蒙版弹幕的实现原理,其实网上已经有很多细致的讨论和研究。个人总结了一下,大致要点如下:基于用户数据和一些机器学习的相关应用,可以提炼出视频的关键主体服务端预先对视频进行处理,并生成相应的蒙版数据客户端播放视频时,实时地加载对应资源通过一些前端的技术手段,实现弹幕的蒙版处理客户端方面,由于 B 站弹幕是基于 div+css 的实现,因而采用了 svg 格式来传输矢量蒙版(至少目前是这样),通过 CSS 遮罩的方式实现渲染。逼乎上有一篇关于这个方案的讨论,感兴趣的童鞋可以移步 这里 进行了解。Barrage UIBarrage UI 是个人最近实现的一个前端弹幕组件,主要用于在前端页面中挂载弹幕动画。组件提供了一系列的操作接口以方便用户对弹幕的相关特性进行定制。你也可以在渲染层面对动画中的每一帧图形进行处理,比如:实时读取视频信息对每一帧视频图像进行实时处理,计算出抠图蒙版将计算出的蒙版传给弹幕组件,以实现实时的蒙版弹幕下面是基于 Barrage UI 组件实现的蒙版弹幕效果:由于文中不方便嵌入视频,Demo 的实际效果请移步到 此处 查看。下面我们来介绍如何实现上图的动画效果。色键(色度键控)Demo 中使用的了初音小姐姐跳舞的视频。最主要的特点是除了人物外,视频的背景是比较一致的纯色。对于这种类型的图像,我们可以使用 色键 的方式进行抠图(生成“蒙版”)。色度键控,又称色彩嵌空,是一种去背合成技术。Chroma 为纯色之意,Key 则是抽离颜色之意。把被拍摄的人物或物体放置于绿幕的前面,并进行去背后,将其替换成其他的背景。此技术在电影、电视剧及游戏制作中被大量使用,色键也是虚拟摄影棚(Virtual studio)与视觉效果(Visual effects)当中的一个重要环节。下图是色键技术的一个示例:在绿幕前穿着蓝色衣服的小姐姐,左图为去背前,右图为去背后的新背景。如何扣取视频图像在浏览器环境中,我们可以通过 canvas 画布实时地绘制视频的每一帧,并从画布中读取到图像中每个像素的 RGBA 信息,检测每个点的 R(red)、G(green)、B(blue) 值是否满足要求,最终将需要扣除的像素的 A(alpha) 值置为 0,即可得到用于合成蒙版弹幕的蒙版图像。注意:Barrage UI 组件的蒙版功能是基于 Canvas 2D API 的 CanvasRenderingContext2D.globalCompositeOperation 属性实现的(使用了 source-in 的混合模式),因而只需将不需要的像素设置为透明(alpha=0)即可,并不需要改变图像的 RGB 色值。下面介绍此案例的代码实现。具体实现安装 Barrage UI 组件直接使用 yarn 或 npm 安装此组件:yarn add barrage-ui or npm install –save barrage-uiHTML + CSS准备一个 video 元素用于播放视频,video 的父级元素用于挂载弹幕:<div id=“container”> <video id=“video” src=“videos/demo.mp4” controls></video></div>根据视频的实际尺寸(880×540)设置 #container 与 #video 的样式:html,body { font: 14px/18px Helvetica, Arial, ‘Microsoft Yahei’, Verdana, sans-serif; width: 100%; margin: 0; padding: 0; background: #eee; overflow: hidden;}#container,#video { width: 880px; height: 540px;}#container { margin: 0 auto; margin-top: 50vh; margin-left: 50vw; transform: translate(-50%, -50%); background-color: #ddd;}创建弹幕import Barrage from ‘barrage-ui’;import data from ‘utils/mockData’;// 获取父级容器const container = document.getElementById(‘container’);// 创建弹幕实例const barrage = new Barrage({ container: container,});// 重置画布高度,避免弹幕遮挡视频播放控件barrage.canvas.height = container.clientHeight - 80;// 装填弹幕数据barrage.setData(data);其中,mockData 是用于生成随机弹幕数据的方法。关于弹幕数据的内容与格式,详见 Barrage UI 项目文档实时获取视频图像// 获取 video 元素const video = document.getElementById(‘video’);// 新建一个画布来实时绘制视频(纯绘图,不用添加进页面)const vCanvas = document.createElement(‘canvas’);vCanvas.width = video.clientWidth;vCanvas.height = video.clientHeight;const vContext = vCanvas.getContext(‘2d’);// 实时绘制视频到画布barrage.afterRender = () => { vContext.drawImage(video, 0, 0, vCanvas.width, vCanvas.height);};使用组件提供的渲染周期钩子 .afterRender() 可以在弹幕动画的每一帧图像渲染后,将视频图像绘制到中间画布 vCanvas 上。注意这里的 vCanvas 画布主要用于实时地获取视频图像,并不需要添加到页面中。实时计算蒙版信息// 渲染前读取画布 vCanvas 的数据,并处理为蒙版图像barrage.beforeRender = () => { // 读取图像 const frame = vContext.getImageData(0, 0, vCanvas.width, vCanvas.height); // 图像总像素个数 const pxCount = frame.data.length / 4; // 将 frame 构造成我们需要的蒙版图像 for (let i = 0; i < pxCount; i++) { // 这里不用 ES6 解构赋值的写法,主要为了保证性能 // PS: 这里如果用解构赋值语法将导致大量新对象的创建,是个很耗时的过程 const r = frame.data[i * 4 + 0]; const g = frame.data[i * 4 + 1]; const b = frame.data[i * 4 + 2]; // 将黑色区域以外的内容设为透明 if (r > 15 || g > 15 || b > 15) { frame.data[4 * i + 3] = 0; } } // 设置蒙版 barrage.setMask(frame);};使用组件提供的渲染周期钩子 .beforeRender() 可以在弹幕动画的每一帧图像渲染前计算出蒙版图像。其中,用于更新蒙版的接口为 .setMask()。视频、弹幕的操作绑定最后,为了让弹幕的行为与视频播放的操作协同,还需要进行一些绑定的操作:// 绑定播放事件video.addEventListener( ‘play’, () => { barrage.play(); }, false);// 绑定暂停事件video.addEventListener( ‘pause’, () => { barrage.pause(); }, false);// 切换播放进度video.addEventListener( ‘seeked’, () => { barrage.goto(video.currentTime * 1000); }, false);这里分别用到 Brrage UI 组件的 .play() .pause .goto() 三个接口,分别用于播放、暂停和切换弹幕动画的进度。需要注意的是,通过 video.currentTime 属性获取到的视频播放进度是一个单位为 秒 的浮点数,需要转换为 毫秒数 再传给弹幕组件。源码奉上本文的案例已上传 github,感兴趣的童鞋可以点击 这里 查看源码细节。关于 Barrage UI 组件如果有什么建议和疑问,欢迎大家在项目中提 issue 给我,帮助我持续改进和迭代,更欢迎 star 和 PR。感谢您能耐心读到此处,如果觉得有趣或有用,不妨 点赞/评论/转发 此文,再谢。 ...

January 20, 2019 · 2 min · jiezi

可实现B站 蒙版弹幕 效果的前端组件 —— barrage-ui

barrage-uiBest and lightest barrage component for web ui.适用于 web 端用户界面和播放器的轻量级弹幕组件用途为你的 视频播放器、图片浏览器 等元素挂载弹幕动画用于实现 B 站(bilibili.com) 风格的 蒙版弹幕 效果安装yarn add barrage-ui或npm install –save barrage-ui快速开始import Barrage from ‘barrage-ui’;import example from ‘barrage-ui/example.json’; // 组件提供的示例数据// 加载弹幕const barrage = new Barrage({ container: ‘barrage’, // 父级容器或ID data: example, // 弹幕数据 config: { // 全局配置项 duration: 20000, // 弹幕循环周期(单位:毫秒) fontFamily: ‘Microsoft Yahei’, // 弹幕默认字体 defaultColor: ‘#fff’, // 弹幕默认颜色 },});// 新增一条弹幕barrage.add({ time: 1000, // 弹幕出现的时间(单位:毫秒) text: ‘这是新增的一条弹幕’, // 弹幕文本内容 fontSize: 24, // 该条弹幕的字号大小(单位:像素),会覆盖全局设置 color: ‘#0ff’, // 该条弹幕的颜色,会覆盖全局设置});// 播放弹幕barrage.play();初始化参数创建弹幕实例时,需要传入的初始化参数如下:参数数据类型默认值说明containerstring/element必传,无默认值弹幕的挂载点dataarray[]弹幕数据configobject详见全局配置项详见全局配置项maskstring/ImageDatastring/ImageData蒙版图像,用于实现蒙版弹幕效果,详见蒙版弹幕beforeRenderfunctionctx => {}帧渲染前的回调,函数参数为 canvas 画布的上下文afterRenderfunctionctx => {}帧渲染后的回调,函数参数为 canvas 画布的上下文overlapOptimizedbooleanfalse弹幕装填时是否启用布局优化,以尽可能避免使相邻时间的弹幕重叠其中,container 参数在初始化实例时必传,其他参数为可选,数据类型及默认值如上表所示。全局配置项配置项及默认值弹幕的所有全局配置项及默认值如下:{ duration: -1, // -1 表示不循环播放 speed: 100, // 弹幕的运动速度 fontSize: 20, // 文字大小,单位:像素 fontFamily: ‘serif’, // 字体 textShadowBlur: 0.1, // 字体阴影大小,有效值 0-1 opacity: 1.0, // 透明度,有效值 0-1 defaultColor: ‘#fff’, // 默认颜色,与 CSS 颜色属性一致}更新配置项如果你的弹幕实例已创建或者正在播放,可以通过 .setConfig() 方法进行实时更新:// 更新全局透明度barrage.setConfig({ opacity: 0.5 });弹幕数据结构与内容弹幕数据为一个对象数组。每个数组元素对应一条弹幕记录,其结构如下:{ createdAt: ‘2019-01-13T13:34:47.126Z’, time: 1200, text: ‘我膨胀了’, fontFamily: ‘SimSun’, fontSize: 32, color: ‘yellow’,}数据字段createdAt - 弹幕的创建时间 (必须)time - 弹幕的动画时间 (必须)text - 弹幕文本内容 (必须)fontFamily - 弹幕文本的字体 (可选)fontSize - 弹幕文本字号大小,单位:像素 (可选)color - 弹幕文本的颜色 (可选)装填弹幕装填弹幕有两种方式:方式一:初始化时传入数据const barrage = new Barrage({ container: ‘barrage’, data: JSON_DATA, // JSON_DATA -> 你的弹幕数据});方式二:初始化后更新数据const barrage = new Barrage({ container: ‘barrage’,});barrage.setData(JSON_DATA); // JSON_DATA -> 你的弹幕数据新增弹幕如果你的弹幕实例已创建或者正在播放,可以通过 .add() 方法新增一条记录:barrage.add({ time: 1000, text: ‘这是新增的一条弹幕’, fontSize: 24, color: ‘#0ff’,});.add() 方法一般搭配 数据提交/请求 操作进行使用,以实现真实的线上应用。场景举例某用户在客户端提交了一条弹幕到服务端,服务端将数据存储并分发给正在进行会话的客户端,最后客户端通过 .add() 方法进行数据更新。动画控制接口.play()描述用于播放动画。若当前为暂停状态,则从当前进度继续播放用例barrage.play();.pause()描述用于暂停动画用例barrage.pause();.replay()描述用于重新开始播放动画用例barrage.replay();.goto(progress)描述用于跳转播放进度。此方法在动画播放和暂停的状态下均有效参数progress - 待跳转的进度。值为一个毫秒数,表示跳转到动画的第几毫秒用例barrage.goto(15000); // 跳转到第 15 秒蒙版弹幕Barrage 组件提供了实现 蒙版弹幕 效果的可能。基于本组件实现的 demo 效果如下:什么是“蒙版弹幕”蒙版弹幕 是由知名弹幕视频网站 bilibili 于 2018 年中推出的一种弹幕渲染效果,可以有效减少弹幕文字对视频主体信息的干扰。详细资料可参考 bilibili 的相关文章:弹幕阳光计划第十弹 蒙版听说过吗,弹幕黑科技了解一下?不挡脸,放肆看!B 站黑科技蒙版弹幕揭秘实现原理如果你熟悉最著名的图像处理软件——Adobe Photoshop,那么你应该对 “蒙版” 的概念不陌生,“蒙版弹幕” 的实现原理与此类似,即:将图像的一部分 “隐藏”。Barrage 组件的初始化参数中的 mask 一项即用于处理蒙版效果。对于上文截图中的效果,其使用的蒙版图像效果如下:弹幕渲染时,会将蒙版图像中 “镂空” 的部分(图像 RGBA 通道中 Alpha 通道为 0 的像素)去除,从而达到 “蒙版弹幕” 的效果。简单蒙版弹幕的实现为 barrage 实例设置蒙版图像(mask)即可实现蒙版弹幕效果。可通过初始化参数 mask 传入蒙版图像:import Barrage from ‘barrage-ui’;import example from ‘barrage-ui/example.json’;const barrage = new Barrage({ container: ‘barrage’, data: example, mask: ‘mask.png’, // 传入蒙版图像的 url});也可以在弹幕初始化后,通过 .setMask() 方法进行实时更新:import Barrage from ‘barrage-ui’;import example from ‘barrage-ui/example.json’;const barrage = new Barrage({ container: ‘barrage’, data: example,});// 设置蒙版图像barrage.setMask(‘mask.png’); // 传入蒙版图像的 url注意事项mask 参数和 .setMask() 方法的参数类型一致,可接收图像的 url 或 ImageData实时渲染上文的示例仅能够实现一帧图像的渲染(只设置了一次 mask),要实现实时的蒙版效果,需要对弹幕动画的每一帧进行处理。使用组件提供的 beforeRender 钩子函数,可以轻易的实现:import Barrage from ‘barrage-ui’;import example from ‘barrage-ui/example.json’;const barrage = new Barrage({ container: ‘barrage’, data: example, beforeRender: () => { const newImage = getMask(); // 用于获取当前帧对应蒙版的方法 barrage.setMask(newImage); },});当然,beforeRender 钩子也可以在弹幕初始化之后挂载:import Barrage from ‘barrage-ui’;import example from ‘barrage-ui/example.json’;const barrage = new Barrage({ container: ‘barrage’, data: example,});barrage.beforeRender = () => { const newImage = getMask(); // 用于获取当前帧对应蒙版的方法 barrage.setMask(newImage);}; ...

January 15, 2019 · 2 min · jiezi