一个针对性缓解海外观看 B 站卡顿的 Chrome 扩展。
它不是加速器、不改路由、不碰代理。它只做两件事,都直接对应下面实测出来的根因。
非官方项目,与上海宽娱数码科技有限公司 / bilibili 无任何关联,未获其授权或认可。 「bilibili」「哔哩哔哩」为其各自所有者的商标,此处仅用于说明本扩展的适用范围。
⚠️ 当前为 v0.1.0,尚未经过长期实际使用验证。 详见「已知限制」。
- 采样 20 个视频的
playurl,20/20 都被分配到海外 CDN 节点 (upos-sz-mirrorcosov.bilivideo.comICMP 60ms、upos-hz-mirrorakam.akamaized.net6ms)。 没有被丢到国内 PCDN。作为对照,通用的upos-sz-mirrorcos(无ov)解析出的全是国内 IP,RTT 215–256ms。 - 链路容量充足:16MB 连续下载 31 Mbps,命中缓存的分片可达 150–180 Mbps。
- 稳态播放实测 52 秒:缓冲从 32s 涨到 71s,零 stall,掉帧 18/7351。
同一个 URL、同一个 host、同样 512KB:
| 请求 | TTFB | 总耗时 | 吞吐 |
|---|---|---|---|
| 已取过的区间 | 20 ms | 59 ms | 70.85 Mbps |
| 换一个没碰过的 offset | 9476 ms | 15667 ms | 0.27 Mbps |
| 同一 offset 立刻再取 | 6 ms | 6 ms | 710.9 Mbps |
到 Akamai 的网络 RTT 是 6ms。那 9.5 秒不是传输时间,是边缘节点回中国源站取数据的时间。
这解释了两个现象:有的视频流畅有的卡到没法看;以及一拖进度条就卡(跳到未缓存的字节区间 = 必然 miss)。
播放器把带宽估计持久化在 localStorage["bilibili_dash_throughput_lru_v1"],
key 只按 host|网络类型|流类型 区分。实测抓到的中毒状态:
"XHR|upos-sz-mirrorcosov.bilivideo.com|4g|video": {
"samples": [2727, 2379, 3078, 2853, 2225, 3695, 30000, 2993],
"p25Kbps": 2379, "p50Kbps": 2853, "latencyMs": 4406
}真实链路是 31 Mbps。ABR 选流用的是 p25/p50 这类保守分位数,一次 miss 注入的 0.3 Mbps 样本就能把它拉下来, 而且这个状态跨视频、跨页面刷新保留 —— 视频 A 的一次 miss 会压着视频 B 的清晰度。
叠加:缓冲目标 nc_sdk_policy.dash_buffer_time 只有 30 秒,扛不住一次 15 秒的回源填充。
既然「同一段字节第二次取只要 6ms」,就抢在播放器前面把它取一遍。
- 被动观测播放器的 XHR/fetch,记录每个媒体文件已请求到的最大字节位置(anchor)。 只观测,不拦截、不改写、不代答 —— 正常播放路径完全不受影响。
- 在 anchor 前方拉一个滑动窗口(默认 48MB),按块预取,把边缘节点喂热。
- 数据读完即丢弃 —— 要的不是数据本身,是 CDN 被喂热这个副作用。
按 pathname 归并 track,所以 playurl 重新签名后仍然是同一条轨道。
- 启动时(
document_start,早于播放器读取)清洗一次已有的脏值。 - 之后 patch
Storage.prototype.setItem,每次写入都就地清洗,保证它不会再变脏。 - 低于下限的样本被丢弃,p25/p50/ewma 重算,
latencyMs压到上限。 - 健康数据原样返回,不做无谓改写。
预热的本质是强制触发本来可能不会发生的跨境回源 —— 而跨境传输恰恰是 B 站 CDN 成本最贵的部分。 所以有两道闸门:
- 起播观察(默认 20s):页面上累计播放满 20 秒才开始预取。 打开扫两眼就关掉的视频完全不产生任何预取。
- 单文件上限(默认 200MB):封顶单个视频的预取总量,防止在一个长视频上无限拉取。
再加上默认的 auto 模式(只对已经出现慢请求的视频预取)、标签页隐藏时暂停、
播放器缓冲告急时让出带宽 —— 平时的额外开销应当接近于零。
popup 显示实时的媒体请求数、慢请求数、已预热 MB、冷文件数、各 CDN 的 TTFB p50/p95, 以及估计器当前的 p25/p50 —— 卡的时候能直接看出是哪一类问题。
冷区间预取的实测(693MB 文件,upos-sz-mirrorcosov):
| 方案 | 耗时 | 有效吞吐 | 成功率 |
|---|---|---|---|
| 单发 4MB | 60s 后 network error |
– | 0/1 |
| 单发 1MB | 13.0s | 0.64 Mbps | 1/1 |
| 8×512KB 并发 | 24.2s | 1.39 Mbps | 8/8 |
| 4×1MB 并发 | 12.2s | 2.76 Mbps | 4/4 |
| 8×1MB 并发 | 34.5s | 1.70 Mbps | 7/8 |
| 12×1MB 并发 | 50.5s | 1.49 Mbps | 9/12 |
| 复读已预热区间 | 47ms | 89.81 Mbps | 1/1 |
结论:回源本身是瓶颈,块太大会超时,并发太高会互相挤且失败率上升。
默认取 chunkMB=1, concurrency=4,并对失败重试 2 次。
最后一行是这个扩展成立的依据:预热后同区间快了 140 倍。
冷填充的聚合速率只有约 2.8 Mbps。
这意味着对一个完全冷的高码率视频,预取追不上实时播放:
| 流 | 码率 | 预取能否追上 |
|---|---|---|
| 1080P60 AVC | 9876 kbps | ❌ 远追不上 |
| 1080P AVC | 4461 kbps | ❌ 追不上 |
| 1080P60 HEVC | 3723 kbps | |
| 1080P HEVC | 1920 kbps | ✅ |
| 720P AVC | 1889 kbps | ✅ |
所以这个扩展在下列场景收益最大:拖动进度条后的局部冷区间、暂停时提前铺路、中等码率的流。 对于全程冷的 1080P60 AVC,它能减少停顿次数但无法消除。
配套建议:在播放器设置里优先使用 HEVC / AV1。
同画质下 1080P60 的 AVC 是 9876 kbps,HEVC 3723、AV1 3493 —— 字节数直接降 2.6 倍,
既少踩 miss,也让预取真的追得上。服务端 dash_config.hevc_enable 和 av1_enable 都是开的。
其他限制:
world: "MAIN"的内容脚本需要 Chrome 111+。- 预取的流量是额外的(预取一遍 + 播放器再取一遍)。单个视频最多 200MB(可调),
且需先累计播放 20 秒才启动。默认的
auto模式只对已探测到慢请求的视频预取, 平时零额外流量;always模式则对每个视频都预热。 - 这些额外请求会落在 B 站的 CDN 与跨境回源上。个人使用无所谓, 但这是公开分发前应当知情的一点。
- 只处理网页版播放器。
- 打开
chrome://extensions/ - 右上角开启「开发者模式」
- 点「加载已解压的扩展程序」,选择本目录
node test/inject.test.js23 项单元测试,其中估计器清洗部分直接用实测抓到的中毒快照做输入。
| 项 | 默认 | 说明 |
|---|---|---|
| 预取模式 | auto |
auto 只在探测到慢请求后预取;always 总是预取;off 只统计 |
| 前方预热 | 48 MB | anchor 之后预热多少 |
| 分块大小 | 1 MB | 实测最优,别调大 |
| 并发 | 4 | 实测最优,调高反而更慢且失败率上升 |
| 慢请求阈值 | 800 ms | TTFB 超过此值判定该文件为冷 |
| 起播观察 | 20 s | 累计播放满此时长才开始预取 |
| 单文件上限 | 200 MB | 单个媒体文件的预取总量封顶 |
| 写入时清洗 | 开 | 估计器护栏 |
| 下限 | 8000 kbps | 估计器的 p25/p50/ewma 下限 |
popup 里还有「立即清空估计器并刷新页面」—— 卡住之后一直卡时按这个。
manifest.json
icons/ 16/32/48/128 图标(128 亦用作商店图标)
src/inject.js MAIN world:观测、预取、估计器护栏
src/bridge.js ISOLATED world:chrome.storage 与页面之间转发
src/popup.html popup UI
src/popup.js
test/inject.test.js 单元测试(在 vm 里加载真实的 inject.js)
scripts/build.mjs 打包成商店可上传的 zip
PRIVACY.md 隐私政策
npm test
npm run buildnpm run build 产出 dist/bililoader-<version>.zip,并校验三件事:
manifest.json位于 zip 根目录- 条目名使用正斜杠(Windows PowerShell 5.1 的
Compress-Archive会生成反斜杠, 违反 ZIP 规范,所以脚本优先使用 pwsh) - manifest 里引用的每个文件(图标、popup、content scripts)都确实被打进了包
已验证:
- 26 项单元测试,在 vm 里加载真实的
inject.js源码执行; 估计器清洗部分直接用实测抓到的中毒快照做输入 - 估计器护栏在真实 B 站页面上验证:普通 localStorage key 与 sessionStorage 不受影响、 中毒值写入即被清洗、非法 JSON 不抛异常
- 预热有效性与全部预取参数均来自实测(见上文两张表)
尚未验证: 扩展作为完整扩展在真实 Chrome 中加载运行的端到端链路 (popup、chrome.storage 桥接)。装上后打开 popup 看「媒体请求」计数是否在涨即可确认注入成功。