应使用Shared Worker作为全局音频播放仲裁者,仅允许一个页面发声,其余静音或暂停;Worker需统一路径创建,维护activeTabId和isPlaying状态,所有播放请求经Worker中转并广播指令同步各页面状态。

要防止多个标签页同时播放音频造成声音重叠,核心不是“禁止其他页面播放”,而是让 Shared Worker 充当一个全局播放仲裁者:只允许一个页面发声,其余页面静音或暂停,且状态实时同步。
Shared Worker 必须是同一实例
所有页面必须用完全一致的路径创建实例,比如:new SharedWorker('/workers/audio-controller.js')
协议、域名、端口、路径缺一不可。否则浏览器会启动多个独立 Worker,协调就失效了。
Worker 端维护两个关键状态
-
activeTabId: 当前被授权播放的页面唯一标识(如crypto.randomUUID()生成) -
isPlaying: 布尔值,表示当前是否有音频正在播放(注意:它反映的是“实际发声”状态,不是“调用了 play()”)
这两个变量在 Worker 线程内直接声明即可共享,无需额外同步机制。
播放请求必须走 Worker 中转
页面不能自己调 audio.play(),而应发送消息:
worker.port.postMessage({ type: 'REQUEST_PLAY', tabId: myTabId });Worker 收到后判断:
- 若
!isPlaying,则设activeTabId = myTabId,isPlaying = true,并广播{ type: 'GRANT_PLAY', tabId: myTabId } - 若已有活跃播放,则向原
activeTabId发{ type: 'PAUSE' },再向当前页发{ type: 'GRANT_PLAY', tabId: myTabId },同时更新activeTabId
页面端需响应三类指令
- 收到
'GRANT_PLAY':调用audio.play(),并确保audio.muted = false - 收到
'PAUSE':执行audio.pause(),可选设audio.muted = true防止残留音频 - 收到
'RELEASE'(如页面关闭或切后台):主动清空自身状态,并通知 Worker 释放权限
补充健壮性细节
- 页面监听
visibilitychange,切到后台时发RELEASE,切回前台可尝试重新申请(避免锁死) - Worker 中为每个 port 绑定
port.onclose,自动清理对应 tabId,防止僵尸连接占用状态 - 避免依赖
audio.paused判断播放状态——它可能因策略失败返回true却仍在缓冲;以 Worker 的isPlaying为准
本质上,这不是“谁先谁赢”的抢占,而是“谁被授权谁播”的委托机制。只要 Worker 在线,任意页面的操作都会被统一裁决,声音自然不会重叠。


















