技术细节
弹幕任何地方是一个纯前端有简单后端的浏览器扩展(mv3),通过注入脚本的方式在网页上显示弹幕。
所有的用户数据都保存在浏览器中。由于弹幕数据量一般会大于浏览器可同步的限制,弹幕数据只保存在当前设备上。
这个扩展本质上是一个弹幕播放器,用于在任意网站上播放用户导入的弹幕文件。搜索等功能属于附加功能,用于提升用户体验。
此扩展基本只做两件事情:
- 获取弹幕
- 渲染弹幕
弹幕数据保存在扩展的IndexedDB
中,分为两种类型:本地弹幕和第三方。
即用户导入的弹幕。作为最基本的获取弹幕的方式,保证扩展可以在不联网的情况下使用。导入接受常见的xml
文件和部分json
文件。
除了本地弹幕之外,其他的获取方式均依赖第三方弹幕视频网站。
除弹弹Play外,所有第三方弹幕请求都是在用户的浏览器中发起的。
扩展通过host_permissions
权限让请求带上用户的cookie
,从而获取到登录状态下的搜索结果和弹幕。
从设计原则上来说,扩展尽可能的最小化对第三方网站的请求,只获取必要的数据,因此不提供类似批量下载弹幕的功能。
由于搜索结果和弹幕数据都是和用户登录状态相关的,所以扩展会在用户启用第三方弹幕源时检查用户的登录状态。
在启用 B站弹幕源时,扩展会先GET
https://www.bilibili.com/
以保证cookie
正常,然后再确认用户的登录状态,如果未登录则会提示登录。
腾讯视频需要的cookie
通过javascript
注入,所以无法简单通过GET
请求来获取。启用时,如果发现cookie
缺失,会要求用户手动前往腾讯视频页面获取cookie
。
渲染指在正确的网站的正确的视频上显示弹幕。
扩展安装时会请求所有网站的权限,但并不是所有网站都需要弹幕,因此需要装填配置来告诉扩展哪些网站需要弹幕。
通过白名单的方式,只有在配置中指定的网址模式才会注入脚本并显示弹幕。同时,装填配置可以用来关联网址和其他配置和规则,比如网页适配和弹幕样式(未实装)等。
装填配置中的网址模式会传递给chrome.scripting.registerContentScripts
,只有匹配的网址才会注入脚本。
弹幕是和视频绑定的,会与视频的播放/暂停同步。扩展始终只会渲染一个视频的弹幕(单个文档的情况),如果存在多个视频,就需要判断在哪个个视频上显示弹幕。
可能出现多个视频的情况有:
- 广告
- 视频预览,比如鼠标悬停时自动播放的视频
- 隐藏的视频,有些网站会使用隐藏的视频元素来实现一些功能
扩展使用简单的算法决定弹幕于哪个视频绑定,如果选择了错误的视频用户依然可以通过装填配置来指定视频元素。
渲染弹幕主要的问题是如何保证弹幕始终显示在视频上方。
考虑的最坏情况是视频全屏,一般全屏是通过Top Layer
实现的,所以弹幕也需要使用Top Layer
,这样才能保证弹幕始终显示在视频上方。
这样做会导致无法实现弹幕互动,例如鼠标悬停、点击等。由于弹幕层在最上层,会遮挡下方的元素,比如视频控件等。如果要和下方的元素交互,就需要给弹幕层设置pointer-events: none
,这样可以避免弹幕层拦截点击事件,但也导致弹幕无法接收用户互动。
iframe
Section titled “iframe”内容脚本的注入与否取决于用户定义的装填配置,配置中的网址模式会传递给chrome.scripting.registerContentScripts
,只有匹配的网址才会注入脚本。
这个方法对于大多数网站是有效的,特别是各种自架的 SPA 网站。然而后来发现有很多视频网站,尤其是民间的视频网站,会使用iframe
来嵌套视频。
那么问题来了。iframe
是一个独立的文档,有自己的地址,并且和主文档之间很难进行交互。
如果视频在iframe
中,而用户提供的是主文档的地址,那么扩展会因为被注入到主文档中而无法访问iframe
中的视频,导致无法显示弹幕。
解决这个问题有两种方法:
- 用户手动添加
iframe
的地址到装填配置中,这样扩展会被注入到iframe
中,就可以访问到视频元素了 - 将扩展注入到所有文档中,这样扩展就可以访问到所有的视频元素
第一种
v0.16.0
之前的版本采用的是第一种方法。这种方法虽然可以解决问题,但是需要用户手动添加iframe
的地址,存在技术门槛。而且这样也存在一些问题:
- 多个
iframe
套娃并且网址相似时,只靠网址模式无法精确控制具体注入到哪个iframe
中,可能会产生页面中存在多个内容脚本的而发生冲突情况 iframe
中的脚本无法自动匹配弹幕,这是因为可以用于匹配的信息一般在主文档中,而iframe
中的脚本无法访问主文档的元素
第二种
为了解决上述问题,v0.16.0
之后的版本采用的是第二种方法。
具体实现是将内容脚本拆分为两个模块:控件和弹幕播放器。控件只注入在主文档中,用于用户交互和控制,而弹幕播放器注入到所有文档中(包括主文档),用于视频检测和弹幕渲染。
也就是说,一个网页中可能存在多个弹幕播放器,但只有一个控件。控件和播放器类似于主从关系,控件会负责弹幕播放器的注入和启动,两边通过chrome.runtime.sendMessage
进行通信。
相比第一种方法,这种方法更通用也更易于使用,但也还是有一些问题:
- 视频在
iframe
中时无法使用画中画功能(documentPictureInPicture
API 限制) - 如果存在多个
iframe
,并且多个iframe
中都有视频,扩展无法判断哪个视频是用户正在观看的视频,可能会导致弹幕显示在错误的视频上
Shadow DOM
Section titled “Shadow DOM”Shadow DOM 会阻止脚本访问元素。
如果一个网站的视频元素位于 Shadow DOM 中,扩展无法直接访问到视频元素,导致无法正常工作。
目前没有遇到这个情况,如果有的话大概需要靠劫持attachShadow
等API来解决。