找回密码
 会员注册
查看: 20|回复: 0

哔哩哔哩应援弹幕

[复制链接]

5

主题

0

回帖

16

积分

新手上路

积分
16
发表于 2024-10-6 23:35:51 | 显示全部楼层 |阅读模式
本期作者臧至聪哔哩哔哩资深开发工程师负责B站移动端弹幕相关业务,致力于持续探索更多更新奇更有趣的创意交互形式。背景哔哩哔哩移动端日均活跃用户超过5千万,日均视频播放量超过12亿次,用户日均使用时长达到了75分钟。在越来越多新用户涌入哔哩哔哩的同时,提高视频的互动性和趣味性也变得至关重要。弹幕作为一种观看视频的交互形式,同时也是B站的一个非常重要的标签,深受广大用户的喜爱。作为业内弹幕领域的标杆,B站在近段时间内推出了多种全新的弹幕类交互产品,如投票弹幕、关注三连弹幕、烟花特效弹幕、应援弹幕等。这些弹幕的背后都离不开Chronos引擎——一款B站自研的移动端跨平台的渲染引擎。它可以运行在Android、iOS等多个平台之上,使用TypeScript作为业务开发语言,并提供了丰富的API,使业务层能够非常便捷高效地实现文字、图形的渲染及动画。今天就来介绍一下基于Chronos引擎,我们最新推出的一款全新的弹幕交互产品——应援弹幕。应援弹幕视频中通常会出现一些高潮或情绪渲染力较强的场景和镜头,这些场景将调动用户情绪、引起用户共鸣。目前用户主要通过发弹幕、跟队形等方式来表达情感和共鸣,形式比较单一。现推出一款应援弹幕,将用户发送的弹幕与当下视频场景强相关的文字或图片组合,由此打造视频定制化的弹幕体验,以独特性和新鲜感刺激用户发弹幕,贴合视频内容表达情感,增强互动趣味性。整体方案对于应援弹幕来说,我们不光要能够绘制文字,还要根据给定的背景底图将文字填充进去,达到一种类似于词云的效果。同时对于文字的填充,需要有动画效果来营造一种填充感,用以激发用户参与互动的热情。关于应援弹幕中词云的效果的实现,有两种方案:1. 客户端每次展示前,根据该条应援弹幕中的背景图及弹幕数据,实时计算出弹幕的布局后,再展示给用户;2. 服务端将弹幕的布局信息提前计算完成,然后把计算结果连同数据部分一并发送给客户端,客户端根据服务端的数据及计算结果,直接进行展示。乍一看,方案2会是一个对客户端实现非常友好地方案。但细细想来,这个方案在可能存在以下问题:a. 众所周知,文字所占区域的大小,与字体和字号有着密不可分的关系。移动端设备众多,且不说不同厂商不同品牌不同型号的手机的字体有区别,有的时候同样的手机,不同的系统版本,字体也有所不同。如何保证服务端计算的布局结果,在所有设备上的渲染效果一致是个比较大的问题。b. 虽然文字的渲染会受到各种设备条件的影响,在多设备中存在效果不一致的情况,但是图片可以在多端保持渲染效果的一致性。如果服务端事先将应援弹幕的布局结果生成为图片,下发给客户端,就可以保证最终渲染效果的一致性。但是这种整张图片的方式会失去整个应援弹幕局部动画的能力,而且也减少了整个应援弹幕用户交互部分的可玩性。由于应援弹幕本身就是想要激发用户的互动热情,提升用户对于弹幕的参与度,最终我们还是选择了方案1的方式,由客户端完成实时渲染。上图是应援弹幕简单的示意流程,其中应援弹幕数据中至少应包含以下信息:progress:应援弹幕上屏时间点(相对于视频时间)duration:应援弹幕持续时间position:应援弹幕显示位置picture:应援弹幕背景图dms:应援弹幕中填充的弹幕数据客户端根据应援弹幕数据中picture和dms的信息可以完成应援弹幕词云效果的动态布局,并根据position、progress和duration的信息实现整个应援弹幕上屏渲染的流程。由于应援弹幕整体的体验与词云填充效果有着密不可分的关系,一方面越贴合底图的文字填充会带来更好的是视觉效果,另一方面,越快完成布局,尽可能快的将效果展现给用户,能在用户参与交互的过程中,快速的给予用户反馈,提升用户的参与度。如何在移动端实现一个既能满足实时性要求,又有着相对好的视觉体验的词云效果填充,成为了整体实现的一个难点。词云填充效果实现要实现词云填充效果,首先需要感知被填充图片的内容,也就是哪些区域可以被弹幕填充,哪些区域无法被弹幕填充。有一个最简单的办法,就是获取到图片中每个像素点的色值,根据所有像素点的色值,就可以计算出整幅图片中,哪些位置是可以被填充的。......image.toPixelData().forEach((pixel, index) => { ?if (pixel.alpha maxCol || minRow > maxRow) { ? ? ?return true; ? ?} ? ?for (let col = minCol; col 0 ? 0 : 1; ? ?this._integralPixelArray[index] = this._filledArray[index]; ? ?if (col > 0) { ? ? ?this._integralPixelArray[index] += this._integralPixelArray[this.positionToIndex(col - 1, row)]; ? ?} ? ?if (row > 0) { ? ? ?this._integralPixelArray[index] += this._integralPixelArray[this.positionToIndex(col, row - 1)]; ? ?} ? ?if (col > 0 & row > 0) { ? ? ?this._integralPixelArray[index] -= this._integralPixelArray[this.positionToIndex(col - 1, row - 1)]; ? ?} ?}}......protected isAvailable(minCol: number, minRow: number, maxCol: number, maxRow: number): boolean { ? ?const totalPixelCount = this._integralPixelArray[this.positionToIndex(maxCol, maxRow)]; ? ?const topPixelCount = minRow Position { ?let x = 0; ?let y = 0; ?return function (t: number, offset: number = 0) { ? ?t = t + offset; ? ?const sign = t = this._width) & (pos.row = this._height)) { break; } if (pos.col = this._width || pos.row = this._height) { continue; } const index = this.positionToIndex(pos.col, pos.row); if (this._colorArray[index][3] { ? ?const textureKey: string | null = response.key ?? null; ? ?if (expired) { ? ? ?cron.TransferCenter.instance.popObjectForKey(textureKey)?.release(); ? ? ?return; ? ?} ? ?const count: number = response.count ?? 0; ? ?const completed: boolean = response.completed ?? false; ? ?const success: boolean = response.success ?? false; ? ?if (completed) { ? ? ?this.reportShowEventIfNeeded(success, count); ? ?} else { ? ? ?this.triggerWordCloudEngineUpdate(); ? ?} ? ?const texture = cron.TransferCenter.instance.popObjectForKey(textureKey); ? ?this._displayer.addOneGroup(texture, completed, success); ? ?texture.release(); ?});......Main......const timeout = request.timeout;const count = request.count;const results = wordCloudEngine.run(timeout, count);const img = wordCloudEngine.snapshot(results);const texture = cron.Texture.createFromImage(img);const key = `word_cloud_${token++}`;cron.TransferCenter.instance.pushObjectForKey(key, texture);texture.release();response.key = key;response.completed = wordCloudEngine.completed;response.success = wordCloudEngine.filledRatio >= DEFAULT_FILLED_RATIO;response.count = wordCloudEngine.results.length;......run(timeout: number = Number.POSITIVE_INFINITY, count?: number): Result[] { const results: Result[] = []; const ts = Date.now(); while (!this.completed & Date.now() - ts | null { ?if (this.completed) { ? ?return null; ?} ?const model = this._models.next(); ?const style = this.findAvailable(model); ?if (!style) { ? ?return null; ?} ?const result = { ? ?model, ? ?style, ?}; ?this._results.push(result); ?return result;}Worker渲染优化为了优化渲染效率,并兼顾一些灵动的动画,应援弹幕整体将布局结果分批次合并成多幅纹理,通过纹理的叠加,提升了渲染的效率。同时,在合并过程中,通过混色模式的设置,可以很轻松地实现下图的效果。最终效果 已关注 关注 重播 分享 赞 关闭观看更多更多哔哩哔哩技术已关注分享视频,时长00:180/000:00/00:18 切换到横屏模式 继续播放进度条,百分之0播放00:00/00:1800:18 倍速播放中 0.5倍 0.75倍 1.0倍 1.5倍 2.0倍 超清 流畅 您的浏览器不支持 video 标签 继续观看 哔哩哔哩应援弹幕 观看更多转载,哔哩哔哩应援弹幕哔哩哔哩技术已关注分享点赞在看已同步到看一看写下你的评论 视频详情 总结与展望伴随着Chronos引擎功能的不断强大,在未来,我们会持续探索更多新型的弹幕展现形式及创意交互,增强用户在视频观看过程中的参与度,给用户带来更多更新奇、更有趣的视频观看体验。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2025-1-11 06:00 , Processed in 0.787536 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表