Bili-SyncPlay 是一个“浏览器扩展(Chrome / Edge / Firefox)+ WebSocket 服务端”的哔哩哔哩同步观影项目。用户可以创建或加入房间,分享当前视频,并在参与者之间同步播放、暂停、跳转和播放速率。
它覆盖了完整的本地使用链路:
- 在 Chrome / Edge / Firefox 121+ 中加载未打包扩展
- 启动本地同步服务
- 创建房间并复制邀请串
- 让多个成员保持同一共享视频的同步播放
本仓库是一个 monorepo:
extension/:浏览器扩展(Chrome / Edge / Firefox)server/:WebSocket 房间服务与管理后台packages/protocol/:共享协议类型
- 邀请格式:
roomCode:joinToken - 默认本地服务地址:
ws://localhost:8787 - 本地开发浏览器:Chrome、Edge、Firefox 121+
- 生产环境建议地址:
wss://<你的域名>
如果你想直接使用已发布版本,可以直接从以下已上架商店安装:
npm install
npm run build
Chrome / Edge(npm run build 产出 extension/dist):
- 打开
chrome://extensions - 开启开发者模式
- 点击
加载已解压的扩展程序 - 选择
extension/dist
Firefox 121+(先构建 Firefox 目标):
npm run build:extension:firefox # 产出 extension/dist-firefox
- 打开
about:debugging#/runtime/this-firefox - 点击
临时载入附加组件… - 选择
extension/dist-firefox/manifest.json
Firefox 构建产出 event page 形态后台(background.scripts,因 Firefox 不支持 MV3 background.service_worker),并覆盖扩展 CSP,使明文 ws:// 服务端不会被强制升级为 wss://。临时附加组件在 Firefox 关闭后移除,每次重启需重新载入。
在未打包扩展连接本地服务器之前,需要先把当前扩展 Origin 加入 ALLOWED_ORIGINS。
PowerShell:
$env:ALLOWED_ORIGINS="chrome-extension://<extension-id>"
npm run dev:server
Bash:
ALLOWED_ORIGINS=chrome-extension://<extension-id> \
npm run dev:server
Firefox Origin 说明。 Firefox 给每个安装分配随机 moz-extension://<uuid>(重装会变、各用户不同),不像 Chrome 固定扩展 ID 那样有一个通用值:
- 自建 / 少数用户:从
about:debugging(扩展的 Internal UUID / 清单 URL)或服务端被拒握手日志读到该 UUID,把这个精确的moz-extension://<uuid>加入ALLOWED_ORIGINS;重装扩展后需更新。 - 公共 / 共享服务端:设
ALLOW_ANY_FIREFOX_EXTENSION_ORIGIN=true,接受任意格式正确的moz-extension://<uuid>而无需逐一枚举。它仍拒绝网页 Origin,且不替代房间/成员 token 鉴权(见下方环境变量参考)。
Firefox 把扩展后台视为安全上下文,非 localhost 服务端必须用 wss://;Firefox 构建已覆盖扩展 CSP,使本地开发时 ws://localhost 不被强制升级。
- 打开扩展弹窗
- 创建房间,或者使用
roomCode:joinToken加入已有房间 - 打开受支持的 Bilibili 视频页面
- 在弹窗中点击
同步当前页视频 - 其他房间成员会打开同一视频并进入同步模式
如果成员在仍处于房间时浏览到其他未共享视频页面,该页面会保持本地模式,除非他们显式再次同步,否则不会影响房间。
- 房间能力
- 创建房间并获取邀请串
- 使用
roomCode:joinToken加入房间 - 直接在弹窗中复制并分享邀请串
- 同步能力
- 在扩展弹窗中分享当前页面视频
- 同步播放、暂停、跳转和播放速率
- 房间成员自动打开当前共享的视频
- 页面内反馈
- 成员加入和离开提示
- 共享视频变更提示
- 播放、暂停、跳转、倍速变化提示
- 房间内的本地浏览隔离
- 未共享页面不会把播放状态广播回房间
- 在未共享页面上的手动播放仅在本地生效
https://www.bilibili.com/video/*https://www.bilibili.com/bangumi/play/*https://www.bilibili.com/festival/*https://www.bilibili.com/list/watchlater*,且页面 URL 中带有bvidhttps://www.bilibili.com/medialist/play/watchlater*,且页面 URL 中带有bvid
视频变体识别:
- 多 P 视频通过
?p=识别 - festival 页面通过
bvid + cid识别
Bili-SyncPlay/
extension/ 浏览器扩展(Chrome/Edge/Firefox)
server/ WebSocket 房间服务器
packages/protocol/ 共享协议类型
scripts/ 发布打包脚本
docs/ 运维、迁移和政策文档
.github/workflows/ GitHub Actions 工作流
| 依赖 | 最低版本 | 推荐版本 | 说明 |
|---|---|---|---|
| Node.js | 18 | 22 | 参见 .nvmrc;Node 20 和 22 均受支持 |
| npm | 8 | 10 | 随对应 Node.js 版本附带 |
| Chrome / Edge | 当前稳定版 | 当前稳定版 | 用于加载未打包扩展 |
| Firefox | 121 | 当前稳定版 | 可选;使用 Firefox 构建(dist-firefox,event page 后台) |
| Redis | 6.0 | 7+ | 单机模式可选;多节点部署和重启后持久化必须使用 |
| 反向代理 | 任意支持 WebSocket 的代理 | Nginx 1.18+ | 生产环境中用于 TLS 终止和 wss:// |
- 无 Redis 时不做多节点一致性保证。 当
ROOM_STORE_PROVIDER=memory时,每个服务实例各自维护房间状态。连接到不同节点的成员会看到不同的房间。 - 不内置负载均衡。 多节点部署依赖外部入口层(Nginx、HAProxy、云 SLB/ALB)分发 WebSocket 连接,服务端本身不实现 L4/L7 负载均衡。
- 不恢复浏览器会话。 房间成员状态(
roomCode、joinToken、memberToken)存储在chrome.storage.session,浏览器关闭后即清除。用户需在下次打开浏览器后重新加入房间。 - 不提供终端用户账号系统。 房间访问仅通过
roomCode:joinToken邀请串控制,没有面向观众的注册或登录机制。 - 不支持移动端浏览器或 Safari。 扩展为 Manifest V3:Chrome/Edge(service worker 后台)与 Firefox 121+(event page 后台);Safari 与移动端浏览器不在范围内。
- 默认服务器地址:
ws://localhost:8787 - 服务器地址输入为空时,会回退到构建时默认值
- 仅接受
ws://和wss:// - 本地未打包扩展开发要求
ALLOWED_ORIGINS=chrome-extension://<extension-id>(Chrome/Edge)或当前moz-extension://<uuid>/ALLOW_ANY_FIREFOX_EXTENSION_ORIGIN=true(Firefox;见“启动本地服务器”)
如果你要在本地使用后台页面,需要先带上管理认证配置启动服务端,然后访问:
http://localhost:8787/admin
这对应的是单进程本地开发模式,也就是管理面和 WebSocket 服务共用同一个 npm run dev:server 进程。
如果你使用的是独立 Global Admin 进程,则入口通常会变成下面两种之一:
http://localhost:8788/admin
https://admin.example.com/admin
其中:
http://localhost:8787/admin:单进程开发或未拆分管理面的场景http://localhost:8788/admin:本机直接启动server/dist/global-admin-index.jshttps://admin.example.com/admin:生产环境经反向代理后的统一管理面地址
PowerShell 示例:
$env:ADMIN_USERNAME="admin"
$env:ADMIN_PASSWORD_HASH="sha256:<hex-password-hash>"
$env:ADMIN_SESSION_SECRET="<random-secret>"
$env:ADMIN_ROLE="admin"
npm run dev:server
如果你只是在本地或非生产环境下预览后台演示数据,需要显式开启:
$env:ADMIN_UI_DEMO_ENABLED="true"
npm run dev:server
未开启这个变量时,后台页面上的 ?demo=1 会被忽略。
本地生成 sha256:<hex> 密码哈希:
PowerShell:
$password = "secret-123"
$bytes = [System.Text.Encoding]::UTF8.GetBytes($password)
$hash = [System.BitConverter]::ToString(
[System.Security.Cryptography.SHA256]::Create().ComputeHash($bytes)
).Replace("-", "").ToLower()
"sha256:$hash"
Node.js:
node -e "const { createHash } = require('node:crypto'); const password = 'secret-123'; console.log('sha256:' + createHash('sha256').update(password).digest('hex'));"
当前后台页面已经覆盖:
- 概览
- 房间列表和房间详情
- 运行事件
- 审计日志
- 配置摘要
- 关房、过期、清空共享视频、踢人、断开会话等现有管理动作
- 被踢成员会被临时阻止使用旧
memberToken立即自动重连
安装依赖:
npm install
在本地运行仓库级检查前,请先执行 npm install 安装依赖;CI 中则统一使用 npm ci 基于锁文件做干净安装,然后再执行同一套检查。
推荐直接使用根工作区命令:
npm run lint
npm run format:check
npm run typecheck
npm run build
npm test
常用命令说明:
npm run lint:执行全仓 ESLint 检查npm run lint:fix:执行可安全应用的 ESLint 自动修复npm run format:用 Prettier 重写格式npm run format:check:只检查格式,不改文件npm run typecheck:执行 protocol、server、extension 源码的 TypeScript 语义检查npm run build:按依赖顺序构建protocol、server、extensionnpm test:执行 audit gate 测试,以及 protocol、server、extension 的全仓测试npm run audit:执行依赖审计门禁;未进入白名单的high或critical漏洞会导致失败npm run test:audit-gate:执行依赖审计门禁的单元测试npm run test:server:redis:显式执行 server 的 Redis 持久化回归测试
开发约定:
- 保持入口文件轻量化,并且让共享规则维持单一来源。
- 本地检查前先执行
npm install安装依赖;CI 中统一先执行npm ci,再跑同一套校验流程。 - 提交前执行
npm run lint、npm run format:check、npm run typecheck、npm run build、npm test。 - 完整贡献约束见 CONTRIBUTING.md。
CI 会在 npm ci 后执行 npm run audit。该门禁会运行 npm audit --json --audit-level=high,只要发现未被 audit-allowlist.json 中有效条目覆盖的 high 或 critical 漏洞,就会失败。
当出现 high 级别审计结果时:
- 优先升级或替换存在漏洞的依赖,并提交对应 lockfile 变更。
- 如果暂时没有可用修复且风险已经评估,可以使用 audit gate 输出的 ID 添加短期白名单条目:
{
"id": "npm:<package>:<advisory-source>",
"expires": "YYYY-MM-DD",
"reason": "为什么可以短期接受,以及后续如何移除"
}
- 过期时间应尽量短。过期、格式错误或缺少过期时间的条目都会自动让门禁失败。
- 修复或移除漏洞依赖时,应在同一变更中删除对应白名单条目。
仓库现在在 bench/ 下提供了可复现的基准脚本,对应 issue #67 里要求的三类高负载场景。
命令:
npm run bench:single-room
npm run bench:redis-broadcast
npm run bench:reconnect-storm
npm run bench:ci-light
每个脚本都会把标准化 JSON 打到 stdout,也可以用 --output <path> 落盘。
示例:
npm run bench:single-room -- --output .tmp/bench-single.json
npm run bench:redis-broadcast -- --duration-seconds 30 --sample-watchers 12
npm run bench:reconnect-storm -- --members 500 --output .tmp/bench-reconnect.json
默认场景:
bench:single-room:单节点、单房间、100 成员,playback:update以 10 Hz 连续发送 60 秒bench:redis-broadcast:两台 room node 通过 Redis 互联,负载与上面一致,owner 固定在节点 A,其余成员固定在节点 Bbench:reconnect-storm:同一房间 500 成员先断线,再同时带旧memberToken回连bench:ci-light:面向 CI 的轻量烟雾基线,覆盖一个小规模单节点广播场景和一个小规模重连风暴场景
CI 基线行为:
bench:ci-light会读取bench/ci-light-baseline.json,运行轻量场景,并输出results.json、comparison.json和summary.md。- CI 只在“明显退化”时失败:错误率超过配置上限,或
P95延迟超过基线倍数阈值。 .github/workflows/ci.yml会把这些结果作为 artifact 上传,方便在 PR 里回看原始数据。
Redis 行为:
bench:redis-broadcast在设置了REDIS_URL时会直接复用该实例。- 如果没有设置
REDIS_URL,且PATH中存在redis-server,脚本会自动拉起一个临时本地 Redis。 - 结果 JSON 结构固定、便于 diff:配置、吞吐、延迟百分位(
P50/P95/P99)和错误率都会按同一 schema 输出。
结果结构:
{
"schemaVersion": 1,
"scenario": "redis-broadcast",
"startedAt": "2026-04-22T10:00:00.000Z",
"completedAt": "2026-04-22T10:01:00.250Z",
"config": {},
"metrics": {
"throughput": {},
"latency": {},
"errorRatePercent": 0,
"errors": 0
},
"notes": []
}
说明:
- 广播延迟默认只从可配置数量的 watcher socket 采样,避免压测器自己在每次广播上串行等待全量客户端确认。
- 重连延迟统计的是从 socket 打开到回房后收到第一条
room:state的完整耗时。
构建全部内容:
npm run build
使用固定的 Chrome 扩展 ID 构建扩展:
$env:BILI_SYNCPLAY_EXTENSION_KEY="<chrome-web-store-public-key>"
npm run build -w @bili-syncplay/extension
如果设置了 BILI_SYNCPLAY_EXTENSION_KEY,构建会把它写入 extension/dist/manifest.json 的 manifest.key。这里应使用与你在 Chrome Web Store 发布项对应的同一个公钥,这样本地加载的扩展才能和已发布版本保持相同的扩展 ID。
运行自动化测试:
npm test
当前仓库中的测试覆盖包括:
- protocol 客户端消息校验
- server WebSocket 校验、认证、Origin 过滤和限流检查
- background 房间状态竞态处理
也可以使用 workspace 级测试命令:
npm run test -w @bili-syncplay/protocol
npm run test -w @bili-syncplay/server
npm run test:redis -w @bili-syncplay/server
npm run test -w @bili-syncplay/extension
Redis 集成测试说明:
npm run test -w @bili-syncplay/server会保留 Redis 专项测试为可选项;未配置REDIS_URL时可能跳过npm run test:redis -w @bili-syncplay/server是显式的 Redis 回归测试入口- 在仓库根目录也可以运行
npm run test:server:redis - 这些显式 Redis 测试命令要求设置
REDIS_URL,缺失时会直接失败
仓库现在遵循“薄入口 + 具名模块”的组织方式。
extension/src/backgroundindex.ts只负责装配- 运行态统一收敛在
state-store.ts - socket、room session、popup state、diagnostics、tab 协调分别由独立 controller 承载
extension/src/contentindex.ts只负责装配- 运行态统一收敛在
content-store.ts - 播放同步、room-state hydration、导航、视频绑定、分享识别由独立 controller 承载
extension/src/popupindex.ts只负责装配- 本地 UI 状态统一收敛在
popup-store.ts - template、refs、render、actions、background port 同步各自独立
extension/src/shared- 扩展端共享 helper 必须沉淀在这里,例如共享视频 URL 归一化,不要回到各入口文件各写一份
packages/protocol/src- 协议类型位于
types/* - 类型守卫位于
guards/* index.ts保持兼容导出面
- 协议类型位于
server/srcapp.ts只负责运行时装配- 环境变量解析位于
config/* - bootstrap 拼装位于
bootstrap/* - admin 路由分发位于
admin/routes/*
当前回归测试已经开始按这些边界补齐,不再只覆盖“功能能不能跑通”,也覆盖重构后 store/controller/helper 的关键行为。
后续继续改仓库时,默认遵守以下约束:
- 优先把新行为放进已有具名模块,而不是继续拉长
index.ts - 入口文件只保留初始化、依赖装配和监听注册
- 共享规则只能有一个可信来源;不要重新引入本地
normalizeUrl()包装或重复 parser - 新增状态优先进入对应 store,不再随手增加新的顶层可变变量
- 如果一个文件同时开始混入状态、IO 和业务决策,应在它再次膨胀前拆分
- 修改 store、controller、helper、protocol guard、server config/router 边界时,必须同步补或改对应测试
建议提交前自检:
npm run lint
npm run format:check
npm run typecheck
npm run build
npm test
启动本地服务器:
npm run dev:server
默认服务器地址:
ws://localhost:8787
开发说明:
@bili-syncplay/server依赖@bili-syncplay/protocol的构建产物- 对于全新本地环境,优先使用
npm run build,而不是单独构建server - 扩展默认不会永久保持 socket 连接;只有在会话状态中已存在房间,或用户创建 / 加入房间时才会建立连接
- 重新进入已有房间现在需要保存的
joinToken;断开连接后,旧的memberToken会被丢弃 - 如果你修改了协议类型或消息校验,需要重新构建
packages/protocol和server - 本地服务器默认会拒绝扩展连接,除非
ALLOWED_ORIGINS包含当前chrome-extension://<extension-id> - 你可以在
chrome://extensions中查看未打包扩展的 ID
Chrome 显示的扩展版本来自 extension/dist/manifest.json。 构建过程中,该 manifest 版本会根据根目录 package.json 自动生成。
- 如果用户在加入房间前点击
Sync current page video,扩展会先提示创建房间 - 如果房间当前已经共享了另一个视频,弹窗会在替换前请求确认
- background service worker 只会转发当前识别为共享标签页的播放更新
- 切换服务器地址会断开当前 socket;如果扩展仍有活动房间或待创建房间,会使用新地址重新连接
- 如果持久化的服务器地址非法,扩展会保留该值并阻止自动重连,直到用户修正地址
- 支持的播放页面依赖 Bilibili 的 DOM 和 URL 模式,因此如果 Bilibili 后续改版,festival 页面和稍后再看页面可能需要兼容性更新
扩展有意按生命周期拆分持久化状态:
chrome.storage.session:roomCode,joinToken,memberToken,memberId,roomStatechrome.storage.local:displayName,serverUrl
实际影响:
- 浏览器重启后不会自动恢复之前的房间
- 自定义服务器地址会在浏览器重启后保留
- 房间会话态与用户偏好会分别持久化,房间状态写入失败不会把
serverUrl或displayName留在半更新状态 - 只有在浏览器会话中仍保留
roomCode和joinToken时,弹窗才能重新进入当前房间 memberToken会在断开连接时被有意清除,并在重新加入成功后重新签发- 如果持久化的服务器地址非法,扩展会保留原始值并停止自动重连,直到地址被修正
- 关闭浏览器后,下次启动不会自动恢复之前的房间
推荐环境:
- Node.js 22(见
.nvmrc) - Redis
- Nginx 反向代理
- 生产环境使用
wss://服务器地址
扩展支持在弹窗中切换服务器地址,因此你可以从本地开发切换到已部署的服务器,例如:
wss://sync.example.com
扩展的服务器地址只接受 ws:// 和 wss://;空输入会回退到当前构建内置的默认值。未设置 BILI_SYNCPLAY_DEFAULT_SERVER_URL 时,该默认值是 ws://localhost:8787。
如果你希望 Chrome 应用商店提交包内置公共服务器地址、而 GitHub 源码继续保持 ws://localhost:8787,构建扩展时设置环境变量 BILI_SYNCPLAY_DEFAULT_SERVER_URL 即可,例如在 PowerShell 中:
$env:BILI_SYNCPLAY_DEFAULT_SERVER_URL="wss://sync.example.com"
npm run build:release
不设置该环境变量时,构建产物仍然使用 ws://localhost:8787;设置后,用户在弹窗里清空服务器地址并保存,也会回退到这个构建时注入的地址。
本地开发时,ALLOWED_ORIGINS 必须包含当前 chrome-extension://<extension-id>,否则服务端会以 origin_not_allowed 拒绝 WebSocket 握手。
服务端现在也支持可选的 JSON 配置文件。加载优先级为:
- 内置默认值
- 当前工作目录下的
server.config.json,或BILI_SYNCPLAY_CONFIG指定的文件 - 环境变量
这样可以在保持现有纯环境变量启动方式完全兼容的前提下,把生产环境里稳定的非敏感配置收敛到文件中。
server.config.json 示例:
{
"port": 8787,
"globalAdminPort": 8788,
"security": {
"allowedOrigins": [
"chrome-extension://<extension-id>",
"https://sync.example.com"
],
"trustedProxyAddresses": ["127.0.0.1", "10.0.0.10"]
},
"persistence": {
"provider": "redis",
"runtimeStoreProvider": "redis",
"roomEventBusProvider": "redis",
"adminCommandBusProvider": "redis",
"nodeHeartbeatEnabled": true,
"redisUrl": "redis://127.0.0.1:6379"
},
"adminUi": {
"enabled": false
}
}
以下管理后台敏感字段仍然只支持环境变量:
ADMIN_USERNAMEADMIN_PASSWORD_HASHADMIN_SESSION_SECRET
当前服务器实现:
- 监听
PORT或server.config.json中的port,默认值为8787 - 在同一个端口上同时提供 WebSocket 流量和简单健康检查
- 对
GET /返回{"ok":true,"service":"bili-syncplay-server"} - 在同一个端口上暴露管理控制面板和后台接口:
/admin、/healthz、/readyz、/api/admin/* - 支持
memory和redis两种房间存储实现 - 当
ROOM_STORE_PROVIDER=redis时会持久化房间基础状态 - 房间加入需要
roomCode + joinToken,房间消息需要memberToken - 服务重连或服务端重启后会重新签发
memberToken - 最后一名成员离开后,房间不会立即删除,而是保留到
EMPTY_ROOM_TTL_MS到期 - 支持 Origin 白名单、连接限流、消息限流和结构化安全日志
现在服务端已经支持完整多节点拓扑,包括共享管理员会话、共享事件与审计流、共享运行时索引、跨节点房间状态广播、跨节点管理命令,以及独立的全局管理入口。
- 普通用户始终连接单一公共地址,例如
wss://sync.example.com - 入口层负责 TLS 终止、反向代理和连接分发
- Room Node 负责承载 WebSocket 长连接和健康检查
- Global Admin 负责
/admin与/api/admin/* - Redis 负责共享持久化、运行时索引、事件总线和命令总线
推荐生产拓扑:
- 统一入口层:
Nginx、HAProxy、SLB/ALB等,负责 TLS 终止和 WebSocket 反向代理 room-node-a:承载 WebSocket 房间流量和探活room-node-b:承载 WebSocket 房间流量和探活global-admin:承载/admin与/api/admin/*redis:共享持久化、运行时索引、事件总线和命令总线
服务端不会在应用进程内实现 L4/L7 负载均衡;多节点部署需要依赖外部入口层,把用户连接统一接入后再转发到各个 Room Node。普通用户应始终连接单一公共地址,例如 wss://sync.example.com,而不是手动选择节点地址。
提示 如果你只是本地开发或单机部署,可以继续使用单节点模式。下面这部分主要面向生产多节点部署。
日常扩缩容、Redis 故障、管理员口令轮换和常见告警处理见 多节点运维 Runbook。
完整多节点上线建议统一开启以下 provider:
ROOM_STORE_PROVIDER=redisADMIN_SESSION_STORE_PROVIDER=redisADMIN_EVENT_STORE_PROVIDER=redisADMIN_AUDIT_STORE_PROVIDER=redisRUNTIME_STORE_PROVIDER=redisROOM_EVENT_BUS_PROVIDER=redisADMIN_COMMAND_BUS_PROVIDER=redisNODE_HEARTBEAT_ENABLED=true
Room Node 示例:
BILI_SYNCPLAY_CONFIG=/etc/bili-syncplay/server.config.json \
PORT=8787 \
INSTANCE_ID=room-node-a \
ADMIN_SESSION_STORE_PROVIDER=redis \
ADMIN_EVENT_STORE_PROVIDER=redis \
ADMIN_AUDIT_STORE_PROVIDER=redis \
GLOBAL_ADMIN_ENABLED=false \
node server/dist/index.js
独立 Global Admin 示例:
BILI_SYNCPLAY_CONFIG=/etc/bili-syncplay/server.config.json \
GLOBAL_ADMIN_PORT=8788 \
INSTANCE_ID=global-admin \
ADMIN_SESSION_STORE_PROVIDER=redis \
ADMIN_EVENT_STORE_PROVIDER=redis \
ADMIN_AUDIT_STORE_PROVIDER=redis \
GLOBAL_ADMIN_ENABLED=true \
node server/dist/global-admin-index.js
如果管理 UI 需要请求一个独立 API 域名,可设置 GLOBAL_ADMIN_API_BASE_URL=https://admin.example.com。
| 角色 | 典型进程 | 对外职责 | 必须唯一 | 必须保持一致 | 推荐值 / 说明 |
|---|---|---|---|---|---|
room-node |
server/dist/index.js |
WebSocket、/、/healthz、/readyz |
INSTANCE_ID、监听地址/端口 |
REDIS_URL、各类 *_PROVIDER、安全与限流参数 |
GLOBAL_ADMIN_ENABLED=false |
global-admin |
server/dist/global-admin-index.js |
/admin、/api/admin/* |
INSTANCE_ID、GLOBAL_ADMIN_PORT |
REDIS_URL、管理员认证参数、共享 provider 配置 |
GLOBAL_ADMIN_ENABLED=true |
edge |
nginx / haproxy / 云 LB |
TLS 终止、统一入口、反向代理、连接分发 | 对外域名、证书、upstream 定义 | 指向的后端节点列表 | 用户只连接统一入口地址 |
redis |
redis-server |
共享持久化、运行时索引、总线 | 实例地址、密码、ACL | 所有节点都要指向同一个 Redis | 生产建议仅内网开放 |
所有 Room Node 与 Global Admin 都应保持一致的配置:
REDIS_URLROOM_STORE_PROVIDER=redisADMIN_SESSION_STORE_PROVIDER=redisADMIN_EVENT_STORE_PROVIDER=redisADMIN_AUDIT_STORE_PROVIDER=redisRUNTIME_STORE_PROVIDER=redisROOM_EVENT_BUS_PROVIDER=redisADMIN_COMMAND_BUS_PROVIDER=redisNODE_HEARTBEAT_ENABLED=true- 与业务正确性相关的限流、安全和房间容量参数,例如
MAX_MEMBERS_PER_ROOM、MAX_MESSAGE_BYTES、ALLOWED_ORIGINS - 管理员认证配置,例如
ADMIN_USERNAME、ADMIN_PASSWORD_HASH、ADMIN_SESSION_SECRET
每个节点必须不同或按角色区分的配置:
INSTANCE_ID:每个进程都必须唯一,例如room-node-a、room-node-b、global-adminPORT:Room Node 自己监听的 HTTP/WebSocket 端口GLOBAL_ADMIN_PORT:仅global-admin使用GLOBAL_ADMIN_ENABLED:Room Node 设为false,独立管理面设为true- 监听地址、防火墙规则、systemd 服务名、日志路径
如果当前只有两台服务器,推荐先按下面的方式部署:
- 服务器 1:
Nginx + Redis + room-node-a + global-admin - 服务器 2:
room-node-b
建议端口规划:
| 机器 | 角色 | 建议监听 | 是否公网开放 | 说明 |
|---|---|---|---|---|
| 服务器 1 | nginx |
80/443 |
是 | 用户统一入口 |
| 服务器 1 | room-node-a |
127.0.0.1:8787 或内网地址 |
否 | 由入口层反代 |
| 服务器 1 | global-admin |
127.0.0.1:8788 或内网地址 |
否 | 由入口层反代 |
| 服务器 1 | redis |
127.0.0.1:6379 或内网地址 |
否 | 只允许节点访问 |
| 服务器 2 | room-node-b |
10.0.0.12:8787 等内网地址 |
否 | 由服务器 1 的入口层反代 |
服务器 1 的 Room Node 环境变量示意:
BILI_SYNCPLAY_CONFIG=/etc/bili-syncplay/server.config.json \
PORT=8787 \
INSTANCE_ID=room-node-a \
REDIS_URL=redis://10.0.0.11:6379 \
ROOM_STORE_PROVIDER=redis \
ADMIN_SESSION_STORE_PROVIDER=redis \
ADMIN_EVENT_STORE_PROVIDER=redis \
ADMIN_AUDIT_STORE_PROVIDER=redis \
RUNTIME_STORE_PROVIDER=redis \
ROOM_EVENT_BUS_PROVIDER=redis \
ADMIN_COMMAND_BUS_PROVIDER=redis \
NODE_HEARTBEAT_ENABLED=true \
GLOBAL_ADMIN_ENABLED=false \
node server/dist/index.js
服务器 2 的 Room Node 环境变量示意:
BILI_SYNCPLAY_CONFIG=/etc/bili-syncplay/server.config.json \
PORT=8787 \
INSTANCE_ID=room-node-b \
REDIS_URL=redis://10.0.0.11:6379 \
ROOM_STORE_PROVIDER=redis \
ADMIN_SESSION_STORE_PROVIDER=redis \
ADMIN_EVENT_STORE_PROVIDER=redis \
ADMIN_AUDIT_STORE_PROVIDER=redis \
RUNTIME_STORE_PROVIDER=redis \
ROOM_EVENT_BUS_PROVIDER=redis \
ADMIN_COMMAND_BUS_PROVIDER=redis \
NODE_HEARTBEAT_ENABLED=true \
GLOBAL_ADMIN_ENABLED=false \
node server/dist/index.js
服务器 1 的 Global Admin 环境变量示意:
BILI_SYNCPLAY_CONFIG=/etc/bili-syncplay/server.config.json \
GLOBAL_ADMIN_PORT=8788 \
INSTANCE_ID=global-admin \
REDIS_URL=redis://10.0.0.11:6379 \
ROOM_STORE_PROVIDER=redis \
ADMIN_SESSION_STORE_PROVIDER=redis \
ADMIN_EVENT_STORE_PROVIDER=redis \
ADMIN_AUDIT_STORE_PROVIDER=redis \
RUNTIME_STORE_PROVIDER=redis \
ROOM_EVENT_BUS_PROVIDER=redis \
ADMIN_COMMAND_BUS_PROVIDER=redis \
NODE_HEARTBEAT_ENABLED=true \
GLOBAL_ADMIN_ENABLED=true \
node server/dist/global-admin-index.js
如果入口机同时承载 room-node-a、global-admin 和 redis,它通常会比其他节点承担更多网络和 CPU 压力。此时建议在入口层给远端 Room Node 更高权重,或者至少使用 least_conn,不要按 1:1 平均分配长连接。
多节点控制面当前使用的 Redis 键族:
bsp:room:*、bsp:room-index、bsp:room-expiry:房间基础持久化bsp:runtime:*:共享 session、房间成员、被踢 token 与节点心跳bsp:admin:session:*:共享管理员 Bearer 会话bsp:events:运行事件流bsp:audit-logs:管理审计流bsp:room-events:房间事件总线频道bsp:admin-command:*、bsp:admin-command-result:*:管理命令频道
服务器支持以下环境变量。虽然内置了安全默认值,但生产环境应显式设置:
BILI_SYNCPLAY_CONFIG:可选的 JSON 配置文件路径;未设置时会优先查找当前工作目录下的server.config.jsonALLOWED_ORIGINS:逗号分隔的 WebSocketOrigin白名单- 如果
ALLOWED_ORIGINS为空,服务器默认拒绝所有显式Origin ALLOW_MISSING_ORIGIN_IN_DEV:设为true时允许缺失Origin头ALLOW_ANY_FIREFOX_EXTENSION_ORIGIN:设为true时接受任意格式正确的moz-extension://<uuid>Origin;Firefox 每个安装随机分配 UUID,公共/共享服务端无法逐一枚举进ALLOWED_ORIGINS。仍会拒绝网页 Origin(网页永远无法呈现moz-extension://Origin),且不替代房间/成员 token 鉴权;默认falseTRUSTED_PROXY_ADDRESSES:逗号分隔的受信代理 socket IP 列表;只有来自这些代理的请求才会使用X-Forwarded-ForMAX_CONNECTIONS_PER_IP:每个 IP 允许的最大并发 WebSocket 连接数CONNECTION_ATTEMPTS_PER_MINUTE:每个 IP 每分钟最大握手尝试次数MAX_MEMBERS_PER_ROOM:房间成员上限MAX_MESSAGE_BYTES:WebSocket 消息字节上限INVALID_MESSAGE_CLOSE_THRESHOLD:在断开连接前允许的无效消息次数ROOM_STORE_PROVIDER:memory或redisEMPTY_ROOM_TTL_MS:空房保留时长,超时后删除ROOM_CLEANUP_INTERVAL_MS:服务端扫描并清理过期房间的周期REDIS_URL:当ROOM_STORE_PROVIDER=redis时使用的 Redis 连接地址ADMIN_USERNAME:管理后台登录用户名ADMIN_PASSWORD_HASH:管理后台密码哈希,当前支持sha256:<hex>或scrypt:<salt>:<base64url>ADMIN_SESSION_SECRET:用于绑定后台 Bearer Token 与服务端会话的 secretADMIN_SESSION_TTL_MS:后台会话有效期,单位毫秒ADMIN_ROLE:后台角色,可选viewer、operator、adminADMIN_UI_DEMO_ENABLED:是否开启后台内置 demo 模式,适用于本地 / 非生产预览,默认falseADMIN_SESSION_STORE_PROVIDER:memory或redisADMIN_EVENT_STORE_PROVIDER:memory或redisADMIN_AUDIT_STORE_PROVIDER:memory或redisRUNTIME_STORE_PROVIDER:memory或redisROOM_EVENT_BUS_PROVIDER:none、memory或redisADMIN_COMMAND_BUS_PROVIDER:none、memory或redisGLOBAL_ADMIN_ENABLED:设为false时,Room Node 保留/、/healthz、/readyz,但关闭/admin与/api/admin/*GLOBAL_ADMIN_API_BASE_URL:可选的管理 UI API 基址覆盖项GLOBAL_ADMIN_PORT:server/dist/global-admin-index.js使用的 HTTP 端口NODE_HEARTBEAT_ENABLED:是否开启节点心跳NODE_HEARTBEAT_INTERVAL_MS:节点心跳间隔,单位毫秒NODE_HEARTBEAT_TTL_MS:节点心跳 TTL,单位毫秒RATE_LIMIT_ROOM_CREATE_PER_MINUTERATE_LIMIT_ROOM_JOIN_PER_MINUTERATE_LIMIT_VIDEO_SHARE_PER_10_SECONDSRATE_LIMIT_PLAYBACK_UPDATE_PER_SECONDRATE_LIMIT_PLAYBACK_UPDATE_BURSTRATE_LIMIT_SYNC_REQUEST_PER_10_SECONDSRATE_LIMIT_SYNC_PING_PER_SECONDRATE_LIMIT_SYNC_PING_BURST
示例:
PORT=8787 \
ALLOWED_ORIGINS=chrome-extension://<extension-id>,https://sync.example.com,http://localhost:3000 \
TRUSTED_PROXY_ADDRESSES=127.0.0.1,10.0.0.10 \
ROOM_STORE_PROVIDER=redis \
REDIS_URL=redis://127.0.0.1:6379 \
EMPTY_ROOM_TTL_MS=900000 \
ROOM_CLEANUP_INTERVAL_MS=60000 \
MAX_CONNECTIONS_PER_IP=10 \
CONNECTION_ATTEMPTS_PER_MINUTE=20 \
MAX_MEMBERS_PER_ROOM=8 \
MAX_MESSAGE_BYTES=8192 \
ADMIN_USERNAME=admin \
ADMIN_PASSWORD_HASH=sha256:<hex-password-hash> \
ADMIN_SESSION_SECRET=<random-secret> \
ADMIN_SESSION_TTL_MS=43200000 \
node server/dist/index.js
快速生成后台密码哈希:
node -e "const { createHash } = require('node:crypto'); console.log('sha256:' + createHash('sha256').update('secret-123').digest('hex'));"
服务端现在已经内置 P0 管理后台,只读接口与主服务复用同一个 HTTP 端口。
管理控制面板入口:
- 打开
http://localhost:8787/admin - 使用
ADMIN_USERNAME、ADMIN_PASSWORD_HASH、ADMIN_SESSION_SECRET、ADMIN_ROLE配置的账号登录 - 页面已覆盖登录、概览、房间列表、房间详情、运行事件、审计日志、配置摘要,以及现有管理动作
角色模型:
viewer:只读访问概览、房间、事件、审计日志、配置摘要operator:在viewer基础上可执行房间和会话管理动作admin:当前能力与operator基本一致,为后续更高权限治理能力预留扩展位
动作语义说明:
踢出成员会断开当前成员会话,并临时阻止客户端拿旧memberToken立即自动重连断开会话只关闭指定 socket;如果客户端仍持有有效房间上下文,后续仍可正常重新加入
当前已实现接口:
GET /metricsGET /healthzGET /readyzPOST /api/admin/auth/loginPOST /api/admin/auth/logoutGET /api/admin/meGET /api/admin/overviewGET /api/admin/configGET /api/admin/roomsGET /api/admin/rooms/:roomCodeGET /api/admin/eventsGET /api/admin/audit-logsPOST /api/admin/rooms/:roomCode/closePOST /api/admin/rooms/:roomCode/expirePOST /api/admin/rooms/:roomCode/clear-videoPOST /api/admin/rooms/:roomCode/members/:memberId/kickPOST /api/admin/sessions/:sessionId/disconnect
鉴权方式:
- 管理接口使用
Authorization: Bearer <token> - 登录成功后返回服务端签发的 session token
ADMIN_ROLE用于控制当前唯一后台账号的角色,可选viewer、operator、adminINSTANCE_ID用于标识当前服务实例,并会出现在 overview、room detail 和 audit log 中- 写操作要求
operator及以上权限 - 如果未配置管理后台环境变量,管理认证接口会返回 unavailable / unauthorized
示例环境:
- Ubuntu 24.04 LTS
- 域名:
sync.example.com - 应用目录:
/opt/bili-syncplay - 服务用户:
bili-syncplay - 内部端口:
8787
先安装 Node.js 22(见 .nvmrc)、Redis 和 Nginx,然后克隆仓库:
sudo mkdir -p /opt/bili-syncplay
sudo chown "$USER":"$USER" /opt/bili-syncplay
git clone https://github.com/<your-org>/Bili-SyncPlay.git /opt/bili-syncplay
cd /opt/bili-syncplay
npm install
npm run build
为什么首轮部署推荐使用 npm run build:
- 它会构建
packages/protocol,而这是服务器运行时所必需的 - 它可以避免只构建部分 workspace,导致
server指向缺失的 protocol 产物
如果你只想构建服务器包:
npm run build -w @bili-syncplay/server
仅当 packages/protocol 已经构建且未变化时再使用这个命令。
生产环境入口文件为:
server/dist/index.js
你可以先手动启动它以验证构建结果:
cd /opt/bili-syncplay
PORT=8787 ROOM_STORE_PROVIDER=memory node server/dist/index.js
如果你准备使用 Redis 持久化房间状态,建议先验证 Redis 连通性:
redis-cli -u redis://127.0.0.1:6379 ping
预期响应:
PONG
预期启动日志:
Bili-SyncPlay server listening on http://localhost:8787
在另一个 shell 中验证本地健康检查:
curl http://127.0.0.1:8787/
预期响应:
{ "ok": true, "service": "bili-syncplay-server" }
创建独立用户:
sudo useradd --system --home /opt/bili-syncplay --shell /usr/sbin/nologin bili-syncplay
sudo chown -R bili-syncplay:bili-syncplay /opt/bili-syncplay
创建 /etc/systemd/system/bili-syncplay-room-node-a.service:
[Unit]
Description=Bili-SyncPlay room node A
After=network.target
[Service]
Type=simple
User=bili-syncplay
Group=bili-syncplay
WorkingDirectory=/opt/bili-syncplay
Environment=BILI_SYNCPLAY_CONFIG=/etc/bili-syncplay/server.config.json
Environment=PORT=8787
Environment=INSTANCE_ID=room-node-a
Environment=REDIS_URL=redis://127.0.0.1:6379
Environment=ROOM_STORE_PROVIDER=redis
Environment=ADMIN_SESSION_STORE_PROVIDER=redis
Environment=ADMIN_EVENT_STORE_PROVIDER=redis
Environment=ADMIN_AUDIT_STORE_PROVIDER=redis
Environment=RUNTIME_STORE_PROVIDER=redis
Environment=ROOM_EVENT_BUS_PROVIDER=redis
Environment=ADMIN_COMMAND_BUS_PROVIDER=redis
Environment=NODE_HEARTBEAT_ENABLED=true
Environment=GLOBAL_ADMIN_ENABLED=false
Environment=ADMIN_USERNAME=admin
Environment=ADMIN_PASSWORD_HASH=sha256:<hex-password-hash>
Environment=ADMIN_SESSION_SECRET=<random-secret>
ExecStart=/usr/bin/node /opt/bili-syncplay/server/dist/index.js
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
创建 /etc/systemd/system/bili-syncplay-global-admin.service:
[Unit]
Description=Bili-SyncPlay global admin
After=network.target
[Service]
Type=simple
User=bili-syncplay
Group=bili-syncplay
WorkingDirectory=/opt/bili-syncplay
Environment=BILI_SYNCPLAY_CONFIG=/etc/bili-syncplay/server.config.json
Environment=GLOBAL_ADMIN_PORT=8788
Environment=INSTANCE_ID=global-admin
Environment=REDIS_URL=redis://127.0.0.1:6379
Environment=ROOM_STORE_PROVIDER=redis
Environment=ADMIN_SESSION_STORE_PROVIDER=redis
Environment=ADMIN_EVENT_STORE_PROVIDER=redis
Environment=ADMIN_AUDIT_STORE_PROVIDER=redis
Environment=RUNTIME_STORE_PROVIDER=redis
Environment=ROOM_EVENT_BUS_PROVIDER=redis
Environment=ADMIN_COMMAND_BUS_PROVIDER=redis
Environment=NODE_HEARTBEAT_ENABLED=true
Environment=GLOBAL_ADMIN_ENABLED=true
Environment=ADMIN_USERNAME=admin
Environment=ADMIN_PASSWORD_HASH=sha256:<hex-password-hash>
Environment=ADMIN_SESSION_SECRET=<random-secret>
ExecStart=/usr/bin/node /opt/bili-syncplay/server/dist/global-admin-index.js
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
把公共的非敏感配置写入 /etc/bili-syncplay/server.config.json:
{
"security": {
"allowedOrigins": [
"chrome-extension://<extension-id>",
"https://sync.example.com"
],
"trustedProxyAddresses": ["127.0.0.1", "10.0.0.10"]
},
"persistence": {
"provider": "redis",
"runtimeStoreProvider": "redis",
"roomEventBusProvider": "redis",
"adminCommandBusProvider": "redis",
"nodeHeartbeatEnabled": true,
"redisUrl": "redis://127.0.0.1:6379",
"emptyRoomTtlMs": 900000,
"roomCleanupIntervalMs": 60000
}
}
启用并启动它们:
sudo systemctl daemon-reload
sudo systemctl enable --now bili-syncplay-room-node-a
sudo systemctl enable --now bili-syncplay-global-admin
sudo systemctl status bili-syncplay-room-node-a
sudo systemctl status bili-syncplay-global-admin
查看日志:
sudo journalctl -u bili-syncplay-room-node-a -f
sudo journalctl -u bili-syncplay-global-admin -f
下面先给出单机部署示例,再给出多节点 upstream 示例。单机示例适合本地或单节点生产;如果你已经启用完整多节点拓扑,应优先使用多节点示例。
建议 WebSocket 是长连接场景。多节点入口优先考虑
least_conn,其次再考虑默认轮询;只有在上线初期需要运维兜底时再额外保留 sticky。
创建 /etc/nginx/sites-available/bili-syncplay.conf:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;
limit_req_zone $binary_remote_addr zone=req_per_ip:10m rate=20r/m;
limit_req_zone $binary_remote_addr zone=admin_req_per_ip:10m rate=5r/s;
server {
listen 80;
server_name sync.example.com;
location ^~ /admin {
proxy_pass http://127.0.0.1:8788;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ^~ /api/admin/ {
limit_req zone=admin_req_per_ip burst=20 nodelay;
proxy_pass http://127.0.0.1:8788;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
limit_conn conn_per_ip 10;
limit_req zone=req_per_ip burst=10 nodelay;
proxy_pass http://127.0.0.1:8787;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600;
}
}
建议把更严格的请求频率限制保留在默认的 WebSocket 入口上,不要直接复用到 /admin 和 /api/admin/*。管理后台在首屏加载和执行操作时会并发请求多个接口,而服务端本身已经对认证和房间相关操作做了限流控制。
如果入口机需要把 WebSocket 连接分发到多个 Room Node,可改成 upstream。下面示例使用 least_conn,对长连接场景通常比默认轮询更稳妥:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;
limit_req_zone $binary_remote_addr zone=req_per_ip:10m rate=20r/m;
limit_req_zone $binary_remote_addr zone=admin_req_per_ip:10m rate=5r/s;
upstream bili_syncplay_ws {
least_conn;
server 127.0.0.1:8787;
server 10.0.0.12:8787;
}
upstream bili_syncplay_admin {
server 127.0.0.1:8788;
}
server {
listen 80;
server_name sync.example.com;
location ^~ /admin {
proxy_pass http://bili_syncplay_admin;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location ^~ /api/admin/ {
limit_req zone=admin_req_per_ip burst=20 nodelay;
proxy_pass http://bili_syncplay_admin;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
limit_conn conn_per_ip 10;
limit_req zone=req_per_ip burst=10 nodelay;
proxy_pass http://bili_syncplay_ws;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600;
}
}
在这个拓扑里:
- 普通用户只连接
wss://sync.example.com - 入口层负责把新建 WebSocket 连接分发到某个 Room Node
- 现有长连接一旦建立,就固定驻留在被选中的节点上
- 全局管理面建议继续收敛到独立的
global-admin进程 - 当所有 Redis 共享能力都已开启时,正确性上不再依赖 sticky 路由;但上线初期仍可保留 sticky 作为运维兜底开关
启用站点并校验配置:
sudo ln -s /etc/nginx/sites-available/bili-syncplay.conf /etc/nginx/sites-enabled/bili-syncplay.conf
sudo nginx -t
sudo systemctl reload nginx
生产环境中的扩展 WebSocket 服务应使用 wss://。常见做法是将 Certbot 与 Nginx 配合使用:
sudo certbot --nginx -d sync.example.com
证书签发后,验证:
curl https://sync.example.com/
此时扩展应使用:
wss://sync.example.com
扩展支持在弹窗中切换服务器地址,因此在生产环境中你可以将客户端指向:
wss://sync.example.com
本地测试时,切回:
ws://localhost:8787
房间邀请现在以 roomCode:joinToken 的形式分享。弹窗复制操作会复制这个邀请串,加入输入框也接受同样格式。
当你更新服务器代码时,先在应用目录里拉取并重新构建:
cd /opt/bili-syncplay
git pull
npm install
npm run build
如果你确认只有 server/ 发生变化,且 packages/protocol 没有变化,也可以只构建服务端:
npm run build -w @bili-syncplay/server
单机 / 单进程部署重启方式:
sudo systemctl restart bili-syncplay
多节点部署重启方式:
sudo systemctl restart bili-syncplay-room-node-a
sudo systemctl restart bili-syncplay-room-node-b
sudo systemctl restart bili-syncplay-global-admin
如果有多台 Room Node,建议滚动重启,而不是一次性全部重启:
- 先重启一个 Room Node
- 观察
GET /readyz、日志和全局管理面是否恢复正常 - 再继续重启下一个 Room Node
- 最后重启
global-admin
- 当
ROOM_STORE_PROVIDER=memory时,进程重启后房间仍会全部丢失。 - 当
ROOM_STORE_PROVIDER=redis时,房间基础状态会在重启后保留,直到过期或被删除。 - 最后一名成员离开后,房间不会立刻删除;服务端会写入
expiresAt,并在EMPTY_ROOM_TTL_MS到期后清理。 - 加入房间需要同时提供
roomCode和joinToken;发送房间消息需要有效的memberToken。 memberToken是会话态,不会从持久层恢复;重连或重启后都需要重新加入并重新签发。- 握手阶段的 Origin 检查默认拒绝,除非你在开发环境中显式允许缺失
Origin。 - 只有当 socket 对端命中
TRUSTED_PROXY_ADDRESSES时才会读取X-Forwarded-For。 - 健康检查同时提供
GET /与GET /healthz;就绪检查为GET /readyz。 - 如果你使用云防火墙,请放行入站
80和443,并将8787仅暴露给 localhost。 - 如果你不想使用 Nginx,也可以直接暴露 Node 服务,但浏览器和扩展仍应通过带有效 TLS 证书的
wss://连接。 - 当 Redis 相关 provider 全部开启后,房间基础状态、管理员会话、运行时索引、房间状态广播与管理命令路由都可在多个服务实例之间共享。
- 生产环境推荐把
/admin与/api/admin/*收敛到独立 Global Admin 进程。 - Room Node 可以设置
GLOBAL_ADMIN_ENABLED=false,只保留 WebSocket 流量与/、/healthz、/readyz。 - 当所有 Redis 共享能力都已开启时,多实例部署不再依赖 sticky 路由来保证房间状态正确性。
常见的开发侧失败场景:
无法连接到同步服务器。:扩展无法访问配置的服务器地址,或由该地址推导出的 HTTP 健康检查失败。- 服务端日志反复出现
origin_not_allowed:ALLOWED_ORIGINS没有包含当前chrome-extension://<extension-id> 房间不存在。:请求的房间号在当前服务器实例上不存在。- 服务重启后如果看到
房间不存在。,也可能表示该房间已经超过空房保留期并被清理。 加入码无效。:邀请串错误、已失效,或来自其他房间。成员令牌无效。:当前会话丢失了房间绑定、服务端已经重启,或客户端需要重新加入以获取新 token。请求过于频繁。:某个房间操作或同步消息触发了配置的限流。- 握手阶段返回
403:请求的Origin不在ALLOWED_ORIGINS中,或者在ALLOW_MISSING_ORIGIN_IN_DEV关闭时缺少Origin。 - 连接级 IP 限制看起来未生效:检查反向代理的 socket IP 是否已加入
TRUSTED_PROXY_ADDRESSES;默认情况下服务器只使用真实 socket 地址。 请先打开一个哔哩哔哩视频页面。:当前活动标签页 URL 不匹配扩展内容脚本的目标页面。当前页面没有可播放的视频。:内容脚本已加载,但页面没有暴露可用的视频载荷。无法访问当前页面。:Chrome 无法把消息传给内容脚本,通常是因为加载未打包扩展后没有刷新页面,或当前标签页 URL 不受支持。
常用检查:
# 服务器健康检查
curl http://127.0.0.1:8787/
# 服务器测试
npm run test -w @bili-syncplay/server
# Redis 集成回归
REDIS_URL=redis://127.0.0.1:6379 npm run test:redis -w @bili-syncplay/server
# 完整多节点回归
REDIS_URL=redis://127.0.0.1:6379 npx tsx --test server/test/multi-node-*.test.ts
# 协议测试
npm run test -w @bili-syncplay/protocol
# 扩展测试
npm run test -w @bili-syncplay/extension
Chrome 侧调试建议:
- 在
chrome://extensions查看扩展 service worker 日志 - 从
chrome://extensions复制未打包扩展 ID,并加入ALLOWED_ORIGINS - 重新构建
extension/dist后,重新加载未打包扩展 - 扩展重新加载后,刷新已打开的 Bilibili 标签页,以便重新注入内容脚本
先更新 workspace 版本:
npm run release:version -- 0.9.0
该命令会更新:
- 根目录
package.json packages/protocol/package.jsonserver/package.jsonextension/package.jsonpackage-lock.json
构建扩展发布 zip:
npm run build:release
输出:
release/bili-syncplay-extension-v<version>.zip
仓库已经包含一个 GitHub Actions 工作流,用于:
- 在
v*标签上触发 - 构建扩展
- 创建 GitHub Release
- 上传 zip 产物
示例:
npm run release:version -- 0.9.0
git push origin main
git tag v0.9.0
git push origin v0.9.0
QQ 群:1031990108
数据统计
数据评估
关于Bili SyncPlay特别声明
本站莉可POI提供的Bili SyncPlay都来源于网络,不保证外部链接的准确性和完整性,同时,对于该外部链接的指向,不由莉可POI实际控制,在2026年5月31日 04:22收录时,该网页上的内容,都属于合规合法,后期网页的内容如出现违规,可以直接联系网站管理员进行删除,莉可POI不承担任何责任。
相关导航
没有相关内容!
暂无评论...

