Chrome 拓展踩坑记

先啰嗦几句

话说哥们儿最近新写了个 Chrome 拓展,有一个听起来很装逼的名字,叫“前方高能”。这个拓展主要是用来在 B 站视频的下方显示一个实时弹幕密度的小图表,现在更到了 0.2 的版本,要是感兴趣的话可以去 Github 看看,如果能顺便来 Star 一发的话,就是坠吼的。

下面的内容也就是跟这个拓展有关的。如题目所说,这是我在写这个拓展踩过的一些坑。因为我自己在开发这个拓展的过程中,遇到一些问题的时候几乎找不到中文资料参考,甚至有时候连英文资料都找不到。所以我觉得我确实有写一点东西的必要了。我这里使用的 Chrome 浏览器版本是 52.0.2743.116。

(顺便一提,Chrome 拓展上架要给谷歌爸爸交 5 刀的保护费,虽然钱确实不多,但是蛋疼在需要用信用卡交,还特么不能用国内的。然而本人一介穷屌哪来的信用卡,求老司机支招如何有办法不用信用卡搞这个。)

想法

说起开发这个拓展的想法,无他,只是前两天突然脑洞一开,就自己想“如果能把 B 站的弹幕密度实时统计出来,应该会很有趣”。有了这个想法,自然就开始行动了。只不过这玩意儿刚开始还不是拓展,最初甚至连所有的功能都是写在一个函数里面的,然后就用了一个本地特别原始的小服务器挂起来,用 AddJS 注入到页面里,然后用了大约两个小时写成了一个雏形。当时因为没有双向同步的进度条,图表都是和视频进度条同宽的。

“前方高能”最初的样子

写完了之后我自己去玩了一下,发现还真挺好玩,于是便萌生了做一个拓展的想法,让更多的人能体验到这么好玩的东西。

开始

早就听说 Chrome 的拓展开发就是用的咱们最熟悉的 HTML + CSS + JavaScript 三大件,这给了我极大的信心。于是我立马就去 Bing 找文档,找到了一篇貌似是360 平台的拓展开发文档的东西。毕竟 360 浏览器也是用 Webkit 内核的东西,而且看地下说明是 360 工程师直接翻译了 Chromium 的文档挂在上面,于是本着不折腾自己的原则,我就姑且拿这个作为我接下来开发的参考。

首先看了前面一小段,我大概了解到了 Chrome 拓展的文件分布。文档原文是这样说的:

每个应用(扩展)都应该包含下面的文件:

  • 一个manifest文件
  • 一个或多个html文件(除非这个应用是一个皮肤)
  • 可选的一个或多个javascript文件
  • 可选的任何需要的其他文件,例如图片

不难看出,chrome 拓展的文件结构应该和普通的网页以及 Web App 别无二致,只是多了一个名叫 manifest.json 的文件,相当于对整个 Web App 的一个配置。这里在官方文档里面写得很详细,我在这里就不展开讲了。直到 0.2 版,我这个拓展的文件目录是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
chrome
│ bg.html
│ icon_128.png
│ icon_19.png
│ manifest.json
│ popup.html

├─js
│ ├─bg
│ │ XOrigin.js
│ │
│ ├─bilibili
│ │ constVar.js
│ │ mainFunc.js
│ │
│ └─common
│ commonConstVar.js
│ commonFunc.js

└─third-party
└─js
echarts.custom.js
zepto.min.js

如你所见,虽然这个功能并不算复杂,但是我还是拆分出了不少的文件,并且分门别类把它们放好,我甚至把我用到所有不变的常量都放在了单独的文件里。这样做的好处在于,如果我以后要添加针对更多视频网站的支持,以及对视频网站 API 的变化做出的适应性变更,以及如果我以后要将这些常量做成用户可配置的选项,这样及时且且充分的模块化会让我在很大程度上少做很多无用的工作,从而能够比较从容地面对变化。

有了开始的配置文件,接下来我们就可以正式地开工了。因为之前我已经写了一个雏形,所以实现最基本的功能(也就是在播放器的下面显示一个图表)并不困难,直接将写好的脚本注入相应的页面即可,仔细看看文档的话应该很容易实现。不过还是有几点值得一说。

获取弹幕数据

因为之前研读过哔哩哔哩助手的代码,大概知道每个视频的弹幕大致是以一个 cid 来标记,然后通过一个 http://comment.bilibili.com/{$cid}.xml 的地址来获取的。现在关键是如何来获取这样的一个 cid 。看了几种播放器页面的源码以后我发现,不同的播放器对于 cid 的处理都是作为一个参数传进播放器的。但是 B 站各种播放器传参方式不尽相同。但是我却发现了一个共同之处,在传参的时候必然会出现一个 cid=23333&aid=33333 这样的字符串。所以我自然想到了使用正则表达式来摆平这件事,像这样:

1
2
// 项目中用到了Zepto,以下会出现 jQuery 的写法,下同
cid = $('body').html().match(/cid=\d*/)[0].slice(4);

在写出这个以后,我本因自己奇大无比的脑洞而折服。然而我眉头一皱,发现事情貌似并没有想象中那么简单。万一哪个 UP 主某一天闲得蛋疼,在个人说明里面写一个诸如 cid=23333 之类的东西,那我这种写法必然会爆炸。在思考了三十秒种之后,我又换了一种写法:

1
2
// $player 是播放器的 jQuery 对象
cid = $player.parent().html().match(/cid=\d*/)[0].slice(4);

这样的话,即便以后 B 站以后换 HTML5 播放器,而且有人在视频说明或评论里捣乱,我觉得在获取 cid 方面,我也不用再重新适配了。

更新:后来 B 站新推出了“番剧”页面,于是通过这样的手段来获取 cid 便显得困难。但后来经过研究,发现 B 站其实有一个存在一个接口(http://api.bilibili.com/view),可以通过视频的 AV 号和可以用来获取视频的信息,其中就包括视频的 cid ,于是这样我们就不需要像上面那样提取 cid 了,直接提取到 AV 号(普通页面通过解析页面的 url,番剧页面通过解析 url 得到番剧编号,然后用另一个接口(http://bangumi.bilibili.com/web_api/episode/)获取到 AV 号之后再获取 cid),这样的话,在番剧页面跳集不刷新页面的场景,我们也只需要绑定 readystatechange 事件,重新读一遍 url 再解析渲染就可以了。

解析弹幕数据

由于弹幕是很规矩的 XML 格式,所以在解析弹幕方面并没有遇到太大困难。只是在测试的时候发现了一点差点被遗漏的东西。

当我把插件的基础功能完成之后,就开始找各种视频来测试。在这样一个视频中却发现了一个奇怪的问题,那就是图表有数据的部分只有前面很小一部分,而且时间轴显示了一个比视频长度长得多的时间(图表 x 轴的长度是)。把处理过的数据打进控制台,也是出现了和图表一样的问题。于是我就下载了弹幕的 XML 文件进行查看,发现有这么一行……

1
<d p="100000000,8,25,16777215,1408723989,2,e6f18f01,574173329">trace("test"); </d>

作为对比,这里放几条普通的弹幕数据:

1
2
3
4
5
6
<d p="2091.6560058594,1,25,9487136,1469703784,0,49d36669,2162939419">发条弹幕证明我还活着</d>
<d p="2738.2260742188,1,25,9487136,1469705065,0,49d36669,2163062241">存活</d>
<d p="3190.1789550781,1,25,9487136,1469705535,0,49d36669,2163105603">存活</d>
<d p="1554.5989990234,1,25,15138834,1469728356,0,ea71db37,2165167499">国际天团组合</d>
<d p="2418.1818847656,1,25,16777215,1469764133,0,9e4f43c4,2166449009">史密斯麻辣隔壁</d>
<d p="2551.6499023438,1,25,16777215,1469764307,0,9e4f43c4,2166464105">居然是原曲不使用</d>

原来是一条在“第 100000000 秒”的时候发射的代码弹幕。

这样我们就可以很容易地解决这个问题了。只要想办法把特殊的弹幕过滤掉即可。并且我发现每个弹幕的 p 属性的第五个值恰好标注的是弹幕类型,那么我们就只需要统计普通弹幕就可以了。

更新:后来我发现了事实上是可以通过直接发送请求的方式来发送一个任意时间点的弹幕,于是在接下来的新版中计划改进图表的加载策略,即播放器准备就绪时先读取到整个视频的长度,然后结合读取到的弹幕数据进行整理。

进度条双向绑定

本来在了解了 B 站播放器的 API 之后,结合 echarts 的点击事件监听,做出点击图表空降是个很容易的事情。但是我想做个更骚的功能,就是在图标上页搞一个跟播放器同步的进度指示,也就是所谓的双向绑定。刚开始我的方案是在图表上画一条垂直于横轴的标线,然后每隔一小段时间就更新一次进度条数据,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var timelineMove = setInterval(function() {
try {
// getPos 函数用来获取播放器进度条当前的位置
var nowTime = Math.floor(player.getPos());
} catch (e) {
// 防止因为播放器尚未加载完毕导致的 error 输出
if ((e + '').indexOf('is not a function') !== -1) {
console.log('Player not ready yet');
} else {
console.error(e);
}
}
chartOption.series.markLine.data[1].label.normal.formatter = timeNumToStr(nowTime);
chartOption.series.markLine.data[1].xAxis = Math.floor(nowTime / step);
myChart.setOption(chartOption);
}, 100);

但是这样会出现一个问题。由于图表数据一直在频繁地刷新,导致图表无法点击,从而使本来好好的空降功能变成了废物。出现问题就必须想办法解决。然后我发现事实上这个表最多只需要每秒更新一次,于是我就改变了方案,就是只有当侦测到时间轴的整秒变化才刷新图表,这样整个情况就会好很多,像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var lastTime = 0;
var timelineMove = setInterval(function() {
try {
var nowTime = Math.floor(player.getPos());
} catch (e) {
if ((e + '').indexOf('is not a function') !== -1) {
console.log('Player not ready yet');
} else {
console.error(e);
}
}
if (nowTime !== lastTime) {
chartOption.series.markLine.data[1].label.normal.formatter = timeNumToStr(nowTime);
chartOption.series.markLine.data[1].xAxis = Math.floor(nowTime / step);
myChart.setOption(chartOption);
lastTime = nowTime;
}
}, 100);

iframe 播放器的适配

正当我得意洋洋,准备去传说中的 av10492 遛一遛我的插件的时候,意外发生了……

更新:这里本来的内容是想写部分视频播放器镶嵌在与父页面不同域的iframe中,我是如何用 chrome 插件的 background page 来实现跨域 iframe 的数据传输。但是在最新版的 B 站视频页面中, B 站启用了最新的 HTML5 播放器,革掉了几乎所有 iframe 播放器(除掉极个别使用第三方播放器的视频)的命,并且显然 chrome 浏览器都是可以完美兼容 HTML5 播放器的。所以在新版的“前方高能”插件中,我并没有保留对这方面的兼容,故此部分内容在此也就不再做赘述。

赏我点钱让我去看 7777777 吧!