这是 NeriPlayer 一起听功能当前使用的 Cloudflare Workers 服务端实现,
以 np-submodule/NeriPlayer-LTW 的形式随主仓库一起维护。
- 主项目仓库:https://github.com/cwuom/NeriPlayer
- 运行时:Cloudflare Workers + Durable Objects + WebSocket hibernation
- 创建房间 / 加入房间,并直接返回可用的
wsUrl - 控制者 / 听众双角色与 HMAC Token 鉴权
- 邀请链接必须携带首次加入所需的
joinSecret;已有成员重连使用自己的memberSecret,这些密钥不会写入脱敏房间状态 - WebSocket 实时同步播放状态、队列、切歌、循环模式、随机播放和房间设置
- 听众可发起控制请求,由 Worker 校验房间策略、房主在线状态和目标歌曲后直接提交
- 可选共享房主解析出的多个播放候选,减少听众端重复解析压力;Worker 会持久缓存 房主当前曲目最多三条去重后的 HTTP(S) 候选,缓存命中时听众无需等待房主在线, 且只会公开当前曲目的候选。听众始终先按本机音质策略解析,候选只作为本次会话的 失败回退,不写入歌曲或离线缓存
- 本地歌曲不能创建房间或进入同步事件;关闭
shareAudioLinks后会立刻清空 房间里已缓存的候选 - 房间状态持久化、控制者离线检测与自动关房
- 开启自动暂停时,新成员加入和成员通过“离开房间”接口显式退出都会暂停房间并广播权威状态;同一成员使用
memberSecret或 Token 重连不会触发暂停,普通 WebSocket 断线仍保留成员以支持重连 - 控制事件可携带
clientInstanceId、clientSequence、clientTimeMs,Worker 会 过滤过期顺序;WebSocket 支持np_ping/np_pong返回服务端时钟并刷新房主存活时间 - 支持
Deploy to Cloudflare一键部署或本地源码手动部署
- 自己部署一个轻量的一起听同步服务
- 给 NeriPlayer Android 客户端配置自定义服务端地址
- 本地调试一起听协议、房间状态和 WebSocket 消息
它不是媒体代理或公共曲库服务。音频播放能力仍来自 NeriPlayer 客户端本身, Worker 只负责房间状态、权限、队列、同步事件和当前曲目的会话候选。
.
├─ src/
│ └─ worker.js # Worker 入口 + ListeningRoomDO
├─ .dev.vars.example # 本地开发示例密钥
├─ package.json
├─ README.md
└─ wrangler.toml # Durable Object 绑定与迁移配置
POST /api/rooms:创建房间POST /api/rooms/:roomId/join:加入房间GET /api/rooms/:roomId/state:获取房间快照POST /api/rooms/:roomId/leave:使用成员 Bearer Token 显式离开房间并广播成员离开POST /api/rooms/:roomId/control:通过Authorization: Bearer <token>提交控制事件GET /api/rooms/:roomId/ws?token=...:建立 WebSocketGET /healthz:健康检查
典型流程:
[创建/加入房间] → [HTTP 返回 wsUrl + token]
↓
[WebSocket 连接] → [welcome 消息携带 state 快照]
↓
[控制事件/听众请求] → [Durable Object 校验与落盘] → [广播 room_state_updated]
PLAYPAUSESEEKPLAYBACK_MODESET_TRACKSET_QUEUEHEARTBEATTRACK_FINISHED
REQUEST_PLAYREQUEST_PAUSEREQUEST_SEEKREQUEST_PLAYBACK_MODEREQUEST_SET_TRACKREQUEST_SET_QUEUE
当 allowMemberControl=true 且控制者在线时,Worker 会直接仲裁并提交这些请求,
不会再等待控制者客户端弹出确认。REQUEST_PLAY、REQUEST_PAUSE、
REQUEST_SEEK 和 REQUEST_PLAYBACK_MODE 必须携带能匹配当前歌曲的
requestTrackStableKey(也可从事件里的 track 或 queue[currentIndex] 推导),
避免延迟请求误操作已经切换的歌曲。REQUEST_PLAYBACK_MODE 携带队列时,只接受
歌曲多重集合不变且 currentIndex 仍指向当前歌曲的顺序,并原样提交请求端的随机或
恢复顺序;增删歌曲或切换当前歌曲会被拒绝。旧客户端没有携带队列时,开启随机仍使用
服务端兼容乱序,关闭随机则保留当前顺序;携带未重排旧队列的客户端也会走同一兼容乱序。
REQUEST_SET_TRACK 只能选择服务端当前队列
中已有的曲目,成员携带的队列不会写入房态。REQUEST_SET_QUEUE 每次只允许一种
受校验的队列变更:保留当前曲目的重排、单曲插入、单曲移除,或移除当前曲目后按
移除位置选择下一首(末尾则选择前一首);单曲队列还可被清空。它不会允许一次替换
多首曲目,并会保留未受影响时的播放状态和进度。控制事件排序优先使用同一用户、同一
客户端实例的有效 clientSequence,只丢弃小于或等于已记录序号的事件。无法使用序号时,
才按用户比较 clientTimeMs 并丢弃时间严格更早的事件。
协议版本 2 的 Android 客户端会在 schemaVersion=2 的房间中发送带
queueMutation.baseRoomVersion 的移动、插入、移除或紧凑重排操作。Worker 会把操作
重放到收到事件时的最新权威队列,保留已经到达的远端增删,并按稳定键重算当前曲目索引;
操作基线过期不会直接覆盖房态。随机开启时的重排和歌曲结束切歌也使用同一操作路径,
关闭随机始终恢复服务端维护的顺序快照。播放、暂停、跳转和心跳事件不再用旧队列快照
覆盖房态;旧客户端仍可使用完整快照兼容路径。
REQUEST_LINKLINK_READYLINK_UNAVAILABLEUPDATE_SETTINGS
GET /api/rooms/:roomId/state 响应在顶层返回 expectedPositionMs 和 serverNowMs;
WebSocket 的 room_state_updated 广播在顶层返回 expectedPositionMs 和 nowMs。
这些是消息层字段,不属于 state 房间快照。
expectedPositionMs 由服务端根据当前播放状态推算:
播放中时按 basePositionMs + elapsed * playbackRate 前进,暂停时保持基础位置。
当 repeatMode=1 且当前曲目时长有效时,位置会按曲目时长取模;其他模式保持
非负的推算值。客户端应使用该字段配合对应消息的服务端时间校正本地进度,不要把候选播放地址
缓存当成普通歌曲缓存。
- 房间号固定为 6 位,使用大写字母和数字的可读字符集
nickname长度为 1-24,当前允许中文、英文字母和数字- 每个房间对应一个
ListeningRoomDO - Durable Object storage 持久化房间快照与成员状态
- 新成员必须提供正确的
joinSecret才能加入;已有成员只能用自己的memberSecret或有效 Token 重连 - 创建快照和后续控制事件均拒绝本地歌曲;旧房态中的本地歌曲会在加载时剔除
allowMemberControl、autoPauseOnMemberChange、shareAudioLinks三个房间设置都可由控制者通过UPDATE_SETTINGS更新playback.repeatMode只接受0(关闭)、1(单曲循环)、2(列表循环),playback.shuffleEnabled为布尔值;旧客户端缺少这些字段时,Android 客户端 仍按可选字段兼容- 当
shareAudioLinks=false时,HTTPstate快照,以及 WebSocketwelcome/room_state_updated消息里的state,都会把track.streamUrl、track.streamUrls与队列对应字段清空 UPDATE_SETTINGS关闭shareAudioLinks后,会立即清空房间里已缓存的候选- 重新开启
shareAudioLinks后,房主客户端会立即重新上传当前歌曲的候选播放地址;房态只会 公开当前曲目的缓存候选,历史缓存不会泄漏到队列中的其他歌曲 REQUEST_LINK在shareAudioLinks=false时会直接失败,返回audio link sharing disabled- 命中当前曲目缓存的
REQUEST_LINK会直接广播权威房态;forceRefresh=true会绕过 缓存并向在线房主请求刷新,用于候选失效后的恢复 - 房主确认当前曲目只能得到试听片段时会发送
LINK_UNAVAILABLE,Worker 只清除该当前 曲目的候选缓存并广播最新房态,避免旧试听地址继续下发给听众 - 响应或广播顶层的
expectedPositionMs是服务端推算的位置,播放中会随playbackRate前进;单曲循环按当前曲目durationMs回绕。提交循环或随机模式前 会先按旧播放语义重新锚定位置,避免新一轮已经开始时仍停留在上一轮曲末 - Token 有效期为 24 小时,由
LISTEN_TOGETHER_TOKEN_SECRET参与 HMAC 签名 /state与/control都需要有效的Authorization: Bearer <token>;WebSocket 连接使用返回的wsUrl中的 token- 队列上限为 2000 首,避免房间状态无限膨胀
- 控制者心跳超过 45 秒未更新后房间会进入挂起状态;控制者在 10 分钟 宽限期内重连可恢复房间,超时后房间自动关闭并清理 Durable Object 存储
- Worker 不存储平台 Cookie、账号密码或 GitHub/WebDAV 凭据
- HTTP/WS Token 只用于房间控制鉴权,不代表第三方音乐平台授权
GET /api/rooms/:roomId/state需要房间成员的 Bearer Token;健康检查/healthz仍保持公开shareAudioLinks开启时会同步房主解析出的播放候选,请只在可信房间使用; 这些最多三条候选只用于当前会话回退,不是 NeriPlayer 的歌曲或离线缓存- 自建实例的日志、访问控制和域名策略由部署者自己负责
- Node.js
>= 22.0.0 - Cloudflare 账号
- Wrangler 4
下面的按钮会从公开模板仓库拉起 Cloudflare 部署流程 如果你直接使用当前子模块源码,请走后面的手动部署
npm ci
npm run check
npx wrangler devnpm run check 会依次校验 Node.js 版本,对 src/worker.js、src/stream-url-cache.js、
src/queue-order.js、src/queue-mutation.js 和 src/playback-position.js 执行 node --check,
运行 test/*.test.mjs 的缓存、队列、位置和协议测试,再执行 wrangler deploy --dry-run。
它不替代真实 Cloudflare 环境中的 create/join/WebSocket 流程验证。
本地开发可复制 .dev.vars.example 为 .dev.vars,并填入
LISTEN_TOGETHER_TOKEN_SECRET。建议使用下面的命令生成密钥:
openssl rand -hex 32npx wrangler loginnpx wrangler secret put LISTEN_TOGETHER_TOKEN_SECRETnpm ci
npm run check
npm run deploy部署完成后,Wrangler 会输出对应的 *.workers.dev 地址。
- 打开 NeriPlayer 设置页
- 进入一起听服务端配置
- 填入你的 Workers 地址,例如
https://example.workers.dev - 点击可用性测试
- 通过创建房间或加入邀请链接验证 WebSocket 同步