-
2020-11-07 15:46:56
因为要增加一下小程序的互动,或者说互相督促的作用吧,加个弹幕大家可以日常开撕,哈哈哈……效果如下,视频只能给url,就没弄了。
思路
1、首先弹幕的滚动,我是想用css的动画来写,主要是其他的不会,菜是原罪。
2、每个弹幕不能在同一轨道上显示,给个随机数给css动画top值,就能不重叠显示everyone的弹幕。
3、小程序云还是比较容易上手的,将输入的弹幕传入云数据库,这里实时显示我取巧了,直接把你发射的弹幕先投屏到轨道上,但其他人看到你的弹幕要从云上拿数据。
4、显示他人弹幕的话,主要担心数据量太大,我只拿本月的弹幕数据下来做遍历展示,当然做了个手动选择弹幕开启关闭。前端页面
参考了一个博主的样式,但写了太久了我忘了是谁了……
背景框
<view class='launchGird'> <!-- 背景框 --> <view class='displayGroup'> <view class='dmGroup' wx:for="{{ dmData }}" wx:key="id" style="top:{{ item.top }}%; animation: dmAnimation {{item.time}}s linear {{ index*3 }}s infinite; ">
弹幕文字框
其实我没写啥框的样式,哈哈哈哈
<!-- 弹幕文字框 --> <view class='dmItem'> <view class='dm'> <text class='content'>{{ item.content }}</text> </view> </view> </view>
自定义新轨道
<!-- 自定义新轨道 --> <view class='dmGroup' wx:for="{{ dmData1 }}" wx:key="id" style="top:{{ item.top }}%; animation: dmAnimation {{item.time}}s linear {{ index*3 }}s infinite; "> <view class='dmItem'> <view class='dm'> <text class='content'>{{ item.content }}</text> </view> </view> </view> <!-- 背景图 --> <view class='focus'> <image src='cloud://xiaoxiong-6p4ms.7869-xiaoxiong-6p4ms-1301094256/swiper/scut11.jpg' class='img' mode='aspectFix'></image> </view> <view class='shadowOverlay'></view> </view>
弹幕输入框
<!-- 弹幕输入框 --> <view class="barrage-inputText"> <view class="barrage-input"> <input bindinput="getvalue" value="{{baseData1}}" maxlength="100" placeholder="发个弹幕试试?"/> </view> <view class="barrage-shoot"> <button size="mini" bindtap="bind_shoot">Send</button> </view> <switch id="switch_" bindchange="barrageSwitch"/> <text>弹幕</text> </view> </view>
CSS样式
.launchGird{ padding: 20rpx; position: relative; height:100%;} .displayGroup{ background: #eee; border: 20rpx solid #fff; box-shadow: 0 0 10rpx #eee; border-radius: 10rpx; margin-bottom: 40rpx; height: 420rpx; position: relative; overflow: hidden;} .dmGroup{ position: absolute; top:140rpx; left: 100%; z-index: 10; animation-timing-function: linear; animation-fill-mode: none; transform: translateZ(0); white-space: nowrap; height: 60rpx; } .dmItem{ display: inline-flex; margin-right: 60rpx; white-space: nowrap; } .dmItem .dm{ display: inline-flex; vertical-align: middle; align-items: center; position: relative; height: 50rpx; line-height: 50rpx; padding: 0 15rpx 0 5rpx; background: rgba(0,0,0,.5); border-radius: 25rpx; overflow: hidden; font-size: 24rpx; color: #fff; } .dmItem .dm .content{display: inline-block; max-width: 440rpx; height: 50rpx; line-height: 50rpx; margin-right: 10rpx; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; } .focus{ position: absolute; left: 0; top: 0; z-index: 2; width: 100%; height: 100%; border-radius: 10rpx; overflow: hidden; } .shadowOverlay { position: absolute; top: 0; left: 0; z-index: 5; width: 100%; height: 100%; overflow: hidden; background-image: linear-gradient(0deg, rgba(0,0,0,.8) 0%, rgba(0,0,0,.3) 50%,rgba(0,0,0,.1) 100%); } .img{ width: 100%; height: 100%; border: 0; } /* 发射框 */ .barrage-inputText{ /* position: absolute; */ display: flex; background-color: rgb(219, 219, 219); width: 100%; height: 80rpx; flex-direction: row; nav-index: 2; justify-content: center; align-items: center; box-shadow:0.5px 0.5px 3px #ffffff; } .barrage-input{ background-color: rgb(243, 255, 239); width: 60%; height: 30px; } .barrage-shoot{ display:flex; margin-left: 10px; width: 25%; height: 30px; justify-content: center; } .shoot{ width: 100%; color: black; }
JS以及云存储/读取
1、首先我们要获取用户输入的内容,暂时存储到data里的baseData1,因为用户每次只有一句话所以给个字符串就OK
const app = getApp() const DB = wx.cloud.database().collection('scut-dm')//这里你写自己的数据库名字 var util = require('../../utils/util.js') var dayTime = util.formatTime(new Date()); Page({ data: { baseData: [],//历史弹幕(云加载) baseData1:"",//即时输入内容 dmData: [],//加载后处理数据 dmData1:[],//键盘输入传入 }, //获取来自input的内容 getvalue(e){ this.setData({ baseData1:e.detail.value }) // console.log(e.detail.value) },
2、发射弹幕&将弹幕内容传入云
//发射弹幕,处理input的内容加时间和top变成数组,再交给页面遍历 bind_shoot: function () { // 将即时DM传入云 DB.add({ data: { content: this.data.baseData1, date: dayTime }, success(res) { console.log('添加成功', res) }, fail(res) { console.log("添加失败", res) } }) //即使发送弹幕 const dmArr = []; const _b = this.data.baseData1; for (let i = 0; i < 1; i++) { const time = Math.floor(Math.random() * 10); const _time = time < 6 ? 6 + i : time + i;//这是弹幕从左到右的随机时间 const top = Math.floor(Math.random() * 80) + 2;//这里是给每个弹幕一个随机高度,避免重叠显示 const _p = { content: _b, top, time: _time, }; dmArr.push(_p); } this.setData({ dmData1: dmArr,//将文本,位置,运动速度给dmData1给前端遍历 baseData1:"" }); // console.log("啦啦啦啦", dmArr) },
3、调用云历史弹幕
这里我通过当前年月字段来匹配本月的弹幕,所以对当前时间做分割(其实也是自己nc数据库没做索引搞得匹配麻烦的一批,当然对各位大佬来说怎么写都很简单)拿到的数据存到baseData数组// 调用历史弹幕 let arr ="" let arrf = "" let str = dayTime arr=str.split("/")//分离成数组["2020","08","27 时分秒"] // arrf = arr[2].split(" ")//提取第三个数组分离成["27","时分秒"] // arr.splice(2,2,arrf[0])取出27并添加到arr第三个位置 // const arr1 = arr.join(",").replace(/,/g, "")替换所有得都好成/ arr.splice(2,2) arrf = arr.join(",").replace(/,/g, "/") console.log("这是什么",arrf) // console.log("这是什么", arr1) DB.where({ // name: _name, date: wx.cloud.database().RegExp({ regexp: arrf, options: 'i', }) }).get({ success(res) { console.log("模糊查询成功", res) that.setData({ baseData: res.data }) }, fail() { console.log("获取失败", res) } }); // wx.cloud.callFunction({ // name: "getdm2", // success(res) { // console.log("获取成功dm-------", res.result.data) // that.setData({ // baseData: res.result.data // }) // }, // fail(res) { // console.log("获取失败dm", res) // } // }) },
4、处理历史弹幕位置,就是将上面云拿到的数据处理遍历。其实这个函数和上面实时的函数是一样的,要是想省略可以直接传参,但……我还是选择了ctrl+c/ctrl+v,没有为什么,就是懒。
// 处理弹幕位置 setDM: function () { const dmArr = []; const _b = this.data.baseData;//导入了来自app.js的所有弹幕消息 for (let i = 0; i < _b.length; i++) { const time = Math.floor(Math.random() * 10); const _time = time < 6 ? 6 + i : time + i; const top = Math.floor(Math.random() * 80) + 2; const _p = { content: _b[i].content, top, time: _time, }; dmArr.push(_p); } this.setData({ dmData: dmArr }); },
5、判断用户是否打开弹幕开关
//判断是否打开弹幕 barrageSwitch(e) { if (e.detail.value) { this.setDM() } else{ this.setData({ dmData:[] }) } },
大概就是这么些东西,挺简单的。
更多相关内容 -
带你实现完整的视频弹幕系统
2021-01-09 22:37:36本文字数:6244字预计阅读时间:35分钟+介绍弹幕诞生于日本的视频平台,后来被B站这种短视频平台引入到国内,并在国内发展壮大。后来逐渐被长视频平台所接受,现在视频相关的应用基本上都会有...本文字数:6244字
预计阅读时间:35分钟
+
介绍
弹幕诞生于日本的视频平台,后来被B站这种短视频平台引入到国内,并在国内发展壮大。后来逐渐被长视频平台所接受,现在视频相关的应用基本上都会有弹幕。
但是长视频弹幕和B站这类的短视频弹幕还不太一样,短视频平台有自己特有的弹幕文化,所以弹幕更注重和用户的互动。长视频平台还是以看剧为主,弹幕类似于评论的功能,所以不能影响用户看剧,弹幕不能太密集,而且相互之间最好不要有遮盖,否则会对视频内容会有比较明显的影响。
本篇文章主要从长视频平台的角度来讲弹幕的实现原理,但其实短视频平台的弹幕也是同样的原理,区别在于短视频可能弹幕种类会多一些。
+
技术实现
画布
以我公司应用为例,有
iPhone
和iPad
两个平台,在iPhone
平台上有横竖屏的概念,都需要展示弹幕。在iPad
上有大小屏的概念,也需要都展示弹幕。弹幕的技术方案肯定是两个平台用一套,但需要考虑跨不同设备和屏幕的情况。所以,对于这个问题,我通过画布的概念来解决通用性的问题。画布并不区分屏幕大小和比例的概念,只是单纯的用来展示弹幕,并不处理其他业务逻辑,通过一个
Render
类来控制画布的渲染。对于不同设备上的差异,例如iPad
字体大一些,iPhone
字体小一些这种情况,通过config
类来进行控制,画布内部不做判断。小屏上画布会根据比例少展示一些,大屏上则多展示一些。字体变大画布也会根据比例和左右间距进行控制,保证展示比例是对的,并且在屏幕宽高发生改变后,自动适应新的尺寸,不会出现弹幕衔接断开的问题,例如
iPad
上大小屏切换。外部在使用时,只需要传入一个frame
即可,不需要关注画布内部的调整。弹幕轨道
从屏幕上来看,可以看到弹幕一般都是一行一行的。为了方便对弹幕视图进行管理,以及后续的扩展工作,我对弹幕设计了“轨道”的概念。每一行都是一个轨道,对弹幕进行横向的管理,这一行包括速度、末端弹幕、高度等参数,这些参数适用于这一行的所有弹幕。轨道是一个虚拟的概念,并没有对应的视图。
轨道有对应的类来实现,类中会包含一个数组,数组中有这一行所有的弹幕。这个思路有点像玩过的一款游戏-节奏大师,里面也有音乐轨道的概念,每个轨道上对应不同速度和颜色的音符,音符数量也是不固定的,根据节奏来决定。
轨道还有一个好处在于,对于不同速度的弹幕比较好控制。例如腾讯视频的弹幕其实是不同速度的,但是你仔细观察的话,可以发现他们的弹幕是“奇偶行不同速”,也就是奇数行一个速度,偶数行一个速度,让人从感官上来觉得所有弹幕的速度都不一样。如果通过轨道的方式就很好实现,不同的轨道根据当前所在行数,对发出的弹幕设置不同的速度即可。
有时候看视频过程中会从右侧出现一条活动弹幕,可能是视频中的梗,也可能是类似于广告的互动。但是活动弹幕出现时一般是单行清屏的,也就是和普通弹幕是互斥的,展示活动弹幕的时候前后没有普通弹幕。这种通过轨道的方式也比较好实现,每条弹幕都对应一个时间段,根据活动弹幕的时间和速度,将活动弹幕展示的前后时间,将这段时间轨道暂时关闭,只保留活动弹幕即可。
轮询
每条弹幕都对应着一个展示时间,所以需要每隔一段时间就找一下有没有需要展示的弹幕。我设计的方案是通过轮询,来驱动弹幕展示。
通过
CADisplayLink
来进行轮训,将frameInterval
设置为60,即每秒轮询一次。在轮询的回调中查找有没有要展示的弹幕,有的话就从上到下查找每条轨道,某条轨道有位置可以展示的话就交给这条轨道展示,如果所有轨道都有正在展示的弹幕,则将此条弹幕丢弃。是否有位置是根据屏幕最右侧,最后一条弹幕是否已经展示完全,并且后面有空余位置来决定的。对于取数据的部分,数据和视图的逻辑是分离的,相互之间并没有耦合关系。取数据时只是从一个很小的字典中,根据时间取出所用的弹幕数据,并转化为
model
。字典的数据很少,最多十秒的数据,而且这里并不会接触到读数据库的操作,也没有网络请求的逻辑,这些都是独立的逻辑,后面会讲到。弹幕视图
经常看视频的同学应该会知道,弹幕的展示形式有很多,有带明星头像的、有带点赞数的、带矩形背景色的,很多种展示形态。为了更好的对视图进行组织,所以我采用的就是很普通的
UIView
的展示形式,并没有为了性能去做很复杂的渲染操作。用
UIView
的好处主要就是方便做布局和子视图管理,但在屏幕上做动画时,是对CALayer
进行渲染的。也就是说UIView
就是用来做视图组织,并不会直接参与渲染,这也符合苹果的设计理念。复用池
弹幕是一个高频使用的控件,所以不能一直频繁创建,以及添加和移除视图,会对性能有影响。所以就像很多同学设计的模块一样,我也引入了缓存池的概念,我这里叫复用池。
弹幕复用池和
UITableView
的复用池类似,离开屏幕的弹幕会被放在复用池中等待复用,下次直接从复用池中取而不重新创建。弹幕视图做的工作就是接收新的model
对象,并根据弹幕类型进行不同的视图布局。并且弹幕只会在创建时被
addSubview
一次,当弹幕离开屏幕不会被从父视图移除,这样弹幕从复用池中取出时也不需要被addSubview
。当动画执行完成后,弹幕就直接留在动画结束的位置,下次做动画时弹幕会自动回到fromValue
的位置。实际上视图结构就如上图所示,灰色区域就是可视区域。系统弹幕
在视频刚开始时会有引导信息,比如引导用户发弹幕,或者提示弹幕有多少条,这个我们叫做系统弹幕。系统弹幕一般是展示到屏幕中间时,才开始展示后续弹幕。但是要精确的计算到弹幕到达屏幕中间,然后再展示后续弹幕,这种的采用清除前后特定时间段的弹幕就不太精确,所以我们采用的是另一套实现方案。
系统弹幕的实现是通过一个更高精度的
CADisplayLink
进行轮询检测,也就是把frameInterval
设置的更小,我这里设置的是10,也就是每秒检测六次。但是进行检测时不能直接用CALayer
进行判断,需要使用presentationLayer
也就是屏幕上正在展示的layer
进行检测,通过这个layer
获取到的frame
和屏幕上显示的才是一致的。这里简单介绍一下
CALayer
的结构,我们都知道UIView
是对CALayer
的一层封装,实际上屏幕上的显示都是通过layer
来实现的,而layer
本身也分为以下三层,并有不同的功能。presentationLayer,其本身是当前帧的一个拷贝,每次获取都是一个新的对象,和动画过程中屏幕上显示的位置是一样的。
modelLayer,表示
layer
动画完成后的真实值,如果打印一下modelLayer
和layer
的话,发现二者其实是一个对象。renderLayer,渲染帧,应用程序会根据视图层级,构成由
layer
组成的渲染树,renderLayer
就代表layer
在渲染树中的对象。
炫彩弹幕
在播放弹幕的过程中,我们可以看到有渐变颜色的弹幕,我们叫做“炫彩弹幕”。这种弹幕有一个很明显的特征,就是其颜色是渐变的。这时候要考虑性能的问题,因为播放高清视频时本身性能消耗就很大,在弹幕量比较大的情况下,会造成更多的性能消耗,所以减少性能消耗就是很重要的,渐变弹幕可能会使性能消耗加剧。
对于渐变文字,一般都是通过
mask
的方式实现,下面放一个CAGradientLayer
做渐变,上面盖一个文字的layer
。但是这种会触发离屏渲染,会导致性能下降,并不能用这种方案。经过我们的尝试,决定用设置渐变文字颜色的方式解决。CGFloat scale = [UIScreen mainScreen].scale; UIGraphicsBeginImageContextWithOptions(imageSize, NO, scale); CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSaveGState(context); CGColorSpaceRef colorSpace = CGColorGetColorSpace([[colors lastObject] CGColor]); CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (CFArrayRef)ar, NULL); CGPoint start = CGPointMake(0.0, 0.0); CGPoint end = CGPointMake(imageSize.width, 0.0); CGContextDrawLinearGradient(context, gradient, start, end, kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); CGGradientRelease(gradient); CGContextRestoreGState(context); UIGraphicsEndImageContext();
实现方式就是先开辟一个上下文,用来进行图片绘制,随后对上下文进行一个渐变的绘制,最后获取到一个
UIImage
,并将图片赋值给UILabel
的textColor
即可。从离屏检测来看,并未发生离屏渲染,fps也始终保持在一个很高的水平。
暂停和开始
弹幕是随视频播放和暂停的,所以需要对弹幕提供暂停和继续的支持,对于这块我采用的
CAMediaTiming
协议来处理,可以通过此协议对动画的过程进行控制。代码中加0.05是为了避免弹幕在暂停时导致的回跳,所以加上一个时间差。具体原因是因为通过
convertTime:fromLayer:
方法计算得到的时间,和屏幕上弹幕的位置依然存在一个微弱的时间差,而导致渲染时视图位置发生回跳,这个0.05是一个实践得来的经验值。- (void)pauseAnimation { // 增加判断条件,避免重复调用 if (self.layer.speed == 0.f) { return; } CFTimeInterval pausedTime = [self.layer convertTime:CACurrentMediaTime() fromLayer:nil]; self.layer.speed = 0.f; self.layer.timeOffset = pausedTime + 0.05f; } - (void)resumeAnimation { // 增加判断条件,避免重复调用 if (self.layer.speed == 1.f) { return; } CFTimeInterval pausedTime = self.layer.timeOffset; self.layer.speed = 1.0; self.layer.timeOffset = 0.0; self.layer.beginTime = 0.0; CFTimeInterval timeSincePause = [self.layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime; self.layer.beginTime = timeSincePause; }
CAMediaTiming
协议是用来对动画过程控制的一个协议,例如通过CoreAnimation
创建的动画,CALayer
遵守了这个协议。这样如果需要对动画进行控制的话,不需要引用一个CABasicAnimation
对象,然后再修改动画属性这种方式对动画流程进行控制,只需要直接对layer的属性进行修改即可。下面是
CAMediaTiming
协议中一些关键的属性,在上文中也用到了其中的部分属性。beginTime,动画开始时间,可以控制动画延迟展示。一般是一个绝对时间,为了保证准确性,最好先对当前
layer
进行一个转换,延迟展示在后面加对应的时间即可。duration,动画结束时间。
speed,动画执行速度,默认是1。动画最终执行时间=
duration
/speed
,也就是duration
是3秒,speed
是2,最终动画执行时间是1.5秒。timeOffset,控制动画进程,主要用来结合
speed
来对动画进行暂停和开始。repeatCount,重复执行次数,和
repeatDuration
互斥。repeatDuration,重复执行时间,如果时间不是
duration
的倍数,最后一次的动画会执行不完整。autoreverses,动画反转,在动画执行完成后,是否按照原先的过程反向执行一次。此属性会对
duration
有一个叠加效果,如果duration
是1s,autoreverses
设置为YES
后时间就是2s。fillMode,如果想要动画在开始时,就停留在
fromValue
的位置,就可以设置为kCAFillModeBackwards
。如果想要动画结束时停留在toValue
的位置,就设置为kCAFillModeForwards
,如果两种都要就设置为kCAFillModeBoth
,默认是kCAFillModeRemoved
,即动画结束后移除。
+
发送弹幕
插入弹幕
现在弹幕一般都会结合剧中主角,以及各种文字颜色让你去选择,通过这些功能也可以带来一部分付费用户。当发送一条弹幕时,会从上到下查找轨道,查找轨道时是通过
presentationLayer
来进行frame
的判断,如果layer
的最右边不在屏幕外,并且距离右侧屏幕还有一定空隙,项目中写的是10pt,则表示有空位可以插入下一条弹幕,这条弹幕会被放在这条轨道上。如果当前轨道没有空位置,则从上到下逐条查找轨道,直到找到有空位的轨道。如果当前屏幕上弹幕较多,所有轨道都没有空位,则这一条弹幕会被抛弃。
如果是自己发的弹幕,这个是必须要展示出来的,因为用户发的弹幕要在界面上给用户一个反馈。对于自己发的弹幕,会有一个插队操作,优先级比其他弹幕都要高。自己发的弹幕并不入本地数据库,只是进行一个网络请求传给服务器,以及在界面上进行展示。
选择角色
在上面的图片中可以看到,文本之前会有角色和角色名,这些都是独立于输入文字之外的。用户如果删除完输入的文字之后,再点击删除要把角色也一起删除掉。输入框页面构成是一个
UITextField
,左边的角色头像和角色名是一个自定义View
,被当做textField
的leftView
来展示。如果删除的话就是将leftView
置nil
即可。问题在于,如果使用
UIControlEventEditingChanged
的事件,只能获取到文本发生变化时的内容,如果输入框的文字已经被删完,而角色是一个leftView
,但由于文本已经为空,则无法再获取到删除事件,也就不能把角色删除掉。对于这个问题,我们找到了下面的协议来实现。
UITextField
遵守UITextInput
协议,但UITextInput
协议继承自UIKeyInput
协议,所以也就拥有下面两个方法。下面两个方法分别在插入文字,以及点击删除按钮时调用,即使文本已经为空,依然可以收到deleteBackward
的回调。在这个回调里就可以判断文本是否为空,如果为空则删除角色即可。@protocol UIKeyInput <UITextInputTraits> @property(nonatomic, readonly) BOOL hasText; - (void)insertText:(NSString *)text; - (void)deleteBackward; @end
+
弹幕设置
参数调整
弹幕一般都不是一种形态,很多参数都是可以调整的,对于
iPhone
和iPad
两个平台参数还不一样,调整范围也不一样。这些参数肯定是不能放在业务代码里进行判断的,这样各种判断条件散落在项目中,会导致代码耦合严重。对于这个问题,我们的实现方式是通过
BarrageConfig
来区分不同平台,将两个平台的数值差异都放在这个类中。业务部分直接读取属性即可,不需要做任何判断,包括退出进程的持久化也在内部完成,这样就可以让业务部分使用无感知,也保证了各个类中的数值统一。当有任何参数的改动,都可以对
BarrageConfig
进行修改,然后调用Render
的layoutBarrageSubviews
进行渲染即可。因为调整参数之后,屏幕上已经显示的弹幕也需要跟着变,而且变得过程中还是在动画执行过程中,动画执行不能断掉,所以对动画的处理就很重要。这部分处理起来比较复杂,就不详细讲了。点赞
弹幕还会有点赞和长按的功能,点赞一般是点击屏幕然后出现一个选择视图,点击点赞后有一个动画效果。长按就是选中一个弹幕,识别到手势长按之后,右侧出现一个举报页面。
这两个手势我用
tap
和longPress
两个手势来处理,并给longPress
设置了一个0.2s的识别时间,将这两种手势的识别交给系统去做,这样也比较省事。这两个手势都加到
Render
上,而不是每个弹幕视图对应一个手势,这样管理起来也比较简单。这样在手势识别时,就需要先找到手势触摸点,再根据触摸点查找对应的弹幕视图,查找的时候依然通过presentationLayer
来查找区域,而不能用视图做查找。- (void)singleTapHandle:(UITapGestureRecognizer *)tapGestureRecognizer { CGPoint touchLocation = [tapGestureRecognizer locationInView:self.barrageRender]; __block BOOL barrageLiked = NO; weakifyself; [self enumerateObjectsUsingBlock:^(SVBarrageItemLabelView *itemLabel, NSUInteger index, BOOL *stop) { strongifyself; if ([itemLabel.layer.presentationLayer hitTest:touchLocation] && barrageLiked == NO) { barrageLiked = YES; [self likeAction:itemLabel withTouchLocation:touchLocation]; *stop = YES; } }]; }
+
弹幕广告
广告
对于这么好的一个展示位置,广告部门必然不会放过。在视频播放过程中,会根据金主爸爸投放要求,在指定的时间展示一个广告弹幕,并且这个弹幕的形态还是不固定的。也就是说大小、动画形式都不能确定,而且这条弹幕还要在最上层展示。
对于这个问题,我们采用的方案是,给广告专门留了一个视图,视图层级高于
Render
,在初始化广告SDK的时候传给SDK,这样就把广告弹幕的控制交给SDK,我们不做处理。图层管理
播放器上存在很多图层,播控、弹幕
Render
、广告之类的,看得到的和看不到的有很多。对于这个问题,播放器创建了一个继承自NSObject
的视图管理器,这个视图管理器可以对视图进行分层管理。播放器上的视图,都需要调用指定的方法,将自己加到对应的图层上,移除也需要调用对应的方法。当需要调整前后顺序时,修改定义的枚举即可。
+
数据分离
前面一直说的都是视图的部分,没有涉及数据的部分,这是因为UI和数据其实是解耦和的,二者并没有强耦合,所以可以单独拿出来讲。数据部分的设计,类似于播放器的
local server
方案,将请求数据到本地,和从本地读取数据做了一个拆分。请求数据
弹幕数据量比较大,肯定是不能一次都请求下来的,这样很容易造成请求失败的情况。所以这块采取的是五分钟一个分片数据,在当前的五分钟弹幕快播完的前十秒,开始请求下一个时间段的弹幕。如果拖动进度条,则拖动完成后开始请求新位置的弹幕。在每次请求前都会查一下库,数据是否已存在。
请求数据由业务部分驱动,请求数据后并不会直接拿来使用,而是存入本地数据库,这部分比较像服务器往本地写
ts
分片的操作。数据库存储的部分,推荐使用WCDB
,弹幕这块主要都是批量数据处理,而WCDB
对于批量数据的处理,性能高于FMDB
。取数据
取数据同样由业务层驱动,为了减少频繁进行数据库读写,每隔十秒钟进行一次数据库批量读取,并转换为
model
返回给上层。弹幕模块在内存中维护了一个字典,字典以时间为key
,数组为value
,因为同一时间可能会有多条弹幕。从数据库批量获取的数据会被保存到字典中,上层业务层在使用数据时,都是通过字典来获取数据,这样也实现了数据层和业务层的一个解耦和。上层业务层每隔一秒从字典中读取一次数据,并通过数据找到合适的轨道,将数据传给合适的轨道来处理。
+
弹幕防挡探索
现在很多视频网站都上线了弹幕防遮挡方案,对于视频中的人物,弹幕会在其下方展示,而不会遮挡住人物。还有的应用针对弹幕遮挡进行了新的探索,即成为付费会员后,可以选择只有自己喜欢的爱豆不被遮挡,其他人依然被遮挡。
语义分割
根据业务场景我们分析,首先需要把人像部分分割出来,获取到人像的位置之后才能做后续的操作。所以人像分割的部分采取语义分割的方式实现,提前对视频关键帧进行标注,这个工作量是很庞大的,所以需要一个专门的标注团队去完成。根据标注后的模型,通过机器学习的方式,让计算机可以准确的识别出人的位置,并导出多边形路径。
这里面还涉及一个问题,就是近景识别和远景识别的问题,机器进行识别时只需要识别近景人物,远景人物并不需要进行识别,否则弹幕展示效果会受到很大影响。语义分割可以通过Google的
Mask_RCNN
来实现。客户端实现方案
客户端的实现方案是通过人像的多边形路径,对原视频抠出人像并导出一个新的视频。在播放的时候实际上是前后两个播放器在播放,弹幕夹在两个播放器中间来实现的。并且前面的人像层需要做边缘虚化,让弹幕的过渡显得自然些,否则会太突兀。
这种方案的过渡效果会好一些。因为对每一帧视频进行切割的时候,每一帧并不能保证相邻帧切割的边缘相差都不大,也就是相邻近的帧边缘不能保证很好的衔接,这样就容易出现视频连续性的问题。前后两个播放器叠加的方案,两个层的视频内容实际上是衔接很紧密的,把弹幕层去掉你根本看不出来这是两层播放器,所以连续性的问题就不明显了。
前端实现方案
前端的实现方案是服务端将多边形路径放在一个svg文件中,并将文件下发给前端,前端通过
css
的mask‑image
遮罩实现的。通过遮罩把人像部分抠出来,人像之外依然是黑色区域,黑色是可显示区域,和iOS的mask
属性类似。B站是最开始做弹幕防挡的,现在B站已经不局限于真人弹幕防挡了,现在很多番剧中的动漫人物也支持弹幕防挡。可以看下面的视频感受一下。
B站番剧弹幕防挡视频链接:https://www.bilibili.com/video/BV1Db411C7hJ
-
Dplayer弹幕的获取和提交
2019-09-28 17:40:37vue项目中播放M3u8格式的视频,最终选择了...因为Dplayer 文档中有默认的弹幕库,你也可以自己搭建弹幕服务器,貌似是node 环境,Dplayer中有配置参数,可以提交代码和获取代码,大致是 danmaku: { id: '9E2...vue项目中播放M3u8格式的视频,最终选择了Dplayer这款播放器,DPlayer是一个可爱的HTML5 弹幕视频播放器,可以帮助人们轻松地构建视频和弹幕。
因为Dplayer 文档中有默认的弹幕库,你也可以自己搭建弹幕服务器,貌似是node 环境,Dplayer中有配置参数,可以提交代码和获取代码,大致是
danmaku: { id: '9E2E3368B56CDBB4', //视频的id api: 'https://api.prprpr.me/dplayer/', //dplayer中的弹幕接口 token: 'tokendemo', //这里是做token认证的, maximum: 1000, //弹幕获取的数量 addition: ['https://api.prprpr.me/dplayer/v3/bilibili?aid=4157142'], user: 'DIYgod', //弹幕作者 bottom: '15%', // 距离头部的距离 unlimited: true, },
因为dplayer的弹幕默认在接口后面添加了 /v3,比如说项目的接口为https://xxx/xxx/xxx?id=1,但是经过dplayer发送的请求就成了https://xxx/xxx/xxx/v3?id=1,这样我们根本就无法获取到数据了。
解决方案
- 1 首先,vue项目中不要用 npm install dplayer
- 2 把dplayer下载到项目中,形成一个dplayer的文件
- 3 修改dplayer中的DPlayer.min.js
把 文件内所有带/v3 的代码统统删除,
ok—大功告成
- 3 修改dplayer中的DPlayer.min.js
-
实时弹幕系统的设计与实现
2018-12-13 14:44:27看新年晚会的时候,发现最大的乐趣就是微信上墙了,但是量大了要等好久才能看见自己发的,为什么不能是弹幕的形式呢? 发现在GitHub上开源了一个JS弹幕模块核心CommentCoreLibrary,慢慢开始学习Node.js的一套。原来...前言:原文2014年发布在CNode社区,现在同步一下
看新年晚会的时候,发现最大的乐趣就是微信上墙了,但是量大了要等好久才能看见自己发的,为什么不能是弹幕的形式呢?
发现在GitHub上开源了一个JS弹幕模块核心CommentCoreLibrary,慢慢开始学习Node.js的一套。原来是比较做后台开发的,也是第一次做这样的分享,请大家多多指教啦……一、Express
Express是Node.js最流行的一款web框架,小而灵活。Node.js和npm的安装配置可以参考这里。
可以通过npm安装Express(参考),也可以使用Express application generator快速产生一个Express样例(参考)。
对于Express初学者,用Express application generator生成样例更有利于快速上手。因此就以此为例:
# install Express application generator $ npm install express-generator -g # create an Express app named danmaku $ express danmaku # install dependencies $ cd danmaku $ npm install # run the app on Windows $ set DEBUG=danmaku & node .\bin\www # or $ npm start
关于
set DEBUG=danmaku
可以见此文。
可以用npm start
启动服务器是因为在packege.json中有了这么一段:"scripts": { "start": "node ./bin/www" }
Express的4比之3,把服务器配置和服务器启动做了分离,原来都在app.js里,现在将启动代码放到了www中。
现在,浏览一遍这个Express样例,对这框架就可以知道个大概了。- bin:存放启动项目的脚本文件
- node_modules:存放所有的项目依赖库
- public:静态文件(css、js、img等)
- routes:路由文件(MVC中的C,controller)
- views:页面文件(jade或ejs模板)
- package.json:项目依赖配置及开发者信息
- app.js:应用核心配置文件
更多参考:
二、路由
将实时弹幕系统实际上是分为三个角色:
- 服务端:监听客户端连接、弹幕事件等并响应。
- 发射客户端:由用户发射弹幕。以emitCtrl.js作为emit页面的controller。
- 屏幕客户端:接收弹幕并显示。以indexCtrl.js作为index页面的controller。
添加 routes/indexCtrl.js
var express = require('express'); var router = express.Router(); /* GET home page. */ router.get('/', function(req, res, next) { res.render('index',{title:"danmaku"}); }); module.exports = router;
添加 routes/emitCtrl.js
var express = require('express'); var router = express.Router(); /* GET emit page. */ router.get('/', function(req, res, next) { res.render('emit'); }); module.exports = router;
修改 app.js
var indexCtrl = require('./routes/indexCtrl'); var emitCtrl = require('./routes/emitCtrl'); ... app.use('/', indexCtrl); app.use('/emit', emitCtrl);
启动后可查看到index页面。
在后面还会对emitCtrl.js增加弹幕配置的文件config.json的读取。
三、屏幕客户端
1. 静态
CommentCoreLibrary是GitHub上开源的JS弹幕模块核心,提供从基本骨架到高级弹幕的支持。
考虑到实际,感觉并不应该引入外部库。如果作为外部库用,需要$ npm install comment-core-library --save
使用时(去除public)
<link rel="stylesheet" href="/node_modules/comment-core-library/build/style.css" /> <script src="/node_modules/comment-core-library/build/CommentCoreLibrary.js"></script>
另外CommentCoreLibrary模块也有点笨重。
所以换种方式,将CommentCoreLibrary.js放入public/javascripts,style.css放入public/stylesheets中。
添加views/index.jade
doctype html html head title= title link(rel='stylesheet', href='/stylesheets/style.css') link(rel='stylesheet', href='/stylesheets/index.css') script(src='/javascripts/CommentCoreLibrary.js') body #my-player.abp(style='width:100%; height:600px; background:#000;') #my-comment-stage.container ul#messages script(src='http://cdn.bootcss.com/jquery/2.1.3/jquery.js') script(src='/javascripts/index.js')
添加public/stylesheets/index.css
* { margin: 0; padding: 0; box-sizing: border-box; } #messages { list-style-type: none; margin: 0; padding: 0; } #messages li { padding: 5px 10px; } #messages li:nth-child(odd) { background: #eee; } body { margin:0px; padding:0px; font-family: "Segoe UI", "Microsoft Yahei", sans-serif; }
添加public/javascripts/index.js
window.addEventListener('load', function () { // 在窗体载入完毕后再绑定 var CM = new CommentManager($('#my-comment-stage')); CM.init(); // 先启用弹幕播放(之后可以停止) CM.start(); // 开放 CM 对象到全局这样就可以在 console 终端里操控 window.CM = CM; });
然后在Console里怒射一弹:
var danmaku = { "mode": 1, "text": "hello world", "stime": 0, "size": 25, "color": 0xff00ff, "dur": 10000 }; CM.send(danmaku);
不过这其实根本没用上服务器,也就是静态网页一样的效果。
2. 动态(服务端)
动态是实现一个真正的“屏幕客户端”,监听等待“显示弹幕”的事件,并实时显示。
在CommentCoreLibrary的Doc中有一段:
实时弹幕也需要后端服务器的支持。实时弹幕可以采取Polling(定时读取)或者 Push Notify(监听等待)两个主动和被动模式实现。
WebSocket
是HTML5开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。既然是双向通信,就意味着服务器端和客户端可以同时发送并响应请求,而不再像HTTP的请求和响应。WebSocket通信协议于2011年被IETF定为标准RFC 6455,WebSocketAPI被W3C定为标准。知乎上关于WebSocket的科普。Socket.IO
是一个开源的WebSocket库,它通过Node.js实现WebSocket服务端,同时也提供客户端JS库。Socket.IO支持以事件为基础的实时双向通讯,它可以工作在任何平台、浏览器或移动设备。
Socket.IO支持4种协议:WebSocket、htmlfile、xhr-polling、jsonp-polling,它会自动根据浏览器选择适合的通讯方式,从而让开发者可以聚焦到功能的实现而不是平台的兼容性,同时具有不错的稳定性和性能。
p.s. 实际过程中踩到了phpwebsocket的坑。npm install socket.io --save
修改www(Express4从app.js里把启动分出来了)
// Create socket.io var io = require('socket.io')(server); ... // Wait for socket event io.on('connection', function(socket){ console.log('a user connected'); socket.on('disconnect', function(){ console.log('user disconnected'); }); socket.on('danmaku send', function(msg){ console.log('message: ' + msg); io.emit('danmaku show', msg); }); });
修改index.jade
script(src='http://cdn.bootcss.com/socket.io/1.3.2/socket.io.js') script(src='http://cdn.bootcss.com/jquery/2.1.3/jquery.js') script(src='/javascripts/index.js')
修改index.js
window.addEventListener('load', function () { // 在窗体载入完毕后再绑定 var CM = new CommentManager($('#my-comment-stage')); CM.init(); // 先启用弹幕播放(之后可以停止) CM.start(); // 开放 CM 对象到全局这样就可以在 console 终端里操控 window.CM = CM; var socket = io(); socket.on('danmaku show', function (msg) { console.log(msg); $('#messages').append($('<li>').text(msg)); var danmaku = JSON.parse(msg); CM.send(danmaku); }); });
这样就由服务端监听了“connection”、“disconnect”和“danmaku send”三个事件,特别是在收到“danmaku send”时会发送“danmaku show”事件。而屏幕客户端监听“danmaku show”事件,并把传递来的弹幕显示出来。
启动后打开index,确实能看到"connection"事件执行的提示。
四、发射客户端
发射客户端发送“danmaku send”事件及弹幕给服务端。
除此之外,在CommentCoreLibrary里可以对弹幕属性进行设置,比如文字大小、模式、颜色,将它们的可选值写成配置文件,并设定默认值。
添加public/jsons/config.json
{"sizes":[{"size":12,"title":"非常小"},{"size":16,"title":"较小"},{"size":18,"title":"小"},{"size":25,"title":"中"},{"size":36,"title":"大"},{"size":45,"title":"较大"},{"size":64,"title":"非常大"}], "modes":[{"mode":1,"title":"顶端滚动"},{"mode":2,"title":"底端滚动"},{"mode":5,"title":"顶端渐隐"},{"mode":4,"title":"底端渐隐"},{"mode":6,"title":"逆向滚动"}], "colors":[{"color":"000000","title":"黑色"},{"color":"C0C0C0","title":"灰色"},{"color":"ffffff","title":"白色"},{"color":"ff0000","title":"红色"},{"color":"00ff00","title":"绿色"},{"color":"0000ff","title":"蓝色"},{"color":"ffff00","title":"黄色"},{"color":"00ffff","title":"墨绿"},{"color":"ff00ff","title":"洋红"}], "inits":{"size":3,"mode":0,"color":4}}
修改emitCtrl.js,读取配置
var fs = require('fs'); ... /* GET emit page. */ router.get('/', function (req, res, next) { var config = JSON.parse(fs.readFileSync(__dirname + './../public/jsons/config.json')); res.render('emit', { title: 'Emitter', sizes: config.sizes, modes: config.modes, colors: config.colors, inits: config.inits}); });
添加views/emit.jade
doctype html html head title= title meta(name='viewport', content='width=device-width, initial-scale=1,maximum-scale=1') link(rel='stylesheet',href='http://cdn.bootcss.com/jquery-mobile/1.4.3/jquery.mobile.css') script(src='http://cdn.bootcss.com/socket.io/1.3.2/socket.io.js') script(src='http://cdn.bootcss.com/jquery/2.1.3/jquery.min.js') script(src='http://cdn.bootcss.com/jquery-mobile/1.4.3/jquery.mobile.js') body div(data-role='page') div(data-role='content') div.ui-grid-b a#size.ui-btn.ui-btn-inline.ui-block-a(href='#popupMenu_font', data-rel='popup', data-transition='pop',data-position-to="window",danmaku-size= sizes[inits.size].size )= sizes[inits.size].title #popupMenu_font(data-role='popup', data-theme='b',data-overlay-theme='b', style='min-width:210px;') ul(data-role='listview') each val, index in sizes li a(data-rel='back',danmaku-size=val.size)= val.title a#mode.ui-btn.ui-btn-inline.ui-block-b(href='#popupMenu_mode', data-rel='popup', data-transition='pop',data-position-to="window",danmaku-mode= modes[inits.mode].mode )= modes[inits.mode].title #popupMenu_mode(data-role='popup', data-theme='b',data-overlay-theme='b', style='min-width:210px;') ul(data-role='listview') each val, index in modes li a(data-rel='back',danmaku-mode=val.mode)= val.title a#color.ui-btn.ui-btn-inline.ui-block-c(href='#popupMenu_color', data-rel='popup', data-transition='pop',data-position-to="window",danmaku-color= colors[inits.color].color )= colors[inits.color].title #popupMenu_color(data-role='popup', data-theme='b',data-overlay-theme='b', style='min-width:210px;') .ui-grid-b - var i=0; each val, index in colors case i++%3 when 0: a.ui-block-a(data-rel="back", style='background-color:#'+val.color+';min-height:60px;line-height:60px;text-align:center',danmaku-color=val.color)= val.title when 1: a.ui-block-b(data-rel="back", style='background-color:#'+val.color+';min-height:60px;line-height:60px;text-align:center',danmaku-color=val.color)= val.title when 2: a.ui-block-c(data-rel="back", style='background-color:#'+val.color+';min-height:60px;line-height:60px;text-align:center',danmaku-color=val.color)= val.title textarea#msg(placeholder='来一发弹幕~') button#btnSend 发射 script(src='/javascripts/emit.js')
添加public/javascripts/emit.js
var socket = io(); $('#popupMenu_font a').click(function(e){ $('#size').text($(e.target).text()).attr("danmaku-size",$(e.target).attr("danmaku-size")); }); $('#popupMenu_mode a').click(function(e){ $('#mode').text($(e.target).text()).attr("danmaku-mode",$(e.target).attr("danmaku-mode")); }); $('#popupMenu_color a').click(function(e){ $('#color').text($(e.target).text()).attr("danmaku-color",$(e.target).attr("danmaku-color")); }); $('#btnSend').click(function(e){ e.preventDefault(); var danmaku = { "mode": Number($("#mode").attr("danmaku-mode")), "text": $('#msg').val(), "stime":0, "size": Number($("#size").attr("danmaku-size")), "color":parseInt($("#color").attr("danmaku-color"),16), "dur":10000 }; var msg=JSON.stringify(danmaku); console.log(msg); socket.emit('danmaku send',msg); $('#msg').val(""); });
最后整个效果就是这样啦~ 源码在此:https://github.com/cstackess/danmaku
-
基于HTML5的有弹幕功能的视频播放器
2021-06-11 10:59:52接下来,在body中需要放置播放器的位置加入如下代码:最后,关键的部分,配置参数,调用插件。$("#danmup").DanmuPlayer({src: "abc.mp4", //视频源height: "480px", //区域的高度width: "800px", //区域的宽度... -
《用python 玩转数据》项目——B站弹幕数据分析
2020-12-11 02:01:371. 背景在视频网站上,一边看视频一边发弹幕已经是网友的习惯。在B站上有很多种类的视频,也聚集了各种爱好的网友。本项目,就是对B站弹幕数据进行分析。选取分析的对象是B站上点播量过1.4亿的一部剧《Re:从零开始的... -
用jQuery制作视频弹幕
2019-01-31 18:50:40在播放视频时显示弹幕,自己发布的评论也可显示在弹幕上。 -
用java爬取斗鱼弹幕
2020-07-06 19:13:05将建立弹幕的数据库表 .将信息写入数据库 一、连接websocket 斗鱼弹幕推送是通过websocket进行的消息推送。 Websocket简介:WebSocket是HTML5开始提供的一种浏览器与服务器间进行全双工通讯的网络技术。依靠这种技术... -
点播直播弹幕实现1
2021-10-17 16:39:29点播弹幕实现 后端仓库 简要说明 之前突然觉得弹幕视频还挺有意思的,就想自己也实现点播弹幕和直播弹幕。由于是主学后端的,对于自己手动实现前端的弹幕功能感到困难,于是上网搜索有没有相关实现,首先找到了** ... -
如何从数据库中筛选出达成指定里程碑节点的项目_Shopee 的分布式数据库实践之路...
2020-10-23 15:13:18今年也有一些新功能选择直接基于 TiDB 做开发,比如店铺标签、直播弹幕和选品服务等。这些新模块的数据量和查询量都还比较小,有待持续观察验证。 TiDB 3.0 GA 后,新的 Titan (https://github.com/tikv/titan) 存储... -
抓取斗鱼直播弹幕
2018-03-05 15:33:53原文地址:http://blog.csdn.net/bfboys/article/details/52853041前几...→抓取数据的基本思路就是: 抓包 → 分析请求信息 → 模拟发送请求 → 获得数据知乎回答地址:如何获取斗鱼直播间的弹幕信息? - Brucezz ... -
网络爬虫实战(四):爬取腾讯视频电视剧弹幕-Go语言中文社区
2020-12-05 05:15:59而在他的多部小说中,翻拍次数最多的无疑就是《倚天屠龙记》了,而且次数已经高达十四次。最早的是1963香港导演将这个小说以电影形式拍成了上下两集。虽然是第一次,但不得不说导演也是非常的有新意,和金庸多次沟通... -
假如让你从 0 到 1 实现一个直播弹幕系统
2020-10-03 10:33:00斗鱼:如何打造一个高性能、高可用直播系统架构 蘑菇街直播架构 直播相关知识之一-基本架构 直播中弹幕是一个非常亮眼和重要的功能,相比于秒杀架构,直播弹幕系统也有很多有趣的知识可以挖掘,一起来 YY 下如何... -
哔哩哔哩弹幕处理+情感倾向分析
2021-01-27 20:31:031.首先获取某一视频的弹幕 用此方法获取B站视频的XML弹幕 2.弹幕解析部分 将xml弹幕解析,并写入csv文件 代码部分: import xml.etree.ElementTree as ET import csv import time tree = ET.ElementTree(file='Test... -
goeasy+jquery+ckplayer实现动态实时视频弹幕
2017-05-22 12:07:05- 由于是实时弹幕,所以需要时时刻刻读取数据库内容,可是大量的ajax请求会影响服务器性能。于是就使用了goeasy第三方实时消息推送框架。 - 弹幕的动态化可以使用jquery的animate动画来实现.弹幕的位置和颜色,随便... -
在网站上的视频直播添加弹幕做法
2020-09-25 16:02:09弹幕使用的是阿里云的Aliplayer 参考官网:https://help.aliyun.com/document_detail/125570.html?spm=a2c4g.11186623.6.1099.16b824654fRSp6 ... -
Dplayer Html5 弹幕视频播放器的实现
2019-04-26 14:29:34本弹幕视频,通过 Html5 Dplayer实现。 Dplayer官网:http://dplayer.js.org/#/ Dplayer文档:http://dplayer.js.org/#/zh-Hans/ Dplayer GitHub:https://github.com/MoePlayer/DPlayer 先看效果图 一、... -
基于Bilibili热门视频Top100弹幕的数据爬取与分析(报告版)
2020-05-18 05:28:08能够在观看视频的过程中发表自己的评论,并且评论可以在你所希望的时间点、位置以滑行或停留的方式出现在视频中,所有观看视频的人都可以看见评论,这样一类的评论叫做弹幕,此类网站叫弹幕网站。 弹幕视频系统源自... -
bilibili弹幕爬取与比对分析
2019-10-03 01:41:17给定up主uid和用户uid,爬取用户在该up主所有视频中发的所有弹幕 需求拆解 获取up主所有视频 打开b站,随便搜索一个up主,打开所有视频页面,f12看异步请求就一目了然了 接口地址:https://space.bilibili.... -
java 视频播放 弹幕技术 视频弹幕 视频截图 springmvc mybatis SSM
2017-12-16 18:42:00A代码编辑器,在线模版编辑,仿开发工具编辑器,pdf在线预览,文件转换...带页面、建表sql脚本,处理类,service等完整模块C 集成阿里巴巴数据库连接池druid数据库连接池阿里巴巴的 druid。Druid在监控、可扩展性、... -
pandas数据分析和pyecharts可视化周杰伦MV弹幕(多图长文)
2021-04-14 21:29:57基准时间为 1970-1-1 08:00:00 弹幕池 0普通池 1字幕池 2特殊池【目前特殊池为高级弹幕专用】 发送者ID 发送者的ID,用于“屏蔽此弹幕的发送者”功能 弹幕ID 弹幕在弹幕数据库中rowID 用于“历史弹幕”功能。... -
企鹅弹幕提取并制作关键词云3
2021-03-18 01:13:231.1腾讯视频弹幕提取并制作关键词云1:弹幕在哪里1.2腾讯视频弹幕提取并制作关键词云2:scrapy弹幕的获取1.3企鹅弹幕提取并制作关键词云3:自动爬取全集+sql数据库写入1.3 scrapy爬取弹幕爬取弹幕的部分在这一小节... -
Mysql数据库查询超时,这样优化快速解决问题
2021-11-18 01:06:58◆问题发现期初在七月份时,经常发现有几个定时任务报错,查看了下异常原因,大概定位是数据库执行异常◆查找原因1 和 DBA 排查 mycat(公司使用 mycat ) 和 mysql 的... -
Html5弹幕视频播放器插件
2021-06-19 01:54:52接下来,在body中需要放置播放器的位置加入如下代码:最后,关键的部分,配置参数,调用插件。$("#danmup").DanmuPlayer({src: "abc.mp4", //视频源height: "480px", //区域的高度 width: "800px", //区域的宽度 ... -
基于websocket技术的网页弹幕实现
2017-02-17 17:35:541、打开弹幕,从数据库里读取历史弹幕; 2、一个客户端发送弹幕,所有的客户端均可以看到。 具体实现: 网页端: index.jsp "java" import="java.util.*" pageEncoding="UTF-8"%> String path =