必须广播纯状态对象而非播放器实例,因broadcastchannel无法克隆媒体元素;需校验可见性、就绪状态并防消息乱序,关闭频道与轮询兜底缺一不可。

不能靠 BroadcastChannel 直接同步播放器实例或调用 play(),必须广播纯状态对象,再由各窗口主动更新本地 HTMLMediaElement —— 否则会报 Failed to execute 'postMessage' on 'BroadcastChannel': An object could not be cloned. 错误。
为什么 currentTime / muted 不能直接赋值就完事
浏览器对媒体操作有严格策略:后台标签页无法自动播放、currentTime 赋值可能触发 InvalidStateError(元数据未加载)、muted 变更不一定立即生效(尤其在 iOS Safari 中需配合用户手势)。单纯发个 { currentTime: 123.45, muted: true } 过去,不等于对方能立刻安全执行。
- 收到消息后必须先检查
document.visibilityState === 'visible',否则跳过play()和currentTime设置 - 设置
currentTime前加try/catch,捕获InvalidStateError和NotAllowedError - 用
player.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA判断是否可 seek -
muted可直接赋值,但需同步更新 UI(如静音按钮图标),避免视觉错位
消息结构必须带 timestamp 和 sourceTabId
多窗口快速拖动进度条时,容易出现新消息晚于旧消息到达(网络/调度抖动),导致播放器被“倒退”到旧时间点。仅靠 type 和 currentTime 不足以防重放或乱序。
- 每条消息必须包含
timestamp: Date.now()和唯一sourceTabId: Math.random().toString(36).substr(2, 9) - 接收端维护一个
lastProcessedTimestamp,忽略event.data.timestamp 的消息 - 频道名固定为
'media-playback-sync',大小写敏感,所有页面必须一致 - 不要在消息里传
player.duration或player.buffered—— 它们不可靠且不可克隆,需要各窗口自行读取
如何安全触发 play() 和处理自动播放拦截
即使当前页面可见,player.play() 仍可能被浏览器拒绝,尤其是没有用户交互上下文时。不能假设 paused: false 就一定能播起来。
- 收到
{ paused: false }时,只调用player.play().catch(e => console.warn('auto-play blocked', e)) - 不要在
message回调里直接player.currentTime = x后立刻play()—— 先等canplay或loadeddata事件 - 对静音状态,可同步设
player.muted = true+player.volume = 0双保险(部分安卓 WebView 对单设muted响应滞后) - 若使用
hls.js或Video.js,需先调用其 API 获取当前currentTime和muted,再组装对象发送,不能直接读 DOM 属性
关闭频道和兜底轮询不能省
页面卸载时不 close() 会导致内存泄漏;后台标签页收不到消息时,仅靠广播会丢状态。这两点在音视频场景下尤为明显 —— 用户切走再切回,进度可能已偏移。
- 在
beforeunload或 VueonBeforeUnmount/ReactuseEffect cleanup中调用channel.close() - 当
document.visibilityState === 'visible'时,每 30 秒轮询一次 localStorage 是否有新lastSyncTime,若有则主动拉取最新状态(作为广播丢失的补偿) - 轮询逻辑必须加防抖,避免多个标签页同时触发请求
- 不要复用未清理的
BroadcastChannel实例 —— 每个页面只创建一个,挂到window.__mediaSyncChannel等全局变量便于统一管理










